MySQL 查询优化三则
原则
- 避免全表扫描
方法
- 在where及order by涉及的列上建立索引
- 避免在where子句中避免null值的判断,否则会导致进行全表扫描,例如
select id from t where num is null
可以在num上设置默认值0,确保num列没有null值,然后
select id from t where num = 0 - 尽量避免在where子句中使用!=或<>操作符,否则引擎将放弃索引而进行全表扫描
- 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10 union all select id from t where num=20 - in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3 - 下面的查询也将导致全表扫描:
select id from t where name like '%abc%'
若要提高效率,可以考虑全文检索 - 如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
select id from t wherenum=@num
可以改为强制查询使用索引:
select id from t with(index(索引名)) wherenum= @num - 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2 - 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where substring(name,1,3)='abc' --name以abc开头的id
select id from t where datediff(day,createdate,'2005-11-30')=0 --‘2005-11-30’生成的id

我们有分页查询订单详细信息的需求,订单表目前有300多万的数据,而且order表在orderNo列上有索引,传统的写法可能使用limit offset, pageSize的写法来实现,但是有一个问题,在offset非常大的时候,查询会很慢,因为会使Mysql扫描大量的无用行,然后再扔掉,例如下面这样的语句
select orderNo, state, payType, ctime
from `order`
order by orderNo limit 2000000, 20;
查询结果如下
+----------------------+-------+---------+---------------+
| orderNo | state | payType | ctime |
+----------------------+-------+---------+---------------+
| KC312049Dn0fwL6S67pr | 1 | 2 | 1477496232335 |
| KC312049dOcs7Vbhb0Xw | 1 | 2 | 1477495563416 |
| KC312049DSKeJDYoJj4l | 1 | 2 | 1477496110295 |
| KC312049Dxhcd8yDinjH | 1 | 2 | 1476836872392 |
| KC312049dZvq0FCFgIi4 | 1 | 2 | 1479480408504 |
| KC312049e5AnVZxQ9x2L | 1 | 2 | 1480523251937 |
| KC312049E5MDWi86KpmS | 1 | 2 | 1477495599973 |
| KC312049E7B55XdT0c1q | 1 | 2 | 1477495660296 |
| KC312049Et9RajZgYIHl | 1 | 2 | 1477495538095 |
| KC312049F7gry3ah4QUl | 1 | 2 | 1477495553585 |
| KC312049Ff2diSRvMcAN | 1 | 2 | 1477496484309 |
| KC312049FNLa2qRmrYh0 | 1 | 2 | 1475764142929 |
| KC312049FVHbVyu9peoO | 1 | 2 | 1477496221733 |
| KC312049g2AHmW0hUYbh | 1 | 2 | 1479480264818 |
| KC312049GbCPr2ZKN07d | 1 | 2 | 1477496403856 |
| KC312049gckVBMX77pot | 1 | 2 | 1477495852336 |
| KC312049GeLTHsQf7D6v | 1 | 2 | 1475764268969 |
| KC312049gfMyBax7ipwv | 1 | 2 | 1479480227645 |
| KC312049ggnHiJMtszL3 | 1 | 2 | 1477496584497 |
| KC312049GpaqqGnFGrjy | 1 | 2 | 1475764171923 |
+----------------------+-------+---------+---------------+
20 rows in set (13.26 sec)
结果用了13秒才查出来,性能上是无法接受的,我们可以优化一下
由于上面的问题最重要的原因是扫描了大量无用的行,所以我们就从这里入手,我们知道orderNo列是索引的,我们可以想过办法通过索引覆盖查询来查出必要的需要扫描的行,然后再去扫描实际的数据行
例如,可以改成下面这样
select orderNo, state, payType, ctime
from `order` inner join (
select orderNo
from `order`
order by orderNo limit 2000000, 20
) as o using(orderNo);
查询结果如下
+----------------------+-------+---------+---------------+
| orderNo | state | payType | ctime |
+----------------------+-------+---------+---------------+
| KC312049Dn0fwL6S67pr | 1 | 2 | 1477496232335 |
| KC312049dOcs7Vbhb0Xw | 1 | 2 | 1477495563416 |
| KC312049DSKeJDYoJj4l | 1 | 2 | 1477496110295 |
| KC312049Dxhcd8yDinjH | 1 | 2 | 1476836872392 |
| KC312049dZvq0FCFgIi4 | 1 | 2 | 1479480408504 |
| KC312049e5AnVZxQ9x2L | 1 | 2 | 1480523251937 |
| KC312049E5MDWi86KpmS | 1 | 2 | 1477495599973 |
| KC312049E7B55XdT0c1q | 1 | 2 | 1477495660296 |
| KC312049Et9RajZgYIHl | 1 | 2 | 1477495538095 |
| KC312049F7gry3ah4QUl | 1 | 2 | 1477495553585 |
| KC312049Ff2diSRvMcAN | 1 | 2 | 1477496484309 |
| KC312049FNLa2qRmrYh0 | 1 | 2 | 1475764142929 |
| KC312049FVHbVyu9peoO | 1 | 2 | 1477496221733 |
| KC312049g2AHmW0hUYbh | 1 | 2 | 1479480264818 |
| KC312049GbCPr2ZKN07d | 1 | 2 | 1477496403856 |
| KC312049gckVBMX77pot | 1 | 2 | 1477495852336 |
| KC312049GeLTHsQf7D6v | 1 | 2 | 1475764268969 |
| KC312049gfMyBax7ipwv | 1 | 2 | 1479480227645 |
| KC312049ggnHiJMtszL3 | 1 | 2 | 1477496584497 |
| KC312049GpaqqGnFGrjy | 1 | 2 | 1475764171923 |
+----------------------+-------+---------+---------------+
20 rows in set (0.54 sec)
通过上面的改写,原本13秒的查询只需要0.5秒便实现了,效果相当明显
这是因为inner join的表通过索引覆盖查询直接通过索引找到了需要返回的数据行,然后order表通过与这个派生表进行关联,只扫描20行数据就可以了
个人公众号,欢迎关注,不定期抽奖送书

创建一个测试的表
create table test_swap(x char(10), y char(10));
插入几条数据
insert into test_swap values('x1', 'y1'), ('x2', 'y2'), ('x3', null), (null, 'y4');
看一下现在表的样子
select * from test_swap;
输出
+------+------+
| x | y |
+------+------+
| x1 | y1 |
| x2 | y2 |
| x3 | NULL |
| NULL | y4 |
+------+------+
4 rows in set (0.00 sec)
执行交换语句
update test_swap set x=(@t:=x), x=y, y=@t;
再看一下交换后表的样子
select * from test_swap;
输出
+------+------+
| x | y |
+------+------+
| y1 | x1 |
| y2 | x2 |
| NULL | x3 |
| y4 | NULL |
+------+------+
4 rows in set (0.00 sec)
交换成功
参考
END