Как взять подзапрос DISTINCT ON, упорядоченный по отдельному столбцу, и сделать его быстрым? - PullRequest
3 голосов
/ 17 марта 2019

(AKA - с запросом и данными, очень похожими на вопрос " Выбор строк, упорядоченных по одному столбцу и различающихся по другому ", как я могу заставить его работать быстро). Postgres 11.

У меня есть таблица prediction с (article_id, prediction_date, predicted_as, article_published_date), которая представляет выходные данные классификатора для набора статей.

Новые статьи часто добавляются в отдельную таблицу (представлена ​​FK article_id), а новые прогнозы добавляются по мере настройки нашего классификатора.

Пример данных:

| id      | article_id |  predicted_as | prediction_date | article_published_date
| 1009381 | 362718     |  negative     | 2018-07-27      | 2018-06-26
| 1009382 | 362718     |  positive     | 2018-08-12      | 2018-06-26
| 1009383 | 362719     |  positive     | 2018-08-13      | 2010-09-22
| 1009384 | 362719     |  positive     | 2018-09-28      | 2010-09-22
| 1009385 | 362719     |  negative     | 2018-10-01      | 2010-09-22

Сценарий создания таблицы:

create table prediction
(
    id serial not null
        constraint prediction_pkey
            primary key,
    article_id integer not null
        constraint prediction_article_id_fkey
            references article,
    predicted_as classifiedas not null,
    prediction_date date not null,
    article_published_date date not null
);

create index prediction_article_id_prediction_date_idx
    on prediction (article_id asc, prediction_date desc);

Мы часто хотим просмотреть самую последнюю классификацию для каждой статьи. Для этого мы используем:

SELECT DISTINCT ON (article_id) article_id, id, article_published_date
FROM prediction
ORDER BY article_id, prediction_date desc

, который возвращает что-то вроде:

| id     | article_id |  predicted_as | prediction_date | article_published_date
| 120950 | 1          | negative      | 2018-06-29      | 2018-03-25
| 120951 | 2          | negative      | 2018-06-29      | 2018-03-19

При индексе (article_id, prediciton_date desc) этот запрос выполняется очень быстро (~ 15 мс). Это план объяснения:

Unique  (cost=0.56..775374.53 rows=1058394 width=20)
  ->  Index Scan using prediction_article_id_prediction_date_id_idx on prediction  (cost=0.56..756071.98 rows=7721023 width=20)

Пока все хорошо.

Проблема возникает, когда я хочу отсортировать этот результат по элементу article_published_field. Например:

explain (analyze, buffers)
select *
  from (
         select distinct on (article_id) article_id, id, article_published_date
         from prediction
         order by article_id, prediction_date desc
       ) most_recent_predictions
  order by article_published_date desc
  limit 3;

Это работает, но выполнение запроса занимает ~ 3-4 секунды, что делает его слишком медленным для непосредственного ответа на веб-запрос.

Вот план объяснения:

Limit  (cost=558262.52..558262.53 rows=3 width=12) (actual time=4748.977..4748.979 rows=3 loops=1)
  Buffers: shared hit=7621849 read=9051
  ->  Sort  (cost=558262.52..560851.50 rows=1035593 width=12) (actual time=4748.975..4748.976 rows=3 loops=1)
        Sort Key: most_recent_predictions.article_published_date DESC
        Sort Method: top-N heapsort  Memory: 25kB
        Buffers: shared hit=7621849 read=9051
        ->  Subquery Scan on most_recent_predictions  (cost=0.43..544877.67 rows=1035593 width=12) (actual time=0.092..4508.464 rows=1670807 loops=1)
              Buffers: shared hit=7621849 read=9051
              ->  Result  (cost=0.43..534521.74 rows=1035593 width=16) (actual time=0.092..4312.916 rows=1670807 loops=1)
                    Buffers: shared hit=7621849 read=9051
                    ->  Unique  (cost=0.43..534521.74 rows=1035593 width=16) (actual time=0.090..4056.644 rows=1670807 loops=1)
                          Buffers: shared hit=7621849 read=9051
                          ->  Index Scan using prediction_article_id_prediction_date_idx on prediction  (cost=0.43..515295.09 rows=7690662 width=16) (actual time=0.089..3248.250 rows=7690662 loops=1)
                                Buffers: shared hit=7621849 read=9051
Planning Time: 0.130 ms
Execution Time: 4749.007 ms

Можно ли как-нибудь ускорить выполнение этого запроса, или мне придется прибегнуть к обновлению материализованного представления или настройке триггерной системы для быстрого получения этих данных?

Для справки:

  • таблица prediction имеет 7,7M строк
  • в таблице prediction
  • есть индекс на (article_id, prediciton_date desc) и индекс на article_published_date desc
  • VACUUM ANALYSE запущено

Ответы [ 3 ]

1 голос
/ 17 марта 2019

Интересно, сможете ли вы сделать эту работу:

select article_id, id, article_published_date
from prediction p
where p.prediction_date = (select max(p2.prediction_date)
                           from prediction p2
                           where p2.article_id = p.article_id
                          )
order by article_published_date desc;

Затем используйте эти два индекса:

  • (article_published_date desc, prediction_date, article_id, id)
  • (article_id, prediction_date desc).
1 голос
/ 17 марта 2019

Хотя вы просто хотите получить тривиально небольшое количество строк результатов (LIMIT 3 в вашем примере), и если есть какая-либо положительная корреляция между article_published_date и prediction_date, этот запрос должен быть радикально быстрее, так как нужно только отсканировать несколько кортежей с вершины добавленного индекса (и перепроверить с помощью второго индекса):

Имеют эти два индекса :

CREATE INDEX ON prediction (article_published_date DESC, prediction_date DESC, article_id DESC);

CREATE INDEX ON prediction (article_id, prediction_date DESC);

Рекурсивный запрос:

WITH RECURSIVE cte AS (
   (
   SELECT p.article_published_date, p.article_id, p.prediction_date, ARRAY[p.article_id] AS a_ids
   FROM   prediction p
   WHERE  NOT EXISTS (  -- no later row for same article
      SELECT FROM prediction
      WHERE  article_id = p.article_id
      AND    prediction_date > p.prediction_date
      )
   ORDER  BY p.article_published_date DESC, p.prediction_date DESC, p.article_id DESC
   LIMIT  1
   )
   UNION ALL
   SELECT p.article_published_date, p.article_id, p.prediction_date, a_ids || p.article_id
   FROM   cte c, LATERAL (
      SELECT p.article_published_date, p.article_id, p.prediction_date
      FROM   prediction p
      WHERE (p.article_published_date, p.prediction_date, p.article_id)
          < (c.article_published_date, c.prediction_date, c.article_id)
      AND    p.article_id <> ALL(a_ids)   -- different article
      AND    NOT EXISTS (                 -- no later row for same article
         SELECT FROM prediction
         WHERE  article_id = p.article_id
         AND    prediction_date > p.prediction_date
         )
      ORDER  BY p.article_published_date DESC, p.prediction_date DESC, p.article_id DESC
      LIMIT  1
      ) p
   )
SELECT article_published_date, article_id, prediction_date
FROM   cte
LIMIT  3;

Вот решение plpgsql , которое делает то же самое, возможно, немного быстрее:

CREATE OR REPLACE FUNCTION f_top_n_predictions(_n int = 3)
  RETURNS TABLE (_article_published_date date, _article_id int, _prediction_date date) AS
$func$
DECLARE
   a_ids int[];
BEGIN
   FOR _article_published_date, _article_id, _prediction_date IN
      SELECT article_published_date, article_id, prediction_date
      FROM   prediction
      ORDER  BY article_published_date DESC, prediction_date DESC, article_id DESC
   LOOP
      IF _article_id = ANY(a_ids)
      OR EXISTS (SELECT FROM prediction p
                 WHERE  p.article_id = _article_id
                 AND    p.prediction_date > _prediction_date) THEN
         -- do nothing         
      ELSE
         RETURN NEXT;
         a_ids := a_ids || _article_id;
         EXIT WHEN cardinality(a_ids) >= _n;
      END IF;
   END LOOP;
END
$func$  LANGUAGE plpgsql;

Звоните:

SELECT * FROM f_top_n_predictions();

Я добавлю объяснение, если оно работает для вас, поскольку объяснение - это больше работы, чем сам запрос.


Помимо этого, с более чем несколькими прогнозами на статью и с дополнительной таблицей article этот запрос становится претендентом:

SELECT p.*
FROM   article a
CROSS  JOIN LATERAL (
   SELECT p.article_published_date, p.article_id, p.prediction_date
   FROM   prediction p
   WHERE  p.article_id = a.id
   ORDER  BY p.prediction_date DESC
   LIMIT  1
   ) p
ORDER  BY p.article_published_date DESC;

Но вам это не нужно, если запрос выше выполняет свою работу. Становится интересно для большего или нет LIMIT.

Основа:

дБ <> скрипка здесь , демонстрируя все.

1 голос
/ 17 марта 2019

Одна вещь, которую вы можете попробовать , - это использовать оконную функцию ROW_NUMBER() OVER(...) вместо DISTINCT ON() (что подразумевает ограничения на предложение ORDER BY). Этот метод функционально эквивалентен вашему второму запросу, и может иметь возможность воспользоваться существующими индексами:

SELECT *
FROM (
    SELECT 
        article_id, 
        id, 
        article_published_date,
        ROW_NUMBER() OVER(PARTITION BY article_id ORDER BY prediction_date DESC) rn
    FROM prediction 
) x WHERE rn = 1
ORDER BY article_published_date DESC
LIMIT 3;

Демонстрация на DB Fiddle .

...