刚入行那会儿,我特别喜欢在代码评审时给同事挑SQL毛病。直到有天我的查询把生产库拖垮,DBA拿着执行计划找我"谈心"后,才真正明白什么叫"SQL刺客"。今天分享8个真实踩过的坑,每个都能让查询性能下降百倍,来看看你中过几招?
sql复制-- user_id是varchar类型但存的是数字
SELECT * FROM orders WHERE user_id = 10086;
这个查询会导致全表扫描,因为字符串字段与数字比较时会发生隐式转换。我见过一个千万级用户表因此CPU飙到100%,正确的写法应该是:
sql复制SELECT * FROM orders WHERE user_id = '10086';
注意:在MySQL中,当字符集不同时(如utf8与utf8mb4)也会发生隐式转换
sql复制-- 有联合索引(status, create_time)
SELECT * FROM articles WHERE create_time > '2023-01-01';
这个查询用不到索引,就像用字典时直接翻到第100页找某个字。应该改为:
sql复制SELECT * FROM articles WHERE status = 1 AND create_time > '2023-01-01';
实测在500万数据量下,前者执行需要4.2秒,后者仅0.05秒。
sql复制-- 忘记写连接条件
SELECT * FROM users, orders, products;
我们线上就发生过这种事故——三个百万级表的笛卡尔积直接把内存撑爆。建议养成使用显式JOIN的习惯:
sql复制SELECT * FROM users
JOIN orders ON users.id = orders.user_id
JOIN products ON orders.product_id = products.id;
java复制// 伪代码示例
List<User> users = userDao.findAll();
for(User user : users) {
List<Order> orders = orderDao.findByUserId(user.getId());
// ...
}
这种写法会产生1次用户查询+N次订单查询。应该改用JOIN一次性获取:
sql复制SELECT u.*, o.* FROM users u
LEFT JOIN orders o ON u.id = o.user_id
sql复制SELECT * FROM logs ORDER BY id LIMIT 1000000, 20;
这种写法会先读取1000020条记录再丢弃前100万条。去年我们系统就因为这个查询导致IOPS飙升。优化方案:
sql复制-- 方案1:记录上次查询的最大ID
SELECT * FROM logs WHERE id > 1000000 ORDER BY id LIMIT 20;
-- 方案2:JOIN优化(MySQL8.0+)
SELECT t.* FROM logs t
JOIN (SELECT id FROM logs ORDER BY id LIMIT 1000000, 20) tmp
ON t.id = tmp.id;
测试数据:偏移量1万时,原方案1.2秒,优化方案0.02秒。
sql复制-- create_time有索引
SELECT * FROM orders WHERE DATE_FORMAT(create_time, '%Y-%m') = '2023-01';
这会让索引失效,就像把字典每页标题都涂黑再查找。应该改为范围查询:
sql复制SELECT * FROM orders
WHERE create_time >= '2023-01-01'
AND create_time < '2023-02-01';
sql复制SELECT DISTINCT user_id FROM orders;
DISTINCT会导致隐式排序,在大表上非常昂贵。如果只需要去重,可以改用:
sql复制SELECT user_id FROM orders GROUP BY user_id;
在1000万订单数据测试中,前者耗时8.7秒,后者仅1.3秒。
java复制@Transactional
public void processOrder() {
// 查询操作
User user = userDao.findById(userId);
// 网络调用
paymentService.callExternalAPI();
// 更新操作
orderDao.updateStatus();
}
这个事务包含了远程调用,可能导致锁持有时间过长。应该拆分为:
java复制public void processOrder() {
User user = userDao.findById(userId);
paymentResult = paymentService.callExternalAPI();
updateOrderInTransaction(paymentResult);
}
@Transactional
void updateOrderInTransaction(PaymentResult result) {
orderDao.updateStatus();
}
sql复制BEGIN;
SELECT * FROM products WHERE id=1 FOR UPDATE;
-- 其他操作
COMMIT;
过度使用悲观锁会导致并发性能下降。多数情况下用乐观锁就够了:
sql复制UPDATE products SET stock=stock-1, version=version+1
WHERE id=1 AND version=123;
sql复制SELECT COUNT(*) FROM user_actions;
对于千万级表,这个查询可能耗时数秒。如果不需要精确计数,可以考虑:
sql复制-- 方案1:使用估算值
EXPLAIN SELECT COUNT(*) FROM user_actions;
-- 方案2:维护计数表
SELECT total FROM counter_table WHERE table_name='user_actions';
sql复制-- 忘记加WHERE条件
UPDATE users SET status=0;
这是DBA的噩梦,一定要养成先SELECT确认再写UPDATE的习惯,或者使用事务:
sql复制BEGIN;
SELECT * FROM users WHERE ...;
UPDATE users SET status=0 WHERE ...;
COMMIT;
sql复制SELECT * FROM products
WHERE status=1 OR category_id=5;
这种OR查询往往走不了索引。可以改写为UNION ALL:
sql复制SELECT * FROM products WHERE status=1
UNION ALL
SELECT * FROM products WHERE category_id=5 AND status!=1;
sql复制SELECT content FROM articles WHERE id=123;
如果content是TEXT大字段,而业务只需要元数据,应该避免查询大字段:
sql复制SELECT title, author, create_time FROM articles WHERE id=123;
拿到一个慢查询,我通常这样分析:
sql复制EXPLAIN FORMAT=JSON
SELECT * FROM orders o JOIN users u ON o.user_id=u.id
WHERE u.register_time > '2023-01-01';
重点关注:
在my.cnf中配置:
ini复制slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
分析工具推荐:
bash复制mysqldumpslow -s t /var/log/mysql/mysql-slow.log
pt-query-digest /var/log/mysql/mysql-slow.log
我们团队现在强制要求:
推荐搭建:
曾经有个分页查询把CPU打到90%,加上监控后设置了自动kill,再也没发生过类似事故。