对网上流传的一份开发规范的理解1

171 阅读9分钟

开始

最近在网络上找到了一个关于sql优化的建议,现在针对这些建议去做一些测试。

1、对查询进行优化,应该尽量的避免全表扫描,考虑在where条件或者order by上面建立索引。

理解:《数据库索引设计与优化》这本书里面提到了三星索引的概念,在设计索引的时候尽可能的要满足三星索引的要求。

第一颗星,where后面的谓词条件能匹配到索引,而且要尽量的匹配一个窄的索引片。

第二颗星,排序要能用到索引,毕竟索引中的数据是一种有序的状态,可以利用索引避免排序。

第三颗星,覆盖索引,使用覆盖索引能大大的减少查询过程中回表的次数。回表对于数据库来说是一个开销较大的操作,往往意味着大量的随机读取,如果select的列通过索引就能获取,那么就只需要对这个索引进行扫描就好,不需要回表做查询。


2、在创建表的时候,如果确认里面的数据不需要存储null值,就应该使用not null约束。

理解:记得《高性能Mysql》中有一段关于not null的描述,Mysql是难以优化引用了可空列的查询,这样会使得查询更加复杂难以优化,况且在可以为null的列中,Mysql还需要额外的一个字节去存储该值是否为null。


3、应尽量避免在where子句中使用or来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,可以使用UNION合并查询:select id from t where num=10 union all select id from t where num=20

理解:对于这个建议,个人有一点点不同的看法。在这个例子中使用的都是num,等同于num in (10,20),不需要做其它的union操作。就算使用了不同的索引列比如A=1 or B=2问题也不大,在5.1之前的mysql中,是只能使用一个索引的,在这种情况下很显然需要把or拆成两个查询然后做并集操作。但是在5.1的版本中就已经引入了索引合并(index merge),所谓的索引合并就是利用多个索引分别进行扫描,然后将他们的结果进行合并(intersect/union) 在5.5~5.7的版本中均做过测试,在A=1 or B=2这种类型的SQL中,都可以用到索引合并技术

mysql> explain select * from country where country_id='14' or country='Zambia';
+----+-------------+---------+------------+-------------+---------------------+---------------------+---------+------+------+----------+-----------------------------------------------+
| id | select_type | table   | partitions | type        | possible_keys       | key                 | key_len | ref  | rows | filtered | Extra                                         |
+----+-------------+---------+------------+-------------+---------------------+---------------------+---------+------+------+----------+-----------------------------------------------+
|  1 | SIMPLE      | country | NULL       | index_merge | PRIMARY,idx_country | PRIMARY,idx_country | 2,152   | NULL |    2 |   100.00 | Using union(PRIMARY,idx_country); Using where |
+----+-------------+---------+------------+-------------+---------------------+---------------------+---------+------+------+----------+-----------------------------------------------+

通过optimizer_trace的分析也可以看出,最终or在执行的时候是通过索引扫描完成的


4、in和not in也要慎用,否则会导致全表扫描,对于连续的数值,能用between就不要用in了:Select id from t where num between 1 and 3。 blog.csdn.net/u011781769/…

理解:创建测试数据

mh01@3306>show create table abc;
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                              |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| abc   | CREATE TABLE `abc` (
  `c1` int(11) NOT NULL AUTO_INCREMENT,
  `c2` varchar(256) DEFAULT NULL,
  KEY `c1` (`c1`)
) ENGINE=InnoDB AUTO_INCREMENT=1002 DEFAULT CHARSET=utf8 |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

在对表进行explain select * from abc where c1 between 100 and 104;和explain select * from abc where c1 in (100,101,102,103,104);的时候,他们的执行计划都是一样的。(不贴图了)

分别使用optimizer_trace验证性能: 使用between的方式:

从中可以发现,between在后台被转换为了100<=c1<=104的方式,总共获取了5行数据,开销为7.01

使用in的方式

可以发现使用in的方式在后台是被作为5个between .. and的方式,开销为11.01,远大于使用between的7.01,可以证明between的效率的确比in要高。


5、下面的查询也将导致全表扫描:select id from t where name like‘%abc%’或者select id from t where name like‘%abc’

理解:这个是很显然的问题,不满足最左匹配原则。


6、如果在where子句中使用计算,也会导致全表扫描

理解:这个规范在《高性能Mysql》中有详细的说明,如果查询中的列不是独立的,则Mysql就不会使用索引。独立的列指的是索引列不能是表达式的一个部分,也不能是函数的参数。

mh01@3306>explain select * from sakila.country where country_id=10;
+----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table   | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | country | NULL       | const | PRIMARY       | PRIMARY | 2       | const |    1 |   100.00 | NULL  |
+----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

mh01@3306>explain select * from sakila.country where country_id + 1=11;
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | country | NULL       | ALL  | NULL          | NULL | NULL    | NULL |  109 |   100.00 | Using where |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

7、很多时候用exists代替in是一个好的选择:select num from a where num in(select num from b)。用下面的语句替换:select num from a where exists(select 1 from b where num=a.num)

理解:对于exists和in来说,不能单纯的说用exists代替in就好好,主要还是要看两个表数据量的问题,核心的思想就是小表驱动大表。比如说在原文中提到的例子,如果说b的数据量要小于a表,那么in的方式就是合适的,因为in是先执行子查询里面的内容,类似于用子查询去驱动外层查询。

如果说a表的数据量要小于b表,那么就应该使用exists,对于exists来说,是用外层查询去驱动内部的查询。把外部的一条数据传递到内层查询中,如果成立就返回。

下面使用sakila示例数据库做一个简单的测试

mh01@3306>select count(*) from film_actor;
+----------+
| count(*) |
+----------+
|     5462 |
+----------+
1 row in set (0.01 sec)
mh01@3306>select count(*) from actor where last_name like 'A%';
+----------+
| count(*) |
+----------+
|        7 |
+----------+
1 row in set (0.00 sec)

在这种情况下如果想要获取last_name为A开头的人对应的film信息,就有两种方式,也就是用in或者是exists的方式。区别就是驱动表不同。现在测试一下两种方式的开销

使用in的方式:

使用exists的方式:

可以看到使用exist方式的cost明显要比in的方式高


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

理解:太多的索引很显然对数据的插入有影响,毕竟需要额外维护索引,同时也需要更多的存储空间。但在《数据库索引设计与优化》这本书中提到一个思想,就是不应该对索引的个数有一个硬性的要求,如果对提高业务的RT时间由帮助(尤其是以读为主的业务),就算多几个索引也是没有关系的,优化的核心思想不在于CPU使用率下降了多少,IO繁忙度下降了多少,关键在于用户感知度也就是响应时间是否有提高。

在这里我简单的测试了一下索引过多对insert的影响:

--表结构如下:
 CREATE TABLE `abc` (
  `c1` int(11) NOT NULL AUTO_INCREMENT,
  `c2` varchar(256) DEFAULT NULL,
  `c3` varchar(256) DEFAULT NULL,
  `c4` varchar(256) DEFAULT NULL,
  `c5` varchar(256) DEFAULT NULL,
  `c6` varchar(256) DEFAULT NULL,
  `c7` varchar(256) DEFAULT NULL,
  `c8` varchar(256) DEFAULT NULL,
  `c9` varchar(256) DEFAULT NULL,
  `c10` varchar(256) DEFAULT NULL,
  `c11` varchar(256) DEFAULT NULL,
  KEY `c1` (`c1`));
  

在这个结构中插入100W的数据,使用mysqladmin监控到insert速度大约为14000

[root@mh01 ~]# mysqladmin ext -i 2 -r | grep -i Com_insert
| Com_insert                                    | 13855                                            |
| Com_insert_select                             | 0                                                |
| Com_insert                                    | 14963                                            |
| Com_insert_select                             | 0                                                |
| Com_insert                                    | 12624                                            |
| Com_insert_select                             | 0                                                |

修改表结构,多加一些索引

 CREATE TABLE `abc` (
  `c1` int(11) NOT NULL AUTO_INCREMENT,
  `c2` varchar(256) DEFAULT NULL,
  `c3` varchar(256) DEFAULT NULL,
  `c4` varchar(256) DEFAULT NULL,
  `c5` varchar(256) DEFAULT NULL,
  `c6` varchar(256) DEFAULT NULL,
  `c7` varchar(256) DEFAULT NULL,
  `c8` varchar(256) DEFAULT NULL,
  `c9` varchar(256) DEFAULT NULL,
  `c10` varchar(256) DEFAULT NULL,
  `c11` varchar(256) DEFAULT NULL,
  KEY `c1` (`c1`),
  KEY `c2` (`c2`),
  KEY `c3` (`c3`),
  KEY `c4` (`c4`),
  KEY `c5` (`c5`),
  KEY `c6` (`c6`),
  KEY `c7` (`c7`),
  KEY `c8` (`c8`),
  KEY `c9` (`c9`),
  KEY `c10` (`c10`),
  KEY `c11` (`c11`)
);

此时会发现insert的速度有一个明显的下降

[root@mh01 ~]# mysqladmin ext -i 2 -r | grep -i Com_insert
| Com_insert                                    | 1571723                                          |
| Com_insert_select                             | 1                                                |
| Com_insert                                    | 10091                                            |
| Com_insert_select                             | 0                                                |
| Com_insert                                    | 9939                                             |
| Com_insert_select                             | 0                                                |
| Com_insert                                    | 10254                                            |
| Com_insert_select                             | 0                                                |

9、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。

理解:如果是数字类型的数据就不要设计为字符类型,Mysql更加擅长处理数字类型的数据类型。另外,如果本身是数字类型的数据被定义为字符类型,那么该列做表关联的时候可能会发生数据类型转换导致索引失效。


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

理解:相对来说不是很赞同这个规范,作为一个一个变长的字段类型,在处理行长度相差较大的环境中有天生的优势,比如能节省空间,节省内存和CPU等等。但是varchar也有一些缺点,比如它更容易产生碎片等,而且更加浪费空间(对于行长度小且行长度变化不大的来说)因为他需要额外的一到两个字节来存储长度。总的来说如果应用具有如下特点可以使用varchar:

  • 行长度变化很大的
  • 变更不是太频繁的

以下特点的可以使用char:

  • 定长的,比如就存一个Y、N,或者是MD5这种类型的数据
  • 经常变更的

设计非变长字符的时候特别要注意考虑拓展性,在Mysql中做数据类型的变更是不能通过online ddl完成的,是采用copy + not allow dml方式创建的。


11、最好不要使用返回所有:select * from t,用具体的字段列表代替“*”,不要返回用不到的任何字段。

理解:自己能做的事情就不要交给Mysql了,Mysql在需要把*替换为真实的字段,而且*是肯定不能用到覆盖索引的

参考

blog.csdn.net/u011781769/…