使用ORDER BY进行LIMIT会使查询变慢

时间:2018-12-07 15:24:06

标签: postgresql indexing sql-execution-plan postgresql-performance

我在PostgreSQL 9.5.14中优化查询时遇到问题。

select *
from file as f
join product_collection pc on (f.product_collection_id = pc.id)
where pc.mission_id = 7
order by f.id asc
limit 100;

大约需要100秒。如果删除limit子句,则大约需要0.5:

使用limit

explain (analyze,buffers) ... -- query exactly as above
                                                                             QUERY PLAN                                                                             
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.84..859.32 rows=100 width=457) (actual time=102793.422..102856.884 rows=100 loops=1)
   Buffers: shared hit=222430592
   ->  Nested Loop  (cost=0.84..58412343.43 rows=6804163 width=457) (actual time=102793.417..102856.872 rows=100 loops=1)
         Buffers: shared hit=222430592
         ->  Index Scan using file_pkey on file f  (cost=0.57..23409008.61 rows=113831736 width=330) (actual time=0.048..28207.152 rows=55858772 loops=1)
               Buffers: shared hit=55652672
         ->  Index Scan using product_collection_pkey on product_collection pc  (cost=0.28..0.30 rows=1 width=127) (actual time=0.001..0.001 rows=0 loops=55858772)
               Index Cond: (id = f.product_collection_id)
               Filter: (mission_id = 7)
               Rows Removed by Filter: 1
               Buffers: shared hit=166777920
 Planning time: 0.803 ms
 Execution time: 102856.988 ms

没有limit

=>  explain (analyze,buffers)  ... -- query as above, just without limit
                                                                                 QUERY PLAN                                                                                 
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=20509671.01..20526681.42 rows=6804163 width=457) (actual time=456.175..510.596 rows=142055 loops=1)
   Sort Key: f.id
   Sort Method: quicksort  Memory: 79392kB
   Buffers: shared hit=37956
   ->  Nested Loop  (cost=0.84..16494851.02 rows=6804163 width=457) (actual time=0.044..231.051 rows=142055 loops=1)
         Buffers: shared hit=37956
         ->  Index Scan using product_collection_mission_id_index on product_collection pc  (cost=0.28..46.13 rows=87 width=127) (actual time=0.017..0.101 rows=87 loops=1)
               Index Cond: (mission_id = 7)
               Buffers: shared hit=10
         ->  Index Scan using file_product_collection_id_index on file f  (cost=0.57..187900.11 rows=169535 width=330) (actual time=0.007..1.335 rows=1633 loops=87)
               Index Cond: (product_collection_id = pc.id)
               Buffers: shared hit=37946
 Planning time: 0.807 ms
 Execution time: 569.865 ms

我已将数据库复制到备份服务器,以便我可以安全地操作数据库而无需进行其他更改。

基数:
file:113,831,736行。
product_collection:1370行。
没有LIMIT的查询:142,055行。
SELECT count(*) FROM product_collection WHERE mission_id = 7:87行。

我尝试过的事情:

  • 搜索堆栈溢出
  • 真空完全分析
  • 在file.product_collection_id和file.id上创建两个列索引。 (在每个接触的字段上已经有单列​​索引。)
  • 在file.id和file.product_collection_id上创建两个列索引。
  • 增加file.id和file.product_collection_id的统计信息,然后重新进行真空分析。
  • 更改各种查询计划器设置。
  • 创建非实体化视图。
  • 自言自语地走在走廊上。

它们似乎都没有显着改变性能。

有想法吗?

OP的更新
在PostgreSQL 9.6和10.4上进行了测试,发现计划或性能没有明显变化。

但是,将random_page_cost设置得足够低是在无限制搜索中获得更快性能的唯一方法。

使用默认的random_page_cost = 4,不使用limit

                                                                           QUERY PLAN                                                                           
----------------------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=9270013.01..9287875.64 rows=7145054 width=457) (actual time=47782.523..47843.812 rows=145697 loops=1)
   Sort Key: f.id
   Sort Method: external sort  Disk: 59416kB
   Buffers: shared hit=3997185 read=1295264, temp read=7427 written=7427
   ->  Hash Join  (cost=24.19..6966882.72 rows=7145054 width=457) (actual time=1.323..47458.767 rows=145697 loops=1)
         Hash Cond: (f.product_collection_id = pc.id)
         Buffers: shared hit=3997182 read=1295264
         ->  Seq Scan on file f  (cost=0.00..6458232.17 rows=116580217 width=330) (actual time=0.007..17097.581 rows=116729984 loops=1)
               Buffers: shared hit=3997169 read=1295261
         ->  Hash  (cost=23.08..23.08 rows=89 width=127) (actual time=0.840..0.840 rows=87 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 15kB
               Buffers: shared hit=13 read=3
               ->  Bitmap Heap Scan on product_collection pc  (cost=4.97..23.08 rows=89 width=127) (actual time=0.722..0.801 rows=87 loops=1)
                     Recheck Cond: (mission_id = 7)
                     Heap Blocks: exact=10
                     Buffers: shared hit=13 read=3
                     ->  Bitmap Index Scan on product_collection_mission_id_index  (cost=0.00..4.95 rows=89 width=0) (actual time=0.707..0.707 rows=87 loops=1)
                           Index Cond: (mission_id = 7)
                           Buffers: shared hit=3 read=3
 Planning time: 0.929 ms
 Execution time: 47911.689 ms

以下用户Erwin的回答将使我花费一些时间来完全理解并概括所需的所有用例。同时,我们可能会使用实例化视图或将表结构扁平化。

1 个答案:

答案 0 :(得分:3)

对于Postgres查询计划者来说,此查询比看起来要难。根据基数,数据分布,值频率,大小...,可能会出现完全不同的查询计划,并且计划者很难预测哪个是最佳的。当前版本的Postgres在多个方面都可以做到这一点,但是仍然难以优化。

由于仅从product_collection中检索了相对较少的行,因此在LATERAL subquery中使用LIMIT进行的等效查询应避免性能下降:

SELECT *
FROM   product_collection pc
CROSS  JOIN LATERAL (
   SELECT *
   FROM   file f  -- big table
   WHERE  f.product_collection_id = pc.id
   ORDER  BY f.id
   LIMIT  100
   ) f
WHERE  pc.mission_id = 7
ORDER  BY f.id
LIMIT  100;

编辑:这将产生一个由OP提供的explain (analyze,verbose)的查询计划:

                                                                         QUERY PLAN                                                                          
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=30524.34..30524.59 rows=100 width=457) (actual time=13.128..13.167 rows=100 loops=1)
   Buffers: shared hit=3213
   ->  Sort  (cost=30524.34..30546.09 rows=8700 width=457) (actual time=13.126..13.152 rows=100 loops=1)
         Sort Key: file.id
         Sort Method: top-N heapsort  Memory: 76kB
         Buffers: shared hit=3213
         ->  Nested Loop  (cost=0.57..30191.83 rows=8700 width=457) (actual time=0.060..9.868 rows=2880 loops=1)
               Buffers: shared hit=3213
               ->  Seq Scan on product_collection pc  (cost=0.00..69.12 rows=87 width=127) (actual time=0.024..0.336 rows=87 loops=1)
                     Filter: (mission_id = 7)
                     Rows Removed by Filter: 1283
                     Buffers: shared hit=13
               ->  Limit  (cost=0.57..344.24 rows=100 width=330) (actual time=0.008..0.071 rows=33 loops=87)
                     Buffers: shared hit=3200
                         ->  Index Scan using file_pc_id_index on file  (cost=0.57..582642.42 rows=169535 width=330) (actual time=0.007..0.065 rows=33 loops=87)
                           Index Cond: (product_collection_id = pc.id)
                           Buffers: shared hit=3200
 Planning time: 0.595 ms
 Execution time: 13.319 ms

您需要以下索引(也将帮助您进行原始查询):

CREATE INDEX idx1 ON file (product_collection_id, id);     -- crucial
CREATE INDEX idx2 ON product_collection (mission_id, id);  -- helpful

您提到:

  

file.idfile.product_collection_id上的两个列索引。

等等但是我们反过来需要它:最后是id索引表达式的顺序至关重要。参见:

理论上:product_collection中只有87行,我们只能获取 maximum (最大xem)为87 x 100 = 8700行(如果不是每个pc.id都具有100行,则更少file),然后在进入前100名之前对其进行排序。性能会随着从product_collection获得的行数和更大的LIMIT而下降。

使用上面的多列索引idx1,即进行87次快速索引扫描。其余的不是很贵。

根据其他信息,可以进行更多优化。相关: