5个容易忽视的PostgreSQL查询性能瓶颈

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: PostgreSQL 查询计划器充满了惊喜,因此编写高性能查询的常识性方法有时会产生误导。

postgreSQL瓶颈.jpg

PostgreSQL 查询计划器充满了惊喜,因此编写高性能查询的常识性方法有时会产生误导。在这篇博文中,我将描述借助 EXPLAIN ANALYZE 和 Postgres 元数据分析优化看似显而易见的查询的示例。

所有测试查询都是在 PostgreSQL 12 上针对一百万个对象的表执行的。如果您想使用较小的开发数据集复制类似的行为,则必须通过运行以下命令来阻止使用顺序扫描:

SET enable_seqscan TO off;

本教程假定您对阅读 EXPLAIN ANALYZE 报告有一定的基本了解。您可以查看此博客文章以了解该主题的介绍。


1. 通过函数调用搜索

通过使用 PostgreSQL 函数调用修改的值进行搜索是很常见的。让我们看一下通过小写值搜索列的查询计划:

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM users WHERE lower(email) = 'email@example.com' ;
->  Parallel Seq Scan on users
   Filter: (lower((email)::text) = 'email@example.com'::text)
   Rows Removed by Filter: 333667
   Buffers: shared hit=1248 read=41725
Execution Time: 180.813 ms

该报告表明查询计划程序执行低效的Seq ScanFilterBUFFERS来执行查询。由于在查询中添加了选项,我们可以看到数据库必须使用慢速磁盘读取操作来获取超过 40k 的数据页,并且其中只有大约 1k被缓存在内存中。

按函数搜索的查询不能使用标准索引。因此,您需要添加自定义索引以使其高效。但是,在每个查询的基础上添加自定义索引并不是一种非常可扩展的方法。您可能会发现自己有多个冗余索引,这些索引会减慢写入操作。

如果大小写字母无关紧要,您可以运行迁移以将所有值小写,并使标准索引正常工作。但是,如果您仍想在数据库中存储大写字符,您可以考虑使用CITEXT 扩展名。它创建了一个不区分大小写的列,可以在不创建自定义索引的情况下进行高效搜索。

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM users WHERE email = 'Email@exaMple.Com' ;
 Index Scan using index_users_on_email on users
   Index Cond: (email = 'Email@exaMple.Com'::citext)
   Buffers: shared hit=3
 Execution Time: 0.128 ms

原始查询的180 毫秒执行时间可能看起来并不多。但我们刚刚设法将其加速了几个数量级,降至 1毫秒以下!无论数据大小如何,新解决方案都将保持高性能,并且查询仅从内存缓存中获取三个缓冲区块。此外,通过利用扩展,我们可以避免添加额外的索引。


2. 按模式搜索

LIKE和ILIKE查询经常被使用,但并不总是很明显,需要额外的设置来有效地执行它们。让我们看看示例查询在标准 B 树索引下的表现:

EXPLAINANALYZESELECT*FROM users

WHERE email LIKE'%@example.com';


->  Parallel Seq Scan on users
    Filter:((email)::text ~~'%@example.com'::text)
Execution Time:111.263 ms

和以前一样,查询计划器无法利用索引,不得不求助于低效Seq ScanFilter.

为了加快这个查询的速度,我们必须添加一个自定义扩展和索引类型。运行以下命令:

CREATE EXTENSION IF NOT EXISTS btree_gin;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX index_users_on_email_gin ON users USING gin (email gin_trgm_ops);

让我们重新运行我们的查询:

EXPLAINANALYZESELECT*FROM users

WHERE email LIKE'%@example.com';


Bitmap Heap Scan on users
  Recheck Cond:((email)::text ~~'%@example.com'::text)
  ->  Bitmap Index Scan on index_users_on_email_gin
        Index Cond:((email)::text ~~'%@example.com'::text)
Execution Time:0.206 ms

现在它在1ms以下执行。请记住,gin索引的更新速度比标准的要慢。因此,您应该避免将它们添加到经常更新的表中。


3.按NULLS LAST排序

按 NULLS LAST 排序,除非将列配置为 NOT NULL,否则在使用它进行排序时必须小心。默认的ASC 顺序将始终在结果末尾返回 NULL 值。但是,如果您想按降序对可能为 NULL 的字符串进行排序,但将所有 NULL 保留在最后怎么办?

一种初始方法可能是利用 NULLS LAST 自定义排序顺序。

让我们仔细看看这些查询会生成的 EXPLAIN ANALYZE 输出:

EXPLAIN ANALYZE
SELECT * FROM users
ORDER BY email DESC LIMIT 10;
->  Index Scan Backward using index_users_on_email on users
  Execution Time: 0.641 ms


我们可以看到一个Index Scan Backward条目,因此我们的查询正确地使用了索引,并且几乎立即执行。但是,此查询的结果将始终从NULL值开始。因此,如果我们想将它们移动到相应的末尾,我们可以像这样重写它:

EXPLAIN ANALYZE
SELECT * FROM users
ORDER BY email DESC NULLS LAST LIMIT 10;
->  Sort  (cost=41482.85..42525.55 rows=417083 width=41) (actual time=5572.524..5572.589 rows=8 loops=3)
   Sort Key: email DESC NULLS LAST
   Sort Method: top-N heapsort  Memory: 26kB
   ->  Parallel Seq Scan on users  (cost=0.00..32469.83 rows=417083 width=41) (actual time=0.037..2793.011 rows=333667 loops=3)
 Execution Time: 5578.725 ms

但正如您所看到的查询,现在执行了超过5 SECONDS。尽管email列被索引,但标准索引不能用于带有NULLS LAST选项的排序。相反,数据库必须在内存中对整个表进行排序,或者退回到更慢的磁盘排序。它不仅会降低性能,而且还会显的增加整体内存使用量。

您可以通过添加自定义索引来修复它,NULLS LAST如PostgreSQL 文档中所述。但是,就像在按函数搜索的情况下一样,在每个查询的基础上添加自定义索引是一种不好的做法。

获得所需结果的一种简单方法是编写两个查询。第一个将获取已排序的非空值。如果结果不满足LIMIT,则另一个查询会获取剩余的带有NULL值的行。

SELECT *
FROM users ORDER BY email DESC
WHERE email IS NOT NULL LIMIT 10;
SELECT *
FROM users
WHERE email IS NULL LIMIT 10;


4.Bloated null_indexes

正如我们在前面的示例中确定的那样,添加正确的索引可以显着提高查询执行时间。但是,过度使用索引会大大增加数据库的大小并增加维护内存的使用。此外,必须在每次写入操作时更新索引。所以限制它们的数量和范围通常是一个好方法。

您的数据库可能有一些所谓的(我认为)“NULL 索引”。这些是包含高比率NULL值的索引。

根据业务逻辑,NULL可能会使用一个值进行搜索,因此这些索引是正确的。但是通常您不会编写查询来搜索包含特定NULL值的行。如果是这种情况,重新创建索引以排除NULLs 将减少磁盘使用量并限制必须更新的频率。

您可以运行以下命令来删除和重建索引以仅包含NOT NULL行:

DROP INDEX CONCURRENTLY users_reset_token_ix;
CREATE INDEX CONCURRENTLY users_reset_token_ix ON users(reset_token)
WHERE reset_token IS NOT NULL;

值得注意的是,这个索引仍然可以被显式搜索所有NOT NULL值的查询使用。

您可以查看PG Extrasnull_indexes方法(或执行其原始 SQL 源代码)以查看您的数据库是否有许多可以削减的索引以及预期的磁盘空间节省:

index      | index_size | unique | indexed_column | null_frac | expected_saving
--------------------+------------+--------+----------------+-----------+-----------------
 users_reset_token  | 1445 MB    | t      | reset_token    |   97.00%  | 1401 MB
 plan_cancelled_at  | 539 MB     | f      | cancelled_at   |    8.30%  | 44 MB
 users_email        | 18 MB      | t      | email          |   28.67%  | 5160 kB

您可以在这篇博文中阅读更多关于使用 PG Extras 优化 PostgreSQL 性能的信息。


5.更新交易范围

通常推荐的做法是将数据库提交的数量保持在最低限度。这意味着将多个更新查询包装到单个事务中应该可以提高写入性能。

对于许多常见场景,这是一个最佳策略。但是,使用单个事务进行大量数据更新可能会导致所谓的锁问题。那么让我们看看在单个事务中更新超过 100k 行有什么影响:

UPDATE messages SET status = 'archived';

当事务仍处于挂起状态时,您可以使用PG Extraslocks方法(或执行其原始 SQL 源代码)调查它生成的锁。

您可能没有足够大的数据集来locks在更新事务仍在运行时手动执行 SQL。在这种情况下,您可以像这样在单个事务中伪造缓慢的执行时间:

BEGIN;
UPDATE messages SET status = 'archived';
SELECT pg_sleep(15);
COMMIT;

将SQL执行延迟15秒的简单方法


现在,运行locksSQL 应该会返回类似的输出:

relname             |       mode       |          query_snippet
-------------------------------------------------------------------------------
 messages                 | RowExclusiveLock | UPDATE "messages" SET "status" = $1
 index_messages_on_status | RowExclusiveLock | UPDATE "messages" SET "status" = $1
 index_messages_on_text   | RowExclusiveLock | UPDATE "messages" SET "status" = $1
 index_messages_on_time   | RowExclusiveLock | UPDATE "messages" SET "status" = $1

可以看到更新操作获取了RowExclusiveLock并锁定了对应的索引。这意味着在漫长的单事务更新过程中尝试更新相同行的任何其他进程都必须等待它完成。

因此,后台工作进程执行的大规模更新可能会使 Web 服务器进程超时并导致面向用户的应用程序中断。

为避免此问题,您可以使用类似的 SQL 将批处理添加到更新操作:

UPDATE messages SET status = 'archived'
  WHERE id IN
  (SELECT ID FROM messages ORDER BY ID LIMIT 10000 OFFSET 0);
UPDATE messages SET status = 'archived'
  WHERE id IN
  (SELECT ID FROM messages ORDER BY ID LIMIT 10000 OFFSET 10000);
UPDATE messages SET status = 'archived'
  WHERE id IN
  (SELECT ID FROM messages ORDER BY ID LIMIT 10000 OFFSET 20000);
...

上面的示例一次更新 10k 的行。整个操作可能需要比在单个事务中执行更长的时间。但是,每个更新步骤都会快速提交数据库更改,因此其他进程不会卡住。

如果您怀疑您的应用程序的性能因锁定事务而下降,您可以结合使用locksPG blockingExtras 方法来监控长期表锁。


概括

优化 PostgreSQL 的挑战在于,大多数问题只有在数据集和流量足够大的情况下才会出现。在使用小型开发数据库创建新功能时,您不太可能发现潜在的瓶颈。这就是为什么必须监控生产性能并定期深入到 EXPLAIN ANALYZE 输出以保持事情以最佳速度运行的原因。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
消息中间件 存储 关系型数据库
PostgreSQL技术大讲堂 - 第33讲:并行查询管理
PostgreSQL从小白到专家,技术大讲堂 - 第33讲:并行查询管理
467 1
|
7月前
|
存储 关系型数据库 数据库
postgresql|数据库|提升查询性能的物化视图解析
postgresql|数据库|提升查询性能的物化视图解析
706 0
|
存储 关系型数据库 物联网
沉浸式学习PostgreSQL|PolarDB 14: 共享单车、徒步、旅游、网约车轨迹查询
本文的目的是帮助你了解如何设计轨迹表, 如何高性能的写入、查询、分析轨迹数据.
704 0
|
SQL 关系型数据库 Go
PostgreSQL 查询语句大全
PostgreSQL 查询语句大全
110 0
|
1月前
|
SQL 关系型数据库 数据库
PostgreSQL性能飙升的秘密:这几个调优技巧让你的数据库查询速度翻倍!
【10月更文挑战第25天】本文介绍了几种有效提升 PostgreSQL 数据库查询效率的方法,包括索引优化、查询优化、配置优化和硬件优化。通过合理设计索引、编写高效 SQL 查询、调整配置参数和选择合适硬件,可以显著提高数据库性能。
236 1
|
4月前
|
缓存 关系型数据库 数据库
PostgreSQL 查询性能
【8月更文挑战第5天】PostgreSQL 查询性能
84 8
|
6月前
|
SQL 关系型数据库 数据库
Python执行PostgreSQL数据库查询语句,并打印查询结果
本文介绍了如何使用Python连接和查询PostgreSQL数据库。首先,确保安装了`psycopg2`库,然后创建数据库连接函数。接着,展示如何编写SQL查询并执行,例如从`employees`表中选取所有记录。此外,还讨论了处理查询结果、格式化输出和异常处理的方法。最后,提到了参数化查询和事务处理以增强安全性及确保数据一致性。
Python执行PostgreSQL数据库查询语句,并打印查询结果
|
5月前
|
Java 关系型数据库 API
使用Spring Boot和PostgreSQL构建高级查询
使用Spring Boot和PostgreSQL构建高级查询
|
6月前
|
SQL 关系型数据库 数据库
Python查询PostgreSQL数据库
木头左教你如何用Python连接PostgreSQL数据库:安装`psycopg2`库,建立连接,执行SQL脚本如创建表、插入数据,同时掌握错误处理和事务管理。别忘了性能优化,利用索引、批量操作提升效率。下期更精彩!💡 csvfile
Python查询PostgreSQL数据库
|
7月前
|
SQL 人工智能 Oracle
PostgreSQL 递归查询(含层级和结构)
PostgreSQL 递归查询(含层级和结构)