Структура БД для записи партий Dice Chess
Контекст и масштаб
Мы проектируем базу данных для хранения и аналитики партий в Dice Chess. Основные вводные данные:
- База данных: PostgreSQL (отлично подходит для планируемых объемов, поддерживает массивы и быстрые хеш-индексы).
- Масштаб: Ожидается от нескольких миллионов до десятков миллионов партий (импорт 140k уже есть, планируется полная загрузка истории с сервера и генерация партий ботами).
- Железо: Локальный сервер ASUS (8Gb RAM, 128Gb SSD + 1 Tb HDD). При 10-20 миллионах партий БД может занять десятки гигабайт, что вполне комфортно поместится на SSD (или HDD для архива).
- Отсутствие промежуточных позиций: Нас интересует только позиция до броска кубиков и позиция в конце хода. Промежуточные микро-ходы (и позиции между ними) не требуют отдельной таблицы и индексов.
Zobrist Hashing vs xxHash64 (Сравнение подходов)
Ты просил объяснить разницу между этими двумя способами хеширования.
Zobrist Hashing
Это стандарт де-факто для шахматных движков (например, в твоем dicechess-engine-scala).
- Как работает: Для каждой фигуры на каждой клетке генерируется случайное 64-битное число. Также генерируются числа для прав рокировки, en passant и очереди хода. Хеш позиции — это просто побитовое ИСКЛЮЧАЮЩЕЕ ИЛИ (
XOR) всех этих чисел. - Главный плюс (Инкрементальность): Если мы двигаем коня с b1 на c3, нам не нужно пересчитывать хеш всей доски. Мы берем текущий хеш, делаем
XORчисла для “конь на b1” (убираем его) иXORчисла для “конь на c3” (ставим его). Это происходит за наносекунды. Идеально для дерева перебора ходов. - Минус для базы данных: Хеш жестко привязан к конкретному набору случайных чисел (таблице Zobrist). Если мы захотим перенести данные в другую систему, другой язык программирования или скрипт аналитики, нам придется везде таскать за собой этот точный массив из сотен случайных чисел. Если массив потеряется или изменится хоть один бит — все хеши в БД станут невалидными.
xxHash64 (или аналоги вроде MurmurHash)
Это современная, универсальная функция хеширования для строк и данных.
- Как работает: Берет готовую строку (в нашем случае нормализованный
FEN) и прогоняет через математический алгоритм, получая 64-битное число. - Плюсы для базы данных:
- Детерминированность: Строка
rnbqkbnr/pppppppp/8/... w KQkq -всегда и везде (в Scala, Python, Postgres, JS) даст один и тот же хеш. Нам не нужно хранить магические таблицы. - В Postgres есть встроенная поддержка эффективных хеш-индексов:
CREATE INDEX ... USING hash.
- Детерминированность: Строка
- Минус: Неинктрементально. Чтобы получить хеш новой позиции, нужно сначала собрать FEN-строку, а потом прогнать ее через алгоритм. Для БД это не проблема, так как мы записываем уже готовые ходы, а не перебираем миллионы вариантов в секунду.
Вывод: Zobrist идеален внутри движка во время поиска лучшего хода (Expectimax). Но когда партия уже сыграна и мы кладем ее в базу для долгосрочного хранения и поиска, лучше взять FEN-строку и построить по ней классический индекс (через xxhash64 или встроенный в Postgres hash), так как это надежнее с точки зрения долговременного хранения.
Обновленная схема БД
Мы избавляемся от таблицы micro_moves. Сами ходы (например, “b1c3, e2e4”) мы можем сохранить просто массивом строк прямо внутри таблицы turns. Это колоссально сэкономит место и ускорит запись/чтение.
erDiagram players ||--o{ games : "plays as white/black" games ||--o{ turns : "consists of" games ||--o{ game_events : "has events" positions ||--o{ turns : "referenced by" positions { bigint id PK varchar normalized_fen UK bigint fen_hash "INDEX" char active_color varchar castling varchar en_passant } games { uuid id PK varchar source uuid white_player_id FK uuid black_player_id FK varchar mode smallint result varchar termination bigint initial_position_id FK bigint final_position_id FK timestamptz started_at jsonb metadata } turns { bigint id PK uuid game_id FK smallint turn_number char active_color bigint position_id FK varchar dice_sorted varchar[] played_moves bigint position_after_id FK } game_events { bigint id PK uuid game_id FK smallint sequence_number varchar event_type char actor_color jsonb payload }
1. Таблица positions — дедуплицированный справочник позиций
Хранит только уникальные FEN-строки до кубиков.
CREATE TABLE positions (
id BIGSERIAL PRIMARY KEY,
-- Первые 4 поля DFEN БЕЗ halfmove clock и fullmove counter
normalized_fen VARCHAR(100) NOT NULL,
-- Хеш для сверхбыстрого точечного поиска
fen_hash BIGINT NOT NULL,
-- Для специфичных аналитических запросов
piece_placement VARCHAR(72) NOT NULL,
active_color CHAR(1) NOT NULL,
castling VARCHAR(4) NOT NULL DEFAULT '-',
en_passant VARCHAR(12) NOT NULL DEFAULT '-',
CONSTRAINT uq_normalized_fen UNIQUE (normalized_fen)
);
CREATE INDEX idx_positions_hash ON positions USING hash (fen_hash);
-- Опционально: индекс для поиска просто по расстановке фигур
CREATE INDEX idx_positions_placement ON positions (piece_placement);2. Таблица games — мастер-запись о партии
CREATE TABLE games (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source VARCHAR(20) NOT NULL, -- 'online', 'bot', 'imported'
white_player_id UUID REFERENCES players(id),
black_player_id UUID REFERENCES players(id),
mode VARCHAR(10) NOT NULL DEFAULT 'classic',
result SMALLINT, -- 1 = белые, -1 = чёрные, 0 = ничья
termination VARCHAR(20), -- 'king_lost', 'timeout', 'resignation', etc.
initial_position_id BIGINT REFERENCES positions(id),
final_position_id BIGINT REFERENCES positions(id),
total_turns SMALLINT,
started_at TIMESTAMPTZ,
metadata JSONB, -- Вся специфика (time limits, ratings, ext IDs)
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_games_started ON games (started_at);
CREATE INDEX idx_games_players ON games (white_player_id, black_player_id);3. Таблица turns — Оптимизированная история ходов
Каждая строка — это один полный ход игрока (позиция до, бросок, действия, позиция после).
CREATE TABLE turns (
id BIGSERIAL PRIMARY KEY,
game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE,
turn_number SMALLINT NOT NULL,
active_color CHAR(1) NOT NULL,
-- Позиция НА НАЧАЛО хода (до броска кубиков)
position_id BIGINT NOT NULL REFERENCES positions(id),
-- Отсортированные кубики для индексированного поиска ("BNP", "KQR", "-")
dice_sorted VARCHAR(3) NOT NULL,
-- Массив микро-ходов, сыгранных в этот ход (например: ARRAY['b1c3', 'e2e4'])
-- Пустой массив, если ход пропущен
played_moves VARCHAR(5)[],
-- Позиция ПОСЛЕ завершения всех микро-ходов
position_after_id BIGINT REFERENCES positions(id),
-- Опционально: время размышления
thinking_time_ms INT,
CONSTRAINT uq_game_turn UNIQUE (game_id, turn_number)
);
-- ★ КЛЮЧЕВОЙ ИНДЕКС: поиск по стартовой позиции хода
CREATE INDEX idx_turns_position ON turns (position_id);
-- ★ КЛЮЧЕВОЙ ИНДЕКС: поиск по стартовой позиции + кубикам
CREATE INDEX idx_turns_pos_dice ON turns (position_id, dice_sorted);4. Таблица game_events — тайминги, удвоения, ничьи
CREATE TABLE game_events (
id BIGSERIAL PRIMARY KEY,
game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE,
sequence_number SMALLINT NOT NULL,
turn_number SMALLINT,
event_type VARCHAR(20) NOT NULL, -- 'double_offer', 'resignation', etc.
actor_color CHAR(1),
clock_white_ms INT,
clock_black_ms INT,
payload JSONB,
CONSTRAINT uq_game_event UNIQUE (game_id, sequence_number)
);Оценка объёма данных (10 миллионов партий)
Убрав micro_moves и упаковав их в VARCHAR[] массив внутри turns, мы радикально сжимаем базу данных.
| Таблица | На 10,000,000 партий (Оценка) |
|---|---|
games | ~5 GB |
turns (по ~30 ходов на партию) | 300M строк, ~25 GB |
game_events | 30M строк, ~2 GB |
positions | Допустим, 50M уникальных позиций, ~6 GB |
| Индексы Postgres | ~15-20 GB |
| Итоговый размер БД | ~50 - 60 GB |
Это превосходно ляжет на 128Gb SSD локального сервера ASUS. Запросы по поиску конкретной позиции будут отрабатывать за миллисекунды за счет хеш-индекса в positions и B-tree индексов в turns.
User Review Required
Если эта архитектура (с удаленной таблицей micro_moves и использованием VARCHAR[] в turns) тебя полностью устраивает, мы можем считать фазу проектирования БД успешно завершенной.
Останется только в будущем написать скрипты, которые возьмут 140k исходных JSON файлов с сайта и аккуратно зальют их в этот PostgreSQL.