SQL语句执行时间太慢,有什么优化措施?以及衍生的相关问题
SQL语句执行时间太慢,有什么优化措施?
可以从四个方面进行:
第一个是查询是否添加了索引
如果没有的话,为查询字段添加索引,
还有是否存在让索引失效的场景,像是没有遵循最左前缀,进行了一些类型转化
第二点是SQL语句本身的优化,
1、如避免使用SELECT *,只查询需要的字段
2、优化JOIN操作,避免笛卡尔积
如 SELECT * FROM a JOIN b(缺少 a.id = b.a_id),会产生 “笛卡尔积”(数据量 = 表 a 行数 × 表 b 行数),瞬间耗尽数据库资源。 优化:JOIN 必须加关联条件,且关联字段需建索引(如 a.id 和 b.a_id)。
3、大表与大表直接 JOIN 若两张表均有百万级数据,直接 JOIN 会产生大量中间结果,耗时极长。
第三点是表结构设计优化
像是采用分库分表的方式,解决数据量过大问题
- 水平分表(按行拆分):将一张表按规则拆分为多张表,每张表结构相同,数据不同。常见规则:
- 时间范围:
orders_2023
、orders_2024
(按年份拆分); - 哈希:
user_0
~user_31
(按user_id % 32
拆分)。工具:Sharding-JDBC、MyCat。
- 时间范围:
- 垂直分库(按业务拆分):将一个数据库按业务模块拆分为多个数据库,如电商系统拆分为
user_db
(用户)、order_db
(订单)、product_db
(商品),避免单库压力过大。
第四点是架构优化
1、像是使用redis提前存储数据,减轻数据库的请求压力,避免每次查询都访问数据库。
2、采用读写分离的方式,将 “读操作”(如查询)路由到从库,“写操作”(如插入、更新)路由到主库,避免主库读压力过大。
衍生出的问题:
1、为什么添加索引后,SQL的执行时间就变快了呐?
首先我们要了解索引这个概念,如果将数据库比作一本,那么索引就相当于是这本书的目录,而如果没有目录的话,当查找某个内容的话,你只能一页一页查找,,数据量越大,翻页时间越长;
当添加了目录后,你就可以精准的定位到某个对应,解决无效的翻页时间。
索引的核心原理就是将“全表扫描”转化为“精准定位”
数据库表的原始数据(行数据)存储在磁盘上,默认是 “无序” 的(除非按主键排序)。当没有索引时,查询数据(如 WHERE user_id = 123
)需要做以下操作:
- 从磁盘读取表的第一行数据,检查
user_id
是否等于 123; - 不等于则继续读第二行、第三行…… 直到遍历完所有行(全表扫描);
- 若表有 100 万行数据,最坏情况需要读取 100 万次磁盘 —— 而磁盘 IO 是数据库性能的 “最大瓶颈”(磁盘读写速度比内存慢 1000 倍以上)。
添加索引后,情况完全不同:索引会单独创建一个 “有序的索引结构”,把 “查询条件字段(如 user_id
)” 和 “行数据的磁盘地址” 关联起来,并且按 user_id
排序。此时查询 user_id = 123
的流程变成:
- 去索引结构中查找
user_id = 123
—— 由于索引是有序的,可通过 “二分查找”(类似查字典)快速定位,只需 3~4 次磁盘 IO(100 万数据的二分查找次数仅约 20 次,远少于全表扫描的 100 万次); - 从索引中获取对应行数据的磁盘地址;
- 直接根据地址读取目标行数据,无需遍历其他行。
底层逻辑
索引的数据结构是B + 树索引
B+树作为索引的存储结构。选择B+树的原因包括:
- 节点可以有更多子节点,路径更短;
- 磁盘读写代价更低,非叶子节点只存储键值和指针,叶子节点存储数据;
- B+树适合范围查询和扫描,因为叶子节点形成了一个双向链表。
2、如何分析这条执行很慢的SQL语句?
采用explain命令,分析这条SQL的执行情况。通过key
和key_len
可以检查是否命中了索引,如果已经添加了索引,也可以判断索引是否有效。通过type
字段可以查看SQL是否有优化空间,比如是否存在全索引扫描或全表扫描。通过extra
建议可以判断是否出现回表情况,如果出现,可以尝试添加索引或修改返回字段来优化。
3、索引失效的场景
- 没有遵循最左前缀原则。
- 使用了模糊查询且
%
号在前面。 - 在索引字段上进行了运算或类型转换。
- 使用了复合索引但在中间使用了范围查询,导致右边的条件索引失效。
**扩展:**最左前缀原则
索引失效的最左前缀原则是针对联合索引(多字段索引)的一条核心规则, 简单来说:在联合索引中,查询条件必须从索引的第一个字段开始匹配,且中间不能跳过任何字段,否则跳过的字段及之后的字段无法使用索引,导致索引失效或部分失效。 底层原理:
联合索引在底层(如 B + 树)的存储是 “先按第一个字段排序,第一个字段相同的再按第二个字段排序,以此类推”。
如对对(a, b, c)
建立联合索引
- 先按
a
升序排列; - 当
a
相等时,按b
升序排列; - 当
a
和b
都相等时,按c
升序排列。
4、读写分离模式下如何保证主从数据一致性
原因:由于主库数据同步到从库存在延迟(如网络传输、SQL 执行耗时),可能导致 “主库写入数据后,从库读取不到最新数据” 的问题。
解决方式:
- 配置合适刷盘策越
- 减少binlog的日志量,避免大事务,拆分为事务。
- 写读后延迟等待,比如写操作后,线程休眠一段时间,再读从库
- 增加重试机制,:读从库时若获取到旧数据(可通过版本号或时间戳判断),重试几次(如 3 次,每次间隔 50ms),直到获取最新数据或超时后读主库。
- 对于强一致要求的数据,像是金融-支付,可以读主库,弱一致性的数据,像是电商商品展示,日志查询,允许一定的延迟,可以读从库。
5、如何保证缓存和数据库的数据一致性,(如,一次大量的请求到来,如何添加缓存?)
核心原则:先操作数据库,然后再是缓存
方案一:
最常用的方案,适合大多数业务场景(最终一致性),流程如下:
1. 读操作
- 先查缓存:命中则直接返回;
- 缓存未命中:查数据库,将结果写入缓存,再返回。
2. 写操作
- 先更新数据库;
- 再删除缓存(而非更新缓存)。
为什么删除缓存,而不是更新? 主要是避免 “缓存更新逻辑与数据库更新逻辑不一致” 导致的错误(如数据库有触发器 / 事务,缓存更新可能漏处理);
方案二:
相对于方案一做出一点改变: 更新数据库后主动更新缓存
需要注意的点是 必须在数据库事务内更新缓存,确保数据库与缓存操作 “同成功同失败”。
方案三:
延迟双删 在高并发场景下,可能出现 “数据库已更新,但缓存删除请求因网络延迟未执行” 的情况,导致旧数据残留。
操作原理:
- 第一次删除:尽可能在数据库更新前清除旧缓存;
- 第二次删除:针对 “数据库更新后,缓存删除请求失败” 或 “有其他线程在数据库更新期间写入了旧数据到缓存” 的场景,再次清理。
方案四:
基于 binlog 的异步更新缓存(高可用场景)
通过监听数据库 binlog(如 MySQL 的 binlog),异步更新缓存,适合读写分离、高并发场景:
- 流程:
- 数据库更新后,binlog 记录数据变更;
- 监听组件解析 binlog,获取变更数据;
- 缓存更新服务根据变更数据,异步更新或删除缓存。