【JAVA核心知识】32:查询性能优化 ---《高性能MySQL》读书笔记

查询性能优化

  • 1 优化数据访问
    • 1.1 是否向数据库请求了不需要的数据
    • 1.2 是否在扫描额外的记录
  • 2 重构查询的方式
  • 3 查询的过程
    • 3.1 通信协议
    • 3.2 查询缓存
    • 3.3 查询优化处理
  • 4 查询优化器的限制
    • 4.1 关联子查询
    • 4.2 UNION的行数限制
    • 4.3 并行执行
    • 4.4 禁止同一个表上的查询和更新
  • 5 优化特定类型的查询(一些小技巧)
    • 5.1 优化COUNT()查询
    • 5.2 优化关联查询
    • 5.3 优化子查询
    • 5.4 优化LIMIT及分页
    • 5.5 使用UNION ALL而不是 UNION
    • delete与truncate

1 优化数据访问

1.1 是否向数据库请求了不需要的数据

  • 查询了不需要的行
  • 总是取出所有的列
  • 多表关联时返回了全部列
  • 重复查询相同的数据

1.2 是否在扫描额外的记录

  • 扫描的行数和返回的行数
  • 扫描的行数和访问类型。访问类型从慢到块依次是:全表扫描,索引扫描,范围扫描,唯一索引扫描,常数引用

如果查询扫描大量数据只返回少数的行,可以尝试以下技巧:

  • 使用覆盖索引
  • 改变库表结构:如单独的汇总表
  • 重构查询

2 重构查询的方式

  1. 衡量采用一个复杂查询还是多个简单查询。有的时候将一个大查询分解为多个小查询是很有必要的。但是也并不是总是这样,这取决于分解查询会带来多大的好处,比如用10次独立查询返回10行数据,显然是十分糟糕的
  2. 切分查询,对一个大查询拆分处理。经典的例子是删除旧数据。一次性完成会锁住很多数据,占满日志,耗尽资源,阻塞其他查询。将一个大的DELETE切分成多个小的,就会变得高效且对服务器影响较小。
  3. 分解关联查询。将一个关联查询分解为多个单表查询语句。比如先查表A,获得符合条件的数据,然后用这个数据组装表B的查询语句。PS:事实上,大型的应用系统开发过程中,联表查询应该尽量避免 。

3 查询的过程

  1. 客户端发送一条查询给服务器
  2. 服务器先检查缓存,如果命中了缓存,则立即返回存储在缓存中的结果,否则进入下一阶段
  3. 服务器端进行SQL解析,预处理,再由优化器生成对应的执行计划
  4. MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询
  5. 将结果返回客户端。即使客户端不需要,也依然会返回结果。返回的过程是一个增量的,逐步返回的过程。服务器会在获得第一条结果时就逐步返回结果给客户端。这样服务器无需存储太多的结果,客户端也可以在第一时间获得结果。

3.1 通信协议

MySQL客户端和服务端之间的通信协议是半双工的。即任何时刻,要么服务器向客户端发生数据,要么客户端向服务器发送数据,这两个动作不能同时发生。这种通信协议让MySQL通信简单快捷,但是缺点就是没法进行流量控制。一旦一端开始发送消息,另一端必须接收完整消息才能响应它。这也是为什么查询语句很长时,需要参数max_allowed_packet的原因,一旦客户端发送了请求,他能做的事情就只有等待响应了。服务器响应给客户端同样如此,客户端必须完整接收整个结果,不能只接收前几条,然后让服务器停止发送数据,且MySQL通常需要等所有数据发送完成才会释放查询所占用的资源。这也是为什么在必要的实际需要加LIMIT的原因。 因此查询看似是客户端去服务器拉取数据的过程,实质上却是客户端和服务端互相推送数据的过程。

3.2 查询缓存

查询缓存打开的情况下,MySQL会优先检查缓存,这个检查是通过一个大小写敏感的哈希查询实现的。如果命中了缓存,MySQL会在权限检查之后返回缓存中的结果。这种情况下,查询不会被解析,不会生成执行计划,也不会执行。

3.3 查询优化处理

缓存如果没命中,查询的下一步就是生成执行计划,然后再根据这个执行计划和存储引擎交互。这个过程包对SQL进行解析,验证语法和关键字是否错误,表和列是否存在。选择“最优”的执行计划。一个SQL会有多种执行方式,优化器的作用就是找到“最优”的那个执行计划。为什么最优打引号呢?这是因为优化器用读取数据页的数量来衡量最优程度。优化器依赖存储引擎的统计信息,但是存储引擎提供的信息可能不是精确的,优化器不会考虑执行时间,只会考虑如何读取最小的数据页,优化器不会考虑数据页是否是顺序扫描,也不会考虑数据页是否已经缓存在内存,已经不需要磁盘I/O可,同时优化器不会考虑并发…等等限制。这些限制使得优化器选择出来的只是其认为的最优,也许不是实际执行的最优。但是不要认为自己比优化器更加聪明,也许你会占点便宜,但是更有可能使得查询变的复杂,难以维护性能更差。因此让优化器按照自己的工作方式就行。对执行计划的优化包括静态优化和动态优化。可以理解静态优化是编译时优化,比如将where条件转换成等价形式。动态优化可以理解为运行时优化,在执行过程中不断修正。

4 查询优化器的限制

4.1 关联子查询

有这样一个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()的策略到底哪个性能更好,而不是盲目的采用某个方式。

4.2 UNION的行数限制

如果使用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操作。

4.3 并行执行

MySQL无法利用多核特性并行执行。因此也不用花时间去尝试寻找并行执行查询的方法

4.4 禁止同一个表上的查询和更新

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;

5 优化特定类型的查询(一些小技巧)

5.1 优化COUNT()查询

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不会真正的执行查询,成本很低。
对于一些特殊的场景,可以建立一个汇总表来维护数量。

5.2 优化关联查询

  • 确保ON或者USING子句中的列有索引。在创建索引时需要考虑关联的顺序。如果表A和表B用列C关联。那么需要在次表建立索引,主表无需建立索引。如A left join B,以A表为主,此时可以理解为在A表拿一条数据,然后去B表找对应的数据,因此应该在次表B上建立索引,A表全表扫描,无需创建索引。相应的如果A right join B,那么此时A为次表,那么就应该在A表建立索引。如果是A inner join B,那么B表为主表,A表为次表。关联查询时不需要在主表创建索引,因为不会用到,额外的索引只会带来额外的负担
  • 确保GROUP BY和ORDER BY中的表达式只涉及到一个表的列。

5.3 优化子查询

优化子查询最好的方式就是用关联查询代替。但是并不是绝对的,上面已经说过,子查询并不总是性能很差。因此需要自己去测试,评估。

5.4 优化LIMIT及分页

当偏移量较大时,就需要扫描大量的数据。如LIMIT 1000,20,虽然只返回了20行数据,但是实质上却扫描了1020行的数据。一个好的方法是偏移量较大时使用索引覆盖扫描:先使用覆盖索引获得目标列队索引键值,然后再通过索引值去查询具体的内容:在条件列加索引,去扫描ID,然后再根据ID查询行。如果是无条件的全表LIMIT查ID,那么MySQL会自己选择一个索引作为依据,如果没有其他索引,那么会选择主键。总之肯定比全表扫描快。
如果排序列本身就是有序的,且你是顺序读取,那么在读取一页之后,可以将极限值记下来,在下次的查询时使用WHERE限定不进行全表扫描。
在分页上,可以不设置指定页跳转,仅设置上一页,下一页。如果每页展示20条数据,那么可以获取21条数据,如果有21条数据那么就有下一页,否则就每页下一页,避免空查询。
页面总数上,如果不要求绝对精确,也可以使用上面COUNT()的技巧,获得模糊的总行数,然后计算总页数。

5.5 使用UNION ALL而不是 UNION

除非必须消除重复行,否则使用UNION ALL而不是 UNION。如果没有ALL,MySQL会给临时表加上DISTINCT选项,这回导致对整个临时表做唯一性检查,这样的代价非常的高。无论有没有ALL,使用UNION,MySQL总是现将结果放入临时表,然后再读出,再返回客户端。虽然很多时间这样做没有必要。

delete与truncate

delete删除的时候是一条一条的删除记录,他配合事务,可以将删除的数据找回。
truncate删除,他是将整个表摧毁,然后创建一张一摸一样的表,他删除的数据无法找回。
还有一点,delete自增主键uid不会重置,truncate的话uid则会重置为初始值。

参考资料:《高性能MySQL》

PS:
【JAVA核心知识】系列导航 [持续更新中…]
关联导航:MySQL架构基础
关联导航:MySQL数据类型选择与设计
关联导航:创建高性能的索引
关联导航:EXPLAIN的使用
欢迎关注…

你可能感兴趣的:(JAVA核心知识,数据库,mysql,性能优化,数据库)