想象一个周末的傍晚,你所在的城市突然大堵车,交警大队的电话被瞬间打爆——“这条路为什么走不动了?”其实,每一条 SQL 查询就像城市里的一辆车,数据库就是这座城市的庞大路网。当某条 SQL 执行得异常缓慢,它就是一台“堵在路中央的抛锚车”,不仅自己过不去,还拖垮身后排成长队的请求。MySQL 慢查询,正是数据库世界里最需要被揪出来的“堵车元凶”。 治理堵车,交警的第一步是调出路口的监控探头——这对应着数据库的慢查询日志;第二步,把一天的监控录像送到指挥中心做大数据分析——这对应着 pt-query-digest 这样的专业分析工具;第三步,派最有经验的交警去现场勘探——这对应着 EXPLAIN 执行计划;第四步,给关键路口修一条快速专用道——这对应着索引优化;第五步,为每辆车重新规划最优路线——这对应着SQL 改写。这套从发现到解决的完整方法论,就是后端工程师和 DBA 每天都在使用的“城市交通治理术”。

岗位关联

掌握这套系统化的慢查询定位与优化方法,是后端工程师从“能用数据库”走向“会调数据库”的分水岭。它让你在面对线上慢 SQL 时不再靠直觉猜,而是像老刑警一样按证据链排查——这是数据库性能调优最实用的通用工具箱。

目录
  1. 可视化演示
  2. 日志开启配置:给路口装上监控探头
  3. 日志分析工具:指挥中心的拥堵分析大屏
  4. EXPLAIN定位:交警现场的勘探报告
  5. 索引优化策略:修建快速专用车道
  6. SQL改写技巧:重新规划行车路线
  7. 优化器原理:聪明导航员的算路逻辑
  8. 治理实践体系:从救火到防火
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

慢查询是数据库性能的头号杀手,定位和优化它是后端和DBA的日常必修课。本文带你搞懂慢查询日志开启、pt-que... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

慢查询日志的:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

日志开启配置:给路口装上监控探头

想要治理堵车,第一件事不是修路,而是在每个路口装上监控探头。没有探头数据,你连哪里堵、堵多久、堵多严重都无从知晓。MySQL 的慢查询日志,就是数据库世界的“路口监控探头”——它把所有执行时间超过预设阈值的 SQL 语句,原原本本地记录到日志文件中。

开启它非常简单。在 MySQL 配置文件(通常是 my.cnf 或 my.ini)中设置 slow_query_log = ON 打开开关,用 slow_query_log_file = /var/log/mysql-slow.log 指定日志落盘路径,再用 long_query_time = 1 告诉数据库“超过 1 秒就算慢”。注意这个参数的单位是秒且支持小数,0.5 就代表 500 毫秒。生产环境建议从 1 秒起步,观察日志量后再逐步收紧。

日志里的每条记录包含关键四要素:Query_time 是总执行时间,Lock_time 是锁等待耗时,Rows_examined 是扫描行数,Rows_sent 是实际返回行数。当 Rows_examined 远大于 Rows_sent 时,说明这条 SQL 翻遍全城只为送一份外卖——典型的无效扫描。

装上探头只是第一步,日志文件会像录像带一样越堆越多。下一步,我们需要一套能自动分析海量录像带的“指挥中心系统”。

Tquery≥long_query_time  ⟹  recordedT_{query} \geq \text{long\_query\_time} \implies \text{recorded}
这条式子描述的是慢查询的判定逻辑:当一条查询的执行耗时大于等于你设定的阈值时,它就会被当作“违章记录”写入慢查询日志。
  1. T_query(查询执行时间)这条 SQL 从开始执行到结束所花费的秒数,相当于外卖骑手从接单到送达的总时长。
    TqueryT_{query}
  2. long_query_time(慢查询阈值)预先设定的“堵多久算慢”的标准。MySQL 默认值是 10 秒,对大多数业务过于宽松,生产环境通常调整为 1 秒或 0.5 秒。
    long_query_time=1 second\text{long\_query\_time} = 1\ \text{second}
  3. ≥(比较判断)将实际耗时与阈值比较,大于等于则条件成立。
    ≥\geq
  4. recorded(记录动作)条件满足后,MySQL 把该 SQL 连同执行信息写入慢查询日志文件,形成案发现场的完整存档。
    recorded\text{recorded}

日志分析工具:指挥中心的拥堵分析大屏

当全城的监控录像带堆积如山,你不可能一卷一卷人工回放。交警大队的指挥中心会把海量数据汇总成一块“大屏”——哪条路最堵、堵了多少次、集中在哪个时段,一目了然。在 MySQL 世界里,pt-query-digest 就是这块“大屏”。

它是 Percona Toolkit 工具包中最著名的慢查询分析工具。读取慢查询日志后,pt-query-digest 会把所有 SQL 按“指纹”聚合——所谓指纹,就是把具体值替换成占位符后的归一化模板,比如 SELECT * FROM users WHERE id = 1 和 WHERE id = 2 会归为同一类。然后统计每一类 SQL 的出现次数、总耗时、平均耗时、最大耗时,并生成图文报告。

报告中最重要的部分是 Query Review 表,它把 SQL 模板按总耗时从高到低排序。实际使用中,重点盯三个指标:总耗时(Sum)反映这类 SQL 对系统的总影响,与出现次数成正比;平均耗时(Avg)反映单次执行的性能;出现次数(Count)反映是否属于高频访问路径。总耗时和平均耗时双高,就是全村的“堵车王”。

大屏锁定了“堵车王”后,要找出它具体堵在哪个路口,还需要经验最丰富的交警到现场画一张勘探图——这就是 EXPLAIN 的舞台。

R=NslowNtotal×100%R = \frac{N_{slow}}{N_{total}} \times 100\%
慢查询占比等于慢查询条数除以总查询条数。这个比例是衡量系统“拥堵程度”的宏观指标,超过 5% 就该拉响警报了。
  1. N_slow(慢查询条数)从慢查询日志中统计出的记录总数,相当于指挥中心接到的堵车投诉电话数。
    NslowN_{slow}
  2. N_total(总查询条数)数据库自启动以来处理的查询总数,可用 SHOW GLOBAL STATUS LIKE 'Questions' 获取。
    NtotalN_{total}
  3. R(慢查询占比)两者相除再乘以 100%,得到慢查询在全部查询中的占比,是宏观健康度指标。
    NslowNtotal×100%\frac{N_{slow}}{N_{total}} \times 100\%

EXPLAIN定位:交警现场的勘探报告

指挥中心锁定嫌疑路段后,交警立即出警勘探现场——是红绿灯配时不合理?前方修路?还是路口没有方向标识?在数据库世界,EXPLAIN 就是这张“现场勘探报告”。你只需在 SELECT 语句前加上 EXPLAIN 关键字,MySQL 就会告诉你这条 SQL 将如何执行,而且不会真正运行它。

执行 EXPLAIN SELECT ... 后,MySQL 返回一张结果表,每一行描述一个表访问步骤。最值得看的四列是:id 执行顺序、type 访问类型、key 实际使用的索引、rows 估算扫描行数。type 是勘探报告的核心,从好到差依次为:system > const > eq_ref > ref > range > index > ALL。ALL 表示全表扫描,是报告里画满红叉的部分。

rows 列不是精确值,而是优化器基于统计信息估算的扫描行数,rows 越小说明访问路径越精准。type=ref 表示通过普通二级索引定位,是最常见的优化目标;type=range 表示索引范围扫描,多来自 BETWEEN 或 IN;type=ALL 则是必须处理的红色警报。结合 filtered 可以估算最终命中的行数比例。

勘探完毕,确认是“路口设计不合理”之后就可以动手了——在关键路口加装快速专用车道,这正是索引的用武之地。

filtered=RmatchRscan×100%\text{filtered} = \frac{R_{match}}{R_{scan}} \times 100\%
filtered 表示从扫描行数中,最终通过 WHERE 条件过滤后留下的比例。这个值越低,说明扫描的无用功越多,就像交警发现 100 辆过路车中只有 10 辆是真正要进这个路口的。
  1. R_scan(扫描行数)EXPLAIN 输出中的 rows 列,表示优化器估计需要扫描的行数,相当于交警预估这个路口要通过的车辆总数。
    RscanR_{scan}
  2. R_match(实际命中行数)经过 WHERE 条件层层过滤后真正有用的行数,是真正需要送达的目的地车辆。
    RmatchR_{match}
  3. percent(过滤百分比)R_match 除以 R_scan,衡量扫描效率。值越低,浪费的扫描越多,SQL 越需要优化。
    RmatchRscan×100%\frac{R_{match}}{R_{scan}} \times 100\%

索引优化策略:修建快速专用车道

堵车的路口经勘探发现,问题在于所有左转车辆和直行车辆混在一起抢道。解决办法是修一条“左转快速专用道”,让车辆进入后直接到达目的地。数据库里的索引正是这个机制:为某一列或某几列建立一份有序的目录,查询时像查字典一样直接翻到目标页,而不是逐页翻阅整本书。

MySQL 最常用的是 B+Tree 索引。当我们对多列建立复合索引(比如 INDEX(a,b,c))时,就引出了最左前缀法则:查询必须先用到索引最左列 a,索引才会被启用。只要 a 在查询条件中,无论 b、c 是否出现,a 都能大幅减少扫描量。另外,覆盖索引指索引中已包含查询所需的全部列,可以免去回表(回到主键索引取完整行)的额外开销,相当于“高速终点直达”。

但加索引并非万能。索引失效的典型场景有:对索引列使用函数(如 WHERE YEAR(create_time)=2024)、隐式类型转换(字符串列与数值比较)、LIKE 以通配符开头(LIKE '%abc')。建索引前还要评估列的选择性——如果某列只有“男/女”两种值,区分度太低,索引形同虚设。

车道修好后,如果司机的驾驶路线本身就很绕,依然快不起来。所以下一项工作,是为每一辆“慢车”重新规划最优行驶路线。

S=COUNT(DISTINCT col)COUNT(col)S = \frac{\text{COUNT}(\text{DISTINCT}\ col)}{\text{COUNT}(col)}
选择率(选择性)衡量一列取值的丰富程度,等于去重后的不同值个数除以总行数。S 越接近 1,该列分布越分散,越适合建索引;S 接近 0,则建索引的收益微乎其微。
  1. COUNT(DISTINCT col)(去重值个数)这一列有多少种不同的取值,相当于一个城市划分成多少个区县。
    COUNT(DISTINCT col)\text{COUNT}(\text{DISTINCT}\ col)
  2. COUNT(col)(总行数)整张表有多少行数据,相当于整个城市的总人口。
    COUNT(col)\text{COUNT}(col)
  3. S(选择性指标)两者相除得到选择性。S 越高,索引越能精准筛选,建索引的价值越大。
    S=COUNT(DISTINCT col)COUNT(col)S = \frac{\text{COUNT}(\text{DISTINCT}\ col)}{\text{COUNT}(col)}

SQL改写技巧:重新规划行车路线

同一段路程,新手司机可能左拐右绕频繁变道,老司机却走一条大道直达。SQL 的写法也如此——同一个业务目标,不同写法的执行效率可能相差几十倍。SQL 改写,就是给数据库这辆车重新规划一条更高效的行驶路线。

最常见的改写有三类。第一,把 SELECT * 改成 SELECT 需要的列,减少数据传输量,毕竟没人会开车绕城一圈就为了买瓶水。第二,深分页优化——LIMIT 1000000, 10 会让数据库先扫描 100 万行再全部丢弃,等价于开车到终点才发现导航导错了。更优的写法是延迟关联:先只查主键,再回表取出需要的数据。第三,多表关联尽量用 JOIN 而非大量 IN 子查询,并遵循小表驱动大表原则。

LIMIT offset, size 的成本模型可以这样理解:总代价约等于扫描 offset + size 行的成本。当 offset 非常大时,主要开销来自前面 offset 行的无效扫描。改写策略是改用“上一次位置”(keyset pagination):WHERE id > last_id ORDER BY id LIMIT 10,让扫描量恒定不变。

路线规划得再好,还得看导航软件够不够聪明——MySQL 优化器就是车里的那位导航员。理解它的思考方式,才能写出它真正听得懂的 SQL。

C=Cscan×(O+P)C = C_{scan} \times (O + P)
分页查询的总成本约等于单行扫描成本乘以“偏移量 + 页大小”。偏移量越大,无效扫描越多,这正是深分页慢的根本原因。
  1. C_scan(单行扫描成本)数据库扫描一行数据所需的平均开销,相当于车辆行驶一公里的油耗。
    CscanC_{scan}
  2. O(偏移量 offset)要跳过的前多少行,对应 LIMIT 后的第一个参数。
    OO
  3. P(页大小)本次返回的记录数,对应 LIMIT 后的第二个参数。
    PP
  4. C(总成本)扫描总量乘以单行成本。当 O 远大于 P 时,O 主导总成本,所以深分页代价极高。
    C=Cscan×(O+P)C = C_{scan} \times (O + P)

优化器原理:聪明导航员的算路逻辑

同样输入“去机场”,高德地图会综合实时路况、红绿灯数量、限行规则,给出多条路线并标注推荐。MySQL 优化器就是这样的“导航员”——执行 SQL 前,它先基于统计信息估算所有可选执行计划的代价,然后选择代价最小的一套方案。

代价估算的核心数据来自两张统计表:mysql.innodb_table_stats 存表行数等基础信息,mysql.innodb_index_stats 存索引基数(索引去重值的数量)和数据分布。MySQL 为顺序读、随机读、比较操作、临时表等操作定义了不同的“权重单价”。优化器用这套模型为每种执行路径打分,分越低越优先。

查询重写是优化器的隐藏技能。例如 WHERE id IN (1,2,3) 可能被重写为多个等值条件的组合;EXISTS 子查询在特定场景下被改写成半连接(semi-join)。理解优化器后你会发现,与其研究各种偏门技巧,不如做好两件基础事:定期 ANALYZE TABLE 保持统计信息新鲜;避免在 WHERE 子句中使用函数,否则索引很可能被“弃用”。

有了一次成功的路段改造,交警队不会就此收工——真正有经验的城市交通管理者,会把经验固化为制度,让今天的堵点明天不再复发。这正是慢查询治理实践体系的精髓。

Cost=Wseq×Nseq+Widx×Nidx\text{Cost} = W_{seq} \times N_{seq} + W_{idx} \times N_{idx}
这是一个简化的代价模型:总代价等于顺序读的权重乘以扫描量,加上索引读的权重乘以索引扫描量。优化器就是用这把“尺子”量出最省力的路线的。
  1. W_seq(顺序读权重)按物理顺序扫描一页数据的代价单价,相当于普通公路每公里油耗。
    WseqW_{seq}
  2. N_seq(顺序扫描页量)全表扫描预计要读多少页数据。
    NseqN_{seq}
  3. W_idx(索引读权重)通过索引随机读一页的代价单价,通常高于顺序读,相当于高速每公里收费。
    WidxW_{idx}
  4. N_idx(索引扫描页量)走索引预计访问的页数,因为索引树很矮,这个值往往远小于全表扫描量。
    NidxN_{idx}
  5. Cost(总代价)两部分之和,优化器选择 Cost 最小的执行计划。
    Cost=Wseq×Nseq+Widx×Nidx\text{Cost} = W_{seq} \times N_{seq} + W_{idx} \times N_{idx}

治理实践体系:从救火到防火

治理堵车最有经验的城市,靠的不是每次堵车后派交警去现场疏通,而是一套长效机制:实时路况预警、新开路段前的车流模拟、季度性的交通规划复盘。慢查询治理同样需要这样的“成熟制度”。

治理体系的第一层是监控告警。把慢查询日志接入 Prometheus 等监控系统,一旦慢查询数量或某类 SQL 耗时超过阈值就触发告警,让“火势”还在苗头时就被扑灭。第二层是压测验证。改动索引或上线重构 SQL 之前,先在测试环境用 sysbench 模拟线上流量,对比优化前后的性能指标,而不是上线后等用户投诉。

第三层是迭代优化:每轮优化后都要收集新数据,回到“日志 → 分析 → 定位 → 优化”的循环起点。团队还应把常用优化沉淀为 SQL 规范,例如“禁止 SELECT *”“分页必须带排序字段”“上线前必须走 EXPLAIN 评审”——把个人经验变成组织制度。整个体系是一个不停旋转的闭环:发现问题、验证方案、实施优化、复盘沉淀。

到此,你已经从“堵车时手忙脚乱的交警”成长为“统筹全局的交通局长”。把方法论变成习惯,慢查询就不再是深夜事故的元凶,而是可控可量化的日常管理对象。

Speedup=TbeforeTafter\text{Speedup} = \frac{T_{before}}{T_{after}}
加速比是衡量优化效果最直观的指标:优化前的耗时除以优化后的耗时。加速比为 10,表示这条 SQL 跑快了 10 倍。
  1. T_before(优化前耗时)改动前该 SQL 的平均执行时间,是堵车现场的历史数据。
    TbeforeT_{before}
  2. T_after(优化后耗时)改动后该 SQL 的平均执行时间。
    TafterT_{after}
  3. Speedup(加速比)两者相除。大于 1 说明优化有效,数值越大越好。汇报工作时,这个数字比“我优化了一下”有力得多。
    Speedup=TbeforeTafter\text{Speedup} = \frac{T_{before}}{T_{after}}

AI 应用场景

场景一:大促前的 AI 智能巡检

阶段:数据准备与异常检测

解决的问题:传统大促前,DBA 手动分析慢查询日志耗时数小时且容易遗漏隐藏在深处的“慢 N+1”问题。AI 巡检系统自动抓取慢查询日志,用聚类算法把相似 SQL 归并为模式,再由大模型自动给出索引建议和改写建议,把数小时的人工排查压缩到分钟级。

具体案例:某电商平台在 618 大促前的例行巡检中,AI 助手自动发现“订单表按 user_id 分组统计”这条 SQL 的全表扫描量高达 2000 万行。AI 结合表统计信息推荐建立 (user_id, status) 复合索引,并改写掉一次多余子查询。优化后该 SQL 从 2.8 秒降到 0.03 秒,加速比接近 100 倍。

场景二:AI 数据库自治助手

阶段:模型推理与智能决策

解决的问题:普通工程师写 SQL 时无法实时评估性能风险。AI 助手在开发环境的数据库网关中接入执行计划分析,每次 EXPLAIN 由 AI 模型解读 rows、type 等指标,标记可疑的全表扫描并生成优化说明,相当于给每个开发者配了一位 24 小时在线的 DBA 副驾。

具体案例:某公司的内部 AI 编码助手中加入 SQL 检查插件,在代码评审阶段自动拦截 SELECT * 和无 WHERE 条件的 UPDATE。一位新入职工程师提交的报表查询被 AI 提示“存在深分页风险”,系统直接给出改写后的延迟关联示例,代码一次通过评审。

Python 演示

python65 行
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""MySQL slow query log analyzer: Top N slow SQL"""import refrom collections import defaultdict  class SlowLogParser:    def __init__(self, log_path):        self.log_path = log_path        self.queries = []        self.sql_stats = defaultdict(            lambda: {"count": 0, "total_time": 0.0}        )     def parse(self):        with open(self.log_path, "r", encoding="utf-8") as f:            exec_time = 0.0            sql_buf = ""            for line in f:                m = re.search(r"Query_time:\s+([\d.]+)", line)                if m:                    exec_time = float(m.group(1))                    continue                if not line.startswith("#") and exec_time > 0:                    sql_buf += line.strip() + " "                    if line.rstrip().endswith(";"):                        sql = sql_buf.strip()                        self._record(sql, exec_time)                        exec_time = 0.0                        sql_buf = ""     def _record(self, sql, exec_time):        sql_norm = re.sub(r"\d+", "?", sql)        sql_norm = re.sub(r"'.*?'", "?", sql_norm)        self.sql_stats[sql_norm]["count"] += 1        self.sql_stats[sql_norm]["total_time"] += exec_time        self.queries.append((sql_norm, exec_time))     def top_n(self, n=10):        items = sorted(            self.sql_stats.items(),            key=lambda kv: kv[1]["total_time"],            reverse=True        )[:n]        return [(sql, v["count"], v["total_time"]) for sql, v in items]     def report(self, n=10):        print(f"parsed {len(self.queries)} slow queries")        header = f"{'rank':<6}{'count':<8}{'total(s)':<12}sql template"        print(header)        print("-" * 70)        for i, (sql, cnt, total) in enumerate(self.top_n(n), 1):            preview = sql[:50] + ("..." if len(sql) > 50 else "")            print(f"{i:<6}{cnt:<8}{total:<12.2f}{preview}")  def main():    parser = SlowLogParser("mysql-slow.log")    parser.parse()    parser.report(10)  if __name__ == "__main__":    main()
目的:这是一个迷你版 pt-query-digest,用正则表达式解析 MySQL 慢查询日志,按“SQL 指纹”聚合统计,输出累计耗时最长的 Top 10 慢 SQL 模板。
  1. m = re.search(r"Query_time:\s+([\d.]+)", line)从日志行中提取执行时间,这是判断“堵不堵”的直接证据。
  2. sql_norm = re.sub(r"\d+", "?", sql)把 SQL 中的数字替换成占位符,完成“指纹归一化”,让只有数字不同的同类 SQL 聚到一起。
  3. self.sql_stats[sql_norm]["count"] += 1累计该类 SQL 出现的次数,用于发现高频慢查询。
  4. self.sql_stats[sql_norm]["total_time"] += exec_time累计该类 SQL 的总耗时,这是排名时最重要的依据。
  5. key=lambda kv: kv[1]["total_time"]按总耗时从大到小排序,让“全城最大的堵点”排在最前面。
  6. parser.parse()入口处执行解析,一行代码吃进整个日志文件。

常见误区

误区我刚接触慢查询优化,觉得设得越小越好,干脆把 long_query_time 设成 0,把所有 SQL 全记下来,这样不是最安全吗?

正解阈值设得太小会让慢查询日志疯狂增长,像把整座城市每一辆车的行驶轨迹都录下来,存储很快爆掉,而且真正有价值的慢 SQL 会被淹没在海量记录中。合适的做法是从 1 秒起步,观察日志量后逐步调整。判断标准是:日志量只占系统总体的“黄金少数”,能精准暴露问题即可。

为何常见:初学者的心态是“宁可错杀一千,不能放过一个”,忽略了日志本身也有 IO 开销和存储成本,慢查询日志记录越密集,对线上性能的反噬越明显。

误区我用 EXPLAIN 一查,看到 type 是 ALL,立刻慌了,觉得必须马上加个索引,不然系统就要崩了。

正解type=ALL 只是表明走的是全表扫描,但全表扫描不等于慢。如果表只有几千行,全表扫描可能比走索引更快——就像在只有两辆车的小路口,装红绿灯纯属多余。正确做法是结合 rows 列看估算扫描行数,再对比实际表大小。如果扫描量远大于表行数的合理范围,或者该 SQL 执行频繁,才需要加索引。

为何常见:很多人把 ALL 当成了绝对错误,却没有理解 EXPLAIN 的本质是“执行方式的快照”,忽略了表大小、数据分布和 SQL 频率这些更基础的判断维度。

误区既然索引能加速查询,那我给每个查询条件列都建一个索引,这样无论怎么写 SQL 都能走索引,天下无敌。

正解索引不是免费的,每一棵 B+Tree 都要占用磁盘空间,每次 INSERT/UPDATE/DELETE 都要同步维护索引树,索引越多写入越慢。这就像城市里修了太多单行道,虽然方便了部分直达,却让整体路网变得拥堵。正确的做法是优先用复合索引覆盖高频查询,删除重复或低选择率的索引,让“路网密度”恰到好处。

为何常见:查询优化是显性收益,写入开销是隐性成本。初学者只看到了 SELECT 变快,忽略了写放大效应,等上线后写入性能暴跌才追悔莫及。

误区我建了复合索引 `(a,b,c)`,一直以为查询时必须写成 `WHERE a=1 AND b=2 AND c=3`,如果只写 b 和 c 就用不上索引,所以经常被迫给后端加一个无意义的 a 条件。

正解最左前缀法则说的是“查询条件中必须包含最左列 a,索引才能被用来定位”,并没有要求把 a 放在 WHERE 的第一个位置,也不要求 b、c 必须全部出现。WHERE b=2 AND a=1 和 WHERE a=1 AND c=3 都能用上索引的 a 部分。MySQL 优化器会帮你调整 WHERE 顺序。关键是理解:“从最左列开始连续匹配”是必要条件,不是“所有列必须全用上”。

为何常见:这个误区源于网上的简化表述被层层转述后走样,很多人只记住了“最左”两个字,而没有理解 B+Tree 索引按列顺序构造的本质。

面试题

Q1. MySQL 中控制慢查询是否开启的开关参数是?

单选题 · easy

  • A.slow_query_log 开关参数← 正确答案
  • B.long_query_time 阈值参数
  • C.log_output 输出目标参数
  • D.slow_query_log_file 路径参数

答案:A。slow_query_log 是总开关,置为 ON 才会开始记录慢查询;long_query_time 是判定阈值,log_output 控制日志输出到文件还是表,slow_query_log_file 指定文件路径。四个参数共同协作,但“开不开”由开关决定。

Q2. MySQL 默认的 long_query_time 阈值是多少秒?

单选题 · easy

  • A.0.5 秒超时阈值
  • B.1 秒执行时间
  • C.10 秒执行时间← 正确答案
  • D.5 秒执行时间

答案:C。MySQL 默认 long_query_time 为 10 秒。这个值对大多数互联网业务过于宽松,一条 3 秒的慢 SQL 在默认配置下不会被记录。生产环境通常需要手动调低到 1 秒或 0.5 秒。

Q3. EXPLAIN 输出中哪种 type 列值意味着全表扫描?

单选题 · mid

  • A.type 值为 ALL 全表扫← 正确答案
  • B.type 值为 ref 普通索引
  • C.type 值为 range 范围扫
  • D.type 值为 const 常量查

答案:A。ALL 表示全表扫描,是最差的访问类型,通常意味着扫描行数巨大,需要重点优化。ref 是普通索引等值匹配,range 是索引范围扫描,const 是通过主键或唯一索引定位,效率远高于 ALL。

Q4. 以下哪些情况会导致索引失效?

多选题 · mid

  • A.对索引列使用函数运算← 正确答案
  • B.对索引列做隐式类型转换← 正确答案
  • C.查询条件中无任何索引列
  • D.使用 LIKE 前导通配符查← 正确答案

答案:ABD。对索引列使用函数(如 YEAR(date))会让 B+Tree 无法按序定位;隐式类型转换会破坏索引列的值比较;LIKE 以通配符开头导致无法从树根开始匹配后缀。选项 C 是“本来就没索引可用”,不属于“索引失效”。

Q5. 关于最左前缀法则的正确理解是?

单选题 · mid

  • A.查询条件必须覆盖全部索引
  • B.需从复合索引最左列开始← 正确答案
  • C.不使用最左列也能走索引
  • D.复合索引任意列都能生效

答案:B。最左前缀法则要求查询必须包含复合索引的最左列,索引才能被用来加速定位。但不是必须覆盖全部列,也不是要求 WHERE 里最左列写在第一个位置。不使用最左列时,索引无法用于定位。

Q6. 深分页 LIMIT 1000000,10 的优化方案有哪些?

多选题 · mid

  • A.使用延迟关联先查主键← 正确答案
  • B.用上一页最后 ID 做游标← 正确答案
  • C.把 LIMIT 数值继续调大
  • D.增加 SELECT 返回列数

答案:AB。延迟关联先利用索引查主键,再回表取完整数据,避免扫描大量无关行;游标分页(WHERE id > last_id)让扫描量恒定。调大 LIMIT 只会更慢,增加返回列会增加 IO 开销,都是反向操作。

Q7. 关于覆盖索引的优势描述最准确的是?

单选题 · hard

  • A.覆盖索引可以完全避免回表← 正确答案
  • B.覆盖索引可以避免排序操作
  • C.覆盖索引可以让全表扫描提速
  • D.覆盖索引可以减少所有锁等待

答案:A。覆盖索引指索引中包含了查询所需的全部列,InnoDB 无需回主键索引取完整行,减少一次随机 IO。它并不保证避免排序,也不能让全表扫描变快,更与锁等待无直接关系。

Q8. EXPLAIN 中 KEY 列为 NULL 代表什么?

单选题 · easy

  • A.查询发生了死锁需要重试
  • B.该查询没有使用任何索引← 正确答案
  • C.索引已损坏需要重建
  • D.查询返回了空结果集

答案:B。KEY 列显示实际使用的索引名,NULL 说明查询没有用到任何索引,大概率走全表扫描。这和张三的 WHERE 条件没命中索引规则有关,也可能是无索引可命中。死锁、索引损坏、空结果集与 KEY 列无关。

Q9. 优化器进行代价估算时通常考虑哪些因素?

多选题 · hard

  • A.表行数和索引基数统计← 正确答案
  • B.WHERE 条件的过滤因子← 正确答案
  • C.数据页位于内存或磁盘← 正确答案
  • D.客户端与服务端网络延迟

答案:ABC。优化器依赖统计信息估算行数(A),用过滤因子估算 WHERE 条件的命中率(B),内存与磁盘的访问代价权重不同(C)。网络延迟属于客户端到服务器的传输环节,不在 MySQL 优化器的代价模型范围内。

Q10. pt-query-digest 分析报告主要按哪个指标排序?

单选题 · mid

  • A.按平均耗时从高到低排
  • B.按总耗时从高到低排列← 正确答案
  • C.按查询次数从高到低排
  • D.按锁等待时间从高到低排

答案:B。pt-query-digest 的报告默认按总耗时(Sum)排序,因为总耗时反映该类 SQL 对系统的累计影响,等于平均耗时乘执行次数。只看平均耗时容易漏掉高频但单次不慢的 SQL,只看次数又会忽略真正耗时的重 SQL。