概述
上文我们通过explain分析大概了解了怎么去做分析SQL,本文将具体的深入Mysql SQL优化的相关方案。
初始化脚本
通过如下脚本,在mysql数据库中新建对应的实验表和数据。
CREATE TABLE `user_info` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL DEFAULT '',
`age` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name_index` (`name`)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8;
INSERT INTO user_info (NAME, age) VALUES ('xys', 20);
INSERT INTO user_info (NAME, age) VALUES ('a', 21);
INSERT INTO user_info (NAME, age) VALUES ('b', 23);
INSERT INTO user_info (NAME, age) VALUES ('c', 50);
INSERT INTO user_info (NAME, age) VALUES ('d', 15);
INSERT INTO user_info (NAME, age) VALUES ('e', 20);
INSERT INTO user_info (NAME, age) VALUES ('f', 21);
INSERT INTO user_info (NAME, age) VALUES ('g', 23);
INSERT INTO user_info (NAME, age) VALUES ('h', 50);
INSERT INTO user_info (NAME, age) VALUES ('i', 15);
CREATE TABLE `order_info` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(20) DEFAULT NULL,
`product_name` VARCHAR(50) NOT NULL DEFAULT '',
`productor` VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_product_detail_index` (`user_id`, `product_name`, `productor`)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8;
INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p1', 'WHH');
INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p2', 'WL');
INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p1', 'DX');
INSERT INTO order_info (user_id, product_name, productor) VALUES (2, 'p1', 'WHH');
INSERT INTO order_info (user_id, product_name, productor) VALUES (2, 'p5', 'WL');
INSERT INTO order_info (user_id, product_name, productor) VALUES (3, 'p3', 'MA');
INSERT INTO order_info (user_id, product_name, productor) VALUES (4, 'p1', 'WHH');
INSERT INTO order_info (user_id, product_name, productor) VALUES (6, 'p1', 'WHH');
INSERT INTO order_info (user_id, product_name, productor) VALUES (9, 'p8', 'TE');
INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p199', 'WHH99');
-- 验证分区
CREATE TABLE user_temp (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20),
PRIMARY KEY (id)
)ENGINE = INNODB PARTITION BY KEY (id) PARTITIONS 3;
INSERT INTO user_temp VALUES(1,"hello") ;
INSERT INTO user_temp VALUES(2,"world") ;
INSERT INTO user_temp VALUES(3,"nice") ;
DROP PROCEDURE IF EXISTS batchInsert;
-- 批量插入数据
DELIMITER //
CREATE PROCEDURE batchInsert()
BEGIN
DECLARE num INT;
SET num=20;
WHILE num<=100000 DO
INSERT INTO user_info (id, NAME, age) VALUES (num ,CONCAT('xys',num) , 20);
INSERT INTO order_info (user_id, product_name, productor) VALUES (num, CONCAT('p',num) , CONCAT('WHH',num));
SET num=num+1;
END WHILE;
END
//
DELIMITER ; #恢复;表示结束
CALL batchInsert;
SELECT COUNT(1) FROM `user_info`;
SELECT COUNT(1) FROM `order_info`;
-- user_info增加一列
ALTER TABLE user_info ADD COLUMN `oid` BIGINT(20);
UPDATE user_info u , order_info o SET u.oid = o.user_id WHERE o.user_id = u.id ;
ALTER TABLE order_info ADD COLUMN `description` VARCHAR(255) ;
Mysql SQL优化方案
总体方向:
- 是否用到索引且正确使用了索引
- 尽量避免使用函数
- 尽量避免对大表全表扫描
- 定期对表ANALYZE TABLE
- 了解特定于每个表的存储引擎的调优技术、索引技术和配置参数。
- 优化 InnoDB “只读事务”中的技术优化
- 尽量避免优化器自动执行转换
- 调整 MySQL 用于缓存的内存区域的大小和属性。 通过有效使用 InnoDB 缓冲池、MyISAM 键缓存和 MySQL 查询缓存,重复查询运行得更快
- 对于使用高速缓存区域快速运行的查询,您仍然可以进一步优化,以便它们需要更少的高速缓存,从而使您的应用程序更具可扩展性。 可扩展性意味着您的应用程序可以处理更多并发用户、更大的请求等,而不会导致性能大幅下降。
- 处理锁问题,其中查询速度可能会受到同时访问表的其他会话的影响。
细分方向:
-
- 查询每个表索引,并使用最佳索引,除非优化器认为使用表扫描更有效。 有一次,根据最佳索引是否跨越表的 30% 来使用扫描,但固定百分比不再决定使用索引还是扫描之间的选择。 优化器现在更加复杂,它的估计基于其他因素,例如表大小、行数和 I/O 块大小。
- 在某些情况下,MySQL 可以从索引中读取行,甚至无需查阅数据文件。 如果索引中使用的所有列都是数字,则仅使用索引树来解析查询。
- 没有 WHERE 的单个表上的 COUNT(*) 直接从 MyISAM 和 MEMORY 表的表信息中检索。
-
范围条件提取算法可以处理任意深度的嵌套 AND/OR 构造,其输出不依赖于条件在 WHERE 子句中出现的顺序。
image-20210830101755466.png-- Range Optimization Btree INDEX EXPLAIN SELECT * FROM order_info WHERE product_name LIKE 'x%' AND user_id=1 AND productor LIKE 'p%';
对于 BTREE 索引,间隔可以用于与 AND 组合的条件,其中每个条件使用 = ,< = > ,IS NULL,> ,< ,> = ,< = ,将关键部分与常量值进行比较!= 、 < > 、 BETWEEN 或 LIKE‘ pattern’(其中‘ pattern’不以通配符开头)。只要可以确定包含与条件匹配的所有行的单个键元组(如果 < > 或!) ,就可以使用间隔!= 使用)。
image-20210830102338648.png-- Range Optimization Btree INDEX 调整下productor,发现key_len变短了,索引只匹配部分了。 EXPLAIN SELECT * FROM order_info WHERE product_name LIKE 'x%' AND user_id=1 AND productor LIKE '%p%';
范围访问内存限制,range_optimizer_max_mem_size当值大于0时,优化器将跟踪在考虑范围访问方法时消耗的内存。如果即将超过指定的限制,则放弃范围访问方法,并考虑使用其他方法,包括全表扫描。
image-20210830104635131.pngSET SESSION range_optimizer_max_mem_size = 1024; EXPLAIN SELECT * FROM order_info WHERE product_name LIKE 'x%' AND user_id=1 AND productor LIKE 'p%'; SET SESSION range_optimizer_max_mem_size = 8388608; EXPLAIN SELECT * FROM order_info WHERE product_name LIKE 'x%' AND user_id=1 AND productor LIKE 'p%';
-
对多个索引分别进行条件扫描,然后将它们各自的结果进行合并(intersect/union)。这种访问方法只合并来自单个表的索引扫描,而不是跨多个表进行扫描。合并可以产生它的基础扫描的并集、交集或交集。
image-20210830113520250.png-- Index Merge Optimization EXPLAIN SELECT * FROM order_info WHERE user_id > 100000 OR id > 100000;
index merge使得我们可以使用到多个索引同时进行扫描,然后将结果进行合并。听起来好像是很好的功能,但是如果出现了 index intersect merge,那么一般同时也意味着我们的索引建立得不太合理,因为 index intersect merge 是可以通过建立 复合索引进行更一步优化的。
-
Index Condition Pushdown Optimization
Mysql默认开启ICP 。当关闭ICP时,index 仅仅是data access 的一种访问方式,存储引擎通过索引回表获取的数据会传递到MySQL Server 层进行where条件过滤。当打开ICP时,如果部分where条件能使用索引中的字段,MySQL Server 会把这部分下推到引擎层,可以利用index过滤的where条件在存储引擎层进行数据过滤,而非将所有通过index access的结果传递到MySQL server层进行where过滤. 优化效果:ICP能减少引擎层访问基表的次数和MySQL Server 访问存储引擎的次数, 减少io次数,提高查询语句性能。
image-20210830143040833.png image-20210830143058041.png-- Index Condition Pushdown Optimization SELECT @@optimizer_switch; SET optimizer_switch='index_condition_pushdown=on'; SET profiling = 1; SELECT * FROM order_info WHERE user_id=1 AND productor LIKE '%p%' ; SET profiling = 0; SHOW PROFILES; SHOW PROFILE CPU,block IO FOR QUERY 5; SET optimizer_switch='index_condition_pushdown=off'; SET profiling = 1; SELECT * FROM order_info WHERE user_id=1 AND productor LIKE '%p%' ; SET profiling = 0; SHOW PROFILES; SHOW PROFILE CPU,block IO FOR QUERY 6;
-
Nested-Loop Join 和 Block Nested-Loop and Batched Key Access Joins
Nested-loop join (NLJ) :一个简单的嵌套循环联接(NLJ)算法一次一个读取循环中第一个表中的行,将每一行传递给一个嵌套循环,该循环处理联接中的下一个表。只要存在要联接的表,这个过程就会重复多次。
Block Nested-Loop (BNL) :连接算法使用外部循环中读取的行的缓冲,以减少内部循环中的表必须读取的次数。例如,如果将10行读入缓冲区,并将缓冲区传递给下一个内部循环,则内部循环中读取的每一行都可以与缓冲区中的所有10行进行比较。这样可以减少内部表必须读取的次数一个数量级。
Join_buffe_size系统变量确定用于处理查询的每个连接缓冲区的大小, 连接缓冲区在执行连接之前分配,在查询完成后释放。
当连接类型为
ALL
orindex
orrange
时,可以使用连接缓冲。缓冲的使用也适用于外部连接。Batched Key Access Joins (BKA):对于多表join语句,当MySQL使用索引访问第二个join表的时候,使用一个join buffer来收集第一个操作对象生成的相关列值。BKA构建好key后,批量传给引擎层做索引查找。key是通过MRR接口提交给引擎的,这样,MRR使得查询更有效率。如果外部表扫描的是主键,那么表中的记录访问都是比较有序的,但是如果联接的列是非主键索引,那么对于表中记录的访问可能就是非常离散的。因此对于非主键索引的联接,Batched Key Access Join算法将能极大提高SQL的执行效率。BKA算法支持内连接,外连接和半连接操作,包括嵌套外连接。
索引join 原理
image-20210830165537690.png-- Batched Key Access SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'; EXPLAIN SELECT u.* FROM user_info u, order_info o WHERE o.user_id = u.oid ; SET optimizer_switch='mrr_cost_based=on';
-
在辅助索引上使用范围扫描读取行时,如果表很大并且没有存储在存储引擎的缓存中,则会导致对基表进行许多随机磁盘访问。通过磁盘扫描多范围读(MRR)优化,MySQL 试图通过仅扫描索引并收集相关行的键来减少对范围扫描的随机磁盘访问次数。然后对键进行排序。磁盘扫描 MRR 的动机是减少随机磁盘访问的次数,而不是实现对基表数据更多的顺序扫描。当使用 MRR 时,EXPLAIN 输出中的 Extra 列显示 Using MRR。
image-20210830155532178.png image-20210830160724536.png-- Multi-Range Read Optimization SELECT @@optimizer_switch; ALTER TABLE user_info ADD INDEX idx_age(age); SET optimizer_switch='mrr_cost_based=off'; SET optimizer_switch='mrr=on'; SET profiling = 1; EXPLAIN SELECT * FROM user_info WHERE age BETWEEN 1 AND 2 ; SET profiling = 0; SET optimizer_switch='mrr=off'; SET profiling = 1; EXPLAIN SELECT * FROM user_info WHERE age BETWEEN 1 AND 2 ; SET profiling = 0; SHOW PROFILES; select @@read_rnd_buffer_size;
默认只有在优化器认为 MRR 可以带来优化的情况下才会走 MRR,如果你想不管什么时候能走 MRR 的都走 MRR 的话,你要把 mrr_cost_based 设置为 off,不过最好不要这么干,因为这确实是一个坑,MRR 不一定什么时候都好,全表扫描有时候会更加快.
image-20210830160741168.pngMRR 要把主键排个序,这样之后对磁盘的操作就是由顺序读代替之前的随机读。从资源的使用情况上来看就是让 CPU 和内存多做点事,来换磁盘的顺序读。然而排序是需要内存的,这块内存的大小就由参数 read_rnd_buffer_size 来控制.
image-20210830161510453.png -
可以使用使用索引来优化排序的效率,有些情况下,索引来优化排序是没有效果的,可以参考官网这些条件Use of Indexes to Satisfy ORDER BY 。特别注意:在一条SQL里,对于一张表的查询 一次只能使用一个索引(注:排除发生index merge的可能性),也就是说当WHERE子句与ORDER BY子句要使用的索引不一致时,MySQL只能使用其中一个索引(B+树) 。
B+树.png
如果索引不能用于满足 orderby 子句,则 MySQL 执行filesort,读取表行并对它们进行排序。为了获得用于filesort的内存,优化器在前面分配固定数量的 sort_ buffer_ size 。如果结果集太大而无法放入内存,则文件分类操作根据需要使用临时磁盘文件。filesort是否会使用磁盘取决于它操作的数据量大小,总结来说就是,
Filesort按排序方式来划分 , 分为两种:
- 数据量小时,在内存中快排。
- 数据量大时,在内存中分块快排,再在磁盘上将各个块做归并。
根据回表查询的次数,filesort又可以分为两种方式:
- 回表读取两次数据(two-pass):两次传输排序。
- 回表读取一次数据(single-pass):单次传输排序。
两次传输排序会进行两次回表操作:第一次回表用于在WHERE子句中筛选出满足条件的rowid以及rowid对应的ORDER BY的列值;第二次回表发生在ORDER BY子句对指定列进行排序之后,通过rowid回表查出SELECT子句需要的字段信息。
单次传输排序的弊端在于会将所有涉及到的列都放入排序缓冲区,排序缓冲区一次能放下的tuples更少了,进行归并排序的概率增大。列数据量越大,需要的归并路数更多,增加了额外的I/O开销。所以列数据量太大时,单次传输排序的效率可能还不如两次传输排序。MySQL的filesort会尽可能使用单次传输排序,但是为了防止上述情况发生,MySQL做了以下限制:
- 所有需要的列或ORDER BY的列只要是BLOB或者TEXT类型,则使用两次传输排序。
- 所有需要的列和ORDER BY的列总大小超过max_length_for_sort_data字节,则使用两次传输排序。
增加 read_rnd_buffer _ size 变量值,以便一次读取更多行。
image-20210830185459602.png-- Order by[ORDER BY Optimization] ALTER TABLE user_info DROP INDEX idx_oid; EXPLAIN SELECT * FROM user_info u WHERE u.oid < 1000 ORDER BY u.oid; -- 没有索引,走filesort ALTER TABLE user_info ADD INDEX idx_oid(oid); EXPLAIN SELECT * FROM user_info u WHERE u.oid < 1000 ORDER BY u.oid; EXPLAIN SELECT * FROM user_info u WHERE u.oid < 90000 ORDER BY u.oid; -- 结果集太大,即使有索引也没用,还是走filesort EXPLAIN SELECT * FROM user_info u WHERE u.oid < 90000 ORDER BY u.oid LIMIT 1000; -- 使用limit可以减少结果集从而不走filesort
-
满足 GROUP BY 子句的最常用方法是扫描整个表并创建一个新的临时表,其中每个组的所有行都是连续的,然后使用这个临时表发现组并应用聚合函数。在某些情况下,MySQL 可以做得更好,并通过使用索引访问避免创建临时表。但使用 GROUP BY 索引的最重要的前提条件是,所有 GROUP BY 列都引用来自同一索引的属性,并且索引按顺序存储其键(例如,对于 BTREE 索引是如此,但对于 HASH 索引则不是这样)。根据我的经验, MySQL GROUP BY 并不总是做出正确选择的地方。您可能需要使用 FORCE INDEX 以您希望的方式执行查询。以下两种使用索引情况:
如果Loose Index Scan适用于查询,则在 Extra 列中的 EXPLAIN 输出显示 Using Index for group-by。
Group by在无法使用loose index scan,还可以选择tight,若两者都不可选,则只能借助临时表;扫描索引时,须读取所有满足条件的索引键,要么是全索引扫描,要么是范围索引扫描;
此外,若没法使用索引,我们可以使用SQL_BIG_RESULT优化,一般来说,MySQL 只有在我们拥有大量组时才更喜欢使用这个SQL_BIG_RESULT计划,因为在这种情况下,排序比拥有临时表更有效。
image-20210831104819931.png-- GROUP BY Optimization ALTER TABLE user_info DROP INDEX idx_age; EXPLAIN SELECT age FROM user_info GROUP BY age; -- Using temporary; Using filesort EXPLAIN SELECT SQL_BIG_RESULT age FROM user_info GROUP BY age; -- Using filesort ALTER TABLE user_info ADD INDEX idx_age(age); EXPLAIN SELECT age FROM user_info GROUP BY age; -- Using index for group-by EXPLAIN SELECT age ,MAX(ID) FROM user_info GROUP BY age; -- Using index for group-by EXPLAIN SELECT age ,COUNT(ID) FROM user_info GROUP BY age; -- Using index
-
在大多数情况下,DISTINCT 子句可以被视为 groupby 的特殊情况。由于这种等价性,适用于 groupby 查询的优化还可以应用于带 DISTINCT 子句的查询。
image-20210831110512132.png-- DISTINCT Optimization EXPLAIN SELECT DISTINCT age FROM user_info;
-
如果你order by和limit一起使用,那么mysql在排序结果中找到最初的row_count行之后就会完成这条语句,而不是对整个结果集进行排序。如果使用了索引排序,它就非常快地完成。但如果在order by语句中返回的结果集有很多行,那么非排序的列的返回结果是随机的。
image-20210831133837737.png-- LIMIT Query Optimization EXPLAIN SELECT user_id FROM order_info LIMIT 10; -- Using index EXPLAIN SELECT * FROM order_info LIMIT 10; -- ALL SELECT * FROM order_info ORDER BY user_id; SELECT * FROM order_info ORDER BY user_id LIMIT 10; -- 如果在order by语句中返回的结果集有很多行,那么非排序的列的返回结果是随机的。 SELECT * FROM order_info ORDER BY user_id ,id LIMIT 10; -- 保证排序正确
-
Function Call Optimization 和 Row Constructor Expression Optimization
MySQL 函数在内部被标记为确定性或不确定性。如果给定函数参数的固定值,对于不同的调用,函数可以返回不同的结果,则该函数是不确定的。非确定性函数的示例: RAND () ,UUID ()。不确定性函数可能会影响查询性能。例如,某些优化可能不可用。
其次,请避免将行构造函数与 AND/OR 表达式混合使用。
image-20210831142350445.png image-20210831142358466.png-- Row Constructor Expression Optimization EXPLAIN SELECT * FROM order_info WHERE user_id=1 AND (productor,product_name) > ('p','x'); EXPLAIN SELECT * FROM order_info WHERE user_id=1 AND productor > 'p' AND product_name > 'x';
-
使用 ANALYZE TABLE tbl_name以避免优化器错误地选择表扫描。
对扫描的表使用 FORCE INDEX 来告诉 MySQL 强制是很使用索引。
-
Optimizing Subqueries, Derived Tables, and View References
对于 IN (或 = ANY)子查询,优化器使用半连接策略来改进子查询的执行。优化器可以识别出 IN 子句要求子查询从表中只返回一个实例。在这种情况下,查询可以使用半连接(semijoin)。在 MySQL 中,子查询必须满足以下条件才能作为半连接处理,这些条件参考这里。半连接处理策略做出基于成本的选择:
- Duplicate Weedout:将半连接视为连接运行,并使用临时表删除重复记录。explain分析时,Extra 列中的 Start Temporary 和 End Temporary 表明了 Duplicate Weedout 的临时表用法。
- FirstMatch: 当扫描内部表中的行组合时,如果给定值组有多个实例,请选择一个实例,而不是全部返回. explain分析时,Extra 列中的 FirstMatch (tbl _ name)表示连接捷径。
- LooseScan: 使用索引扫描子查询表,该索引允许从每个子查询的值组中选择单个值. explain分析时,Extra 列中的 LooseScan (m. . n)表示使用了 LooseScan 策略
- materialization : 将子查询实体化为一个索引临时表,该临时表用于执行连接,其中索引用于删除重复项。在将临时表与外部表连接时,该索引还可以在稍后用于查找; 否则,将扫描该表。explain分析时,select_type为MATERIALIZED ,table为subquery
N
时使用了materialization。
image-20210831171050838.png-- Optimizing Subqueries, Derived Tables, and View References SELECT @@optimizer_switch; -- materialization=on EXPLAIN SELECT o.id FROM order_info o WHERE o.user_id IN (SELECT oid FROM user_info WHERE oid <1 ) ; -- Using where; Using index EXPLAIN SELECT o.id FROM order_info o WHERE o.user_id IN (SELECT oid FROM user_info WHERE oid <10 ) ; -- Using where; Using index; LooseScan EXPLAIN SELECT o.id FROM order_info o WHERE o.user_id IN (SELECT oid FROM user_info WHERE oid <100 ) ; -- MATERIALIZED
对于 NOT IN (或 < > ALL)子查询,优化器有以下策略: Materialization、
EXISTS
strategy。
image-20210831170859489.png image-20210831171009924.pngSET optimizer_switch='materialization=off'; EXPLAIN SELECT o.id FROM order_info o WHERE o.user_id NOT IN (SELECT oid FROM user_info WHERE oid <100 ) ; SET optimizer_switch='materialization=on'; EXPLAIN SELECT o.id FROM order_info o WHERE o.user_id NOT IN (SELECT oid FROM user_info WHERE oid <100 ) ;
对于派生表,优化器有以下选项,将派生表合并到外部查询块中,将派生表显示为内部临时表。
-
Optimizing Data Change Statements
插入一行所需的时间由下列因素决定:连接、向服务器发送查询、解析查询、插入行、插入索引、关闭。当然还有打开表的开销,但对于每个并发运行的查询,打开表只需要一次,所以不考虑。
加速插入方案:
-
减少索引的数量。
-
如果要同时插入来自同一客户端的多行,请使用带有多个 VALUES 列表的 INSERT 语句一次插入多行,如果要将数据添加到非空表中,可以调优 bulk_insert_buffer_size 变量,使数据插入更快。
-
从文本文件加载表时,使用 loaddata。这通常比使用 INSERT 语句快20倍。
-
利用默认值可以减少了 MySQL 必须进行的解析,并提高了插入速度。
UPDATE 语句方案
-
更新语句像 SELECT 查询一样优化,并带有额外的写开销。写入的速度取决于更新的数据量和更新的索引数量。
-
延迟更新,然后在接下来的一行中进行多次更新。
优化 DELETE 语句
-
删除 MyISAM 表中单个行所需的时间与索引的数量成正比。要更快地删除行,可以通过增加key_buffer_ size 系统变量来增加键缓存的大小。
-
要从 MyISAM 表中删除所有行,truncate table要比delete快。
-
-
Optimizing Database Privileges
特权设置越复杂,应用于所有 SQL 语句的开销就越大。通过简化 GRANT 语句建立的特权,MySQL 可以减少客户端执行语句时的权限检查开销。
网友评论