为什么添加 ORDER BY 会大大加快查询速度?

Why does adding ORDER BY drastically speed up query?

我在 PostgreSQL 中发现了一些非常奇怪和违反直觉的行为。

我的查询结构如下。我正在从子查询中选择 ID 和计数。子查询执行过滤、连接、计数,但仅按 ID 排序,因为我使用 DISTINCT ON() 仅获取唯一 ID。

然后外部查询进行正确的排序,并根据需要进行任何限制和偏移。以下是查询结构的示例:

SELECT s.id, s.item_count
FROM (
    SELECT DISTINCT ON (work_items.id) work_items.id
        , work_item_states.disposition AS disposition
        , COUNT(work_items.id) OVER () AS item_count
    FROM work_items
    JOIN work_item_states ON work_item_states.work_item_refer = work_items.id
    WHERE work_item_states.disposition = 'cancelled'
    ORDER BY work_items.id
) AS s
ORDER BY s.disposition
LIMIT 50
OFFSET 0

不过我发现了一些奇怪的事情。我的数据库有几百万个条目,所以整体查询不是最快的。但是当我删除 OUTER 查询中的 ORDER BY 子句时,它大大减慢了查询时间。

但是,如果我也删除 LIMIT 子句,它会再次变快,尽管这个示例查询返回了 800 000 多个结果。

总而言之,对于外部查询:

排序方式 AND LIMIT - 快速

...
) AS s
ORDER BY s.disposition
LIMIT 50
OFFSET 0

限制 - 非常慢

...
) AS s
LIMIT 50
OFFSET 0

ORDER BY - 速度很快,尽管有 800 000 个结果

...
) AS s
ORDER BY s.disposition
OFFSET 0

都不是 - 速度很快,尽管有 800 000 个结果

...
) AS s
OFFSET 0

为了了解仅使用 LIMIT 子句会慢多少,同时使用、两者都不使用或仅使用 ORDER BY,查询不超过 10 秒。

然而,仅使用 LIMIT 子句,查询大约需要 15 分钟,是原来的 7 倍多!

您可能认为 ORDER BY 反而会减慢速度,因为它必须对子查询的结果进行排序,但事实似乎并非如此。这非常违反直觉。

如果有人知道这里的幕后发生了什么,我将非常感谢他们对此有所了解。

谢谢

EDIT - 添加了语句的执行计划:

ORDER BY and LIMIT 执行计划

Limit  (cost=518486.52..518486.65 rows=50 width=53)
  ->  Sort  (cost=518486.52..520495.59 rows=803628 width=53)
        Sort Key: s.disposition
        ->  Subquery Scan on s  (cost=479736.16..491790.58 rows=803628 width=53)
              ->  Unique  (cost=479736.16..483754.30 rows=803628 width=53)
                    ->  Sort  (cost=479736.16..481745.23 rows=803628 width=53)
                          Sort Key: work_items.id
                          ->  WindowAgg  (cost=136262.98..345979.65 rows=803628 width=53)
                                ->  Hash Join  (cost=136262.98..335934.30 rows=803628 width=45)
                                      Hash Cond: (work_items.id = work_item_states.work_item_refer)
                                      ->  Seq Scan on work_items  (cost=0.00..106679.48 rows=4020148 width=37)
                                      ->  Hash  (cost=119152.97..119152.97 rows=803681 width=45)
                                            ->  Bitmap Heap Scan on work_item_states  (cost=18968.96..119152.97 rows=803681 width=45)
                                                  Recheck Cond: (disposition = 'cancelled'::text)
                                                  ->  Bitmap Index Scan on idx_work_item_states_disposition  (cost=0.00..18768.04 rows=803681 width=0)
                                                        Index Cond: (disposition = 'cancelled'::text)

只有LIMIT个执行计划

Limit  (cost=1.11..69.52 rows=50 width=45)
  ->  Subquery Scan on s  (cost=1.11..1099599.17 rows=803628 width=45)
        ->  Unique  (cost=1.11..1091562.89 rows=803628 width=77)
              ->  WindowAgg  (cost=1.11..1089553.82 rows=803628 width=77)
                    ->  Merge Join  (cost=1.11..1079508.47 rows=803628 width=37)
                          Merge Cond: (work_items.id = work_item_states.work_item_refer)
                          ->  Index Only Scan using idx_work_items_id on work_items  (cost=0.56..477365.14 rows=4020148 width=37)
                          ->  Index Scan using idx_work_item_states_work_item_refer on work_item_states  (cost=0.56..582047.48 rows=803681 width=37)
                                Filter: (disposition = 'cancelled'::text)

仅ORDER BY执行计划

Sort  (cost=625547.09..627556.16 rows=803628 width=53)
  Sort Key: s.disposition
  ->  Subquery Scan on s  (cost=479736.16..491790.58 rows=803628 width=53)
        ->  Unique  (cost=479736.16..483754.30 rows=803628 width=53)
              ->  Sort  (cost=479736.16..481745.23 rows=803628 width=53)
                    Sort Key: work_items.id
                    ->  WindowAgg  (cost=136262.98..345979.65 rows=803628 width=53)
                          ->  Hash Join  (cost=136262.98..335934.30 rows=803628 width=45)
                                Hash Cond: (work_items.id = work_item_states.work_item_refer)
                                ->  Seq Scan on work_items  (cost=0.00..106679.48 rows=4020148 width=37)
                                ->  Hash  (cost=119152.97..119152.97 rows=803681 width=45)
                                      ->  Bitmap Heap Scan on work_item_states  (cost=18968.96..119152.97 rows=803681 width=45)
                                            Recheck Cond: (disposition = 'cancelled'::text)
                                            ->  Bitmap Index Scan on idx_work_item_states_disposition  (cost=0.00..18768.04 rows=803681 width=0)
                                                  Index Cond: (disposition = 'cancelled'::text)

你没有 post 你的执行计划,但我已经 crystal 准备好了,所以我会猜猜发生了什么。

在你的第二个非常慢的查询中,优化器有一个聪明的主意如何让它变快: 它使用 id 上的索引扫描 work_items,在嵌套循环中从 work_item_states 中获取所有匹配行,并过滤掉所有不匹配 work_item_states.disposition = 'cancelled' 的行,直到找到 50 个不同的结果。

这是个好主意,但优化器不知道所有带有 work_item_states.disposition = 'cancelled' 的行都与 work_items 匹配并具有高 id,因此它必须一直扫描直到它已找到它的 50 行。

所有其他查询都不允许计划者选择该策略,因为只有 work_items.id 顺序中的几行才可以。