找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3661

积分

0

好友

485

主题
发表于 2 小时前 | 查看: 4| 回复: 0

MySQL 因为开源、免费、性能不错,在国内使用非常广泛。但在日常开发中,我们经常会遇到各种容易“踩坑”的情况,导致查询变慢、数据不一致,甚至系统崩溃。这篇文章就结合实战,梳理了 MySQL 开发中最常见的 15 个坑,并给出对应的优化策略,希望对你有帮助。

1. 查询不加 WHERE 条件

一些开发者习惯一次性查出整张表的所有数据,然后在内存中处理业务逻辑,认为这样代码更高效。

反例:

SELECT * FROM users;

这种做法,数据量小的时候看不出问题。但数据量一旦增长,每个操作都要扫描全表,极易导致内存溢出(OOM),处理速度也会急剧下降。

正例:

SELECT * FROM users WHERE code = '1001';

使用具体的 WHERE 条件先过滤数据,再做处理,既安全又高效。

2. 没有使用索引

线上系统刚上线时数据较少,没加索引也能跑。但随着用户量增加,数据呈指数级增长,某天你会突然发现查询变得极慢。

比如这个查询:

SELECT * FROM orders WHERE customer_id = 123;

customer_id 字段加上索引,速度会大幅提升:

CREATE INDEX idx_customer ON orders(customer_id);

3. 不处理 NULL 值

统计时经常忽略 NULL 的影响,导致结果与预期大相径庭。

反例:

SELECT COUNT(name) FROM users;

这条语句只统计了 name 字段不为 NULL 的记录数,并不是总行数。

如果想统计所有记录,应该用 COUNT(*)

SELECT COUNT(*) FROM users;

这样才能得到真正的总行数。

4. 数据类型选错

有些开发者在建表时随意使用 VARCHAR(255),不仅浪费存储,还会降低查询性能。

反例:

CREATE TABLE products (
    id INT,
    status VARCHAR(255)
);

如果字段值只有几种固定状态,完全可以用更小的整形类型。例如改为 tinyint

CREATE TABLE products (
    id INT,
    status tinyint(1) DEFAULT '0' COMMENT '状态 1:有效 0:无效'
);

这样既节省空间,又提升效率。

5. 深分页问题

分页查询是日常操作,一般我们都用 LIMIT

SELECT * FROM users LIMIT 0,10;

当数据量很大时,前几页性能可能尚可。但翻到第 10 万页时,查询会变得异常缓慢,这就是“深分页”问题。

为什么会慢?因为 MySQL 需要先扫描前面所有的数据,再丢弃掉不需要的行,只返回最后 10 条。下面给出三种优化方案。

5.1 记录上一次的 ID

核心思路:避免重复扫描已经查过的数据。配合自增主键,记录上一次查询结果的最大 ID,下次翻页时直接从该 ID 之后开始。

SELECT id,name FROM orders
WHERE id > 1000000
LIMIT 100000,10;

假设上次查询返回的最大 ID 是 1000000,这次从 1000001 开始取 10 条。但要注意:

  • 仅适合连续翻页(上一页/下一页),不适用于随机跳页。
  • 要求 ID 字段必须连续自增。

5.2 使用子查询

先用子查询找出符合条件的少量主键,再用主键去关联原表取完整数据。这样可以走覆盖索引,大幅减少回表次数。

SELECT * FROM orders WHERE id IN (
  SELECT id FROM (
    SELECT id FROM orders WHERE create_time > '2024-08-11' LIMIT 100000, 10
  ) t
);

原本的 SQL 扫描 100010 条记录,却要回表 100010 次;而子查询方式只回表 10 次,性能自然提升。

5.3 使用 INNER JOIN 关联查询

与子查询思路类似,通过 JOIN 先把满足条件的主键集筛选出来,再做关联查询。

SELECT o1.* FROM orders o1
INNER JOIN (
   SELECT id FROM orders
    WHERE create_time > '2024-08-11'
    LIMIT 100000,10
) AS o2 ON o1.id = o2.id;

这种方式同样能有效减少回表,加快查询速度。

6. 没有用 EXPLAIN 分析查询

一条 SQL 跑得慢,不先看执行计划就盲目调整,往往事倍功半。

正例:

EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

EXPLAIN 会告诉你查询到底是怎么执行的:是否走索引、扫描了多少行、使用了哪种连接方式等,帮助你精准定位瓶颈。想深入了解,可以看这篇:SQL性能优化神器

7. 字符集设置不当

很多人习惯将字符集设为 utf8,我自己以前也这样干过。但后来踩坑了:用户输入表情符号时,程序直接报错;汉字也可能变成乱码,体验极差。

建表时建议直接使用 utf8mb4

CREATE TABLE messages (
    id INT,
    content TEXT
) CHARACTER SET utf8mb4;

utf8mb4 能够支持更多字符,包括常用的中文汉字和 Emoji 表情,避免乱码烦扰。

8. SQL 注入风险

拼接 SQL 字符串是典型的安全隐患,很容易被 SQL 注入攻击。比如在动态排序时,直接用参数拼 ORDER BY 字段或排序方式,一旦处理不当就可能出大事。

反例:

String query = "SELECT * FROM users WHERE email = '" + userInput + "';";

应当使用预编译语句(PreparedStatement):

PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE email = ?");
stmt.setString(1, userInput);

在 MyBatis 中,尽量使用 #{} 进行参数替换,避免使用 ${}。关于 SQL 注入的更多细节,可以参考这篇文章:卧槽,sql注入竟然把我们的系统搞挂了

9. 事务问题

更新多个相关表时不加事务,是数据不一致的常见诱因。

反例:

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

用户 1 向用户 2 转账 100 元,如果没有事务,可能出现用户 1 钱已扣掉,用户 2 却没收到的情况。

正确的做法是开启事务:

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

这样即便第二步失败,第一步也会回滚。在 Spring 中,除了声明式 @Transactional,更推荐使用 TransactionTemplate 进行编程式事务管理,可控性更好。

10. 校对规则问题

表和字段可以指定 COLLATE 校对规则,主要分三类:

  • _ci(case insensitive):不区分大小写
  • _cs(case sensitive):区分大小写
  • _bin:按二进制存储,也区分大小写

最常用的是 utf8mb4_general_ci(默认不区分大小写)和 utf8mb4_bin

例如品牌表使用了 utf8mb4_unicode_ci(不区分大小写):

CREATE TABLE `brand` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(30) NOT NULL COMMENT '品牌名称',
  `create_user_id` bigint NOT NULL COMMENT '创建人ID',
  `create_user_name` varchar(30) NOT NULL COMMENT '创建人名称',
  `create_time` datetime(3) DEFAULT NULL COMMENT '创建日期',
  `update_user_id` bigint DEFAULT NULL COMMENT '修改人ID',
  `update_user_name` varchar(30) DEFAULT NULL COMMENT '修改人名称',
  `update_time` datetime(3) DEFAULT NULL COMMENT '修改时间',
  `is_del` tinyint(1) DEFAULT '0' COMMENT '是否删除 1:已删除 0:未删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='品牌表';

查询时:

SELECT * FROM brand WHERE `name`='yoyo';

这条 SQL 也能匹配到 YOYO。但如果业务代码里直接使用 equals 方法判断字符串,就会因为大小写不一致而出错。此时应改用 equalsIgnoreCase 方法。

11. 使用过多的 SELECT *

习惯用 SELECT * 会把所有字段都取出来,很多实际用不到,白白浪费网络带宽和查询资源。

反例:

SELECT * FROM orders;

只查需要的字段:

SELECT id, total FROM orders;

按需取列,简单又高效。

12. 索引失效

你是否遇到过:明明建了索引,但查询时索引就是没用上,SQL 突然变得奇慢无比?用 EXPLAIN 看一下执行计划,确认是否真的失效了。

常见的索引失效原因,下面这张思维导图总结得很清楚:

MySQL索引失效的10种常见原因思维导图

如果想进一步了解,可以阅读这篇详细的文章:聊聊索引失效的10种场景,太坑了避免索引失效的思路其实也可以总结成一句话:用对索引,少走弯路。你可以把这张图收藏起来,方便随时对照检查。索引失效往往是查询性能恶化的元凶,务必重视。

13. 频繁修改表或数据

高并发场景下,频繁 ALTER 表结构、大批量 UPDATEDELETE,都可能引发锁表,导致大量用户请求堆积,系统响应变慢。

优化建议:

  • 大量更新或删除数据时,分批执行,不要一次性处理过多记录。
  • ALTERDROP 等修改表结构的操作,避开业务高峰期,尽量选在凌晨低负载时执行。
  • 可以考虑使用 Percona Toolkit、gh-ost 等在线 DDL 工具,在不锁表的前提下完成表结构变更。

14. 没有定期备份

最怕队友误删数据,这种事我自己就遇到过好几次。测试环境整张表被清空,数据全没了才追悔莫及。

定期用 mysqldump 备份是基本操作:

mysqldump -u root -p database_name > backup.sql

配置一个定时任务,每天或每周自动备份一次。关键时刻,通过 mysql 命令就能快速恢复数据,让风险降到最低。

15. 忘了归档历史数据

数据表越来越大,历史数据堆积严重,查询越来越慢。但用户真正关心的往往是近一个月、三个月或一年的数据,一年以前的数据大概率没人翻了。

建议将历史数据迁移到归档库,主库只保留近期热数据。这样既保证了核心业务的查询性能,也满足了极少数的历史查询需求。


以上 15 个坑,你踩过几个?希望这份避坑指南能帮你在日常开发中少走弯路。更多数据库实战经验,欢迎到云栈社区一起交流探讨。




上一篇:MySQL千万级大表建索引:阿里二面考察的6个核心要点
下一篇:Coding Agent安全治理实践:OpenAI内部沙箱、审批与遥测架构
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-11 21:35 , Processed in 0.643269 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表