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_2023orders_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)需要做以下操作:

  1. 从磁盘读取表的第一行数据,检查 user_id 是否等于 123;
  2. 不等于则继续读第二行、第三行...... 直到遍历完所有行(全表扫描);
  3. 若表有 100 万行数据,最坏情况需要读取 100 万次磁盘 ------ 而磁盘 IO 是数据库性能的 "最大瓶颈"(磁盘读写速度比内存慢 1000 倍以上)。

添加索引后,情况完全不同:索引会单独创建一个 "有序的索引结构 ",把 "查询条件字段(如 user_id)" 和 "行数据的磁盘地址" 关联起来,并且按 user_id 排序。此时查询 user_id = 123 的流程变成:

  1. 去索引结构中查找 user_id = 123------ 由于索引是有序的,可通过 "二分查找"(类似查字典)快速定位,只需 3~4 次磁盘 IO(100 万数据的二分查找次数仅约 20 次,远少于全表扫描的 100 万次);
  2. 从索引中获取对应行数据的磁盘地址;
  3. 直接根据地址读取目标行数据,无需遍历其他行。

底层逻辑

索引的数据结构是B + 树索引

B+树作为索引的存储结构。选择B+树的原因包括:

  • 节点可以有更多子节点,路径更短;
  • 磁盘读写代价更低,非叶子节点只存储键值和指针,叶子节点存储数据;
  • B+树适合范围查询和扫描,因为叶子节点形成了一个双向链表。

2、如何分析这条执行很慢的SQL语句?

采用explain命令,分析这条SQL的执行情况。通过keykey_len可以检查是否命中了索引,如果已经添加了索引,也可以判断索引是否有效。通过type字段可以查看SQL是否有优化空间,比如是否存在全索引扫描或全表扫描。通过extra建议可以判断是否出现回表情况,如果出现,可以尝试添加索引或修改返回字段来优化。

3、索引失效的场景

  • 没有遵循最左前缀原则。
  • 使用了模糊查询且%号在前面。
  • 在索引字段上进行了运算或类型转换。
  • 使用了复合索引但在中间使用了范围查询,导致右边的条件索引失效。

**扩展:**最左前缀原则

索引失效的最左前缀原则是针对联合索引(多字段索引)的一条核心规则, 简单来说:在联合索引中,查询条件必须从索引的第一个字段开始匹配,且中间不能跳过任何字段,否则跳过的字段及之后的字段无法使用索引,导致索引失效或部分失效。 底层原理:

联合索引在底层(如 B + 树)的存储是 "先按第一个字段排序,第一个字段相同的再按第二个字段排序,以此类推"

如对对(a, b, c) 建立联合索引

  1. 先按 a 升序排列;
  2. a 相等时,按 b 升序排列;
  3. ab 都相等时,按 c 升序排列。

4、读写分离模式下如何保证主从数据一致性

原因:由于主库数据同步到从库存在延迟(如网络传输、SQL 执行耗时),可能导致 "主库写入数据后,从库读取不到最新数据" 的问题。

解决方式:

  • 配置合适刷盘策越
  • 减少binlog的日志量,避免大事务,拆分为事务。
  • 写读后延迟等待,比如写操作后,线程休眠一段时间,再读从库
  • 增加重试机制,:读从库时若获取到旧数据(可通过版本号或时间戳判断),重试几次(如 3 次,每次间隔 50ms),直到获取最新数据或超时后读主库。
  • 对于强一致要求的数据,像是金融-支付,可以读主库,弱一致性的数据,像是电商商品展示,日志查询,允许一定的延迟,可以读从库。

5、如何保证缓存和数据库的数据一致性,(如,一次大量的请求到来,如何添加缓存?)

核心原则:先操作数据库,然后再是缓存

方案一:

最常用的方案,适合大多数业务场景(最终一致性),流程如下:

1. 读操作

  • 先查缓存:命中则直接返回;
  • 缓存未命中:查数据库,将结果写入缓存,再返回。

2. 写操作

  • 先更新数据库;
  • 再删除缓存(而非更新缓存)。

为什么删除缓存,而不是更新? 主要是避免 "缓存更新逻辑与数据库更新逻辑不一致" 导致的错误(如数据库有触发器 / 事务,缓存更新可能漏处理);

方案二:

相对于方案一做出一点改变: 更新数据库后主动更新缓存

需要注意的点是 必须在数据库事务内更新缓存,确保数据库与缓存操作 "同成功同失败"。

方案三:

延迟双删 在高并发场景下,可能出现 "数据库已更新,但缓存删除请求因网络延迟未执行" 的情况,导致旧数据残留。

操作原理

  • 第一次删除:尽可能在数据库更新前清除旧缓存;
  • 第二次删除:针对 "数据库更新后,缓存删除请求失败" 或 "有其他线程在数据库更新期间写入了旧数据到缓存" 的场景,再次清理。

方案四:

基于 binlog 的异步更新缓存(高可用场景)

通过监听数据库 binlog(如 MySQL 的 binlog),异步更新缓存,适合读写分离、高并发场景:

  1. 流程
    • 数据库更新后,binlog 记录数据变更;
    • 监听组件解析 binlog,获取变更数据;
    • 缓存更新服务根据变更数据,异步更新或删除缓存。
相关推荐
月夕·花晨1 小时前
Gateway-过滤器
java·分布式·spring·spring cloud·微服务·gateway·sentinel
hssfscv2 小时前
JAVA学习笔记——9道综合练习习题+二维数组
java·笔记·学习
初听于你4 小时前
缓存技术揭秘
java·运维·服务器·开发语言·spring·缓存
小蒜学长5 小时前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
恒悦sunsite6 小时前
Ubuntu之apt安装ClickHouse数据库
数据库·clickhouse·ubuntu·列式存储·8123
奥尔特星云大使7 小时前
MySQL 慢查询日志slow query log
android·数据库·mysql·adb·慢日志·slow query log
来自宇宙的曹先生7 小时前
MySQL 存储引擎 API
数据库·mysql
间彧7 小时前
MySQL Performance Schema详解与实战应用
数据库
间彧7 小时前
MySQL Exporter采集的关键指标有哪些,如何解读这些指标?
数据库
weixin_446260857 小时前
Django - 让开发变得简单高效的Web框架
前端·数据库·django