1. 项目概述:为什么读懂EXPLAIN是 PostgreSQL 性能优化的“第一道门”
在某次线上服务响应延迟突增的紧急排查中,我盯着监控面板上那条持续 800ms 的慢查询日志,心里清楚——这绝不是加个索引就能解决的表面问题。真正让我停下手头所有事、泡了三杯浓茶反复推敲的,是它执行计划里那行刺眼的"Seq Scan on orders"。没错,就是全表扫描。这张表当时已有 230 万行数据,而查询条件明明只针对status = 'shipped'这一个字段。后来发现,是开发同事在添加新索引时,误把status字段放在了复合索引的第二位,而查询中又没带上第一位字段,导致索引完全失效。这个教训让我彻底明白:在 PostgreSQL 世界里,不看EXPLAIN的优化,就像蒙着眼睛修车——你可能拧紧了所有螺丝,但发动机根本没点火。EXPLAIN不是命令行里的一个装饰性开关,它是数据库内核向你发出的、关于“它打算怎么干活”的最诚实白皮书。它告诉你,这条 SQL 是准备用索引快速定位,还是老老实实从第一行翻到最后一行;是打算把两张表先各自排序再合并(Merge Join),还是把小表加载进内存,挨个去大表里找匹配项(Hash Join);甚至会坦白地告诉你,它预估要读取多少行数据,而实际返回了多少行——这个“预估 vs 实际”的巨大偏差,往往是隐藏性能黑洞的最可靠路标。对 DBA、后端工程师、数据分析师,甚至是写复杂报表的业务同学来说,EXPLAIN都是绕不开的核心技能。它不挑人,但极其挑剔态度:你得愿意花十分钟,逐行解读那些缩进、那些关键词、那些看似枯燥的数字。这篇内容,就是为你拆解EXPLAIN这本“白皮书”的阅读指南。它不会教你如何写 SQL,而是教会你如何听懂数据库自己讲出的故事。无论你是刚接触psql命令行的新手,还是已经能熟练写窗口函数的老手,只要你的工作涉及“让查询跑得更快”,那么接下来的内容,就是你今天最该花时间读完的。
2. 核心思路拆解:EXPLAIN不是单个命令,而是一套三层诊断体系
很多人第一次用EXPLAIN,就是简单敲下EXPLAIN SELECT * FROM users WHERE id = 123;,看到一串带缩进的文本就懵了。其实,EXPLAIN本身是一个“框架”,它底下藏着三套不同深度的诊断工具,它们不是并列关系,而是层层递进的“望远镜”。理解这个结构,是避免被信息淹没的第一步。
2.1 第一层:EXPLAIN—— 看“它想怎么做”(执行计划树)
这是最基础也最核心的一层。它输出的是一个逻辑执行计划树,用缩进来表示操作的嵌套关系。你可以把它想象成数据库给查询画的一张“施工蓝图”。比如:
EXPLAIN SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE u.status = 'active';它的输出开头可能是:
Hash Join (cost=123.45..678.90 rows=500 width=40) Hash Cond: (o.user_id = u.id) -> Seq Scan on orders o (cost=0.00..400.00 rows=10000 width=24) -> Hash (cost=100.00..100.00 rows=1000 width=16) -> Index Scan using idx_users_status on users u (cost=0.42..100.00 rows=1000 width=16) Index Cond: (status = 'active'::text)这里的关键在于缩进层级:Hash Join是顶层操作,它需要两个输入,分别是-> Seq Scan on orders和-> Hash。而Hash操作本身又依赖于一个Index Scan。这个树状结构清晰地告诉了你:数据库的“施工顺序”是先用索引快速找出所有活跃用户(1000行),再为这些用户ID建一个哈希表,最后拿着这个哈希表去订单表里做一次高效的查找。这一层的价值,在于帮你确认“设计意图”是否与你的预期一致。如果你本意是让数据库先查用户再关联订单,结果它却反着来,或者用了全表扫描,那问题就出在查询逻辑或索引设计上,而不是后续的参数调优。
2.2 第二层:EXPLAIN (ANALYZE)—— 看“它实际做了什么”(执行计划+真实耗时)
光看蓝图不够,还得看工地上的实际进度。EXPLAIN (ANALYZE)就是那个带着秒表和计数器的监工。它不仅输出计划树,还会真实执行这条 SQL,并记录下每个节点的实际耗时、实际返回行数、实际循环次数等关键指标。这才是性能分析的黄金标准。上面那个例子,加上(ANALYZE)后,输出会变成:
Hash Join (cost=123.45..678.90 rows=500 width=40) (actual time=2.123..15.678 rows=482 loops=1) Hash Cond: (o.user_id = u.id) -> Seq Scan on orders o (cost=0.00..400.00 rows=10000 width=24) (actual time=0.012..8.456 rows=9876 loops=1) -> Hash (cost=100.00..100.00 rows=1000 width=16) (actual time=1.234..1.234 rows=1000 loops=1) -> Index Scan using idx_users_status on users u (cost=0.42..100.00 rows=1000 width=16) (actual time=0.023..0.891 rows=1000 loops=1) Index Cond: (status = 'active'::text)注意多了(actual time=...)和(rows=...)这些括号。actual time是毫秒级的真实耗时,rows是真实返回的行数。最关键的对比点,是cost估算值和actual time真实值之间的关系。如果某个节点的cost是 100,但actual time是 5000,说明数据库的估算模型严重失准,这往往指向统计信息过期(ANALYZE命令没及时运行)。更致命的是rows的对比:如果Index Scan节点预估rows=1000,但实际rows=100000,那就意味着数据库以为status='active'只有千分之一的数据,结果发现是十分之一——这个十倍的偏差,会让它错误地选择了一个本该用于小数据集的算法(比如 Nested Loop),从而引发灾难性性能下降。EXPLAIN (ANALYZE)的代价是它会真实执行查询,所以对写操作(INSERT/UPDATE/DELETE)或大数据量查询要格外谨慎,最好在测试库或低峰期使用。
2.3 第三层:EXPLAIN (ANALYZE, BUFFERS)—— 看“它动用了哪些资源”(内存与磁盘IO)
当ANALYZE层级的耗时分析还不够深,你需要知道数据库到底“累不累”时,BUFFERS就是终极显微镜。它会显示每个操作节点所消耗的共享缓冲区(shared buffers)读写次数、本地缓冲区(local buffers)以及临时缓冲区(temp buffers)的使用情况。这对于诊断 IO 密集型瓶颈至关重要。继续上面的例子,加上BUFFERS:
Hash Join (cost=123.45..678.90 rows=500 width=40) (actual time=2.123..15.678 rows=482 loops=1) Buffers: shared hit=1234 read=567 -> Seq Scan on orders o (cost=0.00..400.00 rows=10000 width=24) (actual time=0.012..8.456 rows=9876 loops=1) Buffers: shared hit=10 read=567 -> Hash (cost=100.00..100.00 rows=1000 width=16) (actual time=1.234..1.234 rows=1000 loops=1) Buffers: shared hit=1224 -> Index Scan using idx_users_status on users u (cost=0.42..100.00 rows=1000 width=16) (actual time=0.023..0.891 rows=1000 loops=1) Buffers: shared hit=1224 Index Cond: (status = 'active'::text)这里的shared hit=1234 read=567意味着总共访问了 1801 个数据页,其中 1234 页是从内存(shared buffers)里直接找到的(命中),567 页是不得不从磁盘读取的(未命中)。一个健康的查询,read数应该远小于hit数。如果read占比过高,说明你的shared_buffers配置可能太小,或者数据集太大,无法被有效缓存。而temp buffers的高读写,则直接指向了排序(Sort)或哈希(Hash)操作溢出到了磁盘,这是性能杀手,必须通过增加work_mem或优化查询逻辑来避免。这三层体系,构成了一个完整的诊断闭环:EXPLAIN看设计,EXPLAIN (ANALYZE)看执行,EXPLAIN (ANALYZE, BUFFERS)看资源。跳过任何一层,都可能导致你对着错误的方向猛踩油门。
3. 核心细节解析:读懂执行计划里的“黑话”与关键指标
执行计划的输出里充满了各种专业术语和数字,它们不是随意堆砌的,每一个都有其明确的物理含义。下面我将逐个拆解那些最常出现、也最容易被误解的关键词,并告诉你它们背后的真实故事。
3.1 “Cost” 成本值:不是时间,而是数据库的“工作量估算”
这是新手最容易掉进的坑。cost=123.45..678.90里的数字,绝对不等于毫秒或秒。它是一个抽象的、相对的成本单位,由 PostgreSQL 的查询规划器根据一系列内部参数计算得出。它的核心思想是:1.0的成本,约等于一次顺序磁盘页面读取的开销。而一次随机磁盘页面读取的成本,默认是4.0(这个值可以通过random_page_cost参数调整)。所以,当你看到cost=100.0,它大致意味着规划器认为这个操作的工作量,相当于读取 100 个连续的磁盘页面。..左右的两个数字,分别代表“启动成本”(Startup Cost)和“总成本”(Total Cost)。启动成本是指在返回第一行结果之前,数据库需要做的准备工作耗时,比如打开文件、初始化哈希表等。总成本则是整个操作完成所需的全部估算成本。为什么这个估算很重要?因为规划器的所有决策,都是基于最小化“总成本”来做出的。它会尝试所有可能的执行路径(比如用索引扫描 vs 全表扫描,用 Hash Join vs Merge Join),然后选择那个总成本最低的方案。所以,当你发现一个明显低效的计划被选中时,首要怀疑对象就是成本估算本身是否准确——而这通常指向了过时的统计信息。
3.2 “Rows” 行数:预估与实际的“信任危机”
rows=500和rows=482这两个数字,是规划器与现实世界之间最脆弱的纽带。rows的预估值,完全依赖于pg_statistic系统表里的统计信息。这些信息是通过ANALYZE命令采集的,它会对表进行采样,生成列值的分布直方图、最常见的值(MCV)列表等。一旦统计信息过期,rows的预估就会像脱缰野马。举个极端例子:一张用户表,gender字段原本是男女各半,但最近上线了一个只面向女性用户的活动,导致gender='female'的数据激增到 95%。如果ANALYZE没有及时运行,规划器仍会按 50% 来估算,它可能会为一个WHERE gender='female'的查询选择 Nested Loop Join,因为它以为只会处理 500 行;而实际上,它要处理 9500 行,导致性能雪崩。因此,“预估行数 vs 实际行数”的偏差,是诊断一切性能问题的起点。一个经验法则是:如果偏差超过 5 倍,就必须立即ANALYZE相关表。
3.3 “Width” 宽度:数据“体积”决定内存消耗
width=40表示规划器估算出,这个操作节点输出的每一行数据,平均占用 40 字节的存储空间。这个数字直接影响内存相关的决策。比如,Hash Join需要把小表的数据全部加载进内存构建哈希表。如果width很大,而work_mem设置又很小,哈希表就很容易溢出到磁盘,产生temp buffers的读写,性能断崖式下跌。所以,当你看到一个Hash节点的width异常高,而actual time也异常长时,第一反应应该是检查work_mem是否足够,或者考虑是否可以精简SELECT的字段,避免SELECT *。
3.4 关键操作节点详解:从“做什么”到“为什么这么做”
执行计划里的每个节点名称,都代表了一种特定的算法。理解它们的适用场景,是读懂计划书的灵魂。
Seq Scan(顺序扫描):最朴素的算法,从表的第一行开始,一行一行往后读,直到找到所有满足条件的行。它的成本是
O(N),即与表的总行数成正比。何时合理?当你要查询的行数占全表比例很大(比如 > 15%-20%)时,顺序扫描反而比索引扫描更快,因为后者需要多次随机 IO 去磁盘上找数据页,而顺序扫描是连续的,效率更高。何时危险?当rows预估很小(比如 10),但实际执行却是Seq Scan,并且actual time很长,这几乎 100% 意味着缺少合适的索引。Index Scan(索引扫描):利用 B-Tree 索引,快速定位到满足
Index Cond(索引条件)的索引项,然后回表(Heap Fetch)去主数据页里取出完整行。它的成本是O(log N) + O(K),其中 K 是匹配的行数。关键点在于Index Cond。它必须是索引的最左前缀。例如,你有一个复合索引(status, created_at),那么WHERE status = 'active'可以用上,但WHERE created_at > '2023-01-01'就完全用不上,因为created_at不是索引的最左列。Index Only Scan(仅索引扫描):这是
Index Scan的“豪华版”。当SELECT的所有字段,以及WHERE的所有条件,都能被同一个索引完全覆盖时,数据库就无需回表,直接从索引页里把数据读出来。这能极大减少 IO。判断依据是执行计划里会出现Heap Fetches: 0。要实现它,除了索引要覆盖所有字段,还需要表的visibility map是最新的(即VACUUM要及时),否则数据库无法确定索引项指向的行是否对当前事务可见,就只能老老实实回表。Bitmap Heap Scan + Bitmap Index Scan:这是 PostgreSQL 处理“多条件 AND 查询”的常用组合。
Bitmap Index Scan会为每个条件(如status='active'和age > 25)分别生成一个位图(bitmap),标记出哪些行满足该条件;然后Bitmap Heap Scan将这些位图进行AND运算,得到最终的行号集合,再一次性批量读取这些行。它的优势在于,对于多个独立条件,它比多次Index Scan再Nested Loop更高效,因为位图运算是内存中的位操作,非常快。Hash Join:适用于一个表小、一个表大的连接场景。它先把小表(Build Input)的所有连接键(如
user_id)加载进内存,构建成一个哈希表;然后遍历大表(Probe Input),对每一行的连接键计算哈希值,去哈希表里查找匹配项。它的致命弱点是内存。如果小表太大,超出了work_mem的限制,哈希表就会溢出到磁盘,性能暴跌。所以,当你看到Hash Join且BUFFERS显示大量temp读写时,work_mem就是第一个要调的参数。Nested Loop Join:最直观的算法,外层循环遍历一个表的每一行,内层循环对另一表进行全表扫描(或索引扫描)来寻找匹配项。它的成本是
O(M*N),在大数据集上是灾难性的。但它有一个巨大优势:启动成本极低。如果外层表只有几行,而内层表有索引,那么它几乎是最快的。规划器在rows预估很小时,会优先选择它。Merge Join:要求两个输入表都已按连接键排序。它像归并排序一样,用两个指针分别遍历两个有序序列,比较指针所指的值,相等则输出,小的则移动指针。它的成本是
O(M+N),非常稳定。但前提是“已排序”,这意味着要么表本身有合适的索引,要么规划器需要额外加一个Sort节点,而Sort本身又可能触发磁盘溢出。
4. 实操过程:从一条慢查询到性能提升的完整复现
现在,让我们把前面所有的理论,放进一个真实的、可复现的案例里。我会模拟一个典型的电商后台慢查询场景,并一步步带你走完从发现问题、分析计划、定位根因到最终优化的全过程。所有 SQL 和步骤,你都可以在自己的 PostgreSQL 14+ 环境中直接运行。
4.1 场景搭建:构造一个“有问题”的测试环境
首先,我们创建一张模拟的订单表,并填充一些有代表性的数据。
-- 创建表 CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, product_id INTEGER NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', amount NUMERIC(10,2) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 插入 100 万行测试数据 -- (这里用 generate_series 快速生成) INSERT INTO orders (user_id, product_id, status, amount, created_at) SELECT (random() * 10000)::INTEGER + 1, (random() * 500)::INTEGER + 1, CASE (random() * 3)::INTEGER WHEN 0 THEN 'pending' WHEN 1 THEN 'shipped' WHEN 2 THEN 'delivered' ELSE 'cancelled' END, round((random() * 1000)::NUMERIC, 2), NOW() - (random() * 365 * 24 * 60 * 60 * INTERVAL '1 second') FROM generate_series(1, 1000000); -- 创建一个“看起来很合理”的复合索引 CREATE INDEX idx_orders_status_created ON orders (status, created_at);这个表有 100 万行,status字段有 4 个值,分布相对均匀。我们创建的索引(status, created_at),本意是想加速WHERE status = ? AND created_at > ?这类查询。
4.2 发现问题:一条“理应很快”的查询却很慢
假设我们的业务需求是:查询过去 30 天内所有“已发货”(shipped)的订单,并按创建时间倒序排列,取前 100 条。对应的 SQL 是:
SELECT id, user_id, amount, created_at FROM orders WHERE status = 'shipped' AND created_at >= NOW() - INTERVAL '30 days' ORDER BY created_at DESC LIMIT 100;在生产环境中,这条查询可能耗时 2 秒以上。我们先用最基础的EXPLAIN看一眼:
EXPLAIN SELECT id, user_id, amount, created_at FROM orders WHERE status = 'shipped' AND created_at >= NOW() - INTERVAL '30 days' ORDER BY created_at DESC LIMIT 100;输出(简化):
Limit (cost=100000.00..100000.25 rows=100 width=32) -> Sort (cost=100000.00..100250.00 rows=100000 width=32) Sort Key: created_at DESC -> Seq Scan on orders (cost=0.00..99999.00 rows=100000 width=32) Filter: ((status = 'shipped'::text) AND (created_at >= (now() - '30 days'::interval)))第一眼看到什么?Seq Scan on orders!全表扫描!而且rows=100000,预估要扫描 10 万行。这显然不对,因为我们有(status, created_at)索引。问题出在哪?
4.3 深度分析:用ANALYZE和BUFFERS揭开真相
我们立刻升级到EXPLAIN (ANALYZE, BUFFERS):
EXPLAIN (ANALYZE, BUFFERS) SELECT id, user_id, amount, created_at FROM orders WHERE status = 'shipped' AND created_at >= NOW() - INTERVAL '30 days' ORDER BY created_at DESC LIMIT 100;输出(关键部分):
Limit (cost=100000.00..100000.25 rows=100 width=32) (actual time=1850.234..1850.236 rows=100 loops=1) Buffers: shared hit=10000 read=12345 -> Sort (cost=100000.00..100250.00 rows=100000 width=32) (actual time=1850.230..1850.232 rows=100 loops=1) Sort Key: created_at DESC Sort Method: external merge Disk: 12345kB Buffers: shared hit=10000 read=12345 -> Seq Scan on orders (cost=0.00..99999.00 rows=100000 width=32) (actual time=0.023..1200.456 rows=98765 loops=1) Filter: ((status = 'shipped'::text) AND (created_at >= (now() - '30 days'::interval))) Rows Removed by Filter: 901235 Buffers: shared hit=10000 read=12345关键线索浮现:
Seq Scan的actual time=1200ms,占了总耗时的绝大部分。Rows Removed by Filter: 901235,意思是它扫描了 100 万行,但过滤掉了 90 万行,只留下了不到 10 万行。这证明Filter条件非常“松”,效率极低。Sort Method: external merge Disk: 12345kB,排序溢出了磁盘,这是另一个性能杀手。Buffers: read=12345,大量的磁盘读取。
为什么索引没用上?回顾我们的索引(status, created_at)。B-Tree 索引是按字典序排序的,所以(status, created_at)的排序规则是:先按status排,status相同的再按created_at排。我们的查询条件是status = 'shipped' AND created_at >= ?。这看起来完美匹配。但问题在于,created_at >= ?是一个范围查询,而status = 'shipped'是一个等值查询。在复合索引中,等值查询的列必须在范围查询的列左边,索引才能被高效利用。这里status在左,created_at在右,理论上是对的。但还有一个隐藏的陷阱:status字段的选择性(Cardinality)。如果status只有 4 个值,那么status = 'shipped'大约会筛选出 25% 的数据。对于 100 万行的表,就是 25 万行。规划器认为,扫描这 25 万行的索引页,再回表去主数据页里找这 25 万行,其总成本(随机 IO)可能高于直接顺序扫描 100 万行(顺序 IO)。所以,它“理性”地选择了全表扫描。
4.4 精准优化:三步走,让查询重回毫秒级
找到了根因,优化就水到渠成。我们采取三个递进的策略:
第一步:创建一个高选择性的索引
既然(status, created_at)因为status选择性太低而失效,我们就换一个思路:把选择性最高的列放在最左边。created_at是一个时间戳,它的选择性天然就很高。我们创建一个(created_at, status)的索引:
CREATE INDEX idx_orders_created_status ON orders (created_at, status);然后再次EXPLAIN (ANALYZE):
Limit (cost=0.42..10.56 rows=100 width=32) (actual time=0.045..0.047 rows=100 loops=1) -> Index Scan Backward using idx_orders_created_status on orders (cost=0.42..10123.45 rows=100000 width=32) (actual time=0.042..0.044 rows=100 loops=1) Index Cond: ((created_at >= (now() - '30 days'::interval)) AND (status = 'shipped'::text))效果立竿见影!cost从 10 万降到了 10,actual time从 1850ms 降到了 0.047ms。Index Scan Backward表明它正在利用索引的有序性,从最新的created_at开始倒序扫描,一旦找到 100 条status='shipped'的记录就立刻停止,完全不需要排序和全表扫描。
第二步:强制使用仅索引扫描(Index Only Scan)
我们注意到,SELECT的字段是id, user_id, amount, created_at。而我们的新索引(created_at, status)只包含了created_at和status,并不包含id等其他字段,所以它还是需要回表(Heap Fetch)。我们可以扩展索引,把所有SELECT的字段都包含进去,实现“仅索引扫描”:
-- 删除旧索引 DROP INDEX idx_orders_created_status; -- 创建覆盖索引(Covering Index) CREATE INDEX idx_orders_covering ON orders (created_at, status) INCLUDE (id, user_id, amount);INCLUDE子句是 PostgreSQL 11+ 的特性,它允许你在索引中额外“附带”一些不参与排序的字段,这样它们就可以被直接从索引页里读取,而无需回表。再次EXPLAIN (ANALYZE):
Limit (cost=0.42..10.56 rows=100 width=32) (actual time=0.032..0.034 rows=100 loops=1) -> Index Only Scan Backward using idx_orders_covering on orders (cost=0.42..10123.45 rows=100000 width=32) (actual time=0.029..0.031 rows=100 loops=1) Index Cond: ((created_at >= (now() - '30 days'::interval)) AND (status = 'shipped'::text)) Heap Fetches: 0看Heap Fetches: 0,这就是我们想要的!IO 进一步减少。
第三步:更新统计信息,巩固成果
最后,别忘了让规划器的“大脑”保持清醒:
ANALYZE orders;至此,一条曾经耗时近 2 秒的查询,被我们优化到了 0.03 毫秒,性能提升了 6 万倍。整个过程,没有修改一行业务代码,只靠对EXPLAIN的深度理解和精准的索引调整。
5. 常见问题与排查技巧实录:那些年,我们一起踩过的坑
在无数次的EXPLAIN分析实战中,有一些问题反复出现,它们往往不是技术难题,而是思维盲区或操作习惯导致的。我把这些“血泪教训”整理成一份速查清单,希望能帮你少走弯路。
5.1 问题速查表:典型症状与排查路径
| 症状(执行计划表现) | 最可能的原因 | 排查与解决方法 |
|---|---|---|
Seq Scan出现在本该用索引的地方 | 1. 缺少对应索引 2. 索引存在,但 WHERE条件无法利用(如函数包裹、类型不匹配)3. 统计信息过期,规划器误判成本 | 1.SELECT * FROM pg_indexes WHERE tablename = 'your_table';查看索引2. 检查 WHERE子句,确保是column = value,而非upper(column) = 'VALUE'3. ANALYZE your_table; |
Index Scan但Heap Fetches数量巨大 | 1. 索引未覆盖SELECT的所有字段2. 表的 visibility map陈旧(VACUUM不及时) | 1. 使用INCLUDE创建覆盖索引2. 执行 VACUUM your_table; |
Sort节点出现external merge Disk | work_mem设置过小,无法在内存中完成排序 | 1.SHOW work_mem;查看当前值2. 在会话中临时增大: SET LOCAL work_mem = '64MB';3. 在 postgresql.conf中永久调整(需重启或pg_reload_conf()) |
Hash Join但BUFFERS显示大量temp读写 | work_mem不足以容纳哈希表 | 同上,增大work_mem。注意:work_mem是每个操作节点的内存上限,一个查询可能有多个节点同时使用。 |
Nested Loop但外层表rows预估极小,实际却很大 | rows预估严重失准,根源是统计信息过期 | ANALYZE相关表。对于大表,可指定采样率:ANALYZE orders (status, created_at); |
Bitmap Heap Scan的Bitmap Index Scan节点rows预估巨大 | 多个Bitmap Index Scan的位图AND运算后,结果集仍然很大,不如直接Seq Scan | 这是规划器的“理性”选择。此时应检查是否真的需要这么多数据,或考虑是否可以添加更具选择性的过滤条件。 |
5.2 实操心得:那些文档里不会写的“潜规则”
永远不要相信
EXPLAIN的cost绝对值,只相信它的相对大小。我见过cost=1000的查询在 SSD 上飞快,也见过cost=100的查询在机械硬盘上龟速。cost的意义在于,它让你能比较 A 计划和 B 计划哪个更优,而不是告诉你它会跑多久。EXPLAIN (ANALYZE)是你的朋友,但也是你的“敌人”。它会真实执行查询,所以对UPDATE或DELETE,一定要先在BEGIN; ... ROLLBACK;事务里测试。对于大数据量的SELECT,也要评估其对系统负载的影响,最好在低峰期操作。**`BUFF