- 前情提要:
前面讲了一些场景下针对性的优化。
6.7.5 优化Limit分页
- 场景
偏移量非常大,数据量非常的时候。
例如Limit 10000,20.
工作原理:
Mysql查询10020条记录,然后返回最后20条。前面1W条都被丢弃。解决方案:
- 限制分页的数量;(不让查这么大偏移量)
- 使用覆盖索引优化性能。(延迟关联)
- 记录上次分页的id。(利用索引)
- 方案2(延迟关联)
其中方案2,使用覆盖索引优化的原理:
- 使用覆盖索引,返回目标行的id;
- 再使用关联查询,返回目标行的所有列。
方案2在偏移量很大的时候,性能优化明显。
- 方案3(记录上次分页id)
这个方案在多次顺序分页的时候性能很好。
比如上次分页查询到了id为10240的地方,记录这个值(可以在应用层),然后下次的查询就可以是:1
2
3select * from t
where id>10240
limit 20
6.7.6 SQL_CALC_FOUND_ROWS相关
使用limit的时候,如果加上hint:
1 | SQL_CALC_FOUND_ROWS |
就可以获得limit之前结果集的行数。
- 工作原理:
Mysql扫描整个结果集,获得行数。
如果不用这个hint,则Mysql只扫描limit的行数返回即可,因此这个hint的代价可能非常大。
使用这个hint的可能原因及相关优化:
- 想知道是否应该显示“下一页”的按钮;
解决方案:如果原来是limit 20,则改成limit 21即可,这样就知道需不需要“下一页”了。 - 应用层进行缓存;
- 使用Explain结果的行数估算。
6.7.7 优化UNION查询
UNION
去重后合并UNION ALL
不去重,直接合并Mysql执行UNION的原理:
创建临时表,UNION的过程逐步填充行。
优化方法:
将WHERE,limit,ORDER BY子句置入内部UNION的子句,以提前过滤。
6.7.9 用户自定义变量
- 示例:
1
2
3
4
5
6
7
8-- 定义:
set @one :=1;
set @min_actor := (select min(id) from actor); -- 赋值运算优先级很低,所以右边一般都要括号。
set @last_week := current_date-interval 1 week;
-- 使用:
select * from t
where col <= @last_week;
缺点:
- 使用自定义变量后,无法使用查询缓存;
- 表名,列名,Limit中无法使用;
- 仅在当前连接中有用;(如果持久化连接、连接池,则会发生数据竞态,出BUG)
- 不能定义变量的类型;
- 优化器可能将变量优化掉,导致结果不可预期;
- 赋值的顺序和时机不固定,导致结果不可预期;
- 使用未定义的变量不会产生任何语法错误。(编译期无法判断错误)出错几率很大。
- 优点:
优化排名语句
实现行号功能:
1 | set @rownum:=0; |
查询并且更新数据
原查询:(2条语句)
1 | update t set lastUpdated=now() |
改写后:(2条语句)
1 | update t set lastUpdated=now() |
统计更新和插入的数量
- 示例:第一句的返回值能知道更改了n1行;
1
2
3
4
5
6
7
8
9insert into t (c1,c2) values
(4,4)
,(2,1)
,(3,1)
on duplicate key update
c1=values(c1)
+( 0* (@x:= @x+1));
select @x;
第二句通过@x
统计到更新了n2行;
因此:
更新的行数=n2;
插入的行数=n1-n2。
6.8 案例学习
6.8.1 使用MYSQL构建一个队列表
- tips:
无论什么时候,避免Select .. For Update
这种用法。
原因:会造成事务阻塞。总是有更好的方法替代这个用法。
此外,对于索引列,它产生排他行锁;
对于非索引列,它产生排他表锁。
相关函数:
1 | select connection_id(); |
案例:
表unsent_emails:
- id
- status (未发送,已发送,正在发送)
- owner (正在处理的连接id,默认是0)
- ts
错误示例:
1 | Begin; |
select和update之间的间隙时间内,所有相同的查询会阻塞。(所有线程全都在竞争1个排他锁)
改进后:
1 | set autocommit=1; |
改进后,不必使用select ... for update
,避免了事务阻塞。
唯一需要注意的时,需要定期检查是否有线程挂了,需要把它原来抢注的任务重置为无人拥有。
此外,消息队列还是别用Mysql实现了,用MQ,redis之类的吧。XD
6.8.2 计算两点之间的距离
- Tip:
复杂地理信息计算建议使用PostgreSql.
案例:
某个点附近所有可以出租的房子。
(一定半径内的所有点)
(或社交网站中匹配附近的用户)
表Locations:
- id
- name
- lat: 经度
- lon:纬度
A点和B点之间的距离计算公式CAL
:
1 | Acos( |
原查询:
1 | select * from locations |
上述查询无法使用索引,且非常消耗CPU。
优化方法:降低精度;修改上述的椭圆计算公式,近似成立方体公式。
- 优化后:
1 | select * from locations |
上述查询可以利用索引(lat)或(lon),但由于都是范围查询,无法利用联合索引。
为了进一步优化范围查询,可以使用枚举优化(使用IN列表)。
由于lat和lon列其实不是离散值,可以手动创建离散列:
新增两列存储坐标的近似值Floor(),然后在查询中使用IN将所有点的整数值放在列表中:
- Lat_floor: floor(lat)
- Lon_floor: floor(lon)
- Key(Lon_floor,Lat_floor)
- 进一步优化:
1 | select * from locations |
上述优化可以利用索引(lat_floor,lon_floor),因此比前面更快。
- 精度恢复:
上述优化只是滤掉不适合的点,可以进一步使用原来的公式进行过滤。由于此时剩下的点不多,因此即使原来的公式很复杂,性能也能够接受。
6.8.3 用户自定义函数(UDF)
书里说的比较含糊,总之是写一个C/C++程序作为后台程序,接受某种网络通信协议。
(TODO,进一步搜索别人的实际案例。)
6.9 总结
查询优化的思路:
- 检查执行的时间消耗;
- 检查Schema设计;
- 检查索引设计;(三星系统)
- 检查Explain计划(是否利用到索引,范围索引转化为枚举)
- 其他的各种优化:
(1) limit优化;
(2) 覆盖索引+延迟关联;
(3) 复杂查询分解;
(4) 执行计划搜索提前终止;
(5) 排序优化(1次传输,2次传输),Order By的列提取到第一张表;
(6) UNION优化:手动下推谓词;
(7) Count优化:反向过滤, 利用MyISam的统计信息;
(8) Delayed优化:日志系统异步写入;
(9) 排他锁优化:避免使用Select ... for update
,尽早释放锁(先抢注,释放锁,后处理)。