尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

被DBA骂了3天后,我手搓了一个AI索引推荐引擎,慢查询直接干掉90%!

被DBA骂了3天后,我手搓了一个AI索引推荐引擎,慢查询直接干掉90%! “墨夶订单系统CPU 100%了”我垂死病中惊坐起打开电脑一看又是那个该死的报表SQL。我反手加了个联合索引 (status, create_time, user_id)心想这下总该快了吧结果呢查询是快了点但写入直接雪崩订单表插入超时整个交易链路卡死。第二天早上DBA老李指着我的鼻子骂“你当索引是不要钱的啊加这么多联合索引写入时B树分裂的开销你算过吗瞎搞”我当场自闭。说实话做开发的谁没在索引上栽过跟头凭直觉加索引加了不生效玄学看了几篇博客加索引查询快了写入崩了翻车找DBA加索引被骂得狗血淋头憋屈直到后来我花了整整一个月把基于工作负载的AI智能索引推荐这套体系研究透了并手搓了一个推荐引擎。上线一周慢查询直接干掉90%DBA老李看我的眼神都温柔了。今天这篇文章就是我这一个月的全部心血。⚠️ 核心价值读完这篇你会彻底搞懂——为什么凭直觉加索引是死路一条AI是怎么基于工作负载给你推荐索引的不是大模型瞎猜是严谨的算法怎么从零手搓一个生产级的索引推荐引擎索引推荐的5个致命暗坑踩过一个就够你喝一壶的二、先搞懂为什么凭直觉加索引是死路一条2.1 加索引的三大玄学翻车现场在讲AI推荐之前咱们先复盘一下那些年踩过的坑。魔性比喻加索引就像给汽车加尾翼。加一个跑得快加十个车没飞起来油耗先爆了。 翻车现场1最左前缀原则没搞懂索引白加– 表里有个联合索引idx_a_b_c (a, b, c)– ❌ 傻操作查询条件没有a索引直接失效SELECT * FROM orders WHERE b 1 AND c 2;– 优化器你搁这儿跟我玩捉迷藏呢全表扫描走起– ❌ 傻操作a用了范围查询b和c的索引直接断掉SELECT * FROM orders WHERE a 10 AND b 1 AND c 2;– 优化器a用完后b和c只能靠全表扫了惊不惊喜 翻车现场2区分度太低索引变成废纸– 给 gender性别加索引CREATE INDEX idx_gender ON users(gender);– 查询女性用户SELECT * FROM users WHERE gender ‘F’;– 优化器一算女性占50%走索引还得回表不如直接全表扫描– 你的索引花了10GB磁盘空间一次都没被用过。纯纯的占着茅坑不拉屎。 翻车现场3写入放大查询爽了写入崩了– 为了一个报表查询加了个超宽联合索引CREATE INDEX idx_report ON orders(status, type, channel, region, create_time, amount);– 查询确实快了从5秒降到0.1秒。– 但是每次插入一条订单数据库要更新这个6列的B树– 高并发写入时B树频繁分裂CPU直接打满写入延迟从5ms飙到500ms。– 业务方墨夶下单怎么这么卡– 我……默默把索引删了2.2 核心痛点我们到底缺什么凭直觉加索引之所以翻车根本原因在于我们只看到了单条SQL没看到全局工作负载。金句来了索引不是为单条SQL服务的是为整个工作负载Workload服务的。一条SQL加个索引快了可能导致另外10条SQL的写入变慢。这就是为什么我们需要基于工作负载的智能索引推荐。三、什么是基于工作负载的AI智能索引推荐3.1 一句话说透 智能索引推荐 抓取所有SQL工作负载 提取特征 算法评估代价 给出最优索引组合它不是让ChatGPT瞎猜而是基于严谨的代价模型Cost-Based Model 和运筹学算法如贪心算法/背包问题在查询收益和写入/存储开销之间找到最优解。打个比方人工加索引 去饭店点菜凭感觉点可能点了一桌子菜结果吃不完。AI索引推荐 营养师根据你的体检报告工作负载精确计算卡路里代价给你配一套最完美的减脂餐索引组合。3.2 核心架构推荐引擎的四大模块一个生产级的索引推荐引擎必须包含以下四个模块flowchart TDA[1. 工作负载采集] --|慢查询日志/Audit Log| B[2. SQL特征提取与聚类]B --|提取WHERE/JOIN/ORDER BY| C[3. 候选索引生成]C --|生成所有可能的索引| D[4. 代价评估与贪心选择]D --|调用EXPLAIN计算收益| E[ 输出最优索引组合]style A fill:#4dabf7,color:#fff style B fill:#51cf66,color:#fff style C fill:#ffd43b,color:#333 style D fill:#ff6b6b,color:#fff style E fill:#845ef7,color:#fff接下来咱们一个模块一个模块扒代码直接上注释比代码还长四、核心代码实战手搓一个生产级索引推荐引擎老铁们泡好咖啡下面这段Python代码是整个文章的灵魂。我会用 sqlglot一个超强的SQL解析库和 psycopg2PostgreSQL驱动从零实现一个索引推荐引擎。4.1 模块一工作负载采集与SQL特征提取首先我们要从数据库的 pg_stat_statementsPG/金仓的慢查询统计视图中抓取真实的工作负载并解析出SQL的特征。模块一工作负载采集与SQL特征提取作者墨夶 | 生产环境实测可用依赖pip install sqlglot psycopg2-binaryimport sqlglotfrom sqlglot import expfrom collections import defaultdictimport reclass WorkloadAnalyzer:“”工作负载分析器负责从数据库抓取SQL并提取索引相关特征。 设计思想不要分析所有SQL只分析SELECT/UPDATE/DELETE并且要过滤掉系统自带的SQL和超短耗时的SQL。“”def init(self, db_conn): self.db_conn db_conn # 存储提取出的SQL特征 # 结构{table_name: {equality_cols: set(), range_cols: set(), # order_cols: set(), query_count: int, total_time: float}} self.table_features defaultdict(lambda: { equality_cols: defaultdict(int), # 等值查询列及频率 range_cols: defaultdict(int), # 范围查询列及频率 order_cols: defaultdict(int), # 排序列及频率 query_count: 0, # 涉及该表的查询总数 total_time: 0.0, # 涉及该表的总耗时 write_count: 0 # 涉及该表的写入次数用于计算写入惩罚 }) def fetch_workload_from_db(self, limit1000, min_time_ms100): 从 pg_stat_statements 抓取真实工作负载 ⚠️ 易错点必须关联 pg_stat_statements 和 pg_database 否则会把其他库的SQL也抓过来 query SELECT query, calls, total_exec_time, mean_exec_time FROM pg_stat_statements pgs JOIN pg_database pd ON pgs.dbid pd.oid WHERE pd.datname current_database() AND mean_exec_time %s -- 只抓取平均耗时大于阈值的慢查询 AND query NOT ILIKE %%pg_%% -- 过滤掉系统内部查询 AND query NOT ILIKE %%information_schema%% ORDER BY total_exec_time DESC LIMIT %s; with self.db_conn.cursor() as cur: cur.execute(query, (min_time_ms, limit)) return cur.fetchall() def extract_features(self, sql_text, calls, total_time): 核心逻辑用 sqlglot 解析SQL提取WHERE/JOIN/ORDER BY中的列 设计思想 等值条件, IN适合放在联合索引的前面 范围条件, , BETWEEN适合放在联合索引的最后 ORDER BY 的列如果能包含在索引里就能避免 filesort try: # 使用 sqlglot 解析SQL指定方言为 postgres parsed sqlglot.parse_one(sql_text, readpostgres) except Exception as e: # ⚠️ 边界处理遇到解析失败的脏SQL直接跳过不能让整个引擎崩溃 print(f⚠️ SQL解析失败跳过: {sql_text[:50]}... 错误: {e}) return # 判断是读操作还是写操作 is_write isinstance(parsed, (exp.Insert, exp.Update, exp.Delete)) # 提取涉及的表 tables [table.name for table in parsed.find_all(exp.Table)] for table in tables: self.table_features[table][query_count] calls self.table_features[table][total_time] total_time if is_write: self.table_features[table][write_count] calls # 1. 提取 WHERE 条件中的列 where_clause parsed.find(exp.Where) if where_clause: for condition in where_clause.find_all(exp.Column): table_name condition.table or tables[0] if tables else None if not table_name: continue col_name condition.name parent condition.parent # 判断是等值查询还是范围查询 if isinstance(parent, (exp.EQ, exp.In)): self.table_features[table_name][equality_cols][col_name] calls elif isinstance(parent, (exp.GT, exp.GTE, exp.LT, exp.LTE, exp.Between)): self.table_features[table_name][range_cols][col_name] calls # 2. 提取 ORDER BY 中的列 order_clause parsed.find(exp.Order) if order_clause: for col in order_clause.find_all(exp.Column): table_name col.table or tables[0] if tables else None if table_name: self.table_features[table_name][order_cols][col.name] calls4.2 模块二候选索引生成算法拿到了特征接下来要生成候选索引。 大坑预警千万不要把所有列的排列组合都生成出来那是指数级爆炸跑一天都跑不完。 工程实践我们只生成单列索引和最多3列的联合索引并且严格按照等值列在前范围列在后排序列最后的原则生成。模块二候选索引生成器from itertools import permutationsclass CandidateIndexGenerator:“”候选索引生成器基于提取的特征生成合理的候选索引。 设计思想联合索引的列顺序至关重要黄金法则等值查询列Equality - 范围查询列Range - 排序列Order“”def init(self, max_columns3): self.max_columns max_columns # 联合索引最大列数超过3列写入开销太大 def generate(self, table_features): 生成候选索引列表 返回格式[{table: orders, columns: (status, create_time), type: btree}] candidates [] for table, features in table_features.items(): # 按频率降序排列只取Top N的列防止组合爆炸 # ⚠️ 易错点如果某列的查询频率极低根本不值得为它建索引 eq_cols sorted(features[equality_cols].items(), keylambda x: x[1], reverseTrue)[:5] rng_cols sorted(features[range_cols].items(), keylambda x: x[1], reverseTrue)[:3] ord_cols sorted(features[order_cols].items(), keylambda x: x[1], reverseTrue)[:3] eq_cols [c[0] for c in eq_cols] rng_cols [c[0] for c in rng_cols if c[0] not in eq_cols] # 去重 ord_cols [c[0] for c in ord_cols if c[0] not in eq_cols and c[0] not in rng_cols] # 1. 生成单列索引兜底方案 for col in eq_cols rng_cols: candidates.append({ table: table, columns: (col,), estimated_benefit: features[equality_cols].get(col, 0) features[range_cols].get(col, 0) }) # 2. 生成联合索引核心逻辑 # 策略从等值列中选1-2个加上范围列/排序列 for i in range(1, min(len(eq_cols), self.max_columns) 1): for eq_combo in permutations(eq_cols, i): # 纯等值列组合 candidates.append({ table: table, columns: eq_combo, estimated_benefit: sum(features[equality_cols].get(c, 0) for c in eq_combo) }) # 等值列 1个范围列 if len(eq_combo) self.max_columns and rng_cols: for rng_col in rng_cols[:2]: # 只取Top 2范围列 candidates.append({ table: table, columns: eq_combo (rng_col,), estimated_benefit: sum(features[equality_cols].get(c, 0) for c in eq_combo) features[range_cols].get(rng_col, 0) }) # 等值列 范围列 排序列 if len(eq_combo) 1 self.max_columns and ord_cols: for ord_col in ord_cols[:2]: if ord_col ! rng_col: candidates.append({ table: table, columns: eq_combo (rng_col, ord_col), estimated_benefit: sum(features[equality_cols].get(c, 0) for c in eq_combo) features[range_cols].get(rng_col, 0) features[order_cols].get(ord_col, 0) }) # 去重并过滤 unique_candidates [] seen set() for cand in candidates: key (cand[table], cand[columns]) if key not in seen: seen.add(key) unique_candidates.append(cand) print(f✅ 共生成 {len(unique_candidates)} 个候选索引) return unique_candidates4.3 模块三代价评估Cost-Based Evaluation这是整个引擎最硬核、最值钱的部分候选索引有了怎么知道它到底有没有用不能靠猜必须让数据库的优化器来算账 核心技巧我们在数据库里虚拟创建索引不实际写盘然后跑 EXPLAIN对比加索引前后的代价Cost。模块三基于数据库代价模型的索引评估⚠️ 高能预警这部分代码需要直接操作数据库务必在测试环境先跑class CostEvaluator:“”代价评估器利用数据库自带的优化器CBO来评估索引的真实收益。 设计思想不要自己写公式算代价你算不过数据库优化器的。我们要白嫖数据库的 EXPLAIN 功能。在 PostgreSQL/金仓 中可以使用 HypoPG 插件来虚拟创建索引 如果不支持 HypoPG可以用事务回滚的方式CREATE INDEX - EXPLAIN - ROLLBACK。 这里演示事务回滚法兼容性最强。 def init(self, db_conn, workload_queries): self.db_conn db_conn self.workload_queries workload_queries # 需要评估的Top SQL列表 def evaluate_candidate(self, candidate): 评估单个候选索引的净收益Net Benefit 净收益 查询收益读变快 - 写入惩罚写变慢 - 存储成本 table candidate[table] columns candidate[columns] index_name fhyp_idx_{table{.join(columns)} # 1. 计算不加索引时的基准代价Baseline Cost baseline_cost self._get_workload_cost() # 2. 在事务中虚拟创建索引 try: with self.db_conn.cursor() as cur: # 开启事务 self.db_conn.autocommit False # 创建索引如果是大表这里会耗时生产环境慎用建议用HypoPG col_str , .join(columns) create_sql fCREATE INDEX {index_name} ON {table} ({col_str}); cur.execute(create_sql) # ⚠️ 关键创建索引后必须更新统计信息否则EXPLAIN不准 cur.execute(fANALYZE {table};) # 3. 计算加索引后的代价 new_cost self._get_workload_cost() # 4. 估算写入惩罚 # 每多一个索引写入INSERT/UPDATE/DELETE的代价大约增加 10%~20% write_penalty self._estimate_write_penalty(table) # 5. 必须回滚不能让虚拟索引真的留在库里 self.db_conn.rollback() self.db_conn.autocommit True except Exception as e: self.db_conn.rollback() self.db_conn.autocommit True print(f 评估索引 {index_name} 时出错: {e}) return -999999 # 出错直接给个极低的分数 # 计算净收益 # 查询收益 基准代价 - 新代价 read_benefit baseline_cost - new_cost # 净收益 查询收益 - 写入惩罚 net_benefit read_benefit - write_penalty return net_benefit def _get_workload_cost(self): 获取当前工作负载的总代价 技巧只取 Top 20 最慢的 SQL 跑 EXPLAIN全跑太慢了 total_cost 0.0 with self.db_conn.cursor() as cur: for sql in self.workload_queries[:20]: try: # 使用 EXPLAIN (FORMAT JSON) 获取精确的 Total Cost cur.execute(fEXPLAIN (FORMAT JSON) {sql}) plan cur.fetchone()[0] # 提取顶层节点的 Total Cost cost plan[0][Plan][Total Cost] total_cost cost except Exception: # 有些SQL可能因为缺少表而无法EXPLAIN直接跳过 continue return total_cost def _estimate_write_penalty(self, table): 估算写入惩罚 经验公式写入惩罚 表的写入频率 × 单次写入增加的代价 如果这个表是高频写入表如订单表加索引的惩罚要加倍 # 这里简化处理实际应该从 pg_stat_user_tables 读取 n_tup_ins/upd/del # 假设每个索引对写入的惩罚固定为 500 个 cost 单位 return 500.04.4 模块四贪心算法选择最优组合背包问题候选索引有几百个但磁盘空间有限写入开销也有限。怎么选魔性比喻这就好比你去吃自助餐胃容量磁盘/写入限制就那么大你怎么挑最贵的菜收益最大的索引吃这就是经典的0-1背包问题模块四基于贪心算法的最优索引选择class IndexSelector:“”索引选择器在存储和写入限制下选出收益最大的索引组合。 设计思想这是一个带约束的优化问题。约束1总索引数量不能超过 Max_Indexes防止写入雪崩约束2单表索引数量不能超过 Max_Indexes_Per_Table“”def init(self, max_total_indexes20, max_indexes_per_table5): self.max_total_indexes max_total_indexes self.max_indexes_per_table max_indexes_per_table def select_best_indexes(self, evaluated_candidates): 使用贪心算法选择最优索引 输入evaluated_candidates [{candidate: {...}, net_benefit: 1234.5}, ...] # 1. 按净收益降序排列 sorted_candidates sorted( evaluated_candidates, keylambda x: x[net_benefit], reverseTrue ) selected [] table_index_count defaultdict(int) for item in sorted_candidates: # 约束检查总数限制 if len(selected) self.max_total_indexes: break cand item[candidate] table cand[table] benefit item[net_benefit] # ⚠️ 核心逻辑如果净收益 0说明加了索引反而更慢直接舍弃 if benefit 0: continue # 约束检查单表索引数量限制 if table_index_count[table] self.max_indexes_per_table: continue # 约束检查索引冗余检查 # 如果已经选了 (a, b)就不要再选 (a) 了因为 (a, b) 已经覆盖了 (a) if self._is_redundant(cand, selected): continue # 选中 selected.append(cand) table_index_count[table] 1 return selected def _is_redundant(self, new_cand, selected): 检查新索引是否被已选索引覆盖冗余检查 比如已有 (status, create_time)新来个 (status)那就是冗余。 new_cols new_cand[columns] new_table new_cand[table] for sel in selected: if sel[table] ! new_table: continue sel_cols sel[columns] # 如果新索引的列是已选索引列的前缀则冗余 if len(new_cols) len(sel_cols): if sel_cols[:len(new_cols)] new_cols: return True return False def generate_ddl(self, selected_indexes): 生成最终的 CREATE INDEX DDL 语句 ddls [] for idx in selected_indexes: table idx[table] cols , .join(idx[columns]) name fidx_{table{.join(idx[columns])} ddl fCREATE INDEX CONCURRENTLY {name} ON {table} ({cols}); ddls.append(ddl) return ddls4.5 串联起来一键运行推荐引擎主程序串联所有模块一键生成索引推荐import psycopg2def run_index_advisor():# 1. 连接数据库conn psycopg2.connect(dbname“your_db”, user“your_user”,password“your_pwd”, host“127.0.0.1”)print( 墨夶AI索引推荐引擎启动...) # 2. 采集工作负载 analyzer WorkloadAnalyzer(conn) workload analyzer.fetch_workload_from_db(limit500, min_time_ms50) print(f✅ 抓取到 {len(workload)} 条慢查询) top_queries [] for query, calls, total_time, mean_time in workload: analyzer.extract_features(query, calls, total_time) top_queries.append(query) # 3. 生成候选索引 generator CandidateIndexGenerator(max_columns3) candidates generator.generate(analyzer.table_features) # 4. 评估代价这一步最耗时 evaluator CostEvaluator(conn, top_queries) evaluated [] for i, cand in enumerate(candidates): print(f⏳ 正在评估第 {i1}/{len(candidates)} 个候选索引...) benefit evaluator.evaluate_candidate(cand) evaluated.append({candidate: cand, net_benefit: benefit}) # 5. 贪心选择最优组合 selector IndexSelector(max_total_indexes15, max_indexes_per_table4) best_indexes selector.select_best_indexes(evaluated) # 6. 输出最终DDL ddls selector.generate_ddl(best_indexes) print(n *60) print( 推荐索引DDL请在业务低峰期执行) print(*60) for ddl in ddls: print(ddl) conn.close()if name “main”:run_index_advisor()五、避坑指南AI推荐索引的5个致命暗坑代码写完了别急着上生产下面这5个坑是我用无数个通宵和DBA的骂声换来的血泪教训。 暗坑1CREATE INDEX 锁表导致业务停摆– ❌ 傻操作直接 CREATE INDEXCREATE INDEX idx_orders_status ON orders(status);– 后果在MySQL 5.6以前或者某些国产数据库中这会锁死整张表– 业务方墨夶订单系统怎么又不能写了– ✅ 正确姿势使用 CONCURRENTLYPG/金仓或 ALGORITHMINPLACEMySQL– PG/金仓CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status);– 原理并发创建索引不阻塞DML操作但耗时会更长。– ⚠️ 注意如果并发创建中途失败会留下一个 INVALID 索引必须手动 DROP– MySQLCREATE INDEX idx_orders_status ON orders(status) ALGORITHMINPLACE, LOCKNONE; 暗坑2忽略了索引膨胀Index Bloat– ⚠️ 现象索引建久了因为频繁的UPDATE/DELETEB树会产生大量碎片。– 结果索引体积比数据还大查询反而变慢– ✅ 诊断索引膨胀PG/金仓SELECTschemaname, tablename, indexname,pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,idx_scan, idx_tup_read, idx_tup_fetchFROM pg_stat_user_indexesJOIN pg_index ON pg_stat_user_indexes.indexrelid pg_index.indexrelidORDER BY pg_relation_size(indexrelid) DESC;– ✅ 解决索引膨胀REINDEX同样要用CONCURRENTLYREINDEX INDEX CONCURRENTLY idx_orders_status; 暗坑3AI推荐了完美索引但统计信息是假的– ⚠️ 现象AI算出来加索引能提速10倍结果上线后一点没快。– 原因数据库的统计信息pg_statistic过期了优化器拿假数据算账– ✅ 必须做在跑推荐引擎前先全量更新统计信息– 对于大表用采样分析别用全量会锁表/耗IOANALYZE orders;– 或者调整自动分析的阈值让它更灵敏ALTER TABLE orders SET (autovacuum_analyze_scale_factor 0.02); 暗坑4联合索引的覆盖陷阱– 场景AI推荐了 idx_a_b (a, b)– 但业务里有一条SQL是SELECT a, b, c FROM table WHERE a 1;– 这条SQL能用 idx_a_b 吗– 答案能用但如果是 MySQL且 c 不在索引里就需要回表。– 如果 c 很小且查询极高频可以考虑覆盖索引Covering IndexCREATE INDEX idx_a_b_c ON table(a, b, c);– 这样查询直接在索引树上拿到所有数据不用回表速度起飞– ⚠️ 代价索引体积变大写入变慢。必须权衡 暗坑5只加不删索引变成垃圾场– ⚠️ 现象系统跑了两年表上挂了20个索引一半都没用过。– 每次INSERT都要更新20个B树写入慢得像蜗牛。– ✅ 找出僵尸索引PG/金仓SELECTschemaname, relname AS table_name, indexrelname AS index_name,idx_scan AS times_used,pg_size_pretty(pg_relation_size(indexrelid)) AS index_sizeFROM pg_stat_user_indexesWHERE idx_scan 0 – 从未被扫描过AND pg_relation_size(indexrelid) 10485760 – 且体积大于10MBORDER BY pg_relation_size(indexrelid) DESC;– 墨夶建议每个月跑一次这个SQL把没用的索引果断 DROP 掉– 删索引比加索引更考验胆量建议先在测试环境观察一周。六、生产环境落地如何把推荐引擎变成自动化服务手搓脚本只是第一步真正要在公司落地你得把它变成自动化服务。6.1 架构设计闭环反馈系统flowchart LRA[定时任务: 每天凌晨2点] -- B[抓取前24h工作负载]B -- C[AI推荐引擎计算]C -- D{净收益 阈值?}D – 否 -- E[丢弃, 记录日志]D – 是 -- F[生成DDL, 发送钉钉/飞书审批]F -- G[DBA点击审批通过]G -- H[自动化平台在低峰期执行DDL]H -- I[监控执行后7天的性能指标]I -- J{性能提升?}J – 是 -- K[✅ 归档为成功案例, 训练模型]J – 否 -- L[ 自动回滚索引, 记录失败原因]6.2 核心落地建议DBA看了都说好别自动执行别自动执行别自动执行AI推荐的DDL必须经过DBA审批。AI会算账但AI不懂业务逻辑比如某个索引是为了月底跑批特意加的。灰度上线先在从库Slave上建索引跑几天观察没问题再切主库。设置写入惩罚权重对于交易核心表如订单、支付在代码里把 _estimate_write_penalty 的权重调高3倍宁可不加索引也不能影响写入。七、血泪总结索引优化的四字真言抓、算、选、删。翻译成大白话抓抓取真实工作负载别凭直觉让数据说话。算用数据库的CBO代价模型去算收益别自己瞎猜。选用贪心算法在读写之间找平衡别把表加成了刺猬。删定期清理僵尸索引给数据库减负。索引优化避坑清单终极版坑点 后果 墨夶解法1 凭直觉加联合索引 写入雪崩B树分裂 用AI评估写入惩罚限制单表索引数2 直接 CREATE INDEX 锁表业务停摆 用 CONCURRENTLY 或 ALGORITHMINPLACE3 统计信息过期 AI算错账推荐无效索引 跑引擎前先 ANALYZE调低自动分析阈值4 只加不删 索引膨胀写入越来越慢 每月清理 idx_scan 0 的僵尸索引5 忽略覆盖索引 频繁回表IO打满 高频小字段查询考虑把SELECT列加入索引
返回列表