AI索引推荐的边界与陷阱:为什么AI建议的索引不一定是对的
AI辅助索引推荐是数据库智能化中最受期待的功能之一。想象一下:AI自动分析慢查询日志和数据分布,给出建索引建议,DBA一键执行——听起来完美。但在实际使用中,AI的索引建议远非"一键最优"。
一、当AI建议了18个索引:一次差点发生的索引爆炸
今年年初,团队在一个用户行为分析库上试用了某AI索引推荐工具。工具分析了慢查询日志后,为一张200GB的表推荐了18个新索引。初看似乎每个建议都有道理:每个慢查询确实能从对应的索引中获益。但仔细分析后发现:
- 18个索引中有6个是单列索引,可以通过调整复合索引的列顺序合并为3个
- 3个索引覆盖的查询一周只执行几次,却会在每次写入时产生索引维护开销
- 2个索引包含低基数列(如status字段只有3个值),选择性极差
如果全盘接受这18个索引,写入性能将下降约30%,存储空间增加40%,而查询性能的提升可能只有10%。
二、索引推荐的决策权衡模型
三、智能索引推荐验证系统
#!/usr/bin/env python3 """AI索引推荐评估器:验证AI建议的索引是否值得创建""" import pymysql from typing import Dict, List, Tuple, Optional from dataclasses import dataclass, field from datetime import datetime, timedelta @dataclass class IndexCandidate: table_name: str columns: List[str] index_type: str # BTREE, HASH, FULLTEXT estimated_benefit: float # AI估算的收益(查询时间节省ms) source_queries: List[str] = field(default_factory=list) @dataclass class IndexEvaluation: candidate: IndexCandidate query_benefit_score: float # 查询收益分 (0-100) write_cost_score: float # 写入成本分 (0-100, 越低越好) storage_cost_gb: float cardinality_score: float # 选择性分 (0-100) overall_score: float # 综合评分 recommendation: str # CREATE / REJECT / GRAY_TEST class IndexRecommenderValidator: def __init__(self, db_config: dict): self.db_config = db_config def _connect(self): try: return pymysql.connect(**self.db_config) except pymysql.Error as e: raise ConnectionError(f"DB connection failed: {e}") def analyze_cardinality(self, table: str, columns: List[str]) -> float: """分析列的选择性(基数/总行数)""" conn = self._connect() try: with conn.cursor() as cur: for col in columns: try: cur.execute(f""" SELECT COUNT(DISTINCT `{col}`) / GREATEST(COUNT(*), 1) * 100 FROM `{table}` """) ratio = cur.fetchone()[0] return float(ratio) except pymysql.Error: continue return 0 finally: conn.close() def analyze_query_benefit(self, candidate: IndexCandidate) -> float: """分析查询收益:有多少查询能从该索引中受益""" conn = self._connect() try: benefits = [] with conn.cursor() as cur: for query_pattern in candidate.source_queries: # 用EXPLAIN分析索引效果 explain_sql = f"EXPLAIN {query_pattern}" try: cur.execute(explain_sql) plan = cur.fetchall() # 简化的收益评估 if plan: rows = plan[0][8] if len(plan[0]) > 8 else 0 if rows: # 扫描行数多=收益大 benefit = min(100, (rows or 0) / 10000 * 10) benefits.append(benefit) except pymysql.Error: continue if benefits: return sum(benefits) / len(benefits) return 0 finally: conn.close() def analyze_write_cost(self, candidate: IndexCandidate) -> float: """分析写入成本:索引列越多,写入开销越大""" col_count = len(candidate.columns) col_length_penalty = sum(len(c) for c in candidate.columns) * 0.5 base_cost = 10 # 基础写入开销 cost = base_cost + col_count * 8 + col_length_penalty # 低基数列的写入成本相对更高(收益小) cardinality = self.analyze_cardinality( candidate.table_name, candidate.columns ) if cardinality < 10: cost *= 1.5 # 低基数列写入成本更高 return min(100, cost) def estimate_storage(self, candidate: IndexCandidate, table_size_gb: float) -> float: """估算索引存储开销""" col_count = len(candidate.columns) desc_penalty = "DESC" in str(candidate.columns).upper() base_ratio = 0.15 # 基础索引空间占比 factor = col_count * 0.8 if desc_penalty: factor *= 1.2 return table_size_gb * base_ratio * factor def evaluate(self, candidate: IndexCandidate, table_size_gb: float = 100) -> IndexEvaluation: """综合评估一个索引候选""" # 多维度评分 query_benefit = self.analyze_query_benefit(candidate) write_cost = self.analyze_write_cost(candidate) storage = self.estimate_storage(candidate, table_size_gb) cardinality = self.analyze_cardinality( candidate.table_name, candidate.columns ) # 综合评分公式 overall = ( query_benefit * 0.4 + (100 - write_cost) * 0.2 + (100 - min(100, storage * 10)) * 0.1 + min(cardinality, 100) * 0.3 ) # 推荐决策 if overall >= 70: rec = "CREATE" elif overall >= 40: rec = "GRAY_TEST" else: rec = "REJECT" return IndexEvaluation( candidate=candidate, query_benefit_score=round(query_benefit, 1), write_cost_score=round(write_cost, 1), storage_cost_gb=round(storage, 2), cardinality_score=round(cardinality, 1), overall_score=round(overall, 1), recommendation=rec ) def batch_evaluate(self, candidates: List[IndexCandidate], table_size_gb: float = 100) -> List[IndexEvaluation]: """批量评估多个候选索引""" results = [] for candidate in candidates: try: evaluation = self.evaluate(candidate, table_size_gb) results.append(evaluation) except Exception as e: print(f"[ERROR] 评估{candidate.table_name}.{candidate.columns}失败: {e}") # 按综合评分排序 results.sort(key=lambda x: x.overall_score, reverse=True) return results def generate_report(self, evaluations: List[IndexEvaluation]) -> str: """生成评估报告""" lines = [] lines.append("=" * 60) lines.append("AI索引推荐评估报告") lines.append("=" * 60) create = [e for e in evaluations if e.recommendation == "CREATE"] gray = [e for e in evaluations if e.recommendation == "GRAY_TEST"] reject = [e for e in evaluations if e.recommendation == "REJECT"] lines.append(f"\nCREATE: {len(create)} | GRAY_TEST: {len(gray)} | REJECT: {len(reject)}") if create: lines.append("\n[建议创建]") for e in create: lines.append(f" {e.candidate.table_name}({','.join(e.candidate.columns)}) " f"- 评分: {e.overall_score}") if reject: lines.append("\n[建议拒绝]") for e in reject: lines.append(f" {e.candidate.table_name}({','.join(e.candidate.columns)}) " f"- 评分: {e.overall_score}") if e.cardinality_score < 10: lines.append(f" 原因: 选择性过低 ({e.cardinality_score})") if e.write_cost_score > 60: lines.append(f" 原因: 写入成本过高 ({e.write_cost_score})") return "\n".join(lines) if __name__ == "__main__": validator = IndexRecommenderValidator({ "host": "localhost", "user": "root", "password": "", "charset": "utf8mb4" }) candidates = [ IndexCandidate("orders", ["user_id"], "BTREE", estimated_benefit=100, source_queries=[ "SELECT * FROM orders WHERE user_id = 123" ]), IndexCandidate("orders", ["status"], "BTREE", estimated_benefit=50, source_queries=[ "SELECT * FROM orders WHERE status = 'pending'" ]), IndexCandidate("orders", ["user_id", "status", "created_at", "amount"], "BTREE", estimated_benefit=200, source_queries=[ "SELECT * FROM orders WHERE user_id = 1 AND status = 'done' ORDER BY created_at" ]), ] results = validator.batch_evaluate(candidates) print(validator.generate_report(results))四、AI索引推荐的四个核心边界
边界一:无法感知写入负载。AI分析慢查询日志时看到的只是SELECT语句,完全不知道这张表的写入频率。在写入密集型表上创建过多索引是灾难性的。
边界二:无法评估索引间的覆盖关系。AI可能为WHERE a=1推荐索引(a),又为WHERE a=1 AND b=2推荐索引(a,b)。实际上后者已经覆盖了前者的需求。
边界三:低基数列的误判。status、type、is_deleted等低基数列在查询日志中出现频率很高,但B-Tree索引对它们的帮助微乎其微。
边界四:缺乏长期视角。AI看到的只是当前的查询模式,无法预判业务逻辑变更对索引需求的影响。
五、总结
AI索引推荐工具的正确使用方式是辅助分析而非替代决策。DBA的职责是将AI的建议结合写入负载、表规模、列基数、业务重要性等AI看不到的维度做综合判断。经验法则:如果AI推荐了超过5个索引,先怀疑是不是分析出错了。