Запросить последние N связанных строк в строке

У меня есть следующий запрос, который извлекает id из последнего N observations для каждого station:

SELECT id
FROM (
  SELECT station_id, id, created_at,
         row_number() OVER(PARTITION BY station_id
                           ORDER BY created_at DESC) AS rn
  FROM (
      SELECT station_id, id, created_at
      FROM observations
  ) s
) s
WHERE rn <= #{n}
ORDER BY station_id, created_at DESC;

У меня есть индексы на id, station_id, created_at.

Это единственное решение, с которым я столкнулся, который может получить более одной записи на каждую станцию. Однако он довольно медленный (154,0 мс для таблицы из 81000 записей).

Как ускорить запрос?

Ответ 1

Предполагая текущую версию Postgres 9.3.

Индекс

Во-первых, индекс с несколькими столбцами поможет:

CREATE INDEX observations_special_idx
ON observations(station_id, created_at DESC, id)

created_at DESC немного лучше подходит, но индекс все равно будет сканироваться назад с почти той же скоростью без DESC.

Предполагая, что created_at определен NOT NULL, еще рассмотрите DESC NULLS LAST в индексе и запросе:

Последний столбец id полезен только в том случае, если вы получаете сканирование с индексом из этого, что, вероятно, не будет работать если вы добавляете много новых строк постоянно. В этом случае удалите id из индекса.

Упрощенный запрос (все еще медленный)

Упростите свой запрос, внутренний подзапрос не поможет:

SELECT id
FROM  (
  SELECT station_id, id, created_at
       , row_number() OVER (PARTITION BY station_id
                            ORDER BY created_at DESC) AS rn
  FROM   observations
  ) s
WHERE  rn <= #{n}
ORDER  BY station_id, created_at DESC;

Должно быть немного быстрее, но все же медленно.

Быстрый запрос

  • Предполагая, что у вас относительно несколько станций и относительно много наблюдений на станцию.
  • Также предполагается, что station_id id определяется как NOT NULL.

Чтобы быть действительно быстрым, вам нужен эквивалент свободного индекса сканирования (не реализованного в Postgres). Связанный ответ:

Если у вас есть отдельная таблица stations (что кажется вероятным), вы можете эмулировать ее с помощью JOIN LATERAL (Postgres 9.3 +):

SELECT o.id
FROM   stations s
JOIN   LATERAL (
   SELECT id, created_at
   FROM   observations
   WHERE  station_id = s.id  -- lateral reference
   ORDER  BY created_at DESC
   LIMIT  #{n}
   ) o ON TRUE
ORDER  BY s.id, o.created_at DESC;

Если у вас нет таблицы stations, следующая лучшая вещь - создать и поддерживать ее. Возможно, добавьте ссылку на внешний ключ, чтобы обеспечить реляционную целостность.

Если это не вариант, вы можете перетащить такую ​​таблицу на лету. Простые опции:

SELECT DISTINCT station_id FROM observations;
SELECT station_id FROM observations GROUP BY 1;

Но для этого потребуется последовательное сканирование и быть слишком медленным. Trick Postgres в использовании вышеуказанного индекса (или любого индекса btree с station_id в качестве ведущего столбца) с рекурсивным CTE:

WITH RECURSIVE stations AS (
   (                  -- extra pair of parentheses ...
   SELECT station_id
   FROM   observations
   ORDER  BY station_id
   LIMIT  1
   )                  -- ... is required!
   UNION ALL
   SELECT (SELECT station_id
           FROM   observations
           WHERE  station_id > s.station_id
           ORDER  BY station_id
           LIMIT  1)
   FROM   stations s
   WHERE  s.station_id IS NOT NULL  -- serves as break condition
   )
SELECT station_id
FROM   stations
WHERE  station_id IS NOT NULL;      -- remove dangling row with NULL

Используйте это как drop-in replacement для таблицы stations в приведенном выше простом запросе:

WITH RECURSIVE stations AS (
   (
   SELECT station_id
   FROM   observations
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT station_id
           FROM   observations
           WHERE  station_id > s.station_id
           ORDER  BY station_id
           LIMIT  1)
   FROM   stations s
   WHERE  s.station_id IS NOT NULL
   )
SELECT o.id
FROM   stations s
JOIN   LATERAL (
   SELECT id, created_at
   FROM   observations
   WHERE  station_id = s.station_id
   ORDER  BY created_at DESC
   LIMIT  #{n}
   ) o ON TRUE
WHERE  s.station_id IS NOT NULL
ORDER  BY s.station_id, o.created_at DESC;

Это должно быть быстрее, чем у вас порядков.

SQL Fiddle.

Ответ 2

Это хороший андерселлер, только если вам не требуется запрашивать текущие текущие данные.

Подготовка (требуется postgresql 9.3)

drop materialized view test;
create materialized view test as select * from (
  SELECT station_id, id, created_at,
      row_number() OVER(
          PARTITION BY station_id
          ORDER BY created_at DESC
      ) as rn
  FROM (
      SELECT
          station_id,
          id,
          created_at
      FROM observations
  ) s
 ) q WHERE q.rn <= 100 -- use a value that will be your max limit number for further queries
ORDER BY station_id, rn DESC ;


create index idx_test on test(station_id,rn,created_at);

Как запрашивать данные:

select * from test where rn<10 order by station_id,created_at;

Ваш исходный запрос был 281 мс на моей машине, и этот новый был 15 мс.

Как обновить представление со свежими данными:

refresh materialized view test;

У меня есть другое решение, которое не требует материализованного представления и работает с актуальными и актуальными данными. Но учитывая, что вам не нужны современные данные, это материализованное представление намного более эффективно.