可以从以下维度对数据库进行优化:
虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化和逻辑查询优化两大块。
其实,用不用索引,最终都是优化器说了算。优化器是基于cost开销(通过JSON格式可以看到开销数据)的,它不是基于规则,也不是基于语义。怎么样开销小就怎么来。另外,SQL语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。
EXPLAIN可以输出四种格式: 传统格式 , JSON格式 , TREE格式 以及可视化输出 。用户可以根据需要选择适用于自己的格式。
创建两个表,通过函数和存储过程填充数据。
CREATE TABLE `class` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`className` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
`monitor` INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `student` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`stuno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`classId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
#CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
// 查询语句
SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classid=4 AND NAME= 'abcd';
// 分别建立不同的索引
CREATE INDEX idx_age ON student (age) ; // 索引1
CREATE INDEX idx_age_classid oN student (age,classid); // 索引2
CREATE INDEX idx_age_classid_name ON student (age,classid,NAME); // 索引3
当数据量非常大时,发现使用索引3的执行时间最短。也就是说,尽量将WHERE后的字段都建立索引(如果有多个,建立联合索引)。
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student.name = 'abcd';
针对于上面的查询语句,虽然已经创建了索引idx_age_classid_name,但是WHERE后没有使用age,所以无法使用此索引。这一点可以从索引数据结构的角度来解释,创建索引idx_age_classid_name时,B+树首先根据age来排序,如果age相同,再根据classid,如果classid相同,再根据name。而WHERE后没有使用age,所以无法从此B+树获取结果,从而无法使用索引。

如果此时再插入一条主键值为 9 的记录,那它插入的位置就如下图:

可这个数据页已经满了,再插进来咋办呢?我们需要把当前 页面分裂 成两个页面,把本页中的一些记录移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗 !所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的 主键值依次递增 ,这样就不会发生这样的性能损耗了。所以我们建议:让主键具有 AUTO_INCREMENT ,让存储引擎自己为表生成主键,而不是我们手动插入 。
CREATE INDEX idx_sno ON student(stuno);
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;
CREATE INDEX idx_name ON student(NAME);
EXPLAIN SELECT id, stuno, name FROM student WHERE SUBSTRING(name, 1,3)='abc';
CREATE INDEX idx_name ON student(NAME);
# 未使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123;
# 使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123';
因为name的类型为varchar,name=123会发生类型转换
create index idx_age_classid_name on student(age,classid,name);
// 此时使用到联合索引的age和id,name失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ;

使用到age和id,这两个字段的类型均为INT,所以key_len为4 + 1(NULL) + 4 + 1(NULL)
create index idx_age_name_classid on student(age,name,classid);
// 此时可以使用到age + name + id
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abc' AND student.classId>20 ;

开发中范围查询,例如:金额查询,日期查询往往都是范围查询。应将查询条件放置where语句最后(
创建的联合索引中,务必把范围涉及到的字段写在最后)
不等于的时候又要去一个一个找,所以使用不上索引。
这个道理和1.7一样
最好在设计数据表时就将字段设置为
NOT NULL约束,比如可以将INT类型的字段,默认值设置为0;将字符类型的字段默认值设置为空字符串(‘’)
OR前后的两个条件中的列都是索引时,查询中才使用索引。只有一个条件列进行了索引是没有意义的,只要有条件列没有进行索引,就会进行全表扫描,因此索引的条件列也会失效。当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。公司里暂时用不到,以后用到再学习
FileSort和Index排序。Index排序中,索引可以保证数据的有序性,不需要再进行排序,效率更高。全表扫描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。场景:查询年龄为30岁的,且学生编号小于101000的学生,按用户名称排序
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
优化思路:
为了去掉filesort我们可以把索引建成:
// 索引1
CREATE INDEX idx_age_name ON student(age,NAME);
// 索引2
CREATE INDEX idx_age_stuno_name ON student (age,stuno,NAME);
在explain结果中可以看到,使用索引1时,Extra中没有filesort;使用索引2时,Extra中有filesort,但是使用索引2的速度却高于使用索引1。
所有的排序都是在
条件过滤之后才执行的。所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际提升性能很有限。相对的stuno<101000这个条件,如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比最高,是最优选择。
当【范围条件】和【group by或者order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。
双路排序
首先从磁盘中只读取order by后面用到的排序字段,在sort_buffer中排好序;然后从磁盘中读取其他字段,得到结果;
取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的
单路排序
从磁盘读取查询需要的 所有列 ,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间, 因为它把每一行都保存在内存中了。
在sort_buffer中,单路比多路要多占用很多空间,因为单路是把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排…从而
多次I/O。
sort_buffer_sizemax_length_for_sort_dataselect * 是一个大忌。最好只Query需要的字段。最佳左前缀法则max_length_for_sort_data 和 sort_buffer_size 参数的设置Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
- 索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。
一个索引包含了满足查询结果的数据就叫做覆盖索引。- 非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。简单说就是,
索引列+主键包含SELECT 到 FROM之间查询的列。
CREATE INDEX idx_age_name ON student (age,NAME);
EXPLAIN SELECT * FROM student WHERE age <> 20;

如上图所示,没有使用索引,符合以前说的索引失效情形;
EXPLAIN SELECT age,NAME FROM student WHERE age <> 20;

当查询条件改变之后,使用了索引。
这是因为情形1,优化器认为使用索引的成本(二级索引运算+回表)>全表扫描的成本,所以不使用索引;而情形2中,使用覆盖索引,不需要回表操作,此时优化器认为使用索引的成本更低。
避免了对主键的二次查询,减少了IO操作提升了查询效率。范围查找来说,对比随机从磁盘读取每一行的数据IO要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO转变成索引查找的顺序IO。索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作。
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
准备工作:
CREATE TABLE `people` (
`id` INT NOT NULL AUTO_INCREMENT,
`zipcode` VARCHAR(20) COLLATE utf8_bin DEFAULT NULL,
`firstname` VARCHAR(20) COLLATE utf8_bin DEFAULT NULL,
`lastname` VARCHAR(20) COLLATE utf8_bin DEFAULT NULL,
`address` VARCHAR(50) COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `zip_last_first` (`zipcode`,`lastname`,`firstname`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;
INSERT INTO `people` VALUES
('1', '000001', '三', '张', '北京市'),
('2', '000002', '四', '李', '南京市'),
('3', '000003', '五', '王', '上海市'),
('4', '000001', '六', '赵', '天津市');
查询语句为:
EXPLAIN SELECT * FROM people
WHERE zipcode='000001'
AND lastname LIKE '%张%'
AND address LIKE '%北京市%';
结果为:

- 在Extra中有“
using index condition”,表示使用了索引下推。- 索引zip_last_first中包含了字段zipcode,lastname,firstname,当查询时,可以直接使用到zipcode字段,假设经过zipcode过滤后的数据有10000条,就意味着要回表再去查这10000条数据。但是此时发现在索引中包括lastname,所以直接在存储引擎中对lastname进行过滤,假设过滤后的数据只剩下10条,此时再去回表查的成本就大大降低了。

问:在 MySQL 中统计数据表的行数,可以使用三种方式: SELECT COUNT(*) 、 SELECT COUNT(1) 和SELECT COUNT(具体字段) ,使用这三者之间的查询效率是怎样的?


在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:
查询数据字典 将"*"按序转换成所有列名,这会大大的耗费资源和时间。覆盖索引还有一些主键的设计原则,以淘宝为例,后续如果会用到再去看视频。