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回收空间