一、MySQL进阶
1. SQL优化
"慢"是数据库最大的敌人,而"优化"则是对抗它的终极武器。
SQL 优化的核心目标就是:用最少的资源(CPU、内存、磁盘 I/O),最快地完成数据查询或操作。它不是玄学,而是有章可循的技术艺术。
1.1 插入数据
想象一下:你要把一万条用户信息写入数据库,一条一条地 insert,那画面太美不敢看 😵💫
✅ 正确姿势:批量插入!
INSERT INTO users (name, email) VALUES
('张三', 'zhang@example.com'),
('李四', 'li@example.com'),
('王五', 'wang@example.com');
✅ 优点:
- 减少网络往返次数
- 减少事务开销
- 提升整体吞吐量
💡 高级技巧:使用 ON DUPLICATE KEY UPDATE
当你担心重复插入时,可以这样写:
INSERT INTO users (id, name, email) VALUES
(1, '张三', 'zhang@example.com')
ON DUPLICATE KEY UPDATE email = VALUES(email);
这样既能插入新数据,又能更新已有记录,避免了重复判断逻辑。
⚠️ 小贴士:对于大量数据,考虑使用
LOAD DATA INFILE或MyISAM表引擎(非事务性)提升导入速度。


正常输入连接数据库指令,如果需要load加载指令,那么就需要加上参数 --local --infile

通过该指令就可以查看load全局参数是否开启。

将开关设置为1,即打开load

1.2 主键优化
主键是表中每一行的"身份证号",它是索引的基础,也是查询性能的关键。
所有的数据都会出现在叶子节点,而非叶子节点仅仅起到一个索引数据的作用,而非叶子节点的索引以及叶子节点当中的数据最终呢都存放在一个逻辑结构叫页配置当中的,而这些黄色的这些块儿实际上都是一个一个配置(页),一个页的大小是固定的。



由于乱序插入,现在插入50,叶子节点必须是有序的。

此时页会进行如下操作,新建一个页面,第一个页由于存储的数据超过了百分之50,那么在中间位置进行分裂,将左侧的部分数据移动到新的页面中,在将50插入到此页的后面。

然后对链表的指针进行重新的设置,进行如下的链表顺序的指针,可见页分裂是很消耗性能的


主键的设计原则:
如果主键长度比较长,则意味着二级索引比较多,那么将会占用大量的磁盘空间,而且在搜索的时候将会耗费大量的磁盘io。所以要尽量的降低主键的长度。

🌟 进阶玩法:UUID vs 自增 ID?
有人喜欢用 UUID 做主键,因为它分布式友好,但代价是:
- 随机性高 → 导致页分裂(Page Splitting)
- 索引碎片化严重 → 查询变慢
👉 所以,除非你真的需要全局唯一且不可预测的 ID,否则还是乖乖用 BIGINT AUTO_INCREMENT 吧!
1.3 order by优化
排序是最容易被忽视的性能杀手。一个看似简单的 ORDER BY 可能让查询时间从毫秒级飙升到秒级!
❌ 危险操作:没有索引的排序
sql
SELECT * FROM users ORDER BY name;
如果 name 字段没有索引,MySQL 必须将所有数据加载到内存中进行排序,这就是所谓的 "Filesort" ------ 消耗巨大!
✅ 安全做法:为排序字段建立索引
sql
ALTER TABLE users ADD INDEX idx_name (name);
然后:
sql
SELECT * FROM users ORDER BY name; -- 现在可以走索引了!
🚫 注意:组合排序的陷阱
sql
SELECT * FROM users ORDER BY name, age;
只有当 name 和 age 都出现在索引中,并且顺序一致时,才能利用索引排序。
👉 最佳实践:创建联合索引
sql
CREATE INDEX idx_name_age ON users(name, age);
💡 高级技巧:覆盖索引(Covering Index)
如果你只需要部分字段,可以让索引"覆盖"整个查询:
sql
SELECT name, email FROM users ORDER BY name;
-- 如果索引是 (name, email),则无需回表!
✅ 结论:ORDER BY 的字段必须有索引,尤其是多列排序时,联合索引是首选!


1.4 gruop by优化
GROUP BY 用于聚合统计,比如统计每个城市的用户数量。但它同样可能成为性能黑洞。

❌ 低效写法:未加索引的分组
sql
SELECT city, COUNT(*) FROM users GROUP BY city;
如果没有 city 的索引,MySQL 必须扫描全表,再按城市分组,耗时惊人!
✅ 优化策略:给分组字段建索引
sql
ALTER TABLE users ADD INDEX idx_city (city);
现在查询就能高效执行了!
🚩 特别提醒:GROUP BY 与 WHERE 的关系
sql
SELECT city, COUNT(*)
FROM users
WHERE status = 'active'
GROUP BY city;
这里 status 和 city 都参与过滤和分组,建议创建复合索引:
sql
CREATE INDEX idx_status_city ON users(status, city);
这样既能过滤,又能分组,完美!
🤔 分组结果太多怎么办?
如果分组后的结果集太大(如超过百万行),考虑:
- 是否真的需要全部返回?
- 是否可以用采样或分页处理?
- 是否可以预计算并缓存?
📌 小知识:MySQL 中
GROUP BY默认不保证排序,若需有序输出,请显式加上ORDER BY。
1.5 limit优化

🔍 1.5 LIMIT 优化:分页查询不踩坑!
❌ 问题:传统分页导致性能雪崩
你是否见过这样的代码?
sql
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 10000;
这是一条典型的"翻页"语句,用于加载第 1001~1010 条记录。
但是!当 OFFSET 很大时,MySQL 必须:
- 扫描前 10000 行数据;
- 跳过它们;
- 只返回最后 10 行。
👉 这意味着:即使只返回 10 条数据,也要扫描 10000+ 行!随着页码增大,响应时间指数级增长,最终拖垮数据库。
✅ 解决方案一:使用「游标分页」(Cursor-based Pagination)
放弃 OFFSET,改用上次查询的最后一个 ID 作为起点。
sql
-- 第一页
SELECT * FROM users ORDER BY id LIMIT 10;
-- 第二页:从上一页最后一个用户的 id 开始
SELECT * FROM users WHERE id > 1000 ORDER BY id LIMIT 10;
✅ 优点:
- 每次都只扫描最近的数据
- 性能稳定,不受页码影响
- 支持高并发场景
💡 适合场景:无限滚动列表、消息流、日志查看等。
✅ 解决方案二:利用索引覆盖 + 子查询(适用于复杂条件)
如果你需要根据多个字段排序或过滤,可以结合子查询和索引优化。
sql
SELECT * FROM users
WHERE id IN (
SELECT id FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 10 OFFSET 50
)
ORDER BY created_at DESC;
虽然结构复杂,但可以通过索引避免全表扫描。
🛠 实战建议
| 场景 | 推荐方式 |
|---|---|
| 简单分页(按主键) | 游标分页(推荐) |
| 多条件分页 | 使用索引覆盖 + 子查询 |
| 前端展示 | 尽量控制每页数量(如 ≤ 50) |
| 高频翻页 | 缓存结果集或使用 Redis 分页 |
⚠️ 提醒:永远不要在生产环境使用
LIMIT 10 OFFSET 1000000这种写法!
1.6 count优化



❌ 误区:盲目使用 COUNT(*)
很多人以为 COUNT(*) 是最快的计数方式,其实不然。它的行为取决于上下文。
三种常见写法对比:
sql
COUNT(*) -- 统计所有行,包括 NULL
COUNT(1) -- 同样统计所有行,常用于快速计数
COUNT(column) -- 只统计非 NULL 的列值
🔥 重点来了:
COUNT(*)和COUNT(1)在大多数情况下是等价的,且性能相近。
但为什么说"盲目使用"是错误的?
✅ 正确做法:选择合适的计数方式
情况一:你要统计总行数(含 NULL)
sql
SELECT COUNT(*) FROM users;
✔️ 推荐:这是最通用的方式。
情况二:你要统计有效用户(排除删除标记)
sql
SELECT COUNT(id) FROM users WHERE deleted = 0;
✔️ 更精准,也更高效,因为 id 是主键,不会为 NULL。
情况三:你要统计某个字段的非空值
sql
SELECT COUNT(email) FROM users;
✔️ 如果 email 允许为空,这会忽略 NULL 值。
💡 高级技巧:使用索引加速 COUNT
如果表很大,频繁执行 COUNT(*) 会导致全表扫描。
解决方案:维护一个统计表或使用缓存。
方案 A:创建汇总表(Summary Table)
sql
CREATE TABLE user_stats (
total_users INT,
active_users INT,
updated_at TIMESTAMP
);
-- 定期更新
INSERT INTO user_stats VALUES (
(SELECT COUNT(*) FROM users),
(SELECT COUNT(*) FROM users WHERE status = 'active'),
NOW()
);
然后查询时直接读取:
sql
SELECT total_users FROM user_stats;
✅ 优点:毫秒级响应,适合报表类场景。
方案 B:Redis 缓存计数
sql
SET user_count:total 100000 EX 3600
配合定时任务刷新,避免重复计算。
🚫 注意:
COUNT(*)不一定比COUNT(1)快,但在 InnoDB 中,两者都会触发一次全表扫描(除非有覆盖索引)。
1.7 update优化

UPDATE 是数据库中最"危险"的操作之一。它不仅改变数据,还可能引发锁竞争、事务阻塞、甚至死锁。
❌ 危险操作:无条件批量更新
sql
UPDATE users SET status = 'inactive';
⚠️ 这个语句会锁定整张表(如果是 MyISAM),或者对每一行加行锁(InnoDB),严重影响并发性能!
✅ 安全做法:带条件更新 + 小批量处理
sql
UPDATE users
SET status = 'inactive'
WHERE last_login < '2023-01-01'
AND status = 'active';
这样可以:
- 减少影响范围
- 利用索引(如
last_login有索引) - 避免全表扫描
🚀 进阶优化:分批更新(Batch Update)
对于大数据量更新,避免一次性完成,采用"分批次"策略:
sql
-- 示例:每次更新 1000 条
UPDATE users
SET status = 'archived'
WHERE id BETWEEN 1 AND 1000
AND status = 'old';
-- 下一批
UPDATE users
SET status = 'archived'
WHERE id BETWEEN 1001 AND 2000
AND status = 'old';
或者使用循环脚本自动执行:
sql
DELIMITER //
CREATE PROCEDURE UpdateUsersBatch()
BEGIN
WHILE ROW_COUNT() > 0 DO
UPDATE users
SET status = 'archived'
WHERE status = 'old'
AND id IN (
SELECT id FROM users
WHERE status = 'old'
ORDER BY id LIMIT 1000
);
END WHILE;
END //
DELIMITER ;
✅ 优点:降低锁冲突概率,防止长时间持有锁。
🔒 锁机制提醒:InnoDB 的行锁 vs 表锁
- InnoDB 默认使用行级锁,但若没有索引,会退化为表锁。
- 因此,UPDATE 的 WHERE 条件必须走索引!
sql
-- ✅ 正确:有索引
UPDATE users SET age = 25 WHERE email = 'zhang@example.com';
-- ❌ 错误:无索引,可能导致全表扫描和锁表
UPDATE users SET age = 25 WHERE name LIKE '%张%'; -- 没有索引
🔄 事务控制建议
sql
START TRANSACTION;
UPDATE users SET status = 'pending' WHERE id = 1;
-- 其他操作...
COMMIT; -- 或 ROLLBACK
避免长事务,减少锁等待时间。

SQL 优化是一场永无止境的旅程。每一次看似微小的调整,都可能是系统性能飞跃的关键一步。
💬 "优秀的程序员,不仅能让程序跑起来,更能让它跑得快、跑得稳。"