面试官:在你的项目中,是如何进行SQL优化的?

354 阅读6分钟

本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~

这是近期的技术面试中妥妥的高频题,但很多同学的回答思路是错误的,他们往往会穷举一些进行SQL优化的case,如下图所示。

但需要注意的是,面试官是在问题中做了前置限定的,那就是“在你的项目中”。

面试官想要听的答案是,候选人在项目中对于慢SQL的监控——》分析——》优化的整体思路,以此来洞察其解决问题能力。

所以,对于该问题的回答,应该是另外一种思路。

SQL监控

进行SQL优化的第一步,当然是在生产环境中把系统中的慢SQL找出来了,有这样几种方式。

1、基于Druid连接池

其核心配置步骤‌为:

依赖引入‌,在Spring Boot项目中集成druid-spring-boot-starter依赖,替代默认的HikariCP连接池。

启用监控功能‌,在application.yml中配置stat和wall过滤器,暴露Druid监控端点(如/druid/sql.html)

spring:
  datasource:
    druid:
      filters: stat,wall,log4j2
      stat-view-servlet:
        enabled: true

功能特性包括:

慢SQL记录‌,通过slow-sql-millis参数定义慢查询阈值(如执行时间超过2秒),自动记录SQL语句及执行耗时。 ‌

可视化面板‌,内置监控页面展示SQL执行次数、最大耗时、最慢SQL排行等数据,支持导出日志分析。

2、MySQL原生慢查询日志

‌配置方法很简单,‌修改MySQL的my.cnf配置文件‌,重启MySQL服务即可生效。

slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log  
long_query_time = 1 # 单位:秒  ‌

在慢查询日志中,记录执行时间、锁定时间、返回行数等详细信息,可通过mysqldumpslow工具进行聚合分析。

慢查询日志方式比连接池方式更加准确一些,毕竟减少了网络层面的误差。

3、云平台自带功能

目前主流的方式,都是通过阿里云、腾讯云等平台自带功能来完成慢查询监控的,其底层实现是通过Exporter进行慢SQL收集,再被监控平台拉取并展示。

如下图所示:

当然,也有一些主流的互联网大厂,通过自研类似平台的方式进行慢SQL采集,其实都是大同小异的。

这里有一个问题,如果系统中的慢SQL很多,那我们应该优先优化哪个?

有同学会说,当然是优化查询时间最慢的那个SQL了。其实不然。

我们应该优化查询时间 * 查询次数 = 执行总时长最长的那个SQL,它对于数据库的影响才是最大的。

SQL分析

拿到慢SQL之后,我们要做的第一件事情,就是通过explain去查看它对应的执行计划了。

如下图所示:

在这里面,我们有几个需要额外关注的字段。

1、type列

该字段表示表访问方式,性能从优到劣排序为:

system > const > eq_ref > ref > range > index > ALL‌

  • const‌:通过主键或唯一索引定位单行(如WHERE id=1
  • eq_ref‌:联表查询时使用主键或唯一索引匹配(如JOIN ... ON a.id=b.id
  • ref‌:非唯一索引的等值查询(如WHERE name='张三'
  • range‌:索引范围扫描(如WHERE age>20
  • index:全索引扫描
  • ALL‌:全表扫描

这里需要特别注意的是,如果type字段为index或ALL,除非表中的记录数特别少,或者确实需要读取全部数据,否则一定是要进行SQL优化的。

2、key列与possible_keys列‌

  • possible_keys‌:SQL语句可能使用的索引,可以为多个,可以为Null。

  • key‌:SQL语句实际使用的索引,只有一个,可以为Null。

3、rows列与filtered列‌

  • rows:预计需要扫描的行数。

  • filtered:返回结果的记录数占需要读到的记录数的百分比。举个例子,一条SQL语句扫描1000行数据,最终只返回100条数据,那filtered就是10%。

    这里需要注意的是,filter的值越大就证明则过滤条件 + 其对应的索引越有效,减少了不必要的行扫描。

4、Extra

其他信息,如果出现 Using temporary(临时表) 或 Using filesort(文件排序),大概率需要进行SQL优化了。

SQL优化

在真实项目中进行慢SQL优化,有70%的场景都可以通过加索引来解决,其中的联合索引是比较考验工程师实力的。

一提到联合索引,可能很多同学都知道要遵循最左前缀原则、以及利用覆盖索引,但除此之外还有一些注意点。

1、区分度与查询频次的平衡

一提到联合索引,很多同学的第一反应是选择区分度高的字段作为先导列。

但假设这样一个场景,在某系统中A字段比B字段区分度高一些,但通过B字段进行查询的频率比A字段多几十倍,那应该如何选择?

当然是选择B字段作为先导列了,对吧?但是不是可以把(A,B)和(B,A)都建立上联合索引呢?

2、读操作与写操作的平衡

是不是可以把(A,B)和(B,A)都建立上联合索引呢?当然可以,但前提是系统中没有高并发写操作的情况。

因为过多的联合索引会导致数据库写操作的性能下降,从而在高并发场景下出现瓶颈。

如上文所说,有70%的慢SQL优化场景可以通过加索引来解决,还有20%的场景是MySQL的查询优化器“犯傻”,没有选择正确的索引来执行SQL语句。

此时,我们可以通过FORCE INDEX关键字,强制查询优化器走正确的索引进行执行。

btw:还记得我们在上文中提到,在SQL语句执行计划中的possible_keys字段吗?

如果该索引没出现在possible_keys字段中,即使我们通过FORCE INDEX关键字进行指定,索引也不会生效。

剩下10%的慢SQL优化场景,真的需要case by case的进行优化了。

比如:在多表关联的场景中,可以通过STRAIGHT_JOIN关键字进行强制指定,把查询记录数比较少的表作为驱动表,这样可以提升性能。

比如:在子查询场景中,如果子查询返回的记录数较少,主查询中的表较大且又有索引时应该用in。

反之如果外层的主查询返回的记录数较少,二子查询中的表大又有索引时使用exists。

当然,总有一些慢SQL是MySQL优化不了的,我们需要借助于Elasticsarch、ClickHouse、Doris、Kylin等数据库进行优化,毕竟术业有专攻。