1. 避免使用 SELECT *
,使用具体字段
反例:
SELECT * FROM employee;
正例:
SELECT id, name, age FROM employee;
使用具体字段可以节省资源、减少网络开销,且能避免回表查询。
2. 避免在 WHERE
子句中使用 OR
反例:
SELECT * FROM user WHERE userid=1 OR age=18;
正例:
-- 使用 UNION ALL
SELECT * FROM user WHERE userid=1
UNION ALL
SELECT * FROM user WHERE age=18;
原因:OR
会导致索引失效并引发全表扫描。
3. 使用 LIMIT
避免不必要的数据返回
反例:
SELECT id, order_date FROM order_tab WHERE user_id=666 ORDER BY create_date DESC;
正例:
SELECT id, order_date FROM order_tab WHERE user_id=666 ORDER BY create_date DESC LIMIT 1;
LIMIT
提升查询效率,避免多余的数据返回。
4. 使用数值类型代替字符串
例子:性别字段建议用数值(如0代表女生,1代表男生)而非字符串(如"WOMEN"、"MAN")。
原因:数值类型占用存储空间小、比较速度更快。
5. 批量操作(插入、删除、查询)
反例:
for(User u : list) {
INSERT INTO user(name, age) VALUES(#name#, #age#);
}
正例:
INSERT INTO user(name, age) VALUES
<foreach collection="list" item="item" index="index" separator=",">
(#{item.name}, #{item.age})
</foreach>
原因:批量插入性能更优。
6. 使用 UNION ALL
替换 UNION
(无重复记录时)
反例:
SELECT * FROM user WHERE userid=1
UNION
SELECT * FROM user WHERE age=10;
正例:
SELECT * FROM user WHERE userid=1
UNION ALL
SELECT * FROM user WHERE age=10;
原因:UNION
会排序和合并,UNION ALL
则省去这一步。
7. 尽可能使用 NOT NULL
定义字段
原因:NOT NULL
8. 避免在索引列上使用内置函数
反例:
SELECT userId, loginTime FROM loginuser WHERE DATE_ADD(loginTime, INTERVAL 7 DAY) >= NOW();
正例:
SELECT userId, loginTime FROM loginuser WHERE loginTime >= DATE_ADD(NOW(), INTERVAL -7 DAY);
原因:索引列上使用函数会导致索引失效。
9. 避免在 WHERE
子句中对字段进行表达式操作
反例:
SELECT * FROM user WHERE age - 1 = 10;
正例:
SELECT * FROM user WHERE age = 11;
10. 在 GROUP BY
前进行条件过滤
反例:
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
HAVING city = '北京';
正例:
SELECT user_id, SUM(amount) AS total_amount
FROM orders
WHERE city = '北京'
GROUP BY user_id;
11. 优化 LIKE
语句
反例:
SELECT userId, name FROM user WHERE userId LIKE '%123';
正例:
SELECT userId, name FROM user WHERE userId LIKE '123%';
原因:%
放在前面会导致索引失效。
12. 使用小表驱动大表
小表先执行以减少扫描量,如使用 EXISTS
或 IN
进行过滤。
假设我们有个客户表和一个订单表。其中订单表有10万记录,客户表只有1000行记录。现在要查询下单过的客户信息,可以这样写:
SELECT * FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
EXISTS 会逐行扫描 customers 表(即小表),对每一行 c.id,在 orders 表(大表)中检查是否有 customer_id = c.id 的记录。当然,也可以使用in实现:
SELECT * FROM customers
WHERE id IN (
SELECT customer_id FROM orders
);
in 查询会先执行内部查询部分 SELECT customer_id FROM orders,获得 orders 表(大表)中的所有 customer_id,然后在 customers 表(小表)中查找匹配的 id。因为orders表的数据量比较大,因此这里用exists效果会相对更好一点。
13. IN
查询的元素不宜太多
如果使用了in,即使后面的条件加了索引,还是要注意in后面的元素不要过多哈。
in元素一般建议不要超过200个,如果超过了,建议分组,每次200一组进行哈。
反例:
select user_id,name from user where user_id in (1,2,3...1000000);
如果我们对in的条件不做任何限制的话,该查询语句一次性可能会查询出非常多的数据,很容易导致接口超时。尤其有时候,我们是用的子查询,in后面的子查询,你都不知道数量有多少那种,更容易采坑.
如下这种子查询:
select * from user where user_id in (select author_id from artilce where type = 1);
正例是,分批进行,比如每批200个:
select user_id,name from user where user_id in (1,2,3...200);
14. 优化 LIMIT
分页
避免深分页,使用“标签记录法”或“延迟关联法”提升性能。
我们日常做分页需求时,一般会用 limit 实现,但是当偏移量特别大的时候,查询效率就变得低下,也就是出现深分页问题。
反例:
select id,name,balance from account where create_time> '2020-09-19' limit 100000,10;
我们可以通过减少回表次数来优化。一般有标签记录法和延迟关联法。
标签记录法就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。就好像看书一样,上次看到哪里了,你就折叠一下或者夹个书签,下次来看的时候,直接就翻到啦。
假设上一次记录到100000,则SQL可以修改为:
select id,name,balance FROM account where id > 100000 limit 10;
这样的话,后面无论翻多少页,性能都会不错的,因为命中了id索引。但是这种方式有局限性:需要一种类似连续自增的字段。延迟关联法延迟关联法,就是把条件转移到主键索引树,然后减少回表。
如下:
select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.create_time > '2020-09-19' limit 100000, 10) AS acct2 on acct1.id= acct2.id;
优化思路就是,先通过idx_create_time二级索引树查询到满足条件的主键ID,再与原表通过主键ID内连接,这样后面直接走了主键索引了,同时也减少了回表。
15. 优先使用连接查询而非子查询
因为使用子查询,可能会创建临时表。
反例:
SELECT * FROM customers WHERE id IN (SELECT customer_id FROM orders);
IN 子查询会在 orders 表中查询所有 customer_id,并生成一个临时结果集。
正例:
SELECT DISTINCT c.* FROM customers c JOIN orders o ON c.id = o.customer_id;
通过 JOIN 直接将 customers 和 orders 表关联,符合条件的记录一次性筛选完成。
MySQL 优化器通常可以利用索引来加速 JOIN,避免了临时表的创建,查询效果就更佳
16. Inner join 、left join、right join,优先使用Inner join,如果是left join,左边表结果尽量小
如需 LEFT JOIN
,左表数据结果尽量小。
Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
都满足SQL需求的前提下,推荐优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。
反例:
select * from tab1 t1 left join tab2 t2 on t1.size = t2.size where t1.id>2;
正例:
select * from (select * from tab1 where id >2) t1 left join tab2 t2 on t1.size = t2.size;
理由:如果inner join是等值连接,或许返回的行数比较少,所以性能相对会好一点。同理,使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少。
17. 避免 !=
或 <>
操作符
反例:
SELECT age, name FROM user WHERE age <> 18;
正例:可分为两个查询。
select age,name from user where age <18;
select age,name from user where age >18;
使用!=和<>很可能会让索引失效
18. 使用联合索引时遵循最左匹配原则
例如联合索引 (userId, age)
,查询 userId
和 age
时优先使用 userId
。
表结构:(有一个联合索引idx_userid_age,userId在前,age在后)
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` int(11) NOT NULL,
`age` int(11) DEFAULT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_userid_age` (`userId`,`age`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
反例:
select * from user where age = 10;
正例://符合最左匹配原则
select * from user where userid=10 and age =10;
//符合最左匹配原则
select * from user where userid =10;
理由:当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。联合索引不满足最左原则,索引一般会失效,但是这个还跟Mysql优化器有关的。
19. 对 WHERE
和 ORDER BY
涉及的列建索引
反例:
SELECT * FROM user WHERE address = '深圳' ORDER BY age;
正例:
覆盖索引能够使得你的SQL语句不需要回表,仅仅访问索引就能够得到所有需要的数据,大大提高了查询效率。
ALTER TABLE user ADD INDEX idx_address_age (address, age);
20. 使用覆盖索引
正例:
SELECT id, name FROM user WHERE userid LIKE '123%';
21. 删除冗余索引
避免重复索引,节省资源。
反例:
KEY `idx_userId` (`userId`)
KEY `idx_userId_age` (`userId`,`age`)
正例:
//删除userId索引,因为组合索引(A,B)相当于创建了(A)和(A,B)索引
KEY `idx_userId_age` (`userId`,`age`)
理由:重复的索引需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能的。
22. 避免超过3个以上的表连接
23. 索引数不宜超过5个
索引不宜太多,一般5个以内。
索引并不是越多越好,索引虽然提高了查询的效率,但是也降低了插入和更新的效率。
insert或update时有可能会重建索引,所以建索引需要慎重考虑,视具体情况来定。
一个表的索引数最好不要超过5个,若太多需要考虑一些索引是否没有存在的必要。
24. 索引不适合建立在大量重复数据的字段上
如性别字段,重复数据多时优化器可能放弃索引。
25. 字符串类型字段在 WHERE
中使用引号
反例:
SELECT * FROM user WHERE userid = 123;
正例:
SELECT * FROM user WHERE userid = '123';
26. 避免返回过多数据量
反例:
SELECT * FROM LivingInfo WHERE watchId = userId AND watchTime >= DATE_SUB(NOW(), INTERVAL 1 YEAR);
正例:
-- 分页查询
SELECT * FROM LivingInfo WHERE watchId = userId AND watchTime >= DATE_SUB(NOW(), INTERVAL 1 YEAR) LIMIT offset, pageSize;
理由:
查询效率:当返回的数据量过大时,查询所需的时间会显著增加,导致数据库性能下降。
通过限制返回的数据量,可以缩短查询时间,提高数据库响应速度。
网络传输:大量数据的传输会占用网络带宽,可能导致网络拥堵和延迟。
减少返回的数据量可以降低网络传输的负担,提高数据传输效率。。
在 SQL 优化方面,除了已经列举的26个技巧,这里再补充9个技巧,使优化点达到35条。这些补充技巧包含一些更加细化的实践,帮助进一步提升 SQL 查询的效率:
27. 合理利用视图(View)进行复杂查询
如果一个复杂查询需要频繁使用,可以考虑创建视图,以简化查询结构并提高查询效率。
正例:
CREATE VIEW view_user_orders AS
SELECT u.id, u.name, o.order_id, o.amount
FROM user u JOIN orders o ON u.id = o.user_id;
-- 使用视图查询
SELECT * FROM view_user_orders WHERE amount > 100;
28. 使用表分区(Partitioning)优化大表性能
对于数据量较大的表,通过分区可以有效提升查询效率。表分区可以按日期、数值范围等方式进行分割。
正例:
CREATE TABLE sales (
sale_id INT,
sale_date DATE,
amount DECIMAL(10,2)
) PARTITION BY RANGE (YEAR(sale_date)) (
PARTITION p2019 VALUES LESS THAN (2020),
PARTITION p2020 VALUES LESS THAN (2021),
PARTITION p2021 VALUES LESS THAN (2022)
);
29. 合理使用存储过程(Stored Procedure)来减少多次 SQL 交互
将多步操作写入存储过程,可以减少客户端与数据库之间的多次交互,提高效率。
正例:
CREATE PROCEDURE update_and_select(IN user_id INT)
BEGIN
UPDATE users SET last_login = NOW() WHERE id = user_id;
SELECT * FROM users WHERE id = user_id;
END;
30. 对频繁变更的数据使用缓存
对于频繁查询的静态或相对稳定的数据,可考虑将查询结果存放到缓存(如 Redis)中,以减轻数据库的负担。
31. 使用适当的隔离级别
在高并发环境中选择适当的事务隔离级别(如 READ COMMITTED),可以避免不必要的锁竞争和阻塞,提升并发效率。
32. 使用合适的数据类型
选择合适的数据类型会节省存储空间,提升处理速度。例如:TINYINT
(1字节)代替INT
(4字节),VARCHAR(50)
代替CHAR(50)
,存储长度尽可能精确匹配业务需求。
33. 避免频繁更新索引列
在高并发写操作的场景中,频繁更新索引字段会导致索引重建,影响性能。如果字段变动频繁且无查询需求,建议避免对该字段建立索引。
34. 避免在事务中执行非必要的操作
在事务中应避免执行耗时操作,比如网络请求或复杂计算,以减少锁的持有时间。优先确保事务操作集中在必要的数据变更上。
35. 使用批量更新或删除
对于批量更新或删除数据,避免一次性操作大量记录。可以分批次执行,以减少锁定时间,减轻系统压力。
正例:
-- 分批删除
DELETE FROM orders WHERE status = 'obsolete' LIMIT 1000;
这些 SQL 优化技巧涵盖了数据库的方方面面。希望这些补充的优化建议可以帮助进一步提升数据库的整体性能和稳定性。
该文章在 2024/12/12 10:25:59 编辑过