PostgreSQL 中的 EXPLAIN ANALYZE 执行了真的 SQL
PostgreSQL 中 EXPLAIN ANALYZE 的真相:它不是“只解释”,而是真的执行
对于许多刚接触 PostgreSQL 的开发者来说,EXPLAIN ANALYZE 是一个强大的调优工具。它能显示实际执行时间和行数,而不是依赖估算。然而,这里有一个必须牢记的核心事实:EXPLAIN ANALYZE 确实会执行你的 SQL 语句。它不是“模拟”运行,而是真实地去操作数据、调用函数、消耗资源。这篇教程将为你揭开这个机制背后的细节、潜在风险以及安全使用的必要技巧。
普通 EXPLAIN 与 带 ANALYZE 的区别
在深入之前,我们先明确这两种命令的根本差异。
EXPLAIN:只生成执行计划,不执行语句。它依赖表统计信息来估算返回的行数和成本。因此,它非常快,且不会对数据库产生任何副作用。EXPLAIN ANALYZE:生成计划并实际执行语句。它除了显示估算成本,还会显示真实的执行时间、实际返回的行数、循环次数等。这些准确数据的代价就是语句被真实运行了一遍。
可以简单地理解:EXPLAIN 是图纸,EXPLAIN ANALYZE 是按图纸真建了一次。
如何证明它真执行了?—— 三个不可逆的证据
如果你还半信半疑,下面这三个实验能让你亲眼看到证据。
证据一:有副作用的操作会真实发生
执行任何会修改数据的语句都能立刻验证。
-- 创建一张测试表
CREATE TABLE test_explain (id SERIAL PRIMARY KEY, value TEXT);
-- 使用 EXPLAIN ANALYZE 执行 INSERT
EXPLAIN ANALYZE INSERT INTO test_explain (value) VALUES ('hello');
你会看到类似这样的输出:
Insert on test_explain (cost=0.00..0.01 rows=1 width=32) (actual time=0.021..0.022 rows=1 loops=1)
...
然后查询表,记录已经真实存在了:
SELECT * FROM test_explain;
同样,UPDATE 会修改数据,DELETE 会移除数据, ALTER 会改变表结构。千万不要在一个生产数据库上对写操作随意使用 EXPLAIN ANALYZE。
证据二:序列(SEQUENCE)的值会被消耗
即使插入被事务回滚,序列值也不会重置。这是验证真执行的一个非常干净的证据。
-- 获取当前序列值
SELECT currval('test_explain_id_seq'); -- 假设序列名为 test_explain_id_seq
-- 如果报错,先用 INSERT 初始化序列,然后立刻删除行。
-- 在一个事务块中使用 EXPLAIN ANALYZE 并回滚
BEGIN;
EXPLAIN ANALYZE INSERT INTO test_explain (value) VALUES ('will be rolled back');
ROLLBACK;
-- 再次查看序列值
SELECT currval('test_explain_id_seq');
你会发现序列值增加了,即使事务回滚了,那个被分配的 ID 也永不会被使用。这证明了 INSERT 语句确实被发送至服务器并执行了。
证据三:volatile 函数会被真实调用
定义一个带有副作用的 volatile 函数,如记录日志或写入文件,就能捕捉到调用痕迹。
CREATE FUNCTION log_analyze_call() RETURNS INTEGER AS $$
BEGIN
RAISE NOTICE 'Hi, I was actually executed!';
RETURN 1;
END;
$$ LANGUAGE plpgsql VOLATILE;
-- 将函数放在 SELECT 语句中
EXPLAIN ANALYZE SELECT log_analyze_call();
输出中会包含 NOTICE 信息,证明函数体确实被执行了。
它会带来哪些实际风险?
知道它会执行后,风险就显而易见了。
-
意外修改生产数据 在一个关键的线上表上执行
EXPLAIN ANALYZE UPDATE ...,会导致数据被不可逆地更改。这是最常见也最危险的操作。 -
触发长事务和锁争用 一个沉重的
SELECT语句被带ANALYZE执行,可能运行几分钟甚至更久,期间持有共享锁。如果此时有其他事务想对这个表加排他锁(例如ALTER TABLE),就会造成锁等待,进而可能阻塞整个系统的写入。 -
消耗大量 I/O 和 CPU 执行全表扫描或复杂的连接,会真实地读取大量数据块到内存,消耗 I/O 带宽和 CPU 时间,影响其他正常查询的性能。
-
造成磁盘空间增长 如果语句包含大事务,生成的 WAL 日志或临时文件可能大量占用磁盘。
如何安全地使用 EXPLAIN ANALYZE?
绝对不要把 EXPLAIN ANALYZE 当作无害的“查看器”。遵循这些实践,你才能避开地雷。
规则一:永远包裹在事务中并立即回滚
对于任何可能修改数据或状态的语句,这是铁律。你获得执行统计信息,同时一切恢复原样(除了序列值的递增,这通常可接受)。
BEGIN;
EXPLAIN ANALYZE DELETE FROM orders WHERE created_at < now() - interval '1 year';
ROLLBACK;
这样,计划输出会打印在屏幕上,但删除操作被撤销。这让你能安全地分析一个重量级 DELETE 的实际执行时间。
规则二:在只读的从库(Standby)上执行
对于单纯的 SELECT 查询,如果担心性能影响,最佳位置是搭建的只读副本。即使在从库上运行一小时,也不会阻塞主库的写入。但注意,从库上的数据统计信息可能与主库略有差异,可能导致计划不同。
规则三:利用 EXPLAIN 的扩展选项降低开销
PostgreSQL 提供了精确控制 ANALYZE 行为的选项。你可以在获取关键信息的同时,把执行开销降到最低。
TIMING OFF:关闭每步操作的计时。这能极大减少对系统时钟的调用开销,尤其当查询有很多琐碎的子操作时。你仍会得到“实际返回行数”,但得不到毫秒级的精确时间。EXPLAIN (ANALYZE, TIMING OFF) SELECT * FROM large_table;COSTS OFF:不在输出中显示估算成本和行数。这能让输出更简洁,同时可能减少一点点计划阶段的 CPU 开销。BUFFERS:显示缓冲区使用情况(命中、读取、写入等)。这个选项本身不改变执行的真实性,但提供了关键的缓存效率指标,且额外开销很小。SETTINGS:显示影响查询计划的配置参数,同样有助于诊断,且基本无额外开销。
一个常用组合是:
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS) SELECT ... ;
这能获得实际行数和 I/O 情况,同时避免高频计时带来的性能干扰,特别适合分析执行时间在几百毫秒左右,但内部操作无数次的查询。
规则四:从简单的 EXPLAIN 开始
首先运行一个普通的 EXPLAIN。检查其计划是否合理,是否使用了预期的索引。如果发现潜在问题(如误用了全表扫描),先尝试修复统计信息或查询写法,再用 ANALYZE 验证效果。这能避免在错误路径上浪费执行时间。
特殊情况:当执行本身是目的时
在某些极其罕见的情况下,你确实需要让副作用发生,但同时想看到执行统计。例如,你希望执行一个数据清理过程,并记录其每一步的真实耗时。这时,你根本不需要事务回滚。但要明确:你是在“执行并测量”,而不是“分析而不影响”。
总结
EXPLAIN ANALYZE 是 PostgreSQL 性能调优的终极武器,但它是一把双刃剑。牢记它“真执行”的本质,并始终:
- 写操作必用事务包裹并回滚。
- 只读查询也是真实消耗资源,优先在从库或非高峰期运行。
- 善用
TIMING OFF等选项降低测量开销。 - 生产环境必须三思而后行。
当你掌握了这些指南,EXPLAIN ANALYZE 就会从一个危险的未知按钮,变成你最锋利的性能手术刀。