mysql索引的数据结构,各自优劣
索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,
InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因
此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议
选择BTree索引。
Data Structure Visualization
在线数据结构演示网站
哈希索引:
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到
叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快
B树(B-树)
B树又名平衡多路二叉树,和平衡二叉树的区别在于:
- 子数节点数不同:平衡二叉树每个节点最多有两个节点,而M阶B树代表每个节点最多可以有M个子树
- 每个节点包含的数据量不同:平衡二叉树每个节点最多包含一个关键字(当前节点)代表的值和两个孩子(左右)指针。而对于B树(M阶),一个节点可以最多拥有M-1个关键字,M个链表指针。对于B树的每个中间节点有k-1个关键字(数字数据)和k个子数(k介于阶数M和M/2之间,M/2向上取整)。
- 叶子节点的分布不同: B树的所有叶子节点都在同一层,并且叶子节点只有关键字,指向孩子的指针都为null.
B树的满足条件
- 若根结点不是终端结点,则至少有2棵树。
- 除根结点外所有的非叶子结点都至少有M/2课子树,至多M个子树(关键字数目=子数数目-1)。
- 所有叶子节点都位于同一层。
B+树:
B+树是基于B-树的一种变体,有着比B-树更高的查询性能。
B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针
相互链接。在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而且
基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用
于数据库、文件系统等场景。
B+树的满足条件
- 节点的子树数目和关键字数目相同(而B树是子树数目-1)
- 节点的关键字(叫做索引)表示的是子树中的最大数据,在子树中同样含有这个数据。
- 叶子节点包含了全部的数据,同时符号从左到右,从小到大的顺序,并用指针连接在一起。
B+树对比B树
连续插入22个数据,b树会构建一个4层数,b+树才3层
1.层级更低(更加矮胖),IO次数更少。由于 B+ 树的中间节点不含有实际数据,只有子树的最大数据和子树指针,因此磁盘页中可以容纳更多节点元素,也就是说同样数据情况下,B+ 树会 B 树更加“矮胖”,因此查询效率更快。
2.每次都需要查询到叶子节点,查询性能稳定。B树这时就能体现出优势,由于出现频率较高的树,在B树中往往在上层(非叶子结点),查找到该结点就会成功并结束查询,相对较快。而B+树由于非叶子结点关键字只是代表索引,因此在B+树中,无论查找成功与否,都是走了一条从根到叶子节点的路径。
3.B+树范围查询更加方便。 需要查询某个范围内的数据时,由于 B+ 树的叶子节点是一个有序链表,只需在叶子节点上遍历即可,不用像 B 树那样挨个中序遍历比较大小。
全文索引
mysql执行计划怎么看
执行计划就是sql的执行查询的顺序,以及如何使用索引查询,返回的结果集的行数
EXPLAIN SELECT * from A where X=? and Y=?
1。id :是一个有顺序的编号,是查询的顺序号,有几个 select 就显示几行。id的顺序是按 select 出现
的顺序增长的。id列的值越大执行优先级越高越先执行,id列的值相同则从上往下执行,id列的值为
NULL最后执行。
2。selectType 表示查询中每个select子句的类型
SIMPLE: 表示此查询不包含 UNION 查询或子查询
PRIMARY: 表示此查询是最外层的查询(包含子查询)
SUBQUERY: 子查询中的第一个 SELECT
UNION: 表示此查询是 UNION 的第二或随后的查询
DEPENDENT UNION: UNION 中的第二个或后面的查询语句, 取决于外面的查询
UNION RESULT, UNION 的结果
DEPENDENT SUBQUERY: 子查询中的第一个 SELECT, 取决于外面的查询. 即子查询依赖于外层查
询的结果.
DERIVED:衍生,表示导出表的SELECT(FROM子句的子查询)
3.table:表示该语句查询的表
4.type:优化sql的重要字段,也是我们判断sql性能和优化程度重要指标。他的取值类型范围:
const:通过索引一次命中,匹配一行数据
system: 表中只有一行记录,相当于系统表;
eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配
ref: 非唯一性索引扫描,返回匹配某个值的所有
range: 只检索给定范围的行,使用一个索引来选择行,一般用于between、<、>;
index: 只遍历索引树;
ALL: 表示全表扫描,这个类型的查询是性能最差的查询之一。 那么基本就是随着表的数量增多,
执行效率越慢。
执行效率:
ALL < index < range< ref < eq_ref < const < system。最好是避免ALL和index
5.possible_keys:它表示Mysql在执行该sql语句的时候,可能用到的索引信息,仅仅是可能,实际不一
定会用到。
6.key:此字段是 mysql 在当前查询时所真正使用到的索引。 他是possible_keys的子集
7.key_len:表示查询优化器使用了索引的字节数,这个字段可以评估组合索引是否完全被使用,这也是
我们优化sql时,评估索引的重要指标
9.rows:mysql 查询优化器根据统计信息,估算该sql返回结果集需要扫描读取的行数,这个值相关重
要,索引优化之后,扫描读取的行数越多,说明索引设置不对,或者字段传入的类型之类的问题,说明
要优化空间越大
10.filtered:返回结果的行占需要读到的行(rows列的值)的百分比,就是百分比越高,说明需要查询到
数据越准确, 百分比越小,说明查询到的数据量大,而结果集很少
11.extra
using filesort :表示 mysql 对结果集进行外部排序,不能通过索引顺序达到排序效果。一般有
using filesort都建议优化去掉,因为这样的查询 cpu 资源消耗大,延时大。
using index:覆盖索引扫描,表示查询在索引树中就可查找所需数据,不用扫描表数据文件,往
往说明性能不错。
using temporary:查询有使用临时表, 一般出现于排序, 分组和多表 join 的情况, 查询效率不
高,建议优化。
using where :sql使用了where过滤,效率较高。
什么是最左前缀原则?什么是最左匹配原则
通常我们在建立联合索引的时候,也就是对多个字段建立索引,相信建立过索引的同学们会发现,无论是oralce还是mysql都会让我们选择索引的顺序,比如我们想在a,b,c三个字段上建立一个联合索引,我们可以选择自己想要的优先级,a、b、c,或者是b、a、c 或者是c、a、b等顺序。为什么数据库会让我们选择字段的顺序呢?不都是三个字段的联合索引么?这里就引出了数据库索引的最左前缀原理。
比如:索引index1:(a,b,c)有三个字段,我们在使用sql语句来查询的时候,会发现很多情况下不按照我们想象的来走索引。
测试
CREATE TABLE IF NOT EXISTS `tb_user` ( `id` int(11) DEFAULT NULL, `name` varchar(50) DEFAULT NULL, `sex` int(11) DEFAULT NULL, `addr` varchar(50) DEFAULT NULL, KEY `联合索引` (`name`,`sex`,`addr`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `tb_user` (`id`, `name`, `sex`, `addr`) VALUES (1, '王小五', 1, '北京'), (2, '李四', 2, '上海');
`联合索引` (`name`,`sex`,`addr`) 内部创建了 name、 name sex、 name sex addr 三个索引
不会走index1索引的sql语句
explain select * from tb_user where sex = 1 explain select * from tb_user where addr = '北京' explain select * from tb_user where sex = 1 and addr = '北京' explain select * from tb_user where trim(name) = '王五'
会走index1索引的sql语句
where后面条件不区分顺序,因为mysql内部有sql优化器,会让sql语句尽量走索引
explain SELECT * FROM tb_user t WHERE t.name = '王五'
explain SELECT * FROM tb_user t WHERE t.name = '王五' and t.sex=1
explain SELECT * FROM tb_user t WHERE t.name = '王五' and t.sex=1 AND t.addr='北京'
explain SELECT * FROM tb_user t WHERE t.name = '王五' and t.addr='北京'
该语句也会走索引,但是只走name的索引,ken_len 和 单查name是值一样
explain SELECT * FROM tb_user t WHERE t.name = '王五' and t.sex>1 AND t.addr='北京'
该语句也会走索引,但是只走name和sex的索引,ken_len 和 只查name sex是值一样
因为 索引是从左到右匹配,遇到范围(>、<、between、like)查询就停止匹配
关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?
在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运
维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是load了不需要的数据列?还 是数据量太大?
所以优化也是针对这三个方向来的,
首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载
了许多结果中并不需要的列,对语句进行分析以及重写。
分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽
可能的命中索引。
如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者
纵向的分表。
drop,delete与truncate的区别
相同点:
truncate和不带where子句的delete,以及drop都会删除表内的数据
不同点:
- truncate会清除表数据并重置id从1开始,delete就只删除记录,drop可以用来删除表或数据库并且将表所占用的空间全部释放
- truncate和delete只删除数据不删除表的结构。drop语句将删除表的结构被依赖的约(constrain),触发器(trigger),依赖于该表的存储过程/函数将保留,但是变为 invalid 状态。
- 速度上一般来说: drop> truncate > delete
- 使用上,想删除部分数据行用 delete,想删除表用 drop,想保留表而将所有数据删除,如果和事务无关,用truncate即可。如果和事务有关,或者想触发trigger,还是用delete。
- delete是DML语句,不会自动提交。drop/truncate都是DDL语句,执行后会自动提交。
总结:数据量小 几万以下-> delete truncate(更推荐)
数据量比较大 几百万 几千万 -> drop create
mysql里记录货币用什么字段类型好
NUMERIC 和DECIMAL 类型被MySQL实现为同样的类型,这在SQL92标准允许。他们被用于保存值,该值的准确精度是极其重要的值,例如与金钱有关的数据。
DECIMAL和NUMERIC值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。
事务的四个特性及含义
数据库事务transanction正确执行的四个基本要素:ACID,原子性(Atomicity)、一致性(Correspondence)、隔离性(Isolation)、持久性(Durability)。
原子性: 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性: 在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
隔离性: 隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行 相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请 求,使得在同一时间仅有一个请求用于同一数据。
持久性: 在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
MYSQL相比于其他数据库有哪些特点?
MySQL是一个小型关系型数据库管理系统,开发者为瑞典MySQL AB公司,现在已经被Sun公司收购,支持FreeBSD、Linux、MAC、Windows等多种操作系统与其他的大型数据库例如Oracle、DB2、SQL Server等相比功能稍弱一些。其特点有:
- 可以处理拥有上千万条记录的大型数据;
- 支持常见的SQL语句规范;
- 可移植行高,安装简单小巧;
- 良好的运行效率,有丰富信息的网络支持;
- 调试、管理,优化简单(相对其他大型数据库)。
视图的作用,视图可以更改么?
视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的sql操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。
视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。
存储过程与触发器的区别
触发器与存储过程非常相似,触发器也是SQL语句集,两者唯一的区别是触发器不能用EXECUTE语句调用,而是在用户执行Transact-SQL语句时自动触发(激活)执行。
触发器是在一个修改了指定表中的数据时执行的存储过程。通常通过创建触发器来强制实现不同表中的逻辑相关数据的引用完整性和一致性。由于用户不能绕过触发器,所以可以用它来强制实施复杂的业务规则,以确保数据的完整性。
触发器不同于存储过程,触发器主要是通过事件执行触发而被执行的,而存储过程可以通过存储过程名称名字而直接调用。当对某一表进行诸如UPDATE、INSERT、DELETE这些操作时,SQLSERVER就会自动执行触发器所定义的SQL语句,从而确保对数据的处理必须符合这些SQL语句所定义的规则。
索引的作用?和它的优点缺点是什么?
索引就是一种特殊的查询表,数据库的搜索引擎可以利用它加速对数据的检索。它很类似与现实生活中书的目录,不需要查询整本书内容就可以找到想要的数据。
索引可以是唯一的,创建索引允许指定单个列或者是多个列。缺点是它减慢了数据录入的速度,同时也增加了数据库的尺寸大小。
union 与union all的区别
union 在进行表连接后会筛选掉重复的记录,所以在表连接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果。union all 则会显示重复结果,只是简单的两个结果合并并返回.所以效率比union高,在保证没有重复数据的情况下用union all.
SQL语言包括哪几部分?每部分都有哪些操作关键字?
SQL语言包括数据定义(DDL)、数据操纵(DML),数据控制(DCL)和数据查询(DQL)四个部分。
- 数据定义:Create Table,Alter Table,Drop Table, Craete/Drop Index等
- 数据操纵:insert,update,delete,
- 数据控制:grant,revoke
- 数据查询:select
数据库三大范式是什么
第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。
引擎
1、MySQL存储引擎MyISAM与InnoDB区别
存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。
常用的存储引擎有以下:
- Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
- MyIASM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。
- MEMORY引擎:所有的数据都在内存中,数据的处理速度快,但是安全性不高
2、MyISAM索引与InnoDB索引的区别?
- InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
- InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
- MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据,数据放在另外一个文件中。
- InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
- InnoDB支持事务,而MyISAM不支持事务
- InnoDB支持行级锁,而MyISAM支持表级锁
- InnoDB支持MVCC, 而MyISAM不支持
- InnoDB支持外键,而MyISAM不支持
- InnoDB不支持全文索引,而MyISAM支持。
- InnoDB不能通过直接拷贝表文件的方法拷贝表到另外一台机器, myisam 支持
- InnoDB表支持多种行格式, myisam 不支持
- InnoDB是索引组织表, myisam 是堆表
3、存储引擎选择
如果没有特别的需求,使用默认的Innodb即可。
MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。
SQL优化(推荐阅读)
优化成本:硬件>系统配置>数据库表结构>SQL 及索引。
优化效果:硬件<系统配置<数据库表结构<SQL 及索引。
对于MySQL层优化我一般遵从五个原则:
- 减少数据访问:设置合理的字段类型,启用压缩,通过索引访问等减少磁盘 IO。
- 返回更少的数据:只返回需要的字段和数据分页处理,减少磁盘 IO 及网络 IO。
- 减少交互次数:批量 DML 操作,函数存储等减少数据连接次数。
- 减少服务器 CPU 开销:尽量减少数据库排序操作以及全表查询,减少 CPU 内存占用。
- 利用更多资源:使用表分区,可以增加并行操作,更大限度利用 CPU 资源。
总结到 SQL 优化中,就如下三点:
- 最大化利用索引。
- 尽可能避免全表扫描。
- 减少无效数据的查询。
理解 SQL 优化原理 ,首先要搞清楚 SQL 执行顺序。
SELECT 语句,语法顺序如下:
SELECT DISTINCT <select_list> FROM <left_table> <join_type> JOIN <right_table> ON <join_condition> WHERE <where_condition> GROUP BY <group_by_list> HAVING <having_condition> ORDER BY <order_by_condition> LIMIT <limit_number>
SELECT 语句,执行顺序如下:
FROM <表名> # 选取表,将多个表数据通过笛卡尔积变成一个表。 ON <筛选条件> # 对笛卡尔积的虚表进行筛选 JOIN <join, left join, right join...> <join表> # 指定join,用于添加数据到on之后的虚表中,例如left join会将左表的剩余数据添加到虚表中 WHERE <where条件> # 对上述虚表进行筛选 GROUP BY <分组条件> # 分组 <SUM()等聚合函数> # 用于having子句进行判断,在书写上这类聚合函数是写在having判断里面的 HAVING <分组筛选> # 对分组后的结果进行聚合筛选 SELECT <返回数据列表> # 返回的单列必须在group by子句中,聚合函数除外 DISTINCT # 数据除重 ORDER BY <排序条件> # 排序 LIMIT <行数限制>
以下 SQL 优化策略适用于数据量较大的场景下,如果数据量较小,没必要以此为准,以免画蛇添足。
避免不走索引的场景
①尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描
如下:
SELECT * FROM t WHERE username LIKE '%陈%'
优化方式:尽量在字段后面使用模糊查询。
如下:
SELECT * FROM t WHERE username LIKE '陈%'
如果需求是要在前面使用模糊查询:
- 使用 MySQL 内置函数 INSTR(str,substr)来匹配,作用类似于 Java 中的 indexOf(),查询字符串出现的角标位置。
- 使用 FullText 全文索引,用 match against 检索。
- 数据量较大的情况,建议引用 ElasticSearch、Solr,亿级数据量检索速度秒级。
- 当表数据量较少(几千条儿那种),别整花里胡哨的,直接用 like '%xx%'。
②尽量避免使用 in 和 not in,会导致引擎走全表扫描
如下:
SELECT * FROM t WHERE id IN (2,3)
优化方式:如果是连续数值,可以用 between 代替。
如下:
SELECT * FROM t WHERE id BETWEEN 2 AND 3
如果是子查询,可以用 exists 代替。
如下:
-- 不走索引
select * from A where A.id in (select id from B);
-- 走索引
select * from A where exists (select * from B where B.id = A.id);
③尽量避免使用 or,会导致数据库引擎放弃索引进行全表扫描
如下:
SELECT * FROM t WHERE id = 1 OR id = 3
优化方式:可以用 union 代替 or。
如下:
SELECT * FROM t WHERE id = 1
UNION
SELECT * FROM t WHERE id = 3
④尽量避免进行 null 值的判断,会导致数据库引擎放弃索引进行全表扫描
如下:
SELECT * FROM t WHERE score IS NULL
优化方式:可以给字段添加默认值 0,对 0 值进行判断。
如下:
SELECT * FROM t WHERE score = 0
⑤尽量避免在 where 条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描
可以将表达式、函数操作移动到等号右侧,如下:
-- 全表扫描
SELECT * FROM T WHERE score/10 = 9
-- 走索引
SELECT * FROM T WHERE score = 10*9
⑥当数据量大时,避免使用 where 1=1 的条件
通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。
如下:
SELECT username, age, sex FROM T WHERE 1=1
优化方式:用代码拼装 SQL 时进行判断,没 where 条件就去掉 where,有 where 条件就加 and。
⑦查询条件不能用 <> 或者 !=
使用索引列作为条件进行查询时,需要避免使用<>或者!=等判断条件。
如确实业务需要,使用到不等于符号,需要在重新评估索引建立,避免在此字段上建立索引,改由查询条件中其他索引字段代替。
⑧where 条件仅包含复合索引非前置列
如下:复合(联合)索引包含 key_part1,key_part2,key_part3 三列,但 SQL 语句没有包含索引前置列"key_part1",按照 MySQL 联合索引的最左匹配原则,不会走联合索引。
select col1 from table where key_part2=1 and key_part3=2
⑨隐式类型转换造成不使用索引
如下 SQL 语句由于索引对列类型为 varchar,但给定的值为数值,涉及隐式类型转换,造成不能正确走索引。
select col1 from table where col_varchar=123;
⑩order by 条件要与 where 中条件一致,否则 order by 不会利用索引进行排序
如下:
-- 不走age索引
SELECT * FROM t order by age;
-- 走age索引
SELECT * FROM t where age > 0 order by age;
对于上面的语句,数据库的处理顺序是:
- 第一步:根据 where 条件和统计信息生成执行计划,得到数据。
- 第二步:将得到的数据排序。当执行处理数据(order by)时,数据库会先查看第一步的执行计划,看 order by 的字段是否在执行计划中利用了索引。如果是,则可以利用索引顺序而直接取得已经排好序的数据。如果不是,则重新进行排序操作。
- 第三步:返回排序后的数据。
当 order by 中的字段出现在 where 条件中时,才会利用索引而不再二次排序,更准确的说,order by 中的字段在执行计划中利用了索引时,不用排序操作。
这个结论不仅对 order by 有效,对其他需要排序的操作也有效。比如 group by 、union 、distinct 等。
⑪正确使用 hint 优化语句
MySQL 中可以使用 hint 指定优化器在执行时选择或忽略特定的索引。
一般而言,处于版本变更带来的表结构索引变化,更建议避免使用 hint,而是通过 Analyze table 多收集统计信息。
但在特定场合下,指定 hint 可以排除其他索引干扰而指定更优的执行计划:
- USE INDEX 在你查询语句中表名的后面,添加 USE INDEX 来提供希望 MySQL 去参考的索引列表,就可以让 MySQL 不再考虑其他可用的索引。例子: SELECT col1 FROM table USE INDEX (mod_time, name)...
- IGNORE INDEX 如果只是单纯的想让 MySQL 忽略一个或者多个索引,可以使用 IGNORE INDEX 作为 Hint。例子: SELECT col1 FROM table IGNORE INDEX (priority) ...
- FORCE INDEX 为强制 MySQL 使用一个特定的索引,可在查询中使用FORCE INDEX 作为 Hint。例子: SELECT col1 FROM table FORCE INDEX (mod_time) ...
在查询的时候,数据库系统会自动分析查询语句,并选择一个最合适的索引。但是很多时候,数据库系统的查询优化器并不一定总是能使用最优索引。
如果我们知道如何选择索引,可以使用 FORCE INDEX 强制查询使用指定的索引。
例如:
SELECT * FROM students FORCE INDEX (idx_class_id) WHERE class_id = 1 ORDER BY id DESC;
SELECT 语句其他优化
①避免出现 select *
首先,select * 操作在任何类型数据库中都不是一个好的 SQL 编写习惯。
使用 select * 取出全部列,会让优化器无法完成索引覆盖扫描这类优化,会影响优化器对执行计划的选择,也会增加网络带宽消耗,更会带来额外的 I/O,内存和 CPU 消耗。
②避免出现不确定结果的函数
特定针对主从复制这类业务场景。由于原理上从库复制的是主库执行的语句,使用如 now()、rand()、sysdate()、current_user() 等不确定结果的函数很容易导致主库与从库相应的数据不一致。
另外不确定值的函数,产生的 SQL 语句无法利用 query cache。
③多表关联查询时,小表在前,大表在后
在 MySQL 中,执行 from 后的表关联查询是从左往右执行的(Oracle 相反),第一张表会涉及到全表扫描。
所以将小表放在前面,先扫小表,扫描快效率较高,在扫描后面的大表,或许只扫描大表的前 100 行就符合返回条件并 return 了。
例如:表 1 有 50 条数据,表 2 有 30 亿条数据;如果全表扫描表 2,你品,那就先去吃个饭再说吧是吧。
④使用表的别名
当在 SQL 语句中连接多个表时,请使用表的别名并把别名前缀于每个列名上。这样就可以减少解析的时间并减少哪些友列名歧义引起的语法错误。
⑤用 where 字句替换 HAVING 字句
避免使用 HAVING 字句,因为 HAVING 只会在检索出所有记录之后才对结果集进行过滤,而 where 则是在聚合前刷选记录,如果能通过 where 字句限制记录的数目,那就能减少这方面的开销。
HAVING 中的条件一般用于聚合函数的过滤,除此之外,应该将条件写在 where 字句中。
where 和 having 的区别:where 后面不能使用组函数。
⑥调整 Where 字句中的连接顺序
MySQL 采用从左往右,自上而下的顺序解析 where 子句。根据这个原理,应将过滤数据多的条件往前放,最快速度缩小结果集。
增删改 DML 语句优化
①大批量插入数据
如果同时执行大量的插入,建议使用多个值的 INSERT 语句(方法二)。这比使用分开 INSERT 语句快(方法一),一般情况下批量插入效率有几倍的差别。
方法一:
insert into T values(1,2);
insert into T values(1,3);
insert into T values(1,4);
方法二:
Insert into T values(1,2),(1,3),(1,4);
选择后一种方法的原因有三:
- 减少 SQL 语句解析的操作,MySQL 没有类似 Oracle 的 share pool,采用方法二,只需要解析一次就能进行数据的插入操作。
- 在特定场景可以减少对 DB 连接次数。
- SQL 语句较短,可以减少网络传输的 IO。
②适当使用 commit
适当使用 commit 可以释放事务占用的资源而减少消耗,commit 后能释放的资源如下:
- 事务占用的 undo 数据块。
- 事务在 redo log 中记录的数据块。
- 释放事务施加的,减少锁争用影响性能。特别是在需要使用 delete 删除大量数据的时候,必须分解删除量并定期 commit。
③避免重复查询更新的数据
针对业务中经常出现的更新行同时又希望获得改行信息的需求,MySQL 并不支持 PostgreSQL 那样的 UPDATE RETURNING 语法,在 MySQL 中可以通过变量实现。
例如,更新一行记录的时间戳,同时希望查询当前记录中存放的时间戳是什么?
简单方法实现:
Update t1 set time=now() where col1=1;
Select time from t1 where id =1;
使用变量,可以重写为以下方式:
Update t1 set time=now () where col1=1 and @now: = now ();
Select @now;
前后二者都需要两次网络来回,但使用变量避免了再次访问数据表,特别是当 t1 表数据量较大时,后者比前者快很多。
查询条件优化
①对于复杂的查询,可以使用中间临时表暂存数据
②优化 group by 语句
默认情况下,MySQL 会对 GROUP BY 分组的所有值进行排序,如 “GROUP BY col1,col2,....;” 查询的方法如同在查询中指定 “ORDER BY col1,col2,...;” 。
如果显式包括一个包含相同的列的 ORDER BY 子句,MySQL 可以毫不减速地对它进行优化,尽管仍然进行排序。
因此,如果查询包括 GROUP BY 但你并不想对分组的值进行排序,你可以指定 ORDER BY NULL 禁止排序。
例如:
SELECT col1, col2, COUNT(*) FROM table GROUP BY col1, col2 ORDER BY NULL ;
③优化 join 语句
MySQL 中可以通过子查询来使用 SELECT 语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。
使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询可以被更有效率的连接(JOIN)..替代。
例子:假设要将所有没有订单记录的用户取出来,可以用下面这个查询完成:
SELECT col1 FROM customerinfo WHERE CustomerID NOT in (SELECT CustomerID FROM salesinfo )
如果使用连接(JOIN)..来完成这个查询工作,速度将会有所提升。
尤其是当 salesinfo 表中对 CustomerID 建有索引的话,性能将会更好,查询如下:
SELECT col1 FROM customerinfo
LEFT JOIN salesinfoON customerinfo.CustomerID=salesinfo.CustomerID
WHERE salesinfo.CustomerID IS NULL
连接(JOIN)..之所以更有效率一些,是因为 MySQL 不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。
④优化 union 查询
MySQL 通过创建并填充临时表的方式来执行 union 查询。除非确实要消除重复的行,否则建议使用 union all。
原因在于如果没有 all 这个关键词,MySQL 会给临时表加上 distinct 选项,这会导致对整个临时表的数据做唯一性校验,这样做的消耗相当高。
高效:
SELECT COL1, COL2, COL3 FROM TABLE WHERE COL1 = 10
UNION ALL
SELECT COL1, COL2, COL3 FROM TABLE WHERE COL3= 'TEST';
低效:
SELECT COL1, COL2, COL3 FROM TABLE WHERE COL1 = 10
UNION
SELECT COL1, COL2, COL3 FROM TABLE WHERE COL3= 'TEST';
⑤拆分复杂 SQL 为多个小 SQL,避免大事务
如下:
- 简单的 SQL 容易使用到 MySQL 的 QUERY CACHE。
- 减少锁表时间特别是使用 MyISAM 存储引擎的表。
- 可以使用多核 CPU。
⑥使用 truncate 代替 delete
当删除全表中记录时,使用 delete 语句的操作会被记录到 undo 块中,删除记录也记录 binlog。
当确认需要删除全表时,会产生很大量的 binlog 并占用大量的 undo 数据块,此时既没有很好的效率也占用了大量的资源。
使用 truncate 替代,不会记录可恢复的信息,数据不能被恢复。也因此使用 truncate 操作有其极少的资源占用与极快的时间。另外,使用 truncate 可以回收表的水位,使自增字段值归零。
⑦使用合理的分页方式以提高分页效率
使用合理的分页方式以提高分页效率 针对展现等分页需求,合适的分页方式能够提高分页的效率。
案例 1:
select * from t where thread_id = 10000 and deleted = 0
order by gmt_create asc limit 0, 15;
上述例子通过一次性根据过滤条件取出所有字段进行排序返回。数据访问开销=索引 IO+索引全部记录结果对应的表数据 IO。
因此,该种写法越翻到后面执行效率越差,时间越长,尤其表数据量很大的时候。
适用场景:当中间结果集很小(10000 行以下)或者查询条件复杂(指涉及多个不同查询字段或者多表连接)时适用。
案例 2:
select t.* from (select id from t where thread_id = 10000 and deleted = 0
order by gmt_create asc limit 0, 15) a, t
where a.id = t.id;
上述例子必须满足 t 表主键是 id 列,且有覆盖索引 secondary key:(thread_id, deleted, gmt_create)。
通过先根据过滤条件利用覆盖索引取出主键 id 进行排序,再进行 join 操作取出其他字段。
数据访问开销=索引 IO+索引分页后结果(例子中是 15 行)对应的表数据 IO。因此,该写法每次翻页消耗的资源和时间都基本相同,就像翻第一页一样。
适用场景:当查询和排序字段(即 where 子句和 order by 子句涉及的字段)有对应覆盖索引时,且中间结果集很大的情况时适用。
建表优化
①在表中建立索引,优先考虑 where、order by 使用到的字段。
②尽量使用数字型字段(如性别,男:1 女:2),若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
③查询数据量大的表 会造成查询缓慢。主要的原因是扫描行数过多。这个时候可以通过程序,分段分页进行查询,循环遍历,将结果合并处理进行展示。
要查询 100000 到 100050 的数据,如下:
SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY ID ASC) AS rowid,*
FROM infoTab)t WHERE t.rowid > 100000 AND t.rowid <= 100050
④用 varchar/nvarchar 代替 char/nchar。
尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL 也包含在内),都是占用 100 个字符的空间的,如果是 varchar 这样的变长字段, null 不占用空间。
MYSQL之连接
这里介绍四种用的比较多的连接方式
1.left join (左外连接或称左外连接):给定两张表A和表B 连接条件为主键和从表中的外键相等 也即通过相同的字段连接 可以查询查询的范围为左半部分和重叠的部分.
A id name B id a_id test
1 张三 1 1 测试1
2 李四
1 张三 1 1 测试
2 李四 null null null
2 right join (右外连接或称右外连接):给定两张表A和表B 连接条件为主键和从表中的外键相等 也即通过相同的字段连接 可以查询查询的范围为右半部分和重叠的部分.
1 张三 1 1 测试1
3.内连接 inner join 连接条件为两表中相同的字段
查询结果为两表重叠的记录。
1 张三 1 1 测试1
4.自连接(self join):针对相同的表进行的连接被称为“自连接”(self join)
你们订单表数据量过大怎么办?
-
- 分析数据量过大会产生什么问题
-
-
- 读写效率低,且数据库性能不高
-
-
- 分库分表
-
-
- 终极方案
-
-
- 主从复制(读写分离) + 数据归档(DBA)
-
-
- 展开说一下
-
分库分表实战
MVCC机制(扩展阅读)
MVCC实现原理也是一道非常高频的面试题,自己在整理这篇文章的时候,感觉到网上的资料在讲这块知识点上写的五花八门,好像大家的理解并没有一致。
这里将自己所理解的做一个总结,个人会觉得这是一篇含金量挺高的一篇文章(哈哈),所以请你坚持认真的看下去,一定会对你有收获。
如果文章中哪里没有理解,或者认为我讲的不对的地方,都欢迎留言一起交流哈。
前言
一些基本概念我这里不在做阐述了。好比什么是事务? 事务的ACID? 四大隔离级别?
有关事务并发存在的问题之前有写过一篇文章:一文详解脏读、不可重复读、幻读
如果你还不清楚不可重复读和幻读的区别,非常建议看完上面这篇文章。因为好多人会把不可重复读和幻读搞在一起。
所以会认为MVCC能解决幻读,其实MVCC解决的不是幻读,而是不可重复读,下面会用实际例子来证明这一点。
一、什么是MVCC
多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。
在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
MVCC只在已提交读(Read Committed)和可重复读(Repeatable Read)两个隔离级别下工作,其他两个隔离级别和MVCC是不兼容的。因为未提交读,总数读取最新的数据行,而不是读取符合当前事务版本的数据行。而串行化(Serializable)则会对读的所有数据多加锁。
MVCC的实现原理主要是依赖每一行记录中两个隐藏字段,undo log,ReadView
二、MVCC相关的一些概念
这里我们先来理解下有关MVCC相关的一些概念,这些概念都理解后,我们会通过实际例子来演示MVCC的具体工作流程是怎么样的。
1、事务版本号
事务每次开启时,都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序。这就是事务版本号。
也就是每当begin的时候,首选要做的就是从数据库获得一个自增长的事务ID,它也就是当前事务的事务ID。
2、隐藏字段
对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id、roll_pointer,如果数据表中存在主键或者非NULL的UNIQUE键时不会创建row_id,否则InnoDB会自动生成单调递增的隐藏主键row_id。
列名 | 是否必须 | 描述 |
row_id | 否 | 单调递增的行ID,不是必需的,占用6个字节。 这个跟MVCC关系不大 |
trx_id | 是 | 记录操作该行数据事务的事务ID |
roll_pointer | 是 | 回滚指针,指向当前记录行的undo log信息 |
这里的记录操作,指的是insert|update|delete。对于delete操作而已,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted,并非真正删除。
3、undo log
undo log可以理解成回滚日志,它存储的是老版本数据。在表记录修改之前,会先把原始数据拷贝到undo log里,如果事务回滚,即可以通过undo log来还原数据。或者如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。
在insert/update/delete(本质也是做更新,只是更新一个特殊的删除位字段)操作时,都会产生undo log。
在InnoDB里,undo log分为如下两类:
1)insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
2)update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被删除。
undo log有什么用途呢?
1、事务回滚时,保证原子性和一致性。
2、如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本(用于MVCC快照读)。
4、版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。如下:
5、快照读和当前读
快照读: 读取的是记录数据的可见版本(有旧的版本)。不加锁,普通的select语句都是快照读,如:
select * from user where id = 1;
当前读:读取的是记录数据的最新版本,显式加锁的都是当前读
select * from user where id = 1 for update; select * from user where id = 1 lock in share mode;
6、ReadView
ReadView是事务在进行快照读的时候生成的记录快照, 可以帮助我们解决可见性问题的
如果一个事务要查询行记录,需要读取哪个版本的行记录呢? ReadView 就是来解决这个问题的。 ReadView 保存了当前事务开启时所有活跃的事务列表。换个角度,可以理解为: ReadView 保存了不应该让这个事务看到的其他事务 ID 列表。
ReadView是如何保证可见性判断的呢?我们先看看 ReadView 的几个重要属性
- trx_ids: 当前系统中那些活跃(未提交)的读写事务ID, 它数据结构为一个List。(重点注意:这里的trx_ids中的活跃事务,不包括当前事务自己和已提交的事务,这点非常重要)
- low_limit_id: 目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
- up_limit_id: 活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。
- creator_trx_id: 表示生成该 ReadView 的事务的 事务id
访问某条记录的时候如何判断该记录是否可见,具体规则如下:
- 如果被访问版本的 事务ID = creator_trx_id,那么表示当前事务访问的是自己修改过的记录,那么该版本对当前事务可见;
- 如果被访问版本的 事务ID < up_limit_id,那么表示生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的 事务ID > low_limit_id 值,那么表示生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的 事务ID在 up_limit_id和m_low_limit_id 之间,那就需要判断一下版本的事务ID是不是在 trx_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
画张图来理解下
这里需要思考的一个问题就是 何时创建ReadView?
上面说过,ReadView是来解决一个事务需要读取哪个版本的行记录的问题的。那么说明什么?只有在select的时候才会创建ReadView。但在不同的隔离级别是有区别的:
在RC隔离级别下,是每个select都会创建最新的ReadView;而在RR隔离级别下,则是当事务中的第一个select请求才创建ReadView(下面会详细举例说明)。
那insert/update/delete操作呢?
这样操作不会创建ReadView。但是这些操作在事务开启(begin)且其未提交的时候,那么它的事务ID,会存在在其它存在查询事务的ReadView记录中,也就是trx_ids中。
三、MVCC实现原理分析
1、如何查询一条记录
- 获取事务自己事务ID,即trx_id。(这个也不是select的时候获取的,而是这个事务开启的时候获取的 也就是begin的时候)
- 获取ReadView(这个才是select的时候才会生成的)
- 数据库表中如果查询到数据,那就到ReadView中的事务版本号进行比较。
- 如果不符合ReadView的可见性规则, 即就需要Undo log中历史快照,直到返回符合规则的数据;
InnoDB 实现MVCC,是通过ReadView+ Undo Log 实现的,Undo Log 保存了历史快照,ReadView可见性规则帮助判断当前版本的数据是否可见。
2、MVCC是如何实现读已提交和可重复读的呢?
其实其它流程都是一样的,读已提交和可重复读唯一的区别在于:在RC隔离级别下,是每个select都会创建最新的ReadView;而在RR隔离级别下,则是当事务中的第一个select请求才创建ReadView。
看完下面这个例子你应该就明白了。
四、经典面试题:MVCC能否解决了幻读问题呢?
有关这个问题查了很多资料,有的说能解决,有的说不能解决,也有人说能解决部分幻读场景。这里部分解决指的是能解决快照读的幻读问题,不能解决当前读的幻读问题。
具体可以看下面这篇文章
面试题之:MVCC能否解决幻读?https://blog.csdn.net/qq_35590091/article/details/107734005
先说我的结论:
MVCC能解决不可重复读问题,但是不能解决幻读问题,不论是快照读和当前读都不能解决。RR级别解决幻读靠的是锁机制,而不是MVCC机制。
既然网上那么多人说,MVCC解决能解决快照读下的幻读问题, 那这里通过举示例来说明,MVCC解决不了快照读的幻读问题。
假设有张用户表,这张表的 id 是主键。表中一开始有4条数据。
这里是在RR级别下研究(可重复读)。
1、事务A,查询是否存在 id=5 的记录,没有则插入,这是我们期望的正常业务逻辑。
2、这个时候 事务B 新增的一条 id=5 的记录,并提交事务。
3、事务A,再去查询 id=5 的时候,发现还是没有记录。
上面的文章是这样来举例说明,事务A第一次和第二次读到的是一样的,所以认为解决了幻读。我不认为这个是解决了幻读,而是解决了不可能重复读。它保证了第一次和第二次所读到的结果是一样的。
解决幻读了吗?显然没有,因为这个时候如果事务A执行一条插入操作
INSERT INTO `user` (`id`, `name`, `pwd`) VALUES (5, '田七', 'fff');
最终 事务A 提交事务,发现报错了。这就很奇怪,查的时候明明没有这条记录,但插入的时候 却告诉我 主键冲突,这就好像幻觉一样。这才是幻读问题。
所以说MVCC是不能解决的,要想解决还是需要锁。
这里事务A能正常的插入的前提就是其它事务不能插入id=5并提交成功。要解决这个问题也很简单,就是事务A先获得id=5这个排它锁。
我们可以在事务A第一次查询的时候加一个排他锁
select * from `user` where id = 5 for update
那么事务B的插入动作永远属于堵塞状态,直到事务A插入成功,并提交。那么最终是事务B报主键冲突而回滚。但事务A不会因为查询的时候没有这条记录,插入失败。也就解决了幻读问题。
所以说 RR级别下解决幻读问题靠的是锁机制,而不是MVCC机制。
如果有不认同我的观点的,可以留下你的观点,我们可以一起讨论交流哈。