如果查询扫描大量数据只返回少数的行,可以尝试以下技巧:
MySQL客户端和服务端之间的通信协议是半双工的。即任何时刻,要么服务器向客户端发生数据,要么客户端向服务器发送数据,这两个动作不能同时发生。这种通信协议让MySQL通信简单快捷,但是缺点就是没法进行流量控制。一旦一端开始发送消息,另一端必须接收完整消息才能响应它。这也是为什么查询语句很长时,需要参数max_allowed_packet的原因,一旦客户端发送了请求,他能做的事情就只有等待响应了。服务器响应给客户端同样如此,客户端必须完整接收整个结果,不能只接收前几条,然后让服务器停止发送数据,且MySQL通常需要等所有数据发送完成才会释放查询所占用的资源。这也是为什么在必要的实际需要加LIMIT的原因。 因此查询看似是客户端去服务器拉取数据的过程,实质上却是客户端和服务端互相推送数据的过程。
查询缓存打开的情况下,MySQL会优先检查缓存,这个检查是通过一个大小写敏感的哈希查询实现的。如果命中了缓存,MySQL会在权限检查之后返回缓存中的结果。这种情况下,查询不会被解析,不会生成执行计划,也不会执行。
缓存如果没命中,查询的下一步就是生成执行计划,然后再根据这个执行计划和存储引擎交互。这个过程包对SQL进行解析,验证语法和关键字是否错误,表和列是否存在。选择“最优”的执行计划。一个SQL会有多种执行方式,优化器的作用就是找到“最优”的那个执行计划。为什么最优打引号呢?这是因为优化器用读取数据页的数量来衡量最优程度。优化器依赖存储引擎的统计信息,但是存储引擎提供的信息可能不是精确的,优化器不会考虑执行时间,只会考虑如何读取最小的数据页,优化器不会考虑数据页是否是顺序扫描,也不会考虑数据页是否已经缓存在内存,已经不需要磁盘I/O可,同时优化器不会考虑并发…等等限制。这些限制使得优化器选择出来的只是其认为的最优,也许不是实际执行的最优。但是不要认为自己比优化器更加聪明,也许你会占点便宜,但是更有可能使得查询变的复杂,难以维护性能更差。因此让优化器按照自己的工作方式就行。对执行计划的优化包括静态优化和动态优化。可以理解静态优化是编译时优化,比如将where条件转换成等价形式。动态优化可以理解为运行时优化,在执行过程中不断修正。
有这样一个SQL:
SELECT * FROM TABLE_A
WHERE COLUMN_A IN(
SELECT COLUMN_B1 FROM TABLE_B WHERE COLUMN_B2 = 1);
假设SELECT COLUMN_B1 FROM TABLE_B WHERE COLUMN_B2 = 1
的结果是 X,Y,Z
。你是否认为执行方式是这样的:
SELECT * FROM TABLE_A WHERE COLUMN_A IN(X,Y,Z);
然而事实上执行方式上却是:
SELECT * FROM TABLE_A
WHERE EXISTS (
SELECT * FROM TABLE_B WHERE COLUMN_B2 = 1
AND TABLE_A.COLUMN_A = TABLE_B.COLUMN_B1);
这是一个联表查询。此时我们可以改写SQL:
SELECT * FROM TABLE_A
WHERE COLUMN_A IN(
SELECT GROUP_CONCAT(COLUMN_B1) FROM TABLE_B WHERE COLUMN_B2 = 1);
使用GROUP_CONCAT()函数在IN()中构造一个由逗号分隔成的列表让查询回到我们预想的那样,或者使用INNER JOIN关联查询,也能得到意想不到的优化。
但是所有的关联子查询性能都很差吗?其实不然,他在某些场景会比IN()更优。因此如果你的项目允许使用关联子查询,那么你需要自己测试原生的关联子查询策略和改造使用IN()的策略到底哪个性能更好,而不是盲目的采用某个方式。
如果使用LIMIT只获取UNION语句的部分结果集。那么可以在各个子句中分别使用LIMIT减少临时表的大小。
如
(SELECT COLUMN_A, COLUMN_B
FROM TABLE_A
ORDER BY COLUMN_C)
UNION ALL
(SELECT COLUMN_A, COLUMN_B
FROM TABLE_B
ORDER BY COLUMN_C)
LIMIT 20;
如果A表有100行,B表有200行,那么临时表就会有300行,此时可以这样写
(SELECT COLUMN_A, COLUMN_B
FROM TABLE_A
ORDER BY COLUMN_C
LIMIT 20)
UNION ALL
(SELECT COLUMN_A, COLUMN_B
FROM TABLE_B
ORDER BY COLUMN_C
LIMIT 20)
LIMIT 20;
这样临时表就只有40行了。不过要注意从临时表取出的顺序是不一定的,如果想要正确的顺序,还有需要全局的ORDER BY 和LIMIT操作。
MySQL无法利用多核特性并行执行。因此也不用花时间去尝试寻找并行执行查询的方法
MySQL禁止一个SQL对同一张表同时进行查询和更新:
UPDATE TABLE_A AS OUT_TBL SET cnt = (SELECT COUNT(*) FROM TABLE_A AS INNER_TBL WHERE INNER_TBL.COLUMN_A = OUT_TBL .COLUMN_A);
这个SQL语法上正确,但是执行时就会报错。可以通过生成临时表的方式绕过这个限制:
UPDATE TABLE_A AS OUT_TBL INNER JOIN (SELECT COUNT(*) as cnt, COLUMN_A FROM TABLE_A AS INNER_TBL) USING(COLUMN_A) AS DER SET OUT_TBL.cnt = DER.cnt;
COUNT()用来统计某个列中非NULL值的数目。如果MySQL确定COUNT()括号内的列不为空,那么就会去统计行数。COUNT(*)和COUNT(ID)是一样的,并不存在什么COUNT(*)会扩展成所有列的情况。
如果总数固定,如26个字母。那么如果SIGN有索引,查询条件是 (SIGN > B),那么我们就需要扫描24行,但是我们如果将查询条件改成SIGN < B,那么仅需扫描1行。此时可以用总数26-1-1得到大于B的结果行数。
如果业务场景要求不那么精确的COUNT值,如网站访问人数,此时可以用近似值代替。使用EXPLAIN来修饰查询SQL,得到的结果中的rows列就是一个行数的预估值。EXPLAIN使得SQL不会真正的执行查询,成本很低。
对于一些特殊的场景,可以建立一个汇总表来维护数量。
优化子查询最好的方式就是用关联查询代替。但是并不是绝对的,上面已经说过,子查询并不总是性能很差。因此需要自己去测试,评估。
当偏移量较大时,就需要扫描大量的数据。如LIMIT 1000,20,虽然只返回了20行数据,但是实质上却扫描了1020行的数据。一个好的方法是偏移量较大时使用索引覆盖扫描:先使用覆盖索引获得目标列队索引键值,然后再通过索引值去查询具体的内容:在条件列加索引,去扫描ID,然后再根据ID查询行。如果是无条件的全表LIMIT查ID,那么MySQL会自己选择一个索引作为依据,如果没有其他索引,那么会选择主键。总之肯定比全表扫描快。
如果排序列本身就是有序的,且你是顺序读取,那么在读取一页之后,可以将极限值记下来,在下次的查询时使用WHERE限定不进行全表扫描。
在分页上,可以不设置指定页跳转,仅设置上一页,下一页。如果每页展示20条数据,那么可以获取21条数据,如果有21条数据那么就有下一页,否则就每页下一页,避免空查询。
页面总数上,如果不要求绝对精确,也可以使用上面COUNT()的技巧,获得模糊的总行数,然后计算总页数。
除非必须消除重复行,否则使用UNION ALL而不是 UNION。如果没有ALL,MySQL会给临时表加上DISTINCT选项,这回导致对整个临时表做唯一性检查,这样的代价非常的高。无论有没有ALL,使用UNION,MySQL总是现将结果放入临时表,然后再读出,再返回客户端。虽然很多时间这样做没有必要。
delete删除的时候是一条一条的删除记录,他配合事务,可以将删除的数据找回。
truncate删除,他是将整个表摧毁,然后创建一张一摸一样的表,他删除的数据无法找回。
还有一点,delete自增主键uid不会重置,truncate的话uid则会重置为初始值。
参考资料:《高性能MySQL》
PS:
【JAVA核心知识】系列导航 [持续更新中…]
关联导航:MySQL架构基础
关联导航:MySQL数据类型选择与设计
关联导航:创建高性能的索引
关联导航:EXPLAIN的使用
欢迎关注…