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 树索引下的表现:

EXPLAIN ANALYZE SELECT * 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);

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

EXPLAIN ANALYZE SELECT * 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 输出以保持事情以最佳速度运行的原因。

5个容易忽视的PostgreSQL查询性能瓶颈的更多相关文章

  1. postgresql查询语句

    //查询表名称SELECT tablename FROM pg_tablesWHERE tablename NOT LIKE 'pg%'AND tablename NOT LIKE 'sql_%' O ...

  2. postgresql查询的处理过程

    本文简单描述了Postgresql服务器接收到查询后到返回给客户端结果之间一般操作顺序,总的流程图如下: 第一步: 客户端程序可以是任何符合 PostgreSQL 协议规范的程序,如 JDBC 驱动. ...

  3. 「在 Kubernetes 上运行 Pgpool-Il」实现 PostgreSQL 查询(读)负载均衡和连接池

    介绍如何在 Kubernetes 上运行 Pgpool-II 实现 PostgreSQL 读查询负载均衡和连接池. 介绍 因为 PostgreSQL 是一个有状态的应用程序,并且管理 PostgreS ...

  4. 手把手教你分析MySQL查询性能瓶颈,包教包会

    当一条SQL执行较慢,需要分析性能瓶颈,到底慢在哪? 我们一般会使用Explain查看其执行计划,从执行计划中得知这条SQL有没有使用索引?使用了哪个索引? 但是执行计划显示内容不够详细,如果显示用到 ...

  5. SQL Server性能优化(3)使用SQL Server Profiler查询性能瓶颈

    关于SQL Server Profiler的使用,网上已经有很多教程,比如这一篇文章:SQL Server Profiler:使用方法和指标说明.微软官方文档:https://msdn.microso ...

  6. Postgresql查询出换行符和回车符:

    1.有时候,业务因为回车和换行出现的错误,第一步,首先要查询出回车符和换行符那一条数据: -- 使用chr()和chr()进行查询 SELECT * )||)||'%'; -- 其实查询chr()和c ...

  7. PostgreSQL查询长连接

    apple=# select pid, backend_start, xact_start, query_start, waiting, state, backend_xid from pg_stat ...

  8. PostgreSQL查询数据(连接查询和子查询)

    原料 --用户表 create table "SysUser"( "UserId" serial, --用户Id,自增 "UserName" ...

  9. PostgreSQL 查询、创建、删除索引

    --查询索引 select * from pg_indexes where tablename='tab1'; --创建索引 tab1_bill_code_index 为索引名, create ind ...

随机推荐

  1. 【ASP.NET Core】MVC模型绑定:非规范正文内容的处理

    本篇老周就和老伙伴们分享一下,对于客户端提交的不规范 Body 如何做模型绑定.不必多说,这种情况下,只能自定义 ModelBinder 了.而且最佳方案是不要注册为全局 Binder--毕竟这种特殊 ...

  2. You Don't Know JS Yet Book 1 Notes

    Get Started - 前言 But let me be clear: I don't think it's possible to ever fully know JS. That's not ...

  3. mysql 实现类似Oracle 或 db2 sequence

    第一步:创建一个索引管理表,其中包含,索引名称.最小值.最大值.当前值.增量,并设置主键为索引名称. CREATE TABLE TB_SEQUENCE ( SEQ_NAME VARCHAR(50) N ...

  4. nginx反向代理失败,又是 fastdfs 的锅

    fdfs问题记录 [root@hostcad logs]# systemctl status fdfs_trackerd.service ● fdfs_trackerd.service - LSB: ...

  5. Jinkins流水线脚本使用curl命令调用服务接口,并且使用url传参。

    curl http://xxx.xx.xx.xx:xxxx/jenkins/publish?fileName=${fileName}&tag_name=${tag_name} 如图调用不符合c ...

  6. sp-MVC-ideabaok

    直接通过初始化器创建 或者通过创建maven工程在自己添加需要的东西 配置 dispatcher-servlet.xml 包括扫描加载包: <context:component-scan bas ...

  7. spring-boot 注解集合

    @Configuration 用于定义配置类,可替换XML配置文件,被注解的类内部包含一个或多个@Bean注解方法.可以被AnnotationConfigApplicationContext或者Ann ...

  8. 学习Git(二)

    常用命令 git add 添加 git status 查看状态 git status -s 状态概览 git diff 对比 git diff --staged 对比暂存区 git commit 提交 ...

  9. resin服务之三---独立resin的配置

    独立resin的配置 关掉httpd服务: [root@data-1-1 ~]# killall httpd [root@data-1-1 ~]# lsof -i :80    ------>h ...

  10. freeswitch对接WEBRTC的一个candidate问题

    概述 近几年,WEBRTC的完善与成熟,使得网页上使用webrtc的应用越来越多. Freeswitch是一个开源的软交换平台,可以直接支持webrtc的对接方式. 最近在测试fs和webrtc的对接 ...