PostgreSQL Antipatterns: Индиана Джонс и максимальное значение ключа, или В поисках «последних» записей

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Сегодняшняя задача вполне традиционна для любых учетных систем - поиск записей, содержащих максимальное значение по каждому из ключей. Что-то вроде "покажи мне последний заказ по каждому из клиентов", если переводить в прикладную область.

Кажется, что тут и споткнуться-то негде в реализации - но все оказывается совсем не тривиально.

Постараемся не запутаться
Постараемся не запутаться
КДПВ

Иллюстрации взяты отсюда.

Для определенности пробовать будем на PostgreSQL 13 и начнем с генерации тестовых данных о наших "заказах":

CREATE TABLE orders AS
SELECT
  (random() * 1e3)::integer client_id        -- один из 1K клиентов
, now()::date - (random() * 1e3)::integer dt -- дата заказа
, (random() * 1e3)::integer order_data       -- извлекаемые данные по заказу
FROM
  generate_series(1, 1e5); -- 100K произвольных

Как опытные разработчики мы уже понимаем, что "последний заказ по клиенту" подразумевает индекс по (client_id, dt):

CREATE INDEX ON orders(client_id, dt);
Вот с этим индексом все как полетит!..
Вот с этим индексом все как полетит!..

max + JOIN = 2 x Seq Scan

Сначала на ум приходит самый "тупой" вариант - найти эти самые "последние" значения по каждому из ключей, а потом извлечь соответствующую запись:

SELECT
  *
FROM
  (
    SELECT
      client_id
    , max(dt) dt
    FROM
      orders
    GROUP BY
      client_id
  ) T
JOIN
  orders
    USING(client_id, dt);

Это дает вот такой план на 47мс/1082 buffers, при котором таблица orders полностью перечитывается дважды:

Двойной Seq Scan
Двойной Seq Scan
"Два раза, Карл, два раза!"... хотя, это не отсюда
"Два раза, Карл, два раза!"... хотя, это не отсюда

DISTINCT ON

Но вроде достаточно очевидно, что читать всю таблицу дважды - не очень хорошо. Поскольку нам необходима всего лишь одна запись для каждого client_id, воспользуемся возможностями DISTINCT ON:

SELECT DISTINCT ON(client_id)
  *
FROM
  orders
ORDER BY
  client_id, dt DESC;
Залезли в temp buffers
Залезли в temp buffers

Незадача... стало вдвое медленнее из-за попадания сортировки на диск.

Путь оптимизаций тернист и опасен
Путь оптимизаций тернист и опасен

Попробуем исправить, увеличив объем выделяемой для узла памяти work_mem, как советует нам подсказка на explain.tensor.ru:

SET work_mem = '8MB';
Теперь work_mem хватило для Sort Memory
Теперь work_mem хватило для Sort Memory
Забрезжил свет в конце туннеля
Забрезжил свет в конце туннеля

Помни о сортировке в индексе!

Но обратите внимание: Sort Key: client_id, dt DESC - сортировка-то не совпадает с созданным нами индексом. А что если...

CREATE INDEX ON orders(client_id, dt DESC);
Используем подходящий индекс
Используем подходящий индекс

По времени сравнялись с исходным, но вот buffers теперь в 100 раз больше!

Немного подгорает
Немного подгорает

Рекурсивный "DISTINCT"

Поскольку мы знаем, что клиентов существенно меньше, чем заказов, да и в общем количестве достаточно немного, мы можем использовать модель поиска "следующих" значений client_id прямо из индекса:

WITH RECURSIVE rec AS (
  SELECT
    -1 client_id                  -- инициализируем наименьший ключ индекса
  , NULL::orders r
UNION ALL
  SELECT
    (X).client_id
  , X
  FROM
    rec
  , LATERAL (
      SELECT
        X                         -- возвращаем целую запись таблицы
      FROM
        orders X
      WHERE
        client_id > rec.client_id -- шагаем к "следующему" клиенту
      ORDER BY
        client_id, dt DESC        -- помним о правильной сортировке
      LIMIT 1                     -- нам нужна всего одна запись
    ) T
)
SELECT
  (r).*   -- разворачиваем поля записи в столбцы
FROM
  rec
OFFSET 1; -- пропускаем первую инициализационную запись
Рекурсивный DISTINCT
Рекурсивный DISTINCT

Запрос стал существенно сложнее, зато такой вариант уже выглядит гораздо интереснее в плане скорости: 9.5мс/3008 buffers - в 5 раз быстрее оригинального запроса!

Теперь-то мы - эксперты-оптимизаторы!
Теперь-то мы - эксперты-оптимизаторы!

Источник: https://habr.com/ru/company/tensor/blog/710400/


Интересные статьи

Интересные статьи

Сегодня будет рассказ про избыточные группировки и сортировки в SQL-запросах - как они возникают, по каким признакам их можно потом вычислить и как избавиться от них.
Время от времени приходится слышать мнение от некоторых системных администраторов, а также некоторых 1С-разработчиков, что установка, настройка и поддержка PostgreSQL под Linux очень сложна. Что гораз...
Как найти самые "горячие" запросы на вашем PostgreSQL-сервере? Поискать их в логе и проанализировать план или воспользоваться расширением pg_stat_statements.А если в лог попадает миллион запросов за с...
Установка и использование Managed Service for PostgreSQL и Managed Service for Kubernetes в Yandex Cloud c помощью terraform В этом посте будет описана установка Managed Service for PostgreSQL и Mana...
Мы начали с вопросов, связанных с изоляцией, сделали отступление про организацию данных на низком уровне, подробно поговорили о версиях строк и о том, как из версий получаются снимки данных. З...