前言
当你向 PostgreSQL 发送一条 SELECT * FROM orders WHERE user_id = 42 时,数据库内部经历了怎样的旅程才返回结果?与 MySQL 不同,PostgreSQL 的优化器采用了截然不同的架构设计,其代价模型和统计信息系统更加精密。本文将深入 PostgreSQL 查询处理的全流程,揭示优化器选择执行计划的核心机制。
PostgreSQL 查询处理全流程
整个流程可以归纳为五个阶段:
SQL 文本 │ ▼┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│ Parser │───▶│ Analyzer │───▶│ Rewriter │───▶│ Planner │───▶│ Executor ││ 语法解析 │ │ 语义分析 │ │ 查询重写 │ │ 计划生成 │ │ 执行器 │└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ 生成 Parse 生成 Query 应用 RULE 生成 Plan 执行 Plan Tree Tree 系统 Tree一、Parser:语法解析
1.1 词法分析与语法分析
PostgreSQL 使用手写的递归下降解析器,将 SQL 文本转换为语法树(Parse Tree)。
源码入口在 src/backend/parser/parser.c:
// src/backend/parser/parser.c - 简化List *raw_parser(const char *str, RawParseMode mode){ // 初始化词法分析器 yyscan_t scanner; core_yyscan_t yyscanner;
yyscanner = scanner_init(str, &yyextra.core_yy_extra, ScanKeywords, NumScanKeywords);
// 调用语法分析器 if (base_yyparse(yyscanner) != 0) // 语法错误处理...
return yyextra.parsetree;}示例:解析 SELECT name FROM users WHERE id = 1
Parse Tree (简化表示):┌─────────────────────────────────────┐│ SelectStmt ││ ├── targetList: ││ │ └── ResTarget: name ││ ├── fromClause: ││ │ └── RangeVar: users ││ └── whereClause: ││ └── A_Expr: (=) ││ ├── ColumnRef: id ││ └── A_Const: 1 │└─────────────────────────────────────┘1.2 词法分析的 Token 分类
SQL: SELECT name FROM users WHERE id = 1
Token 流:┌──────────┬────────────┬──────────────────┐│ Token │ 类型 │ 说明 │├──────────┼────────────┼──────────────────┤│ SELECT │ 关键字 │ SQL 命令标识 ││ name │ 标识符 │ 列名引用 ││ FROM │ 关键字 │ 子句标识 ││ users │ 标识符 │ 表名引用 ││ WHERE │ 关键字 │ 条件子句 ││ id │ 标识符 │ 列名引用 ││ = │ 操作符 │ 比较运算 ││ 1 │ 整数常量 │ 字面值 │└──────────┴────────────┴──────────────────┘二、Analyzer:语义分析
2.1 分析流程
语义分析阶段(也称 Parse Analysis)将 Parse Tree 转换为 Query Tree。这个阶段的核心逻辑在 src/backend/parser/analyze.c:
2.2 范围表(Range Table)
范围表是 PostgreSQL 查询树中的核心数据结构,记录查询涉及的所有关系(表、视图、子查询等):
// src/include/nodes/parsenodes.h - 简化typedef struct RangeTblEntry{ NodeTag type; RTEKind rtekind; // 关系类型: 表、子查询、函数等 Oid relid; // 表的 OID char *relalias; // 别名 Alias *eref; // 有效列名列表 bool inh; // 是否继承子表 AclMode requiredPerms; // 所需权限} RangeTblEntry;Query Tree 核心结构:
// src/include/nodes/parsenodes.h - 简化typedef struct Query{ NodeTag type; CmdType commandType; // SELECT, INSERT, UPDATE, DELETE List *rtable; // 范围表 List *targetList; // 目标列 Node *jointree; // 连接树 (FROM + WHERE) List *groupClause; // GROUP BY Node *havingQual; // HAVING List *sortClause; // ORDER BY Node *limitOffset; // OFFSET Node *limitCount; // LIMIT} Query;2.3 语义分析过程
系统目录查询:
-- 查看表信息 (等价于 Analyzer 的查询)SELECT oid, relname, relnamespace, relkindFROM pg_class WHERE relname = 'users';
-- 查看列信息SELECT attname, atttypid, attnotnull, atthasdefFROM pg_attributeWHERE attrelid = 'users'::regclass AND attnum > 0;
-- 查看表权限SELECT has_table_privilege('users', 'SELECT');三、Rewriter:查询重写
3.1 RULE 系统
PostgreSQL 拥有独特的 RULE 系统,可以在查询重写阶段修改查询。重写器入口在 src/backend/rewrite/rewriteHandler.c:
3.2 视图展开
视图查询重写是最常见的场景。当你查询一个视图时,重写器会将视图引用替换为视图定义:
-- 创建视图CREATE VIEW active_users ASSELECT id, name, email FROM users WHERE status = 'active';
-- 查询视图SELECT name FROM active_users WHERE id > 100;重写过程:
3.3 重写示例:行安全策略
-- 启用行级安全ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 创建策略:用户只能看自己的订单CREATE POLICY user_orders ON orders USING (user_id = current_user_id());
-- 当执行 SELECT * FROM orders 时-- 重写器自动追加条件-- 等价于: SELECT * FROM orders WHERE user_id = current_user_id()四、Planner:计划生成(逻辑优化)
4.1 优化器架构
PostgreSQL 的优化器分为逻辑优化和物理优化两个层次。入口在 src/backend/optimizer/plan/planner.c:
// src/backend/optimizer/plan/planner.c - 简化PlannedStmt *planner(Query *parse, const char *query_string, int cursorOptions, ParamListInfo boundParams){ PlannedStmt *result;
// 核心规划入口 result = standard_planner(parse, query_string, cursorOptions, boundParams); return result;}4.2 子链接提升(SubLink Pullup)
PostgreSQL 会尝试将子查询(SubLink)提升为半连接(Semi Join),以便优化器能选择更优的连接算法:
-- 原始查询SELECT * FROM ordersWHERE user_id IN (SELECT id FROM users WHERE status = 'active');
-- 提升为 Semi JoinSELECT orders.*FROM orders SEMI JOIN users ON orders.user_id = users.id AND users.status = 'active';4.3 谓词下推(Predicate Pushdown)
将过滤条件尽可能下推到扫描节点,减少上层处理的数据量:
SELECT o.*, u.nameFROM orders oJOIN users u ON o.user_id = u.idWHERE u.status = 'active' AND o.amount > 100;优化过程:
优化前 (逻辑表示):┌─────────────────────────────┐│ Filter: u.status='active' ││ AND o.amount > 100 ││ ┌────────────────────────┐ ││ │ Join ON o.user_id=u.id │ ││ │ ├── Seq Scan: orders │ ││ │ └── Seq Scan: users │ ││ └────────────────────────┘ │└─────────────────────────────┘
优化后 (谓词下推):┌─────────────────────────────┐│ Join ON o.user_id = u.id ││ ├── Filter: amount > 100 ││ │ └── Seq Scan: orders ││ └── Filter: status='active'││ └── Seq Scan: users │└─────────────────────────────┘4.4 连接消除(Join Elimination)
当连接不会影响结果时,优化器会消除多余的连接:
-- orders 表有外键约束: user_id REFERENCES users(id)-- 如果查询不使用 users 的任何列SELECT o.*FROM orders oJOIN users u ON o.user_id = u.idWHERE o.amount > 1000;
-- 优化器检测到:-- 1. users 的列未被使用-- 2. 外键保证 user_id 一定存在-- 消除连接:SELECT * FROM orders WHERE amount > 1000;4.5 DISTINCT 消除
当优化器能证明结果已经唯一时,会移除 DISTINCT:
-- 主键列 DISTINCT 无意义SELECT DISTINCT id, name FROM users;
-- 等价于SELECT id, name FROM users;五、Planner:计划生成(物理优化)
5.1 代价模型
PostgreSQL 的代价模型基于磁盘 I/O 和 CPU 消耗。代价单位不是时间,而是相对权重。核心实现在 src/backend/optimizer/path/costsize.c:
总代价 = 启动代价 + 运行代价
启动代价: 获取第一行之前的代价运行代价: 获取所有剩余行的代价总代价: 启动代价 + 运行代价代价参数:
-- 查看代价参数SHOW seq_page_cost; -- 顺序读一个页的代价 (默认 1.0)SHOW random_page_cost; -- 随机读一个页的代价 (默认 4.0)SHOW cpu_tuple_cost; -- 处理一行的 CPU 代价 (默认 0.01)SHOW cpu_index_tuple_cost; -- 处理一个索引项的代价 (默认 0.005)SHOW cpu_operator_cost; -- 执行一个操作符的代价 (默认 0.0025)SHOW effective_cache_size; -- 预期可用的操作系统缓存 (默认 4GB)代价计算示例:
顺序扫描 10000 行的表(1000 页):
startup_cost = 0run_cost = seq_page_cost * pages + cpu_tuple_cost * tuples = 1.0 * 1000 + 0.01 * 10000 = 1000 + 100 = 1100
索引扫描选择 10 行(B+树深度 3):
startup_cost = 随机 I/O 读树节点 = random_page_cost * tree_height = 4.0 * 3 = 12
run_cost = random_page_cost * data_pages + cpu_tuple_cost * tuples = 4.0 * 10 + 0.01 * 10 = 40 + 0.1 = 40.1
total_cost = 12 + 40.1 = 52.15.2 统计信息
PostgreSQL 依靠 pg_statistic 系统表中的统计信息来估算选择率和行数。统计信息由 ANALYZE 命令收集:
-- 手动收集统计信息ANALYZE users;
-- 查看统计信息SELECT attname, n_distinct, null_frac, avg_width, most_common_vals, most_common_freqs, histogram_bounds, correlationFROM pg_statsWHERE tablename = 'users';统计信息字段含义:
┌───────────────────┬────────────────────────────────────────────┐│ 字段 │ 说明 │├───────────────────┼────────────────────────────────────────────┤│ n_distinct │ 不重复值的估计数量 ││ null_frac │ NULL 值的比例 ││ avg_width │ 列的平均字节宽度 ││ most_common_vals │ 最常见值列表 (MCV) ││ most_common_freqs │ 最常见值的频率列表 ││ histogram_bounds │ 等深直方图的边界值 ││ correlation │ 物理顺序与逻辑顺序的相关性 │└───────────────────┴────────────────────────────────────────────┘统计信息如何影响代价:
correlation 的作用:
correlation 接近 1.0: 物理存储顺序与索引顺序一致 → 索引扫描的 I/O 更顺序化 → 实际 random_page_cost 更低
correlation 接近 0.0: 物理存储随机 → 索引扫描需要大量随机 I/O → 可能不如顺序扫描
correlation 接近 -1.0: 物理存储与索引顺序相反 → 类似接近 0 的情况5.3 扫描方式选择
PostgreSQL 支持多种扫描方式,优化器根据统计信息和代价模型选择最优方式:
顺序扫描(Sequential Scan)
最简单的扫描方式,逐页读取整个表:
EXPLAIN SELECT * FROM users;
-- 结果:-- Seq Scan on users (cost=0.00..15.00 rows=1000 width=68)代价计算逻辑(cost_seqscan):
// 简化的代价计算void cost_seqscan(Path *path, PlannerInfo *root, RelOptInfo *baserel){ double pages = baserel->pages; double tuples = baserel->tuples;
// I/O 代价: 读所有页 run_cost += seq_page_cost * pages;
// CPU 代价: 处理每一行 run_cost += cpu_tuple_cost * tuples;}索引扫描(Index Scan)
通过 B+ 树索引查找数据,适合返回少量行的查询:
-- 假设 users.id 有主键索引EXPLAIN SELECT * FROM users WHERE id = 42;
-- 结果:-- Index Scan using users_pkey on users-- (cost=0.28..8.29 rows=1 width=68)-- Index Cond: (id = 42)B+ 树索引查找过程:┌─────────────┐│ Root Page │ Level 2│ [10|20|30] │└──────┬───────┘ │ ┌────┼────────────┐ ▼ ▼ ▼┌────┐ ┌────┐ ┌────┐ Level 1│<10 │ │10-20│ │20-30│ (Internal)└──┬─┘ └──┬─┘ └──┬──┘ │ │ │ ▼ ▼ ▼[Leaf] [Leaf] [Leaf] Level 0指向 指向 指向 (Leaf)CTID CTID CTID→ 表行 → 表行 → 表行位图扫描(Bitmap Scan)
当查询条件涉及多个索引或需要返回较多行时,PostgreSQL 使用位图扫描:
-- 假设有 idx_name(name) 和 idx_age(age) 两个索引EXPLAIN SELECT * FROM usersWHERE name = 'Alice' AND age > 25;
-- 结果:-- Bitmap Heap Scan on users-- (cost=8.50..25.30 rows=5 width=68)-- Recheck Cond: (name = 'Alice' AND age > 25)-- -> BitmapAnd-- -> Bitmap Index Scan on idx_name-- Index Cond: (name = 'Alice')-- -> Bitmap Index Scan on idx_age-- Index Cond: (age > 25)位图扫描 vs 索引扫描:
索引扫描: 适合返回极少行 (< 表的 1-5%) 每找到一行就回表访问
位图扫描: 适合返回较多行 (5-20%) 先构建位图,再按物理顺序批量回表 减少随机 I/O
顺序扫描: 适合返回大量行 (> 20%) 直接全表扫描更高效六、连接算法
6.1 Nested Loop Join
最基本的连接算法。对外表的每一行,扫描内表寻找匹配:
代价公式:
Nested Loop 代价: startup_cost = outer_startup_cost run_cost = outer_total_cost + inner_cost_per_row × outer_rows
如果内表有索引: inner_cost_per_row = 索引查找代价 (较低)
如果内表无索引: inner_cost_per_row = 内表全表扫描代价 (极高)适用场景:外表小,内表有索引。
6.2 Merge Join
两个表按连接键排序后进行归并连接:
Merge Join 示例 (连接键 = id):
外表 (按 id 排序): 内表 (按 id 排序): id | name id | order_no ---+------ ---+--------- 1 | Alice 1 | ORD-001 3 | Bob 2 | ORD-002 5 | Carol 3 | ORD-003 5 | ORD-004
归并过程: 外表 id=1, 内表 id=1 → 匹配 → 输出 外表 id=3, 内表 id=2 → 内表前进 外表 id=3, 内表 id=3 → 匹配 → 输出 外表 id=5, 内表 id=5 → 匹配 → 输出适用场景:数据已排序或需要排序后连接。
6.3 Hash Join
PostgreSQL 中最常用的等值连接算法,尤其适合大表连接:
代价公式:
Hash Join 代价: startup_cost = inner_total_cost + hash_build_cost run_cost = (outer_total_cost - outer_startup_cost) + cpu_hash_cost × outer_rows + cpu_tuple_cost × result_rows
Hash Table 构建代价: hash_build_cost = cpu_operator_cost × inner_rowsEXPLAIN SELECT * FROM orders oJOIN users u ON o.user_id = u.id;
-- 结果:-- Hash Join-- Hash Cond: (o.user_id = u.id)-- -> Seq Scan on orders-- -> Hash-- -> Seq Scan on users三种连接算法对比:
┌───────────────┬──────────────────┬───────────────────┬──────────────────┐│ 算法 │ 适用场景 │ 时间复杂度 │ 内存需求 │├───────────────┼──────────────────┼───────────────────┼──────────────────┤│ Nested Loop │ 小表 + 索引内表 │ O(M × log N) │ 低 ││ Merge Join │ 已排序数据 │ O(M + N) │ 中 (排序) ││ Hash Join │ 大表等值连接 │ O(M + N) │ 高 (Hash Table) │└───────────────┴──────────────────┴───────────────────┴──────────────────┘
M = 外表行数, N = 内表行数七、并行查询
7.1 并行查询架构
PostgreSQL 从 9.6 开始支持并行查询,采用多进程模型(而非多线程):
7.2 并行扫描
Parallel Sequential Scan:将表的页面块分配给多个 Worker 并行扫描:
SET max_parallel_workers_per_gather = 4;
EXPLAIN SELECT count(*) FROM orders;
-- 结果:-- Finalize Aggregate-- -> Gather-- Workers Planned: 3-- -> Partial Aggregate-- -> Parallel Seq Scan on orders并行扫描的协调机制:
// src/backend/access/transam/parallel.c - 简化// Worker 通过共享内存协调扫描进度typedef struct ParallelBlockTableScanDescData{ BlockNumber phs_nblocks; // 表的总块数 pg_atomic_uint64 phs_startblock; // 起始块 pg_atomic_uint64 phs_nallocated; // 已分配的块数} ParallelBlockTableScanDescData;
// 每个 Worker 获取下一个要扫描的块BlockNumberparallel_seqscan_get_next(ParallelBlockTableScanDesc pscan){ // 原子地递增已分配块数 return pg_atomic_fetch_add_u64(&pscan->phs_nallocated, 1);}7.3 并行连接
EXPLAIN SELECT * FROM orders oJOIN users u ON o.user_id = u.idWHERE o.amount > 1000;
-- 结果:-- Gather Merge-- Workers Planned: 2-- -> Merge Join-- Merge Cond: (o.user_id = u.id)-- -> Sort-- -> Parallel Seq Scan on orders o-- Filter: (amount > 1000)-- -> Index Scan using users_pkey on users u7.4 并行相关参数
-- 最大并行 Worker 数SHOW max_parallel_workers; -- 默认 8
-- 每个 Gather 节点的最大 Worker 数SHOW max_parallel_workers_per_gather; -- 默认 2
-- 并行扫描的最小表大小 (8MB)SHOW min_parallel_table_scan_size; -- 默认 8MB
-- 是否启用并行 AppendSHOW enable_parallel_append; -- 默认 on
-- 并行 Worker 的代价因子SHOW parallel_tuple_cost; -- 默认 0.1SHOW parallel_setup_cost; -- 默认 1000.0八、执行计划解读
8.1 EXPLAIN 输出分析
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)SELECT u.name, count(*) as order_countFROM users uJOIN orders o ON u.id = o.user_idWHERE u.status = 'active'GROUP BY u.nameORDER BY order_count DESCLIMIT 10;Limit (cost=35.80..35.82 rows=10 width=24) (actual time=0.152..0.155 rows=10 loops=1) Buffers: shared hit=15 -> Sort (cost=35.80..35.85 rows=20 width=24) (actual time=0.151..0.153 rows=10 loops=1) Sort Key: count(*) DESC Sort Method: top-N heapsort Memory: 25kB Buffers: shared hit=15 -> HashAggregate (cost=35.50..35.70 rows=20 width=24) (actual time=0.128..0.135 rows=20 loops=1) Group Key: u.name Batches: 1 Memory Usage: 24kB Buffers: shared hit=15 -> Hash Join (cost=10.25..32.50 rows=200 width=16) (actual time=0.042..0.090 rows=200 loops=1) Hash Cond: (o.user_id = u.id) Buffers: shared hit=15 -> Seq Scan on orders o (cost=0.00..16.00 rows=1000 width=8) (actual time=0.005..0.025 rows=1000 loops=1) Buffers: shared hit=10 -> Hash (cost=10.12..10.12 rows=10 width=16) (actual time=0.028..0.028 rows=10 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 9kB Buffers: shared hit=5 -> Seq Scan on users u (cost=0.00..10.12 rows=10 width=16) (actual time=0.005..0.023 rows=10 loops=1) Filter: (status = 'active'::text) Rows Removed by Filter: 90 Buffers: shared hit=5Planning: Buffers: shared hit=12Execution Time: 0.210 ms8.2 EXPLAIN 关键字段解读
┌─────────────────┬────────────────────────────────────────────────┐│ 字段 │ 含义 │├─────────────────┼────────────────────────────────────────────────┤│ cost=X..Y │ 启动代价..总代价 (估算) ││ rows=N │ 估算返回行数 ││ width=W │ 估算每行字节数 ││ actual time=X..Y│ 实际启动时间..实际完成时间 (毫秒) ││ rows=N │ 实际返回行数 ││ loops=L │ 执行循环次数 ││ Buffers │ 缓冲区访问统计 (shared hit/read/dirtied) ││ Sort Method │ 排序算法和内存使用 ││ Hash Cond │ Hash Join 的连接条件 ││ Filter │ 过滤条件 ││ Rows Removed │ 被过滤掉的行数 │└─────────────────┴────────────────────────────────────────────────┘8.3 常见执行计划模式
索引查找模式:
EXPLAIN SELECT * FROM users WHERE id = 42;-- Index Scan using users_pkey on users-- Index Cond: (id = 42)多表连接模式:
EXPLAIN SELECT * FROM orders oJOIN users u ON o.user_id = u.idJOIN products p ON o.product_id = p.id;
-- Hash Join on (o.product_id = p.id)-- -> Hash Join on (o.user_id = u.id)-- -> Seq Scan on orders-- -> Hash -> Seq Scan on users-- -> Hash -> Seq Scan on products子查询优化模式:
EXPLAIN SELECT * FROM ordersWHERE user_id IN (SELECT id FROM users WHERE status = 'active');
-- Hash Semi Join-- Hash Cond: (orders.user_id = users.id)-- -> Seq Scan on orders-- -> Hash -> Seq Scan on users-- Filter: (status = 'active')九、优化器实战技巧
9.1 统计信息调优
-- 增大统计目标以提高估算精度ALTER TABLE users ALTER COLUMN status SET STATISTICS 500;
-- 对特定列收集扩展统计信息CREATE STATISTICS users_stats (ndistinct, dependencies, mcv)ON status, city FROM users;ANALYZE users;
-- 查看扩展统计信息SELECT * FROM pg_statistic_extWHERE stxname = 'users_stats';扩展统计信息的作用:
┌──────────────────────┬──────────────────────────────────────────┐│ 类型 │ 解决的问题 │├──────────────────────┼──────────────────────────────────────────┤│ ndistinct │ 多列组合的唯一值估算 ││ dependencies │ 列之间的函数依赖关系 ││ mcv (最常见值) │ 多列组合的频率分布 │└──────────────────────┴──────────────────────────────────────────┘
示例: WHERE city = 'Beijing' AND status = 'active'
无扩展统计: 选择率 = P(city) × P(status) = 0.1 × 0.7 = 0.07 有 dependencies: 发现 status 依赖 city 实际选择率 = P(city='Beijing' AND status='active') = 0.099.2 强制/禁用特定扫描方式
-- 调试时临时禁用某种扫描方式SET enable_seqscan = off; -- 禁用顺序扫描SET enable_indexscan = off; -- 禁用索引扫描SET enable_bitmapscan = off; -- 禁用位图扫描SET enable_hashjoin = off; -- 禁用 Hash JoinSET enable_mergejoin = off; -- 禁用 Merge JoinSET enable_nestloop = off; -- 禁用 Nested Loop
-- 查看优化器决策过程SET enable_seqscan = off;EXPLAIN SELECT * FROM users WHERE status = 'active';-- 现在会优先选择索引扫描 (如果有的话)9.3 pg_hint_plan 扩展
-- 使用提示控制优化器/*+ HashJoin(users orders) */EXPLAIN SELECT * FROM users uJOIN orders o ON u.id = o.user_id;
/*+ SeqScan(users) IndexScan(orders idx_user_id) */EXPLAIN SELECT * FROM users uJOIN orders o ON u.id = o.user_id;总结
PostgreSQL 查询处理完整流程
核心要点
- Parser:手写递归下降解析器,生成 Parse Tree
- Analyzer:语义分析,结合系统目录生成 Query Tree
- Rewriter:RULE 系统和视图展开
- Planner(逻辑优化):子链接提升、谓词下推、连接消除、DISTINCT 消除
- Planner(物理优化):基于代价模型和统计信息选择扫描方式与连接算法
- 并行查询:Gather 模型,多进程并行扫描和连接
常见问题
Q1:PostgreSQL 和 MySQL 的优化器有什么区别?
PostgreSQL 的优化器基于代价(CBO),统计信息更丰富(支持扩展统计信息如 ndistinct、dependencies、MCV)。MySQL 的优化器也是 CBO,但统计信息相对简单。PostgreSQL 支持 RULE 系统做查询重写,MySQL 没有等价机制。并行查询方面,PostgreSQL 使用多进程模型,MySQL 使用多线程。
Q2:什么时候该用位图扫描而不是索引扫描?
当查询返回的行数占表总行数比例较高(通常 5-20%)时,位图扫描更优。位图扫描先收集所有匹配的 CTID,再按物理页顺序访问,减少了随机 I/O。返回行数极少时索引扫描更高效。
Q3:如何判断统计信息是否准确?
对比 EXPLAIN 中的估算行数(rows)和 EXPLAIN ANALYZE 中的实际行数(actual rows)。如果差距很大(比如 10 倍以上),说明统计信息不准确。运行 ANALYZE 更新统计信息,或增大 STATISTICS 目标。
Q4:为什么有时候 PostgreSQL 不走索引?
常见原因:统计信息不准导致优化器误判选择率;返回行数太多(全表扫描更快);random_page_cost 设置过高;列上没有合适的索引;使用了函数导致索引失效(如 WHERE lower(name) = 'alice',需要建表达式索引)。
Q5:并行查询有什么限制?
并行查询不适用于:带 FOR UPDATE/SHARE 的查询;包含 CTE(WITH 子句)中写操作的查询;在事务中已经修改了数据的查询;嵌套过深的子查询。max_parallel_workers_per_gather 限制了每个 Gather 的 Worker 数量。
参考资料
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






