MySQL索引介绍及百万数据SQL优化实践总结

前言

近来在工作中接触数据较多,由于公司系统框架很老,我发现他们好像在一些数据库查询上并没有太多sql优化的痕迹,以至于在查询一些数据量大的数据时常会出现查询长时间延时的现象。因此在求知心的驱使下,笔者结合网上一些sql优化的方法,主动实践总结一些相关基础理论和方法。在此当个笔记分享出来。这里我们不谈论硬件设施优化,或者分库分表等花里胡哨的优化操作,仅对数据层面做实践。

索引的相关基础

索引的概念

既然是接触sql优化,首先想到的自然是索引了。官方点说:

在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。

也就是说,当你读一本10厘米厚的《Java全栈开发大合集》(比如有的话)时,为了找到你需要的技术栈在该书的地方,你可以将索引理解为书的目录,或者之前你在书上做过的标签。总之最终目的只有一个:方便快速查找指定信息。 

索引的结构

Mysql索引主要有两种结构:B+Tree索引和Hash索引.

Hash索引

MySQL中,只有Memory(Memory表只存在内存中,断电会消失,适用于临时表)存储引擎显示支持Hash索引,是Memory表的默认索引类型,尽管Memory表也可以使用B+Tree索引。hsah索引把数据的索引以hash形式组织起来,因此当查找某一条记录的时候,速度非常快。当时因为是hash结构,每个键只对应一个值,而且是散列的方式分布。所以他并不支持范围查找和排序等功能。

B+Tree索引

B+Tree是在B+Tree基础上的一种优化,使其更适合实现外存储索引结构。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。高度小相对应的查询时的磁盘I/O次数就少一些了,进而提高了查询效率。这也是它区别于B-Tree的地方。

B-Tree的每个节点中不仅包含数据的key值,还有data值。但是每个节点的存储空间是有限的,如果data数据较大时将会导致每个节点能存储的key的数量很小,当存储的数据量很大时,会导致B-Tree的深度较大。而B+Tree的非子叶节点不存储数据,每个节点能够存储更多的键值,能够减少磁盘的访问次数,一次磁盘的访问次数相当于很多次的内存比较次数。所以B+Tree使用性能更高。

这里笔者也是看资料总结的,可能并不全面,大家自行查阅资料即可。

索引的类别

Mysql常见索引有:主键索引、唯一索引、普通索引、全文索引、组合索引

  • PRIMARY KEY(主键索引)  ALTER TABLE `table_name` ADD PRIMARY KEY ( `col` ) 
  • UNIQUE(唯一索引)     ALTER TABLE `table_name` ADD UNIQUE (`col`)
  • INDEX(普通索引)     ALTER TABLE `table_name` ADD INDEX index_name (`col`)
  • FULLTEXT(全文索引)      ALTER TABLE `table_name` ADD FULLTEXT ( `col` )
  • 联合索引   ALTER TABLE `table_name` ADD INDEX index_name (`col1`, `col2`, `col3` ) 
  • SPATIAL(空间索引)       ALTER TABLE db_user ADD SPATIAL(`col`)---较为特殊用于空间数据

Mysql各种索引区别:

  • 普通索引:最基本的索引,没有任何限制
  • 唯一索引:与"普通索引"类似,不同的就是:索引列的值必须唯一,但允许有空值。
  • 主键索引:它 是一种特殊的唯一索引,不允许有空值。 
  • 全文索引:仅可用于 MyISAM 表,针对较大的数据,生成全文索引很耗时好空间。
  • 联合索引:为了更多的提高mysql效率可建立组合索引,遵循”最左前缀“原则。创建复合索引时应该将最常用(频率)作限制条件的列放在最左边,依次递减。
  • 空间索引:空间索引是对空间数据类型的字段建立的索引

tips:联合索引的好处:覆盖索引,这一点是最重要的,众所周知非主键索引会先查到主键索引的值再从主键索引上拿到想要的值。但是覆盖索引可以直接在非主键索引上拿到相应的值,减少一次查询。

tips:MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建

这里当我们用Navicat创建索引时,可视化界面也看得到这几类索引: 

实践准备

准备一张测试数据表,并利用存储过程插入100w条数据。

创建测试表并设置相应索引

/*
 Navicat Premium Data Transfer

 Source Server         : Mysql
 Source Server Type    : MySQL
 Source Server Version : 80013
 Source Host           : localhost:3306
 Source Schema         : db_user

 Target Server Type    : MySQL
 Target Server Version : 80013
 File Encoding         : 65001

 Date: 31/03/2023 16:41:50
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for million_test
-- ----------------------------
DROP TABLE IF EXISTS `million_test`;
CREATE TABLE `million_test`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '测试百万数据表id',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '测试名',
  `age` int(11) NULL DEFAULT NULL COMMENT '测试对象年龄,添加索引',
  `score` int(255) NULL DEFAULT NULL COMMENT '测试对象分数',
  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '测试对象地址',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `index_age`(`age`) USING BTREE COMMENT '年龄的索引',
  INDEX `index_score`(`score`) USING BTREE COMMENT 'score的索引',
  INDEX `index_address`(`address`) USING BTREE COMMENT 'address的索引'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '测试百万数据查询及优化效果' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

创建存储过程

CREATE DEFINER=`root`@`localhost` PROCEDURE `million_test`(IN `nums` bigint)
BEGIN
	#Routine body goes here...
	DECLARE index_num int DEFAULT 0;
	while index_num <= nums DO
	insert into million_test(id,name,age,score,address) VALUES
	(null,CONCAT('测试数据',index_num),ROUND(RAND()*50),ROUND(RAND()*100),CONCAT('光谷金融港',ROUND(RAND()*50),'楼'));
	set index_num =index_num + 1;
	end while;
END

执行存储函数,完成实践准备。但是这里有个问题,如果用 InnoDB执行引擎,插入100w的数据会非常慢,你可能打完一王者荣耀都导不完。因此这里推荐先用MyISAM引擎,带数据导入完成后,将引擎改为InnoDB再测试。

tips:如果不需要支持事务,没有并发写入操作,MyISAM存储引擎速度优于InnoDB

数据导入完成后即可看到如下结果:

mysql> desc million_test;
+---------+--------------+------+-----+---------+----------------+
| Field   | Type         | Null | Key | Default | Extra          |
+---------+--------------+------+-----+---------+----------------+
| id      | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name    | varchar(255) | YES  |     | NULL    |                |
| age     | int(11)      | YES  | MUL | NULL    |                |
| score   | int(255)     | YES  | MUL | NULL    |                |
| address | varchar(255) | YES  | MUL | NULL    |                |
+---------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

mysql> select count(id) from million_test;
+-----------+
| count(id) |
+-----------+
|   1133721 |
+-----------+
1 row in set (0.39 sec)

mysql> show index from million_test;
+--------------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table        | Non_unique | Key_name      | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| million_test |          0 | PRIMARY       |            1 | id          | A         |     1129161 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| million_test |          1 | index_age     |            1 | age         | A         |          49 |     NULL |   NULL | YES  | BTREE      |         | 年龄的索引    | YES     | NULL       |
| million_test |          1 | index_score   |            1 | score       | A         |          96 |     NULL |   NULL | YES  | BTREE      |         | score的索引   | YES     | NULL       |
| million_test |          1 | index_address |            1 | address     | A         |          49 |     NULL |   NULL | YES  | BTREE      |         | address的索引 | YES     | NULL       |
+--------------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
4 rows in set (0.10 sec)

优化测试

这里我们主要通过explain来查看sql的执行情况。explain显示了mysql如何使用索引来处理select语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。比如我们查询一下这130多万的数据里age大于10,score小于60的数据名称。

mysql> explain select name from million_test where age>10 and score<60;
+----+-------------+--------------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
| id | select_type | table        | partitions | type | possible_keys         | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | million_test | NULL       | ALL  | index_age,index_score | NULL | NULL    | NULL | 1129161 |    25.00 | Using where |
+----+-------------+--------------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

这里主要的显示信息对应如下解释: 

id SELECT识别符。这是SELECT的查询序列号
select_type

SELECT类型,可以为以下任何一种:

  • SIMPLE:简单SELECT(不使用UNION或子查询)
  • PRIMARY:最外面的SELECT
  • UNION:UNION中的第二个或后面的SELECT语句
  • DEPENDENT UNION:UNION中的第二个或后面的SELECT语句,取决于外面的查询
  • UNION RESULT:UNION 的结果
  • SUBQUERY:子查询中的第一个SELECT
  • DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外面的查询
  • DERIVED:导出表的SELECT(FROM子句的子查询)
partitions 匹配的分区
table

输出的行所引用的表

type

联接类型。下面给出各种联接类型,按照从最佳类型到最坏类型进行排序:

  • system:表仅有一行(=系统表)。这是const联接类型的一个特例。
  • const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,在这行的列值可被优化器剩余部分认为是常数。const表很快,因为它们只读取一次!
  • eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型。
  • ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。
  • ref_or_null:该联接类型如同ref,但是添加了MySQL可以专门搜索包含NULL值的行。
  • index_merge:该联接类型表示使用了索引合并优化方法。
  • unique_subquery:该类型替换了下面形式的IN子查询的ref: value IN (SELECT primary_key FROM single_table WHERE some_expr) unique_subquery是一个索引查找函数,可以完全替换子查询,效率更高。
  • index_subquery:该联接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引: value IN (SELECT key_column FROM single_table WHERE some_expr)
  • range:只检索给定范围的行,使用一个索引来选择行。
  • index:该联接类型与ALL相同,除了只有索引树被扫描。这通常比ALL快,因为索引文件通常比数据文件小。
  • ALL:对于每个来自于先前的表的行组合,进行完整的表扫描。

一般来说,得保证查询至少达到range级别,最好能达到ref,否则就有可能出现性能问题。

possible_keys 指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
key 显示MySQL实际决定使用的键(索引)。如果没有选择使用索引,key就是NULL。
key_len

表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)不损失精确性的情况下,长度越短越好 ,如果键是NULL,则长度为NULL。

ref 显示使用哪个列或常数与key一起从表中选择行。
rows 显示MySQL认为它执行查询时必须检查的行数。多行之间的数据相乘可以估算要处理的行数。
filtered 显示了通过条件过滤出的行数的百分比估计值。
Extra

该列包含MySQL解决查询的详细信息

  • Distinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行。
  • Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行。
  • range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用。
  • Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。
  • Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。
  • Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果。
  • Using where:WHERE 子句用于限制哪一个行匹配下一个表或发送到客户。
  • Using sort_union(...), Using union(...), Using intersect(...):这些函数说明如何为index_merge联接类型合并索引扫描。
  • Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发现了一个索引,可以用来查 询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表。
  • Using MRR,把「随机磁盘读」,转化为「顺序磁盘读」

而比较重要的就是Extra列的返回信息。该列显示MySQL在查询过程中的一些详细信息,是MySQL查询优化器执行查询的过程中对查询计划的重要补充信息。

所以我们查询的这个整体信息就是:我们查询是一个简单的查询语句,引用的表是million_test,没有用到分区,连接类型是ALL也就是说采用的全表扫描,有两个能找到的索引index_age,index_score。虽然有可用的索引,但是实际一个也没用……,然后执行检察时mysql检查了1129161行,符合条件的由25%,最后使用的where子句进行条件匹配。

最后我们可以通过show profiles;来查看一下sql语句的执行时间。但是数据库默认是不开启的,需要我们手动查看profile是否开启。默认情况使用:

mysql> show profiles;
Empty set, 1 warning (0.00 sec)

我们可以通过show variables; 来查看profiling的启动状态,但是它里面信息很多,因此这里推荐使用模糊查询来查看:

mysql> show variables like 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling     | OFF   |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)

这里可以看到,果然是关闭的。因此我们需要手动开启它,然后重新查看状态:

#开启profiling
mysql> set profiling =1;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> show variables like 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling     | ON    |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)

 现在就开启profiles了。再次执行之前的sql查询语句或其他语句,就能看到具体的sql执行时间了:

mysql> show profiles;
+----------+------------+-----------------------------------------------------------------+
| Query_ID | Duration   | Query                                                           |
+----------+------------+-----------------------------------------------------------------+
|        1 | 0.00194375 | show variables like 'profiling'                                 |
|        2 | 0.00074650 | explain select name from million_test where age>10 and score<60 |
|        3 | 1.38044475 | select * from million_test                                      |
+----------+------------+-----------------------------------------------------------------+
3 rows in set, 1 warning (0.00 sec)

可以看到,全表扫描其实是很费时间的!

查看对应的Query_ID的sql语句执行各个操作的耗时情况:

#查看Query_ID为2的sql语句执行具体操作耗时情况
mysql>  show profile for query 2;
+----------------------------+----------+
| Status                     | Duration |
+----------------------------+----------+
| starting                   | 0.000081 |
| checking permissions       | 0.000004 |
| Opening tables             | 0.000035 |
| init                       | 0.000004 |
| System lock                | 0.000007 |
| optimizing                 | 0.000008 |
| statistics                 | 0.000093 |
| preparing                  | 0.000019 |
| explaining                 | 0.000026 |
| end                        | 0.000003 |
| query end                  | 0.000002 |
| waiting for handler commit | 0.000003 |
| query end                  | 0.000004 |
| closing tables             | 0.000005 |
| freeing items              | 0.000445 |
| cleaning up                | 0.000012 |
+----------------------------+----------+
16 rows in set, 1 warning (0.00 sec)

另外需要注意的是:变量profiling是用户变量,每次都得重新启用。

 然后就可以开始真正的sql优化测试。举个常见的例子:在常用的sql优化方案中,我们应尽量避免使用or来连接条件,否则将导致索引失效造成全表扫面。我们来测试看看:

mysql> explain select address from million_test where age = 10 or score>60;
+----+-------------+--------------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
| id | select_type | table        | partitions | type | possible_keys         | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | million_test | NULL       | ALL  | index_age,index_score | NULL | NULL    | NULL | 1129161 |    34.69 | Using where |
+----+-------------+--------------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

mysql> explain select address from million_test where age = 10 union all select address from million_test where score>60;
+----+-------------+--------------+------------+------+---------------+-----------+---------+-------+---------+----------+-------------+
| id | select_type | table        | partitions | type | possible_keys | key       | key_len | ref   | rows    | filtered | Extra       |
+----+-------------+--------------+------------+------+---------------+-----------+---------+-------+---------+----------+-------------+
|  1 | PRIMARY     | million_test | NULL       | ref  | index_age     | index_age | 5       | const |   39612 |   100.00 | NULL        |
|  2 | UNION       | million_test | NULL       | ALL  | index_score   | NULL      | NULL    | NULL  | 1129161 |    50.00 | Using where |
+----+-------------+--------------+------------+------+---------------+-----------+---------+-------+---------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

显然第一种就是索引失效的情况,第二种笔者使用union all来代替or连接,两个索引中有一个能正常使用了。然后看一下执行效率的差别:

mysql> show profiles;
+----------+------------+---------------------------------------------------------------------------------------------------------------------+
| Query_ID | Duration   | Query                                                                                                               |
+----------+------------+---------------------------------------------------------------------------------------------------------------------+                                                                                |
|       1 | 0.00807975 | explain select address from million_test where age = 10 or score>60                                                 |
|       2 | 0.00056075 | explain select address from million_test where age = 10 union all select address from million_test where score>60   |
+----------+------------+---------------------------------------------------------------------------------------------------------------------+
15 rows in set, 1 warning (0.00 sec)

还是挺明显的。但是这里有个问题索引的失效并不仅仅是因为使用>号(大于号)产生的!如果我们查询的是主键id,那么它一样不会失效。并且如果查询的值范围较小(在一定区间内),也是会生效的。

仔细看下面的测试:

#测试id,索引生效
mysql> explain select id  from million_test where score>90;
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
| id | select_type | table        | partitions | type  | possible_keys | key         | key_len | ref  | rows   | filtered | Extra                    |
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
|  1 | SIMPLE      | million_test | NULL       | range | index_score   | index_score | 5       | NULL | 201920 |   100.00 | Using where; Using index |
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)

#测试加索引的address,限制较小的查询范围(~5%),索引生效
mysql> explain  select address from million_test where score>95;
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table        | partitions | type  | possible_keys | key         | key_len | ref  | rows   | filtered | Extra                            |
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | million_test | NULL       | range | index_score   | index_score | 5       | NULL | 105902 |   100.00 | Using index condition; Using MRR |
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)

#大范围小的id查询测试,索引正常生效
mysql> explain  select id from million_test where score>10;
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
| id | select_type | table        | partitions | type  | possible_keys | key         | key_len | ref  | rows   | filtered | Extra                    |
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
|  1 | SIMPLE      | million_test | NULL       | range | index_score   | index_score | 5       | NULL | 564580 |   100.00 | Using where; Using index |
+----+-------------+--------------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
1 row in set, 1 warning (0.01 sec)

#(~9.5%)范围下的address查询测试,索引失效 
mysql> explain  select address from million_test where score>90;
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table        | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | million_test | NULL       | ALL  | index_score   | NULL | NULL    | NULL | 1129161 |    17.88 | Using where |
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

笔者考虑查id索引不失效可能是不用回表的问题,但是其他查询确实存在范围问题。留给大家去思考,也欢迎大佬指明原因~

所以笔者在这想提醒大家: 

网上的sql优化总结很多,但是不一定每个都适应你的查询环境,更多需要我们自己去总结积累,找到真正正确的使用方法。

汇总一些可能有用的sql优化方法


 1、尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。上面已经举过例子了,不再详述。


2、在查询时,考虑在 where 及 order by 涉及的列上建立索引。


3、尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:

select name from million_test where address is null;

4、避免再where子句中使用or来连接条件,可以用union all代替,上面也举例了。

5、模糊查询避免使用前缀模糊查询比如:“%38”。

6、慎用in和not in,可能会导致全表扫描,连续数值能用between就别用in:

mysql> explain select name from million_test where age not in(10,11,12);
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table        | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | million_test | NULL       | ALL  | index_age     | NULL | NULL    | NULL | 1129161 |    88.32 | Using where |
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

mysql> explain select name from million_test where age in(10,11,12);
+----+-------------+--------------+------------+-------+---------------+-----------+---------+------+--------+----------+----------------------------------+
| id | select_type | table        | partitions | type  | possible_keys | key       | key_len | ref  | rows   | filtered | Extra                            |
+----+-------------+--------------+------------+-------+---------------+-----------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | million_test | NULL       | range | index_age     | index_age | 5       | NULL | 118044 |   100.00 | Using index condition; Using MRR |
+----+-------------+--------------+------------+-------+---------------+-----------+---------+------+--------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> explain select name from million_test where age between 10 and 13;
+----+-------------+--------------+------------+-------+---------------+-----------+---------+------+--------+----------+----------------------------------+
| id | select_type | table        | partitions | type  | possible_keys | key       | key_len | ref  | rows   | filtered | Extra                            |
+----+-------------+--------------+------------+-------+---------------+-----------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | million_test | NULL       | range | index_age     | index_age | 5       | NULL | 169580 |   100.00 | Using index condition; Using MRR |
+----+-------------+--------------+------------+-------+---------------+-----------+---------+------+--------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)

7,避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。可以把它改为=号后面计算如:

mysql> explain select name from million_test where age/2=16;
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table        | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | million_test | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1129161 |   100.00 | Using where |
+----+-------------+--------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
#索引生效
mysql> explain select name from million_test where age=16*2;
+----+-------------+--------------+------------+------+---------------+-----------+---------+-------+-------+----------+-------+
| id | select_type | table        | partitions | type | possible_keys | key       | key_len | ref   | rows  | filtered | Extra |
+----+-------------+--------------+------------+------+---------------+-----------+---------+-------+-------+----------+-------+
|  1 | SIMPLE      | million_test | NULL       | ref  | index_age     | index_age | 5       | const | 40950 |   100.00 | NULL  |
+----+-------------+--------------+------------+------+---------------+-----------+---------+-------+-------+----------+-------+
1 row in set, 1 warning (0.00 sec)

8、避免在where子句后面对字段进行函数操作。

9、不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

10、在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使 用,并且应尽可能的让字段顺序与索引顺序相一致。

11、不要写一些没有意义的查询,比如为了需要生成一个空表结构去通过select构建。

12、用exists(not exists)去代替一些会索引失效的in(not in)操作

13、并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段 age,score、address几乎各一半,那么即使在age上建了索引也对查询效率起不了作用。

14、索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。

15.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。

16、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

17、尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

18、任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。

19、尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。

20、避免频繁创建和删除临时表,以减少系统表资源的消耗。

21、临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使 用导出表。

22、在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

23、如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

24、尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

25、使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

26、与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

27、在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。

28、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

29、尽量避免大事务操作,提高系统并发能力。

总结

虽然索引可以加快查询速度,提高 MySQL 的处理性能,但是过多地使用索引也会造成以下弊端

  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
  • 除了数据表占数据空间之外,每一个索引还要占一定的物理空间。如果要建立聚簇索引,那么需要的空间就会更大。
  • 当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。

注意:索引可以在一些情况下加速查询,但是在某些情况下,会降低效率。

索引只是提高效率的一个因素,因此在建立索引的时候应该遵循以下原则:

  • 在经常需要搜索的列上建立索引,可以加快搜索的速度。
  • 在作为主键的列上创建索引,强制该列的唯一性,并组织表中数据的排列结构。
  • 在经常使用表连接的列上创建索引,这些列主要是一些外键,可以加快表连接的速度。
  • 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,所以其指定的范围是连续的。
  • 在经常需要排序的列上创建索引,因为索引已经排序,所以查询时可以利用索引的排序,加快排序查询。
  • 在经常使用 WHERE 子句的列上创建索引,加快条件的判断速度。

最后切记:根据自己的自己使用环境合适建立索引和sql优化~

猜你喜欢

转载自blog.csdn.net/qq_42263280/article/details/129880514