Работали ли вы с JSON-полями или специфическими расширениями PostgreSQL
Что проверяют
Видите ли вы границу между реляционной схемой и схемой внутри колонки, и знаете ли, чем jsonb отличается от json. Плюс прикладное: как документ из базы ложится на типы Go и что происходит с индексами.
Как отвечать
Начните с типов. json хранит исходный текст как есть, jsonb хранит разобранное дерево: запись чуть дороже, зато доступны операторы и индексы, порядок ключей не сохраняется, дубли ключей схлопываются. Для хранения берут jsonb, обычный json оправдан, только если нужен побайтово исходный документ.
Операторы: -> достаёт jsonb, ->> текст, @> проверяет вхождение поддокумента. Индексы: GIN по всей колонке, а вариант jsonb_path_ops компактнее и быстрее, но рассчитан на проверку вхождения и не обслуживает поиск по наличию ключа. Если фильтруете по одному ключу, дешевле индекс по выражению:
create index on products ((attrs->>'sku'));
create index on products using gin (attrs jsonb_path_ops);Со стороны Go скажите, что json.RawMessage и []byte сканируются напрямую, pgx умеет класть структуру в jsonb сам, а для своего типа пишутся driver.Valuer и sql.Scanner. Из расширений упоминайте то, что реально трогали: pg_trgm для поиска по подстроке, citext, pgcrypto.
Где спотыкаются
Складывают в документ то, что явно реляционно. Через полгода по этим полям нужны соединения таблиц, внешние ключи и ограничения, а их нет. Рабочее правило: то, по чему фильтруют и джойнят, выносится отдельной колонкой, в jsonb остаётся редкая переменная часть.
Второе: строят GIN и пишут запрос через сравнение attrs->>'key' со строкой. Такой предикат индекс по документу не использует, нужен либо @>, либо отдельный индекс по выражению. Отсюда жалобы, что индекс есть, а план всё равно с полным сканом.
Третье: обновляют документ целиком, прочитав его в память сервиса. Параллельная запись при этом теряется, и вся широкая строка переписывается заново. Точечное изменение делается запросом через jsonb_set, и это заодно снимает гонку.