前面我们介绍了 MySQL 中怎么样通过索引来优化查询。日常开发中,除了使用查询外,我 们还会使用一些其他的常用 SQL,比如 INSERT、GROUP BY 等。对于这些 SQL 语句,我们该 怎么样进行优化呢?
本文属于SQL 优化系列篇
当用 load
命令导入数据的时候,适当的设置可以提高导入的速度。
对于 MyISAM
存储引擎的表,可以通过以下方式快速的导入大量的数据。
ALTER TABLE tbl_name DISABLE KEYS;
loading the data
ALTER TABLE tbl_name ENABLE KEYS;
DISABLE KEYS
和 ENABLE KEYS
用来打开或者关闭 MyISAM
表非唯一索引的更新。在导入大量的数据到一个非空的 MyISAM
表时,通过设置这两个命令,可以提高导入的效率。对于导入大量数据到一个空的 MyISAM
表,默认就是先导入数据然后才创建索引的,所以不用进行设置。
上面是对MyISAM
表进行数据导入时的优化措施,对于InnoDB
类型的表,这种方式并不能提高导入数据的效率,可以有以下几种方式提高InnoDB
表的导入效率。
因为 InnoDB
类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效地提高导入数据的效率。
例如,下面文本film_test3.txt
是按表film_test4
的主键存储的,那么导入的时候共耗时 27.92
秒。
mysql> load data infile '/home/mysql/film_test3.txt' into table film_test4;
Query OK, 1587168 rows affected (22.92 sec)
Records: 1587168 Deleted: 0 Skipped: 0 Warnings: 0
而下面的 film_test4.txt
是没有任何顺序的文本,那么导入的时候共耗时 31.16
秒。
mysql> load data infile '/home/mysql/film_test4.txt' into table film_test4;
Query OK, 1587168 rows affected (31.16 sec)
Records: 1587168 Deleted: 0 Skipped: 0 Warnings: 0
在导入数据前执行 SET UNIQUE_CHECKS=0
,关闭唯一性校验,在导入结束后执行 SET UNIQUE_CHECKS=1
,恢复唯一性校验,可以提高导入的效率。
例如,当 UNIQUE_CHECKS=1
时:
mysql> load data infile '/home/mysql/film_test3.txt' into table film_test4;
Query OK, 1587168 rows affected (22.92 sec)
Records: 1587168 Deleted: 0 Skipped: 0 Warnings: 0
当 SET UNIQUE_CHECKS=0
时:
mysql> load data infile '/home/mysql/film_test3.txt' into table film_test4;
Query OK, 1587168 rows affected (19.92 sec)
Records: 1587168 Deleted: 0 Skipped: 0 Warnings: 0
如果应用使用自动提交的方式,建议在导入前执行 SET AUTOCOMMIT=0
,关闭自动提交,导入结束后再执行 SET AUTOCOMMIT=1
,打开自动提交,也可以提高导入的效率。
例如,当 AUTOCOMMIT=1
时:
mysql> load data infile '/home/mysql/film_test3.txt' into table film_test4;
Query OK, 1587168 rows affected (22.92 sec)
Records: 1587168 Deleted: 0 Skipped: 0 Warnings: 0
当 AUTOCOMMIT=0
时:
mysql> load data infile '/home/mysql/film_test3.txt' into table film_test4;
Query OK, 1587168 rows affected (20.87 sec)
Records: 1587168 Deleted: 0 Skipped: 0 Warnings: 0
当进行数据INSERT
的时候,可以考虑采用以下几种优化方式。
如果同时从同一客户插入很多行,尽量使用多个值表的INSERT
语句,这种方式将大大缩减客户端与数据库之间的连接、关闭等消耗,使得效率比分开执行的单个INSERT
语 快(在一些情况中几倍)。下面是一次插入多值的一个例子:
insert into test values(1,2),(1,3),(1,4)...
如果从不同客户插入很多行,能通过使用INSERT DELAYED
语句得到更高的速度。 DELAYED
的含义是让INSERT
语句马上执行,其实数据都被放在内存的队列中,并没有真正写入磁盘,这比每条语句分别插入要快的多;LOW_PRIORITY
刚好相反,在所有其 他用户对表的读写完后才进行插入
将索引文件和数据文件分在不同的磁盘上存放(利用建表中的选项)
如果进行批量插入,可以增加bulk_insert_buffer_size
变量值的方法来提高速度,但是, 这只能对MyISAM
表使用;
当从一个文本文件装载一个表时,使用LOAD DATA INFILE
。这通常比使用很多INSERT
语句快20
倍。
默认情况下,MySQL
对所有GROUP BY col1,col2....
的字段进行排序。这与在查询中指定 ORDER BY col1,col2...
类似。因此,如果显式包括一个包含相同的列的ORDER BY
子句,则对MySQL
的实际执行性能没有什么影响。
如果查询包括GROUP BY
但用户想要避免排序结果的消耗,则可以指定ORDER BY NULL
禁止排序,如下面的例子:
mysql> explain select id,sum(moneys) from sales2 group by id\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales2
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra: Using temporary; Using filesort
1 row in set (0.00 sec)
mysql> explain select id,sum(moneys) from sales2 group by id order by null\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales2
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra: Using temporary;
1 row in set (0.00 sec)
从上面的例子可以看出第一个SQL
语句需要进行 “filesort
” , 而第二个SQL
由于ORDER BY NULL
不需要进行“filesort
” ,而filesort
往往非常耗费时间。
在某些情况中, MySQL
可以使用一个索引来满足ORDER BY
子句, 而不需要额外的排序。 WHERE
条件和ORDER BY
使用相同的索引,并且ORDER BY
的顺序和索引顺序相同,并且 ORDER BY
的字段都是升序或者都是降序。
例如,下列SQL可以使用索引。
SELECT * FROM t1 ORDER BY key_part1,key_part2,... ;
SELECT * FROM t1 WHERE key_part1=1 ORDER BY key_part1 DESC, key_part2 DESC;
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 DESC;
但是在以下几种情况下则不使用索引:
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;
--order by的字段混合ASC和DESC
SELECT * FROM t1 WHERE key2=constant ORDER BY key1;
--用于查询行的关键字与ORDER BY中所使用的不相同
SELECT * FROM t1 ORDER BY key1, key2;
--对不同的关键字使用ORDER BY:
MySQL 4.1
开始支持SQL
的子查询。这个技术可以使用SELECT
语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。使用子查询可以一次性地完成很 多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起 来也很容易。但是,有些情况下,子查询可以被更有效率的连接(JOIN
)替代。
在下面的例子中, 要从sales2
表中找到那些在company2
表中不存在的所有公司的信息:
mysql> explain select * from sales2 where company_id not in ( select id from company2 )\G;
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: sales2
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra: Using where
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: company2
type: index_subquery
possible_keys: ind_company2_id
key: ind_company2_id
key_len: 5
ref: func
rows: 2
Extra: Using index
2 rows in set (0.00 sec)
如果使用连接(JOIN
)来完成这个查询工作,速度将会快很多。尤其是当company2
表中对id建有索引的话,性能将会更好,具体查询如下:
mysql> explain select * from sales2 left join company2 on sales2.company_id = company2.id where sales2.company_id is null\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales2
type: ref
possible_keys: ind_sales2_companyid_moneys
key: ind_sales2_companyid_moneys
key_len: 5
ref: const
rows: 1
Extra: Using where
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: ref
type: index_subquery
possible_keys: ind_company2_id
key: ind_company2_id
key_len: 5
ref: sakila.sales2.company_id
rows: 2
Extra:
2 rows in set (0.00 sec)
从执行计划中可以明显看出查询扫描的记录范围和使用索引的情况都有了很大的改善。 连接(JOIN
)之所以更有效率一些,是因为MySQL
不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。
对于含有 OR
的查询子句, 如果要利用索引, 则 OR
之间的每个条件列都必须用到索引; 如果没有索引,则应该考虑增加索引。
SQL
提示(SQL HINT
)是优化数据库的一个重要手段,简单来说就是在 SQL
语句中加入一些 人为的提示来达到优化操作的目的。
下面是一个使用 SQL
提示的例子:
SELECT SQL_BUFFER_RESULTS * FROM...
这个语句将强制 MySQL
生成一个临时结果集。只要临时结果集生成后,所有表上的锁定均被释放。 这能在遇到表锁定问题时或要花很长时间将结果传给客户端时有所帮助,因为 可以尽快释放锁资源。
下面是一些在 MySQL
中常用的 SQL
提示。
在查询语句中表名的后面,添加 USE INDEX
来提供希望 MySQL
去参考的索引列表,就可以让 MySQL
不再考虑其他可用的索引。
mysql> explain select * from sales2 use index (ind_sales2_id) where id = 3\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales2
type: ref
possible_keys: ind_sales2_id
key: ind_sales2_id
key_len: 5
ref: const
rows: 1
Extra: Using where
1 row in set (0.00 sec)
如果用户只是单纯地想让 MySQL
忽略一个或者多个索引,则可以使用 IGNORE INDEX
作 为 HINT
。同样是上面的例子,这次来看一下查询过程忽略索引 ind_sales2_id
的情况:
mysql> explain select * from sales2 ignore index (ind_sales2_id) where id = 3\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales2
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra: Using where
1 row in set (0.00 sec)
从执行计划可以看出,系统忽略了指定的索引,而使用了全表扫描
为强制 MySQL
使用一个特定的索引,可在查询中使用 FORCE INDEX
作为 HINT
。例如, 当不强制使用索引的时候,因为 id
的值都是大于 0
的,因此 MySQL
会默认进行全表扫描, 而不使用索引,如下所示:
mysql> explain select * from sales2 where id > 0 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales2
type: ALL
possible_keys: ind_sales2_id
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra: Using where
1 row in set (0.00 sec)
但是,当使用FORCE INDEX
进行提示时,即便使用索引的效率不是最高,MySQL
还是选择使用了索引,这是 MySQL
留给用户的一个自行选择执行计划的权力。加入 FORCE INDEX
提示后再次执行上面的 SQL
:
mysql> explain select * from sales2 force index (ind_sales2_id) where id > 0 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales2
type: range
possible_keys: ind_sales2_id
key: ind_sales2_id
key_len: 5
ref: NULL
rows: 1000
Extra: Using where
1 row in set (0.00 sec)