Пакетному чтению нужен контракт результата

Замена цикла запросов по одному первичному ключу одним запросом ANY убирает сетевые обмены. Она также может изменить форму ответа. Вызывающая сторона может запросить [42, 7, 42, NULL, -1] и ожидать пять позиций результата. Семантика множеств SQL не обещает такого выравнивания.

Набор строк — не список ответов

При 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 целых строк

В подключённой таблице соответствующий явный вызов кэша выглядит так:

SELECT local_cache.mget(
  'public.items'::regclass,
  ARRAY[42, 7, 42, NULL, -1]::bigint[]
) AS rows;

Он возвращает text[], сохраняя порядок входных данных и дубликаты. Отсутствующие ключи и входные null дают выровненные элементы SQL NULL; каждый присутствующий элемент — сериализованная целая строка. При промахе или обходе кэша читается PostgreSQL. API принимает не более 1 024 ключей за вызов. Он не заменяет проекции, соединения, блокировки строк или произвольное кэширование результатов запросов.

Ограничивайте пакеты и наблюдайте за ними

Для более чем 1 024 ключей явно разделяйте запросы или сохраняйте обычный SQL- запрос. Разбиение на несколько операторов может увидеть разные снимки READ COMMITTED; выбирайте семантику транзакции осознанно. Большие пакеты также увеличивают размер ответа и работу декодирования клиента, поэтому одно лишь «меньше запросов» не доказывает, что запрос стал быстрее.

Для GraphQL пакетная функция DataLoader должна возвращать по одному ответу на каждый входной ключ в том же порядке. Мемоизация на уровне запроса и общий кэш PostgreSQL — отдельные слои; очищайте записи затронутого загрузчика после мутаций. См. полное руководство по пакетам и сравните задержку, полезную нагрузку и пропускную способность с запускателем бенчмарков.