PostgreSQL-02-Optimizer-优化器

模块概览

模块职责

Optimizer(优化器)是 PostgreSQL 查询处理的核心模块,负责将 Analyzer 输出的查询树(Query)转换为高效的执行计划(Plan)。优化器通过路径生成、代价估算和计划选择,在保证查询语义正确的前提下,寻找执行代价最小的方案。

核心能力

  1. 路径生成(Path Generation):枚举所有可能的执行路径

    • 表扫描路径:顺序扫描(SeqScan)、索引扫描(IndexScan)、索引唯一扫描(IndexOnlyScan)
    • 连接路径:嵌套循环(NestLoop)、哈希连接(HashJoin)、归并连接(MergeJoin)
    • 聚合路径:哈希聚合(HashAgg)、分组聚合(GroupAgg)
    • 排序路径:快速排序(Sort)、增量排序(Incremental Sort)
  2. 代价估算(Cost Estimation):计算每条路径的执行代价

    • 启动代价(Startup Cost):返回第一行的代价
    • 总代价(Total Cost):返回所有行的代价
    • 基于统计信息:pg_statistic 表中的直方图、MCV(Most Common Values)
    • 参数化代价:seq_page_costrandom_page_costcpu_tuple_cost
  3. 连接顺序优化(Join Order Optimization):确定多表连接的最优顺序

    • 动态规划(DP):表数 ≤ 12 时使用
    • 遗传算法(GEQO):表数 > 12 时使用(避免组合爆炸)
    • 左深树(Left-Deep Tree):优先生成可流水线执行的计划
  4. 并行查询规划(Parallel Query Planning):生成多进程并行执行的计划

    • Parallel Seq Scan:并行顺序扫描
    • Parallel Hash Join:并行哈希连接
    • Gather 节点:汇聚并行工作进程的结果
  5. 分区剪裁(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

架构说明

分层设计

  1. 入口层(standard_planner):

    • 初始化规划器全局状态(PlannerGlobal)
    • 调用子查询规划器处理查询
    • 处理扩展点钩子(planner_hook)
  2. 前置处理层(Preprocessing):

    • 表达式简化:常量折叠、子查询提升
    • 连接树规范化:外连接转换、连接条件重排
    • LIMIT 提取:计算元组分数(tuple_fraction)
  3. 核心规划层(grouping_planner):

    • 路径生成:枚举所有可行的执行路径
    • 代价估算:计算每条路径的代价
    • 路径选择:选择代价最小的路径
  4. 计划生成层(create_plan):

    • 将路径(Path)转换为执行计划节点(Plan)
    • 设置节点参数(如目标列表、条件表达式)
    • 设置变量引用(Var 节点的 varno/varattno)

关键数据结构

  • PlannerInfo:规划器上下文,包含查询树、关系列表、路径列表等
  • RelOptInfo:关系优化信息,记录表或子查询的所有可行路径
  • Path:执行路径的抽象表示,包含代价、排序键等信息
  • Plan:执行计划节点,由执行器解释执行

边界条件

并发性

  • 每个查询独立规划,无跨查询共享状态
  • 访问系统表和统计信息时,使用 MVCC 快照
  • 统计信息可能滞后(最终一致),不影响正确性,仅影响性能

输入限制

  • 连接表数:动态规划算法处理 ≤ 12 表,超过后使用遗传算法
  • 查询复杂度:超深嵌套子查询可能导致规划时间过长
  • 参数数量:准备语句参数数量无硬性限制

性能关键点

  • 规划时间:复杂查询规划可能占总执行时间的 10%-50%
  • 统计信息质量:过期或缺失的统计信息导致次优计划
  • 配置参数random_page_costeffective_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;
}

动态规划原理

动态规划通过保存子问题的最优解,避免重复计算:

  1. 子问题定义RelOptInfo(S) 表示关系集合 S 的最优连接结果
  2. 状态转移RelOptInfo(S ∪ T) = min { RelOptInfo(S) ⨝ RelOptInfo(T) }
  3. 最优子结构:全局最优解包含子问题的最优解

时间复杂度: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 的智能核心,其设计体现了以下原则:

  1. 基于代价的优化(CBO):通过统计信息和代价模型选择最优计划
  2. 动态规划与启发式结合:小规模问题精确求解,大规模问题近似求解
  3. 可扩展性:支持自定义索引访问方法、代价估算函数
  4. 并行化:充分利用多核 CPU 资源

理解优化器的工作原理,有助于:

  • 编写高效的 SQL 查询
  • 诊断性能问题(如次优计划)
  • 调优数据库配置参数
  • 设计合理的索引策略