Vanson's Eternal Blog

MySQL夯实基础

Mysql basic.png
Published on
/90 mins read/---

MySQL

Basic

整体架构

客户端/服务器层(连接层)

作用:处理客户端连接和基础通信。

  • 连接管理:通过线程池处理客户端连接(如JDBC/ODBC),每个连接分配独立线程。
  • 身份认证:验证用户名、密码、主机权限等。
  • 安全与权限:检查用户是否有执行特定操作的权限。
  • 通信协议:支持多种协议(TCP/IP、Unix Socket等)。

核心服务层(Server层)

SQL处理的核心逻辑,跨存储引擎的通用功能。

  • SQL接口:接收SQL命令(如DML、DDL),返回结果。

  • 查询解析器:

    • 词法/语法分析:将SQL转换为语法树。
    • 语义检查:验证表名、列名是否存在。
  • 查询优化器:

    • 生成执行计划:选择最优索引或连接方式(如成本估算)。
    • 重写查询:简化表达式或子查询优化。
  • 执行器:调用存储引擎接口执行计划,处理结果。

  • 缓存(8.0已移除):早期版本缓存SELECT结果,但因命中率低被废弃。

  • 内置函数:如数学运算、日期处理等。

存储引擎层(插件式架构)

负责数据的物理存储和检索,支持多引擎以适应不同场景。

  • InnoDB(默认引擎):
    • 支持事务(ACID)、行级锁、外键。
    • 使用聚集索引(主键索引与数据文件绑定)。
  • MyISAM:
    • 表级锁、全文索引,适合读多写少场景。
    • 不支持事务,崩溃后恢复慢。
  • Memory:内存引擎,高速但临时性。
  • Archive:高压缩比,适合日志归档。
  • 自定义引擎:可通过API开发插件式引擎。

脏读

读取其他事务未提交的数据。若该事务回滚,读取的数据无效‌。

事务A修改数据但未提交 → 事务B读取修改后的值 → 事务A回滚 → 事务B基于无效数据操作导致错误。

-- 事务A(未提交)
BEGIN;
UPDATE users SET balance=500 WHERE id=1;  -- 修改未提交
-- 事务B(脏读)
BEGIN;
SELECT balance FROM users WHERE id=1;     -- 读到500(脏数据)
-- 事务A回滚
ROLLBACK;
 
 

幻读

幻读是指在同一事务内,连续执行两次相同的查询,第二次查询看到了第一次查询没有看到的行,这些"幻影行"通常是由其他事务插入的新数据。

同一事务内多次范围查询,结果集行数因其他事务新增/删除而不同‌

-- 事务A
BEGIN;
SELECT * FROM products WHERE price > 100;
-- 返回结果:id=1, price=150
 
-- 同时事务B插入新数据
INSERT INTO products VALUES(2, '新商品', 200);
COMMIT;
 
-- 事务A再次查询
SELECT * FROM products WHERE price > 100;
-- 现在返回:id=1, price=150 和 id=2, price=200
-- 出现了"幻影行"id=2
COMMIT;
  • READ UNCOMMITTED:会出现幻读
  • READ COMMITTED:会出现幻读
  • REPEATABLE READ:InnoDB通过间隙锁防止幻读
  • SERIALIZABLE:通过完全串行化避免幻读

解决幻读

  • 快照读:普通SELECT使用MVCC(多版本并发控制),基于事务开始时的一致性视图
  • 当前读:SELECT FOR UPDATE等使用间隙锁(Gap Lock)锁定查询范围

与不可重复读区别‌:

  • 不可重复读:同一行数据的值被修改(如余额变化)‌12。
  • 幻读:结果集行数变化(如新增符合条件的数据)‌
-- 事务A
BEGIN;
SELECT * FROM products WHERE price > 100 FOR UPDATE; -- 锁定price>100的范围
-- 此时会阻止其他事务在price>100范围内插入数据
 
-- 事务B尝试插入会被阻塞
INSERT INTO products VALUES(3, '新商品2', 180); -- 阻塞直到事务A提交

实际开发中的注意事项

  • 需要精确控制数据一致性时使用SELECT FOR UPDATE
  • 批量操作时注意间隙锁可能导致的锁范围过大
  • 合理设计索引(间隙锁基于索引工作)
  • 考虑使用乐观锁替代部分场景的悲观锁
‌本质‌读未提交数据结果集行数变化
‌触发操作‌其他事务未提交的 UPDATE / INSERT其他事务已提交的 INSERT / DELETE
‌关注点‌数据有效性(可能回滚)结果集完整性(行数一致性)
‌最小解决隔离级别‌READ COMMITTEDREPEATABLE READ(部分解决)

MySQL中的视图(View)

视图是一个虚拟表,其内容由查询定义。简单来说,视图就是一条SELECT语句执行后返回的结果集。视图本身并不在数据库中以存储的数据形式存在。使用视图的主要场景包括:

  • 简化复杂的SQL操作:视图可以将复杂的查询封装起来,用户只需要查询视图,而不需要了解底层的复杂查询。
  • 提高数据安全性:通过视图,用户只能访问他们被允许查看的数据。
  • 逻辑数据独立性:视图可以帮助将应用程序与底层表结构的变化隔离开来。

MySQL线程模型

核心线程

  • 前端连接线程(Client Connection Threads)

    • 每个客户端连接对应一个独立线程
    • 负责处理客户端认证、SQL请求接收和结果返回
    • 默认最大连接数由max_connections控制(默认151)
  • 后台系统线程(System Threads)

    • Master Thread:核心控制线程,负责脏页刷新、undo页回收等
    • IO Thread:处理异步IO请求(读写、insert buffer、redo log等)
    • Purge Thread:清理无用的undo日志
    • Page Cleaner Thread:脏页刷新
    • 锁监控线程:检测死锁
  • 工作线程(Worker Threads)

    • 线程池模式下动态创建的工作线程
    • 执行实际SQL解析、优化和执行操作

线程池机制

  • 线程组(Thread Group) 每个组管理固定数量的工作线程(默认16个)
  • 监听线程(Listener) 接收新连接请求并分配给线程组
  • 任务队列(Task Queue) 存放待处理的连接请求,工作线程从队列获取任务

存储过程和函数有什么区别

我们常用的操作数据库语言SQL语句在执行的时候需要要先编译,然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执行它。 一个存储过程是一个可编程的函数,它在数据库中创建并保存。它可以有SQL语句和一些特殊的控制结构组成。当希望在不同的应用程序或平台上执行相同的函数,或者封装特定功能时,存储过程是非常有用的。数据库中的存储过程可以看做是对编程中面向对象方法的模拟。它允许控制数据的访问方式。

特性存储过程 (Stored Procedure)函数 (Function)
返回值无直接返回值,可通过OUT参数返回多个值必须有且仅有一个返回值
调用方式使用 CALL proc_name() 单独调用可嵌入SQL语句中直接调用
事务控制支持完整的事务控制(BEGIN/COMMIT/ROLLBACK)通常不包含事务控制
DML操作可执行INSERT/UPDATE/DELETE等操作通常只进行查询和计算
异常处理支持完整的异常处理(DECLARE HANDLER)通常不处理异常
动态SQL支持PREPARE/EXECUTE执行动态SQL不支持动态SQL
临时表可以创建和使用临时表一般不能使用临时表
确定性不要求确定性可声明为DETERMINISTIC(相同输入总是产生相同输出)
适用场景1. 复杂业务逻辑处理2. 批量数据操作3. 需要事务的多个SQL操作1. 数据计算和转换2. 查询中的值处理3. 封装可重用计算逻辑
权限控制可以单独设置EXECUTE权限权限通常跟随调用者
预编译是,执行效率高是,执行效率高
返回结果集可以通过SELECT返回结果集只能返回单一值

JOIN类型

INNER JOIN(内连接)

功能:只返回两个表中满足连接条件的匹配行

特点:

  • 结果集只包含两表都有匹配的记录
  • 如果某行在其中一个表中没有匹配项,则该行不会出现在结果中
  • 性能通常最好,因为结果集最小
SELECT employees.name, departments.department_name
FROM employees
INNER JOIN departments ON employees.dept_id = departments.id;

LEFT JOIN / LEFT OUTER JOIN(左外连接)

功能:返回左表所有记录,即使右表没有匹配

特点:

  • 左表所有行都会出现在结果中
  • 右表无匹配时显示NULL值
  • 常用于"包含所有A,以及匹配的B"场景
SELECT customers.customer_name, orders.order_date
FROM customers
LEFT JOIN orders ON customers.id = orders.customer_id;
 

RIGHT JOIN / RIGHT OUTER JOIN(右外连接)

功能:返回右表所有记录,即使左表没有匹配

特点:

  • 右表所有行都会出现在结果中
  • 左表无匹配时显示NULL值
  • 使用频率通常低于LEFT JOIN
SELECT departments.department_name, employees.name
FROM employees
RIGHT JOIN departments ON employees.dept_id = departments.id;

FULL OUTER JOIN(全外连接)

功能:返回两表所有记录,无匹配时对应侧为NULL MySQL实现方式:通过UNION模拟

SELECT * FROM table1 LEFT JOIN table2 ON table1.id = table2.id
UNION
SELECT * FROM table1 RIGHT JOIN table2 ON table1.id = table2.id
WHERE table1.id IS NULL;

CROSS JOIN(交叉连接)

功能:返回两表的笛卡尔积

特点:

  • 不使用ON条件
  • 结果行数 = 表1行数 × 表2行数
  • 谨慎使用,可能产生巨大结果集

SELF JOIN(自连接)

功能:表与自身连接

典型应用:

  • 层级数据查询(如组织结构)
  • 比较同一表内的记录
  • 示例(查找同一部门的员工):

触发器

触发器是一种特殊的存储过程,它会在满足特定条件时自动执行,这些条件通常与数据库表的INSERT、UPDATE或DELETE操作相关。

触发器可以帮助自动化数据验证、维护数据完整性或执行其他需要在数据修改前后进行的任务。

使用场景:

  • 数据验证:例如,在插入新记录之前检查数据的有效性。
  • 数据完整性:自动更新其他相关表中的数据,以保持数据的一致性。
  • 日志记录:记录对数据表进行的更改,以便后续审计或调试。
  • 事件通知:触发外部系统的通知或操作。

触发点:

  • Before Insert
  • After Insert
  • Before Update
  • After Update
  • Before Delete
  • After Delete

外键约束及其作用

外键约束是一种数据库完整性约束,它指定一个表中的字段是另一个表的主键。外键用于确保引用完整性,

即如果在一个表中有一个字段引用了另一个表的主键,那么这个字段的值必须是对应表中存在的值。

作用:

  • 防止无效数据的插入:通过确保只能插入在另一个表中已存在的外键值,从而维护数据库的完整性。
  • 级联操作:当主键表中的数据发生变化时(如更新或删除),可以自动更新或删除引用该主键的外键表中的相应数据。

存储引擎

MySQL中的存储引擎是用于处理数据库中数据的底层软件组件。

不同的存储引擎提供不同的数据存储机制、索引技巧、锁定水平以及对事务的处理能力。

  • 事务支持:InnoDB支持事务和ACID属性,而MyISAM不支持。
  • 锁机制:InnoDB支持行级锁和MVCC(多版本并发控制),适合高并发场景;MyISAM仅支持表级锁。
  • 数据恢复:InnoDB支持崩溃恢复能力,MyISAM则相对较弱。
  • 存储空间:MyISAM通常占用较少的存储空间,并且支持全文索引;而InnoDB则占用更多的空间来维护事务和行级锁等特性。
  • 应用场景:InnoDB适合需要事务支持、高并发写入和数据恢复能力的应用;MyISAM适合只读或大量读取的应用,以及对存储空间有严格要求的环境。

缓冲池Buffer Pool

缓冲池是InnoDB存储引擎中用于缓存数据和索引的内存区域。当InnoDB需要读取数据时,它首先会检查所需的数据是否已经在缓冲池中。如果是,则直接从缓冲池读取,避免了磁盘I/O的开销。

当数据被修改时,修改后的数据也会先写入缓冲池,然后再异步刷新到磁盘。

缓冲池是InnoDB存储引擎中用于缓存数据和索引的内存区域。当InnoDB需要读取数据时,它首先会检查所需的数据是否已经在缓冲池中。如果是,则直接从缓冲池读取,避免了磁盘I/O的开销。当数据被修改时,修改后的数据也会先写入缓冲池,然后再异步刷新到磁盘。

  • 减少磁盘I/O操作,提高数据访问速度。
  • 通过将热点数据保留在内存中,提高系统的吞吐量。
  • 允许数据库处理比物理内存更多的数据,通过LRU算法管理缓冲池中的数据页。

写缓冲和双写缓冲

写缓冲

通常指的是在非聚集索引页上的更改被缓存起来,以便稍后异步写入磁盘的机制。

这有助于减少磁盘I/O次数,提高写入性能。然而,需要注意的是,在InnoDB中,这个术语可能有些混淆,因为InnoDB使用了一种称为“更改缓冲”(Change Buffer)的机制,它类似于写缓冲,但用于非唯一二级索引的插入、更新和删除操作。

双写缓冲

是InnoDB特有的一个机制,用于防止数据页在写入过程中因系统崩溃而损坏。

当InnoDB需要将一个数据页写入磁盘时,它首先将该页的一个副本写入双写缓冲区,然后再写入其实际位置。

如果在写入实际位置之前发生了系统崩溃,InnoDB可以在重启时从双写缓冲区恢复数据页。

回表

当查询需要访问不在当前二级索引里的列时,InnoDB 必须再通过主键索引把整行数据读出来,这个二次访问主键索引的过程就叫 回表。

为什么会发生回表?

  • InnoDB 二级索引结构:叶子节点只保存「索引键值 + 对应行的主键值」,不保存完整行数据。
  • 查询需要额外列:如果 SQL 需要的列不在二级索引里,就必须用主键值到聚簇索引(主键索引)再查一次,把整行读出来。

如何避免回表?

覆盖索引:让二级索引包含所有需要的列(即 SELECT 的列、WHERE、GROUP BY、ORDER BY 用到的列都包含在联合索引里)。

只查询索引列:SELECT 只查索引里已有的列。

-- 二级索引 (age, name, city) 覆盖了查询列
SELECT name, city FROM user WHERE age = 25;
-- Extra: Using index(不再回表)
 

MVCC

基本特点:

  • 读不加锁:SELECT操作无需申请锁
  • 读写不冲突:读操作不会阻塞写操作,写操作也不会阻塞读操作
  • 版本控制:同一数据存在多个版本
  • 快照读:事务看到的是数据在某个时间点的快照

MVCC实现架构

版本记录系统

InnoDB通过三个隐藏字段实现版本控制:

  • DB_TRX_ID: 6字节, 最近修改该行的事务ID,记录创建/最后一次修改该记录的事务编号
  • DB_ROLL_PTR: 7字节, 回滚指针,指向该记录的undo log记录(构成版本链的关键)
  • DB_ROW_ID: 6字节, 隐含的自增ID(当表无主键时自动生成)

Undo日志版本链

  • 每个修改操作都会生成对应的undo记录
  • 通过DB_ROLL_PTR指针形成版本链
  • 旧版本数据存放在undo表空间中
当前记录 → [undo记录1] → [undo记录2] → [undo记录3]
           (旧版本)      (更旧版本)    (最早版本)

ReadView机制

事务执行快照读时生成的"可见性视图",包含:

  • trx_list:当前活跃事务ID列表
  • up_limit_id:活跃事务最小ID
  • low_limit_id:系统预分配的下个事务ID
  • creator_trx_id:创建该ReadView的事务ID

工作流程

数据可见性判断算法

  • 如果记录DB_TRX_ID < up_limit_id → 可见(事务已提交)
  • 如果DB_TRX_ID ≥ low_limit_id → 不可见(事务在ReadView创建后启动)
  • 如果up_limit_id ≤ DB_TRX_ID < low_limit_id:
    • 检查是否在trx_list中:
      • 存在 → 不可见(事务未提交)
      • 不存在 → 可见(事务已提交)
  • 如果DB_TRX_ID = creator_trx_id → 可见(当前事务自己的修改)

不同操作的处理逻辑

INSERT操作:

新插入行:
- DB_TRX_ID = 当前事务ID
- DB_ROLL_PTR = null
 

DELETE操作:

实际执行逻辑删除:
1. 将行的DB_TRX_ID设为当前事务ID
2. 设置删除标记为1
3. 通过回滚指针维护版本链
 

UPDATE操作:

1. 先对原记录做DELETE标记
2. 插入新记录(DB_TRX_ID=当前事务ID)
3. 通过回滚指针连接新旧版本
 

不同事务级别的表现

隔离级别快照生成时机可见性规则可能问题
READ UNCOMMITTED不使用MVCC总是读取最新数据(包括未提交的)脏读
READ COMMITTED每次SELECT生成新快照只看到已提交的数据不可重复读
REPEATABLE READ第一次SELECT生成快照整个事务期间使用相同快照(InnoDB通过Next-Key Lock解决幻读)
SERIALIZABLE不使用MVCC退化为加锁实现,所有操作串行化性能低下

MVCC的显著优势

  • 并发性能提升

    • 读操作完全不阻塞写操作
    • 写操作只阻塞冲突的写操作
    • 典型OLTP场景读多写少,整体吞吐量显著提高
  • 一致性保证

    • 事务看到的是一致的数据快照
    • 避免脏读、不可重复读问题
    • REPEATABLE READ级别下通过Next-Key Lock解决幻读
  • 系统开销降低

    • 读操作无需获取锁,减少锁争用
    • 减少死锁概率(相比纯锁机制)
    • 回滚操作高效(通过undo日志)

潜在问题

  • 空间放大:需要维护多版本数据,undo日志增长
  • 时间放大:长事务可能导致大量旧版本无法清理
  • 写冲突:并发写入仍需加锁,高并发写入场景仍有瓶颈

优化建议

  • 合理设置事务隔离级别:非必要不使用SERIALIZABLE
  • 控制事务大小:
    • 避免大事务(执行大量DML)
    • 避免长事务(不必要地保持事务开启)
  • 监控undo空间
      SHOW VARIABLES LIKE 'innodb_undo%';
      SELECT * FROM information_schema.INNODB_TRX;
  • 定期维护
    • 监控长事务:SELECT * FROM information_schema.INNODB_TRX;
    • 适当配置innodb_purge_threads(清理线程数)
  • 设计优化:
    • 热点数据考虑垂直拆分
    • 高频更新字段考虑分离到单独表

生产实践

版本链深度监控

-- 检查版本链长度
SELECT COUNT(*) FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;

合理配置参数

[mysqld]
innodb_max_undo_log_size=1G      # 控制undo表空间大小
innodb_purge_threads=4          # 根据CPU核心数设置
transaction-isolation=READ-COMMITTED  # 根据业务需求设置

应用层配合

  • 实现会话超时机制,避免应用层连接泄漏导致长事务
  • 批量操作分批次提交,避免超大事务
  • 读写分离,将报表类查询路由到只读副本

日志系统

  • 日志系统「错误日志(Error Log):记录MySQL启动、运行或停止时的错误信息。
  • 查询日志(General Query Log):记录MySQL服务器接收到的所有客户端连接和SQL查询信息。通常用于分析和审计。
  • 慢查询日志(Slow Query Log):记录执行时间超过指定阈值的SQL查询信息。用于找出需要优化的查询。
  • 二进制日志(Binary Log):记录所有更改数据内容或表结构的SQL语句的信息。主要用于复制和数据恢复。
  • 中继日志(Relay Log):在MySQL复制中,Slave服务器用于保存从Master服务器接收到的二进制日志事件。然后Slave会异步地将这些事件写入其自己的二进制日志(在Slave上为Relay Log)。
  • 重做日志(Redo Log):这是InnoDB存储引擎特有的日志,用于记录事务对数据页的修改。在事务提交时,修改先写入重做日志,然后再异步刷新到磁盘的数据文件中。这保证了事务的持久性和崩溃恢复能力。
  • 撤销日志(Undo Log):也是InnoDB特有的日志,用于保存事务修改前的数据版本。它用于实现MVCC、事务回滚和崩溃恢复等功能。撤销日志在事务提交后可以被清理(但在某些情况下会保留一段时间以支持MVCC)。然而,请注意在MySQL的官方文档中并没有直接提及“撤销日志”这个术语;这里提到的“撤销日志”实际上是指InnoDB的undo tablespace或undo segments中保存的信息。这些信息用于在需要时回滚事务或构建事务的早前版本以供其他事务读取(实现MVCC)。

SQL查询过程

客户端 → 连接器 → 解析器 → 优化器 → 执行器 → (InnoDB: 缓冲池/索引/锁) → 返回结果

客户端请求与连接建立

客户端发送SQL:通过JDBC/ODBC等驱动发送SQL语句(如SELECT * FROM users WHERE id=1)。

连接器处理

  • 验证用户名、密码、主机权限。
  • 分配线程并管理连接(若启用连接池,可能复用现有连接)。
  • 检查wait_timeout等参数,自动断开超时连接。

查询缓存(MySQL 8.0已移除)

历史版本行为:

  • 以SQL语句为Key查询缓存,若命中则直接返回结果。
  • 失效场景:表数据变更(如INSERT/UPDATE)会清空相关缓存。

移除原因:缓存命中率低、维护开销大,建议用外部缓存(如Redis)。

解析与预处理

解析器(Parser):

  • 词法分析:拆分SQL为关键词(如SELECT、FROM)、表名、列名等。
  • 语法分析:检查语法正确性(如括号匹配),生成语法树。

预处理器:

  • 检查表/列是否存在,解析别名。
  • 展开*为所有列名,检查权限。

查询优化(关键阶段)

优化器(Optimizer):

  • 逻辑优化:
    • 简化条件(如1=1 AND id>10 → id>10)。
    • 子查询优化(可能转为JOIN)。
  • 物理优化:
    • 选择索引:基于统计信息(如基数、回表成本)决定是否使用索引。
    • JOIN策略:选择NLJ(嵌套循环)、BNLJ(块嵌套)或Hash Join(8.0+)。
    • 生成执行计划:通过EXPLAIN可查看,如type: ref表示索引扫描。

执行阶段

执行器(Executor):

  • 检查用户对表的SELECT权限(若未在连接层验证)。
  • 调用存储引擎接口,按执行计划逐步操作:
    • 例1:若使用主键索引,执行器调用innodb_read_by_key读取id=1的行。
    • 例2:若全表扫描,则迭代调用innodb_read_first_row/innodb_read_next_row。
  • 处理引擎返回的行数据,过滤不符合条件的行(如服务层WHERE条件)。

存储引擎处理

InnoDB为例

  • 索引查询:
    • 通过B+树定位数据:若id是主键,从聚簇索引中获取行数据。
    • 若使用二级索引,先查索引得主键值,再回表查询(除非覆盖索引)。
  • 事务一致性:根据隔离级别(如RC/RR)使用ReadView或MVCC判断数据可见性。
  • 锁机制:若为SELECT FOR UPDATE,对行加X锁。

结果返回

  • 结果集构建:执行器将最终数据组装成结果集(可能排序/分组)。
  • 返回客户端:通过TCP协议逐步传输结果,客户端逐行获取。

InnoDB 行格式

特性Compact (5.7默认)RedundantDynamic (8.0默认)Compressed
存储效率中等最高(压缩50-70%)
NULL值存储位图(1bit/NULL)固定长度位图位图
变长字段处理前768字节存记录内全部存记录内全部存溢出页压缩后存溢出页
溢出页指针20字节(部分溢出时)20字节20字节
最大行大小≈8000字节≈8000字节≈1TB(理论)≈1TB(压缩后)
CPU开销最低高(zlib压缩)
适用版本MySQL 5.0+仅兼容旧版本MySQL 5.7+MySQL 5.5+
推荐场景常规OLTP遗留系统迁移含TEXT/BLOB的表归档/日志数据
  1. Dynamic vs Compact

    • ✅ 完全消除行溢出导致的页分裂
    • ✅ 支持更大的单行数据(LOB类型)
    • ⚠️ 需要更多溢出页I/O操作
  2. Compressed特性

    • 压缩级别可调(innodb_compression_level)
    • 支持按页压缩(KEY_BLOCK_SIZE)
    • 监控指标:INNODB_CMP/INNODB_CMPMEM
  3. Redundant限制

    • VARCHAR最大65535字节(实际受页限制)
    • 不支持高效的NULL存储
    • 现代MySQL版本已弃用

查看表行格式

SELECT table_name, row_format
FROM information_schema.tables
WHERE table_schema = 'your_db';

空间使用分析

-- 查看表空间分布
SELECT table_name,
       data_length/1024/1024 as data_mb,
       index_length/1024/1024 as index_mb
FROM information_schema.tables
WHERE table_schema = 'your_db';

FLOAT和DOUBLE的区别

  • FLOAT类型数据可以存储至多8位十进制数,并在内存中占4字节。
  • DOUBLE类型数据可以存储至多18位十进制数,并在内存中占8字节。

VARCHAR与TEXT对比

特性VARCHARTEXT
长度限制0-65,535字节(实际约16K)最多65,535字节(实际约16K)
长度声明必须指定长度(如VARCHAR(255))无需也不能指定长度
存储方式行内存储(短文本)或行外存储总是行外存储
存储开销实际字符数+1/2字节长度头实际字符数+2字节长度头
默认值✅ 支持❌ 不支持
索引支持✅ 完整列索引⚠️ 必须前缀索引(1000字节内)
临时表使用❌ 通常不需要✅ 查询时常需创建
排序性能✅ 高效⚠️ 需指定长度(ORDER BY SUBSTRING(text,1,100))
内存处理✅ 内存缓冲区处理⚠️ 常需磁盘临时表
典型应用场景短字符串、标识字段长文本内容、大段描述

VARCHAR(50) 和 VARCHAR(200)

特性VARCHAR(50)VARCHAR(200)说明
最大字符数50200定义的最大字符长度
存储 hello 时的空间占用6 字节6 字节实际内容(5 字节)+ 长度字节(1 字节)
排序时分配的内存按 50 字节分配按 200 字节分配排序时按字段的最大长度分配内存
MEMORY 引擎中的存储固定 50 字节固定 200 字节MEMORY 引擎将 VARCHAR 转换为固定长度的 CHAR
适用场景短字符串(如用户名、密码)长字符串(如描述、评论)根据实际需求选择合适的字段长度

COUNT(*) 与 COUNT(列名)

特性COUNT(*)COUNT(列名)
统计目标统计所有行数统计指定列的非NULL值行数
执行性能⚡️ 最优(自动选择最小索引)⚠️ 依赖列是否有索引
索引利用✅ 优先使用最短的二级索引🔍 仅使用该列的索引(如有)
NULL值处理🔢 包含所有行🚫 自动过滤NULL值
InnoDB优化🏎️ 引擎级优化🛠️ 需逐行检查NULL
MyISAM特性📊 直接读取元数据(无WHERE时)🔍 仍需扫描
推荐指数🌟🌟🌟🌟🌟🌟🌟(仅需统计非NULL值时使用)

推荐 COUNT(*)

  • InnoDB引擎:
    • COUNT(*) 会优先选择最小的二级索引进行统计
    • 如果没有二级索引,则使用聚簇索引(但依然比COUNT(列名)高效)
  • MyISAM引擎:
    • COUNT(*) 直接读取元数据,速度极快(仅限无WHERE条件时)

常见误区澄清 误区:"COUNT(主键)比COUNT(*)快"

  • 事实:在InnoDB中,COUNT(*)会自动优化,性能相当或更好

误区:"COUNT(1)比COUNT(*)高效"

  • 事实:现代MySQL版本中两者性能完全相同

误区:"COUNT(列名)可以避免全表扫描"

  • 事实:只有当该列有索引且NOT NULL时才可能避免

事务

什么是事务?

是一组SQL语句的集合,这些语句要么全部成功执行,要么全部不执行。事务的目的是确保数据库操作的完整性和一致性,即使在并发操作或系统故障的情况下也能保证数据的正确性。

Alice想给Bob转账100元,这个操作需要以下步骤:

  • 从Alice的账户中扣除100元。
  • 向Bob的账户中增加100元。

这两个步骤必须全部成功,否则会导致数据不一致。因此,我们需要将这两个步骤放在一个事务中执行。

-- 开始事务
START TRANSACTION;
 
-- 从Alice的账户中扣除100元
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
 
-- 向Bob的账户中增加100元
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
 
-- 提交事务
COMMIT;
 

事务的四大特性

事务4大特性

原子性、一致性、隔离性、持久性

原⼦性

事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么全部完成,要么全不执行

说明:

  • 如果在执行UPDATE accounts SET balance = balance - 100 WHERE id = 1;时成功,但在执行UPDATE accounts SET balance = balance + 100 WHERE id = 2;时失败(例如,Bob的账户不存在),那么整个事务会回滚。
  • 回滚操作会撤销第一步的操作,恢复Alice的账户余额为1000元。
  • 这样,要么Alice和Bob的账户都成功更新,要么都没有更新,确保了操作的原子性。

一致性

执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的;

  • 在转账操作之前,Alice的余额是1000元,Bob的余额是500元。
  • 转账操作完成后,Alice的余额应该是900元,Bob的余额应该是600元。
  • 如果事务失败并回滚,Alice的余额仍然是1000元,Bob的余额仍然是500元。
  • 无论事务成功还是失败,数据库的状态都保持一致。

隔离性

并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发事务之间数据库是独⽴的;

  • 假设在Alice给Bob转账的同时,Bob也在给Charlie转账100元。
  • 这两个事务是并发执行的,但它们之间不会相互干扰。
  • Alice给Bob转账的事务只会影响Alice和Bob的账户,不会影响Bob给Charlie转账的事务。
  • 数据库通过锁机制(如行锁)来确保每个事务在操作时不会被其他事务干扰。

持久性

⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。

  • 假设在Alice给Bob转账的事务提交后,数据库突然发生故障(如服务器断电)。
  • 当数据库恢复后,Alice的余额仍然是900元,Bob的余额仍然是600元。
  • 事务提交后,数据库通过日志(如重做日志)来确保数据的改变是永久的,即使发生故障也能恢复。

实现保证 MySQL的存储引擎InnoDB使用重做日志保证一致性与持久性,回滚日志保证原子性,使用各种锁来保证隔离性。

事务隔离级别

MySQL中隔离级别的实现原理主要依赖于锁机制和并发控制策略。不同的隔离级别会采用不同的锁类型和锁定范围来确保数据的一致性和并发性。

读未提交

READ UNCOMMITTED

实现原理:

  • 完全不使用锁机制
  • 直接读取内存中最新的数据版本
  • 不检查事务状态,不维护数据快照

特点:

  • 性能最好,几乎没有锁开销
  • 数据一致性最差,可能读到未提交的中间状态
  • 实际生产环境极少使用
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 未提交
 
-- 事务B(READ UNCOMMITTED)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 可能读到事务A未提交的修改

读已提交

READ COMMITTED

实现原理

  • 使用行级锁(Record Lock)保证写操作隔离
  • 基于MVCC(多版本并发控制)实现读操作
  • 每次SELECT都会生成新的ReadView(数据快照)

关键机制

锁机制:

  • 写操作获取行级排他锁(X锁)
  • 读操作不加锁(快照读)

MVCC实现

  • 每行记录包含隐藏字段:DB_TRX_ID(事务ID)、DB_ROLL_PTR(回滚指针)
  • Undo Log存储历史版本,形成版本链
  • 每次查询生成新的ReadView,判断数据可见性

特点

  • 避免了脏读
  • 可能出现不可重复读和幻读
  • Oracle等数据库的默认级别
-- 事务A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 第一次读取,值为1000
 
-- 事务B
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;
 
-- 事务A再次读取
SELECT balance FROM accounts WHERE id = 1;  -- 读到新值900(不可重复读)

可重复读

REPEATABLE READ

实现原理:

  • 行锁 + 间隙锁(Gap Lock)组合成Next-Key Lock
  • MVCC机制下,事务首次读取时生成ReadView并保持到事务结束
  • InnoDB在此级别通过间隙锁避免了幻读

关键机制

  • 锁机制:

    • Next-Key Lock = 行锁 + 间隙锁
    • 锁定范围包括记录本身和索引记录前的间隙
    • 防止其他事务在范围内插入新记录(解决幻读)
  • MVCC增强:

    • 事务首次SELECT时生成ReadView
    • 后所有读操作使用同一ReadView
    • 保证事务内看到一致的数据快照
 
-- 事务A
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 1000;  -- 返回2条记录
 
-- 事务B
START TRANSACTION;
INSERT INTO accounts(id, balance) VALUES(3, 1500);  -- 被间隙锁阻塞
COMMIT;
 
-- 事务A再次查询
SELECT * FROM accounts WHERE balance > 1000;  -- 仍返回2条记录(无幻读)
 

可序列化

SERIALIZABLE

实现原理

  • 所有普通SELECT语句自动转为SELECT ... FOR SHARE(加共享锁)
  • 使用严格的锁机制实现完全串行化执行
  • 读操作获取共享锁(S锁),写操作获取排他锁(X锁)

关键机制

  • 锁转换:

    • 自动为SELECT加共享锁
    • 其他事务可以加共享锁但不能加排他锁
    • 读写操作完全互斥
  • 锁范围:

    • 如果使用索引,锁定符合条件的行
    • 无索引时退化为表锁

特点

  • 最高的隔离级别
  • 性能最差,并发度最低
  • 完全避免所有并发问题
  • 适用于严格要求一致性的金融交易等场景
-- 事务A(SERIALIZABLE隔离级别)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;
-- 实际执行:SELECT balance FROM accounts WHERE id = 1 FOR SHARE;
-- 对id=1的行加S锁(共享锁)
 
-- 事务B(任何隔离级别)
START TRANSACTION;
UPDATE accounts SET balance = 800 WHERE id = 1;
-- 尝试对id=1的行加X锁(排他锁),但S锁与X锁互斥,因此被阻塞
-- 直到事务A提交或超时(innodb_lock_wait_timeout)
特性READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READ (默认)SERIALIZABLE
脏读可能 ✅不可能 ❌不可能 ❌不可能 ❌
不可重复读可能 ✅可能 ✅不可能 ❌不可能 ❌
幻读可能 ✅可能 ✅InnoDB不可能 ❌不可能 ❌
锁机制无锁行锁行锁+间隙锁全表锁/严格间隙锁
MVCC支持不适用每次查询新ReadView事务级ReadView禁用(纯锁实现)
并发性能⭐⭐⭐⭐⭐ (最高)⭐⭐⭐⭐⭐⭐⭐⭐ (最低)
适用场景统计近似值高并发读常规OLTP金融交易
InnoDB实现特点直接读最新数据快照读(语句级)快照读(事务级)所有SELECT变共享锁

如何验证隔离级别行为?

 
-- 1. 查看当前隔离级别
SELECT @@transaction_isolation;
 
-- 2. 设置SERIALIZABLE级别(当前会话)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
 
-- 3. 监控锁等待
SHOW ENGINE INNODB STATUS;  -- 查看LATEST DETECTED DEADLOCK段
 

生产环境建议

  1. 避免过度使用SERIALIZABLE: 除非必须防止所有并发异常,否则优先考虑 REPEATABLE READ + 显式锁(如 SELECT ... FOR UPDATE)。

  2. 优化策略:

    • 缩小事务范围,减少锁持有时间
    • 为查询条件添加索引,避免锁升级为表锁
    • 监控 innodb_lock_wait_timeout(默认50秒)
  3. 替代方案:对于需要强一致性的读操作,可使用:

START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 显式加X锁
COMMIT;

索引

数据结构

存储方式

聚簇索引

非聚簇索引

字段特性

字段数量

联合索引

设计原则

高选择等值 → 范围 → 排序 → 覆盖

  • 等值过滤频繁且选择性高的列放在最左。
  • 范围过滤放在等值列之后,防止范围后面的列失效。
  • 排序 / GROUP BY / DISTINCT 顺序要与索引一致,避免额外排序。
  • 覆盖查询时,把 SELECT 里的列全部放到索引尾部(冗余列)形成覆盖索引。
  • 控制宽度:索引列总长度 ≤ 3072 bytes(InnoDB 限制),过长用前缀索引或哈希。

生效原则

CREATE TABLE t (
  a INT, b INT, c INT, d INT,
  INDEX idx_a_b_c (a, b, c)
);
查询条件是否用到索引用到列原因
WHERE a=1a最左前缀
WHERE a=1 AND b=2a,b连续
WHERE a=1 AND c=3✅(部分)ab 缺失,c 无法使用
WHERE b=2跳过 a
WHERE a=1 AND b>2 AND c=3✅(部分)a,bb 范围后 c 失效
WHERE a=1 AND b IN (2,3) AND c=4✅(部分)a,bIN 也是范围
ORDER BY a,b,ca,b,c顺序完全匹配
ORDER BY b,a顺序不匹配
SELECT a,b,c FROM t WHERE a=1✅(覆盖)a,b,c无需回表

查询场景:

-- 完全匹配规则 --
-- ✅ 完全匹配(使用全部索引)
SELECT * FROM table WHERE a=1 AND b=2 AND c=3;
 
-- ✅ 顺序不影响(优化器自动调整)
SELECT * FROM table WHERE b=2 AND a=1 AND c=3;
 
-- 最左前缀规则 --
-- ✅ 使用a列索引
SELECT * FROM table WHERE a=1;
 
-- ✅ 使用a,b列索引
SELECT * FROM table WHERE a=1 AND b=2;
 
-- ❌ 缺少最左列a(索引失效)
SELECT * FROM table WHERE b=2;
 
-- ❌ 跳过中间列b(只能用到a)
SELECT * FROM table WHERE a=1 AND c=3;
 
-- 范围查询后的列失效 --
-- ✅ 使用a,b列索引(c失效)
SELECT * FROM table WHERE a=1 AND b>2 AND c=3;
 
-- ✅ 使用a,b,c索引(LIKE右匹配不算范围)
SELECT * FROM table WHERE a=1 AND b LIKE 'abc%' AND c=3;
 
-- ❌ 范围查询放前面(只能用到a)
SELECT * FROM table WHERE a>1 AND b=2 AND c=3;
 

排序场景:

-- ORDER BY优化 --
-- ✅ 使用完整索引排序
SELECT * FROM table ORDER BY a, b, c;
 
-- ✅ 使用前缀索引排序
SELECT * FROM table ORDER BY a, b;
 
-- ❌ 非最左前缀排序(filesort)
SELECT * FROM table ORDER BY b, c;
 
 
-- 混合排序方向 --
-- ✅ 方向一致可使用索引
SELECT * FROM table ORDER BY a ASC, b ASC, c ASC;
SELECT * FROM table ORDER BY a DESC, b DESC, c DESC;
 
-- ❌ 方向不一致导致filesort
SELECT * FROM table ORDER BY a ASC, b DESC;

覆盖索引优化

-- ✅ 使用覆盖索引(无需回表)
SELECT a, b FROM table WHERE a=1 AND b=2;
 
-- ❌ 需要回表(查c列但条件不含c)
SELECT a, b, c FROM table WHERE a=1 AND b=2;

失效

-- 函数/运算导致失效
-- ❌ 索引失效
SELECT * FROM table WHERE YEAR(a)=2023;
SELECT * FROM table WHERE b+1=10;
 
-- 类型不匹配
-- ❌ 假设a是字符串类型
SELECT * FROM table WHERE a=123;
 
-- 不等于/IS NOT NULL
-- ❌ 索引可能失效
SELECT * FROM table WHERE a!=1;
SELECT * FROM table WHERE b IS NOT NULL;
 
 

查询优化器如何选索引

  • 成本模型:IO + CPU + 内存
  • 候选索引:所有能用到左前缀的联合索引都会进入代价计算。
  • 回表成本:覆盖索引 IO 成本更低,可能反超单列索引。
  • 统计信息:ANALYZE TABLE 更新 innodb_index_stats,否则可能选错。
  • 索引合并(index merge):多列分别走不同索引再合并,通常比联合索引差,可用 USE INDEX 强制。
EXPLAIN SELECT * FROM t WHERE a=1 AND b>2 ORDER BY c;
-- 优化器可能选 idx_a_b_c 利用 a,b 过滤,但排序需要 filesort
-- 把索引改成 (a,b,c) 或 (a,c,b) 才能避免 filesort,取决于过滤 vs 排序谁更重要

高频误区

误区解释
“把 WHERE 里所有列都建联合索引”顺序错、范围列放中间导致后面列失效。
“联合索引会自动解决 OR 条件”OR 会退化为 index merge 或全表,需改写 UNION。
“ORDER BY 字段只要出现在索引就行”必须紧跟在等值列后、顺序一致且全是 ASC/DESC 同一方向。
“LIKE '%abc' 会走索引”前缀模糊无法使用 B+Tree 有序性,只有 LIKE 'abc%' 可走到前缀。
“低基数列放最左”导致过滤效果差、扫描行多,浪费 IO。

索引优化

失效场景

场景示例(MySQL 8.0)失效原因规避方案
WHERE DATE(create_time) = '2024-07-14'函数/表达式改为范围:create_time >= '2024-07-14' AND < '2024-07-15'
WHERE col = 123 (col 是 varchar)隐式转换显式加 CAST 或改客户端字符集
WHERE a=1 OR b=2 (b 无索引)OR 跨越非索引列改为 UNION ALL 或给 b 建索引
LIKE '%abc'左模糊使用倒序存储 + 右模糊 LIKE REVERSE('%cba')
NOT IN (subquery)优化器放弃索引改写为 LEFT JOIN ... IS NULL
(col1,col2,col3) 但查询 WHERE col2=1违背最左前缀调整列顺序或建 (col2,col1)
SELECT ... WHERE col-1 = 9列参与运算改为 col = 10
IS NULL 在列定义 NOT NULL优化器短路无需处理,逻辑已优化

优化策略

策略原理工具/指标备注
有序主键顺序写入减少页分裂innodb_autoinc_lock_mode=2 + 雪花 ID顺序性 > 安全性
覆盖索引避免回表,减少 IOEXPLAIN Extra: Using index回表次数 = 读取行数
NOT NULL减少 NULL 位图 & 统计信息复杂度INFORMATION_SCHEMA.STATISTICS每 NULL 占 1 byte
前缀索引降低 B+Tree 高度SELECT LEFT(col, n) GROUP BY ... 找最小 nn 需平衡选择性与长度
索引数量维护成本 ∝ 索引数Handler_read_key, Handler_read_next一般 OLTP ≤ 6 个/表

日志

Binlog有哪些格式

Statement模式

记录每一条会修改数据的SQL语句

优点:

  • 日志记录量小,节省磁盘空间和网络IO。
  • 性能较高

缺点:

  • 可能导致主从数据不一致,尤其是涉及非确定性函数(如UUID()、NOW())或存储过程。
  • 需要记录上下文信息以确保SQL语句在从服务器上执行结果一致

结果:

  • Binlog记录了实际执行的SQL语句(INSERT、UPDATE、DELETE)。
  • 每条记录包含时间戳、线程ID、执行时间等元数据。
# at 4
#250708 10:00:00 server id 1  end_log_pos 123 CRC32 0x12345678  Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1688790000/*!*/;
INSERT INTO test VALUES (1, 'Kimi')
/*!*/;
# at 123
#250708 10:00:01 server id 1  end_log_pos 234 CRC32 0x87654321  Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1688790001/*!*/;
UPDATE test SET name = 'Moonshot' WHERE id = 1
/*!*/;
# at 234
#250708 10:00:02 server id 1  end_log_pos 345 CRC32 0x11223344  Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1688790002/*!*/;
DELETE FROM test WHERE id = 1
/*!*/;

ROW

记录每一行数据的变化,而不是SQL语句

优点:

  • 记录详细,可以精确反映数据的变化。
  • 避免了STATEMENT格式中可能出现的数据一致性问题

缺点:

  • 日志量大,尤其是批量操作时。
  • 性能可能受影响

结果: 在ROW格式下,Binlog会记录每一行数据的变化,而不是SQL语句。记录的内容大致如下:

# at 4
#250708 10:00:00 server id 1  end_log_pos 123 CRC32 0x12345678  Table_map: `test`.`test` mapped to number 1
#250708 10:00:00 server id 1  end_log_pos 234 CRC32 0x87654321  Write_rows: table id 1 flags: STMT_END_F
### INSERT INTO `test`.`test`
### SET
###   @1=1
###   @2='Kimi'
# at 234
#250708 10:00:01 server id 1  end_log_pos 345 CRC32 0x11223344  Update_rows: table id 1 flags: STMT_END_F
### UPDATE `test`.`test`
### WHERE
###   @1=1
###   @2='Kimi'
### SET
###   @1=1
###   @2='Moonshot'
# at 345
#250708 10:00:02 server id 1  end_log_pos 456 CRC32 0x44556677  Delete_rows: table id 1 flags: STMT_END_F
### DELETE FROM `test`.`test`
### WHERE
###   @1=1
###   @2='Moonshot'
  • INSERT操作记录了插入的行数据(id=1, name='Kimi')。
  • UPDATE操作记录了更新前后的行数据(id=1, name='Kimi'变为id=1, name='Moonshot')。
  • DELETE操作记录了被删除的行数据(id=1, name='Moonshot')。
  • Binlog中没有记录实际的SQL语句,而是记录了数据行的变化。

MIXED

结合了STATEMENT和ROW两种模式

  • 默认使用STATEMENT格式。
  • 在某些情况下(如涉及非确定性函数、存储过程等)自动切换到ROW格式

优点:

兼顾了STATEMENT的性能和ROW的数据一致性

缺点: 配置和管理相对复杂

结果: 在MIXED格式下,MySQL会根据SQL语句的性质自动选择STATEMENT或ROW格式。假设这些SQL语句都是确定性的,记录的内容可能如下:

# at 4
#250708 10:00:00 server id 1  end_log_pos 123 CRC32 0x12345678  Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1688790000/*!*/;
INSERT INTO test VALUES (1, 'Kimi')
/*!*/;
# at 123
#250708 10:00:01 server id 1  end_log_pos 234 CRC32 0x87654321  Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1688790001/*!*/;
UPDATE test SET name = 'Moonshot' WHERE id = 1
/*!*/;
# at 234
#250708 10:00:02 server id 1  end_log_pos 345 CRC32 0x11223344  Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1688790002/*!*/;
DELETE FROM test WHERE id = 1
/*!*/;

按锁粒度分类

全局锁 (Global Lock)

描述:锁定整个数据库实例

实现:FLUSH TABLES WITH READ LOCK

使用场景:

  • 全库逻辑备份时确保数据一致性
  • 数据库迁移或重大结构变更时防止并发操作

表级锁 (Table Lock)

描述:锁定整张表

类型:

  • 表共享读锁(S锁):LOCK TABLE table_name READ
  • 表独占写锁(X锁):LOCK TABLE table_name WRITE

使用场景:

  • 需要批量操作整张表时
  • 不支持行锁的存储引擎(如MyISAM)的并发控制
  • DDL操作(ALTER TABLE等)时自动加表锁
-- 加表读锁(其他会话可读不可写)
LOCK TABLE users READ;
-- 加表写锁(其他会话不可读写)
LOCK TABLE users WRITE;
-- 解锁
UNLOCK TABLES;

行级锁 (Row Lock)

描述:锁定表中的单行或多行记录

实现:InnoDB引擎支持

使用场景:

  • 高并发环境下对少量数据的精确锁定
  • 事务中对特定记录的修改
-- 共享锁(其他事务可读不可写)
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
 
-- 排他锁(其他事务不可读写)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;

按锁性质分类

共享锁(读锁)

描述:允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁,但其他事务可以并发获得共享锁。主要用于读操作。 语法:SELECT ... LOCK IN SHARE MODE (MySQL 8.0+) 或 SELECT ... FOR SHARE

使用场景:

  • 读取数据时需要确保不被其他事务修改
  • 实现一致性读

排他锁(写锁)

描述:允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。主要用于写操作,如INSERT、UPDATE或DELETE。

语法:SELECT ... FOR UPDATE

使用场景:

  • 更新或删除数据前确保数据不被其他事务修改
  • 实现悲观并发控制

InnoDB特有的锁机制

意向锁 (Intention Lock)

描述:表级锁,表示事务打算在表中的行上获取什么类型的锁

类型:

  • 意向共享锁(IS)
  • 意向排他锁(IX)

作用:提高锁冲突检测效率

BEGIN;
-- 自动在表上加IX锁,在行id=1上加X锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
 
-- 另一个事务尝试加表锁时会检查意向锁
LOCK TABLE users WRITE; -- 会阻塞,因为检测到IX锁存在
 

记录锁 (Record Lock)

描述:锁定索引中的单条记录 使用场景:精确锁定特定行

库存扣减(防止超卖)

-- 事务1:下单扣库存
BEGIN;
-- 锁定商品库存记录
SELECT stock FROM products WHERE product_id = 1001 FOR UPDATE;
-- 检查库存
IF stock > 0 THEN
  UPDATE products SET stock = stock - 1 WHERE product_id = 1001;
END IF;
COMMIT;
 

间隙锁 (Gap Lock)

描述:锁定索引记录之间的间隙,防止其他事务在间隙中插入数据。

特点:

  • 只在REPEATABLE READ隔离级别下有效
  • 可以防止幻读问题
  • 只锁定范围,不锁定记录本身

使用场景:

  • 防止幻读(在REPEATABLE READ隔离级别下)
  • 锁定一个范围但不包括记录本身
-- 假设现有id为1,5,10的记录
BEGIN;
SELECT * FROM products WHERE id BETWEEN 3 AND 7 FOR UPDATE;
-- 这会锁定(1,5)和(5,10)这两个间隙
-- 其他事务不能插入id=4或id=6的记录

临键锁 (Next-Key Lock)

描述:记录锁+间隙锁的组合,锁定记录及其前面的间隙 使用场景:InnoDB在REPEATABLE READ隔离级别下防止幻读的主要手段

插入意向锁 (Insert Intention Lock)

描述:插入操作前设置的一种特殊间隙锁 使用场景:提高并发插入性能

自增锁 (AUTO-INC Lock)

描述:处理自增列的特殊表锁

使用场景:保证自增ID的唯一性和连续性

元数据锁 (Metadata Lock)

描述:保护表结构不被并发修改

元数据锁(Metadata Locks,简称MDL)是MySQL中用于管理对表元数据并发访问的一种锁机制。当一个事务正在对一个表进行结构变更(如ALTER TABLE)或正在访问表的元数据(如查看表的列信息)时,MySQL会使用MDL来确保其他事务不能同时对该表进行结构变更或某些特定的数据操作。

MDL的主要作用是防止多个事务同时修改表的结构或同时访问正在被修改的表结构,从而确保数据的一致性和完整性。例如,当一个事务正在向表中添加新列时,其他事务不能同时删除该列或对该表进行某些可能影响表结构的数据操作。

使用场景:

  • 执行DML时防止DDL操作
  • 执行DDL时防止其他会话访问表

InnoDB

优化

Join的实现

嵌套循环连接 实现原理

  • 最基础的JOIN算法,通过双重循环实现
  • 外层循环遍历驱动表(小表),内层循环遍历被驱动表(大表)
  • 时间复杂度:O(M*N)

适用场景:

  • 被驱动表连接字段有索引
  • 驱动表数据量较小
  • MySQL 8.0前默认算法

块嵌套循环连接

实现原理

  • NLJ的优化版本,减少磁盘I/O
  • 将驱动表数据分块读入join buffer
  • 每块数据批量与被驱动表比较
  • 时间复杂度:O(M*N/B) B为buffer大小

适用场景:

  • 被驱动表无可用索引
  • 内存充足,可配置较大join buffer
  • 小表驱动大表

哈希连接

实现原理:

  • 构建阶段:在内存中为驱动表建立哈希表
  • 探测阶段:扫描被驱动表,计算哈希值查找匹配
  • 时间复杂度:构建O(M)+探测O(N)

适用场景:

  • 等值连接(=)
  • 无索引或索引选择性差
  • 内存足够容纳驱动表哈希表

排序合并连接 实现原理:

  • 对两表按连接键排序
  • 双指针合并扫描已排序数据
  • 时间复杂度:排序O(MlogM + NlogN)+合并O(M+N)

适用场景:

  • 连接条件使用范围比较(>,<等)
  • 数据已按连接键排序
  • 大数据集且内存有限

Join优化

索引优化

  • 为连接字段创建索引:确保ON子句字段有索引
  • 多列索引顺序:区分度高的列在前
  • 覆盖索引:避免回表操作

连接顺序优化

  • 基于统计信息估算成本
  • 小表驱动大表原则
-- 1. 使用STRAIGHT_JOIN强制顺序
SELECT * FROM small_table STRAIGHT_JOIN large_table ON...
 
-- 2. 调整FROM子句顺序(优化器会重排)
SELECT * FROM table1, table2 WHERE...
 
-- 3. 使用优化器提示
SELECT /*+ JOIN_ORDER(t1, t2) */ * FROM t1 JOIN t2...

查询重写技巧

  • 子查询: 改写为JOIN,减少临时表创建
  • 多表JOIN: 分解为多个简单查询,降低复杂度
  • OR条件: 改用UNION ALL,避免全表扫描
  • 派生表: 提前过滤或物化,减少处理数据量
-- 低效: 使用子查询
SELECT * FROM orders WHERE customer_id IN
(SELECT id FROM customers WHERE status='active');
 
-- 高效: 改写为JOIN
SELECT o.* FROM orders o JOIN customers c
ON o.customer_id=c.id WHERE c.status='active';

减少连接操作中的数据量

使用WHERE子句限制连接操作中的数据量,只选择需要的列和行。

使用EXPLAIN分析查询计划

通过EXPLAIN命令查看MySQL如何执行查询,并根据输出结果进行优化调整。

考虑使用缓存

如果某些查询结果经常被重复使用,可以考虑使用查询缓存来提高性能。但需要注意,在高并发和频繁更新的场景下,查询缓存可能会成为性能瓶颈。

分布式查询和分片

对于超大规模的数据集,可以考虑使用分布式查询和分片技术将数据分散到多个节点上进行处理

优化长难查询语句

切分查询

将一个大的查询分解为多个小的查询,尤其是在处理大量数据时。

示例: 假设需要删除 1000 万条数据,一次性删除会导致服务器开销过大,甚至可能引发锁表问题。

-- 分批删除
SET @batch_size = 10000;
SET @offset = 0;
 
WHILE (1) DO
  DELETE FROM your_table
  LIMIT @batch_size OFFSET @offset;
 
  SET @offset = @offset + @batch_size;
 
  -- 暂停一段时间,避免对服务器造成过大压力
  DO SLEEP(1);
END WHILE;

分解关联查询

将复杂的关联查询分解为多个简单的查询,可以提高缓存效率。

示例: 假设有一个复杂的关联查询:

SELECT a.*, b.*
FROM table_a a
JOIN table_b b ON a.id = b.a_id
WHERE a.status = 'active';

优化:

-- 先查询 table_a 中符合条件的记录
SELECT id, name
FROM table_a
WHERE status = 'active';
 
-- 然后根据查询结果逐条查询 table_b
SELECT *
FROM table_b
WHERE a_id IN (SELECT id FROM table_a WHERE status = 'active');

减少冗余记录的查询

避免在查询中返回不必要的列和行,减少数据传输量。

-- 原查询
SELECT * FROM your_table WHERE status = 'active';
 
-- 优化后的查询
SELECT id, name, status
FROM your_table
WHERE status = 'active';

在应用层做关联

在应用层进行数据关联,可以更容易地对数据库进行拆分,提高系统的可扩展性。

示例: 假设需要从两个表中获取数据并进行关联。

-- 查询表 A
SELECT id, name
FROM table_a
WHERE status = 'active';
 
-- 查询表 B
SELECT id, a_id, value
FROM table_b
WHERE a_id IN (SELECT id FROM table_a WHERE status = 'active');
 

执行单个查询减少锁的竞争

尽量减少单次查询的资源消耗,避免长时间锁定资源。

-- 原查询
SELECT * FROM your_table FOR UPDATE;
 
-- 优化后的查询
SELECT id, name
FROM your_table
WHERE status = 'active' FOR UPDATE;

优化LIMIT分页

ID游标法

-- 第一页(常规查询)
SELECT * FROM orders ORDER BY id LIMIT 20;
 
-- 后续分页(记住上次最大ID)
SELECT * FROM orders
WHERE id > 上一页最后一条记录的ID
ORDER BY id
LIMIT 20;

复合键游标法

-- 按时间+ID排序的分页
SELECT * FROM posts
WHERE (created_at, id) > ('2023-01-01 12:00:00', 1000)
ORDER BY created_at, id
LIMIT 20;

优化子查询

用关联查询替代子查询

子查询在某些情况下效率较低,尤其是当子查询返回大量数据时。可以尝试用关联查询替代子查询。

-- 原子查询
SELECT a.*
FROM table_a a
WHERE a.id IN (SELECT b.a_id FROM table_b b WHERE b.status = 'active');
 
-- 优化后的关联查询
SELECT a.*
FROM table_a a
JOIN table_b b ON a.id = b.a_id
WHERE b.status = 'active';

优化 GROUP BY 和 DISTINCT

通过索引来优化,这是最有效的优化方法之一。

-- 原查询
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id;
 
-- 索引优化
CREATE INDEX idx_user_id ON orders(user_id);
 
-- 查询优化
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id;

标识列分组原理

为什么标识列分组更高效

  • 索引利用率:主键/唯一键必然有索引,分组时可直接利用
  • 比较效率:整型主键比较速度比字符串快3-5倍
  • 内存消耗:更小的分组键减少临时表内存使用

基础优化:使用主键替代其他列分组

-- 原查询(低效)
SELECT department_name, COUNT(*)
FROM employees
GROUP BY department_name;
 
-- 优化后(高效)
SELECT d.department_name, COUNT(*)
FROM employees e
JOIN departments d ON e.department_id = d.department_id
GROUP BY e.department_id;  -- 使用关联键分组

多表关联时的分组优化

-- 原查询(使用非索引列分组)
SELECT c.country_name, COUNT(*)
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
GROUP BY c.country_name;
 
-- 优化方案1:使用客户ID分组
SELECT c.country_name, COUNT(*)
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
GROUP BY o.customer_id;  -- 订单表已有customer_id索引
 
-- 优化方案2:使用覆盖索引
ALTER TABLE customers ADD INDEX idx_country (country_name, customer_id);
 
SELECT c.country_name, COUNT(*)
FROM customers c
WHERE customer_id IN (SELECT customer_id FROM orders)
GROUP BY c.country_name;  -- 现在country_name在索引中

避免不必要的 ORDER BY

如果不需要 ORDER BY,在 GROUP BY 时加上 ORDER BY NULL,MySQL 不会再进行文件排序。

-- 原查询
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id
ORDER BY user_id;
 
-- 优化后的查询
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id
ORDER BY NULL;

避免WITH ROLLUP

WITH ROLLUP 会增加额外的聚合计算,可以将其逻辑挪到应用程序中处理,减少数据库的负担。

-- 原查询
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id WITH ROLLUP;
 
-- 优化后的查询
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id;
 
-- 在应用程序中处理超级聚合

WHERE子句

检查清单

  • 是否避免全表扫描?
  • 索引使用是否最优?
  • WHERE条件是否可索引?
  • 是否有不必要的数据类型转换?
  • 是否避免使用导致索引失效的操作?

索引使用优化

索引失效的常见场景

问题类型错误示例优化方案
NULL判断WHERE col IS NULL设默认值,改用WHERE col=0
不等操作WHERE col != 10改写为WHERE col > 10 OR col < 10
OR条件WHERE col=1 OR col=2改用UNION ALL合并查询结果
前导模糊WHERE col LIKE '%abc'使用后缀匹配WHERE col LIKE 'abc%'
隐式转换WHERE str_col=123保持类型一致WHERE str_col='123'
函数操作WHERE YEAR(date)=2023改用范围WHERE date BETWEEN '2023-01-01' AND '2023-12-31'

复合索引最佳实践

-- 创建最左匹配索引
ALTER TABLE users ADD INDEX idx_name_age (last_name, age);
 
-- 有效查询
SELECT * FROM users
WHERE last_name='Smith' AND age>30;  -- 使用索引
 
-- 无效查询
SELECT * FROM users WHERE age>30;    -- 不使用索引

条件表达式优化

范围查询优化

-- 低效写法
SELECT * FROM orders
WHERE total_amount/100 > 50;
 
-- 高效写法
SELECT * FROM orders
WHERE total_amount > 50*100;

IN与BETWEEN选择

-- 离散值用IN
SELECT * FROM products
WHERE category_id IN (1, 3, 5);
 
-- 连续范围用BETWEEN
SELECT * FROM orders
WHERE order_date BETWEEN '2023-01-01' AND '2023-01-31';

OR条件优化方案

-- 低效OR
SELECT * FROM logs
WHERE type='error' OR priority=1;
 
-- 优化方案1:UNION ALL
SELECT * FROM logs WHERE type='error'
UNION ALL
SELECT * FROM logs WHERE priority=1;
 
-- 优化方案2:改写为IN
SELECT * FROM logs
WHERE type='error' OR (type='warning' AND priority=1);

参数化查询优化

预处理语句优势

-- 低效(可能不使用索引)
PREPARE stmt FROM 'SELECT * FROM users WHERE age > ?';
SET @age = 30;
EXECUTE stmt USING @age;
 
-- 高效(强制索引)
PREPARE stmt FROM 'SELECT * FROM users FORCE INDEX(idx_age) WHERE age > ?';

存储过程优化

DELIMITER //
CREATE PROCEDURE GetUsers(IN minAge INT)
BEGIN
    -- 使用提示强制索引
    SELECT * FROM users USE INDEX(idx_age)
    WHERE age > minAge;
END //
DELIMITER ;

核心原则

  • 索引优先:WHERE 和 ORDER BY 的列必须建索引
  • 左侧纯净:WHERE 条件左侧不要有计算/函数
  • 避免全扫:杜绝导致索引失效的写法

死锁

死锁四要素

  • 互斥条件:资源一次只能被一个事务占有
  • 请求与保持:事务持有资源的同时请求新资源
  • 不剥夺条件:已分配资源不能被强制收回
  • 循环等待:事务间形成环形等待链
  • 当两个或多个事务互相持有并请求对方持有的锁时,就会发生死锁。
-- 事务1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 获取id=1的X锁
 
-- 同时事务2
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 获取id=2的X锁
 
-- 事务1尝试获取id=2的锁
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待事务2释放锁
 
-- 事务2尝试获取id=1的锁
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 等待事务1释放锁
 
-- 此时MySQL检测到死锁,会回滚其中一个事务
 

预防死锁

索引优化策略

  • WHERE无索引:为条件列添加索引; 行锁→索引锁,减少锁范围
  • 索引区分度低:使用复合索引或函数索引; 提高过滤效率
  • 全表扫描:强制索引(FORCE INDEX); 避免意外表锁
-- 低效(可能锁全表)
UPDATE orders SET status = 'shipped' WHERE customer_id = 100;
 
-- 优化后
ALTER TABLE orders ADD INDEX idx_customer (customer_id);
UPDATE orders USE INDEX(idx_customer) SET status = 'shipped' WHERE customer_id = 100;
 

事务设计规范

短事务原则

// 错误示范
@Transactional
public void processOrder() {
    // 复杂业务逻辑
    // 远程API调用
    // 文件操作
}
 
// 正确做法
public void businessProcess() {
    // 非事务操作
    remoteService.call();
 
    // 仅数据库操作在事务内
    transactionalService.updateData();
}
 

统一访问顺序

# 所有事务按固定顺序操作表
def transfer_funds(sender, receiver):
    lock_order = sorted([sender, receiver])
    with transaction.atomic():
        withdraw(lock_order[0])
        deposit(lock_order[1])

锁粒度控制技巧

悲观锁精准化

-- 粗粒度锁(不推荐)
SELECT * FROM products WHERE category = 'electronics' FOR UPDATE;
 
-- 细粒度锁(推荐)
SELECT * FROM products WHERE sku = 'ABC123' FOR UPDATE;

乐观锁替代方案

UPDATE inventory
SET stock = stock - 1,
    version = version + 1
WHERE item_id = 100 AND version = 5;

死锁场景

循环等待锁

特征:事务A等待事务B持有的锁,同时事务B等待事务A持有的锁

解决方案:

  • 统一资源访问顺序
  • 使用SELECT ... FOR UPDATE一次性锁定所有需要的资源

间隙锁冲突

特征:涉及范围查询和INSERT操作

-- 将事务隔离级别降为READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
 
-- 或精确化查询条件避免间隙锁
UPDATE orders SET status = 'shipped' WHERE id IN (1, 2, 3);

外键约束死锁

特征:涉及父子表关联操作

-- 按固定顺序操作(先父表后子表)
START TRANSACTION;
UPDATE parent_table ...;
UPDATE child_table ...;
COMMIT;
 

死锁案例

配置

# my.cnf配置
[mysqld]
innodb_print_all_deadlocks = ON      # 记录所有死锁到错误日志
innodb_lock_wait_timeout = 30        # 锁等待超时(秒)

死锁日志分析

-- 查看最近死锁
SHOW ENGINE INNODB STATUS\G
/* 重点观察
LATEST DETECTED DEADLOCK
TRANSACTION TRX_ID
WAITING FOR THIS LOCK
*/
  • WAITING FOR THIS LOCK: 显示被阻塞事务想要的锁
  • HOLDS THE LOCK: 显示阻塞事务已持有的锁
  • ROLLING BACK: 显示被选为牺牲品的事务

解读日志

重点关注输出中的LATEST DETECTED DEADLOCK部分

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-08-20 14:25:36 0x7f8e3c2e6700
*** (1) TRANSACTION:
TRANSACTION 42156, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 32, OS thread handle 139887, query id 1234 192.168.1.1 root updating
UPDATE accounts SET balance = balance - 100 WHERE id = 1
 
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 45 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 42156 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 
*** (1) WAITING FOR THIS LOCK:
RECORD LOCKS space id 45 page no 4 n bits 72 index PRIMARY of table `test`.`accounts` trx id 42156 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 
*** (2) TRANSACTION:
TRANSACTION 42157, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 33, OS thread handle 139888, query id 1235 192.168.1.1 root updating
UPDATE accounts SET balance = balance + 100 WHERE id = 2
 
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 45 page no 4 n bits 72 index PRIMARY of table `test`.`accounts` trx id 42157 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 
*** (2) WAITING FOR THIS LOCK:
RECORD LOCKS space id 45 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 42157 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 
*** WE ROLL BACK TRANSACTION (2)
信息项事务1事务2
事务ID4215642157
持有锁id=1的X锁id=2的X锁
等待锁id=2的X锁id=1的X锁
执行的SQLUPDATE accounts...id=1UPDATE accounts...id=2
锁模式lock_mode X (排他锁)lock_mode X (排他锁)
锁范围locks rec but not gap (仅记录锁)locks rec but not gap
等待时间ACTIVE 12 secACTIVE 10 sec
线程信息thread id 32thread id 33
客户端来源192.168.1.1 root192.168.1.1 root
最终结果成功执行WE ROLL BACK TRANSACTION (2)

死锁解决

短期应急处理

-- 1. 终止阻塞的事务(需SUPER权限)
KILL 33;
 
-- 2. 调整锁等待超时(临时生效)
SET SESSION innodb_lock_wait_timeout = 30;

应用层优化

// 统一资源访问顺序示例
public void transferMoney(int fromId, int toId, BigDecimal amount) {
    // 固定按ID排序获取锁
    int firstId = Math.min(fromId, toId);
    int secondId = Math.max(fromId, toId);
 
    synchronized(getLockObject(firstId)) {
        synchronized(getLockObject(secondId)) {
            // 执行转账操作
        }
    }
}

如何避免死锁:

  • 务必使用索引:确保 WHERE 条件使用索引列,否则会锁全表
  • 保持事务短小精悍,尽快提交
  • 多个事务按相同顺序访问表和数据行
  • 在事务中一次性锁定所有需要的资源
  • 为查询创建合适的索引,减少锁定的范围
  • 设置合理的锁等待超时时间(innodb_lock_wait_timeout)

https://mp.weixin.qq.com/s/aOOa6noApIvBMCRzqUPmvQ

https://mp.weixin.qq.com/s/90_MfmQCjYnL8VfJpDQ6uA

https://mp.weixin.qq.com/s/YPj5zBew1rRBG4J_oY0lMQ

https://mp.weixin.qq.com/s/od5k5Weqnglo9kue4nUBJQ

高级查询技巧

找出连续的数字

表结构:

CREATE TABLE Logs(
    id  INT PRIMARY KEY AUTO_INCREMENT,
    num INT NOT NULL
);

数据 :

idnum
11
21
31
42
51
62
72
82

三表自连接(最朴素)

SELECT DISTINCT l1.num AS ConsecutiveNums
FROM Logs l1
JOIN Logs l2 ON l1.id = l2.id + 1 AND l1.num = l2.num
JOIN Logs l3 ON l1.id = l3.id + 2 AND l1.num = l3.num;
 
  • 原理:把 Logs 视为三个副本,错位 1、2 行进行等值连接。
  • 缺点:
    • 只能硬编码 3 次,扩展到 k 次需再 JOIN k-1 次,SQL 爆炸;
    • 大表性能差(三次全表扫描 + 连接)。
  • 索引利用:id 是主键,可用 range 或 index 扫描,但仍需回表 3 次。

窗口函数(最优雅)

WITH cte AS (
    SELECT
        num,
        LEAD(num,1) OVER (ORDER BY id) AS n1,
        LEAD(num,2) OVER (ORDER BY id) AS n2
    FROM Logs
)
SELECT DISTINCT num AS ConsecutiveNums
FROM cte
WHERE num = n1 AND num = n2;
 
  • 原理:利用 LEAD 把当前行与后两行投影到同一行,然后过滤。
  • 扩展 k 次:把 2 改为 k-1,并用 COALESCE 处理尾部 NULL。
  • 复杂度:一次排序 O(n log n),内存临时表 O(n)。
  • 版本要求:MySQL 8.0+(支持窗口函数)。

用户变量

SELECT DISTINCT num AS ConsecutiveNums
FROM (
    SELECT
        num,
        @cnt   := IF(@prev = num, @cnt + 1, 1) AS cnt,
        @prev  := num
    FROM Logs, (SELECT @cnt := 0, @prev := NULL) init
    ORDER BY id
) AS t
WHERE cnt >= 3;
 
  • 执行过程:
    • 初始化 @cnt=0、@prev=NULL;
    • 按 id 顺序扫描;
    • 若当前 num = @prev,@cnt++;否则重置为 1;
    • 子查询完成后过滤 cnt ≥ 3。
  • 优点:
    • 单表顺序扫描,无额外 JOIN;
    • 任意 k 次只需改 WHERE cnt >= k;
  • 注意:MySQL 8.0 默认开启 derived_merge=on,可能导致变量失效,需关闭或加 NO_MERGE hint。

递归 CTE(MySQL 8.0)

WITH RECURSIVE r AS (
    -- 锚点:从第 1 行开始
    SELECT id, num, 1 AS cnt
    FROM Logs
    WHERE id = 1
 
    UNION ALL
 
    -- 递归:下一行
    SELECT l.id, l.num,
           CASE WHEN l.num = r.num THEN cnt + 1 ELSE 1 END
    FROM Logs l
    JOIN r ON l.id = r.id + 1
)
SELECT DISTINCT num AS ConsecutiveNums
FROM r
WHERE cnt >= 3;
 
  • 逻辑:
    • 锚点取首行;
    • 每次递归把下一行接上来,并累积连续计数;
    • 最终过滤。
  • 优点:
    • 纯 SQL 实现,无需变量;
    • 支持任意 k 次;
  • 缺点:递归深度受 cte_max_recursion_depth 限制(默认 1000)。

电商场景

MySql场景面试题:sql统计分析(1)

满意度调查分组去除最高最低求平均分

电商业务中高净值用户行为分析

电商业务中用户的复购行为分析

电商业务中用户的行为路径和转化率分析

电商业务中用户的分层行为和生命周期价值(LTV)分析

Product

主从复制

12.两台 mysql 服务器,其中一台挂了,怎么让业务端无感切换,并保证正常情况下讲台服务器的数据是一致的?不是核心业务的话,先停写,把备机拉起来,查看两台机器的日志,进行数据补偿,开写。 如果是核心业务的话,现在所有的写操作都在正常的状态机器上。把好的这台机器的备机拉起来,当主机。

以上全是应急操作。实际上数据库的容灾设计要复杂的多。面试官要是问你,备机的数据不一致怎么办,你要勇敢怼回去,你们每秒多少写入操作。

按照百万级表,每秒 1000 的写入效率,正常的设计是,分布在 2 台机器上每台 500。这个级别的数据同步,出现差异的概率 可以忽略不计的。 有一台出现问题,另一台也可以抗住。正常的操作,还是先停写,等数据一致,切换,开写。搞这些切换都是在凌晨 4.00 左右,核心业务的每秒写操作,只有十几个。前后耽搁不到 20 秒。

数据迁移

MySQL千亿级数据线上平滑扩容实战

慢SQL

记一次线上慢SQL调优实战!

Accident

系统堵塞

MySQL生产堵塞严重,定位堵塞源头

https://mp.weixin.qq.com/s/NBnNPlfIpTCX2gHMIOP1tw

https://mp.weixin.qq.com/s/DFmQP0cpFSSv49zgXtLTLg

https://mp.weixin.qq.com/s/IgcSUxC9nLN67PNiZgx3kw

https://mp.weixin.qq.com/s/MchCUZ13D1A6vB-gcHdunQ

https://mp.weixin.qq.com/s/dyZhVMKMxnkf47wPcM-UaQ

https://mp.weixin.qq.com/s/4oK8S6HODm70VVJnEs__EQ

https://mp.weixin.qq.com/s/e-e-FnL5S_8WgCWbnwG4CQ

https://mp.weixin.qq.com/s/g03GNjgGuoVytSlFFFrBbA

https://mp.weixin.qq.com/s/wJXWSK2GkrfZ0QuaWZJoug

https://mp.weixin.qq.com/s/Em7ozJSW0e7LgnxW8OZ10A

https://mp.weixin.qq.com/s/fZ6mWO97bJQHMsARKMl9Ng

https://mp.weixin.qq.com/s/st44xYZu0OiBNA3tTuZ5kQ

← Previous postPHP夯实基础
Next post →Redis夯实基础