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

简介: 在PostgreSQL中修改了一行不明显的代码,把(ANY(ARRAY[...]) 改成 ANY(VALUES(...))),结果查询时间从20s变为0.2s。

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

事出有因

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

查询与计划

结果令人震惊,罪魁祸首竟然是下面这个简单的查询:

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获取与峰值最接近的行数。

                 

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

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

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吧。(编译:张红月 审校:王果)

相关文章
|
5月前
|
SQL 存储 安全
第七章 SQL错误信息 - SQL错误代码 -400 到 -500
第七章 SQL错误信息 - SQL错误代码 -400 到 -500
67 1
|
2月前
|
存储 SQL 安全
【数据库高手的秘密武器:深度解析SQL视图与存储过程的魅力——封装复杂逻辑,实现代码高复用性的终极指南】
【8月更文挑战第31天】本文通过具体代码示例介绍 SQL 视图与存储过程的创建及应用优势。视图作为虚拟表,可简化复杂查询并提升代码可维护性;存储过程则预编译 SQL 语句,支持复杂逻辑与事务处理,增强代码复用性和安全性。通过创建视图 `high_earners` 和存储过程 `get_employee_details` 及 `update_salary` 的实例,展示了二者在实际项目中的强大功能。
29 1
|
1月前
|
SQL 安全 数据库
基于SQL Server事务日志的数据库恢复技术及实战代码详解
基于事务日志的数据库恢复技术是SQL Server中一个非常强大的功能,它能够帮助数据库管理员在数据丢失或损坏的情况下,有效地恢复数据。通过定期备份数据库和事务日志,并在需要时按照正确的步骤恢复,可以最大限度地减少数据丢失的风险。需要注意的是,恢复数据是一个需要谨慎操作的过程,建议在执行恢复操作之前,详细了解相关的操作步骤和注意事项,以确保数据的安全和完整。
59 0
|
2月前
|
JSON 数据格式 Java
化繁为简的魔法:Struts 2 与 JSON 联手打造超流畅数据交换体验,让应用飞起来!
【8月更文挑战第31天】在现代 Web 开发中,JSON 成为数据交换的主流格式,以其轻量、易读和易解析的特点受到青睐。Struts 2 内置对 JSON 的支持,结合 Jackson 库可便捷实现数据传输。本文通过具体示例展示了如何在 Struts 2 中进行 JSON 数据的序列化与反序列化,并结合 AJAX 技术提升 Web 应用的响应速度和用户体验。
76 0
|
2月前
|
SQL 数据库 索引
SQL 编程最佳实践简直太牛啦!带你编写高效又可维护的 SQL 代码,轻松应对数据库挑战!
【8月更文挑战第31天】在SQL编程中,高效与可维护的代码至关重要,不仅能提升数据库性能,还降低维护成本。本文通过案例分析探讨SQL最佳实践:避免全表扫描,利用索引加速查询;合理使用JOIN,避免性能问题;避免使用`SELECT *`,减少不必要的数据传输;使用`COMMIT`和`ROLLBACK`确保事务一致性;添加注释提高代码可读性。遵循这些实践,不仅提升性能,还便于后期维护和扩展。应根据具体情况选择合适方法并持续优化SQL代码。
27 0
|
2月前
|
SQL Java 数据库连接
【Azure 应用服务】Java ODBC代码中,启用 Managed Identity 登录 SQL Server 报错 Managed Identity authentication is not available
【Azure 应用服务】Java ODBC代码中,启用 Managed Identity 登录 SQL Server 报错 Managed Identity authentication is not available
|
3月前
|
关系型数据库 分布式数据库 数据库
PolarDB产品使用问题之将RDS切换到PolarDB-X 2.0时,代码层的SQL该如何改动
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
2月前
|
SQL DataWorks 大数据
DataWorks操作报错合集之SQL代码行数过长产生报错,该如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
2月前
|
SQL 数据库
实时数仓 Hologres产品使用合集之如何找回之前的SQL查询代码
实时数仓Hologres是阿里云推出的一款高性能、实时分析的数据库服务,专为大数据分析和复杂查询场景设计。使用Hologres,企业能够打破传统数据仓库的延迟瓶颈,实现数据到决策的无缝衔接,加速业务创新和响应速度。以下是Hologres产品的一些典型使用场景合集。
|
2月前
|
SQL
SQL SERVER 查询表结构,导出到Excel 生成代码用
SQL SERVER 查询表结构,导出到Excel 生成代码用
33 0
下一篇
无影云桌面