PostgreSQL-02-Optimizer-优化器
模块概览
模块职责
Optimizer(优化器)是 PostgreSQL 查询处理的核心模块,负责将 Analyzer 输出的查询树(Query)转换为高效的执行计划(Plan)。优化器通过路径生成、代价估算和计划选择,在保证查询语义正确的前提下,寻找执行代价最小的方案。
核心能力
-
路径生成(Path Generation):枚举所有可能的执行路径
- 表扫描路径:顺序扫描(SeqScan)、索引扫描(IndexScan)、索引唯一扫描(IndexOnlyScan)
- 连接路径:嵌套循环(NestLoop)、哈希连接(HashJoin)、归并连接(MergeJoin)
- 聚合路径:哈希聚合(HashAgg)、分组聚合(GroupAgg)
- 排序路径:快速排序(Sort)、增量排序(Incremental Sort)
-
代价估算(Cost Estimation):计算每条路径的执行代价
- 启动代价(Startup Cost):返回第一行的代价
- 总代价(Total Cost):返回所有行的代价
- 基于统计信息:pg_statistic 表中的直方图、MCV(Most Common Values)
- 参数化代价:
seq_page_cost、random_page_cost、cpu_tuple_cost等
-
连接顺序优化(Join Order Optimization):确定多表连接的最优顺序
- 动态规划(DP):表数 ≤ 12 时使用
- 遗传算法(GEQO):表数 > 12 时使用(避免组合爆炸)
- 左深树(Left-Deep Tree):优先生成可流水线执行的计划
-
并行查询规划(Parallel Query Planning):生成多进程并行执行的计划
- Parallel Seq Scan:并行顺序扫描
- Parallel Hash Join:并行哈希连接
- Gather 节点:汇聚并行工作进程的结果
-
分区剪裁(Partition Pruning):在规划时排除无需扫描的分区
- 静态剪裁:基于常量条件
- 动态剪裁:基于参数或子查询结果(执行时剪裁)
输入/输出
- 输入:
- 查询树(Query *):Analyzer 输出的语义分析后的查询
- 参数绑定(ParamListInfo):准备语句的参数值
- 游标选项(cursorOptions):如 CURSOR_OPT_FAST_PLAN
- 输出:
- 执行计划(PlannedStmt *):包含计划树(Plan 节点树)
- 计划元数据:参数列表、关系 OID 列表、行安全策略等
上下游依赖
- 上游:Rewriter(重写器)输出的查询树
- 下游:Executor(执行器)接收执行计划
- 依赖模块:
- Catalog(系统表):读取表结构、索引定义
- Statistics(统计信息):读取 pg_statistic
- Cost Functions(代价函数库):计算各操作的代价
架构图
flowchart TB
subgraph Optimizer["Optimizer 优化器"]
direction TB
Entry[standard_planner<br/>入口函数] --> SubQP[subquery_planner<br/>子查询规划]
SubQP --> Prep[前置处理]
Prep --> PrepExpr[表达式预处理]
Prep --> PrepJT[连接树规范化]
Prep --> PrepLimit[LIMIT 预处理]
SubQP --> GP[grouping_planner<br/>分组规划]
GP --> PathGen[路径生成]
PathGen --> ScanPath[扫描路径]
PathGen --> JoinPath[连接路径]
PathGen --> AggPath[聚合路径]
PathGen --> SortPath[排序路径]
PathGen --> CostEst[代价估算]
CostEst --> SeqCost[顺序扫描代价]
CostEst --> IdxCost[索引扫描代价]
CostEst --> JoinCost[连接代价]
GP --> PathSelect[路径选择]
PathSelect --> Cheapest[选择最优路径]
GP --> CreatePlan[create_plan<br/>构造执行计划]
CreatePlan --> ScanPlan[扫描节点]
CreatePlan --> JoinPlan[连接节点]
CreatePlan --> AggPlan[聚合节点]
CreatePlan --> SetRefs[设置变量引用]
SetRefs --> Output[PlannedStmt]
end
subgraph Input["输入"]
Query[Query 查询树]
Params[ParamListInfo]
end
subgraph Dependencies["依赖模块"]
Catalog[Catalog<br/>系统表]
Stats[pg_statistic<br/>统计信息]
CostParams[代价参数<br/>GUC 配置]
end
Query --> Entry
Params --> Entry
PathGen --> Catalog
CostEst --> Stats
CostEst --> CostParams
subgraph Output2["输出"]
Plan[PlannedStmt<br/>执行计划]
end
Output --> Plan
架构说明
分层设计
-
入口层(standard_planner):
- 初始化规划器全局状态(PlannerGlobal)
- 调用子查询规划器处理查询
- 处理扩展点钩子(planner_hook)
-
前置处理层(Preprocessing):
- 表达式简化:常量折叠、子查询提升
- 连接树规范化:外连接转换、连接条件重排
- LIMIT 提取:计算元组分数(tuple_fraction)
-
核心规划层(grouping_planner):
- 路径生成:枚举所有可行的执行路径
- 代价估算:计算每条路径的代价
- 路径选择:选择代价最小的路径
-
计划生成层(create_plan):
- 将路径(Path)转换为执行计划节点(Plan)
- 设置节点参数(如目标列表、条件表达式)
- 设置变量引用(Var 节点的 varno/varattno)
关键数据结构
- PlannerInfo:规划器上下文,包含查询树、关系列表、路径列表等
- RelOptInfo:关系优化信息,记录表或子查询的所有可行路径
- Path:执行路径的抽象表示,包含代价、排序键等信息
- Plan:执行计划节点,由执行器解释执行
边界条件
并发性
- 每个查询独立规划,无跨查询共享状态
- 访问系统表和统计信息时,使用 MVCC 快照
- 统计信息可能滞后(最终一致),不影响正确性,仅影响性能
输入限制
- 连接表数:动态规划算法处理 ≤ 12 表,超过后使用遗传算法
- 查询复杂度:超深嵌套子查询可能导致规划时间过长
- 参数数量:准备语句参数数量无硬性限制
性能关键点
- 规划时间:复杂查询规划可能占总执行时间的 10%-50%
- 统计信息质量:过期或缺失的统计信息导致次优计划
- 配置参数:
random_page_cost、effective_cache_size等影响代价估算
核心数据结构
1. PlannerInfo(规划器上下文)
/* src/include/nodes/pathnodes.h */
typedef struct PlannerInfo
{
NodeTag type;
Query *parse; /* 查询树 */
PlannerGlobal *glob; /* 全局状态 */
Index query_level; /* 查询嵌套层级(1=最外层) */
PlannerInfo *parent_root; /* 父查询的 PlannerInfo */
List *plan_params; /* 计划参数列表 */
Bitmapset *outer_params; /* 外层参数集合 */
RelOptInfo **simple_rel_array; /* 基础关系数组(索引=RTE编号) */
int simple_rel_array_size; /* 数组大小 */
RangeTblEntry **simple_rte_array; /* RTE 数组(快速访问) */
List *append_rel_list; /* AppendRelInfo 列表 */
List *rowMarks; /* PlanRowMark 列表(FOR UPDATE) */
List *placeholder_list; /* PlaceHolderInfo 列表 */
List *fkey_list; /* 外键约束列表 */
List *query_pathkeys; /* 期望的排序键 */
List *group_pathkeys; /* GROUP BY 排序键 */
List *window_pathkeys; /* WINDOW 排序键 */
List *distinct_pathkeys; /* DISTINCT 排序键 */
List *sort_pathkeys; /* ORDER BY 排序键 */
List *eq_classes; /* 等价类列表 */
bool ec_merging_done; /* 等价类合并是否完成 */
List *minmax_aggs; /* MIN/MAX 聚合列表 */
List *initial_rels; /* 初始关系列表 */
/* ... 更多字段 ... */
} PlannerInfo;
关键字段说明
| 字段 | 说明 |
|---|---|
parse |
查询树,包含 SELECT/INSERT/UPDATE/DELETE 的语义信息 |
simple_rel_array |
基础关系数组,simple_rel_array[i] 对应 RTE 编号 i 的 RelOptInfo |
eq_classes |
等价类列表,记录通过等值条件(如 a.x = b.y)关联的列 |
query_pathkeys |
期望的输出排序顺序(如 ORDER BY 子句) |
group_pathkeys |
GROUP BY 的排序键(用于 GroupAgg) |
2. RelOptInfo(关系优化信息)
typedef struct RelOptInfo
{
NodeTag type;
RelOptKind reloptkind; /* 关系类型(表/子查询/连接) */
Relids relids; /* 包含的 RTE 编号集合 */
double rows; /* 估算的输出行数 */
bool consider_startup; /* 是否考虑启动代价 */
bool consider_param_startup; /* 参数化路径的启动代价 */
bool consider_parallel; /* 是否考虑并行路径 */
struct Path **cheapest_startup_path; /* 启动代价最小的路径 */
struct Path **cheapest_total_path; /* 总代价最小的路径 */
struct Path **cheapest_unique_path; /* 唯一化后最便宜的路径 */
List *pathlist; /* 所有可行路径列表 */
List *ppilist; /* ParamPathInfo 列表(参数化路径) */
List *partial_pathlist; /* 部分路径列表(用于并行) */
struct Path **cheapest_partial_path; /* 最便宜的部分路径 */
List *indexlist; /* 可用索引列表 */
List *statlist; /* 扩展统计信息列表 */
BlockNumber pages; /* 估算的物理页数 */
double tuples; /* 估算的总元组数 */
double allvisfrac; /* 可见性映射覆盖比例 */
/* ... 连接相关字段 ... */
List *joininfo; /* 连接子句列表 */
bool has_eclass_joins; /* 是否有等价类连接 */
/* ... 基础关系特定字段 ... */
Index relid; /* RTE 编号(基础关系) */
Oid reltablespace; /* 表空间 OID */
RTEKind rtekind; /* RTE 类型 */
AttrNumber min_attr; /* 最小属性编号 */
AttrNumber max_attr; /* 最大属性编号 */
Relids *attr_needed; /* 每个属性的需求位图 */
int32 *attr_widths; /* 每个属性的平均宽度(字节) */
/* ... 更多字段 ... */
} RelOptInfo;
关键字段说明
| 字段 | 说明 |
|---|---|
reloptkind |
关系类型:RELOPT_BASEREL(基础表)、RELOPT_JOINREL(连接结果)、RELOPT_OTHER_MEMBER_REL(子查询) |
rows |
估算的输出行数,基于选择性(Selectivity)计算 |
pathlist |
所有生成的路径列表,优化器从中选择最优路径 |
cheapest_total_path |
总代价最小的路径,通常用于最终计划 |
indexlist |
可用索引列表(IndexOptInfo),用于生成索引扫描路径 |
3. Path(执行路径)
typedef struct Path
{
NodeTag type;
NodeTag pathtype; /* 路径类型(T_SeqScan/T_IndexScan等) */
RelOptInfo *parent; /* 所属的 RelOptInfo */
PathTarget *pathtarget; /* 输出目标列表 */
ParamPathInfo *param_info; /* 参数化路径信息 */
bool parallel_aware; /* 是否是并行感知路径 */
bool parallel_safe; /* 是否可并行执行 */
int parallel_workers; /* 并行工作进程数 */
double rows; /* 估算的输出行数 */
Cost startup_cost; /* 启动代价 */
Cost total_cost; /* 总代价 */
List *pathkeys; /* 输出排序键 */
} Path;
派生类型(部分)
/* 顺序扫描路径 */
typedef struct Path SeqPath; /* 无额外字段 */
/* 索引扫描路径 */
typedef struct IndexPath
{
Path path;
IndexOptInfo *indexinfo; /* 索引信息 */
List *indexclauses; /* 索引条件子句 */
List *indexorderbys; /* 索引排序列(用于 ORDER BY 优化) */
ScanDirection indexscandir; /* 扫描方向(前向/后向) */
Cost indextotalcost; /* 索引访问总代价 */
Selectivity indexselectivity; /* 索引选择性 */
} IndexPath;
/* 连接路径 */
typedef struct JoinPath
{
Path path;
JoinType jointype; /* 连接类型(INNER/LEFT/RIGHT/FULL) */
bool inner_unique; /* 内表是否唯一 */
Path *outerjoinpath; /* 外表路径 */
Path *innerjoinpath; /* 内表路径 */
List *joinrestrictinfo; /* 连接条件列表 */
} JoinPath;
/* 嵌套循环连接路径 */
typedef struct NestPath
{
JoinPath jpath;
} NestPath;
/* 哈希连接路径 */
typedef struct HashPath
{
JoinPath jpath;
List *path_hashclauses; /* 哈希键列表 */
int num_batches; /* 哈希批次数 */
double inner_rows_total; /* 内表总行数 */
} HashPath;
/* 归并连接路径 */
typedef struct MergePath
{
JoinPath jpath;
List *path_mergeclauses; /* 归并键列表 */
List *outersortkeys; /* 外表排序键 */
List *innersortkeys; /* 内表排序键 */
bool skip_mark_restore; /* 是否跳过标记/恢复 */
bool materialize_inner; /* 是否物化内表 */
} MergePath;
类图
classDiagram
class PlannerInfo {
+Query *parse
+PlannerGlobal *glob
+RelOptInfo **simple_rel_array
+List *eq_classes
+List *query_pathkeys
}
class RelOptInfo {
+RelOptKind reloptkind
+Relids relids
+double rows
+List *pathlist
+Path *cheapest_total_path
+List *indexlist
}
class Path {
<<abstract>>
+NodeTag pathtype
+RelOptInfo *parent
+double rows
+Cost startup_cost
+Cost total_cost
+List *pathkeys
}
class SeqScanPath {
+Path path
}
class IndexScanPath {
+Path path
+IndexOptInfo *indexinfo
+List *indexclauses
}
class JoinPath {
<<abstract>>
+Path path
+JoinType jointype
+Path *outerjoinpath
+Path *innerjoinpath
}
class NestLoopPath {
+JoinPath jpath
}
class HashJoinPath {
+JoinPath jpath
+List *path_hashclauses
}
class MergeJoinPath {
+JoinPath jpath
+List *path_mergeclauses
}
class Plan {
<<abstract>>
+NodeTag type
+List *targetlist
+List *qual
+Plan *lefttree
+Plan *righttree
+Cost startup_cost
+Cost total_cost
+double plan_rows
}
PlannerInfo --> RelOptInfo : simple_rel_array
RelOptInfo --> Path : pathlist
Path <|-- SeqScanPath
Path <|-- IndexScanPath
Path <|-- JoinPath
JoinPath <|-- NestLoopPath
JoinPath <|-- HashJoinPath
JoinPath <|-- MergeJoinPath
Path ..> Plan : create_plan()
核心 API 与调用链路
API 1: standard_planner() - 查询规划入口
函数签名
PlannedStmt *standard_planner(Query *parse,
const char *query_string,
int cursorOptions,
ParamListInfo boundParams,
ExplainState *es);
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
parse |
Query * | 查询树(Analyzer 输出) |
query_string |
const char * | 原始 SQL 文本(用于日志) |
cursorOptions |
int | 游标选项(如 CURSOR_OPT_FAST_PLAN) |
boundParams |
ParamListInfo | 准备语句的参数值 |
es |
ExplainState * | EXPLAIN 状态(NULL 表示正常执行) |
返回值
PlannedStmt *result; /* 执行计划 */
返回的 PlannedStmt 包含:
planTree:执行计划树(Plan 节点)rtable:范围表(RangeTblEntry 列表)resultRelations:目标关系列表(INSERT/UPDATE/DELETE)subplans:子计划列表(InitPlan/SubPlan)relationOids:涉及的关系 OID 列表invalItems:失效缓存列表
核心代码
PlannedStmt *
standard_planner(Query *parse, const char *query_string,
int cursorOptions, ParamListInfo boundParams,
ExplainState *es)
{
PlannerGlobal *glob;
double tuple_fraction;
PlannerInfo *root;
RelOptInfo *final_rel;
Path *best_path;
Plan *top_plan;
PlannedStmt *result;
/* 1. 初始化全局状态 */
glob = makeNode(PlannerGlobal);
glob->boundParams = boundParams;
glob->paramExecTypes = NIL;
glob->subplans = NIL;
glob->subroots = NIL;
glob->rewindPlanIDs = NULL;
glob->finalrtable = NIL;
glob->finalrteperminfos = NIL;
glob->finalrowmarks = NIL;
glob->relationOids = NIL;
glob->invalItems = NIL;
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->lastPlanNodeId = 0;
glob->transientPlan = false;
glob->dependsOnRole = false;
/* 2. 计算元组分数(用于 LIMIT 优化) */
if (parse->limitCount)
tuple_fraction = 0.1; /* 有 LIMIT,假设只取 10% */
else if (cursorOptions & CURSOR_OPT_FAST_PLAN)
tuple_fraction = cursor_tuple_fraction; /* 游标快速模式 */
else
tuple_fraction = 0.0; /* 无 LIMIT,取所有元组 */
/* 3. 调用子查询规划器 */
root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
/* 4. 获取最优路径 */
final_rel = fetch_upper_rel(root, UPPERREL_FINAL, NULL);
best_path = get_cheapest_fractional_path(final_rel, tuple_fraction);
/* 5. 生成执行计划 */
top_plan = create_plan(root, best_path);
/* 6. 设置变量引用(Var 节点的 varno/varattno) */
top_plan = set_plan_references(root, top_plan);
/* 7. 构造 PlannedStmt */
result = makeNode(PlannedStmt);
result->commandType = parse->commandType;
result->queryId = parse->queryId;
result->hasReturning = (parse->returningList != NIL);
result->hasModifyingCTE = parse->hasModifyingCTE;
result->canSetTag = parse->canSetTag;
result->transientPlan = glob->transientPlan;
result->dependsOnRole = glob->dependsOnRole;
result->parallelModeNeeded = glob->parallelModeNeeded;
result->planTree = top_plan;
result->rtable = glob->finalrtable;
result->resultRelations = root->resultRelations;
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->paramExecTypes = glob->paramExecTypes;
result->utilityStmt = parse->utilityStmt;
result->stmt_location = parse->stmt_location;
result->stmt_len = parse->stmt_len;
return result;
}
调用链路
exec_simple_query (postgres.c)
→ pg_plan_query (postgres.c)
→ planner (optimizer.h)
→ standard_planner (planner.c) ← 当前函数
→ subquery_planner (planner.c)
→ grouping_planner (planner.c)
→ query_planner (planmain.c)
→ make_one_rel (allpaths.c)
→ set_base_rel_pathlists (allpaths.c)
→ make_rel_from_joinlist (allpaths.c)
→ create_plan (createplan.c)
→ set_plan_references (setrefs.c)
关键功能实现
功能 1:路径生成(Path Generation)
路径生成是优化器的核心任务,为每个关系(表或子查询)生成所有可行的访问路径。
顺序扫描路径生成
/* src/backend/optimizer/path/allpaths.c */
static void
set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
{
/* 1. 生成顺序扫描路径 */
add_path(rel, create_seqscan_path(root, rel, NULL, 0));
/* 2. 生成并行顺序扫描路径(如果满足条件) */
if (rel->consider_parallel && rel->lateral_relids == NULL)
create_plain_partial_paths(root, rel);
/* 3. 生成索引扫描路径 */
if (rel->indexlist != NIL)
create_index_paths(root, rel);
/* 4. 生成 TID 扫描路径(WHERE ctid = xxx) */
if (has_tidquals(rel))
create_tidscan_paths(root, rel);
}
顺序扫描代价计算
/* src/backend/optimizer/path/costsize.c */
void
cost_seqscan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
ParamPathInfo *param_info)
{
Cost startup_cost = 0;
Cost run_cost = 0;
double spc_seq_page_cost;
/* 1. 计算 CPU 代价:处理每个元组 */
run_cost += cpu_tuple_cost * baserel->tuples;
/* 2. 计算 I/O 代价:顺序读取所有页 */
run_cost += spc_seq_page_cost * baserel->pages;
/* 3. 计算过滤条件代价 */
if (param_info)
{
QualCost qpqual_cost;
cost_qual_eval(&qpqual_cost, param_info->ppi_clauses, root);
startup_cost += qpqual_cost.startup;
run_cost += qpqual_cost.per_tuple * baserel->tuples;
}
path->startup_cost = startup_cost;
path->total_cost = startup_cost + run_cost;
}
关键参数
cpu_tuple_cost:处理一个元组的 CPU 代价(默认 0.01)seq_page_cost:顺序读取一页的 I/O 代价(默认 1.0)baserel->tuples:表的总元组数(来自 pg_class.reltuples)baserel->pages:表的总页数(来自 pg_class.relpages)
索引扫描路径生成
static void
create_index_paths(PlannerInfo *root, RelOptInfo *rel)
{
ListCell *lc;
/* 遍历所有可用索引 */
foreach(lc, rel->indexlist)
{
IndexOptInfo *index = (IndexOptInfo *) lfirst(lc);
/* 1. 匹配索引列与查询条件 */
List *indexclauses = find_index_clauses(root, index);
if (indexclauses == NIL)
continue; /* 索引无法用于当前查询 */
/* 2. 生成索引扫描路径 */
IndexPath *ipath = create_index_path(root, index, indexclauses, NIL,
ForwardScanDirection, false,
NULL, 1.0, false);
add_path(rel, (Path *) ipath);
/* 3. 生成索引唯一扫描路径(如果是唯一索引) */
if (index->unique && list_length(indexclauses) == index->nkeycolumns)
{
/* 唯一索引扫描,最多返回一行 */
ipath->path.rows = 1.0;
}
/* 4. 生成 Index Only Scan 路径(如果索引覆盖所有列) */
if (index_covers_query(root, index, rel))
{
IndexPath *ipath_only = create_index_path(root, index, indexclauses, NIL,
ForwardScanDirection, true,
NULL, 1.0, false);
add_path(rel, (Path *) ipath_only);
}
}
}
索引扫描代价计算
void
cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
bool partial_path)
{
Cost startup_cost = 0;
Cost run_cost = 0;
double num_rows;
double num_pages;
double spc_random_page_cost;
/* 1. 估算索引返回的行数(基于选择性) */
num_rows = path->path.rows;
/* 2. 估算索引访问的页数(基于索引高度和叶子页数) */
double index_pages = path->indexinfo->pages;
double index_tuples = path->indexinfo->tuples;
double tuples_fetched = clamp_row_est(num_rows * loop_count);
/* 3. 索引扫描代价 = 索引页访问 + 堆页访问 */
if (path->indexinfo->amcostestimate)
{
/* 调用索引访问方法的代价估算函数 */
path->indexinfo->amcostestimate(root, path, loop_count,
&startup_cost, &run_cost,
&num_pages, &num_rows, ...);
}
/* 4. 堆页访问代价(随机读) */
run_cost += spc_random_page_cost * num_pages;
/* 5. CPU 代价:处理元组 */
run_cost += cpu_tuple_cost * num_rows;
path->path.startup_cost = startup_cost;
path->path.total_cost = startup_cost + run_cost;
}
关键参数
random_page_cost:随机读取一页的 I/O 代价(默认 4.0,SSD 建议 1.1)effective_cache_size:操作系统缓存大小(用于估算缓存命中率)index->indexselectivity:索引选择性(返回行数 / 总行数)
功能 2:连接顺序优化
PostgreSQL 使用动态规划(DP)算法确定多表连接的最优顺序。
动态规划算法
/* src/backend/optimizer/path/allpaths.c */
RelOptInfo *
make_one_rel(PlannerInfo *root, List *joinlist)
{
RelOptInfo *rel;
/* 1. 为每个基础关系生成路径 */
set_base_rel_sizes(root);
set_base_rel_pathlists(root);
/* 2. 连接顺序优化 */
if (list_length(root->simple_rel_array) <= join_collapse_limit &&
list_length(root->simple_rel_array) <= from_collapse_limit)
{
/* 动态规划算法(表数 ≤ 12) */
rel = standard_join_search(root, joinlist);
}
else
{
/* 遗传算法(表数 > 12) */
rel = geqo(root, joinlist);
}
/* 3. 为最终结果生成聚合/排序路径 */
create_upper_paths(root, rel);
return rel;
}
标准连接搜索(动态规划)
static RelOptInfo *
standard_join_search(PlannerInfo *root, List *joinlist)
{
RelOptInfo *final_rel;
int levels_needed;
List **joinitems;
int lev;
/* 1. 确定连接层级数 */
levels_needed = list_length(root->simple_rel_array);
/* 2. 初始化连接项列表(每层一个列表) */
joinitems = (List **) palloc0((levels_needed + 1) * sizeof(List *));
joinitems[1] = list_copy(root->initial_rels);
/* 3. 逐层连接(从 2 表连接到 N 表连接) */
for (lev = 2; lev <= levels_needed; lev++)
{
ListCell *lc;
/* 遍历上一层的所有关系 */
foreach(lc, joinitems[lev - 1])
{
RelOptInfo *old_rel = (RelOptInfo *) lfirst(lc);
/* 尝试与所有可能的关系连接 */
make_rels_by_clause_joins(root, old_rel, joinitems[lev]);
make_rels_by_clauseless_joins(root, old_rel, joinitems[lev]);
}
/* 生成当前层所有关系的路径 */
foreach(lc, joinitems[lev])
{
RelOptInfo *rel = (RelOptInfo *) lfirst(lc);
generate_gather_paths(root, rel, false);
}
}
/* 4. 返回最顶层的唯一关系(所有表的连接) */
final_rel = (RelOptInfo *) linitial(joinitems[levels_needed]);
return final_rel;
}
动态规划原理
动态规划通过保存子问题的最优解,避免重复计算:
- 子问题定义:
RelOptInfo(S)表示关系集合 S 的最优连接结果 - 状态转移:
RelOptInfo(S ∪ T) = min { RelOptInfo(S) ⨝ RelOptInfo(T) } - 最优子结构:全局最优解包含子问题的最优解
时间复杂度:O(3^N),N 为表数(对于 N=12,约 500 万次操作)
空间复杂度:O(2^N),需存储所有子集的最优解
连接路径生成
static void
hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
RelOptInfo *outerrel, RelOptInfo *innerrel,
JoinType jointype, JoinPathExtraData *extra)
{
List *hashclauses;
ListCell *lc;
/* 1. 提取哈希连接条件(等值条件) */
hashclauses = NIL;
foreach(lc, extra->restrictlist)
{
RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
if (rinfo->hashjoinoperator != InvalidOid)
hashclauses = lappend(hashclauses, rinfo);
}
if (hashclauses == NIL)
return; /* 无等值条件,无法使用哈希连接 */
/* 2. 生成哈希连接路径(外表 Hash Join 内表) */
foreach(lc, outerrel->pathlist)
{
Path *outerpath = (Path *) lfirst(lc);
/* 选择内表的最优路径 */
Path *innerpath = innerrel->cheapest_total_path;
/* 创建哈希连接路径 */
HashPath *hashpath = create_hashjoin_path(root, joinrel, jointype,
extra->sjinfo,
extra->semifactors,
outerpath, innerpath,
extra->restrictlist,
hashclauses);
add_path(joinrel, (Path *) hashpath);
}
}
哈希连接代价计算
void
cost_hashjoin(Path *path, PlannerInfo *root, JoinType jointype,
Path *outer_path, Path *inner_path,
List *hashclauses, SpecialJoinInfo *sjinfo)
{
Cost startup_cost = 0;
Cost run_cost = 0;
double outer_rows = outer_path->rows;
double inner_rows = inner_path->rows;
/* 1. 构建哈希表代价(内表) */
startup_cost += inner_path->total_cost;
startup_cost += cpu_operator_cost * inner_rows; /* 哈希计算 */
/* 2. 探测哈希表代价(外表) */
run_cost += outer_path->total_cost;
run_cost += cpu_operator_cost * outer_rows; /* 哈希计算 */
/* 3. 连接条件评估代价 */
run_cost += cpu_tuple_cost * outer_rows * inner_rows / hash_buckets;
/* 4. 哈希表内存不足时的批次代价 */
if (hash_table_size > work_mem)
{
int num_batches = hash_table_size / work_mem + 1;
/* 内表需要多次读取 */
run_cost += inner_path->total_cost * (num_batches - 1);
}
path->startup_cost = startup_cost;
path->total_cost = startup_cost + run_cost;
}
功能 3:并行查询规划
PostgreSQL 9.6 引入并行查询,允许多个工作进程协同执行查询。
并行顺序扫描路径生成
static void
create_plain_partial_paths(PlannerInfo *root, RelOptInfo *rel)
{
Path *path;
/* 1. 检查表大小(小表不值得并行) */
if (rel->pages < min_parallel_table_scan_size)
return;
/* 2. 计算并行度 */
int parallel_workers = compute_parallel_worker(rel, rel->pages, -1, max_parallel_workers_per_gather);
if (parallel_workers <= 0)
return;
/* 3. 创建并行顺序扫描路径 */
path = create_seqscan_path(root, rel, NULL, parallel_workers);
add_partial_path(rel, path);
}
并行度计算
int
compute_parallel_worker(RelOptInfo *rel, double heap_pages, double index_pages,
int max_workers)
{
int parallel_workers = 0;
/* 1. 基于表大小估算并行度 */
double parallel_threshold = min_parallel_table_scan_size;
while (heap_pages >= (parallel_threshold * 3) &&
parallel_workers < max_workers)
{
parallel_workers++;
parallel_threshold *= 3;
}
/* 2. 考虑系统资源限制 */
parallel_workers = Min(parallel_workers, max_parallel_workers);
return parallel_workers;
}
Gather 节点
并行查询的结果通过 Gather 节点汇聚到主进程:
Gather (cost=0.00..1000.00 rows=100000 width=4)
Workers Planned: 4
-> Parallel Seq Scan on large_table (cost=0.00..1000.00 rows=25000 width=4)
Filter: (status = 'active')
最佳实践与调优
配置参数优化
代价参数
# SSD 环境优化
random_page_cost = 1.1 # 降低随机读代价(默认 4.0)
effective_cache_size = 24GB # 设置为系统内存的 75%
# 并行查询
max_parallel_workers_per_gather = 4 # 单个 Gather 最大并行度
max_parallel_workers = 8 # 全局最大并行工作进程
parallel_tuple_cost = 0.1 # 并行传输元组的代价
统计信息
-- 提高统计信息精度(高选择性列)
ALTER TABLE users ALTER COLUMN email SET STATISTICS 1000;
-- 手动更新统计信息
ANALYZE users;
-- 查看统计信息
SELECT * FROM pg_stats WHERE tablename = 'users';
查询优化技巧
1. 使用 EXPLAIN 分析计划
-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE id = 1;
-- 查看实际执行统计
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE id = 1;
输出示例:
Index Scan using users_pkey on users (cost=0.29..8.31 rows=1 width=100) (actual time=0.012..0.013 rows=1 loops=1)
Index Cond: (id = 1)
Buffers: shared hit=4
Planning Time: 0.084 ms
Execution Time: 0.032 ms
2. 强制使用索引
-- 禁用顺序扫描(调试用)
SET enable_seqscan = off;
-- 查看计划是否改变
EXPLAIN SELECT * FROM users WHERE age > 30;
3. 优化连接查询
-- 避免隐式类型转换(导致索引失效)
-- 错误:WHERE user_id = '123'(user_id 为 integer 类型)
-- 正确:WHERE user_id = 123
-- 使用 JOIN 替代子查询
-- 低效:
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
-- 高效:
SELECT DISTINCT u.* FROM users u JOIN orders o ON u.id = o.user_id;
4. 分区表优化
-- 创建分区表
CREATE TABLE orders (
id SERIAL,
created_at TIMESTAMP NOT NULL,
amount NUMERIC
) PARTITION BY RANGE (created_at);
-- 创建分区
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
-- 查询时自动剪裁分区
EXPLAIN SELECT * FROM orders WHERE created_at >= '2024-01-15';
-- 输出显示仅扫描 orders_2024_01 分区
总结
Optimizer 模块是 PostgreSQL 的智能核心,其设计体现了以下原则:
- 基于代价的优化(CBO):通过统计信息和代价模型选择最优计划
- 动态规划与启发式结合:小规模问题精确求解,大规模问题近似求解
- 可扩展性:支持自定义索引访问方法、代价估算函数
- 并行化:充分利用多核 CPU 资源
理解优化器的工作原理,有助于:
- 编写高效的 SQL 查询
- 诊断性能问题(如次优计划)
- 调优数据库配置参数
- 设计合理的索引策略