第17课:查询优化(CBO/RBO)

查询处理 第17课 / 共25课

📖 课程概述

查询优化器是数据库最复杂的组件之一。它负责从众多等价的查询计划中选择代价最小的执行方案。本课深入讲解两种优化范式:基于规则的优化(RBO)和基于代价的优化(CBO),实现启发式规则和代价模型。

本课目标:实现RBO规则(谓词下推、投影消除、连接重排序)和CBO代价模型。

⚡ RBO vs CBO

基于规则的优化(RBO): 固定规则,不依赖统计数据 1. 谓词下推: 尽早过滤 2. 投影消除: 只选需要的列 3. 连接重排序: 小表驱动 4. 子查询展开: 转为JOIN 5. 常量折叠: 预计算常量表达式 优化前: 优化后: Project(name) Project(name) └── Join(users, orders) └── Join(users, orders) └── Filter(age>20) ├── Filter(age>20) │ └── Scan(users) └── IndexScan(orders) 基于代价的优化(CBO): 估算每个计划的代价,选最优 代价 = CPU代价 + I/O代价 + 内存代价 表统计信息: - 行数(N), 平均行长(L) - 唯一值数(NDV), 空值数 - 最小/最大值, 直方图 - 索引选择性 代价估算示例: 全表扫描: N * L / page_size (I/O次数) 索引查找: tree_height + 1 (I/O次数) 嵌套循环连接: N_outer * cost_inner 哈希连接: (N1 + N2) / page_size + build_hash

💻 C语言实现:查询优化器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <float.h>

#define MAX_TABLES    16
#define MAX_COLUMNS   32
#define MAX_PREDICATES 16

// 逻辑算子类型
typedef enum {
    OP_SCAN, OP_INDEX_SCAN, OP_FILTER, OP_PROJECT,
    OP_SORT, OP_LIMIT, OP_NL_JOIN, OP_HASH_JOIN,
    OP_MERGE_JOIN, OP_AGGREGATE
} OpType;

const char* op_type_str[] = {"Scan","IdxScan","Filter","Project",
    "Sort","Limit","NLJoin","HashJoin","MergeJoin","Aggregate"};

// 逻辑算子
typedef struct OpNode {
    OpType type;
    char   table[MAX_COLUMNS];
    char   columns[MAX_COLUMNS][32];
    int    num_columns;
    char   predicate[128];
    double estimated_rows;
    double estimated_cost;
    struct OpNode* left;
    struct OpNode* right;
    struct OpNode* parent;
} OpNode;

OpNode* op_create(OpType type) {
    OpNode* n = calloc(1, sizeof(OpNode));
    n->type = type;
    return n;
}

// 表统计信息
typedef struct {
    char   name[32];
    int    num_rows;
    int    avg_row_size;
    int    num_pages;
    int    has_index;
    double selectivity;  // 默认选择性
} TableStats;

TableStats table_stats[] = {
    {"users",    100000, 128, 4000, 1, 0.1},
    {"orders",   1000000, 64, 16000, 1, 0.05},
    {"products", 10000, 256, 640, 1, 0.2},
    {"reviews",  500000, 96, 12000, 0, 0.15},
};

TableStats* find_stats(const char* table) {
    for (int i = 0; i < 4; i++)
        if (strcmp(table_stats[i].name, table) == 0) return &table_stats[i];
    return NULL;
}

// ===== RBO: 谓词下推 =====
OpNode* push_down_filter(OpNode* plan) {
    if (!plan) return NULL;
    plan->left = push_down_filter(plan->left);
    plan->right = push_down_filter(plan->right);

    if (plan->type != OP_FILTER) return plan;
    OpNode* filter = plan;
    OpNode* child = plan->left;

    // Filter下面是Join → 尝试下推到Join的子节点
    if (child && (child->type == OP_NL_JOIN || child->type == OP_HASH_JOIN)) {
        // 简化:检查谓词引用的表
        // 如果只引用左表,下推到左子树
        // 如果只引用右表,下推到右子树
        // 否则保留在Join上方
        printf("  [RBO] 尝试谓词下推: %s\n", filter->predicate);

        // 简化实现:随机决定是否下推
        if (child->left && child->left->type == OP_SCAN) {
            OpNode* new_filter = op_create(OP_FILTER);
            strcpy(new_filter->predicate, filter->predicate);
            new_filter->estimated_rows = child->left->estimated_rows * 0.3;
            new_filter->left = child->left;
            child->left = new_filter;
            printf("  [RBO] 谓词下推到左子树\n");
            return child;  // 移除原Filter
        }
    }
    return plan;
}

// ===== RBO: 投影消除 =====
OpNode* eliminate_project(OpNode* plan) {
    if (!plan) return NULL;
    plan->left = eliminate_project(plan->left);
    plan->right = eliminate_project(plan->right);

    // 如果Project下面也是Project,合并
    if (plan->type == OP_PROJECT && plan->left &&
        plan->left->type == OP_PROJECT) {
        printf("  [RBO] 消除冗余Project\n");
        OpNode* inner = plan->left;
        plan->left = inner->left;
        free(inner);
    }
    return plan;
}

// ===== CBO: 代价估算 =====
double estimate_scan_cost(TableStats* stats) {
    return (double)stats->num_pages;  // 一次I/O一页
}

double estimate_index_scan_cost(TableStats* stats, double selectivity) {
    double tree_height = 3.0;  // B+树高度
    return tree_height + stats->num_pages * selectivity;
}

double estimate_nl_join_cost(OpNode* outer, OpNode* inner) {
    return outer->estimated_rows * (inner->estimated_cost + 1.0);
}

double estimate_hash_join_cost(OpNode* left, OpNode* right) {
    double build_cost = left->estimated_rows * 0.01;  // hash build
    double probe_cost = right->estimated_rows * 0.01; // hash probe
    return left->estimated_cost + right->estimated_cost + build_cost + probe_cost;
}

double estimate_sort_cost(OpNode* child) {
    double n = child->estimated_rows;
    return child->estimated_cost + n * log2(n) * 0.001;  // n*log(n)*CPU
}

// 计算整棵计划树的代价
double compute_cost(OpNode* plan) {
    if (!plan) return 0;

    double left_cost = compute_cost(plan->left);
    double right_cost = compute_cost(plan->right);

    switch (plan->type) {
    case OP_SCAN: {
        TableStats* s = find_stats(plan->table);
        plan->estimated_cost = s ? estimate_scan_cost(s) : 1000;
        plan->estimated_rows = s ? s->num_rows : 10000;
        break;
    }
    case OP_INDEX_SCAN: {
        TableStats* s = find_stats(plan->table);
        plan->estimated_cost = s ? estimate_index_scan_cost(s, 0.05) : 50;
        plan->estimated_rows = s ? (int)(s->num_rows * 0.05) : 500;
        break;
    }
    case OP_FILTER: {
        plan->estimated_cost = left_cost + 0.001 * plan->left->estimated_rows;
        plan->estimated_rows = plan->left->estimated_rows * 0.3;
        break;
    }
    case OP_PROJECT: {
        plan->estimated_cost = left_cost + 0.0001 * plan->left->estimated_rows;
        plan->estimated_rows = plan->left->estimated_rows;
        break;
    }
    case OP_SORT: {
        plan->estimated_cost = estimate_sort_cost(plan->left);
        plan->estimated_rows = plan->left->estimated_rows;
        break;
    }
    case OP_NL_JOIN: {
        plan->estimated_cost = estimate_nl_join_cost(plan->left, plan->right);
        plan->estimated_rows = plan->left->estimated_rows * plan->right->estimated_rows * 0.01;
        break;
    }
    case OP_HASH_JOIN: {
        plan->estimated_cost = estimate_hash_join_cost(plan->left, plan->right);
        plan->estimated_rows = plan->left->estimated_rows * plan->right->estimated_rows * 0.01;
        break;
    }
    case OP_LIMIT: {
        plan->estimated_cost = left_cost;
        plan->estimated_rows = 10;  // 简化
        break;
    }
    default:
        plan->estimated_cost = left_cost + right_cost;
        break;
    }
    return plan->estimated_cost;
}

// ===== 连接重排序(CBO) =====
// 简化:尝试不同连接顺序,选代价最小的
OpNode* reorder_join(OpNode* plan) {
    if (!plan) return NULL;
    if (plan->type != OP_HASH_JOIN && plan->type != OP_NL_JOIN) return plan;

    // 尝试交换左右
    OpNode* plan1 = plan;
    OpNode* plan2 = op_create(plan->type);
    plan2->left = plan->right;
    plan2->right = plan->left;

    double cost1 = compute_cost(plan1);
    double cost2 = compute_cost(plan2);

    if (cost2 < cost1) {
        printf("  [CBO] 连接重排序: 交换左右 (代价 %.1f → %.1f)\n", cost1, cost2);
        free(plan);
        return plan2;
    }
    free(plan2);
    return plan;
}

// 打印计划
void print_plan(OpNode* plan, int depth) {
    if (!plan) return;
    printf("%*s%s", depth*2, "", op_type_str[plan->type]);
    if (plan->table[0]) printf(" (%s)", plan->table);
    if (plan->predicate[0]) printf(" [%s]", plan->predicate);
    printf(" rows=%.0f cost=%.1f\n", plan->estimated_rows, plan->estimated_cost);
    print_plan(plan->left, depth + 1);
    print_plan(plan->right, depth + 1);
}

int main() {
    printf("╔══════════════════════════════════════╗\n");
    printf("║   查询优化器 (RBO + CBO)            ║\n");
    printf("╚══════════════════════════════════════╝\n\n");

    // 构建初始查询计划:
    // SELECT u.name FROM users u JOIN orders o ON u.id=o.uid WHERE u.age > 25

    printf("--- 初始逻辑计划 ---\n");
    OpNode* plan = op_create(OP_PROJECT);
    strcpy(plan->columns[0], "name"); plan->num_columns = 1;

    OpNode* filter = op_create(OP_FILTER);
    strcpy(filter->predicate, "age > 25");

    OpNode* join = op_create(OP_NL_JOIN);

    OpNode* scan_users = op_create(OP_SCAN);
    strcpy(scan_users->table, "users");

    OpNode* scan_orders = op_create(OP_SCAN);
    strcpy(scan_orders->table, "orders");

    join->left = scan_users;
    join->right = scan_orders;
    filter->left = join;
    plan->left = filter;

    compute_cost(plan);
    print_plan(plan, 0);

    // RBO优化
    printf("\n--- RBO: 谓词下推 ---\n");
    plan->left = push_down_filter(plan->left);
    compute_cost(plan);
    print_plan(plan, 0);

    // CBO: 选择连接方式
    printf("\n--- CBO: 连接方式选择 ---\n");
    OpNode* new_join = op_create(OP_HASH_JOIN);
    new_join->left = join->left;
    new_join->right = join->right;
    free(join);
    if (plan->left && plan->left->type == OP_FILTER) {
        plan->left->left = new_join;
    }
    compute_cost(plan);
    print_plan(plan, 0);

    // CBO: 连接重排序
    printf("\n--- CBO: 连接重排序 ---\n");
    new_join = reorder_join(new_join);
    compute_cost(plan);
    print_plan(plan, 0);

    // CBO: 索引扫描替代全表扫描
    printf("\n--- CBO: 索引扫描 ---\n");
    OpNode* idx_scan = op_create(OP_INDEX_SCAN);
    strcpy(idx_scan->table, "users");
    // 替换Scan为IndexScan
    if (plan->left && plan->left->type == OP_FILTER) {
        if (plan->left->left && plan->left->left->left &&
            plan->left->left->left->type == OP_SCAN) {
            free(plan->left->left->left);
            plan->left->left->left = idx_scan;
        }
    }
    compute_cost(plan);
    print_plan(plan, 0);

    printf("\n✅ 查询优化器运行完成\n");
    return 0;
}

🐍 Python实现:CBO代价模型

"""
CBO代价模型与计划枚举
动态规划搜索最优连接顺序
"""
from dataclasses import dataclass, field
from typing import List, Dict, Set, Tuple, Optional
from itertools import permutations
import math

@dataclass
class TableStats:
    name: str
    rows: int
    pages: int
    has_index: bool = False
    ndv: Dict[str, int] = field(default_factory=dict)  # column → distinct values

@dataclass
class PlanNode:
    op: str  # scan, idx_scan, filter, project, hash_join, nl_join, sort
    table: str = ""
    predicate: str = ""
    cost: float = 0.0
    rows: float = 0.0
    left: Optional['PlanNode'] = None
    right: Optional['PlanNode'] = None

    def explain(self, depth=0) -> str:
        lines = [f"{'  '*depth}{self.op}"
                 + (f"({self.table})" if self.table else "")
                 + (f"[{self.predicate}]" if self.predicate else "")
                 + f" rows={self.rows:.0f} cost={self.cost:.1f}"]
        if self.left: lines.append(self.left.explain(depth+1))
        if self.right: lines.append(self.right.explain(depth+1))
        return "\n".join(lines)

class CostModel:
    PAGE_SIZE = 8192
    CPU_TUPLE_COST = 0.01
    CPU_INDEX_COST = 0.005
    IO_PAGE_COST = 1.0

    def __init__(self, stats: Dict[str, TableStats]):
        self.stats = stats

    def scan_cost(self, table: str) -> Tuple[float, float]:
        s = self.stats[table]
        io = s.pages * self.IO_PAGE_COST
        cpu = s.rows * self.CPU_TUPLE_COST
        return io + cpu, float(s.rows)

    def idx_scan_cost(self, table: str, selectivity: float) -> Tuple[float, float]:
        s = self.stats[table]
        io = 3 * self.IO_PAGE_COST + s.pages * selectivity * self.IO_PAGE_COST
        cpu = s.rows * selectivity * self.CPU_INDEX_COST
        return io + cpu, s.rows * selectivity

    def filter_cost(self, child: PlanNode, selectivity: float) -> Tuple[float, float]:
        cpu = child.rows * self.CPU_TUPLE_COST
        return child.cost + cpu, child.rows * selectivity

    def hash_join_cost(self, left: PlanNode, right: PlanNode) -> Tuple[float, float]:
        build = left.rows * self.CPU_TUPLE_COST * 2
        probe = right.rows * self.CPU_TUPLE_COST
        io = (left.rows / 100 + right.rows / 100) * self.IO_PAGE_COST
        output_rows = min(left.rows * right.rows * 0.01, left.rows * 10)
        return left.cost + right.cost + build + probe + io, output_rows

    def nl_join_cost(self, left: PlanNode, right: PlanNode) -> Tuple[float, float]:
        inner_cost = right.cost if right.cost > 0 else 1
        output_rows = min(left.rows * right.rows * 0.01, left.rows * 10)
        return left.cost + left.rows * inner_cost, output_rows

    def sort_cost(self, child: PlanNode) -> Tuple[float, float]:
        n = child.rows
        cpu = n * math.log2(max(n, 2)) * self.CPU_TUPLE_COST * 2
        return child.cost + cpu, child.rows

# 枚举最优连接顺序
def find_best_join_order(tables: List[str], model: CostModel) -> PlanNode:
    """动态规划搜索最优连接顺序"""
    n = len(tables)
    if n == 1:
        cost, rows = model.scan_cost(tables[0])
        return PlanNode(op="scan", table=tables[0], cost=cost, rows=rows)

    best_plan = None
    best_cost = float('inf')

    # 尝试所有排列
    for perm in permutations(range(n)):
        # 构建左深树
        plan = PlanNode(op="scan", table=tables[perm[0]])
        plan.cost, plan.rows = model.scan_cost(tables[perm[0]])

        for i in range(1, n):
            right = PlanNode(op="scan", table=tables[perm[i]])
            right.cost, right.rows = model.scan_cost(tables[perm[i]])

            # 尝试Hash Join和NL Join
            hj = PlanNode(op="hash_join", left=plan, right=right)
            hj.cost, hj.rows = model.hash_join_cost(plan, right)

            nlj = PlanNode(op="nl_join", left=plan, right=right)
            nlj.cost, nlj.rows = model.nl_join_cost(plan, right)

            plan = hj if hj.cost < nlj.cost else nlj

        if plan.cost < best_cost:
            best_cost = plan.cost
            best_plan = plan

    return best_plan

# 测试
stats = {
    "users": TableStats("users", 100000, 4000, True, {"id": 100000, "age": 80}),
    "orders": TableStats("orders", 1000000, 16000, True, {"id": 1000000, "uid": 100000}),
    "products": TableStats("products", 10000, 640, True, {"id": 10000}),
    "reviews": TableStats("reviews", 500000, 12000, False, {"id": 500000}),
}

model = CostModel(stats)

print("=== 4表连接优化 ===\n")
plan = find_best_join_order(["users", "orders", "products", "reviews"], model)
print("最优计划:")
print(plan.explain())
print(f"\n总代价: {plan.cost:.1f}")
print("\n✅ CBO代价模型运行完成")

🔑 关键概念总结

📝 练习

  1. 实现基于动态规划的连接顺序枚举,支持5+表连接
  2. 添加直方图统计,提高选择性估算精度
  3. 实现子查询展开优化,将WHERE IN转为SEMI JOIN
  4. 对比RBO和CBO在TPC-H查询上的优化效果

🏆 成就解锁:优化大师

掌握查询优化,你已理解数据库最复杂的组件!

✅ RBO规则 · ✅ CBO代价模型 · ✅ 连接重排序