Пакетному чтению нужен контракт результата
Замена цикла запросов по одному первичному ключу одним запросом ANY убирает
сетевые обмены. Она также может изменить форму ответа. Вызывающая сторона может
запросить [42, 7, 42, NULL, -1] и ожидать пять позиций результата. Семантика
множеств SQL не обещает такого выравнивания.
Примечание от 04.10.2026: В версии 3.0.0 удалён SQL API кэширования строк local_cache.mget(regclass, anyarray) из 2.x; используйте RESP MGET для чтения целых строк.
Набор строк — не список ответов
При WHERE id = ANY($1::bigint[]) дублирующийся ID обычно сопоставляется со
строкой таблицы один раз. Отсутствующий ID не даёт строки. Входной NULL не
сопоставляется с ненулевым первичным ключом, а порядок входных данных не
гарантируется. Добавление ORDER BY id сортирует по ключу, но всё равно не
восстанавливает запрошенные позиции.
Если потребителям нужен набор, это нормально. Если им нужен результат для каждого входного элемента, сделайте позиции частью запроса или восстановите их в приложении.
Явно сохраняйте позиции в SQL
Запустите локальную демонстрацию и выполните это в её
сеансе psql:
WITH requested AS (
SELECT key, position
FROM unnest(ARRAY[42, 7, 42, NULL, -1]::bigint[])
WITH ORDINALITY AS input(key, position)
)
SELECT requested.position,
requested.key,
CASE WHEN items.id IS NULL THEN NULL
ELSE row_to_json(items)::text END AS row
FROM requested
LEFT JOIN public.items AS items ON items.id = requested.key
ORDER BY requested.position;
Столбец ordinality различает оба вхождения 42. Левое соединение сохраняет все
пять позиций, включая null-вход и любой отсутствующий ключ. Для отсутствующего
ключа row равен SQL NULL. В коде приложения передавайте массив параметром, а
не добавляйте ID в SQL конкатенацией. Исходный код вспомогательной функции Node.js
показывает выравнивание результатов на стороне клиента.
Сравните API целых строк
Для чтения целых строк из подключённых таблиц используйте RESP MGET. Кодируйте каждый первичный ключ в ключе вида CRUD:
RESP MGET возвращает массив RESP2 в порядке запроса. Дубликаты сохраняют позиции, а для отсутствующей строки возвращается nil. Ключи RESP задают значения первичных ключей и не содержат позицию SQL NULL в массиве. Если для каждого элемента SQL-массива, включая NULL, нужен результат на соответствующей позиции, используйте показанный выше вариант с WITH ORDINALITY. Одна команда принимает не более 1 024 ключей, размер каждой JSON-строки ограничен 65 536 байтами, а закодированного ответа — 66 560 байтами. RESP MGET не заменяет проекции, соединения, блокировки строк или кэширование произвольных результатов запросов.
REDISCLI_AUTH=DemoRespToken_0123456789abcdef0123456789 redis-cli -2 -p 56379 MGET 'CRUD:pglc_demo.public.items:{"id":42}' \
'CRUD:pglc_demo.public.items:{"id":7}' \
'CRUD:pglc_demo.public.items:{"id":42}' \
'CRUD:pglc_demo.public.items:{"id":-1}'
Ограничивайте пакеты и наблюдайте за ними
Для более чем 1 024 ключей явно разделяйте запросы или сохраняйте обычный SQL-
запрос. Разбиение на несколько операторов может увидеть разные снимки
READ COMMITTED; выбирайте семантику транзакции осознанно. Большие пакеты также
увеличивают размер ответа и работу декодирования клиента, поэтому одно лишь
«меньше запросов» не доказывает, что запрос стал быстрее.
Для GraphQL пакетная функция DataLoader должна возвращать по одному ответу на каждый входной ключ в том же порядке. Мемоизация на уровне запроса и общий кэш PostgreSQL — отдельные слои; очищайте записи затронутого загрузчика после мутаций. См. полное руководство по пакетам и сравните задержку, полезную нагрузку и пропускную способность с запускателем бенчмарков.