MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

PostgreSQL ENABLE_*参数在查询优化中的作用

2021-03-205.3k 阅读

PostgreSQL 中的 ENABLE_* 参数概述

PostgreSQL 提供了一系列以 ENABLE_* 开头的参数,这些参数在查询优化过程中扮演着至关重要的角色。它们允许数据库管理员和开发人员对查询优化器的行为进行精细控制,从而影响查询执行计划的生成。这些参数本质上是开关,通过启用或禁用特定的查询优化策略,帮助我们根据不同的应用场景和数据特点来优化查询性能。

例如,某些查询在特定的硬件环境或数据分布下,使用某些优化策略可能会导致性能下降,这时通过调整 ENABLE_* 参数可以禁用那些不适用的策略,使查询优化器生成更高效的执行计划。

常见的 ENABLE_* 参数及其作用

ENABLE_NESTLOOP

  1. 作用本质:这个参数控制查询优化器是否使用嵌套循环连接(Nested Loop Join)策略。嵌套循环连接是一种基本的连接算法,它通过对两个表进行嵌套循环来匹配符合连接条件的行。对于较小的数据集,嵌套循环连接可能是非常高效的,因为它不需要对数据进行排序或额外的复杂操作。然而,当数据集较大时,嵌套循环连接可能会导致大量的磁盘 I/O 和 CPU 开销,因为它需要对每个外层行都遍历内层表。
  2. 启用与禁用情况
    • 启用(默认):当 ENABLE_NESTLOOPon(默认值)时,查询优化器在生成执行计划时会考虑使用嵌套循环连接。如果查询涉及的表较小,或者表之间存在索引可以快速定位内层表的匹配行,这种连接方式可能会被选中,从而快速返回结果。
    • 禁用:如果将 ENABLE_NESTLOOP 设置为 off,查询优化器在生成执行计划时将不会考虑嵌套循环连接策略。这在某些情况下是有用的,比如已知嵌套循环连接对于当前查询会导致性能问题,而其他连接策略(如哈希连接或排序合并连接)可能更适合。
  3. 代码示例
    • 创建两个示例表:
CREATE TABLE orders (
    order_id serial PRIMARY KEY,
    customer_id int,
    order_date date
);

CREATE TABLE customers (
    customer_id serial PRIMARY KEY,
    customer_name varchar(100)
);
  • 插入一些测试数据:
INSERT INTO customers (customer_name) VALUES ('Alice'), ('Bob'), ('Charlie');
INSERT INTO orders (customer_id, order_date) VALUES (1, '2023 - 01 - 01'), (2, '2023 - 01 - 02'), (1, '2023 - 01 - 03');
  • 查看默认启用 ENABLE_NESTLOOP 时的查询计划:
EXPLAIN SELECT * FROM orders JOIN customers ON orders.customer_id = customers.customer_id;
  • 禁用 ENABLE_NESTLOOP 并查看查询计划:
SET ENABLE_NESTLOOP = off;
EXPLAIN SELECT * FROM orders JOIN customers ON orders.customer_id = customers.customer_id;

通过对比这两个查询计划,可以看到禁用 ENABLE_NESTLOOP 后,查询优化器会尝试使用其他连接策略。

ENABLE_MERGEJOIN

  1. 作用本质ENABLE_MERGEJOIN 参数控制查询优化器是否使用排序合并连接(Merge Join)策略。排序合并连接要求参与连接的两个表(或子查询结果)已经按照连接列排序。这种连接方式通过同时扫描两个已排序的数据集,按照连接条件匹配行。它的优点是在处理较大数据集时,避免了嵌套循环连接可能产生的大量随机 I/O。缺点是需要额外的排序操作,如果表本身没有按连接列排序,那么排序的开销可能较大。
  2. 启用与禁用情况
    • 启用(默认):默认情况下 ENABLE_MERGEJOINon,查询优化器会评估是否可以使用排序合并连接。如果表已经按连接列排序,或者排序的开销相对较小,并且数据量较大时,排序合并连接可能会被选中,因为它可以有效地减少 I/O 操作。
    • 禁用:将 ENABLE_MERGEJOIN 设置为 off 时,查询优化器在生成执行计划时不会考虑排序合并连接策略。这在表没有按连接列排序且排序开销预计很大的情况下,或者已知其他连接策略更适合时,可能是一个合理的选择。
  3. 代码示例
    • 为了演示排序合并连接,先确保表按连接列有合适的索引(如果没有排序的话,排序合并连接可能不会被使用):
CREATE INDEX idx_customers_customer_id ON customers (customer_id);
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
  • 查看默认启用 ENABLE_MERGEJOIN 时的查询计划:
EXPLAIN SELECT * FROM orders JOIN customers ON orders.customer_id = customers.customer_id;
  • 禁用 ENABLE_MERGEJOIN 并查看查询计划:
SET ENABLE_MERGEJOIN = off;
EXPLAIN SELECT * FROM orders JOIN customers ON orders.customer_id = customers.customer_id;

观察查询计划的变化,可以看到禁用该参数后,查询优化器可能会选择其他连接策略。

ENABLE_HASHJOIN

  1. 作用本质ENABLE_HASHJOIN 参数控制查询优化器是否使用哈希连接(Hash Join)策略。哈希连接是一种高效的连接算法,特别适用于处理较大数据集。它的工作原理是先扫描其中一个表(通常是较小的表,称为构建表),并在内存中构建一个哈希表。然后扫描另一个表(探测表),对于探测表中的每一行,通过哈希函数快速查找哈希表中符合连接条件的行。哈希连接在数据量较大且内存充足的情况下表现出色,因为它可以通过一次扫描构建哈希表,然后通过哈希查找快速匹配行,减少了 I/O 操作。
  2. 启用与禁用情况
    • 启用(默认):默认 ENABLE_HASHJOINon,查询优化器会在合适的情况下考虑使用哈希连接。当有一个较小的表可以在内存中构建哈希表,并且另一个表可以通过哈希查找高效匹配时,哈希连接很可能会被选中。
    • 禁用:将 ENABLE_HASHJOIN 设置为 off 时,查询优化器在生成执行计划时将排除哈希连接策略。这在内存不足,无法构建足够大的哈希表,或者已知哈希连接在当前查询场景下性能不佳时可能是必要的。
  3. 代码示例
    • 查看默认启用 ENABLE_HASHJOIN 时的查询计划:
EXPLAIN SELECT * FROM orders JOIN customers ON orders.customer_id = customers.customer_id;
  • 禁用 ENABLE_HASHJOIN 并查看查询计划:
SET ENABLE_HASHJOIN = off;
EXPLAIN SELECT * FROM orders JOIN customers ON orders.customer_id = customers.customer_id;

对比两个查询计划,分析禁用 ENABLE_HASHJOIN 后查询优化器选择的替代连接策略。

ENABLE_BITMAPSCAN

  1. 作用本质ENABLE_BITMAPSCAN 参数控制查询优化器是否使用位图扫描(Bitmap Scan)。位图扫描是一种用于处理多条件查询的优化技术,特别是当查询涉及多个索引时。它通过对每个索引进行单独扫描,生成一个位图,位图中的每一位对应表中的一行,表示该行是否满足相应索引条件。然后通过逻辑操作(如 AND、OR)合并这些位图,得到最终满足所有条件的行的位图,最后根据位图访问实际的表数据。位图扫描可以有效地减少磁盘 I/O,因为它可以通过索引快速定位满足部分条件的行,然后通过位图操作高效地组合这些结果。
  2. 启用与禁用情况
    • 启用(默认):默认情况下 ENABLE_BITMAPSCANon,查询优化器会在查询涉及多个索引且适合使用位图扫描的情况下考虑这种策略。例如,当查询条件涉及多个独立的索引列,并且这些索引的选择性较好时,位图扫描可以显著提高查询性能。
    • 禁用:将 ENABLE_BITMAPSCAN 设置为 off 时,查询优化器在生成执行计划时不会使用位图扫描策略。这在索引结构不适合位图扫描,或者使用其他扫描方式(如顺序扫描或索引扫描)更高效时可能是合适的。
  3. 代码示例
    • 假设我们有一个表 products 有多个索引:
CREATE TABLE products (
    product_id serial PRIMARY KEY,
    product_name varchar(100),
    price decimal(10, 2),
    category_id int
);

CREATE INDEX idx_products_product_name ON products (product_name);
CREATE INDEX idx_products_price ON products (price);
CREATE INDEX idx_products_category_id ON products (category_id);
  • 执行一个涉及多个索引条件的查询并查看默认启用 ENABLE_BITMAPSCAN 时的查询计划:
EXPLAIN SELECT * FROM products WHERE product_name LIKE 'A%' AND price > 100 AND category_id = 5;
  • 禁用 ENABLE_BITMAPSCAN 并查看查询计划:
SET ENABLE_BITMAPSCAN = off;
EXPLAIN SELECT * FROM products WHERE product_name LIKE 'A%' AND price > 100 AND category_id = 5;

观察查询计划的变化,了解禁用位图扫描后查询优化器的替代策略。

ENABLE_INDEXSCAN

  1. 作用本质ENABLE_INDEXSCAN 参数控制查询优化器是否使用索引扫描。索引扫描是通过索引来定位表中满足查询条件的行。索引提供了一种快速访问表数据的方式,特别是当查询条件可以直接映射到索引列时。根据索引的类型(如 B - tree、Hash 等)和查询条件的不同,索引扫描可以高效地减少需要扫描的行数,从而提高查询性能。
  2. 启用与禁用情况
    • 启用(默认):默认 ENABLE_INDEXSCANon,查询优化器会在合适的情况下优先考虑使用索引扫描。如果查询条件有合适的索引支持,并且索引的选择性较好(即索引可以快速定位到少量满足条件的行),索引扫描通常会被选中。
    • 禁用:将 ENABLE_INDEXSCAN 设置为 off 时,查询优化器在生成执行计划时将不考虑索引扫描策略。这在索引维护成本高,或者已知顺序扫描(全表扫描)在当前情况下更高效时可能是合理的。例如,当表非常小,或者查询条件使得索引的选择性很差,索引扫描的开销可能大于全表扫描。
  3. 代码示例
    • products 表为例,查看默认启用 ENABLE_INDEXSCAN 时的查询计划:
EXPLAIN SELECT * FROM products WHERE product_id = 10;
  • 禁用 ENABLE_INDEXSCAN 并查看查询计划:
SET ENABLE_INDEXSCAN = off;
EXPLAIN SELECT * FROM products WHERE product_id = 10;

对比两个查询计划,观察禁用索引扫描后查询优化器如何选择其他方式来获取数据。

ENABLE_SEQSCAN

  1. 作用本质ENABLE_SEQSCAN 参数控制查询优化器是否使用顺序扫描(全表扫描)。顺序扫描是最简单的扫描方式,它从表的开头开始,依次读取每一行数据,直到表的末尾。虽然顺序扫描在处理大数据集时通常效率较低,因为它需要读取整个表的数据,但在某些情况下,它可能是最优的选择,比如表非常小,或者查询条件无法利用索引,或者索引的维护成本过高。
  2. 启用与禁用情况
    • 启用(默认):默认 ENABLE_SEQSCANon,查询优化器会在评估各种扫描策略后,如果认为顺序扫描是最合适的,就会选择它。例如,当表没有合适的索引,或者查询条件使得索引的选择性很差时,顺序扫描可能会被选中。
    • 禁用:将 ENABLE_SEQSCAN 设置为 off 时,查询优化器在生成执行计划时将不会考虑顺序扫描策略。这在不希望进行全表扫描,或者希望强制查询优化器寻找其他更高效的扫描方式(如索引扫描)时可能是有用的。
  3. 代码示例
    • 查看默认启用 ENABLE_SEQSCAN 时的查询计划:
EXPLAIN SELECT * FROM products;
  • 禁用 ENABLE_SEQSCAN 并查看查询计划(如果表没有合适的索引,此时可能会报错,因为没有扫描方式可用):
SET ENABLE_SEQSCAN = off;
EXPLAIN SELECT * FROM products;

通过这个示例可以看到禁用顺序扫描对查询计划生成的影响。

根据不同场景调整 ENABLE_* 参数

小数据量场景

  1. 连接策略:在小数据量场景下,嵌套循环连接可能是非常有效的。因为表数据量小,嵌套循环连接不需要进行复杂的排序或哈希构建操作,直接通过简单的嵌套循环就可以快速完成连接。此时,确保 ENABLE_NESTLOOP 是启用的(默认情况)。例如,在一个小型的企业应用中,某些配置表之间的连接,这些表的数据量可能只有几十条到几百条记录,嵌套循环连接可以快速返回结果。
  2. 扫描策略:对于小表的查询,顺序扫描可能并不一定是低效的。如果表非常小,索引扫描的开销(如索引查找和维护)可能大于直接全表扫描的开销。在这种情况下,即使有索引,查询优化器也可能选择顺序扫描。然而,如果查询条件可以精确匹配索引,索引扫描仍然是一个不错的选择。可以根据实际情况,通过调整 ENABLE_SEQSCANENABLE_INDEXSCAN 参数来影响查询优化器的决策。比如,对于一个只有 100 条记录的配置表,查询特定的一条记录,如果有合适的索引,启用 ENABLE_INDEXSCAN 可能会更快;但如果查询所有记录,启用 ENABLE_SEQSCAN 可能就足够了。

大数据量场景

  1. 连接策略:对于大数据量场景,哈希连接和排序合并连接通常更适合。哈希连接在内存充足的情况下,可以通过快速的哈希查找来完成连接操作,减少 I/O 开销。排序合并连接在表已经按连接列排序或排序开销可接受的情况下,也能高效地处理大数据集。因此,在大数据量场景下,要确保 ENABLE_HASHJOINENABLE_MERGEJOIN 是启用的。例如,在一个电商数据库中,订单表和客户表可能都有大量的数据,使用哈希连接或排序合并连接可以更高效地处理它们之间的连接查询。
  2. 扫描策略:当处理大数据量时,位图扫描在多条件查询且有合适索引的情况下非常有用。通过启用 ENABLE_BITMAPSCAN,查询优化器可以利用多个索引的信息,通过位图操作高效地定位满足所有条件的行。同时,对于单条件查询,如果有选择性好的索引,索引扫描也是首选,要确保 ENABLE_INDEXSCAN 启用。例如,在一个包含百万条商品记录的数据库中,查询价格在某个范围内且属于特定类别的商品,位图扫描可以结合价格索引和类别索引快速定位数据;而如果只是查询特定价格的商品,索引扫描会更合适。

复杂查询场景

  1. 连接策略调整:在复杂查询中,可能涉及多个表的连接,以及不同类型的连接条件。此时,需要综合考虑各种连接策略的适用性。例如,如果查询涉及多个表的级联连接,可能需要根据表的大小、数据分布以及连接条件来决定是否启用或禁用特定的连接策略参数。假设一个复杂查询涉及 A、B、C 三个表的连接,A 表和 B 表连接后再与 C 表连接,如果 A 表和 B 表较小,嵌套循环连接可能适合它们之间的连接,而 A、B 连接结果与 C 表连接时,如果 C 表数据量较大,哈希连接或排序合并连接可能更合适。通过调整 ENABLE_NESTLOOPENABLE_HASHJOINENABLE_MERGEJOIN 参数来引导查询优化器生成更合理的执行计划。
  2. 扫描策略配合:复杂查询往往也包含多个条件,这就需要合理利用扫描策略。如果查询条件涉及多个索引列,启用 ENABLE_BITMAPSCAN 可以有效地利用这些索引进行多条件筛选。同时,对于一些子查询或中间结果集的扫描,也需要根据数据量和查询条件来调整 ENABLE_INDEXSCANENABLE_SEQSCAN 参数。例如,在一个复杂的数据分析查询中,可能需要对多个维度的数据进行筛选和聚合,此时正确调整扫描策略参数可以显著提高查询性能。

性能测试与 ENABLE_* 参数调优

  1. 性能测试工具:为了准确评估调整 ENABLE_* 参数对查询性能的影响,需要使用性能测试工具。在 PostgreSQL 中,可以使用 EXPLAIN ANALYZE 命令。这个命令不仅会显示查询的执行计划,还会实际执行查询并返回每个操作的执行时间统计信息。例如:
EXPLAIN ANALYZE SELECT * FROM orders JOIN customers ON orders.customer_id = customers.customer_id;

通过多次执行 EXPLAIN ANALYZE,并在每次执行前调整不同的 ENABLE_* 参数,可以比较不同参数设置下的查询执行时间,从而找到最优的参数配置。 2. 调优步骤

  • 确定测试查询:首先要确定需要优化的关键查询。这些查询通常是在应用程序中执行频率较高,或者对性能影响较大的查询。例如,在一个电商应用中,查询热门商品的查询可能就是一个关键查询。
  • 基准测试:在不改变任何 ENABLE_* 参数的情况下,使用 EXPLAIN ANALYZE 对测试查询进行基准测试,记录下执行时间和执行计划。这将作为后续比较的基础。
  • 逐个调整参数:开始逐个调整 ENABLE_* 参数,每次调整一个参数,然后再次使用 EXPLAIN ANALYZE 测试查询。观察执行时间和执行计划的变化。如果执行时间缩短,说明当前参数调整是有益的;如果执行时间变长,则恢复该参数并尝试调整下一个参数。
  • 综合调整:在逐个调整参数找到一些有益的参数设置后,可以尝试综合调整多个参数,看是否能进一步优化查询性能。例如,同时调整连接策略参数和扫描策略参数,观察它们之间的协同效应。
  • 验证与固化:在找到最优的参数配置后,需要在不同的数据集和负载条件下进行验证,确保该配置在各种情况下都能保持良好的性能。一旦验证通过,可以将这些参数配置固化到数据库的配置文件中,确保在数据库重启等情况下参数设置仍然生效。

注意事项

  1. 参数相互影响ENABLE_* 参数之间并不是孤立的,它们相互影响。例如,禁用 ENABLE_NESTLOOP 可能会导致查询优化器更倾向于选择哈希连接或排序合并连接,这又可能影响到对 ENABLE_HASHJOINENABLE_MERGEJOIN 参数设置的效果。在调整参数时,需要综合考虑它们之间的相互关系,不能只关注单个参数的变化。
  2. 数据分布变化:数据库中的数据分布可能会随着时间发生变化,这也会影响 ENABLE_* 参数的最佳设置。例如,一个原本较小的表随着业务的发展可能会变得非常大,此时之前适合小数据量的参数设置(如启用嵌套循环连接)可能不再适用,需要重新评估和调整参数。因此,定期对数据库进行性能评估和参数调优是很有必要的。
  3. 系统资源限制:某些查询优化策略,如哈希连接需要足够的内存来构建哈希表。如果系统内存不足,即使启用了 ENABLE_HASHJOIN,查询优化器可能也无法有效地使用哈希连接策略,甚至可能导致性能下降。在调整 ENABLE_* 参数时,需要考虑系统的资源限制,确保所选择的优化策略在当前系统资源条件下是可行的。
  4. 版本兼容性:不同版本的 PostgreSQL 对 ENABLE_* 参数的实现和默认值可能会有所不同。在升级数据库版本时,需要重新评估 ENABLE_* 参数的设置,以确保查询性能不受影响。同时,一些新的版本可能会引入新的优化策略或对现有策略进行改进,这也需要根据新版本的特性来调整参数。