SQL语句写得不严谨或者不适当,没有正确的使用上索引,会带来很严重的性能问题,这时DBA们又要来收拾这些烂滩子了,所以SQL语句的优化,在日常工作中,是占很重要的一部份,当然还有比如OS优化,硬件优化,MySQL Server优化,数据类型优化,应用层优化。我们进行MySQL的一些相关优化进行探讨。
1、优化数据插入:
DISABLE KEYS和ENABLE KEYS用来关闭或者打开MyISAM表非唯一索引的更新,当用load命令导入数据的时候,适当的设置可以提高导入的速度。只用于MyISAM存储引擎的表。看效果:
mysql> show create table test1\G *************************** 1. row *************************** Table: test1 Create Table: CREATE TABLE `test1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` char(20) NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `name` (`name`) ) ENGINE=MyISAM AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) mysql> select count(*) from test1; +----------+ | count(*) | +----------+ | 300000 | +----------+ 1 row in set (0.00 sec) mysql>
可以通过这样的挫方式插入30万行数据:
[root ~]$ for ((i=1;i<=300000;i++));do `mysql -uroot -p123456 -e "insert into sakila.test1 values ($i,floor($i+rand()*$i))"` ;done
1. 如果采用以下传统的方式把test1的数据转移到另一相同的表test2上:(用时75s)
mysql> create table test2 like test1; Query OK, 0 rows affected (0.03 sec) mysql> insert into test2 select * from test1; Query OK, 300000 rows affected, 0 warning (1.15 sec) Records: 300000 Duplicates: 0 Warnings: 0
2.先禁用索引, 在完全导入后, 再开启索引,用时29+35=64s
mysql> create table test4 like test1; Query OK, 0 rows affected (0.00 sec) mysql> ALTER TABLE test4 DISABLE KEYS; Query OK, 0 rows affected (0.00 sec) mysql> insert into test4 select * from test1; Query OK, 300000 rows affected, 0 warning (0.29 sec) Records: 300000 Duplicates: 0 Warnings: 0 mysql> ALTER TABLE test4 ENABLE KEYS; Query OK, 0 rows affected (0.35 sec)
从上面的测试结果来看, 在大批量导入时先禁用索引, 在完全导入后, 再开启索引, 一次性完成重建索引的效率会相对高很多(当数据量越大时,效果就明显了,实验环境,就不造大量数据了^.^)
对于InnoDB存储引擎表,上面的方式并不能提高导入数据的效率。可以有以下几种方式提高Innodb表的导入效率
(1)因为InnoDB类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率。
(2)在导入数据前执行 SET UNIQUE_CHECKS=0,关闭唯一性效验,在导入数据结束以后执行SET UNIQUE_CHECKS=1,恢复唯一性效验,可以提高导入效率。
(3)如果使用自动提交的方式,建议在导入前执行SET AUTOCOMMIT=0,关闭自动提交,导入结束后再执行SET AUTOCOMMIT=1,打开自动提交,也可以提高导入的效率。
(4)对于有外键约束的表,我们在导入数据之前也可以忽略外键检查,因为innodb的外键是即时检查的,所以对导入的每一行都会进行外键的检查。
2.优化INSERT语句
1) 如果你同时从同一客户插入很多行,使用多个值表的INSERT语句。这比使用分开INSERT语句快(在一些情况中几倍)。
如: Insert into test values(1,2),(1,3),(1,4)…
2) 如果你从不同客户插入很多行,能通过使用INSERT DELAYED语句得到更高的速度。Delayed的含义是让insert 语句马上执行,其实数据都被放在内存的队列中,并没有真正写入磁盘;这比每条语句分别插入要快的多;LOW_PRIORITY刚好相反,在所有其他用户对表的读写完后才进行插入。
3) 将索引文件和数据文件分在不同的磁盘上存放(利用建表中的选项)。
4) 如果进行批量插入,可以增加bulk_insert_buffer_size变量值的方法来提高速度,但是,这只能对myisam表使用。
5) 当从一个文本文件装载一个表时,使用LOAD DATA INFILE。这通常比使用很多INSERT语句快N倍。
下面是基于索引的情况下优化,以下几种情况不会用上索引,这要特别注意的:
• 两个表关联字段类型不一样(也包括长度不一样)
• 通过索引扫描的记录数超过30%,变成全表扫描
• 联合索引中,第一个索引列使用范围查询
• 联合索引中,第一个查询条件不是最左索引列
• 模糊查询条件列最左以通配符 % 开始
• 内存表(HEAP 表)使用HASH索引时,使用范围检索或者ORDER BY
• 两个独立索引,其中一个用于检索,一个用于排序
• 使用了不同的 ORDER BY 和 GROUP BY 表达式
3.优化ORDER BY语句
通过索引排序是性能最好的,通常如果SQL语句不合理,就无法使用索引排序,以下几种情况是无法使用索引排序的。
先看看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 | 599 | NULL | NULL | | BTREE | | | | customer | 1 | idx_fk_store_id | 1 | store_id | A | 4 | NULL | NULL | | BTREE | | | | customer | 1 | idx_fk_address_id | 1 | address_id | A | 599 | NULL | NULL | | BTREE | | | | customer | 1 | idx_last_name | 1 | last_name | A | 599 | NULL | NULL | | BTREE | | | +----------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ 4 rows in set (0.03 sec)
1、MySQL中有两种排序方式
mysql> explain select customer_id from customer order by store_id ; +----+-------------+----------+-------+---------------+-----------------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+-----------------+---------+------+------+-------------+ | 1 | SIMPLE | customer | index | NULL | idx_fk_store_id | 1 | NULL | 599 | Using index | +----+-------------+----------+-------+---------------+-----------------+---------+------+------+-------------+ 1 row in set (0.03 sec)
因为查询主键,然后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 | 599 | NULL | +----+-------------+----------+-------+---------------+---------+---------+------+------+-------+ 1 row in set (0.00 sec)
这种排序方式直接使用了主键,也可以说成是使用了聚集索引。因为innodb是索引组织表(index-organized table),通过主键聚集数据,数据都是按照主键排序存放。而聚集索引就是按照没张表的主键构造一颗B+树,同时叶子节点中存放的即为正张表的行记录数据,也将聚集索引的叶子节点称为数据页。聚集索引的这个特性决定了索引组织表中的数据也是索引的一部分。
第二种是通过对返回数据进行排序,也就是通常说的Filesort排序,所有不是通过索引直接返回排序结果的排序都叫Filesort排序。Filesort并不代表通过磁盘文件进行排序,而只是说明进行了一个排序操作,至于排序操作是否使用了磁盘文件或者临时表,取决于mysql服务器对排序参数的设置和需要排序数据的大小。
mysql> explain select * from customer order by store_id; +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ | 1 | SIMPLE | customer | ALL | NULL | NULL | NULL | NULL | 599 | Using filesort | +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ 1 row in set (0.00 sec)
那么这里优化器为什么不使用store_id列上的辅助索引进行排序呢?
当通过辅助索引来查找数据时,innodb存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后通过主键索引来找到一个完整的行记录。举例来说,如果在一棵高度为3的辅助索引树中查找数据,那么需要对这个辅助索引树遍历3次找到指定主键,如果聚集索引树的高度同样为3,那么还需要对聚集索引树进行3次查找,最终找到一个完整的行数所在的页,因此一共需要6次逻辑IO访问以得到最终的一个数据页。
2、查询的where和order by中的列无法组合成索引的最左前缀;
mysql> alter table customer add key idx_stored_email ( store_id , email ); Query OK, 0 rows affected (0.20 sec) Records: 0 Duplicates: 0 Warnings: 0 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 | 599 | Using index; Using filesort | +----+-------------+----------+-------+---------------+------------------+---------+------+------+-----------------------------+ 1 row in set (0.00 sec)
这里为什么又是filesort呢?不是使用了using index吗?虽然使用了覆盖索引(只访问索引的查询,即查询只需要访问索引,而无须访问数据行,最简单的理解,比如翻开一本书,从目录页查找某些内容,但是目录就写的比较详细,我们在目录就找到了自己想看的内容)。但是请别忘记了,idx_stored_email是复合索引,必须遵循最左前缀的原则。
修改成以下情况就可以看到效果了:
mysql> explain select store_id , email from customer order by store_id; +----+-------------+----------+-------+---------------+------------------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+------------------+---------+------+------+-------------+ | 1 | SIMPLE | customer | index | NULL | idx_stored_email | 154 | NULL | 599 | Using index | +----+-------------+----------+-------+---------------+------------------+---------+------+------+-------------+ 1 row in set (0.00 sec)
简单来说,尽量减少额外排序,通过索引直接返回有序数据,where条件和order by使用相同的索引,并且order by的顺序和索引顺序相同。
正确写法:
mysql> explain select store_id , email , customer_id from customer where store_id =1 order by store_id desc; +----+-------------+----------+------+----------------------------------+------------------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+----------------------------------+------------------+---------+-------+------+-------------+ | 1 | SIMPLE | customer | ref | idx_fk_store_id,idx_stored_email | idx_stored_email | 1 | const | 325 | Using index | +----+-------------+----------+------+----------------------------------+------------------+---------+-------+------+-------------+ 1 row in set (0.00 sec)
不当写法:
mysql> explain select store_id , email , customer_id from customer where store_id =1 order by customer_id desc; +----+-------------+----------+------+----------------------------------+------------------+---------+-------+------+------------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+----------------------------------+------------------+---------+-------+------+------------------------------------------+ | 1 | SIMPLE | customer | ref | idx_fk_store_id,idx_stored_email | idx_stored_email | 1 | const | 325 | Using where; Using index; Using filesort | +----+-------------+----------+------+----------------------------------+------------------+---------+-------+------+------------------------------------------+ 1 row in set (0.00 sec)
3、查询在索引列的第一列上是范围条件,不会用上索引;
mysql> explain select * from customer where store_id <5; +----+-------------+----------+------+----------------------------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+----------------------------------+------+---------+------+------+-------------+ | 1 | SIMPLE | customer | ALL | idx_fk_store_id,idx_stored_email | NULL | NULL | NULL | 599 | Using where | +----+-------------+----------+------+----------------------------------+------+---------+------+------+-------------+ 1 row in set (0.00 sec) mysql>
还有一种情况就是:查询条件上有多个等于条件。对排序来说,这也是一种范围查询
4.优化GROUP BY 语句
默认情况下,mysql对所有GROUP BY col1,col2,的字段进行排序。这与在查询中指定ORDER BY col1,col2类似。因此,如果显式包括一个 包含相同列的ORDER BY子句,则对mysql的实际性能没有什么影响。如果查询包括GROUP BY,但我们想要避免排序带来的性能损耗,则可以指定ORDER BY NULL禁止排序,示例如下:
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 | ALL | NULL | NULL | NULL | NULL | 16086 | Using temporary; Using filesort | +----+-------------+---------+------+---------------+------+---------+------+-------+---------------------------------+ 1 row in set (0.18 sec)
可以看见使用了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 | 16086 | Using temporary | +----+-------------+---------+------+---------------+------+---------+------+-------+-----------------+ 1 row in set (0.00 sec)
可以看见已经没有使用Filesort,但是还是使用了内存临时表,这是我们可以创建一个复合索引来优化性能
mysql> alter table payment add key idx_pal (payment_date,amount); Query OK, 0 rows affected (1.27 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 | 8 | NULL | 16086 | Using index | +----+-------------+---------+-------+---------------+---------+---------+------+-------+-------------+ 1 row in set (0.03 sec)
5.优化子查询
MySQL 4.1开始支持SQL的子查询。这个技术可以使用SELECT语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另外一个SELECT语句中。使用子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起来也非常easy,but,在有些情况下,子查询效率非常低下,我们可以使用比较高大上的写法,那就是连接(JOIN)取而代之
mysql> explain select * from customer where customer_id not in ( select customer_id from payment); +----+--------------------+----------+----------------+--------------------+--------------------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+----------+----------------+--------------------+--------------------+---------+------+------+-------------+ | 1 | PRIMARY | customer | ALL | NULL | NULL | NULL | NULL | 599 | Using where | | 2 | DEPENDENT SUBQUERY | payment | index_subquery | idx_fk_customer_id | idx_fk_customer_id | 2 | func | 13 | Using index | +----+--------------------+----------+----------------+--------------------+--------------------+---------+------+------+-------------+ 2 rows in set (0.11 sec)
我解释一下这里的执行计划:
第二行,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 | 599 | NULL | | 1 | SIMPLE | payment | ref | idx_fk_customer_id | idx_fk_customer_id | 2 | sakila.customer.customer_id | 13 | Using where; Not exists | +----+-------------+----------+------+--------------------+--------------------+---------+-----------------------------+------+-------------------------+ 2 rows in set (0.03 sec)
从执行计划看出查询关联类型从index_subquery调整为了ref,在mysql5.5(包含mysql5.5),子查询效率还是不如关联查询(join),连接之所以更有效率,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上需要两个步骤的查询工作。
6.优化OR条件
对于含有OR的查询语句,则无法使用单列索引,但是可以使用复合索引
mysql> explain select * from film where language_id=1 or title ='ACADEMY DINOSAUR'; +----+-------------+-------+------+------------------------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+------------------------------+------+---------+------+------+-------------+ | 1 | SIMPLE | film | ALL | idx_title,idx_fk_language_id | NULL | NULL | NULL | 1000 | Using where | +----+-------------+-------+------+------------------------------+------+---------+------+------+-------------+ 1 row in set (0.05 sec) mysql>