你可曾碰到过数据库忽然卡死,以及CPU飙升至100%,用户无法打开页面的状况呢。这些问题绝大多数都是源于同一个缘由:程序员编写了几行表面上看不出来问题,实际上却能够对数据库造成致命损害的SQL代码。
SELECT * FROM orders WHERE user_id = 12345;
在真实的生产环境里,下面这10个错误写法中的每一个,都曾造成过严重故障。不管你是从事开发工作,还是进行测试工作,只要搞懂这些,便能够避开99%的性能坑。
SELECT id, order_no, amount, status
FROM orders
WHERE user_id = 12345;
有不少人在进行查询操作时,习惯于书写SELECT ,缘由是自认为会比较省事。然而,当你仅仅只需要两三列数据的时候,数据库却把存有几十个字段的整行记录都读取出来了,如此一来,内存以及网络带宽就被毫无必要地浪费掉了。在2025年的时候,某电商平台的订单查询接口因为运用了SELECT ,当单表数据量达到500万行之后,查询所耗费的时间从原本的50毫秒急剧暴涨到了3秒。
情况更为严重的是,SELECT *会致使覆盖索引不再起作用,覆盖索引所指的是索引树当中本身就涵盖了你所需要的全部字段内容,并不需再回到表中去查询数据行,然而一旦使用了星号那就必定要再回到表中进行查询,但用了星号就必然要回表,IO次数翻倍。正确的办法是仅书写需要的字段名称,例如SELECT id, name, status。
SELECT * FROM users WHERE phone LIKE '8%';
-- 方案一:避免前导通配符
SELECT * FROM users WHERE phone LIKE '138%';
-- 方案二:使用全文索引
SELECT * FROM users
WHERE MATCH(phone) AGAINST('138' IN BOOLEAN MODE);
于用户名或者标题字段那儿写LIKE '%关键词%',这般写法会致使数据库舍弃索引,老老实实地从起始处扫描整一张表。某社交平台的用户搜索功能由于这个写法,在拥有2000万用户的表上每一次查询耗费8秒时间,直接使数据库连接池受到拖累。
SELECT * FROM orders WHERE order_no = 123456;
-- order_no字段类型为varchar
因为B + 树索引仅能从左至右进行匹配,所以前导百分号致使索引失效。要是你的确有全文搜索的需求,那就应当采用Elasticsearch专门的搜索引擎,或者MySQL 8.0及以上版本自带的全文索引。正确的做法是防止使用前导百分号,或者转而采用其他搜索方案。
SELECT * FROM orders WHERE order_no = '123456';

这个错误彰显出高度隐蔽性,假定订单表之中的user_id字段为varchar类型,然而你书写where user_id = 123456时,却未添加引号,数据库会暗自将表里每一行的user_id转化成数字之后再予以比较,索引会径直失效,某银行系统正是由于这个问题,一个原本耗时10毫秒的查询演变成了全表扫描,耗费时长为30秒。
那种称作日期的字段,也极具有可能让人在使用过程中不小心踩到坑。比如说,在进行数据查询时,当使用where create_time = '2026-04-17' 这样的语句看似没啥问题,然而呀,如果create_time所对应的字段类型是datetime类型的话,那如此这般的书写方式,就会导致时分秒那一部分的数据被丢失掉。而正确的做法应该是,要让字段类型和查询条件保持相一致的状态,对于字符串类型的字段要添加引号,对于日期类型的字段则需要采用区间查询的方式来进行操作。
SELECT * FROM orders WHERE DATE(create_time) = '2025-01-01';
SELECT * FROM orders
WHERE create_time >= '2025-01-01'
AND create_time < '2025-01-02';
于where条件里头,针对索引字段运用函数,就像where DATE(create_time) = '2026-04-17'这般。数据库没办法反向推导函数之前的原本的值,仅仅能够老老实实去把每一行数据都进行一番计算。某物流系统的运单查询由于这样的写法,每天下午高峰期的时候CPU使用率持续维持在100%。
依循相同的道理,针对字段开展数学运算亦会致使索引失去效用,例如where price + 10 > 100。正确的做法是将函数挪至等号右侧,写作where create_time >= '2026-04-17 00:00:00' ,并且create_time < '2026-04-17 23:59:59' ,如此便能够正常运用索引。
SELECT * FROM logs ORDER BY id LIMIT 1000000, 20;
-- 方案一:记录上一页最大ID
SELECT * FROM logs
WHERE id > 1000000
ORDER BY id LIMIT 20;
-- 方案二:覆盖索引+延迟关联
SELECT l.* FROM logs l
INNER JOIN (
SELECT id FROM logs
ORDER BY id LIMIT 1000000, 20
) tmp ON l.id = tmp.id;
分页查询之际,书写limit 1000000, 20,这般看来仅获取20条数据,然而数据库实则需将前100万条数据统统扫描一回,而后丢弃,最终仅留存20条。于某内容平台的文章列表页,当用户翻动至第100页时,接口响应时间由200毫秒转变为15秒。
把游标分页当作正确方针来运用,把上一页最后一条数据的id记下来,写成where id大于上一页最大id limit 20。或者采用延迟关联,先查找主键再来回表拿数据。在2025年,某个资讯APP改用了这种办法之后,就算翻到500页,查询时间也稳稳地保持在50毫秒以内。
SELECT * FROM orders
WHERE user_id NOT IN (SELECT user_id FROM banned_users);

-- 使用NOT EXISTS替代
SELECT o.* FROM orders o
WHERE NOT EXISTS (
SELECT 1 FROM banned_users b
WHERE b.user_id = o.user_id
);
-- 或使用LEFT JOIN + IS NULL
SELECT o.* FROM orders o
LEFT JOIN banned_users b ON o.user_id = b.user_id
WHERE b.user_id IS NULL;
子查询采用NOT IN形式,看上去逻辑是清晰的,然而,一旦子查询的结果里含有NULL值,那么整个查询就会返回为空的结果,格外严重的是,NOT IN一般是没办法使用索引的,这将会引发全表扫描的情况,有一个某财务系统里的对账查询,就是因为使用了NOT IN,结果运行了40分钟都还没有得出结果。
用NOT EXISTS去替代NOT IN,或者借助LEFT JOIN加上IS NULL判断,这才是那种正确的做法。NOT EXISTS的优化器一般会挑选出更为高效的执行计划。例如把它写成where not exists (select 1 from other_table where id = main.id),性能能提升到10倍以上。
SELECT * FROM products
WHERE category_id = 10 OR price < 100;
-- 使用UNION拆分
SELECT * FROM products WHERE category_id = 10
UNION ALL
SELECT * FROM products WHERE price < 100
AND category_id != 10; -- 避免重复
当多个条件借助OR进行连接之际,举例来说where id = 1或者name = '张三'。要是id以及name并非同一个索引,数据库仅仅能够放弃索引转而采用全表扫描。某CRM系统的客户查询接口由于这样的写法,在数据量达到100万行的时候查询就出现超时状况了。
BEGIN;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- 业务逻辑处理,耗时5秒
UPDATE orders SET status = 2 WHERE id = 1;
-- 忘记COMMIT或延迟COMMIT
采用的正确方式是将OR拆解为两个查询并借助UNION予以合并,或者要确保OR两边的字段均处于同一个复合索引之中。格外推荐的做法是运用IN去替代等值OR,比如说where id in (1,2,3)。在2024年有某一个SaaS平台对这类查询实施重构之后,平均响应时间由2.8秒下降至0.1秒。
-- 缩短事务时间,及时提交
BEGIN;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- 快速执行业务逻辑
UPDATE orders SET status = 2 WHERE id = 1;
COMMIT; -- 立即提交
-- 或设置超时时间
SET innodb_lock_wait_timeout = 5;
打开事务之后去做数据查询,接着开展那些业务逻辑,过了半小时才予以提交。在此期间事务持有的锁不会被释放掉,后续所有需要对这些数据进行修改的请求均在排队等候。某库存系统的一次进行版本发布就由于开发人员忘掉提交事务,致使整个系统被冻结了15分钟。
// 循环执行单条插入
for (Order order : orderList) {
jdbcTemplate.execute("INSERT INTO orders ...");
}

有着更隐蔽危害的情况是,undo log没办法进行清理,进而致使数据库文件出现膨胀现象,以及查询性能有所下降。正确的做法是,事务要尽可能短小,查询应放置在事务之外,只有在需要对数据作出修改的时候,才开启事务并且马上提交。MySQL默认的是50秒的锁等待超时,一旦碰到长事务就会直接报错。
-- 批量插入
INSERT INTO orders (order_no, user_id, amount) VALUES
('ORD001', 1, 100),
('ORD002', 1, 200),
('ORD003', 2, 150);
在循环当中,一条一条地去执行Insert或者Update操作,一千条数据便会产生一千次网络往返以及一千次事务提交。对于某订单导入功能而言,处理两千条数据所要耗费的时间长达四十五秒,在这期间,数据库连接全部被占满。
SELECT * FROM large_table lt
LEFT JOIN small_table st ON lt.id = st.id;
-- large_table: 1000万行,small_table: 100行
以正确的方式来讲,当应用一条insert语句去插入多条值时,就像insert into table values (1),(2),(3)这样。又或许该采用批量预处理语句。在2025年之际,某支付系统对批量更新逻辑予以改造后,处理5000条数据所需的时间,从90秒降低到了0.8秒,性能提升幅度超过100倍。
-- 小表驱动大表
SELECT * FROM small_table st
LEFT JOIN large_table lt ON st.id = lt.id
WHERE st.status = 1;
-- 使用STRAIGHT_JOIN强制指定驱动顺序
SELECT STRAIGHT_JOIN * FROM small_table st
LEFT JOIN large_table lt ON st.id = lt.id;
在进行多表JOIN操作之际,应当运用小结果集去驱动大结果集。要是写反了,举例而言,将几百万行的大表放置于前面当作驱动表,就会引发几百万次针对另一张表的查询。某报表系统里的一个JOIN查询,由于表顺序出现错误,原本1秒能够得出结果的查询竟运行了5分钟。
尽管MySQL优化器会试着去重排JOIN顺序,然而有时候会选得不对。正确的举措是采用STRAIGHT_JOIN强制地指定顺序,将经过过滤后数据量为最小的那个表放置在最前面。剖析执行计划,确认rows估算值,手动进行干预能够让性能提高几十倍。
时常在面试里被问及的上面那10个错误在日常生产环境当中更为常见。在你最近一次代码审批期间,是否察觉到过类似的写法呢?欢迎于评论区域分享你遇到的SQL性能方面的棘手问题,点击点赞以使更多人避开此类陷阱。