news 2026/10/14 17:10:38

PostgreSQL EXPLAIN三层诊断体系:从执行计划到性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL EXPLAIN三层诊断体系:从执行计划到性能优化

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 Diskwork_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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 17:09:13

SAP拆解工单实施指南:订单类型、BOM与成本差异处理

简介:《SAP史上最详细的拆解工单详解》是一份面向SAP PP与FICO顾问的拆解业务实操指南,聚焦生产电脑等产品出现不可返工问题、但部分零部件仍可回收时的拆解工单处理与成本归集。资源共1个PDF文件,压缩包约2.97MB,已有4541人学习下…

作者头像 李华
网站建设 2026/10/14 17:08:36

朴素贝叶斯垃圾邮件过滤实战:从原理到代码实现

1. 从一封垃圾邮件的特征说起很多人第一次接触朴素贝叶斯,都是在"垃圾邮件过滤"这个经典场景里。原因很简单:它把贝叶斯定理从教科书里的公式,变成了一个能跑起来、能看见效果的东西。你不需要理解多复杂的数学推导,只要…

作者头像 李华
网站建设 2026/10/14 17:07:54

LimeSDR桌面射频实验室:开源SDR实现发射接收闭环验证

1. 这不是玩具,是能真正“看见”电磁波的桌面级射频实验室LimeSDR 这几个字母在最近两年的硬件极客圈、通信专业学生和嵌入式开发者群体里出现频率越来越高。它不像传统频谱仪动辄几十万起步,也不像某些廉价RTL-SDR那样只能单向接收、频段窄、动态范围小…

作者头像 李华
网站建设 2026/10/14 17:06:34

jdk1.8.0_231.zip 配置指南:环境变量、编译参数与避坑实践

简介:JDK1.8.0_231 64位免安装版面向Java开发者与运维人员,尤其适合需要在多环境快速部署、避免繁琐安装流程的团队。该版本基于Java 8重要更新,聚焦性能优化、稳定性与安全性,支持Lambda表达式、Stream API及java.time日期时间AP…

作者头像 李华
网站建设 2026/10/14 17:04:29

Google Play应用上架全流程:从账号注册到审核发布实战指南

做海外安卓应用的朋友,好多都会卡在“上架”这一步。2020年那会儿,我处理过一个工具类App的海外发布,从开发者账号注册到最终在Google Play上架成功,中间踩了不少坑,也总结出了一套还算顺手的流程。这篇文章不聊虚的&a…

作者头像 李华
网站建设 2026/10/14 17:04:10

从内存加载DLL:PE手动映射与重定位实现原理详解

简介:从内存加载DLL的完整实现与调用示例,面向需要在运行时绕过磁盘落盘、以资源方式直接加载动态库的Windows开发者。示例工程包含一个测试用DLL项目与一个实际加载测试项目,演示了如何将DLL作为资源编译进程序,并通过MemoryLoad…

作者头像 李华