MySQL常用SQL语句优化

推荐阅读这篇博文,索引说的非常详细到位:http://blog.linezing.com/?p=798#nav-3-2

在数据库日常维护中,最常做的事情就是SQL语句优化,因为这个才是影响性能的最主要因素。当然还有其他方面的,对于InnoDB存储引擎表,上面的方式并不能提高导入数据的效率。可以有以下几种方式提高Innodb表的导入效率

(1)因为InnoDB类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率。

(2)在导入数据前执行 SET UNIQUE_CHECKS=0,关闭唯一性效验,在导入数据结束以后执行SET UNIQUE_CHECKS=1,恢复唯一性效验,可以提高导入效率。

(3)如果使用自动提交的方式,建议在导入前执行SET AUTOCOMMIT=0,关闭自动提交,导入结束后再执行SET AUTOCOMMIT=1,打开自动提交,也可以提高导入的效率。

(4)对于有外键约束的表,我们在导入数据之前也可以忽略外键检查,因为innodb的外键是即时检查的,所以对导入的每一行都会进行外键的检查。

set foreign_key_checks = 0;
load data ............
set foreign_key_checks = 1;

2.优化INSERT语句

(1)如果同时从同一客户端插入大量数据,应该尽量使用多个值的表的INSERT 语句,这种方式将大大减少客户端与数据库服务器之间的连接,关闭等消耗,使得效率比分开执行的单个INSERT语句快(大部分情况下,使用多个值表的INSERT语句能比单个INSERT语句快上好几倍),比如下面一次插入多行:

INSERT INTO VALUES ('yayun',23),('tom',26),('atlas',32),('david',25).......

(2)插入延迟。如果从不同客户端插入很多行,可以通过使用INSERT  DELAYED语句得到更高的速度。DELAYED的意思是让INSERT语句马上执行,其实数据都被放在内存的队列中,并没有真正写入磁盘,这比每条语句分别插入要快的多;LOW_PRIORITY刚好相反,在所有其他用户对表的读写完成后才进行插入。

(3)将索引文件和数据文件放在不同的磁盘(利用建表中的选项)

(4)如果进行批量插入,可以通过增加bulk_insert_buffer_size 变量值的方法来提高速度,这只对MyISAM表有用。

(5)当从一个文本文件装载一个表时,使用LOAD DATA INFILE。通常比使用很多的INSERT语句快。

 无法使用索引的情况

(1).以%开头的like查询
(2).数据类型出现隐式转换的时候也不会使用索引,特别是当列类型是字符串,那么一定记得在where条件中把字符串常量值用引号引起来,否则即便这个列上有索引,MySQL也不会用到,因为MySQL默认把输入的常量值进行转换以后才进行检索
(3).复合索引的情况下,如果查询条件不包含索引列的最左边部分,即不满足最左前缀原则,则不会使用索引
(4).如果mysql估计使用索引扫描比全表扫描更慢,则不使用索引。(扫描数据超过30%,都会走全表)
(5).用or分割开的条件,如果 or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到
(6).字段使用函数,将无法使用索引
(7).Join 语句中 Join 条件字段类型不一致的时候 MySQL 无法使用索引

3.优化ORDER BY语句

通过索引排序是性能最好的,通常如果SQL语句不合理,就无法使用索引排序,以下几种情况是无法使用索引排序的。

(1).查询使用了两种不同的排序方向,但是索引列都是正序排序的;
(2).查询的where和order by中的列无法组合成索引的最左前缀;
(3).查询在索引列的第一列上是范围条件;
(4)查询条件上有多个等于条件。对排序来说,这也是一种范围查询
 在优化ORDER BY语句之前,先来看看MySQL中排序的方式。先看看MySQL官方提供的示例数据库sakila中customer表上的索引情况。
mysql> show index from customer;
+----------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table    | Non_unique | Key_name          | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+----------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| customer |          0 | PRIMARY           |            1 | customer_id | A         |         577 |     NULL | NULL   |      | BTREE      |         |               |
| customer |          1 | idx_fk_store_id   |            1 | store_id    | A         |           3 |     NULL | NULL   |      | BTREE      |         |               |
| customer |          1 | idx_fk_address_id |            1 | address_id  | A         |         577 |     NULL | NULL   |      | BTREE      |         |               |
| customer |          1 | idx_last_name     |            1 | last_name   | A         |         577 |     NULL | NULL   |      | BTREE      |         |               |
+----------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
4 rows in set (0.00 sec)

mysql> select customer_id from customer order by store_id;

因为查询主键,然后store_id列是辅助索引(二级索引),辅助索引上存放了索引键值+对应行的主键,所以直接扫描辅助索引返回有序数据。

mysql> explain select * from customer order by customer_id;
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------+
| id | select_type | table    | type  | possible_keys | key     | key_len | ref  | rows | Extra |
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------+
|  1 | SIMPLE      | customer | index | NULL          | PRIMARY | 2       | NULL |  577 |       |
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------+
1 row in set (0.00 sec)

mysql> select * from customer order by store_id;

那么这里优化器为什么不使用store_id列上的辅助索引进行排序呢?

"table_scan": { "rows": 599, "cost": 5 } /* table_scan */ } ] /* rows_estimation */ }, { "considered_execution_plans": [ { "plan_prefix": [ ] /* plan_prefix */, "table": "`customer` FORCE INDEX (`idx_fk_store_id`)", "best_access_path": { "considered_access_paths": [ { alter table customer add key idx_stored_email ( store_id , email ); Query OK, 0 rows affected (0.19 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql>

mysql> explain select store_id , email from customer order  by email ;            
+----+-------------+----------+-------+---------------+------------------+---------+------+------+-----------------------------+
| id | select_type | table    | type  | possible_keys | key              | key_len | ref  | rows | Extra                       |
+----+-------------+----------+-------+---------------+------------------+---------+------+------+-----------------------------+
|  1 | SIMPLE      | customer | index | NULL          | idx_stored_email | 154     | NULL |  671 | Using index; Using filesort |
+----+-------------+----------+-------+---------------+------------------+---------+------+------+-----------------------------+
1 row in set (0.00 sec)

这里为什么又是filesort呢?不是使用了using index吗?虽然使用

mysql> select store_id,email from customer order by store_id ;

Filesort是通过相应的排序算法,将取得的数据在sort_buffer_size系统变量设置的内存排序区中进行排序,如果内存装载不下,它就会将磁盘上的数据进行分块,再对各个数据块进行排序,然后将各个块合并成有序的结果集。sort_buffer_size设置的排序区是每个线程独占的,所以在同一个时刻,mysql中存在多个sort buffer排序区。该值不要设置的太大,避免耗尽服务器内存。

简单来说

mysql> select store_id,email,customer_id from customer where store_id =1 order by email desc;

mysql> select payment_date,sum(amount) from payment group by payment_date;

 

可以看见使用了Filesort,还使用了内存临时表,这条SQL严重影响性能,所以需要优化:

首先禁止排序,ORDER BY NULL

mysql> explain select payment_date , sum(amount) from payment group by payment_date order by null;    
+----+-------------+---------+------+---------------+------+---------+------+-------+-----------------+
| id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows  | Extra           |
+----+-------------+---------+------+---------------+------+---------+------+-------+-----------------+
|  1 | SIMPLE      | payment | ALL  | NULL          | NULL | NULL    | NULL | 15422 | Using temporary |
+----+-------------+---------+------+---------------+------+---------+------+-------+-----------------+
1 row in set (0.00 sec)

可以看见已经没有使用Filesort,但是还是使用了内存临时表,这是我们可以创建一个复合索引来优化性能

mysql> alter table payment add key idx_pal (payment_date,amount,last_update);                                     
Query OK, 0 rows affected (0.13 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select payment_date , sum(amount) from payment group by payment_date;              
+----+-------------+---------+-------+---------------+---------+---------+------+-------+-------------+
| id | select_type | table   | type  | possible_keys | key     | key_len | ref  | rows  | Extra       |
+----+-------------+---------+-------+---------------+---------+---------+------+-------+-------------+
|  1 | SIMPLE      | payment | index | NULL          | idx_pal | 15      | NULL | 15422 | Using index |
+----+-------------+---------+-------+---------------+---------+---------+------+-------+-------------+
1 row in set (0.01 sec)

mysql> select * from customer where customer_id not in ( select customer_id from payment);

解释一下这里的执行计划:

第二行,id为2,说明优先级最高,最先执行,DEPENDENT SUBQUERY子查询中的第一个SELECT(意味着select依赖于外层查询中的数据),type为index_subquery,与unique_subquery类似,区别在于in的后面是查询非唯一索引字段的子查询,using index使用了覆盖索引。

第一行,id为1,说明优先级最低,可以看见select_type列是PRIMARY,意思是最外层的SELECT查询,可以看见使用了全表扫描。

如果使用连接(join)来完成这个查询,速度将会快很多。尤其是连接条件有索引的情况下:

mysql> explain select * from customer left join payment on customer.customer_id = payment.customer_id where  payment.customer_id is null;
+----+-------------+----------+------+--------------------+--------------------+---------+-----------------------------+------+-------------------------+
| id | select_type | table    | type | possible_keys      | key                | key_len | ref                         | rows | Extra                   |
+----+-------------+----------+------+--------------------+--------------------+---------+-----------------------------+------+-------------------------+
|  1 | SIMPLE      | customer | ALL  | NULL               | NULL               | NULL    | NULL                        |  671 |                         |
|  1 | SIMPLE      | payment  | ref  | idx_fk_customer_id | idx_fk_customer_id | 2       | sakila.customer.customer_id |   12 | Using where; Not exists |
+----+-------------+----------+------+--------------------+--------------------+---------+-----------------------------+------+-------------------------+
2 rows in set (0.00 sec)

从执行计划看出查询关联类型从index_subquery调整为了ref,在mysql5.5(包含mysql5.5),子查询效率还是不如关联查询(join),连接之所以更有效率,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上需要两个步骤的查询工作。

 select * from film where language_id=1 or title ='ACADEMY DINOSAUR';

 

下面看一个较简单明了的例子:

mysql> show create table tt\G
*************************** 1. row ***************************
       Table: tt
Create Table: CREATE TABLE `tt` (
  `id` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  KEY `id` (`id`),
  KEY `age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> select * from tt;
+------+------+
| id   | age  |
+------+------+
|    1 |   23 |
|    2 |   36 |
+------+------+
2 rows in set (0.00 sec)

可以看见表tt有两个单列索引,我们使用如下SQL查询,看是否会使用索引

mysql> explain select * from tt where id=1 or age=36;
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | tt    | ALL  | id,age        | NULL | NULL    | NULL |    2 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.01 sec)

可以看见虽然显示有id,age索引可用,但是没有使用,即全表扫描。我们可以这样优化:

mysql> explain select * from tt where id=1 union all select *  from tt where age=36;
+----+--------------+------------+------+---------------+------+---------+-------+------+-------------+
| id | select_type  | table      | type | possible_keys | key  | key_len | ref   | rows | Extra       |
+----+--------------+------------+------+---------------+------+---------+-------+------+-------------+
|  1 | PRIMARY      | tt         | ref  | id            | id   | 5       | const |    1 | Using where |
|  2 | UNION        | tt         | ref  | age           | age  | 5       | const |    1 | Using where |
| NULL | UNION RESULT | <union1,2> | ALL  | NULL          | NULL | NULL    | NULL  | NULL |             |
+----+--------------+------------+------+---------------+------+---------+-------+------+-------------+
3 rows in set (0.00 sec)

可以看见已经使用了索引,至于这里的执行计划,我就不再说明。有机会我会写一篇mysql执行计划的文章。

看看使用复合索引查询的情况:

mysql> show create table tt\G
*************************** 1. row ***************************
       Table: tt
Create Table: CREATE TABLE `tt` (
  `id` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  KEY `idx_id_age` (`id`,`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> explain select * from tt where id=1 or age=36;
+----+-------------+-------+-------+---------------+------------+---------+------+------+--------------------------+
| id | select_type | table | type  | possible_keys | key        | key_len | ref  | rows | Extra                    |
+----+-------------+-------+-------+---------------+------------+---------+------+------+--------------------------+
|  1 | SIMPLE      | tt    | index | idx_id_age    | idx_id_age | 10      | NULL |    2 | Using where; Using index |
+----+-------------+-------+-------+---------------+------------+---------+------+------+--------------------------+
1 row in set (0.00 sec)

mysql> select film_id,description from film order by title limit 50,5;

 

可以看见实际上使用了全表扫描,如果表有上百万记录,那么这将是一条致命SQL

我们改写成按照索引分页后回表读取行的方式,从执行计划中看不到全表扫描

mysql> explain select a.film_id , a.description from film a inner join (select film_id from film order by title limit 50,5) b on a.film_id=b.film_id; 
+----+-------------+------------+--------+---------------+-----------+---------+-----------+------+-------------+
| id | select_type | table      | type   | possible_keys | key       | key_len | ref       | rows | Extra       |
+----+-------------+------------+--------+---------------+-----------+---------+-----------+------+-------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL      | NULL    | NULL      |    5 |             |
|  1 | PRIMARY     | a          | eq_ref | PRIMARY       | PRIMARY   | 2       | b.film_id |    1 |             |
|  2 | DERIVED     | film       | index  | NULL          | idx_title | 767     | NULL      |   55 | Using index |
+----+-------------+------------+--------+---------------+-----------+---------+-----------+------+-------------+
3 rows in set (0.00 sec)

这里我大概解释一下执行计划:

第三行:

id为2,优先级最高,最先执行
select_type为DERIVED 用来表示包含在from子句中的子查询的select,mysql会递归执行并将结果放到一个临时表中。服务器内部称为“派生表”,因为该临时表是从子查询中派生出来的。
type列为index表示索引树全扫描,mysql遍历整个索引来查询匹配的行,这里就把film_id查询出来了。
Extra列为using index 表示使用覆盖索引

第二行:
select_type为PRIMARY,即复杂查询的最外层,当然这里还不算是最最外层。
table列为a,即film表的别名a,
type列为eq_ref,类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件

第一行:
select_type列的primary表示该查询为外层查询
table列被标记为<derived2>,表示查询结果来自一个衍生表,其中2代表该查询衍生自第2个select查询,即id为2的select

7.其他优化手段

当然还有其他的优化手段,比如索引提示,我这里简单列举一下就行了,因为大部分的时候mysql 优化器都工作的很好。

USE INDEX

提供给优化器参考的索引列表(优化器不一定给你面子哦)

IGNORE INDEX

提示优化器忽略一个或者多个索引

FORCE INDEX

强制优化器使用某个特定索引

 

总结:

  其实SQL语句优化的过程中,无非就是对mysql的执行计划理解,以及B+树索引的理解,其实只要我们理解执行计划和B+树以后,优化SQL语句还是比较简单的,当然还有特别复杂的SQL,我这里只是一些简单例子,当然再复杂的SQL,还是逃脱不了原理性的东西。

链接:https://www.cnblogs.com/gomysql/p/3632209.html

posted @ 2021-05-22 15:05  ~*一生所爱*~  阅读(305)  评论(0编辑  收藏  举报
在国际交往中,实力 永远是维护正义的基础;国防 才是外交真正的后盾;尊严 只在剑峰之上;真理 只在大炮射程之内。