如何采用按单独列排序的 DISTINCT ON 子查询并使其快速?

Posted

技术标签:

【中文标题】如何采用按单独列排序的 DISTINCT ON 子查询并使其快速?【英文标题】:How do I take a DISTINCT ON subquery that is ordered by a separate column, and make it fast? 【发布时间】:2019-08-07 16:59:28 【问题描述】:

(AKA - 查询和数据与问题“Selecting rows ordered by some column and distinct on another”非常相似,我怎样才能让它快速运行)。 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 表有 770 万行 prediction 表中有 170 万个不同的 article_ids (article_id, prediciton_date desc) 上有一个索引,article_published_date desc 上也有一个索引 VACUUM ANALYSE已运行

【问题讨论】:

关于limit 3:是为了测试,还是你真的只想要前三名?您的解释与查询不同步。另外,EXPLAIN 很好,EXPLAIN (ANALYZE, BUFFERS) 更好地帮助我们理解。我假设还有一个表article 包含所有相关(独特)文章? (你提到了一个 FK ......) 您提供了很好的信息,比大多数人都好。尽管如此(如总是),实际的CREATE TABLE 脚本会更有帮助。对于初学者来说,是否定义了列很重要NOT NULL 嗨@ErwinBrandstetter - 我已经用(ANALYZE, BUFFERS 更新了explain 并添加了创建表脚本。 有一个单独的文章表是通过FK访问的,但是这个查询实际上并没有访问它。桌上的article_id就够了。 最重要的问题是我的第一个问题,关于LIMIT 3? 【参考方案1】:

我想知道你是否可以做到这一点:

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).

【讨论】:

嘿,戈登 - 这似乎可以完成这项工作!谢谢 这不会像原来那样删除(article_published_date, prediction_date) 上的重复项。【参考方案2】:

可以尝试的一件事是使用窗口函数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;

Demo on DB Fiddle.

【讨论】:

嘿@GMB - 上面的查询比 distinct on 慢大约 3 倍。 EXPLAIN (ANALYZE, BUFFERS) 在这里:pastebin.com/b6fZy5nP @mjames:感谢有趣的反馈。我猜想 Postgres 能够以某种方式优化 DISTINCT ON() 比标准的 ROW_NUMBER() 更好,毕竟这并不令人惊讶,因为前者与后者相比有点有限(见我的回答)。 @mjames:我可以看到你接受了一个答案,但没有投票,而你有足够的代表。这些答案对你没有用吗? 谢谢@GMB - 没有意识到这是正确的礼仪。所有答案都经过深思熟虑 - 是对每个答案进行投票的最佳做法,还是只是解决方案? @mjames:见this link:您接受您认为解决方案的答案,并upvote您认为有用的所有答案(通常包括接受的解决方案)。【参考方案3】:

虽然您只需要少量的结果行(在您的示例中为LIMIT 3),并且如果article_published_dateprediction_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 更大或没有LIMIT,就会变得有趣。

基础知识:

Optimize GROUP BY query to retrieve latest record per user Can spatial index help a “range - order by - limit” query

dbfiddle here,演示全部。

【讨论】:

嗨@ErwinBrandstetter - 第一个查询比我天真的方法慢得多 - 运行需要43秒(vs 4)。我添加了两个索引,并分析了表格。解释(分析,缓冲区)在这里:pastebin.com/8D5rGQDE 啊!索引是错误的。关键细节是首先拥有article_published_date。你能不能再试一次。我的目标是微秒,而不是秒。另外,我的第二个查询与您的原始查询相比如何?每篇文章只有 5 到 6 行,DISTINCT ON 可能仍然比我的第二个查询要快。 @mjames:您有时间尝试使用固定索引吗?还是 plpgsql 替代方案?

以上是关于如何采用按单独列排序的 DISTINCT ON 子查询并使其快速?的主要内容,如果未能解决你的问题,请参考以下文章

SELECT DISTINCT ON (col) * 有效吗?

子查询

SQL 必知必会- 第三课 排序检索数据

按单独的列排序 ggplot

数据表子表单按列排序

如何按列剩余的几个月和几天对列进行排序