MySQL进阶之路围绕存储引擎(InnoDB支持事务与行锁,MyISAM适合读多写少,Memory基于内存)展开,详解B+树索引结构、聚集索引与二级索引及回表查询,并通过慢查询日志、profile和explain工具进行SQL性能分析,提升数据库优化能力。
把MySQL想象成一座精心设计的四层大楼,每一层都有自己的专属使命,彼此协作处理你的数据请求。下面一层层来看。
这里是MySQL的“前台接待处”,专门负责:
长期稳定更新的攒劲资源: >>>点此立即查看<<<
这里是MySQL的“大脑”,所有智能决策都在这里发生:
这是MySQL最灵活的一层,像是个可更换的物流系统:
这里是实实在在的“数据仓库”:
和其他数据库相比,MySQL的架构能在多种不同场景中应用并发挥良好作用,关键在于存储引擎——插件式的架构将查询处理、其他系统任务和数据存储分离开来。这种设计允许根据业务需求灵活选择合适的存储引擎,适应性非常强。
建表时指定存储引擎
CREATE TABLE 表名(
字段1 字段1类型 [ COMMENT 字段1注释],
...
字段n 字段n类型 [ COMMENT 字段n注释],
) ENGINE = INNODB [ COMMENT 表注释];查询当前数据库支持的存储引擎
show engines;
介绍
InnoDB是一种兼顾高可靠性和高性能的通用存储引擎,MySQL 5.5之后成为默认引擎。
特点
文件
xxx.ibd:表名对应的表空间文件,存储表结构(frm-早期、sdi-新版)、数据和索引。
逻辑存储结构

介绍
MyISAM是MySQL早期的默认存储引擎。
特点
文件
xxx.sdi:存储表结构信息
xxx.MYD: 存储数据
xxx.MYI: 存储索引
Memory引擎的表数据存储在内存中,受硬件或断电影响,通常只作为临时表或缓存使用。
内存存放,hash索引(默认)
xxx.sdi:存储表结构信息
| 特点 | InnoDB | MyISAM | Memory |
|---|---|---|---|
| 存储限制 | 64TB | 有 | 有 |
| 事务安全 | 支持 | - | - |
| 锁机制 | 行锁 | 表锁 | 表锁 |
| B+tree索引 | 支持 | 支持 | 支持 |
| Hash索引 | - | - | 支持 |
| 全文索引 | 支持(5.6版本之后) | 支持 | - |
| 空间使用 | 高 | 低 | N/A |
| 内存使用 | 高 | 低 | 中等 |
| 批量插入速度 | 低 | 高 | 高 |
| 支持外键 | 支持 | - | - |
索引(index)是帮助MySQL高效获取数据的有序数据结构。在数据之外,数据库系统维护着满足特定查找算法的数据结构,这些结构以某种方式引用(指向)数据,从而可以在这些结构上实现高级查找算法,这就是索引。
| 优势 | 劣势 |
|---|---|
| 提高数据检索效率,降低数据库IO成本 | 索引列也要占用空间 |
| 通过索引列对数据排序,降低排序成本,减少CPU消耗 | 索引提高查询效率,却降低INSERT、UPDATE、DELETE时的效率 |
| 索引结果 | 描述 |
|---|---|
| B+Tree索引 | 最常见的索引类型,大部分引擎支持 |
| Hash索引 | 底层用哈希表实现,仅精确匹配有效,不支持范围查询 |
| R-Tree(空间索引) | MyISAM引擎的特殊索引,用于地理空间数据,使用较少 |
| Full-text(全文索引) | 通过倒排索引快速匹配文档,类似Lucene、Solr、ES |
| 索引 | InnoDB | MyISAM | Memory |
|---|---|---|---|
| B+tree | 支持 | 支持 | 支持 |
| Hash索引 | 不支持 | 不支持 | 支持 |
| R-tree索引 | 不支持 | 支持 | 不支持 |
| Full-text | 5.6之后支持 | 支持 | 不支持 |
注意:通常所说的索引,若不特别指明,均指B+树结构组织的索引。
| 分类 | 含义 | 特点 | 关键字 |
|---|---|---|---|
| 主键索引 | 针对表中主键创建的索引 | 默认自动创建,只能有一个 | PRIMARY |
| 唯一索引 | 避免同一表中某数据列值重复 | 可以有多个 | UNIQUE |
| 常规索引 | 快速定位特定数据 | 可以有多个 | |
| 全文索引 | 全文索引查找文本中的关键词,而非比较索引中的值 | 可以有多个 | FULLTEXT |
在InnoDB存储引擎中,根据索引的存储形式,可分为两种:
| 分类 | 含义 | 特点 |
|---|---|---|
| 聚集索引(Clustered Index) | 将数据与索引放到一起,叶子节点保存行数据 | 必须有,且只有一个 |
| 二级索引(Secondary Index) | 将数据与索引分开存储,叶子节点关联对应主键 | 可以有多个 |
聚集索引选取规则:
聚集索引和二级索引的具体结构:

分析一下执行select * from user where name = 'Arm'的查找过程:
回表查询:先到二级索引查找数据,找到主键值,再到聚集索引根据主键获取数据,这个过程称为回表查询。
创建索引
CREATE [ UNIQUE | FULLTEXT ] INDEX 索引名 ON 表名 ( index_col_name,... ) ;
查看索引
SHOW INDEX FROM 表名 ;
删除索引
DROP INDEX 索引名 ON 表名 ;
案例演示:
# 创建系统用户表
create table tb_user(
id int primary key auto_increment comment '主键',
name varchar(50) not null comment '用户名',
phone varchar(11) not null comment '手机号',
email varchar(100) comment '邮箱',
profession varchar(11) comment '专业',
age tinyint unsigned comment '年龄',
gender char(1) comment '性别 , 1: 男, 2: 女',
status char(1) comment '状态',
createtime datetime comment '创建时间'
) comment '系统用户表';
# 插入数据
insert into tb_user (name, phone, email, profession, age, gender, status, createtime) values
('吕布', '17799990000', 'lvbu666@163.com', '软件工程', 23, '1', '6', '2001-02-02 00:00:00'),
('曹操', '17799990001', 'caocao666@qq.com', '通讯工程', 33, '1', '0', '2001-03-05 00:00:00'),
('赵云', '17799990002', '17799990@139.com', '英语', 34, '1', '2', '2002-03-02 00:00:00'),
('孙悟空', '17799990003', '17799990@sina.com', '工程造价', 54, '1', '0', '2001-07-02 00:00:00'),
('花木兰', '17799990004', '19980729@sina.com', '软件工程', 23, '2', '1', '2001-04-22 00:00:00'),
('大乔', '17799990005', 'daqiao666@sina.com', '舞蹈', 22, '2', '0', '2001-02-07 00:00:00'),
('露娜', '17799990006', 'luna_love@sina.com', '应用数学', 24,'2', '0', '2001-02-08 00:00:00')......;
# 数据准备完毕,完成如下需求:
# 1. name字段为姓名字段,可能重复,创建索引
create index idx_user_name on tb_user(name);
# 2. phone字段非空且唯一,创建唯一索引
create unqiue index idx_user_phone on tb_user(phone);
# 3. 为profession、age、status创建联合索引
create index idx_user_pro_age_sta on tb_user(profession, age, status);
# 4. 为email建立索引提升查询效率
create index idx_email on tb_user(email);
# 查看tb_user的所有索引
show index from tb_user;MySQL客户端连接成功后,通过show [session|global] status可以查看服务器状态信息。以下指令可查看当前数据库的INSERT、UPDATE、DELETE、SELECT访问频次:
# session当前会话,global全局 # Com_delete删除次数,Com_insert插入次数,Com_select查询次数,Com_update更新次数 show global status like 'Com_______';
通过查询执行频次,可以了解当前数据库是以增删改为主还是查询为主,便于后续优化。
慢查询日志记录所有执行时间超过指定参数(long_query_time,单位秒,默认10秒)的SQL语句日志。
MySQL默认未开启慢查询日志。通过show variabies like 'slow_query_log'查看:

开启慢查询日志需在MySQL配置文件(/etc/my.cnf)中配置:
# 开启慢日志开关 slow_query_log=1 # 设置慢日志时间2秒,超过2秒视为慢查询 long_query_time=2
配置完后重启MySQL:systemctl restart mysqld,慢日志文件位于/var/lib/mysql/localhost-slow.log。通过慢查询日志可以定位执行效率低的SQL,有针对性地优化。
show profiles 能在SQL优化时帮助我们了解时间耗费在哪里。通过select @@ha ve_profiling查看是否支持profile:

当前MySQL支持profile操作,但开关关闭。可以通过set语句在session/global级别开启:
set profiling = 1;
执行一系列SQL后,通过以下指令查看耗时:
# 查看每一条SQL的耗时基本情况 show profiles; # 查看指定query_id的SQL语句各个阶段的耗时情况 show profile for query query_id; # 查看指定query_id的SQL语句CPU使用情况 show profile cpu for query query_id;
查看每一条SQL的耗时情况:

查看指定SQL各个阶段的耗时情况:

explain或desc命令可以获取MySQL如何执行select语句的信息,包括表如何连接和连接顺序:
# 在select语句前加上关键字explain / desc explain select 字段列表 from 表名 where 条件;

| 字段 | 含义 |
|---|---|
| id | select查询的序列号,表示执行顺序(id相同从上到下,id不同值越大越先执行) |
| select_type | SELECT类型,常见值:SIMPLE(简单表)、PRIMARY(主查询)、UNION(UNION中的第二个或后面查询)、SUBQUERY(子查询)等 |
| type | 连接类型,性能由好到差:NULL、system、const、eq_ref、ref、range、index、all |
| possible_key | 可能应用在这张表上的索引,一个或多个 |
| key | 实际使用的索引,为NULL则未使用索引 |
| key_len | 索引中使用的字节数,值为索引字段最大可能长度,并非实际使用长度,长度越短越好 |
| rows | MySQL认为必须执行查询的行数,在innodb中为估计值 |
| filtered | 返回结果行数占需读取行数的百分比,值越大越好 |
如果索引了多列(联合索引),需遵守最左前缀法则。即查询从索引的最左列开始,且不跳过中间列。如果跳过某一列,索引将部分失效(后面的字段索引失效)。
在tb_user表中,联合索引idx_pro_age_sta涉及三个字段,顺序为profession、age、status。最左前缀法则要求查询时必须包含profession,否则索引全部失效;且中间不能跳过列,否则跳过列后的字段索引失效。
联合索引中,出现范围查询(>、<)时,范围查询右侧的列索引失效。当使用>=或<=时,所有字段都走索引。业务允许的情况下,尽量使用>=或<=这类范围查询。
2.6.3.1 索引列运算
不要在索引列上进行运算操作,否则索引失效。
# 在tb_user表中,存在idx_phone索引 # 等值匹配查询,索引生效 explain select * from tb_user where phone = '17799990015'; # 函数运算后,索引失效 explain select * from tb_user where substring(phone,10,2) = '15';
2.6.3.2 字符串不加引号
字符串类型字段使用时,不加引号,索引将失效。
2.6.3.3 模糊查询
如果仅仅是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。
# 关键字后加%,索引生效 explain select * from tb_user where profession like '软件%'; # 关键字前加%,索引失效 explain select * from tb_user where profession like '%工程'; explain select * from tb_user where profession like '%工%';
2.6.3.3 or连接条件
用or分割的条件,如果or前的列有索引,而后的列没有索引,则涉及的索引都不会被用到。只有当or左右两侧字段都有索引时,索引才会生效。
# 即使id有索引,age没有索引,索引也会失效 explain select * from tb_user where id = 10 or age = 23;
2.6.3.4 数据分布影响
如果MySQL评估使用索引比全表扫描更慢,则不会使用索引。索引适用于索引少量数据,如果通过索引查询返回大批量数据,还不如全表扫描快,此时索引失效。
SQL提示是优化数据库的重要手段,在SQL语句中加入人为提示达到优化目的。
use index:建议MySQL使用某个索引(仅是建议,MySQL内部会再次评估)。
# 在emp表中,根据软件工程专业查找,使用idx_user_pro索引 explain select * from tb_user use index(idx_user_pro) where profession = '软件工程';
ignore index:忽略指定的索引。
# 忽略idx_user_pro索引 explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程';
force index:强制使用索引。
# 强制使用idx_user_pro索引 explain select * from tb_user force index(idx_user_pro) where profession = '软件工程';
覆盖索引是指查询使用了索引,且需要返回的列在该索引中已经全部能找到,应减少使用select *。
来看一组SQL的执行计划差别:
explain select id, profession from tb_user where profession = '软件工程' and age = 31 and status = '0' ; explain select id,profession,age, status from tb_user where profession = '软件工程' and age = 31 and status = '0' ; explain select id,profession,age, status, name from tb_user where profession = '软件工程' and age = 31 and status = '0' ; explain select * from tb_user where profession = '软件工程' and age = 31 and status = '0';
执行结果:

| Extra | 含义 |
|---|---|
| Using where; Using Index | 查找使用了索引,且数据都在索引列中能找到,无需回表 |
| Using index condition | 查找使用了索引,但需要回表查询数据 |
在tb_user表中,联合索引idx_user_pro_age_sta关联了profession、age、status,它是一个二级索引,叶子节点下挂的是主键id。所以当查询返回的数据在id、profession、age、status之中时,直接走二级索引返回数据;如果超出这个范围,需要拿到主键id再去扫描聚集索引获取额外数据,这就是回表。如果一直使用select *,很容易造成回表查询(除非根据主键查询)。
当字段类型为字符串(varchar、text、longtext等),需要索引很长的字符串时,索引会很大,浪费磁盘IO,影响查询效率。此时可以只将字符串的一部分前缀建立索引,大大节约索引空间,提高效率。
create index idx_xxxx on table_name(column(n)) ;
例:为tb_user表的email字段建立长度为5的前缀索引:
create index idx_email_5 on tb_user(email(5));
根据索引的选择性来决定。选择性是不重复的索引值(基数)与数据表记录总数的比值,越高查询效率越高。唯一索引的选择性是1,性能最好。
如果表中存在两个字段各自有单列索引,根据这两个字段查询时,会走一个字段的索引,再回表查询。在业务场景中,如果存在多个查询条件,建议建立联合索引,而非单列索引。
如果需要一次性往数据库表中插入多条记录,可以从以下三个方面优化:
方案一:批量插入数据
insert into tb_user values (1, 'Tom'), (2, 'Cat'), (3, 'Jerry');
方案二:手动控制事务
start transaction; insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry'); insert into tb_test values(4,'Tom'),(5,'Cat'),(6,'Jerry'); insert into tb_test values(7,'Tom'),(8,'Cat'),(9,'Jerry'); commit;
方案三:主键顺序插入,性能高于乱序插入
# 主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3 # 主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89
如果一次性需要插入大批量数据(比如几百万条),使用insert语句性能较低,此时可以使用MySQL数据库提供的load指令:
# 客户端连接服务端时,加上参数 --local-infile mysql --local-infile -u root -p # 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关 set global local_infile = 1; # 执行load指令将准备好的数据加载到表结构中 load data local infile '/root/sql1.log' into table tb_user fields terminated by ',' lines terminated by 'n' ;
主键顺序插入性能高于乱序插入的原因:
数据组织方式
在InnoDB存储引擎中,表数据根据主键顺序组织存放,这种表称为索引组织表(index organized table IOT)。行数据存储在聚集索引的叶子节点上。
数据行记录在逻辑结构page页中,每个页大小固定,默认16K。如果插入的数据行在该页存储不下,会存储到下一个页,页与页之间通过指针连接。


页分裂
页可以为空、填充一半或100%。每个页包含2-N行数据(如果一行数据过大,会行溢出),根据主键排列。

页合并
删除数据时,记录并未物理删除,只是标记(flaged)为删除,空间允许被其他记录声明使用。当页中删除的记录达到MERGE_THRESHOLD(合并页阈值,默认页的50%),InnoDB会寻找最近的页(前或后)看是否可以将两个页合并以优化空间使用。
索引设计原则
MySQL的排序有两种方式:
Using filesort:通过表的索引或全表扫描读取满足条件的数据行,然后在排序缓冲区sortbuffer中完成排序。所有不是通过索引直接返回排序结果的都叫FileSort排序。
Using index:通过有序索引顺序扫描直接返回有序数据,不需要额外排序,操作效率高。
显然Using index性能高,Using filesort性能低,优化时尽量优化为Using index。
order by优化原则:
在分组操作中,通过以下两点优化提升性能:
数据量大时,limit分页查询越往后效率越低。因为执行limit (2000000,10)时,MySQL需要排序前2000010条记录,仅返回2000000-2000010的记录,其他丢弃,查询排序代价非常大。
优化思路:一般分页查询时,通过创建覆盖索引能较好提高性能,可以通过覆盖索引加子查询形式进行优化。
数据量大时,执行count操作非常耗时。
要大幅度提升InnoDB表的count效率,主要优化思路:自己计数(可以借助redis,但带条件的count仍然复杂)。
| count用法 | 含义 |
|---|---|
| count(主键) | InnoDB遍历整张表,取出主键id值返回给服务层,服务层直接按行累加 |
| count(字段) | 没有not null约束:遍历整张表取出字段值,服务层判断是否为null,不为null则累加;有not null约束:直接按行累加 |
| count(数字) | InnoDB遍历整张表但不取值,服务层对每一行放一个数字“1”直接累加 |
| count(*) | InnoDB专门做了优化,不取值,服务层直接按行累加 |
按照效率排序:count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count(*)。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述