【问题标题】:Adding indexes slows down my query in PostgreSQL but the opposite is true in SQLite添加索引会减慢我在 PostgreSQL 中的查询速度,但在 SQLite 中则相反
【发布时间】:2022-09-30 00:07:37
【问题描述】:

我的 PostgreSQL 数据库(v13.8,在 Debian 11(Bullseye)上运行)中有下表,其中包含约 15,000,000 行:

=> \d ncbitaxon
               Table "public.ncbitaxon"
   Column   |  Type   | Collation | Nullable | Default 
------------+---------+-----------+----------+---------
 assertion  | integer |           |          | 
 retraction | integer |           |          | 0
 graph      | text    |           |          | 
 subject    | text    |           |          | 
 predicate  | text    |           |          | 
 object     | text    |           |          | 
 datatype   | text    |           |          | 
 annotation | text    |           |          | 

该表没有主键,因此每个subject 字段可以与多个objectpredicate 字段相关联。我想检索与 predicate 'rdf:type' 和 object 'owl:Class' 相关联的每个主题,但那是不是predicate 'rdfs:subClassOf' 相关联。

该表没有索引,当我运行以下查询时,我会在 2 秒内得到一致的答案(我认为这是可以接受的):

select n1.subject
  from ncbitaxon n1
 where n1.predicate = 'rdf:type'
   and n1.object = 'owl:Class'
   and not exists (
     select 1
       from ncbitaxon n2
      where n2.subject = n1.subject
        and n2.predicate = 'rdfs:subClassOf'
   )

但是,当我在每个 subjectobjectpredicatedatatype 列上添加(非唯一)btree 索引时,性能会大大降低,因此我的结果会在大约 9 秒左右的时间内始终如一地返回(这太慢了)。

我意识到索引不是灵丹妙药,有时甚至可以(显然)减慢选择查询的速度。

但令我困惑的是,当我在同一张表上运行相同的查询时,但这次在 SQLite(v3.34.1,在同一台笔记本电脑上运行)中,我得到了相反的效果。即,有了这些索引,我会在大约 5 秒内得到结果,而没有索引我最终不得不按 Ctrl-C,因为我厌倦了等待结果返回。

我想知道这是否可能是由于某种缓存效应,所以我尝试通过运行来清除 PostgreSQL 的缓存:

echo 1 > /proc/sys/vm/drop_caches
echo 2 > /proc/sys/vm/drop_caches
echo 3 > /proc/sys/vm/drop_caches
systemctl restart postgresql

确实有一点缓存效应,因为我第一次在 PostgreSQL 中运行查询后,它需要大约 3 秒(而不是 2 秒)。但是,如果我有这些索引,这仍然比在 SQLite (~5s) 或 PostgreSQL (~9s) 中运行查询要快。

我很困惑。有人知道这里发生了什么吗?

创建索引语句是:

create index idx_77907_idx_ncbitaxon_predicate on ncbitaxon (predicate);
create index idx_77907_idx_ncbitaxon_subject on ncbitaxon (subject);
create index idx_77907_idx_ncbitaxon_object on ncbitaxon (object);
create index idx_77907_idx_ncbitaxon_datatype on ncbitaxon (datatype);

以下是 explain (analyze, buffers, format text) 对于 with-indexes 情况的输出:

                                                                           QUERY PLAN                                                                            
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.56..549629.43 rows=1 width=17) (actual time=5354.149..7223.752 rows=2 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   Buffers: shared hit=12556526 read=226206
   ->  Nested Loop Anti Join  (cost=0.56..548629.33 rows=1 width=17) (actual time=5942.986..7184.393 rows=1 loops=3)
         Buffers: shared hit=12556526 read=226206
         ->  Parallel Seq Scan on ncbitaxon n1  (cost=0.00..295443.22 rows=168032 width=17) (actual time=137.371..630.607 rows=812952 loops=3)
               Filter: ((predicate = 'rdf:type'::text) AND (object = 'owl:Class'::text))
               Rows Removed by Filter: 4250687
               Buffers: shared hit=6214 read=194286
         ->  Index Scan using idx_77907_idx_ncbitaxon_subject on ncbitaxon n2  (cost=0.56..3.46 rows=5 width=17) (actual time=0.008..0.008 rows=1 loops=2438855)
               Index Cond: (subject = n1.subject)
               Filter: (predicate = 'rdfs:subClassOf'::text)
               Rows Removed by Filter: 4
               Buffers: shared hit=12550312 read=31920
 Planning:
   Buffers: shared hit=14 read=6
 Planning Time: 1.054 ms
 JIT:
   Functions: 27
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 6.551 ms, Inlining 111.762 ms, Optimization 182.297 ms, Emission 117.506 ms, Total 418.115 ms
 Execution Time: 7228.950 ms
(23 rows)

这是针对无索引的情况:

------------------------------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=299030.49..603580.05 rows=1 width=17) (actual time=1461.791..1500.212 rows=2 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   Buffers: shared hit=25757 read=375369, temp read=22444 written=22996
   ->  Parallel Hash Anti Join  (cost=298030.49..602579.95 rows=1 width=17) (actual time=1425.751..1432.604 rows=1 loops=3)
         Hash Cond: (n1.subject = n2.subject)
         Buffers: shared hit=25757 read=375369, temp read=22444 written=22996
         ->  Parallel Seq Scan on ncbitaxon n1  (cost=0.00..295443.22 rows=168032 width=17) (actual time=0.021..447.123 rows=812952 loops=3)
               Filter: ((predicate = 'rdf:type'::text) AND (object = 'owl:Class'::text))
               Rows Removed by Filter: 4250687
               Buffers: shared hit=12868 read=187632
         ->  Parallel Hash  (cost=279619.35..279619.35 rows=1002811 width=17) (actual time=694.169..694.170 rows=812951 loops=3)
               Buckets: 65536  Batches: 64  Memory Usage: 2624kB
               Buffers: shared hit=12772 read=187728, temp written=11456
               ->  Parallel Seq Scan on ncbitaxon n2  (cost=0.00..279619.35 rows=1002811 width=17) (actual time=139.958..552.807 rows=812951 loops=3)
                     Filter: (predicate = 'rdfs:subClassOf'::text)
                     Rows Removed by Filter: 4250687
                     Buffers: shared hit=12772 read=187728
 Planning:
   Buffers: shared hit=5 dirtied=1
 Planning Time: 0.582 ms
 JIT:
   Functions: 39
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 6.349 ms, Inlining 93.709 ms, Optimization 198.947 ms, Emission 126.610 ms, Total 425.615 ms
 Execution Time: 1504.890 ms
(26 rows)

SQLite(带索引)中explain 的输出为:

addr  opcode         p1    p2    p3    p4             p5  comment      
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     27    0                    0   Start at 27  
1     OpenRead       0     34019  0     6              0   root=34019 iDb=0; ncbitaxon
2     OpenRead       2     519613  0     k(2,,)         2   root=519613 iDb=0; idx_ncbitaxon_object
3     String8        0     1     0     owl:Class      0   r[1]='owl:Class'
4     SeekGE         2     26    1     1              0   key=r[1]     
5       IdxGT          2     26    1     1              0   key=r[1]     
6       DeferredSeek   2     0     0                    0   Move 0 to 2.rowid if needed
7       Column         0     4     2                    0   r[2]=ncbitaxon.predicate
8       Ne             3     25    2     BINARY-8       82  if r[2]!=r[3] goto 25
9       Integer        0     4     0                    0   r[4]=0; Init EXISTS result
10      Integer        1     5     0                    0   r[5]=1; LIMIT counter
11      OpenRead       1     34019  0     5              0   root=34019 iDb=0; ncbitaxon
12      OpenRead       3     332263  0     k(2,,)         2   root=332263 iDb=0; idx_ncbitaxon_subject
13      Column         0     3     6                    0   r[6]=ncbitaxon.subject
14      SeekGE         3     22    6     1              0   key=r[6]     
15        IdxGT          3     22    6     1              0   key=r[6]     
16        DeferredSeek   3     0     1                    0   Move 1 to 3.rowid if needed
17        Column         1     4     7                    0   r[7]=ncbitaxon.predicate
18        Ne             8     21    7     BINARY-8       82  if r[7]!=r[8] goto 21
19        Integer        1     4     0                    0   r[4]=1       
20        DecrJumpZero   5     22    0                    0   if (--r[5])==0 goto 22
21      Next           3     15    1                    0                
22      If             4     25    1                    0                
23      Column         0     3     10                   0   r[10]=ncbitaxon.subject
24      ResultRow      10    1     0                    0   output=r[10] 
25    Next           2     5     1                    0                
26    Halt           0     0     0                    0                
27    Transaction    0     0     77    0              1   usesStmtJournal=0
28    String8        0     3     0     rdf:type       0   r[3]='rdf:type'
29    String8        0     8     0     rdfs:subClassOf  0   r[8]='rdfs:subClassOf'
30    Goto           0     1     0                    0                

最后,这是 SQLite 中explain query plan 的输出:

QUERY PLAN
|--SEARCH TABLE ncbitaxon AS n1 USING INDEX idx_ncbitaxon_object (object=?)
`--CORRELATED SCALAR SUBQUERY 1
   `--SEARCH TABLE ncbitaxon AS n2 USING INDEX idx_ncbitaxon_subject (subject=?)

【问题讨论】:

  • 您是否尝试过针对宾语、主语和谓语组合的单一索引? (可能顺序不同,试试看,看数据)
  • 谢谢你的建议。是的,我已经尝试过了。
  • 索引idx_77907_idx_ncbitaxon_subject是没用的,使用这个的时候,数据库还是要过滤predicate,看查询计划。当您对两者的组合进行索引时会发生什么? (并请分享查询计划)

标签: postgresql sqlite query-optimization


【解决方案1】:

我无法给出完整的答案,但这里有一些想法:

  • 在慢速计划中,PostgreSQL 将 ncbitaxon n1 中的行数低估了近 5 倍。尝试通过运行 ANALYZE 收集新的统计信息来改进该估计,或者,如果这还不够,则通过创建这些列的相关性的扩展统计信息。

    如果该估计更准确,那么慢速计划将以更高的成本进行估计并且不会被选择。

  • 在慢速计划中,ncbitaxon n2 上的重复索引扫描比 PostgreSQL 估计的要昂贵得多。这部分是由于上面的错误估计,但可能random_page_cost 设置得低于您的硬件的适当值,因此 PostgreSQL 低估了索引扫描的成本。

【讨论】:

  • 谢谢,这很有趣。我会研究这些建议。
  • 暂时增加random_page_cost 参数有效。当我将其设置得足够高时,PostgreSQL 会正确选择更快的(在这种情况下)顺序扫描。我现在需要对我的硬件的最佳值进行更多调查,因为我无法完全删除这些索引,因为该表上的其他查询需要它们。再次感谢。这个建议很有帮助。
  • 运行create statistics ncbitaxon_statistics on subject, predicate, object from ncbitaxon ; analyze ncbitaxon 也同样有效,似乎比更改random_page_cost 更理想。
  • 是的,扩展统计是更好的解决方案。我并不是要不加选择地增加random_page_cost,只是将其设置为适合您的硬件的值。
猜你喜欢
  • 2021-11-15
  • 1970-01-01
  • 2016-08-28
  • 1970-01-01
  • 1970-01-01
  • 2023-03-22
  • 2021-10-09
  • 2017-12-24
  • 1970-01-01
相关资源
最近更新 更多