数据库索引优化实战指南

描述

 

“70%”不应被视为可复制的生产统计。索引确实会导致或暴露大量 SQL 问题,但 CPU、锁、I/O、连接池和应用重试都可能才是根因。本文统一以 MySQL 8.0 和 InnoDB 为例:先用证据确认 SQL,再以可回滚的方式优化。<数据库主机>、<数据库名>、<业务表> 均为占位符。

先确认慢在哪里

不要把应用日志里的超时直接归咎于索引。先看故障窗口内数据库负载、正在执行的语句、锁等待和慢查询摘要。


			   sqlSELECT VERSION(), @@version_comment, @@sql_mode; SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time'; 

			   sqlSELECT DIGEST_TEXT, COUNT_STAR,        ROUND(SUM_TIMER_WAIT/1000000000000,2AS total_seconds,        ROUND(AVG_TIMER_WAIT/1000000000,2AS avg_ms,        SUM_ROWS_EXAMINED, SUM_ROWS_SENT FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 20; 

			   bashmysql --host='<数据库主机>' --user='<只读用户>' --password    --database='<数据库名>' --execute='SHOW FULL PROCESSLIST' 

			   sqlSELECT w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,        w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx,        dl.OBJECT_SCHEMA, dl.OBJECT_NAME, dl.LOCK_TYPE, dl.LOCK_MODE FROM performance_schema.data_lock_waits AS w JOIN performance_schema.data_locks AS dl   ON dl.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID; 

			   sqlSHOW ENGINE INNODB STATUS; 

			   bashpidof mysqld pidstat -p '' -rud 1 10 iostat -xz 1 10 

			   promql# 指标名以实际 mysqld exporter 暴露的指标为准 rate(mysql_global_status_questions[5m]) 

			   promql# 指标名以实际 mysqld exporter 暴露的指标为准 rate(mysql_global_status_innodb_row_lock_time[5m]) 

锁等待、长事务或磁盘饱和是独立根因。新增索引不能替代缩短事务、统一更新顺序、扩大容量或纠正重试风暴。所有输出都应保存时间点和采集来源,示例输出不能当成生产事实。

用执行计划证明访问路径

EXPLAIN 的估算不等于真实耗时;MySQL 8.0.18+ 的 EXPLAIN ANALYZE 会实际执行 SQL,生产使用前必须确认查询代价,优先在脱敏预发布复现。


			   sqlEXPLAIN FORMAT=TREE SELECT id, created_at, status FROM <业务表> WHERE tenant_id = <租户ID>   AND status = '<状态>'   AND created_at >= '<起始时间>' ORDER BY created_at DESC LIMIT 100; 

			   sqlEXPLAIN ANALYZE SELECT id, created_at, status FROM <业务表> WHERE tenant_id = <租户ID>   AND status = '<状态>'   AND created_at >= '<起始时间>' ORDER BY created_at DESC LIMIT 100; 

			   sqlANALYZE TABLE <业务表>; 

ANALYZE TABLE 会消耗资源,应在低峰并结合监控执行。小表全表扫描可能正确;判断重点是实际扫描行数、过滤比例、临时表、filesort、排序成本和执行时间,而非只看 type=ALL。


			   sqlSELECT COUNT(*AS total_rows,        COUNT(DISTINCT tenant_id) AS tenant_cardinality,        COUNT(DISTINCT status) AS status_cardinality FROM <业务表>; 

			   sql-- 对列调用函数通常降低索引利用 SELECT id FROM <业务表> WHERE DATE(created_at) = '<日期>'; 

			   sql-- 改为可走范围索引的条件 SELECT id FROM <业务表> WHERE created_at >= '<日期> 0000'   AND created_at < '<下一日期> 0000'; 

			   sql-- 若 order_no 为 varchar,数字字面量可能触发隐式转换 SELECT id FROM <业务表> WHERE order_no = 123456; 

			   sqlSELECT id FROM <业务表> WHERE order_no = '<订单号>'; 

复合索引要围绕真实查询组合设计。常见情况是等值过滤列在前,范围列与排序列随后,但是否可以同时满足排序必须用实际 EXPLAIN 验证。低基数字段通常不应单独建索引,也不能凭经验断言“任何字段放前面都更快”。

以可回滚方式建索引

大表 DDL 可能等待元数据锁并影响复制。先备份结构、检查长事务、核实版本和变更窗口;高风险表必须走团队在线 schema 变更平台或低峰灰度,而不是在峰值直接执行。


			   bash#!/usr/bin/env bash set -euo pipefail DB_HOST='<数据库主机>' DB_NAME='<数据库名>' TABLE_NAME='<业务表>' mysqldump --host="$DB_HOST" --user='<备份用户>' --password    --no-data --routines --events "$DB_NAME" "$TABLE_NAME"    > "$TABLE_NAME-schema-$(date -u +%Y%m%dT%H%M%SZ).sql" 

这只是结构备份,不等于数据可恢复备份。还要确认备份可用性、binlog 保留和从库状态。


			   sqlSELECT trx_id, trx_started, trx_mysql_thread_id,        trx_rows_locked, trx_query FROM information_schema.innodb_trx ORDER BY trx_started; 

			   sqlCREATE INDEX idx_orders_tenant_status_created ON <业务表> (tenant_id, status, created_at DESC); 

			   sqlALTER TABLE <业务表>   ADD INDEX idx_orders_tenant_status_created (tenant_id, status, created_at DESC),   ALGORITHM=INPLACE,   LOCK=NONE; 

不同 MySQL 版本、表结构和索引类型对 ALGORITHM 与 LOCK 的支持不同。若明确请求的在线方式失败,不要擅自改为高锁方式;先确认版本帮助和变更平台方案。执行中持续看 metadata lock、磁盘、复制延迟和应用错误。


			   sqlEXPLAIN ANALYZE SELECT id, created_at, status FROM <业务表> WHERE tenant_id = <租户ID>   AND status = '<状态>'   AND created_at >= '<起始时间>' ORDER BY created_at DESC LIMIT 100; 

			   sqlSELECT TABLE_NAME, INDEX_NAME, SUM(CARDINALITYAS cardinality_sum FROM information_schema.statistics WHERE table_schema = '<数据库名>' AND table_name = '<业务表>' GROUP BY TABLE_NAME, INDEX_NAME; 

			   sqlDROP INDEX idx_orders_tenant_status_created ON <业务表>; 

DROP INDEX 同样是高风险变更。回滚前必须确认该索引确实由本次创建、没有被其他已上线查询依赖,并检查副本延迟和维护窗口。

两个常见访问模式

深分页会扫描并丢弃大量记录。对于稳定排序的列表,keyset pagination 往往更可控;排序键必须唯一或加主键作为第二排序键,才能避免漏读和重复。


			   sqlSELECT id, created_at FROM <业务表> WHERE tenant_id = <租户ID> ORDER BY created_at DESC LIMIT 100 OFFSET 100000; 

			   sqlSELECT id, created_at FROM <业务表> WHERE tenant_id = <租户ID>   AND (created_at < '<上一页时间>'        OR (created_at = '<上一页时间>' AND id < <上一页ID>)) ORDER BY created_at DESC, id DESC LIMIT 100; 

			   sqlCREATE INDEX idx_orders_tenant_created_id ON <业务表> (tenant_id, created_at DESC, id DESC); 

N+1 常表现为 QPS 高、单条 SQL 不一定慢。通过 tracing 统计单请求 SQL 数量后,用受控批量查询或预加载减少往返;IN 列表大小需要受包大小、锁持有时间和应用内存约束。


			   sqlSELECT id, customer_id, status FROM <业务表> WHERE id IN (<ID列表>); 

			   sqlSELECT o.id, o.status, c.name FROM <业务表> AS o JOIN customers AS c ON c.id = o.customer_id WHERE o.tenant_id = <租户ID>   AND o.created_at >= '<起始时间>'; 

验收

优化成功要有同等负载下的 rows examined、延迟分位数、数据库 CPU/I/O、锁等待、复制延迟和错误率共同证据,不能只凭一次缓存命中的 EXPLAIN。


			   bash#!/usr/bin/env bash set -euo pipefail mysql --host='<数据库主机>' --user='<只读用户>' --password    --execute='SHOW GLOBAL STATUS LIKE "Threads_running";' mysql --host='<数据库主机>' --user='<只读用户>' --password    --execute='SHOW GLOBAL STATUS LIKE "Slow_queries";' 

			   sqlSHOW CREATE TABLE <业务表>; SHOW INDEX FROM <业务表>; 

保留问题 SQL 摘要、时间窗、原计划与新计划、灰度范围、变更耗时、指标、回滚条件。只有日志、指标、锁证据或执行计划能支持根因结论;“多建索引试试”不是优化方法。

 

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分