修改一行SQL代码 性能提高了100倍

在PostgreSQL中修改了一行不明显的代码,把(ANY(ARRAY[...]) 改为 ANY(VALUES(...))),结果查询时间从20s变为0.2s。最初咱们学习使用EXPLAN ANALYZE来优化代码,到后来,Postgres社区也成为咱们学习提高的一个好帮手,付出总会有回报,咱们产品的性能也所以获得了极大的提高。html

事出有因java

咱们所开发的产品是Datadog,它是专门为那些编写和运营大规模应用的团队、IT运营商提供监控服务的一个平台,帮助他们把海量的数据转化为切实可行的计划、操做方案。而在这周早些时候,咱们的许多数据库所面临的一个性能问题是在一个较小的表上进行大量的key查询。这些查询中的99.9%都是高效灵活的。在极少数实例中,有些数量的性能指标tag查询是费时的,这些查询须要花费20s时间。这也就意味着用户须要在浏览器面前花费这么长的时间来等待图形编辑器作出响应。即便是0.1%,这样的用户体验也显然糟透了,对此,咱们进行了监测,探究为什么速度会这么慢。sql

查询与计划数据库

结果使人震惊,罪魁祸首居然是下面这个简单的查询:数组


1
2
3
4
5
6
7
8
9
10
SELECT c.key,
c.x_key,
c.tags,
x.name
FROM context c
JOIN x
ON c.x_key = x.key
WHERE c.key = ANY (ARRAY[ 15368196 , -- 11 , 000 other keys --)])
AND c.x_key = 1
AND c.tags @> ARRAY[E 'blah' ];



X表拥有上千行数据,C表拥有1500万行数据,这两个表的“key”列都带有适当的索引主键。简单地说,它就是一个简单的主键查询。但有趣地是,随着key列中记录的增长,例如在11000行时,咱们经过添加EXPLAIN (ANALYZE, BUFFERS)前缀来查看key列的值是否与数组中的值匹配:浏览器


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Nested Loop  (cost= 6923.33 .. 11770.59 rows= 1 width= 362 ) (actual time= 17128.188 .. 22109.283 rows= 10858 loops= 1 )
Buffers: shared hit= 83494
->  Bitmap Heap Scan on context c  (cost= 6923.33 .. 11762.31 rows= 1 width= 329 ) (actual time= 17128.121 .. 22031.783 rows= 10858 loops= 1 )
Recheck Cond: ((tags @> '{blah}' ::text[]) AND (x_key = 1 ))
Filter: (key = ANY ( '{15368196,(a lot more keys here)}' ::integer[]))
Buffers: shared hit= 50919
->  BitmapAnd  (cost= 6923.33 .. 6923.33 rows= 269 width= 0 ) (actual time= 132.910 .. 132.910 rows= 0 loops= 1 )
Buffers: shared hit= 1342
->  Bitmap Index Scan on context_tags_idx  (cost= 0.00 .. 1149.61 rows= 15891 width= 0 ) (actual time= 64.614 .. 64.614 rows= 264777 loops= 1 )
Index Cond: (tags @> '{blah}' ::text[])
Buffers: shared hit= 401
->  Bitmap Index Scan on context_x_id_source_type_id_idx  (cost= 0.00 .. 5773.47 rows= 268667 width= 0 ) (actual time= 54.648 .. 54.648 rows= 267659 loops= 1 )
Index Cond: (x_id = 1 )
Buffers: shared hit= 941
->  Index Scan using x_pkey on x  (cost= 0.00 .. 8.27 rows= 1 width= 37 ) (actual time= 0.003 .. 0.004 rows= 1 loops= 10858 )
Index Cond: (x.key = 1 )
Buffers: shared hit= 32575
Total runtime: 22117.417 ms


此次查询共花费22s,咱们能够经过下图对这22s进行很直观的了解,其中大部分时间花费在Postgres和OS之间,而磁盘I/O则花费很是少的时间。

在最低水平,这些查询看起来就像是这些CPU利用率的峰值。在这里主要是想证明一个关键点:数据库不会等待磁盘去读取数据,而是作排序、散列和行比较这些事。编辑器

经过Postgres获取与峰值最接近的行数。ide


显然,咱们的查询在大多数状况下都有条不紊的执行着。oop

Postgres的性能问题:位图堆扫描
post

rows_fetched度量与下面的部分计划是一致的:


1
2
3
4
5
Buffers: shared hit= 83494
->  Bitmap Heap Scan on context c  (cost= 6923.33 .. 11762.31 rows= 1 width= 329 ) (actual time= 17128.121 .. 22031.783 rows= 10858 loops= 1 )
Recheck Cond: ((tags @> '{blah}' ::text[]) AND (x_key = 1 ))
Filter: (key = ANY ( '{15368196,(a lot more keys here)}' ::integer[]))
Buffers: shared hit= 50919


Postgres使用位图堆扫描( Bitmap Heap Scan )来读取C表数据。当关键字的数量较少时,它能够在内存中很是高效地使用索引构建位图。若是位图太大,查询优化器会改变其查找数据的方式。在咱们这个案例中,须要检查大量的关键字,因此它使用了很是类似的方法来检查候选行而且单独检查与x_key和tag相匹配的每一行。而全部的这些“在内存中加载”和“检查每一行”都须要花费大量的时间。


幸运的是,咱们的表有30%都是装载在RAM中,因此在从磁盘上检查行的时候,它不会表现的太糟糕。但在性能上,它仍然存在很是明显的影响。查询过于简单,这是一个很是简单的key查找,因此没有显而易见的数据库或应用重构,它很难找到一些简单的方式来解决这个问题。最后,咱们使用PGSQL-Performance邮件向社区求助。

解决方案

开源帮了咱们,经验丰富的且代码贡献量很是多的Tom Lane让咱们试试这个:


1
2
3
4
5
6
7
8
9
10
SELECT c.key,
c.x_key,
c.tags,
x.name
FROM context c
JOIN x
ON c.x_key = x.key
WHERE c.key = ANY (VALUES ( 15368196 ), -- 11 , 000 other keys --)
AND c.x_key = 1
AND c.tags @> ARRAY[E 'blah' ];


你能发现有啥不一样之处吗?把ARRAY换成了VALUES。

咱们使用ARRAY[...]列举出全部的关键字来进行查询,但却欺骗了查询优化器。Values(...)让优化器充分使用关键字索引。仅仅是一行代码的改变,而且没有产生任何语义的改变。

下面是新查询语句的写法,差异就在于第三和第十四行。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
Nested Loop  (cost= 168.22 .. 2116.29 rows= 148 width= 362 ) (actual time= 22.134 .. 256.531 rows= 10858 loops= 1 )
Buffers: shared hit= 44967
->  Index Scan using x_pkey on x  (cost= 0.00 .. 8.27 rows= 1 width= 37 ) (actual time= 0.071 .. 0.073 rows= 1 loops= 1 )
Index Cond: (id = 1 )
Buffers: shared hit= 4
->  Nested Loop  (cost= 168.22 .. 2106.54 rows= 148 width= 329 ) (actual time= 22.060 .. 242.406 rows= 10858 loops= 1 )
Buffers: shared hit= 44963
->  HashAggregate  (cost= 168.22 .. 170.22 rows= 200 width= 4 ) (actual time= 21.529 .. 32.820 rows= 11215 loops= 1 )
->  Values Scan on "*VALUES*" (cost= 0.00 .. 140.19 rows= 11215 width= 4 ) (actual time= 0.005 .. 9.527 rows= 11215 loops= 1 )
->  Index Scan using context_pkey on context c  (cost= 0.00 .. 9.67 rows= 1 width= 329 ) (actual time= 0.015 .. 0.016 rows= 1 loops= 11215 )
Index Cond: (c.key = "*VALUES*" .column1)
Filter: ((c.tags @> '{blah}' ::text[]) AND (c.x_id = 1 ))
Buffers: shared hit= 44963
Total runtime: 263.639 ms


从22000ms到200ms,仅仅修改了一行代码,速度提高了100倍还多。


产品里新的查询

部署后的代码:

数据库看起来更美观


Postgres慢查询将一去不复返了。但有谁愿意由于这个0.1%的倒霉蛋再去折磨呢?咱们使用Datadog来验证修改是否正确,它可以作出即时验证。若是你想查看Postgres查询速度的各类影响, 不妨试试Datadog吧。

相关文章
相关标签/搜索