在线学习在数据库优化中的应用当查询模式变化时模型如何不刻舟求剑一、当查询模式漂移时你的索引还在刻舟求剑数据库优化器的核心职责是在给定的统计信息下为每一条SQL选择最优的执行计划。传统优化器依赖离线收集的直方图和基数估计然后通过代价模型选择最优路径。这个范式在查询模式稳定时运转良好但在在线业务中频繁遭遇查询模式漂移。一个典型的场景是白天是高频点查的OLTP流量深夜跑批作业切成了大表扫描凌晨又有实时报表的聚合查询插进来。统计信息是凌晨3点ANALYZE的产物到了上午10点早已过时。优化器拿着过时的统计信息做出错误选择——原本走索引覆盖的查询变成了全表扫描原本Hash Join最优的场景却选了Nested Loop。更棘手的是新上线的业务会引入全新的查询模板这些模板从未出现在历史训练集中模型根本没见过。问题的本质在于离线训练的优化模型假设数据分布和查询模式是静态的但生产环境恰恰是动态的。Model Drift带来的性能退化远比人们想象的严重。根据在生产环境的观测在一个日均有300万条查询的MySQL集群上统计信息超过6小时不更新约12%的查询会出现执行计划退化平均响应时间增加3-8倍。传统的解决方式是缩短ANALYZE周期但ANALYZE本身是资源密集型操作在生产高峰期执行可能导致锁竞争和性能抖动。需要一种能在查询运行时持续学习、动态调整决策的机制。二、在线学习让优化器在运行中持续进化在线学习的核心思想是在模型部署后根据实时反馈持续更新模型参数使模型自适应数据分布的变化。在数据库优化领域这可以建模为一个多臂Bandit问题每个候选执行计划是一个臂查询的响应时间或资源消耗是奖励优化器的目标是最小化累积悔值——即实际选择的代价与最优选择代价之差。常用的在线学习策略包括ε-Greedy、UCB和Thompson Samplingε-Greedy以概率ε随机探索新计划以概率1-ε选择当前最优计划。实现简单但探索效率低——对所有非最优计划一视同仁地随机采样无法区分可能还不错的和肯定很差的。UCB为每个计划维护一个置信上界选择UCB值最大的计划。UCB值由估计均值和探索激励两部分组成不确定度高的计划会获得额外的探索机会。这很好地平衡了探索与利用。Thompson Sampling是贝叶斯方法为每个计划的奖励维护一个后验分布每次决策时从各后验分布中采样选择采样值最大的计划。其渐近收敛性质优于UCB但在数据库场景中需要精心选择先验分布——通常用Gamma分布建模响应时间。三、一个在生产上跑的轻量级Bandit框架以下是一个基于Thompson Sampling的查询计划选择器实现针对生产环境做了精简和异常保护import numpy as np from collections import defaultdict from dataclasses import dataclass from typing import Dict, List, Optional import logging import time logger logging.getLogger(__name__) dataclass class PlanArm: 每个候选执行计划的状态 alpha: float 1.0 # Gamma分布的shape参数 beta: float 100.0 # Gamma分布的rate参数初始均值alpha/beta0.01s executions: int 0 last_selected: float 0.0 def expected_latency(self) - float: return self.alpha / max(self.beta, 1e-9) def sample(self) - float: Thompson采样 return np.random.gamma(self.alpha, 1.0 / max(self.beta, 1e-9)) class OnlinePlanSelector: 基于Thompson Sampling的在线查询计划选择器 def __init__(self, decay_factor: float 0.95, cold_start_threshold: int 5): self.templates: Dict[str, Dict[str, PlanArm]] defaultdict(dict) self.decay_factor decay_factor self.cold_start_threshold cold_start_threshold def select_plan(self, template_id: str, plan_ids: List[str]) - Optional[str]: 为给定查询模板选择执行计划 arms self.templates[template_id] # 初始化未见过的计划 for pid in plan_ids: if pid not in arms: arms[pid] PlanArm() candidates [arms[pid] for pid in plan_ids] # 冷启动阶段轮流探索 total_execs sum(arm.executions for arm in candidates) if total_execs self.cold_start_threshold * len(plan_ids): least_tried min(plan_ids, keylambda p: arms[p].executions) logger.info(fCold start: selecting {least_tried} for {template_id}) return least_tried # Thompson Sampling: 采样→选最优 samples [(pid, arms[pid].sample()) for pid in plan_ids] best_plan min(samples, keylambda x: x[1])[0] return best_plan def update_feedback(self, template_id: str, plan_id: str, latency_ms: float, success: bool True): 根据查询执行反馈更新模型 arm self.templates[template_id].get(plan_id) if arm is None: return # 对失败或超时给予惩罚 effective_latency latency_ms if success else latency_ms * 10.0 # 历史衰减让模型更快适应模式变化 if arm.executions 0 and self.decay_factor 1.0: arm.alpha * self.decay_factor arm.beta * self.decay_factor # Gamma后验更新观测到一次延迟latency # 假设响应时间 ~ Gamma(k, θ)共轭先验 arm.alpha 1.0 arm.beta effective_latency / 1000.0 # 转换为秒 arm.executions 1 arm.last_selected time.time() logger.debug( fUpdated {template_id}/{plan_id}: fexpected{arm.expected_latency()*1000:.1f}ms, fexecutions{arm.executions} ) def prune_stale_arms(self, ttl_seconds: float 3600.0): 清理长时间未使用的计划 now time.time() stale_templates [] for tid, arms in self.templates.items(): arms_to_delete [ pid for pid, arm in arms.items() if now - arm.last_selected ttl_seconds and arm.executions 10 ] for pid in arms_to_delete: del arms[pid] logger.info(fPruned stale arm: {tid}/{pid}) if not arms: stale_templates.append(tid) for tid in stale_templates: del self.templates[tid]这个框架的关键设计考量衰减因子0.95的衰减率让模型在约20次观测后遗忘一半的历史信息能跟上查询模式的季节性变化。冷启动保护前N次执行强制轮流尝试所有候选计划避免过早收敛到次优解。失败惩罚对超时或错误的查询加重惩罚防止模型在劣化计划上头铁。TTL清理及时回收不再使用的计划状态控制内存占用。四、在线学习不是银弹收敛速度与稳定性的永恒博弈在线学习的理论优雅不假但工程落地面临几个必须正视的挑战。收敛速度与探索代价是核心矛盾。Thompson Sampling理论上对数悔值最优但前提是奖励分布假设正确。实际查询延迟常是长尾分布——少数查询因锁等待、IO抖动出现秒级延迟。这些离群点会让Gamma分布的均值大幅上移导致一个实际很优秀的计划被打入冷宫。解决办法是对反馈做截断处理比如将延迟超过P99阈值的观测值钳制到阈值或使用中位数替代均值。多查询模板耦合的隐患容易被忽略。一个模板的查询延迟可能受另一个模板的并发量影响。比如模板A和模板B同时大量执行时造成Buffer Pool争抢单独看每个模板的延迟都偏高Bandit可能把两个模板都切换到全表扫描——结果更糟。这个问题单靠每个模板独立的Bandit无法解决需要考虑全局资源感知。A/B测试陷阱在线学习的收敛过程本质上是在生产环境做持续的A/B实验。这个过程必然会产生一定比例的劣化查询。如果劣化查询恰好落在核心业务链路上影响面会超出预期。因此必须有熔断机制——当P99延迟超过阈值时立即回退到默认优化器选择同时降低探索率。五、总结在线学习为数据库优化器提供了在动态负载下自适应的能力。Thompson Sampling框架用几十行代码就能实现一个基础的在线计划选择器通过衰减因子和冷启动保护处理查询模式漂移。但这不是一个开箱即用的方案需要根据实际业务场景精细调节探索率、衰减速度和异常处理策略。从实践经验来看对于查询模式存在明显昼夜周期的业务如电商、金融在线学习能带来15%-30%的P99延迟改善但对于查询模式本身高度随机的场景如BI Ad-hoc查询效果会打折扣。关键在于判断业务是否真的存在可学习的模式漂移而非为用而用。