针对数据库的优化,我们不能单纯的说从哪一个方面,需要结合数据表的建立,数据类型的选择,索引的设计和sql语句来考虑,我就针对怎么建表,怎么选择数据类型,如何应用B-tree索引,hash索引和覆盖索引的特点来建立高效的索引策略,然后我具体对 count()查询,最大最小值查询,关联查询,子查询,GROUP BY ,limit 分页,Union查询做一些具体的说明,最后我说一下怎样使用切分查询和分解关联查询来重构我们的查询方式,
一、数据表的设计
首先我们要根据范式化和反范式化各自的优缺点,选择一个最佳的设计。
(反范式化设计,一个典型列子就是缓存表,它的特点就是在不同的表中存储相同的列,它很好的避免了关联,但是增加了许多冗余的数据,插入和更新可能会慢一些,而范式化设计的数据表通常比较小,很少有冗余的数据,插入更新也比反范式化设计的表要快,但是通常需要做关联操作。)
二、数据类型的选择:
我主要说3点
1、选择简单的,不超过范围的最小类型,避免使用 NULL(可为NULL的列使得索引,索引统计和值比较都更复杂,还会使用更多的存储空间,在mysql里也需要特殊处理)
Example:
(1)日期时间类型最好使用 timestamp 而不用datetime存储,datetime使用了8个字节的存储空间,而timestamp 只使用4个字节的存储空间,可以使用 FROM_UNIXTIME() 和 UNIX_TIMESTAMP()进行相互转化。
(2)IP地址是一个 32 位无符号整数,应该 unsigned int 来存储,不应该使用 varchar(15) 来存储,可以使用 INET_NTOA() 和INET_ATON() 两个函数来相互转化。
2、注意char 和 varchar 的区别,(varchar适合存储最大长度比平均长度大很多和列的更新少,不容易产生碎片的数据,而char适合存储短的,所有值接近同一个长度的数据,对于经常变更的数据也适合用char存储)
3、对于相似或相关的值尽量使用同种数据类型存储,特别是在关联条件中使用的列。
三、高效的索引策略
MyISAM,InnoDB和memory存储引擎都支持B-tree索引,memory支持hash索引,InnoDB支持聚簇索引,那么我们就可以根据它们所支持的索引类型和数据的存储结构来设计高效的索引策略。
1、首先我说说 hash 索引,因为hash查找非常快,hash索引是基于哈希表实现,存储引擎会对所有的索引列计算一个哈希码,哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存每个指向数据行的指针。我们就可以利用它的这个特点来创建一个自定义hash索引提高查询效率。
举个列子,假如现在有一个address表,里面存储大量的 url,而且需要根据url进行搜素查找,我需要执行一条这样的查询:
SELECT id FROM address WHERE url=’http://www.mysql.com’;
那么我直接在url上面建立一个索引好不好呢,答案是肯定能提高查询效率,但是由于这个字段太长,直接用它做索引不是最佳选择,我们应该删除url上的索引,再数据表中增加一个字段crc_url,使用CRC32做hash,然后执行下面的查询:
SELECT id FROM address WHERE url=’http://www.mysql.com’ AND url_crc = CRC32(“http://www.mysql.com”);
这样做性能会非常高,因为mysql优化器会使用这个选择性更高而且体积更小的基于url_crc列的索引来完成查找,只需要根据哈希值做快速的整数比较就可找到索引条目,然后返回对应的列,哈希值我们可以用触发器来维护,
CREATE TRIGGER address_hash_crc_ins BEFORE INSERT ON address FOR EACH ROW BEGIN SET NEW.crc_url = crc32(NEW.url); CREATE TRIGGER address_hash_crc_upd BEFORE UPDATE ON address FOR EACH ROW BEGIN SET NEW.crc_url = crc32(NEW.url);
当然InnoDB有一个特殊的功能叫“自适应哈希索引,当InnoDB注意到某些索引使用的非常频繁是就会在内存中基于B-tree索引在创建一个哈希索引,让B-tree索引页具有hash索引的一些优点”。
当然虽然哈hash索引查找速度非常快。但是也有它的局限性由于它不是按照索引值顺序存储的,所以它无法用于排序操作,也不支持部分索引列匹配查找,因为hash索引始终是使用索引列的全部内容来计算hash值的,哈希索引只支持等值比较查询。
2、在说一下InnoDB支持的聚簇索引,聚簇索引的数据实际上是保存在索引的叶子页中,我先说说MyISAM和InnoDB的数据存储结构:
举个列子:
假如现在有一个这样的查询:
SELECT * FROM questions WHERE question_userid=1 AND question_title LIKE ‘%PHP试题%’;
当数据量很大的时候,返回的行有比较少时,查询速度回很慢,我们需要重写查询并巧妙的设计索引,我需要先添加一个多列索引 userid_title(question_userid,question_title),我将查询改成这样:
SELECT * FROM questions JOIN (SELECT question_id FROM questions WHERE question_userid=1 AND question_title LIKE ‘%PHP试题%’) AS t1 ON(t1.question_id = questions.question_id);
使用这种延迟关联的查询方式,查询的第一阶段mysql可以使用覆盖索引,然后根据这些question_id值在外层查询匹配获取需要的所有值。
四、创建高效的sql语句,
1、优化count
如果是MyISAM存储引擎,而且没有任何的where条件,我们直接使用count(*),mysql会利用存储引擎的特点直接获取这个值,还有一个方法就是利用反正特性,比如有这样一个查询:
SELECT COUNT(*) FROM questions WHERE id>5;
此时将语句改写成:
SELECT (SELECT COUNT(*) FROM questions) - COUNT(*) FROM questions WHERE id<5;
这样就可以大大减小需要扫描的行数,查询优化器会直接将子查询当做一个常数来处理,而且查询小于5的数据很少很多,如果只是需要一个粗略值的话可以直接使用EXPLAIN 中优化器估算出的一个行数就可以当做这个近似值。如果需要分别查询单选题题,判断题各有多少道,可以这样写:
SELECT COUNT(question_type=1 OR NULL) AS ‘单选题’, COUNT(question_type=2 OR NULL) AS ‘多选题’ FROM qustions;
2、优化min()
SELECT MIN(question_id) FROM questions WHERE question_userid=1;
改写成:
SELECT question_id FROM questions USE INDEX(PRIMARY) WHERE question_userid=1 LIMIT 1;
这里使用了一个优化器提示 USE INDEX() 来告诉优化器使用主键索引来查询记录,由于数据存储的时候是按照主键值从小到大存储的,那么满足条件的第一条记录一定是最小值。
3、优化关联查询
如果 A 与 B 通过 c 列关联,关联顺序是 B,A,则只需要在A表的c列上建立索引就好,如果在B表上也建索引,那么这种冗余的索引只会带来额外的负担,还要注意它们的关联字段的数据类型尽量用通一种数据类型,如果查询语句中有 ORDER BY 或者 GROUP BY,那么最好只涉及到一个表中的列,这样mysql才有可能使用索引来优化这个过程。
4、优化GROUP BY
当我们需用分组做分组排序的时候,这个时候我们创建索引就应该尽量让它满足既能用于查找行又能用于排序操作,这样就不用创建临时表来做额外的排序操作了,那么优化GROUP BY 最好的方法就是使用松散索引扫描,如果不能满足松散索引扫描的话,也尽量使用紧凑索引扫描,避免使用临时表来排序。
(1)使用松散索引扫描(当mysql完全利用索引扫描来实现GROUP BY的时候,并不需要扫描所有满足条件的索引键就可以完成操作的出结果)
假设有一个表 t1 有4个列 (c1,c2,c3,c4), 给它建立了一个多列索引 (c2,c3,c4);
做类似这样查询 SELECT c2,MAX(c3) FROM t1 GROUP BY c2,c3;
例子:
SELECT question_userid,question_type FROM questions GROUP BY question_userid,question_type;
(2)使用紧凑索引扫描:
SELECT c2,MAX(c3) FROM t1 WHERE c2=const GROUP BY c3,c4; SELECT question_userid,question_type FROM questions WHERE question_userid=1 GROUP BY question_type;
紧凑索引扫描和松散索引扫描的区别在于,紧凑索引扫描需要在扫描索引的时候,读取所有满足条件的索引键,然后在根据读取的数据来完成GROUP BY 操作,它需要访问WHERE条件中所限定的所有索引键之后才能得出结果。这里的GROUP BY并不是一个连续的索引,但是WHERE 条件中的question_userid 是个常数,弥补了缺失的索引,因此使用紧凑索引扫描。
(3)使用临时表
如果这两种中条件都打不到的话,就只能使用临时表来进行排序了,如果数据量小可以直接在内存里进行,数据量大的话可能还需要借助磁盘来完成,如果我们自己清楚临时表的数据量大小,可以使用优化器提示SQL_SMALL_RESULT 或SQL_GIG_RESULT 告诉优化器在哪儿排序。如果我们只需要对结果分组而不需要排序,可以加一句 ORDER BY NULL 来避免GROUP BY 默认的按照分组字段进行排序。
5、优化 UNION
除非确实需要去除重复的行,否则一定要使用 UNION ALL , 因为如果没有ALL,mysql会给临时表加上DISTINCT做唯一性检查,这样做代价非常高,
Mysql在做UNION 查询时,无法将限制条件从外层“下推”到内层,例如:
(SELECT c1,c2 FROM t1 ORDER BY c2) UNION ALL (SELECT c3,c4 FROM t2 ORDER BY c4) LIMIT 20;
假设从t1表中查出了500条记录,从t2表中查出了800 条记录,那么mysql会将这1300 条记录放入临时表在取出20条。因为它无法将limit下推到内层,我们可以这样优化一下:
(SELECT c1,c2 FROM t1 ORDER BY c2 LIMIT 20) UNION ALL (SELECT c3,c4 FROM t2 ORDER BY c4 LIMIT 20) LIMIT 20;
这样临时表就只有40条记录了,再取出20条。
6、优化limit 分页
对于limit的优化,最常用的地方就是分页,假设我们有很多页,有例如 limit 1000,20 这样的查询,那么mysql需要查询1020条记录,然后将前面的1000条都扔掉,然后返回最后20条,这样做代价太高,我们可以这样来优化: 尽可能使用索引覆盖扫描,而不是查询所有的列,然后根据需要做一次关联在返回所需的列:
例如:
SELECT question_name,question_type FROM questions ORDER BY question_inserttime LIMIT 1000,20;
可以这样改:
SELECT question_name,question_type INNOR JOIN (SELECT question_id FROM questions ORDER BY question_inserttime LIMIT 1000,20) AS t1 USEING(question_id);
这里使用了延迟关联,先使用索引覆盖扫描快速找到对应的20个question_id,然后做一次关联操作返回所需的列。
7、子查询
对于子查询最好是使用关联查询来代替。
当然除了这些查询语句优化外,还可以重构查询方式,比如切分查询和分解关联查询;
五、重构查询方式
1、切分查询
所谓的切分查询,就是将一个大查询分而治之,将一个大查询切分成小查询,每个查询的功能完全一样,只完成一小部分,每次只完成一小部分查询结果。
例如我们有一个很大的message表,我们需要定期去删除里面的数据,需要执行这样一条sql:
DELETE FROM message WHERE create_time < DATE_SUB(NOW(),INTVAL 1 MONTH);
我们可以将它改成这样:
$row_effect = 0; do { $sql = “DELETE FROM message WHERE create_time<DATE_SUB(NOW(),INTVAL 1 MONTH) LIMIT 10000”; $row_effect = $db -> execte($sql); }while $row_effect >0
2、分解关联查询
分解关联查询就是将多表关联查询切分成单个查询,这样做可以让缓存命中率更高,而且执行单个查询可以减少锁的竞争。
该文章是我学习了高性能mysql一书后自己做的一个总结,如果有转载请注明出处:http://www.cnblogs.com/chrdai/p/6809369.html