目录
介绍
了解PostgreSQL查询计划对于开发人员和数据库管理员来说都是一项关键技能。这可能是我们开始优化查询的第一件事,也是验证我们优化的查询是否确实按照我们期望的方式优化的第一件事。
PostgreSQL数据库中的查询生命周期
在我们尝试阅读查询计划之前,提出一些非常基本的问题很重要:
- 为什么我们甚至需要查询计划?
- 计划中具体体现了什么?
- PostgreSQL不够智能,无法自动优化我的查询吗?我为什么要担心计划者?
- 计划器是我唯一需要看的东西吗?
每个查询都会经历不同的阶段,了解每个阶段对数据库的意义很重要。
Postgres查询生命周期图,由https://app.diagrams.net/制作
第一阶段是通过JDBC/ODBC(分别由Microsoft和Oracle创建的用于与数据库交互的API)或通过其他方式如PSQL(Postgres的终端前端)连接到数据库。
第二阶段是将查询转换为称为解析树的中间格式。讨论解析树的内部结构超出了本文的范围,但您可以想象它就像SQL查询的编译形式。
第三阶段就是我们所说的重写系统/规则系统。它采用从第二阶段生成的解析树,并以计划器/优化器可以开始在其中工作的方式重写它。
第四阶段是最重要的阶段,也是数据库的核心。如果没有计划器,执行器就会对如何执行查询、使用什么索引、是否扫描较小的表以消除更多不必要的行等问题一无所知。这个阶段就是我们将在本文中讨论的。
第五个也是最后一个阶段是执行器,它执行实际执行并返回结果。几乎所有的数据库系统都遵循一个或多或少与上述类似的过程。
数据设置
让我们用假数据设置一些虚拟表来运行我们的实验。
create table fake_data(id serial, name text, sentence text, company text);
然后用数据填充这个表。我使用下面的Python脚本来生成随机行。
from faker import Faker
fake = Faker()
# Change this range to whatever value you like
MAX_RANGE = 1000
with open('data.csv', 'w') as f:
for i in range(0, MAX_RANGE):
name = fake.name().replace(",", "")
sentence = fake.sentence(
nb_words=16, variable_nb_words=True
).replace(",", "")
company = fake.company().replace(",", "")
content = "'" + name + "'" + "," + \
"'" + sentence + "'" + "," \
+ "'" + company + "'" + "\n"
f.write(content)
该脚本使用Faker库来生成假数据。它将在根级别生成一个csv文件,并且可以使用以下命令作为常规csv导入到PostgreSQL中:
COPY fake_data(name, sentence, company)
FROM '/path/to/csv' DELIMITER ','
由于id是串行的,它会由PostgreSQL本身自动填充。该表现在包含1119284记录。
SELECT COUNT(*) FROM fake_data;
下面的大多数示例将基于上表。有意保持简单,专注于过程而不是表/数据的复杂性。
本文的特色图片来自Depesz在线解释工具。
进入计划阶段
PostgreSQL和许多其他数据库系统让用户可以在计划阶段看到实际发生的事情。我们可以通过运行所谓的EXPLAIN命令来做到这一点。
PostgreSQL解释一个查询
EXPLAIN SELECT * FROM fake_data LIMIT 10;
EXPLAIN查询输出显示为正常行。
通过使用EXPLAIN,您可以在数据库实际执行查询计划之前查看它们。我们将在下面的部分中了解每一个的部分,但让我们先看看另一个EXPLAIN扩展版本,叫做EXPLAIN ANALYSE。
一起解释分析
EXPLAIN ANALYSE SELECT * FROM fake_data LIMIT 10;
将ANALYZE参数添加到查询会产生计时信息。
与EXPLAIN不同,EXPLAIN ANALYSE实际上在数据库中运行查询。这个选项对于了解计划器是否没有正确发挥其作用非常有帮助,即,从EXPLAIN和EXPLAIN ANALYSE生成的计划是否存在巨大差异。
PostgreSQL是兼容了ANAYLYZE和ANALYSE
什么是数据库中的缓冲区和缓存?
让我们继续讨论一个更有趣的指标,称为BUFFERS。这解释了有多少数据来自PostgreSQL缓存以及有多少必须从磁盘中获取。
EXPLAIN (ANALYSE,BUFFERS) SELECT * FROM fake_data LIMIT 10 OFFSET 200
包含BUFFERS作为参数显示查询正在创建的页面命中。
Buffers:sharedhit=5意味着从PostgreSQL缓存本身获取了五个页面。让我们调整查询以从不同的行偏移。
EXPLAIN (ANALYSE,BUFFERS) SELECT * FROM fake_data LIMIT 10 OFFSET 500
更改OFFSET会导致不同的页面点击次数。
Buffers:sharedhit=7read=5显示5页来自磁盘。该read部分是显示有多少页面来自磁盘的变量,正如已经解释过的hit来自缓存。如果我们再次执行相同的查询(记住ANALYSE运行查询),那么所有数据现在都来自缓存。
再次执行查询意味着缓存现在提供所有结果。
PostgreSQL使用一种称为LRU(最近最少使用)缓存的机制将经常使用的数据存储在内存中。了解缓存的工作原理及其重要性是另一篇文章的主题,但现在我们必须了解的是PostgreSQL具有坚如磐石的缓存机制,我们可以使用EXPLAIN(ANALYSE,BUFFERS)命令查看它是如何工作的。
VERBOSE命令参数
EXPLAIN (ANALYSE,BUFFERS,VERBOSE) SELECT * FROM fake_data LIMIT 10 OFFSET 500
Verbose是另一个提供额外信息的命令参数。
VERBOSE命令参数将为复杂查询提供更多信息。
请注意,Output:id,name,sentence,company是附加的。在复杂的查询计划中,将打印大量其他信息。默认情况下,设置的COSTS和TIMING选项是TRUE,除非您想将它们设置为FALSE,否则无需明确指定它们。
Postgres中的FORMAT解释
PostgreSQL能够以一种很好的格式给出查询计划,例如JSON,这些计划可以以一种语言中立的方式进行解释。
EXPLAIN (ANALYSE,BUFFERS,VERBOSE,FORMAT JSON) SELECT * FROM fake_data LIMIT 10 OFFSET 500
将按JSON格式打印查询计划。您可以通过复制其输出并将其插入到另一个表中来在Arctype中查看此格式,如下面的GIF所示。
将EXPLAINJSON输出插入表并使用JSON视图检查它。
还有各种其他格式:
- Text(默认)
- JSON(上例)
- XML
- YAML
还有另外两个被调用的选项SETTINGS和WAL,它们可以包含在查询计划中,但这些超出了这篇特定文章的范围。
总结一下:
- EXPLAIN是您通常会开始使用的计划类型,并且最常用于生产系统。
- EXPLAIN ANALYSE用于运行查询以及获取查询计划。这是您如何获得计划中的计划时间和执行时间细分以及与执行查询的成本和实际时间的比较。
- EXPLAIN(ANALYSE,BUFFERS)在分析之上使用以获取来自缓存和磁盘的行/页数以及缓存的行为方式。
- EXPLAIN(ANALYSE,BUFFERS,VERBOSE)获取有关查询的详细信息和附加信息。
- EXPLAIN(ANALYSE,BUFFERS,VERBOSE,FORMATJSON)是以特定格式导出的方式;在这个例子中是JSON。
在下一节中,我们将使用这些工具来检查PostgreSQL查询计划的工作原理。为了便于阅读,我们将只查看PostgreSQL查询计划的文本格式。
查询计划的元素
无论复杂性如何,任何查询计划都有一些基本结构。在本节中,我们将重点关注这些结构,这将有助于我们以抽象的方式理解查询计划。
查询的节点
查询计划由节点组成:
EXPLAIN SELECT * FROM fake_data LIMIT 10 OFFSET 500;
节点是执行查询的关键部分。
一个节点可以被认为是数据库执行的一个阶段。节点大多是嵌套的,如上图所示;在Seq Scan它之前和之上完成,然后应用该Limit子句。让我们添加一个Where子句来理解进一步的嵌套。
EXPLAIN SELECT * FROM fake_data where NAME = 'Sandra Smith' LIMIT 10
执行是由内而外发生的。
- 过滤行where name=SandraSmith
- 使用上述过滤器进行顺序扫描
- 在顶部应用限制子句
如您所见,数据库识别出只需要10行,并且一旦达到所需的10行就不会再进行扫描。请注意,我已关闭SETmax_parallel_workers_per_gather=0;以便计划更简单。我们将在后面的文章中探讨并行化。
查询计划器中的成本
成本是数据库查询计划的关键部分,由于它们的表示方式,这些成本很容易被误解。让我们再次看一下带有成本的简单计划。
成本在EXPLAIN输出中表示。
需要注意的几个重要事项是:
- LIMIT子句的启动成本不为零。这是因为启动成本汇总到顶部,您看到的是其下方节点的成本。
- 总成本是一个任意的衡量标准,与计划者的相关性比与用户的相关性更高。在任何实际用例中,您永远不会同时获取整个表数据。
- 众所周知,顺序扫描在估计方面很糟糕,因为数据库不知道如何优化它们。索引可以极大地加速带有WHERE子句的查询。
- Width很重要,因为一行越宽,需要从磁盘获取的数据就越多。这就是为什么遵循数据库表的规范化非常重要。
如果我们实际运行查询,那么成本会更有意义。
数据库计划和执行
计划和执行时间是仅使用EXPLAIN ANALYSE选项获得的指标。
计划和执行是查询执行的2个不同阶段。
Planner(Planning Time)根据各种参数决定查询应该如何运行,Executor(执行时间)运行查询。上面指出的这些参数是抽象的,适用于任何类型的查询。运行时间以毫秒表示。在许多情况下,计划时间和执行时间可能不会很接近,如上所述,计划器可能需要更多时间来计划查询,而执行器需要更少时间,但通常情况并非如此。它们不一定需要彼此匹配,但如果它们偏离很多,那么是时候反省它发生的原因了。
在典型的OLTP系统(如PostgreSQL)中,任何计划和执行的总和应该小于50毫秒,除非它是分析查询/大量写入/已知异常。请记住,OLTP代表在线事务处理。在典型的业务中,交易通常从数千到数百万不等。应始终非常仔细地观察这些执行时间,因为这些较小的成本较高的查询可能汇总起来并增加了巨大的开销。
从这往哪儿走
我们已经涵盖了从查询生命周期到计划器如何做出决策的主题,我故意省略了像节点类型(扫描、排序、连接)这样的主题,因为它们需要专门的文章介绍。本文的目的是广泛了解查询计划器的工作原理、影响其决策的因素以及PostgreSQL提供的工具以更好地理解计划器。
让我们重新审视我们上面提出的问题。
问:为什么我们甚至需要查询计划?
答:“有计划的傻瓜比没有计划的天才要好!”——古老的Arctype谚语。计划对于决定走哪条路是绝对必要的,尤其是当决定是基于统计数据时。
问:计划中具体体现了什么?
A:计划由节点、成本、计划和执行时间组成。节点是查询的基本构建块。成本是节点的基本属性。计划和执行时间以查看实际时间。
问:PostgreSQL不够智能,无法自动优化我的查询吗?我为什么要担心计划器?
A:PostgreSQL实际上非常聪明。随着每个版本的发布,计划器变得越来越好,但没有完全自动化/完美的计划器。这实际上是不切实际的,因为优化可能对一个查询有利,但对另一个查询不利。计划器必须在某处划清界限并提供一致的行为和性能。开发人员/DBA有很多责任编写优化的查询并以更好的方式理解数据库行为。
问:计划器是我唯一需要看的东西吗?
答:绝对不会。还有很多其他的东西,例如应用程序的领域专业知识、表设计和数据库架构等,这些都是非常关键的。但作为开发人员/DBA,理解和提高这些抽象技能对我们的职业生涯极为重要。
有了这些基本知识,我们现在可以自信地阅读任何计划并对正在发生的事情形成一个高层次的想法。查询优化是一个非常广泛的主题,需要了解数据库内部发生的各种事情。在接下来的文章中,我们将看到不同类型的查询及其节点是如何计划和执行的,哪些因素会影响计划器的行为以及我们如何优化它们。
https://www.codeproject.com/Articles/5299479/Anatomy-of-a-PostgreSQL-Query-Plan