开局准备,先创建示例表和示例数据
‐‐ 示例表:
CREATE TABLE ` t1` (
` id` INT ( 11 ) NOT NULL AUTO_INCREMENT ,
` a` INT ( 11 ) DEFAULT NULL ,
` b` INT ( 11 ) DEFAULT NULL ,
PRIMARY KEY ( ` id` ) ,
KEY ` idx_a` ( ` a` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
CREATE TABLE t2 LIKE t1;
‐‐ 插入一些示例数据,往 t1表插入 1 万行记录
DROP PROCEDURE
IF
EXISTS insert_t1;
delimiter ; ;
CREATE PROCEDURE insert_t1 ( ) BEGIN
DECLARE
i INT ;
SET i = 1 ;
WHILE
( i <= 10000 ) DO
INSERT INTO t1 ( a, b )
VALUES
( i, i ) ;
SET i = i + 1 ;
END WHILE ;
END ; ;
delimiter ;
CALL insert_t1 ( ) ;
‐‐ 往 t2表插入 100 行记录
DROP PROCEDURE
IF
EXISTS insert_t2;
delimiter ; ;
CREATE PROCEDURE insert_t2 ( ) BEGIN
DECLARE
i INT ;
SET i = 1 ;
WHILE
( i <= 100 ) DO
INSERT INTO t2 ( a, b )
VALUES
( i, i ) ;
SET i = i + 1 ;
END WHILE ;
END ; ;
delimiter ;
CALL insert_t2 ( ) ;
分页优化
这里分页查询第10000条后的10条数据。这里最后看似最后只查询展示了10条数据,其实这里查询出来10010条数据,然后取其最后10条数据返回展示,因此对一个大表进行分页查询效率会是非常慢的。
select * from employees limit 10000 , 10 ;
优化一:如果自增ID是连续且不间断 的,那么可以先使用ID进行排查其结果在进行分页查询。但是这种在实际场景中并不实用,因为数据的删除缺失会导致自增ID间断缺失 ,导致其结果不一致。
select * from employees where id > 10000 limit 10 ;
优化二:这里看解释结果并没有使用索引,原因是:mysql优化器认为扫描整个索引并查找到没有索引的行(会遍历多个索引树)的成本比全表扫描的成功更高,所以就没有走索引查询。所以这里的优化排序是返回的字段尽量少,所以就先查询出主键,在根据主键ID查询数据行。原sql使用Using filesort文件排序,优化后使用Using index索引排序。
EXPLAIN select * from employees ORDER BY name limit 90000 , 5 ;
EXPLAIN select a. * from employees a INNER JOIN ( select id from employees ORDER BY name limit 90000 , 5 ) b on b. id = a. id;
mysql的表关联常见的有两种算法
嵌套循环链接算法(Nested-Loop join 简称:BLJ)
一次一行循环的从第一张表(驱动表)中读取行,在这行数据中取到关联字段,根据关联字段的另一张表(被驱动表)里取出满足条件的行,然后取出两张表结果合集;
EXPLAIN select * from t1 inner join t2 on t1. a= t2. a;
从上面的执行结果可以看出,t2驱动表,t1是被驱动表。先执行的就是驱动表(执行计划结果的id如果一样则按照从上往下的顺序执行sql),优化器一般会选择小表作为驱动表,所以使用inner join时,排在前面的不一定是驱动表。 当使用left join时,左表是驱动表,右表示被驱动表。当使用right join时,右表示驱动表,左表是被驱动表。当使用join时,mysql会使用小表为渠道表,大表为被驱动表。 使用了NLJ算法。一般join语句中,如果执行计划未出现Using join buffer则表示使用join算法是NLJ算法。 所以上面的sql执行流程:
先在t2中查询数据(如果有筛选条件,那么就先过滤条件),然后根据关键字a到t1中查找,取出t1中满足条件的数据和t2的结果进行合集返回,然后重复之前步骤。 整个过程t2会扫描100次数据,然后根据a条件到t1中也去扫描(一条t2数据的a扫描出一条t1数据),所以t1也扫描了100次,因此整个过程扫描200行。 如果被驱动表关联字段没有使用索引,那么NLJ算法效率会比较低,mysql就会使用BNL算法。
基于块的嵌套循环链接算法(Block Nested-Loop join 简称:BNL)
把驱动表的数据读到join_buffer中,然后扫描被驱动表,把被驱动表的每一行数据跟join_buffer中的数据做对比。
EXPLAIN select * from t1 inner join t2 on t1. b= t2. b;
语句出现Using join buffer说明使用BNL算法,大致sql执行流程是:
先把t2的查询出的数据放到join_buffer,再把t1中数据一行一行的和join_buffer做对比,最后返回满足条件的数据合集。 在整个过程中对t1和t2做的都是全表扫描,所以扫描了10100条数据,而且join_buffer内数据是无序的,所以对t1中数据都要做100判断,所以内存中判断次数是100*10000次。 这里join_buffer大小由join_buffer_size设定,默认256k,如果这个驱动表t2也是大表,导致join_buffer放不下,那么mysql用分段存放数据,比如如果t2有2000条数据,但是join_buffer只能放1000条,那么就先放1000条数据到join_buffer中,然后t1依次判断并得到部分结果,然后清空join_buffer,再放入t2剩余1000条数据,t1再次进行判断得到结果,所以多了一次对t1的扫描。 如果这里使用BLJ算法,那么100万次的磁盘扫描是非常慢的,所以这里使用BNL算法。
所以mysql关联优化
尽量让mysql做join操作时走NLJ算法(索引);然后就是小表驱动大表。 小表的定义是对关联表的过滤筛选后数据量小的表为小表,就应该作为驱动表。
in 和 exsits优化
示例一:select * from A where id in ( select id from B)
示例二:select * from A where exists ( select 1 from B where B. id = A. id)
in:当B表数据集小于A表时,使用in优先于exists exists:当A表数据集小于B表时,exists优先于in exists() 只返回true或者false。因此子查询中select *可以改为select 1,mysql官方说法实际执行时也会忽略子查询的select字段,因此这里没有区别。 exists() 中子查询的实际执行过程可能会经过优化而不是我们理解上的逐条对比。
count(*)优化
set global query_cache_size= 0 ;
set global query_cache_type= 0 ;
EXPLAIN select count ( 1 ) from employees;
EXPLAIN select count ( id) from employees;
EXPLAIN select count ( name) from employees;
EXPLAIN select count ( * ) from employees;
四个操作执行计划结果都是一样的,说明效率上差不多:
字段有索引:count(*) = count(1) > count(字段) > count(id);字段有索引,count(字段)走二级索引,二级索引存储数据比主键索引少,所以count(字段) > count(id)。 字段无索引:count(*) = count(1) > count(id) > count(字段);字段无所以,count(字段)不走索引,count(id)走主键索引,所以count(id) > count(字段)。 count(1)和count(字段)类似,但是count(1)不用取出数据,而count(字段)还要取出数据,所以理论上count(1)比count(字段)要快。 count(),mysql不会把所有字段取出来,而专门做了优化,不取值按行累加,效率很高,所以不需要count(字段)或count(1)来代替count( )。 对于count(id),二级索引相对于主键索引存储数据少,所以检索性能更高,mysql内部做了优化(5.7之后)。
mysql数据类型
数值类型
类型 大小 范围(有符号) 范围(无符号) 作用 tinyint 1字节 (-128, 127) (0, 255) 小的整数型 SMALLINT 2 字节 (-32 768, 32 767) (0, 65 535) 大整数值 MEDIUMINT 3 字节 (-8 388 608, 8 388 607) (0, 16 777 215) 大整数值 INT或INTEGER 4 字节 (-2 147 483 648, 2 147483 647) (0, 4 294 967 295) 大整数值 BIGINT 8 字节 (-9 233 372 036 854 775 808, 9 223 372 036 854 775 807) (0, 18 446 744 073 709 551 615) 极大整数值 FLOAT 4 字节 (-3.402 823 466 E+38, 1.175 494 351 E38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) 0, (1.175 494 351 E-38, 3.402 823 466 E+38) 单精度 浮点数值 DOUBLE 8 字节 (1.797 693 134 862 315 7 E+308, 2.225 073 858 507 201 4 E-308), 0, (2.225 073 858 507 201 4E-308, 1.797 693 134 862 315 7 E+308) 0, (2.225 073 858 507 201 4 E308, 1.797 693 134 862 315 7 E+308) 双精度 浮点数值 DECIMAL 对 DECIMAL(M,D) ,如果M>D,为 M+2否则为D+2 依赖于M和D的值 依赖于M和D的值 小数值
优化建议
如果整形数据没有负数,如ID号,建议指定为UNSIGNED无符号类型,容量可以扩大一倍。 建议使用TINYINT代替ENUM、BITENUM、SET。 避免使用整数的显示宽度(参看文档最后),也就是说,不要用INT(10)类似的方法指定字段显示宽度,直接用INT。 DECIMAL最适合保存准确度要求高,而且用于计算的数据,比如价格。但是在使用DECIMAL类型的时候,注意长度设置。 建议使用整形类型来运算和存储实数,方法是,实数乘以相应的倍数后再操作。 整数通常是最佳的数据类型,因为它速度快,并且能使用AUTO_INCREMENT。
时间类型
类型 大小(字节) 范围(有符号) 格式 用途 DATE 3 1000-01-01 到 9999-12-31 YYYY-MM-DD 日期值 TIME 3 ‘-838:59:59’ 到 ‘838:59:59’ HH:MM:SS 时间值或持续时间 YEAR 1 1901 到 2155 YYYY 年份值 DATETIME 8 1000-01-01 00:00:00 到 9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS 混合日期和时间值 TIMESTAMP 4 1970-01-01 00:00:00 到 2038-01-19 03:14:07 YYYYMMDDhhmmss 混合日期和时间值,时间戳
优化建议
MySQL能存储的最小时间粒度为秒。 建议用DATE数据类型来保存日期。MySQL中默认的日期格式是yyyy-mm-dd。 用MySQL的内建类型DATE、TIME、DATETIME来存储时间,而不是使用字符串。 当数据格式为TIMESTAMP和DATETIME时,可以用CURRENT_TIMESTAMP作为默认(MySQL5.6以后),MySQL会自动返回记录插入的确切时间。 TIMESTAMP是UTC时间戳,与时区相关。 DATETIME的存储格式是一个YYYYMMDD HH:MM:SS的整数,与时区无关,你存了什么,读出来就是什么。 除非有特殊需求,一般的公司建议使用TIMESTAMP,它比DATETIME更节约空间,但是像阿里这样的公司一般会用DATETIME,因为不用考虑TIMESTAMP将来的时间上限问题。 有时人们把Unix的时间戳保存为整数值,但是这通常没有任何好处,这种格式处理起来不太方便,我们并不推荐它。
字符串类型
类型 大小(字节) 用途 CHAR 0-255字节 定长字符串,char(n)当插入的字符串实际长度不足n时,插入空格进行补充保存。在进行检索时,尾部的空格会被去掉。 VARCHAR 0-65535 字节 变长字符串,varchar(n)中的n代表最大列长度,插入的字符串实际长度不足n时不会补充空格 TINYBLOB 0-255字节 不超过 255 个字符的二进制字符串 TINYTEXT 0-255字节 短文本字符串 BLOB 0-65 535字节 二进制形式的长文本数据 TEXT 0-65 535字节 长文本数据 MEDIUMBLOB 0-16 777 215字节 二进制形式的中等长度文本数据 MEDIUMTEXT 0-16 777 215字节 中等长度文本数据 LONGBLOB 0-4 294 967 295字节 二进制形式的极大文本数据 LONGTEXT 0-4 294 967 295字节 极大文本数据
优化建议
字符串的长度相差较大用VARCHAR;字符串短,且所有值都接近一个长度用CHAR。 CHAR和VARCHAR适用于包括人名、邮政编码、电话号码和不超过255个字符长度的任意字母数字组合。那些要用来计算的数字不要用VARCHAR类型保存,因为可能会导致一些与计算相关的问题。换句话说,可能影响到计算的准确性和完整性。 尽量少用BLOB和TEXT,如果实在要用可以考虑将BLOB和TEXT字段单独存一张表,用id关联。 BLOB系列存储二进制字符串,与字符集无关。TEXT系列存储非二进制字符串,与字符集相关。 BLOB和TEXT都不能有默认值。
INT显示宽度
我们经常会使用命令来创建数据表,而且同时会指定一个长度,如下。但是这里的长度并非是TINYINT类型存储的最大长度,而是显示的最大长度。
1 CREATE TABLE ` user ` (
2 ` id` TINYINT ( 2 ) UNSIGNED
3 ) ;
这里表示user表的id字段的类型是TINYINT,可以存储的最大数值是255。所以,在存储数据时,如果存入值小于等于255,如200,虽然超过2位,但是没有超出TINYINT类型长度,所以可以正常保存;如果存入值大于255,如500,那么 MySQL会自动保存为TINYINT类型的最大值255。 在查询数据时,不管查询结果为何值,都按实际输出。这里TINYINT(2)中2的作用就是,当需要在查询结果前填充0时,命令中加上ZEROFILL就可以实现,如:
1 ` id` TINYINT ( 2 ) UNSIGNED ZEROFILL
这样,查询结果如果是5,那输出就是05。如果指定TINYINT(5),那输出就是00005,其实实际存储的值还是5,而且存储的数据不会超过255,只是MySQL输出数据时在前面填充了0。 换句话说,在MySQL命令中,字段的类型长度TINYINT(2)、INT(11)不会影响数据的插入,只会在使用ZEROFILL时有用,让查询结果前填充0。
其他优化方式
查询mysql表的总行数:如果是myisam储存引擎,那么不带where的sql查询性能最高,因为myisam会存储总行数到磁盘上,不需要做额外的统计计算。而innodb会实时计算总行数。
show table status like 'employees' ;
show table status 可以得到总行数,而且性能也很高。