SQL 优化与索引优化面试题

94 阅读12分钟

一、基础概念题答案​

  1. 什么是 SQL 优化?其核心目标是什么?​

SQL 优化是通过调整 SQL 语句结构、数据库表结构、索引设计等方式,提升 SQL 语句执行效率的过程。核心目标有三个:减少数据库资源消耗(如 CPU、内存、IO)、缩短 SQL 执行时间(满足业务响应需求)、提升数据库并发处理能力(避免单条 SQL 阻塞大量请求)。​

  1. 索引的本质是什么?在数据库中,索引主要有哪些数据结构,各有什么优缺点?​

索引的本质是帮助数据库高效查询数据的数据结构,相当于书籍的 “目录”,避免全表扫描。​

常见数据结构及优缺点:​

  • B + 树:主流数据库(MySQL、PostgreSQL)默认索引结构。优点:① 平衡树结构,查询效率稳定(时间复杂度 O (log n));② 叶子节点有序且串联,支持范围查询和排序;③ 非叶子节点仅存索引键,内存利用率高。缺点:插入 / 删除需维护树平衡,性能略低于哈希索引。​

  • 哈希索引:基于哈希表实现。优点:等值查询效率极高(时间复杂度 O (1))。缺点:① 不支持范围查询、排序;② 存在哈希冲突,需额外处理(如链表法);③ 不适用于高频更新场景。​

  • R 树:多用于空间数据索引(如 GIS 系统)。优点:支持多维数据范围查询。缺点:在非空间场景下效率低于 B + 树,维护成本高。​

  1. 聚簇索引和非聚簇索引的区别是什么?以 InnoDB 存储引擎为例,说明其默认的索引类型及特点。​

核心区别:数据是否与索引存储在一起。​

对比维度​聚簇索引​非聚簇索引​
数据存储位置​索引叶子节点存储完整数据​索引叶子节点存储主键值​
查询效率​等值 / 范围查询效率高​需回表(通过主键查数据)​
数量限制​一张表仅能有一个​一张表可有多個​
适用场景​主键查询、高频范围查询​非主键的高频查询​
InnoDB 默认索引类型:聚簇索引。特点:① 以主键(PRIMARY KEY)作为聚簇索引,若未显式定义主键,会选择唯一非空索引作为聚簇索引;若均无,则自动生成隐藏主键(6 字节 row_id);② 非聚簇索引(如普通索引、联合索引)的叶子节点存储主键值,查询时需通过主键 “回表” 获取完整数据(覆盖索引可避免回表)。​

二、SQL 语句优化题答案​

  1. 以下 SQL 语句存在性能问题,请指出问题所在并给出优化方案:​

sq取消自动换行复制

SELECT * FROM order_info WHERE user_id IN (SELECT user_id FROM user_info WHERE age > 30) AND order_time < '2024-01-01';​

  • 问题所在:① IN子查询可能导致数据库执行 “先查子查询,再逐行匹配主查询” 的低效逻辑,尤其当user_info表age > 30的记录较多时,会产生大量临时数据;② SELECT *会查询所有字段,若无需全部字段,会浪费 IO 资源,且无法利用覆盖索引;③ 未明确order_info表是否有user_id、order_time相关索引,可能导致全表扫描。​

  • 优化方案:​

方案 1:将IN子查询改为JOIN,减少临时数据生成:​

sql取消自动换行复制

SELECT oi.user_id, oi.order_time, oi.order_amount -- 只查需要的字段,避免SELECT *​

FROM order_info oi​

JOIN user_info ui ON oi.user_id = ui.user_id​

WHERE ui.age > 30 ​

AND oi.order_time < '2024-01-01';​

方案 2:为user_info(age, user_id)、order_info(user_id, order_time)创建联合索引,进一步提升查询效率(user_info索引覆盖子查询字段,order_info索引支持条件过滤)。​

  1. 当 SQL 语句中使用了OR条件时,可能会导致索引失效,请问为什么?如何避免这种情况,给出具体的优化示例。​
  • 索引失效原因:数据库优化器认为OR连接的多个条件若分别对应不同索引,需多次索引查询后合并结果,效率可能低于全表扫描;若部分条件无对应索引,会直接触发全表扫描。例如WHERE a = 1 OR b = 2,若仅a有索引,b无索引,优化器会放弃索引,选择全表扫描。​
  • 避免方案:​

方案 1:为OR连接的所有条件字段创建联合索引(适用于条件字段固定的场景)。​

示例:原 SQL(a有索引,b无索引,索引失效):​

sql取消自动换行复制

SELECT * FROM table WHERE a = 1 OR b = 2;​

优化:创建联合索引(a, b),或分别为a、b创建独立索引(MySQL 8.0 + 支持 “索引合并”,可利用多个独立索引),优化后 SQL 不变,可触发索引查询。​

方案 2:将OR拆分为UNION(适用于条件字段不适合建联合索引的场景):​

sql取消自动换行复制

SELECT * FROM table WHERE a = 1​

UNION -- 去重,若确认无重复可使用UNION ALL(效率更高)​

SELECT * FROM table WHERE b = 2;​

  1. 解释什么是 “慢查询”,如何开启 MySQL 的慢查询日志?拿到慢查询日志后,通常会使用哪些工具或方法来分析慢查询语句?​
  • 慢查询定义:执行时间超过预设阈值(默认long_query_time = 10秒)的 SQL 语句,或未使用索引的查询(需开启log_queries_not_using_indexes)。​

  • 开启 MySQL 慢查询日志(以 MySQL 5.7 + 为例):​

方式 1:临时开启(重启后失效):​

plaintext取消自动换行复制

SET GLOBAL slow_query_log = ON; -- 开启慢查询日志​

SET GLOBAL long_query_time = 2; -- 设置阈值为2秒(建议根据业务调整)​

SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log'; -- 指定日志存储路径​

SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未使用索引的查询​

方式 2:永久开启(修改配置文件my.cnf/my.ini):​

i取消自动换行复制

[mysqld]​

slow_query_log = ON​

long_query_time = 2​

slow_query_log_file = /var/lib/mysql/slow.log​

log_queries_not_using_indexes = ON​

修改后重启 MySQL:systemctl restart mysqld。​

  • 慢查询日志分析工具 / 方法:​

① mysqldumpslow(MySQL 自带工具):快速统计慢查询类型,例如:​

b取消自动换行复制

mysqldumpslow -s r -t 10 /var/lib/mysql/slow.log -- 按查询次数排序,显示前10条​

② pt-query-digest(Percona Toolkit 工具):详细分析,生成可视化报告,支持过滤、分组,例如:​

bash取消自动换行复制

pt-query-digest /var/lib/mysql/slow.log > slow_analysis.log -- 生成分析报告​

③ 可视化工具:Navicat、MySQL Workbench 的 “慢查询日志分析” 功能,或第三方工具(如 Sqlyog)。​

  1. 对于分页查询,当页码较大时(如LIMIT 10000, 20),SQL 语句执行效率会明显下降,分析其原因并提供至少两种优化方案,要求给出具体的 SQL 示例。​
  • 效率下降原因:LIMIT 10000, 20的逻辑是 “扫描前 10020 条数据,丢弃前 10000 条,返回后 20 条”,若无合适索引,会触发全表扫描,数据量越大,效率越低;即使有索引,也需遍历大量索引节点后回表。​

  • 优化方案:​

方案 1:利用主键有序性,通过 “主键过滤” 减少扫描范围(适用于主键自增场景):​

原 SQL(低效):​

sql取消自动换行复制

SELECT * FROM order_info ORDER BY order_id LIMIT 10000, 20;​

优化后(先查主键,再回表):​

sql取消自动换行复制

SELECT oi.* ​

FROM order_info oi​

JOIN (SELECT order_id FROM order_info ORDER BY order_id LIMIT 10000, 20) AS t​

ON oi.order_id = t.order_id;​

原理:子查询仅扫描索引(order_id为主键索引),获取目标主键后,通过主键回表查询完整数据,减少扫描行数。​

方案 2:使用 “条件分页”,通过上一页的最后一条数据的主键 / 索引字段,缩小查询范围(适用于已知上一页边界的场景,如滚动加载):​

假设上一页最后一条数据的order_id = 10000,优化后 SQL:​

sql取消自动换行复制

SELECT * FROM order_info WHERE order_id > 10000 ORDER BY order_id LIMIT 20;​

原理:直接通过order_id > 10000过滤,仅扫描 20 条数据,无需跳过前 10000 条,效率极大提升。​

方案 3:创建覆盖索引(若分页查询仅需部分字段):​

若查询字段为order_id、order_time、user_id,创建覆盖索引(order_id, order_time, user_id),SQL:​

plaintext取消自动换行复制

SELECT order_id, order_time, user_id FROM order_info ORDER BY order_id LIMIT 10000, 20;​

原理:索引包含所有查询字段,无需回表,直接从索引中获取数据,提升效率。​

三、索引优化题答案​

  1. 假设存在一张商品表(product),包含字段:product_id(主键)、category_id(商品分类 ID)、product_name(商品名称)、price(价格)、create_time(创建时间)。现有高频查询场景:“查询某一分类下价格大于 100 元且创建时间在近 30 天内的商品名称和价格”,请设计合适的索引,并说明设计理由。​

补充:若create_time的范围过滤后数据量已很小,也可简化为(category_id, create_time, price),但包含product_name的覆盖索引效率更高。​

  • 索引设计:创建联合索引(category_id, create_time, price, product_name)。​

  • 设计理由:​

① 遵循 “最左前缀原则”:查询条件中category_id是 “等值条件”(某一分类),优先作为索引第一列,可快速过滤出目标分类的所有商品;​

② create_time是 “范围条件”(近 30 天内),作为索引第二列,在category_id过滤后,进一步缩小时间范围;​

③ price是 “范围条件”(大于 100 元),作为索引第三列,继续过滤价格符合条件的商品;​

④ product_name和price是查询结果字段(商品名称和价格),将其加入索引,形成 “覆盖索引”,避免回表查询,直接从索引中获取结果,提升效率。​

  1. 什么是 “索引失效”?列举至少 5 种导致索引失效的常见情况,并分别给出对应的避免方法。​
  • 索引失效定义:SQL 语句本应使用索引查询,但因条件或语句结构问题,数据库优化器放弃索引,转而使用全表扫描的现象。​

  • 常见情况及避免方法:​

① 索引字段使用函数或运算:如WHERE SUBSTR(product_name, 1, 2) = '手机'(product_name有索引)。​

避免方法:将函数运算转移到右侧,如WHERE product_name LIKE '手机%'(可触发前缀索引);或创建函数索引(如INDEX idx_func (SUBSTR(product_name, 1, 2)))。​

② 索引字段类型不匹配:如product_id是 int 类型,查询时用字符串WHERE product_id = '123'(隐式类型转换)。​

避免方法:确保查询条件的值类型与索引字段类型一致,如WHERE product_id = 123。​

③ 使用NOT IN、!=、<>:如WHERE category_id NOT IN (1,2,3)(category_id有索引)。​

避免方法:用NOT EXISTS或范围查询替代,如WHERE category_id < 1 OR category_id > 3(若分类 ID 连续)。​

④ LIKE以通配符开头:如WHERE product_name LIKE '%手机%'(product_name有索引)。​

避免方法:尽量使用前缀匹配(LIKE '手机%');若需后缀或中间匹配,可使用全文索引(如 MySQL 的 FULLTEXT 索引)。​

⑤ OR连接的条件部分无索引:如WHERE category_id = 1 OR price > 100(仅category_id有索引)。​

避免方法:为所有OR条件字段创建索引(独立索引或联合索引),或拆分为UNION查询(见二.2 题)。​

⑥ 查询条件包含IS NULL/IS NOT NULL:若索引字段允许为 NULL,WHERE price IS NULL可能失效(视数据库版本和数据分布而定)。​

避免方法:字段设计时尽量避免 NULL(用默认值替代,如价格默认 0);或创建包含 NULL 的索引(MySQL 8.0 + 对 NULL 的索引支持更友好)。​

  1. 如何判断一张表的索引是否过多?过多的索引会带来哪些负面影响?在实际工作中,如何平衡查询性能和索引维护成本?​
  • 判断索引过多的方法:​

① 索引数量与表字段数量对比:若索引数量超过表字段数量的 2-3 倍,需警惕(如 10 个字段的表,索引超过 20 个);​

② 索引使用率低:通过数据库工具查看索引使用情况(如 MySQL 的sys.schema_unused_indexes视图,或performance_schema的index_io_usage表),长期未使用(如 3 个月以上)的索引可视为冗余;​

③ 索引重复或重叠:如已存在联合索引(a,b),再创建索引(a),则(a)为冗余索引(联合索引(a,b)可覆盖(a)的查询场景)。​

  • 过多索引的负面影响:​

① 写入性能下降:插入、更新、删除数据时,需同步维护所有相关索引(如 B + 树的分裂、合并),索引越多,写入耗时越长;​

② 占用存储空间:索引需单独存储,过多索引会浪费磁盘空间(尤其是大表,一个索引可能占用 GB 级空间);​

③ 优化器选择负担加重:索引过多时,数据库优化器需遍历更多索引组合,可能导致选择低效索引。​

  • 平衡查询性能与索引维护成本的策略:​

① 按需创建索引:仅为高频查询(如 QPS>10)的条件字段创建索引,低频查询(如每日一次的统计查询)无需建索引;​

② 优先使用联合索引:用一个联合索引覆盖多个查询场景(如(a,b)可覆盖a的等值查询、a+b的组合查询),减少索引数量;​

③ 定期清理冗余索引:每季度或半年审计一次索引,删除未使用、重复或重叠的索引;​

④ 分场景优化:写入密集型表(如订单表)尽量少建索引;查询密集型表(如商品详情表)可适当多建索引,但需控制总量。​

  1. 联合索引的 “最左前缀原则” 是什么?假设创建了联合索引(a, b, c),请判断以下 SQL 语句是否能使用该索引,并说明原因:​
  • 最左前缀原则定义:联合索引的查询效率取决于 “从左到右的连续匹配”,即查询条件需包含索引的最左列,且后续列需连续匹配(等