Скачать CV

Database

JSONB в PostgreSQL это не индекс листинга

GIN на payload выглядит полным. WHERE title->>'ru' всё равно идёт seq scan. Containment и extract это разные работы.

Листинг каталога в магазине, который переехал на PostgreSQL, всё ещё шёл seq scan. В магазинах, где уже был JSON в MySQL, каталог иногда переносили в PostgreSQL ради JSONB и GIN. Листинг по-прежнему фильтровал опубликованные строки и сортировал поле редактора. В WHERE стояло title->>'ru'. EXPLAIN показывал Seq Scan. GIN не использовался.

Редакторы ждали публичную сетку. PostgreSQL жег CPU на extract. Vue SSR печатал сорок плиток и платил чтением всей таблицы. GIN на JSONB собран под containment: payload @> '{"tag":"sale"}'. Оператор ->> даёт текст. Этот текст GIN не хранит, пока нет expression-индекса на extract.

JSONB оставляю документом админки. Публичный список его не читает.

GIN закрывает containment, не любой extract. Stored-колонка может жить на btree.
GIN закрывает containment, не любой extract. Stored-колонка может жить на btree.

Сгенерированный текст, btree для сетки

title_ru это stored generated-колонка из JSONB. SELECT листинга это id, slug, price, title_ru. Индекс (is_published, sort_order, id) покрывает эти поля. Сортировка на маленьком целом, его ставит редактор. Сортировка по title живёт в админке, не на публичной сетке.

Поиск, которому нужен JSON, держит GIN и использует @> или jsonb_path_exists. Смешать оба в одном запросе это seq scan и gin scan, который всё равно сортирует в work_mem.

Я пробовал expression-индекс на (title->>'ru'). Он помог фильтру одной локали. Листинг всё равно сортировал sort_order и всё равно нуждался в published. Два индекса, два плана, а третья локаль значит ещё два expression-индекса. Generated-колонки плюс один btree проще держать в голове. Записи чуть тяжелее. Сетка получила план, который я мог прочитать.

Листинг читает типизированные колонки. Поиск может взять GIN. Один запрос не должен делать оба плохо.
Листинг читает типизированные колонки. Поиск может взять GIN. Один запрос не должен делать оба плохо.

Что в JSONB не кладу

Цена, флаг публикации и порядок сортировки остаются типизированными колонками. Vue SSR получает одну строку локали. PHP не делает json_decode трёх языков, чтобы напечатать сорок плиток. MySQL и PostgreSQL с листинга выглядят одинаково: покрывающий btree, компактные строки, JSON вне горячего пути.

На этом сайте по-прежнему MySQL. PostgreSQL это другой каталог. Правило от движка не меняется. JSON это форма. Список это таблица.

После появления GIN я снял pg_stat_user_indexes. idx_scan у GIN на маршруте листинга оставался около нуля. idx_scan у btree пошёл. Это измерение закрыло спор, что JSONB уже проиндексирован.

Какой опыт из этого

На маршруте листинга читаю EXPLAIN и pg_stat_user_indexes, не факт наличия GIN. Неиспользуемый GIN плюс Seq Scan это обычная форма.

title->>'ru' я не верю, что он возьмёт GIN с payload. Containment это @>. Extract нужен generated-колонке или expression-индексу.

Правило, которое оставляю: типизированные колонки для цены, публикации, сортировки. Одна строка локали в Vue. JSONB остаётся документом админки. Сортировку листинга и JSON-поиск в одном запросе не смешивать.

К заметкам