SQL原理
SELECT执行过程
一条SELECT语句的执行需要经过多个阶段,MySQL的查询优化器会尽可能高效地完成数据检索。
flowchart TD
A[SQL语句] --> B[连接器]
B --> C[查询缓存]
C --> D[解析器]
D --> E[预处理器]
E --> F[查询优化器]
F --> G[执行器]
G --> H[存储引擎]
H --> I[返回结果]
C -->|未命中| D
F --> J{使用索引?}
J -->|是| K[索引扫描]
J -->|否| L[全表扫描]
K --> M[回表获取数据]
L --> M
M --> G
style A fill:#e3f2fd,stroke:#1565c0
style F fill:#fff3e0,stroke:#e65100
style H fill:#e8f5e9,stroke:#2e7d32
style I fill:#c8e6c9,stroke:#2e7d32
SELECT执行流程详解
| 步骤 | 组件 | 主要功能 |
|---|---|---|
| 1 | 连接器 | 建立连接、验证身份、维护会话 |
| 2 | 解析器 | 词法分析、语法分析、构建AST |
| 3 | 预处理器 | 语义检查、权限检查、解析别名 |
| 4 | 优化器 | 生成执行计划、选择索引、成本估算 |
| 5 | 执行器 | 调用存储引擎、过滤数据、返回结果 |
| 6 | 存储引擎 | 读取数据页、返回行数据 |
执行计划分析
| 字段 | 说明 | 重要取值 |
|---|---|---|
| id | 查询编号 | 子查询时不同 |
| select_type | 查询类型 | SIMPLE/PRIMARY/SUBQUERY |
| type | 连接类型 | const > eq_ref > ref > range > index > ALL |
| possible_keys | 可用的索引 | 可能使用的索引列表 |
| key | 实际使用的索引 | 选中的索引名 |
| rows | 预计扫描行数 | 越少越好 |
| Extra | 额外信息 | Using index/Using filesort/Using temporary |
-- 使用 EXPLAIN 分析查询执行计划
EXPLAIN SELECT u.name, o.order_id, o.total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.age > 25 AND o.status = 'completed';
-- 更详细的分析(包含执行时间)
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 25;
联表执行过程
联表查询(JOIN)是SQL中最复杂也最重要的操作之一。理解联表的执行原理有助于编写高效的查询。
JOIN 类型对比
| 类型 | 说明 | 图示 | 适用场景 |
|---|---|---|---|
| INNER JOIN | 内连接 | A ∩ B | 获取两表交集数据 |
| LEFT JOIN | 左连接 | A | 保留左表所有数据 |
| RIGHT JOIN | 右连接 | B | 保留右表所有数据 |
| FULL OUTER JOIN | 全外连接 | A ∪ B | 获取两表全部数据 |
| CROSS JOIN | 交叉连接 | A × B | 笛卡尔积、生成测试数据 |
联表算法
| 算法 | 特点 | 适用场景 |
|---|---|---|
| 嵌套循环 | 外层逐行扫描,内层索引查找 | 小表驱动大表、有索引 |
| 块嵌套循环 | 多行加载到buffer,无索引时用 | 无索引连接,效率较低 |
| 索引嵌套循环 | 使用内层索引 | 高效,有索引的情况 |
| 哈希连接 | 小表加载到哈希表(MySQL 8.0+) | 等值连接、大数据量 |
| 合并连接 | 两表排序后合并 | 有序数据 |
联表执行流程图
flowchart TD
A[SELECT语句] --> B[优化器分析]
B --> C{选择驱动表?}
C -->|小表驱动大表| D[确定连接顺序]
C -->|大表驱动小表| D
D --> E[选择连接算法]
E --> F{有索引?}
F -->|是| G[Index Nested Loop]
F -->|否| H{数据量小?}
H -->|是| I[Block Nested Loop]
H -->|否| J[Hash Join]
G --> K[执行连接]
I --> K
J --> K
K --> L[生成结果集]
L --> M[返回客户端]
style B fill:#fff3e0,stroke:#e65100
style D fill:#e1f5fe,stroke:#01579b
style G fill:#c8e6c9,stroke:#2e7d32
style J fill:#fce4ec,stroke:#c2185b
连接顺序优化
-- 联表查询示例:users -> orders -> order_items -> products
-- 假设:users表100行,orders表10000行,order_items表50000行,products表1000行
-- 优化原则:小表驱动大表,有索引的表放在内层
-- ❌ 错误的连接顺序(大表驱动小表)
SELECT *
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id;
-- ✅ 正确的连接顺序(小表驱动大表)
SELECT *
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id;
-- 查看优化器选择的执行计划
EXPLAIN SELECT *
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id;
-- 强制使用小表作为驱动表
SELECT STRAIGHT_JOIN *
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id;
联接条件详解
-- 不同的JOIN类型执行过程:
-- 1. INNER JOIN(内连接)
-- 只返回两个表中满足连接条件的记录
SELECT u.name, o.order_id
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- 执行过程:遍历驱动表,查找匹配行,组合返回
-- 2. LEFT JOIN(左外连接)
-- 返回左表所有记录,右表无匹配则显示NULL
SELECT u.name, o.order_id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- 执行过程:遍历左表每行,查找右表匹配,无匹配则NULL填充
-- 3. RIGHT JOIN(右外连接)
-- 返回右表所有记录,左表无匹配显示NULL
SELECT u.name, o.order_id
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- 4. CROSS JOIN(笛卡尔积)
-- 返回两表所有记录的组合
SELECT u.name, o.order_id
FROM users u
CROSS JOIN orders o;
-- 执行过程:左表每行 × 右表每行 = 结果集
-- 5. 自连接
-- 表与自身连接,用于层级数据
SELECT a.name AS employee, b.name AS manager
FROM employees a
LEFT JOIN employees b ON a.manager_id = b.id;
联表性能优化
-- 联表查询性能优化技巧:
-- 1. 确保连接列有索引
-- 索引加速连接条件的匹配
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_order_id ON order_items(order_id);
CREATE INDEX idx_product_id ON order_items(product_id);
-- 2. 使用覆盖索引
-- 减少回表操作
CREATE INDEX idx_covering ON orders(user_id, status, total_amount);
SELECT user_id, status, total_amount
FROM orders WHERE user_id = 1;
-- 3. 减少连接数量
-- 过多的表连接会显著降低性能
-- 将不需要的表移除,或使用子查询替代
-- 4. 合理使用连接类型
-- 优先使用INNER JOIN,避免笛卡尔积
SELECT u.name, o.order_id
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed';
-- 5. 添加过滤条件
-- 先过滤减少数据量,再连接
SELECT u.name, o.order_id
FROM users u
INNER JOIN (
SELECT * FROM orders WHERE status = 'completed'
) o ON u.id = o.user_id;
-- 6. 使用EXPLAIN分析
EXPLAIN FORMAT=JSON
SELECT u.name, o.order_id
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed';
INSERT执行过程
INSERT语句用于向表中插入数据,其执行过程涉及多个步骤以确保数据的完整性和持久性。
3.1 INSERT执行流程
flowchart TD
A[INSERT语句] --> B[解析器解析]
B --> C[检查表结构和权限]
C --> D[获取表锁/MDL锁]
D --> E[准备数据页]
E --> F[写入UNDO日志]
F --> G[修改数据页]
G --> H[写入REDO日志]
H --> I[准备Binlog]
I --> J[提交事务]
J --> K[返回结果]
E -->|唯一索引?| L[检查唯一约束]
L -->|冲突| M[报错或更新]
M --> G
style A fill:#e3f2fd,stroke:#1565c0
style F fill:#fff3e0,stroke:#e65100
style G fill:#ffebee,stroke:#c62828
style H fill:#e8f5e9,stroke:#2e7d32
style J fill:#c8e6c9,stroke:#2e7d32
INSERT执行步骤
-- INSERT语句执行详解:
-- 示例表结构
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 执行过程:
-- 1. 解析SQL,验证语法和表结构
-- 2. 检查用户权限
-- 3. 获取MDL锁(Metadata Lock)保护表结构
-- 4. 开启BUF_PAGE,定位插入位置
-- 5. 检查唯一索引约束(记录到undo log)
-- 6. 修改数据页,插入新记录
-- 7. 写入redo log(物理日志,确保持久性)
-- 8. 写入binlog(逻辑日志,用于复制)
-- 9. 提交事务(两阶段提交)
-- 10. 释放锁,返回自增ID
-- 普通INSERT
INSERT INTO users (name, email, age) VALUES ('张三', 'zhangsan@example.com', 25);
-- 批量INSERT
INSERT INTO users (name, email, age) VALUES
('李四', 'lisi@example.com', 30),
('王五', 'wangwu@example.com', 28);
-- INSERT...SELECT
INSERT INTO users (name, email, age)
SELECT name, email, age FROM old_users;
-- INSERT...ON DUPLICATE KEY UPDATEUPS(ERT)
INSERT INTO users (name, email, age) VALUES ('张三', 'zhangsan@example.com', 30)
ON DUPLICATE KEY UPDATE age = 30;
-- REPLACE(存在则删除后插入)
REPLACE INTO users (id, name, email, age) VALUES (1, '张三', 'zhangsan@example.com', 28);
事务日志机制
-- InnoDB事务日志机制:
-- 1. UNDO日志(回滚日志)
-- 记录修改前的数据,用于回滚
-- INSERT: 记录插入的主键
-- UPDATE: 记录修改前的数据
-- DELETE: 记录删除前的完整数据
-- 2. REDO日志(重做日志)
-- 记录修改后的数据,用于崩溃恢复
-- 循环写入,先于数据页刷新到磁盘
-- innodb_log_file_size 控制大小
-- innodb_log_buffer_size 控制缓冲区大小
-- 3. BINLOG(二进制日志)
-- 记录SQL语句,用于主从复制
-- statement: 记录SQL语句
-- row: 记录行数据变化
-- mixed: 混合模式
-- 查看事务相关配置
SHOW VARIABLES LIKE 'innodb_log%';
SHOW VARIABLES LIKE 'binlog_format';
-- 两阶段提交(保证数据一致性)
-- 1. .prepare阶段:写redo log,标记为prepare
-- 2. 写入binlog
-- 3. .commit阶段:标记为commit,完成提交
UPDATE执行过程
UPDATE语句用于修改表中已存在的记录,其执行过程比INSERT更复杂,因为需要先定位数据再进行修改。
UPDATE执行流程
flowchart TD
A[UPDATE语句] --> B[解析器解析]
B --> C[检查表结构和权限]
C --> D[查询数据]
D --> E{找到记录?}
E -->|否| F[返回0行更新]
E -->|是| G[获取排他锁]
G --> H[写入UNDO日志]
H --> I[修改数据页]
I --> J[写入REDO日志]
J --> K[写入Binlog]
K --> L[提交事务]
L --> M[返回影响行数]
D -->|涉及索引| N[更新索引]
N --> G
style A fill:#e3f2fd,stroke:#1565c0
style H fill:#fff3e0,stroke:#e65100
style I fill:#ffebee,stroke:#c62828
style L fill:#c8e6c9,stroke:#2e7d32
UPDATE详细过程
-- UPDATE语句执行详解:
-- 示例
UPDATE users SET age = 30, name = '张三' WHERE id = 1;
-- 执行过程:
-- 1. 解析SQL,确定要修改的列和条件
-- 2. 根据WHERE条件定位要修改的行
-- 3. 对要修改的行加排他锁(X锁)
-- 4. 记录旧值到UNDO日志(用于回滚)
-- 5. 修改数据页中的数据
-- 6. 如果有索引,更新索引
-- 7. 写入REDO日志
-- 8. 写入BINLOG
-- 9. 提交事务
-- 10. 返回影响行数
-- 简单UPDATE
UPDATE users SET age = 30 WHERE id = 1;
-- 条件UPDATE
UPDATE users SET age = age + 1 WHERE age < 30;
-- 关联UPDATE
UPDATE users u
INNER JOIN orders o ON u.id = o.user_id
SET u.total_orders = u.total_orders + 1,
o.updated_at = NOW()
WHERE o.status = 'completed';
-- ORDER BY + LIMIT UPDATE
UPDATE users SET status = 'inactive'
WHERE last_login < '2023-01-01'
ORDER BY last_login ASC LIMIT 100;
行锁与死锁
-- UPDATE语句的锁机制:
-- 1. 行锁(Record Lock)
-- 锁定满足条件的行
UPDATE users SET age = 30 WHERE id = 1;
-- 只锁定id=1这一行
-- 2. 间隙锁(Gap Lock)
-- 锁定记录之间的间隙,防止幻读
UPDATE users SET age = 30 WHERE age > 20 AND age < 30;
-- 锁定(20,30)这个间隙
-- 3. 临键锁(Next-Key Lock)
-- 行锁+间隙锁的组合
-- 查看当前锁
SELECT * FROM information_schema.INNODB_TRX;
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 死锁示例
-- 会话1: UPDATE users SET age = 30 WHERE id = 1; (获取id=1锁)
-- 会话2: UPDATE users SET age = 31 WHERE id = 2; (获取id=2锁)
-- 会话1: UPDATE users SET age = 32 WHERE id = 2; (等待id=2锁)
-- 会话2: UPDATE users SET age = 33 WHERE id = 1; (死锁!)
-- 避免死锁
-- 1. 保持一致的锁顺序
-- 2. 使用低隔离级别
-- 3. 尽量使用索引,减少锁范围
-- 4. 减小事务大小,缩短持有锁的时间
DELETE执行过程
DELETE语句用于删除表中记录,其执行过程需要先定位记录,然后标记删除,最后清理空间。
DELETE执行流程
flowchart TD
A[DELETE语句] --> B[解析器解析]
B --> C[检查表结构和权限]
C --> D[定位要删除的记录]
D --> E[获取排他锁]
E --> F[写入UNDO日志]
F --> G[标记记录为已删除]
G --> H[更新索引]
H --> I[写入REDO日志]
I --> J[写入Binlog]
J --> K[提交事务]
K --> L[后台Purge线程清理]
D -->|有外键?| M[检查外键约束]
M -->|级联删除| N[删除关联数据]
M -->|不删除| O[报错或忽略]
style A fill:#e3f2fd,stroke:#1565c0
style F fill:#fff3e0,stroke:#e65100
style G fill:#ffebee,stroke:#c62828
style L fill:#e0e0e0,stroke:#616161
DELETE执行步骤
-- DELETE语句执行详解:
-- 示例
DELETE FROM users WHERE id = 1;
-- 执行过程:
-- 1. 解析SQL,确定删除条件
-- 2. 根据WHERE条件定位要删除的行
-- 3. 对要删除的行加排他锁(X锁)
-- 4. 记录完整数据到UNDO日志(支持回滚)
-- 5. 将记录标记为"已删除"(不是立即删除)
-- 6. 更新所有相关索引
-- 7. 写入REDO日志
-- 8. 写入BINLOG
-- 9. 提交事务
-- 10. 后台Purge线程真正删除数据
-- 简单DELETE
DELETE FROM users WHERE id = 1;
-- 条件DELETE
DELETE FROM users WHERE age < 18;
-- 关联DELETE
DELETE u FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'cancelled';
-- ORDER BY + LIMIT DELETE
DELETE FROM users
WHERE status = 'inactive'
ORDER BY created_at ASC
LIMIT 100;
-- TRUNCATE(高效清空表)
TRUNCATE TABLE users;
-- TRUNCATE vs DELETE:
-- - TRUNCATE: 重建表,自增计数重置,快
-- - DELETE: 逐行删除,记录日志,慢
删除机制说明
-- DELETE的删除机制:
-- 1. 标记删除 vs 真正删除
-- InnoDB中,DELETE只是标记记录为已删除
-- 真正删除由后台Purge线程处理
-- 这是为了支持MVCC和事务回滚
-- 2. 空间回收
-- 删除后空间不会立即归还操作系统
-- InnoDB会复用这些空间存储新数据
-- 3. 释放空间
-- OPTIMIZE TABLE: 重建表,释放空间
OPTIMIZE TABLE users;
-- ALTER TABLE ENGINE=InnoDB: 同样效果
ALTER TABLE users ENGINE=InnoDB;
-- 4. Purge机制
-- 后台线程定期清理已标记删除的记录
-- 清理undo log和索引
SHOW VARIABLES LIKE 'innodb_purge_threads';
-- 5. 快速删除大量数据
-- 分批删除,避免长时间锁表
DELETE FROM logs
WHERE created_at < '2023-01-01'
LIMIT 10000;
-- 循环执行直到删除完成
总结
| 操作类型 | 关键步骤 | 涉及日志 | 锁类型 |
|---|---|---|---|
| SELECT | 解析→优化→执行→读取 | 一般无日志 | 共享锁(可选) |
| INSERT | 解析→锁表→插入→提交 | UNDO+REDO+BIN | 排他锁 |
| UPDATE | 解析→查询→锁行→修改→提交 | UNDO+REDO+BIN | 排他锁+间隙锁 |
| DELETE | 解析→查询→锁行→标记→提交 | UNDO+REDO+BIN | 排他锁+间隙锁 |
-- 性能优化建议:
-- SELECT优化
-- 1. 使用索引,避免全表扫描
-- 2. 只查询需要的列
-- 3. 避免在WHERE中对字段使用函数
-- 4. 使用EXPLAIN分析执行计划
-- INSERT优化
-- 1. 批量INSERT比单条快
-- 2. 禁用唯一索引检查:SET unique_checks=0;
-- 3. 禁用外键检查:SET foreign_key_checks=0;
-- 4. 使用LOAD DATA INFILE批量导入
-- UPDATE优化
-- 1. 尽量使用主键或索引列更新
-- 2. 减少更新字段数量
-- 3. 批量更新时使用LIMIT分批
-- 4. 避免更新大字段
-- DELETE优化
-- 1. 使用主键删除最快
-- 2. 大量删除时使用分批
-- 3. 使用TRUNCATE替代DELETE清空表
-- 4. 删除后使用OPTIMIZE TABLE回收空间