一、存储数据结构

1、B+树

InnoDB引擎使用B+Tree实现。

B+Tree特性:

1)所有关键字都出现在叶子结点的链表中,且链表中的关键字恰好是有序的;

2)不可能在非叶子结点命中;

3)非叶子结点相当于是叶子结点的索引,叶子结点相当于是存储(Key)数据的数据层;

2、主键索引

主键索引,是聚集索引。叶子节点Value部分存储完整数据。

3、非主键索引

非主键索引,是非聚集(稀疏)索引。叶子节点Value部分存储主键。

4、复合(联合)索引

Key按顺序存储所有的复合键。

二、常用数据类型

1、CHAR

  • 定长,选择策略:

1) 固定长度或者平均长度与最大长度相差很小;

2) 字段频繁更新,不易产生碎片;

  • CHAR(n)

n 表示存储的字符数

  • 可使用sql查询数据字节数

SELECT id, LENGTH(<column>) from <table> order by id asc;

2、VARCHAR

  • 不定长,选择策略:

1)平均长度与最大长度相差很大;

2)字段更新少;

  • VARCHAR(n)

n 表示存储的最大字符数;

3、INT

  • 4字节,带符号最大值21亿,可用于id列;

  • INT(n)

n表示宽度,创建列时用zerofill修饰,查询时补0;

4、TINYINT

  • 1字节,可用于枚举;

  • TINYINT(n)

n表示宽度,创建列时用zerofill修饰,查询时补0;

5、DATETIME

  • 常用时间类型。

  • DATETIME(n)

n表示精度,n=3时,表示时间精确到毫秒;

三、查询计划

explain select <columns> from <table>

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1 SIMPLE employees range idx_name_age_position idx_name_age_position 78 1 100.00 Using index condition

1、id列

若存在子查询或者复杂查询,存在多行select;id大的优先级高,相同id的,从上到下执行;id为NULL优先级最低。

2、select_type列

1) simple

简单查询,不包含union和子查询。

2) primary

复杂查询中,最外层select。

3) subquery

select 中的子查询。

4) derived

from 中的子查询。

5)union

union后的select。

3、table列

表名、衍生表derivenN、unionN

4、type列

性能排序一般为:

NULL > system > const > eq_ref > ref > range > index > ALL

type 在range(包含)之前是可接受的,但还需要根据具体场景优化。

1)NULL

查询语句已被查询分析器优化。

2)system、const

主键或唯一键同常量比较

3)eq_ref

主键或唯一键被用于联接查询。

4)ref

非唯一键、非主键索引。

5)range

范围查询。

6)index

全索引扫描,需要优化。

7)ALL

全表扫描,需要优化。

5、possible_keys列

可能会使用的索引,不一定用。

6、key列

使用的索引。

7、key_len列

索引键长度=各索引键长度(字节数)之和。

CHAR(n)、VARCHAR(n):

表字符集 gbk: 2n字节、utf8: 3n字节、utf8mb4: 4n字节

变长:2字节

可空:1字节

例:表字符集utf8mb4,VARCHAR(10) 可空列的长度=4*10+2+1=43。

8、ref列

索引中列引用的列名或者常量。

9、rows列

预估扫描的数据行数。

10、Extra列

额外信息。

1)Using index

通常表示使用覆盖索引。

2)Using where

通常表示没有用索引(type为ALL)。

3)Using index condition

使用联合索引中的部分(前导列),需要回表查询。

4)Using temporary

使用临时表。

5)Using filesort

没有用索引排序,可能是内存排序或者磁盘文件排序。

查看Trace中“sort_mode”字段。

a、单路排序

"sort_mode"为<sort_key,additional_fields>或者<sort_key,packed_addtional_fields>

通过where条件查找到主键id->取出相关列(select、排序、主键等),放入sort_buffer->对sort_buffer中数据排序->return。

b、双路(回表)排序

"sort_mode"为<sort_key,rowid>

通过where条件查找到主键id->取出排序列和主键id,放入sort_buffer->对sort_buffer中数据排序->遍历id,回原表取select列->return。

通过比较查询字段总长度与max_length_for_sort_data(默认1024字节)参数;前者小使用单路排序、前者大使用双路排序。一般单路排序性能更优,但要平衡内存占用与性能。

6)Select tables optimized away

已被查询分析器优化。

explain select max(id) from <table>;

四、Trace工具

用于分析SQL内部优化和执行过程,比较耗性能,用完后需关闭。

#开启trace
set session optimizer_trace="enabled=on",end_markers_in_json=on;
#执行查询语句
select * from xxx where a > '1' order by b;
#获取trace信息
select * from information_schema.OPTIMIZER_TRACE;
#关闭trace
set session optimizer_trace="enabled=off";

主要信息:

"analyzing_range_alternatives": { #索引使用分析
    "range_scan_alternatives": [{
        "index": "idx_a_b_c",
        "ranges": [
            "1 <= a <= 1"
        ] /* ranges */ ,
        "index_dives_for_eq_ranges": true,
        "rowid_ordered": false, #使用主键排序
        "using_mrr": false,
        "index_only": true, #是覆盖索引
        "rows": 1, #扫描函数
        "cost": 1.21, #使用成本
        "chosen": true #是否选择该索引
    }] /* range_scan_alternatives */ ,
"considered_execution_plans": [{ #执行计划
    "plan_prefix": [] /* plan_prefix */ ,
    "table": "`left_prefix`",
    "best_access_path": {
        "considered_access_paths": [{ #可能的路径
                "access_type": "ref", #访问类型
                "index": "idx_a_b_c", #索引
                "rows": 1, #扫描行数
                "cost": 1.2, #使用成本,在当前trace中对比,数值小的性能高
                "chosen": true #是否选择该索引
            },
            {
                "access_type": "range",
                "range_details": {
                    "used_index": "idx_a_b_c"
                } /* range_details */ ,
                "chosen": false,
                "cause": "heuristic_index_cheaper"
            }
        ] /* considered_access_paths */
    } /* best_access_path */ ,
"join_execution": {
        "select#": 1,
        "steps": [
          {
            "filesort_information": [
              {
                "direction": "asc",
                "table": "`left_prefix`",
                "field": "c"
              }
            ] ,
            "filesort_summary": { #文件排序汇总
              "rows": 1, #预计扫描行
              "examined_rows": 1, #排序行
              "number_of_tmp_files": 0, #0表示内存排序,大于0表示磁盘文件排序
              "sort_buffer_size": 261888, #排序缓存区大小
              "sort_mode": "<sort_key, packed_additional_fields>"#单路排序
            } /* filesort_summary */

五、性能优化

1、索引

1.1 原则

1)业务上具有唯一性的字段,需要建立唯一索引,微小性能损失可以忽略。

2)建联合索引时,区分度高的字段放最左边。最左字段区分度至少要达到0.1,大于0.9最佳。

3)varchar列在建索引时必须指定索引列字符长度,一般长度不超过20。内存足够的情况下,区分度可尽量趋近1。

#查询列的最大字符长度
select char_length(<列名>) as length, left(<列名>, <索引列字符长度>) as index_partial, <列名> as column_text from <table> order by length desc limit 100;
#最左字段区分度
select count(distinct left(<列名>, <索引列字符长度>))/count(*) from <table>; 
#不重复值区分度,用于决定要给某列加索引时,确定索引列字符长度
select count(distinct left(<列名>, <索引列字符长度>))/count(distinct <列名>) from <table>;

1.2 脚本

#查询表索引
show index from <table>;
#创建普通索引
create index idx_a_b_c on <table> (a,b,c);
#普通索引,带上索引长度
create index idx_a_b_c on <table> (a(<length>),b(<length>),c(<length>));
#创建唯一索引
create unique index uk_a on <table> (a);
#删除索引
drop index <index_name> on <table>;
#8.0 修改索引可见性,为了观察不使用索引对查询性能的影响
alter table <table_name> alter index <index_name> [visible | invisible]

2、数据准备

#创建表
CREATE TABLE `employees` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
  `age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
  `position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
  `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
  PRIMARY KEY (`id`),
  KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE,
  KEY `idx_hireTime` (`hire_time`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='员工记录表';

CREATE TABLE `left_prefix` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` varchar(20) COLLATE utf8mb4_bin NOT NULL,
  `b` varchar(20) COLLATE utf8mb4_bin NOT NULL,
  `c` varchar(20) COLLATE utf8mb4_bin NOT NULL,
  `d` varchar(20) COLLATE utf8mb4_bin NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_a_b_c` (`a`,`b`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE `left_prefix2` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` varchar(20) COLLATE utf8mb4_bin NOT NULL,
  `b` varchar(20) COLLATE utf8mb4_bin NOT NULL,
  `c` varchar(20) COLLATE utf8mb4_bin NOT NULL,
  `d` varchar(20) COLLATE utf8mb4_bin NULL,
  PRIMARY KEY (`id`),
  KEY `idx_a_b_c` (`a`,`b`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

#给表left_prefix2插入数据 
DELIMITER ;;
CREATE PROCEDURE prod_insert_left_prefix2_data()
BEGIN
DECLARE n INT DEFAULT 0;
WHILE (n <= 5000000) DO
	SET n = n + 1;
	INSERT INTO left_prefix2(a,b,c,d) values('1','2',n,'');
END WHILE;
END;
CALL prod_insert_left_prefix2_data();

3、优化建议

3.1 id列使用整形(int)自增,降低索引node大小。

3.2 NULL判断需要表扫描, 字段应设置NOT NULL加默认值,查询条件转换成 where d = '' 形式。

explain select * from left_prefix2 where a = '1' and d is null;
explain select * from left_prefix2 where a = '1' and d is not null;

3.3 索引满足最左前缀法则。

查询条件匹配索引前N个列,不跳过中间列,范围查询列或者排序(分组)条件列放最后。

执行计划:

#eq
explain select * from left_prefix where a = '1' and b = '2' and c = '3'; #使用a、b、c
explain select * from left_prefix where a = '1' and c = '3' and b = '2'; #使用a、b、c
explain select * from left_prefix where a = '1' and b = '2'; #使用a、b
explain select * from left_prefix where a = '1' ; #使用a
explain select * from left_prefix where a = '1' and c = '3'; #使用a

explain select * from left_prefix where b = '2' and c = '3';#索引扫描,性能差
explain select * from left_prefix where c = '3';#索引扫描,性能差

#like前缀匹配时,近似看成 '=' ,但 'type' = 'range'
explain select  * from left_prefix where a = '3' and b like 'xxx%' and c = '3'; #使用a、b、c
explain select  * from left_prefix where a = '3' and b like 'xxx%xx%' and c = '3'; #使用a、b、c
explain select  * from left_prefix where a = '3' and b like '%xx%' and c = '3'; #使用a,建议使用搜索引擎
explain select  * from left_prefix where a = '3' and b like '%xx' and c = '3'; #使用a,建议使用搜索引擎

#范围查询
explain select * from left_prefix where a = '1' and b = '2' and c > '3'; #使用a、b、c
explain select * from left_prefix where a = '1' and b > '2'; #使用a、b
explain select * from left_prefix where a = '1' and b in ('2'); #使用a、b
explain select * from left_prefix where a = '1' and b in ('2','3'); #非整形主键索引,一般不走索引,全表扫描
explain select a,b,c from left_prefix where a = '1' and b in ('2','3'); #使用覆盖索引优化IN

explain select * from left_prefix where a = '1' and b > '2' and c = '3'; #使用a、b
explain select a,b,c from left_prefix where a > '1'; #使用a
explain select * from left_prefix where a > '1'; #不使用索引,全表扫描更优,可参考trace
explain select * from left_prefix2 where a > '1'; #使用a,数据量大,使用索引更优
explain select a,b,c from left_prefix where a > '1' and b = '2' and c = '3'; #使用a

explain select * from left_prefix where id > 3; #回表
explain select id from left_prefix where id > 3; #覆盖索引,性能高
#能用in的不用or
explain select * from left_prefix where id in(2,3,4); #整形主键索引,并且查询数据量小的情况,一般会走索引
explain select * from left_prefix where id in(2,3,4,5,6); #整形主键索引,并且查询数据量大的情况,一般不走索引
explain select id from left_prefix where id in(2,3,4,5,6); #整形主键索引,并且查询数据量大的情况,一般不走索引, 可用覆盖索引优化

#使用in代替or。无索引时in比or性能更优。有索引时差不多。
explain select * from left_prefix2 where c in('3333332','3333334','3333336'); #无索引
explain select * from left_prefix2 where c = '3333332' or c = '3333334' or c ='3333336'; 

#排序
explain select * from left_prefix where a = '1' and b = '2' order by c ; #使用a、b, 回表
explain select a,b,c from left_prefix where a = '1' and b = '2' order by c ; #使用a、b, 覆盖索引
explain select a,b,c from left_prefix where a = '1' and b = '2' order by b,c,a ; #使用a、b, using filesort 
explain select * from employees where name='a' and age = 1 order by position; #中间字段为int时, 查询优化成order by position
explain select * from employees where name='a' and age = 1 order by position,age; #中间字段为int时, 查询优化成order by position
explain select * from employees where name='a' and age = 1 order by age,position,name; #中间字段为int时, 查询优化成order by position, 可查看trace
explain select * from left_prefix order by a ; #全表扫描, using filesort 
explain select a,b,c from left_prefix order by a ; #使用覆盖索引优化 
explain select * from left_prefix where a = '1' order by b ; #使用a
explain select * from left_prefix where a = '1' order by c ; #使用a, using filesort 单路排序
explain select * from left_prefix where a = '1' order by b asc,c asc; #使用a
explain select * from left_prefix where a = '1' order by b desc,c desc; #使用a
explain select * from left_prefix where a = '1' order by c asc,b asc; #使用a,using filesort 与索引键排序不一致
explain select * from left_prefix where a = '1' order by b desc,c asc; #使用a,using filesort 与索引键排序不一致,MySQL 8.0可创建降序索引
explain select * from left_prefix where a = '1' order by b asc,c desc; #使用a,using filesort 与索引键排序不一致

3.4 尽量使用覆盖索引,能查询具体列的不使用'select *'。

3.5 不等于(not in 或者 <>) 需要表扫描,应转换成等于形式。

explain select * from left_prefix2 where a <> 'x';
explain select * from left_prefix2 where a not in ('xx','xxxx');

3.6 使用in代替or,无索引时in比or性能更优。

explain select * from left_prefix2 where id in(3333332,3333334,3333336); #有索引时差不多
explain select * from left_prefix2 where id = 3333332 or id = 3333334 or id =3333336; 

explain select * from left_prefix2 where c in('3333332','3333334','3333336');#无索引时in更优
explain select * from left_prefix2 where c = '3333332' or c = '3333334' or c ='3333336';

3.7 索引列使用函数、类型转换、计算操作不走索引。

explain select * from left_prefix2 where left(a,1) = '1';
explain select * from left_prefix2 where date(a) = '2020-12-12';
explain select * from left_prefix2 where a = 1;
explain select * from left_prefix2 where a = '1'+'2';

3.8 分页查询优化

select * from left_prefix2 where a > '0' limit 1000,15;#跳过较少,扫描数少
select * from left_prefix2 where a > '0' limit 2000000,15;#跳过较多,扫描数多
#排序后分页查询,遍历后还要回表查询,走全表扫描,Using filesort
explain select * from left_prefix2 where a > '0' order by a limit 2000000,15;

#优化方式一,1、取出满足条件的id,走覆盖索引;2、根据id查询数据;
explain select id from left_prefix2 where a > '0' order by a limit 2000000,15;
explain select * from left_prefix2 l inner join (select id from left_prefix2 where a > '0' order by a limit 2000000,15) tmp on l.id = tmp.id;

#优化方式二,2、通过索引范围查询达到分页目的 
explain select * from employees where hire_time > '2020-12-12' order by hire_time desc limit 15;

3.9 表连接优化

1) Nested-Loop Join (NLJ)嵌套循环连接算法

被驱动表关联字段有索引。一般小表(参与连接的行少)作为驱动表,大表作为被驱动表。驱动表全表扫描,循环读取一行,使用联接字段查询被驱动表,取出结果合并,返回给客户端。因为被驱动表走索引,性能较高。

explain select * from left_prefix t1 join left_prefix2 t2 on t1.a = t2.a;
explain select * from left_prefix2 t2 join left_prefix t1 on t1.a = t2.a;#替换表顺序不影响查询效率

2) Block Nested-Loop Join(BNL)基于块的嵌套循环连接算法

被驱动表关联字段无索引。一般小表作为驱动表,大表作为被驱动表。驱动表数据加载到join_buffer中,循环读取一行被驱动表数据,与join_buffer中数据比对,取出结果合并,返回给客户端。相当于在内存中做笛卡尔积,性能较低。

explain select * from left_prefix t1 join left_prefix2 t2 on t1.b = t2.b;#Using join buffer (Block Nested Loop)
explain select * from left_prefix2 t2 join left_prefix t1 on t1.b = t2.b;#替换表顺序不影响查询效率

优化方式:

1)给被驱动表关联字段加索引。

2)小表连接不超过3张,数据量大的表尽量不做连接。

*)关联字段类型需一致,否则不走索引。

3.10 count查询优化

查询分析器对count查询会做优化,优先走非聚集索引,因为叶子节点不存储数据,数据量小,扫描效率较高。

UPDATE `left_prefix2` SET `d` = null WHERE (id = 1 or id = 2) ;
create index idx_d on left_prefix2 (d);
explain select count(1) from left_prefix2; #走非主键索引idx_d 5000005
explain select count(id) from left_prefix2; #走非主键索引idx_d 5000005 
explain select count(a) from left_prefix2; #走非主键索引idx_a_b_c 5000005 ,索引键不可空,总数正确
explain select count(d) from left_prefix2; #走非主键索引idx_d 5000003,索引键可空,不计算NULL值
explain select count(*) from left_prefix2; #走非主键索引idx_d 5000005,内部优化,推荐使用

优化方式:

1)使用count(*)查询,兼顾性能和数据准确性。

2)增加计数表,数据增删时,在事务中同步计数。

五、运维优化

脚本:

#查询连接
show full processlist;

#查询事务
select * from information_schema.INNODB_TRX;
#杀死死锁事务
kill [trx_mysql_thread_id]