一.mysql
学习mysql发现的一篇比较不错的博客:
https://blog.csdn.net/ufo___
抽空阅读一下,作为本篇笔记的完善,本篇笔记记录的mysql的知识点并不全
1.索引
(1)了解索引原理需要掌握的一些知识点
(i)索引为什么会增加速度
DB在执行一条Sql语句的时候,默认的方式是根据搜索条件进行全表扫描,
遇到匹配条件的就加入搜索结果集合。如果我们对某一字段增加索引,查询时
就会先去索引列表中一次定位到特定值的行数,大大减少遍历匹配的行数,所
以能明显增加查询的速度。
(iii)了解索引需要知道的一些细节
索引的目的在于提高查询效率,与我们查阅图书所用的目录是一个道理:先定
位到章,然后定位到该章下的一个小节,然后找到页数。相似的例子还有:查
字典,查火车车次,飞机航班等。本质都是:通过不断地缩小想要获取数据的
范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,
有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。数据库也是
一样,但显然要复杂的多,因为不仅面临着等值查询,还有范围查询(>、<、
between、in)、模糊查询(like)、并集查询(or)等等。数据库应该选择怎么样的方
式来应对所有的问题呢?我们回想字典的例子,能不能把数据分成段,然后分段
查 询呢?最简单的如果1000条数据,1到100分成第一段,101到200分成第二
段,201到300分成第三段......这样查第250条数据,只要找第三段就可以了,一
下子去除了90%的无效数据。但如果是1千万的记录呢,分成几段比较好?稍有
算法基础的同学会想到搜索树,其平均复杂度是lgN,具有不错的查询性能。但
这里我们忽略了一个关键的问题,复杂度模型是基于每次相同的操作成本来考虑
的。而数据库实现比较复杂,一方面数据是保存在磁盘上的,另外一方面为了提
高性能,每次又可以把部分数据读入内存来计算,因为我们知道访问磁盘的成本
大概是访问内存的十万倍左右,所以简单的搜索树难以满足复杂的应用场景。
(iv) 磁盘IO与预读
考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,
不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因
为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻
的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体
一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数
据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮
助。
(2)mysql索引
mysql索引数据结构有B+Tree和Hash索引两种,索引在mysql中也叫做键
(1)B+Tree
任何一种数据结构都不是凭空产生的,一定会有它的背景和使用场景,我们现在
总结一下,我们需要这种数据结构能够做些什么,其实很简单,那就是:每次查
找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么我们
就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,b+树应运而
生。
如上图,是一颗b+树(查过一些资料,其它资料的一致反馈这是一棵B-Tree,
不纠结这个问题了,知道查找规则的来龙去脉就可以了),关于b+树的定义可
以参见B+树,这里只说一些重点,浅蓝色的块我们称之为一个磁盘块,可以看
到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1
包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示
在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点
即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点只
不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数
据表中。
关于mysql索引的原理(B+Tree索引)可以查看文章,写的很好,比笔记全:
https://www.jianshu.com/p/8b653423c586(B-tree,读B树)
https://blog.csdn.net/qq_26222859/article/details/80631121(b+tree)
(i)什么是B+Tree
B+Tree是B-Tree的一种变体
什么是B-Tree
* 根结点至少有两个子女。
* 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
* 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
* 所有的叶子结点都位于同一层。
图1就是一颗B-Tree
一颗m阶的B+Tree有如下特点:
* 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保
存数据,只用来索引,所有数据都保存在叶子节点。
* 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,
且叶子结点本身依关键字的大小自小而大顺序链接。
* 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)
元素。
B+Tree的优势:
* 单一节点存储更多的元素,使得查询的IO次数更少。
* 所有查询都要查找到叶子节点,查询性能稳定。
* 所有叶子节点形成有序链表,便于范围查询。
下两个图是一个B+Tree
B-Tree和B+Tree的区别:
* B+树中只有叶子节点会带有指向记录的指针(ROWID),而B树则所有
节点都带有,在内部节点出现的索引项不会再出现在叶子节点中。
* B+树中所有叶子节点都是通过指针连接在一起,而B树不会。
* 所以B+Tree比B-Tree更适合做索引。
MongoDB为什么使用B-Tree而不是B+Tree(也就是说B-Tree的优势)
* B-Tree读做B树
* B+Tree更适合通过外部存储设备获取数据(整体IO次数少);
B-Tree单次比较的成功率高。
想看详细看文章:https://blog.csdn.net/ahjxhy2010/article/details/80339510
(ii)B+Tree的查找过程
如图1所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,
此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1
的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘
块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在
26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第
三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情
况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次
IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那
么总共需要百万次的IO,显然成本非常非常高。
叶子节点存什么?需要了解下面两个概念:
下面这些内容来源于博客:https://blog.csdn.net/ufo___/article/details/80522453
* MyISAM的非聚集索引(也叫二级索引)
那么在MyISAM中是如何利用B+Tree索引组织数据的呢?什么是非聚集索
引呢?
我们知道使用MyISAM引擎储存表数据会产生三个文件,frm .MYD .MYI
其中.frm 是存放表的定义信息(MyISAM和innoDB都有这个文件) .MYD
是存放具体的表的数据 .MYI就是存放该表的所有索引
那什么又是非聚集索引呢?简单来说就是索引文件和数据文件分开存储,索
引树的叶子节点保存对应数据的地址,并且包含了引用行大的主键id值。
假设table表 有主键id和name(name列已经建立了索引),下面画图说明问题:
对创建name列创建索引
create index idx_name on ti(name);
show index from ti;
下面这张图就表现了在MyISAM下索引是如何工作的
信息量非常大,我来一一说明:这个索引树是以建立索引的列的所有数据组
织起来的,可以看出索引十分消耗存储空间,而且叶子节点存储的是对应的
那行数据的地址,查找的时候先通过索引树找到对应数据的地址,再通过地
址找到真正的数据。这也就是为什么不把全部的索引一次性载入内存的原因,
索引太大了。
* innoDB的聚集索引
innoDB的索引树和MyISAM是有区别的,索引和数据放在同一个文件里面,
主键索引树的叶子节点直接存储数据(全部列的数据),所以叫做聚合索
引。假设上面的表用的是InnoDB,建立了主键id索引和name这一列的索
引,那么数据又是如何组织起来的呢,下面画图说明:
可以看到在主键索引的叶子节点上直接存储了对应的数据,数据和索引
在一起,聚合索引。而且其他的索引树的叶子节点对应的是该行数据的
主键id,如果以name进行查找,先在name的索引树上找到对应的主键
id,再通过得到的id进行查找。这样是为什么innoDB查询的时候比
MyISAM慢的原因之一,innoDB必须涉及到主键索引,既占用内存又直
接多了一次主键索引树的查找。
这里顺便解释一个问题:为什么推荐用自增id作为主键?
第一:数字类型的比较速度更快。
自增的id,这个id一定是数字类型,相对于字符串而言,数
字的比较速度快于字符串(字符串需要一个字母一个字母进行
比较),在B+树中,值的插入、查找、 删除都要进行比较,这
样的话用数字作为主键比用字符串做为主键在比较速度上更有
优势。
第二:B树分裂更简单,可以更合理的利用磁盘空间,避免造成磁盘碎片。
自增的id对比乱序的UUID有什么优势呢,可以看到乱序的
UUID在B+树中的插入位置是非常随机的,而自增id每次都往B+树
的最右边进行插入,这样B+树的分裂操作更简单,而且底层分配
空间是分配一段连续的空间,这样每次都像往数组的最后插入数
据,可以更合理的利用连续分配的空间,以免造成碎片过多。
第三:数字类型的索引更节约空间。
索引也是非常占用磁盘空间的,对字段小 的列建索引更节约
磁盘空间,比如对数字型的列建索引就比字符串的列建索引更加
节省磁盘空间。
扩展:MySql复合索引是如何组织树的
表:
下图显示其如何组织数据存储的
(iii)B+Tree的性质
下面的性质得出结论索引字段要尽量的小
先看一种简单说法:
每次IO读取一个磁盘块,一个磁盘磁盘块可以存多项数据,数
据越大,磁盘块存的数据项越小,所以树的高度越高。
在看网络上的说法,比较难懂
通过上面的分析,我们知道IO次数取决于b+树的高度h,假设
当前数据表的数据为N,每个磁盘块的数据项的数量是m,则
有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;
m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个
数据页的大小,是固定的,如果数据项占的空间越小,数据项
的数量越多,树的高度越低。这就是为什么每个数据项,即索
引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。
这也是为什么b+树要求把真实的数据放到叶子节点而不是内
层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,
导致树增高。当数据项等于1时将会退化成线性表。
扩展,块是什么鬼(本节参考文章:
https://www.i3geek.com/archives/1275
还有簇的概念,有空在看)
块、扇区的一些概念
扇区
首先搞清楚扇区一些单位大小关系
一个磁盘按层次分为磁盘组合 -> 单个磁盘 -> 某一盘面
-> 某一磁道 -> 某一扇区
现在在说扇区是什么
每个磁盘有多条同心圆似的磁道,磁道被分割成多个部
分。每部分的弧长加上到圆心的两个半径,恰好形成一
个扇形,这就是扇区。扇区是磁盘中最小的物理存储单
位。通常情况下每个扇区的大小是512字节。(由于不
断提高磁盘的大小,部分厂商设定每个扇区的大小是
4096字节)
磁盘块
磁盘块属于逻辑层面,是操作系统虚拟出来的, 块是操作系
统中最小的逻辑存储单位。操作系统与磁盘打交道的最小单
位是磁盘块,磁盘的读写基本单位是块。
扇区和磁盘块的关系以及为什么要有磁盘块
由于扇区的数量比较小,数目众多在寻址时比较困难,所以
操作系统就将相邻的扇区组合在一起,形成一个块,再对块
进行整体的操作。扇区和磁盘块的关系,通过射磁盘块来映
射。磁盘块和扇区大小的基本关系:
一个块大小=一个扇区大小*2的n次方(这个值只是个例子,
并不是固定的,由操作系统自己决定)
下面的性质得出结论索引的最左匹配特性(即从左往右匹配)
当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,
b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样
的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方
向,如果name相同再依次比较age和sex,最后得到检索的数据;但
当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该
查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必
须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)
这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个
字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹
配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特
性。
学习最左匹配原则的时候复习一下这幅个表格
语句 | 索引是否发挥作用 |
where a=3 | 是 |
where a=3 and b=5 | 是 |
where a=3 and b=5 and c=4 | 是 |
select * from mytable where c=4 and b=6 and a=3; |
本来按理说是不可以的,但是网上说mysql会进行优化,所以和上一句一样,用到了索引 |
where b=3 | 否 |
where c=4 | 否 |
where a=3 and c=4 | a列能用到索引,c不能 |
where a=3 and b>10 and c=7 | a能,b能,c不能,这个的地方b用的是范围查询,也算断点,不过自身能用到索引 |
where a>4 and b=7 and c=9 | a能、b不能、c不能,理由同上 |
select * from mytable where a=3 order by b; | a用到了索引,b在结果排序中也用到了索引的效果 |
select * from mytable where a=3 order by c; | a用到了索引,但是这个地方c没有发挥排序效果,因为中间断点了 |
where a=3 and b like 'xxx%' and c=7 | a能,b能,c不能 |
(iv)覆盖索引(索引覆盖,重点,全看)
我们知道MySQL的B+Tree索引是用我们字段的数据来建立索引的,比
如说我们的主键id字段,就是用所有的id来组织这颗索引树,如果我们再对
name字段建立索引的话,这个二级索引就是用name字段的数据来组织这
颗索引树。那么问题就来了,我们知道对于二级索引而言他的叶子节点存
储了对应数据行的id,也就是说最后我们的查询还是要通过主键id来进行
查询获取数据。如果我们只需要name这个字段呢?比如说
select name from table where name>'aaa'; 我们这个二级索引上保存了的
name字段的所有数据,那么就没有必要再通过id去访问数据行了,直接从
索引上获取数据即可。称之为覆盖索引,有的也翻译为索引覆盖。
我们可以通过expalin来判断查询是否覆盖索引,主要看extra字段是
否有using index 。
比如说:我们有以下表:
对name字段建立索引:
执行sql 【explain select name from tb where name>'w'】;
可以看到我们使用上了索引idx_name,而且extra字段也出现了using index,
说明我们这次查询是覆盖索引的。
把sql稍微修改一下:【explain select id,name from tb where name>'w'】;
可以看到这次查询还是覆盖索引,因为我们二级索引树的叶子节点就保存了
对应数据行的id,所以对于二级索引而言,查询的时候加不加id都不影响是
否是覆盖索引,id也在二级索引树上。
我们再把sql修改一下:【explain select name,age from tb where name>'w'】;
我们对name字段建立的索引上并没有保存age这个字段的数据,也就无法从
索引上来获取数据了,只能访问数据表了。extra字段也没有出现
using index 。那么我们的业务需求就是经常的查询name和age字段咋办呢?
为了再次提高效率我们可以对name和age字段建立复合索引。
例如:
我们再次执行【explain select name,age from tb where name>'w'】
发现我们的新索引并没有使用上,因为我们MySQL优化器认为不值得调用
这个复合索引,因为索引也占空间,在当前就10条数据量,表就三个字段
(id,name,age)的情况下,他认为多一次主键查询比从复合索引上获
取数据的效果更好,而且也使用上了对name字段建立的单值索引。
我们把索引idx_name删掉,在次执行:
【explain select name,age from tb where name>'w'】
这个时候MySQL乖乖用上了我们建立的复合索引,而且extra字段也出现
了using index 。在一个系统应用当中除非特别情况,否则我们也一般也
是建立复合索引。
在举一个列子,延时关联
原Sql:
SELECT store_ids,film_id FROM sakila.inventory where actor='SEAN CARREY'
and title like '%APOLLO%'
对(artist, title, prod_id),建立索引,然后按如下方式进行查询
EXPLAIN SELECT *
FROM products
join (
SELECT prod_id
products
WHERE actor='SEAN CARREY'
and title like '%APOLLO%'
) tl ON (T1.prod_id-products.prod_id )
我们把这种方式叫做延迟关联(deferredjoin),因为延迟了对列的访
问。在查询的第一阶段MySQL可以使用覆盖索引,在FROM子句
的子查询中找到匹配的prod_id,然后根据这些prod_id值在外层
查询匹配获取需要的所有列值。虽然无法使用索引覆盖整个查询,
但总算比完全无法利用索引覆盖的好。
(2)Hash索引
简单地说,哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检
索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法
即可立刻定位到相应的位置,速度非常快。
Hash索引的示意图如下:
(a)Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,
不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多
次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。但是当
Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索
引高。对于选择性比较低的索引键,如果创建 Hash 索引,那么将会存
在大量记录指针信息存于同一个 Hash值相关联。这样要定位某一条记
录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。
(c)Hash 索引仅仅能满足"=","IN"和"<=>"查询,不能使用范围查询。
由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能
用于等值的过滤,不能用于基于范围的过滤,因为经过相应的Hash算
法处理之后的Hash 值的大小关系,并不能保证和Hash运算前完全一
样。
(d)Hash 索引无法被用来避免数据的文件排序操作。
由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且Hash
值的大小关系并不一定和 Hash 运算前的键值完全一样,所以数据库无
法利用索引的数据来避免任何排序运算;
(e)Hash 索引不能利用部分索引键查询。
对于组合索引,Hash 索引在计算Hash 值的时候是组合索引键合并后再
一起计算 Hash 值,而不是单独计算 Hash 值,所以通过组合索引的前
面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。
(f)有大量键重复,hash碰撞频繁,效率也不高
(3)B+Tree索引和Hash索引比较
(a)如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次
算法即可找到相应的键值;当然了,这个前提是,键值都是唯一的。如
果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后
扫描,直到找到相应的数据。
(b)从示意图中也能看到,如果是范围查询检索,这时候哈希索引就毫无用
武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连
续的了,就没办法再利用索引完成范围查询检索;
(c)同理,哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部
分模糊查询(这种部分模糊查询,其实本质上也是范围查询);
(d)哈希索引也不支持多列联合索引的最左匹配规则;
(e)B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有
大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈
希碰撞问题。
2.mysql索引的分类
(1)普通索引(normal)
这是最基本的索引,它没有任何限制。它有以下几种创建方式:
(a)直接创建索引
CREATE INDEX index_name ON table(column(length))
(b)修改表结构的方式添加索引
ALTER TABLE table_name ADD INDEX index_name ON (column(length))
(c)创建表的时候同时创建索引
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) CHARACTER NOT NULL ,
`content` text CHARACTER NULL ,
`time` int(10) NULL DEFAULT NULL ,
PRIMARY KEY (`id`),
INDEX index_name (title(length))
)
(d)删除索引
DROP INDEX index_name ON table
(2)唯一索引(unique)
它与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有
空值。如果是组合索引,则列值的组合必须唯一。
(a)创建唯一索引
CREATE UNIQUE INDEX indexName ON table(column(length))
(b)修改表结构
ALTER TABLE table_name ADD UNIQUE indexName ON (column(length))
(c)创建表的时候直接指定
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) CHARACTER NOT NULL ,
`content` text CHARACTER NULL ,
`time` int(10) NULL DEFAULT NULL ,
UNIQUE indexName (title(length))
);
(3)主键索引
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是
在建表的时候同时创建主键索引:
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) NOT NULL ,
PRIMARY KEY (`id`)
);
(4)组合索引
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个
字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
ALTER TABLE `table` ADD INDEX name_city_age (name,city,age);
组合索引的生效规则:
多列索引发挥作用,需要满足左前缀要求。
以index(a,b,c)为例:
语句 | 索引是否发挥作用 |
where a=3 | 是 |
where a=3 and b=5 | 是 |
where a=3 and b=5 and c=4 | 是 |
where b=3 | 否 |
where c=4 | 否 |
where a=3 and c=4 | a列能用到索引,c不能 |
where a=3 and b>10 and c=7 | a能,b能,c不能 |
where a=3 and b like 'xxx%' and c=7 | a能,b能,c不能 |
注意:对复合索引的非最左前缀字段进行 OR 运算,是无法使用到复合索
引的。
比如:
SELECT * FROM tbl_name WHERE (key_col1 > 10 OR key_col2 = 20) AND nonkey_col=30;
其中key_col1和key_col2建立复合索引,但是却无法使用到索引(
因为key_col2不是最左前缀,key_col2是否生效不知道,有环境测
试一下)。
其原因是,MySQL中的索引,使用的是B+tree, 也就是说他是:
先按照复合索引的 第一个字段的大小来排序,插入到 B+tree
中的,当第一个字段值相同时,在按照第二个字段的值比较来
插入的。那么如果我们需要对:OR key_col2 = 20 这样的条件
也使用复合索引,那么该怎么操作呢?应该要对复合索引进行
全扫描,找出所有 key_col2 =20 的项,然后还要回表去判断
nonkey_col=30,显然代价太大了。所以一般而言
OR key_col2 = 20 这样的条件是无法使用到复合索引的。如果
一定要使用索引,那么可以在 col2 上单独建立一个索引。
(5)全文搜索索引(full textl)
从3.23.23版开始支持全文索引和全文检索,FULLTEXT 用于搜索很长一
篇文章的时候,效果最好。主要用来查找文本中的关键字,而不是直接与
索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索
引擎,而不是简单的where语句的参数匹配。fulltext索引配合
match against操作使用,而不是一般的where语句加like。它可以在
create table,alter table ,create index使用,不过目前只有char、varchar,
text 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数
据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索
引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。
(a)创建表的适合添加全文索引
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) CHARACTER NOT NULL ,
`content` text CHARACTER NULL ,
`time` int(10) NULL DEFAULT NULL ,
PRIMARY KEY (`id`),
FULLTEXT (content)
);
(b)修改表结构添加全文索引
ALTER TABLE article ADD FULLTEXT index_content(content)
(c)直接创建索引
CREATE FULLTEXT INDEX index_content ON article(content)
3.索引优化,避免索引失效
(1)哪些列上适合建索引
(a)索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引
(b)在经常需要搜索的列上,可以加快搜索的速度;
(c)主键和外键
(d)在经常用于连接两张表的列上
(e)在经常使用在WHERE子句中的列上面创建索引
(f)在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其
指定的范围是连续的;
(g)在经常需要排序(order by)的列上创建索引,因为索引已经排序,
这样查询可以利用索引的排序,加快排序查询时间;
(h)数据量超过300的表应该有索引
(i)更新频繁的字段不适合创建索引,不会出现在 where 子句中的字段不
应该创建索引
(2)避免索引失效和写出高性能sql总结
(a)应尽量避免在 where 子句中对字段进行 null 值判断
(b)应尽量避免在 where 子句中使用!=或<>操作符
(c)应尽量避免在 where 子句中使用 or 来连接条件
(d)in 和 not in 也要慎用,否则会导致全表扫描
(e)下面的查询也将导致全表扫描:
select id from t where name like '%abc%'
但是如果是这样: select id from t where name like 'abc%'
索引是可以生效的
(f)如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在
运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到
运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计
划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面
语句将进行全表扫描:
select id from t where num=@num
可以改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num
(g)应尽量避免在 where 子句中对字段进行运算、表达式、函数这将导
致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2
(h)应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃
使用索引而进行全表扫描
(i)隐式转换导致索引失效,例如表的字段tu_mdn定义为varchar2(20),
但在查询时把该字段作为number类型以where条件传给Oracle,这样
会导致索引失效。
错误的例子:select * from test where tu_mdn=13333333333;
正确的例子:select * from test where tu_mdn='13333333333';
(j)并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化
的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如
一表中有字段sex,male、female几乎各一半,那么即使在sex上建
了索引也对查询效率起不了作用。
关于性别字段不适合建索引原因:
因为你访问索引需要付出额外的IO开销,你从索引中拿到的只
是地址,要想真正访问到数据还是要对表进行一次IO。假如你要从表
的100万行数据中取几个数据,
那么利用索引迅速定位,访问索引的这IO开销就非常值了。但如果你
是从100万行数
据中取50万行数据,就比如性别字段,那你相对需要访问50万次索引,
再访问50万次表,加起来的开销并不会比直接对表进行一次完整扫描
小。
(k)很多时候用 exists 代替 in 是一个好的选择
例子:
select a.id, a.workflowid,a.operator,a.stepid
from dbo.[[zping.com]]] a
inner join workflowbase b on a.workflowid=b.id
and operator='4028814111ad9dc10111afc134f10041'
替换为
select a.id,a.workflowid,a.operator ,a.stepid
from dbo.[[zping.com]]] a where exists
(select 'X' from workflowbase b where a.workflowid=b.id)
and operator='4028814111ad9dc10111afc134f10041'
(l)用exists替换DISTINCT
适用于类似这种情况:当提交一个包含一对多表信息(比如部门表和雇
员表)的查询时,避免在SELECT子句中使用DISTINCT。一般可以考
虑用EXIST替换,EXISTS使查询更为迅速。
4.mysql语句的执行过程
此节内容摘自文章:https://www.cnblogs.com/yyjie/p/7788428.html
(1)SQL语句的执行顺序
(8) SELECT (9) DISTINCT (11)
(1) FROM
(3)
(2) ON
(4) WHERE
(5) GROUP BY
(6) WITH {CUBE | ROLLUP}(mysql、oracle貌似没有,先不用关注)
(7) HAVING
(10) ORDER BY
即:from->on->join->where->group by->with->having->select->order by
以上每个步骤都会产生一个虚拟表,该虚拟表被用作下一个步骤的输
入。这些虚拟表对调用者(客户端应用程序或者外部查询)不可用。只有最
后一步生成的表才会会给调用者。如果没有在查询中指定某一个子句,将
跳过相应的步骤。
由此可间,join的效率要好于where,网上查mysql是这样,但是有点
不敢轻信,有空自己测试一下
对写sql的一个现象解释:
group by开始才可以使用select中的别名,他返回的是一个游标,而
不是一个虚拟表,所以在where中不可以使用select中的别名,而
having却可以使用。
sql处理的详细介绍:
(a)FROM:对FROM子句中的前两个表执行笛卡尔积(交叉联接),生成
虚拟表VT1。表名执行顺序是从后往前,所以数据较少的表
尽量放后。
(b)ON:对VT1应用ON筛选器,只有那些使为真才被插入到TV2。
(c)JOIN(OUTER):如果指定了OUTER JOIN(Left Outer join、
Right Outer join),保留表中未找到匹配的行将作
为外部行添加到VT2,生成TV3;如果是InnerJoin,
则不添加行,直接生成TV3;如果FROM子句包含
两个以上的表,则对上一个联接生成的结果表和下
一个表重复执行步骤1到步骤3,直到处理完所有的
表位置。
(d) WHERE:对TV3应用WHERE筛选器,只有使为true的行才插入TV4。
执行顺序为从前往后或者说从左到右。
(e)GROUP BY:按GROUP BY子句中的列列表对TV4中的行进行分组,
生成TV5。执行顺序从左往右分组。
(f) CUTE|ROLLUP:把超组插入VT5,生成VT6。
(g)HAVING:对VT6应用HAVING筛选器,只有使为true的组插入到VT7。
Having语句很耗资源,尽量少用。
(h)SELECT:处理SELECT列表,产生VT8。
(i)DISTINCT:将重复的行从VT8中删除,产品VT9。
(j)ORDER BY:将VT9中的行按ORDER BY子句中的列列表顺序,生成一
个游标(VC10)。执行顺序从左到右,是一个很耗资源的语
句。
(h)TOP:从VC10的开始处选择指定数量或比例的行,生成表TV11,并返
回给调用者。
例子:
select 考生姓名, max(总成绩) as max总成绩
from tb_Grade
where 考生姓名 is not null
group by 考生姓名
having max(总成绩) > 600
order by max总成绩
在上面的示例中 SQL 语句的执行顺序如下:
(a)首先执行 FROM 子句, 从 tb_Grade 表组装数据源的数据
(b) 执行 WHERE 子句, 筛选 tb_Grade 表中所有数据不为 NULL 的数据
(c)执行 GROUP BY 子句, 把 tb_Grade 表按 "学生姓名" 列进行分组(注:
这一步开始才可以使用select中的别名,他返回的是一个游标,而不
是一个表,所以在where中不可以使用select中的别名,而having却可
以使用)
(d) 计算 max() 聚集函数, 按 "总成绩" 求出总成绩中最大的一些数值
(e) 执行 HAVING 子句, 筛选课程的总成绩大于 600 分的.
(f) 执行 ORDER BY 子句, 把最后的结果按 "Max 成绩" 进行排序.
(g)mysql和sql执行顺序基本是一样的, 标准顺序的 SQL 语句为:
(2)Mysql语句的执行顺序
Mysql语句的执行顺序和sql语句的执行顺序基本相同。
5.Mysql的执行计划
什么是执行计划:
执行计划是数据库根据SQL语句和相关表的统计信息作出的一个查询方
案,这个方案是由查询优化器自动分析产生的,比如一条SQL语句如果
用来从一个10万条记录的表中查1条记录,那查询优化器会选择“索引查
找”方式,如果该表进行了归档,当前只剩下5000条记录了,那查询优
化器就会改变方案,采用“全表扫描”方式。同属的说:“模拟Mysql优化
器是如何执行SQL查询语句的,从而知道Mysql是如何处理你的SQL语
句的、分析你的查询语句或是表结构的性能瓶颈”。可见,执行计划并不
是固定的,它是“个性化的”。产生一个正确的“执行计划”有两点很重要。
执行计划需要注意的事项:
* 统一SQL语句的写法
执行计划是可以被重用的,越简单的SQL语句被重用的可能性越高。而复杂的
SQL语句只要有一个字符发生变化就必须重新解析,然后再把这一大堆垃圾塞
在内存里。可想而知,数据库的效率会何等低下。
例如,对于以下两句SQL语句,程序员认为是相同的,数据库查询优化器认为
是不同的:
select*from dual
select*From dual
其实就是大小写不同,查询分析器就认为是两句不同的SQL语句,必须进行两
次解析。生成2个执行计划。所以作为程序员,应该保证相同的查询语句在任
何地方都一致,多一个空格都不行。
* 不要把SQL语句写得太复杂
我经常看到,从数据库中捕捉到的一条SQL语句打印出来有2张A4纸这么
长。一般来说这么复杂的语句通常都是有问题的。我拿着这2页长的SQL语句
去请教原作者,结果他说时间太长,他一时也看不懂了。可想而知,连原作者
都有可能看糊涂的SQL语句,数据库也一样会看糊涂。
一般,将一个Select语句的结果作为子集,然后从该子集中再进行查询,
这种一层嵌套语句还是比较常见的,但是根据经验,超过3层嵌套,查询优化
器就很容易给出错误的执行计划。因为它被绕晕了。像这种类似人工智能的东
西,终究比人的分辨力要差些,如果人都看晕了,我可以保证数据库也会晕的。
* OLTP系统SQL语句必须采用绑定变量
select*from orderheader where changetime >'2010-10-20 00:00:01'
select*from orderheader where changetime >'2010-09-22 00:00:01'
以上两句语句,查询优化器认为是不同的SQL语句,需要解析两次。如果采用
绑定变量:
select*from orderheader where changetime >@chgtime
@chgtime变量可以传入任何值,这样大量的类似查询可以重用该执行计划了,
这可以大大降低数据库解析SQL语句的负担。一次解析,多次重用,是提高数
据库效率的原则。
绑定变量的注意事项:
事物都存在两面性,绑定变量对大多数OLTP处理是适用的,但是也有例
外。比如在where条件中的字段是“倾斜字段”的时候。“倾斜字段”指该列中的绝
大多数的值都是相同的,比如一张人口调查表,其中“民族”这列,90%以上都
是汉族。那么如果一个SQL语句要查询30岁的汉族人口有多少,那“民族”这列
必然要被放在where条件中。这个时候如果采用绑定变量@nation会存在很大
问题。试想如果@nation传入的第一个值是“汉族”,那整个执行计划必然会选
择表扫描。然后,第二个值传入的是“布依族”,按理说“布依族”占的比例可能只
有万分之一,应该采用索引查找。但是,由于重用了第一次解析的“汉族”的那
个执行计划,那么第二次也将采用表扫描方式。这个问题就是著名的“绑定变量
窥测”,建议对于“倾斜字段”不要采用绑定变量。
注:这里不同性别字段,性别字段不适合建立索引,是因为它大约要取一半的
数据,而民族字段却不同,它有时候取大多数数据,有时候取少量数据,
所以它可以加索引
本节参考文章:
https://blog.csdn.net/wuseyukui/article/details/71512793
https://blog.csdn.net/zhuxineli/article/details/14455029
https://www.cnblogs.com/magialmoon/archive/2013/11/23/3439042.html#Extra
https://www.cnblogs.com/man1s/p/10267669.html
https://segmentfault.com/q/1010000014908005/a-1020000014913547
(1)id:id是一组数字,表示查询中执行select子句或操作表的顺序,如果id相同,则执行顺序
从上至下,如果是子查询,id的序号会递增,id越大则优先级越高,越先会被执行。
分三种情况:
(i)id相同:执行顺序由上至下
select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。
(ii)id不同:如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行,自
己推测,有子查询先执行子查询。
(iii)id相同又不同(两种情况同时存在):id如果相同,可以认为是一组,从上往下顺
序执行;在所有组中,id值越大,优先级越高,越先执行
关于derived的说明:
详见select_type中的union
(2)select_type
查询的类型,主要是用于区分普通查询、联合查询、子查询等复杂的查询。
(i)SIMPLE:简单的select查询,查询中不包含子查询或者union
(ii)PRIMARY:查询中包含任何复杂的子部分,最外层查询则被标记为primary
(iii)SUBQUERY:在select 或 where列表中包含了子查询
(iv)DERIVED:在from列表中包含的子查询被标记为derived(衍生),mysql
或递归执行这些子查询,把结果放在临时表里
(v)UNION:若第二个select出现在union之后,则被标记为union;若union包含
在from子句的子查询中,外层select将被标记为derived。
表示临时表的结果来自于这个查询产生。如果是尖括号括起来的
的结果来自于union查询的id为M和N的结果集。
(vi)UNION RESULT:从union表获取结果的select(通俗的说就是union的结果,
自己猜测,就是如果是select * from (select1 union selet2)
那么就有个序号(id),如果是select1 union select2 由于
最外层没有select,所以就是NULL,没有id。具体什么样
子,有机会装了mysql执行计划在测试)
(3)type
访问类型,sql查询优化中一个很重要的指标,结果值从好到坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge >
unique_subquery > index_subquery > range > index > ALL
一般来说,好的sql查询至少达到range级别,最好能达到ref
system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出
现,可以忽略不计。
const:使用唯一索引或者主键,返回记录一定是1行记录的等值where条件时,
通常type是const。其他数据库也叫做唯一索引扫描
eq_ref:唯一性索引扫描(主键或唯一索引),对于每个索引键,表中只有一条
记录与之匹配。常见于主键或唯一索引扫描。
注,与const的区别:
eq_ref是表示的是表和表关联的一行,而const是单表中的一行且通常条
件是个常数。
注意:ALL全表扫描的表记录最少的表如t1表
ref:非唯一性索引扫描,返回匹配某个单独值的所有行。本质是也是一种索引访
问,它返回所有匹配某个单独值的行,因此他可能会找到多个符合条件的行,
所以它应该属于查找和扫描的混合体
fulltext:全文索引检索,要注意,全文索引的优先级很高,若全文索引和普通索引
同时存在时,mysql不管代价,优先选择使用全文索引。
ref_or_null :这种连接类型类似 ref,不同的是增加了对null值的查询。这种连接
类型的优化是从mysql4.1.1开始的,它经常用于子查询。在以下的
例子中,mysql使用ref_or_null 类型来处理ref_table:
select * from ref_table where key_column=expr or key_column is null
index merge: 此节内容内容参考文章:
https://www.cnblogs.com/digdeep/archive/2015/11/18/4975977.html
中文称为“索引合并”。表示查询使用了两个以上的索引,最后取
交集或者并集,常见and ,or的条件使用了不同的索引,官方排
序这个在ref_or_null之后,但是实际上由于要读取所个索引,性
能可能大部分时间都不如range。其执行过程简单的说,其实就
是:对多个索引分别进行条件扫描,然后将它们各自的结果进行
合并。从MySQL5.1开始支持,合并的算法有:intersect,union,
sort_union(交集和并集的各种组合)。
注:索引合并(Index Merge)不适应与全文索引(full-text indexes)
对index merge优化之MySQL 5.6.7:
MySQL在5.6.7之前,使用index merge有一个重要的前提条件:
没有range可以使用。这个限制降低了 MySQL index merge 可
以使用的场景。理想状态是同时评估成本后然后做出选择。如
下例子:
SELECT * FROM t1 WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;
对于这个查询,有两种处理方案:
第一种,索引合并使用条件(goodkey1 < 10 OR goodkey2 < 20)
第二种,范围扫描(range scan)使用条件 badkey < 30优化器可
以选择使用goodkey1和goodkey2做index merge,也
可以使用badkey做range。
因为上面的原则,无论goodkey1和 goodkey2的选择度如何,
MySQL都只会考虑range,而不会使用index merge的访问方式。
5.6.7版本针对此有修复。
索引合并开关配置:
索引合并(Index Merge)的使用取决于optimizer_switch系统变
量的index_merge,index_merge_intersection,
index_merge_union和index_merge_sort_union标志的值。默
认情况下,所有这些标志都打开。 要仅启用特定算法,请将
index_merge设置为关闭,并仅启用其他应允许的其他算法。
* intersect:索引合并Intersection访问算法对所有使用的索引执行同
时扫描,将其结果进行交集运算,即AND。例子如下:
结果进行交集运算,即AND。例子如下:
--例一:条件使用到复合索引中的所有字段或者左前缀字段(对单字段索引也适用)
key_part1=const1 AND key_part2=const2 ... AND key_partN=constN
--例二:主键上的任何范围条件
SELECT * FROM innodb_table WHERE primary_key < 10 AND key_col1=20;
SELECT * FROM tbl_name WHERE (key1_part1=1 AND key1_part2=2) AND key2=2;
上面只说到复合索引,但是其实单字段索引显然也是一
样的。比如:
select * from tab where key1=xx and key2 =xxx; 也是
有可能进行index intersect merge的。另外上面两种情
况的AND组合也一样可能会进行index intersect merge。
如果查询中使用的所有列都被使用的索引覆盖,则
不会检索完整的表行(EXPLAIN输出包含在这种情况下
在Extra字段中 Using index)。索引合并Intersection访
问算法对所有使用的索引执行同时扫描,并产生从合并
索引扫描中接收到的行序列的交集。如果使用的索引不
包括查询中使用的所有列,则只有满足所有使用的键的
范围条件时才检索完整的行。如果其中一个合并条件是
InnoDB表的主键条件,则不用于行检索,而是用于过
滤使用其他条件检索的行。
* union:简单而言,index uion merge就是多个索引条件扫描,对
得到的结果进行并集运算,即OR运算。下面几种类型的
where 条件,以及他们的组合
可能会使用到 index union merge算法:
第一,条件使用到复合索引中的所有字段或者左前缀字段
(对单字段索引也适用)
第二,主键上的任何范围条件
第三,任何符合 index intersect merge 的where条件
SELECT * FROM t1 WHERE key1=1 OR key2=2 OR key3=3;
SELECT * FROM innodb_table WHERE (key1=1 AND key2=2) OR (key3='foo' AND key4='bar') AND key5=5;
解说:第一个例子,就是三个 单字段索引 进行 OR 运算,
所以他们可能会使用 index union merge算法。
第二个例子,复杂一点。(key1=1 AND key2=2) 是
符合index intersect merge;
(key3='foo' AND key4='bar') AND key5=5 也是符合
index intersect merge,所以 二者之间进行 OR 运算,
自然可能会使用 index union merge算法。
* sort_union:多个条件扫描进行 OR 运算,但是不符合
index union merge算法的,此时可能会使用
sort_union算法。官方给出如下两个例子:
SELECT * FROM tbl_name WHERE key_col1 < 10 OR key_col2 < 20;
SELECT * FROM tbl_name WHERE (key_col1 > 10 OR key_col2 = 20) AND nonkey_col=30;
索引合并Sort-Union访问算法和索引合并Union访问
算法的区别在于,索引合并Sort-Union访问算法必须
首先为所有行提取行ID,然后在返回任何行之前对其
进行排序(表示没看懂)。
对 index merge 的进一步优化(表示不是很理解,复合索引不就
是intersect吗?有空搞明白):
index merge使得我们可以使用到多个索引同时进行扫描,然
后将结果进行合并。听起来好像是很好的功能,但是如果出现
了 index intersect merge,那么一般同时也意味着我们的索引
建立得不太合理,因为 index intersect merge 是可以通过建立
复合索引进行更一步优化的。
index merge的局限:
如果我们的条件比较复杂,用到多个 and / or 条件运算,而
MySQL没有使用最优的执行计划,那么可以使用如下的两个
等式将条件进行转换一下:
(x AND y) OR z = (x OR z) AND (y OR z)
(x OR y) AND z = (x AND z) OR (y AND z)
unique_subquery:该类型替换了下面形式的IN子查询的ref:
value IN (SELECT primary_key FROM single_table
WHERE some_expr)
unique_subquery是一个索引查找函数,可以完全替换子查询,
效率更高。
index_subquery:该联接类型类似于unique_subquery。可以替换IN子查询,但只适
合下列形式的子查询中的非唯一索引(子查询可能返回重复值):
value IN (SELECT key_column FROM single_table WHERE
some_expr)
range: 索引的范围查询。只检索给定范围的行,使用一个索引来选择行。key
列显示使用了那个索引。一般就是在where语句中出现了bettween、
<、>、in等的查询。这种索引列上的范围扫描比全索引扫描要好。只
需要开始于某个点,结束于另一个点,不用扫描全部索引。
index:Full Index Scan,index与ALL区别为index类型只遍历索引树。这通常为
ALL块,因为索引文件通常比数据文件小。(Index与ALL虽然都是读全
表,但index是从索引中读取,而ALL是从硬盘读取,出现这个如果能优
化也尽量优化,但是如下例,确实是无法优化)
ALL:Full Table Scan,遍历全表以找到匹配的行
(4)possible_keys
查询可能使用到的索引都会在这里列出来
(5)key
查询真正使用到的索引,如果没有索引被选择,键是NULL;查询
中如果使用了覆盖索引,则该索引仅出现在key列表中;如果
select_type为index_merge时,这里可能出现两个以上的索引,其
他的select_type这里只会出现一个。想要让mysql强行使用或者忽
略在 possible_keys字段中的索引列表,可以在查询语句中使用关
键字force index, use index,或 ignore index。如果是myisam 和 bdb
类型表,可以使用 analyzetable 来帮助分析使用使用哪个索引更好。
如果是 myisam类型表,运行命令 myisamchk --analyze也是一样的
效果。详细的可以查看章节"14.5.2.1 analyze tablesyntax"和
"5.7.2 tablemaintenance and crash recovery"(这里没细究,详
情请百度搜索标黑的章节)。
(6)key_len
表示索引中使用的字节数,查询中使用的索引的长度(最大可能长
度),并非实际使用长度,理论上长度越短越好。key_len是根据
表定义计算而得的,不是通过表内检索出的。当key字段的值为
null时,索引就是null。
(7)ref
显示索引的哪一列被使用了,即“哪些字段或者常量被用来和key
配合从表中查询记录出来“。
如果是使用的常数等值查询,这里会显示const,如果是连
接查询,被驱动表的执行计划这里会显示驱动表的关联字段,
如果是条件使用了表达式或者函数,或者条件列发生了内部隐
式转换,这里可能显示为func
补充,什么是驱动表?
通俗的讲就是先从哪个表开始检索,找到好的驱动表语句
的优化就成功一半了。例如:
select * from a,b where a.id = b.id and a.姓名 = '美格瑞恩'
and b.性别 = '女';在a,b表同等数量级的情况下显然用a表做
为驱动表比较好因为姓名相对于性别来说可以过滤掉更多
的数据,所以想办法使你的执行计划扫描a表先再通过
nest loop与b表关连比较理想。
驱动表的定义:
wwh999 在 2006年总结说,当进行多表连接查询时, 驱动表的
定义为:
* 指定了联接条件时,满足查询条件的记录行数少的表为“驱动表”。
* 未指定联接条件时,行数少的表为“驱动表”。
(8)rows
根据表统计信息及索引选用情况,大致估算出找到所需的记录
所需要读取的行数。
(9)Extra
本字段显示了查询中mysql的附加信息
关于Extra的一些例子参看如下博客:
https://www.cnblogs.com/wy123/p/7366486.html
Extra的中只有出现Using filesort, Using temporary才需要优化
* Distinct
一旦MYSQL找到了与行相联合匹配的行,就不再搜索了
* Not exists
MYSQL优化了LEFT JOIN,一旦它找到了匹配LEFT JOIN标准
的行,就不再搜索了
* Range checked for each
Record(index map:#)
没有找到理想的索引,因此对于从前面表中来的每一个行组合,
MYSQL检查使用哪个索引,并用它来从表中返回行。这是使用
索引的最慢的连接之一
* Using filesort(需要回表查询)
mysql对数据使用一个外部的索引排序,而不是按照表内的索引
进行排序读取。也就是说mysql无法利用索引完成的排序操作称
为“文件排序”。看到这个的时候,查询就需要优化了。
由于索引是先按email排序、再按address排序,所以查询时如果
直接按address排序,索引就不能满足要求了,mysql内部必须再
实现一次“文件排序”。
MySql中的排序:
MySql中有两种排序,一种是“索引排序”,一种是“文件排
序”,索引排序效率比较高,所以排序字段需要建立索引。
MySql中ORDER BY、GROUP BY都会用到排序,
ORDER BY 一定要建立索引;GROUP BY是否建立索引尚
有争议,有的地方说“需要建立”,有的地方说“视情况而定”,
有空在研究一下。关于MySql的排序,
详见:
https://www.cnblogs.com/cchust/p/5304594.html。
该篇文章因为时间关系我没有看
Group By不加索引的理由:
https://blog.csdn.net/epee/article/details/83169669
* Using temporary
看到这个的时候,查询需要优化了。表示 MySQL 在对查询结果
排序时使用临时表。常见于排序 order by 和分组查询 group by上(这只是
一个比较典型的例子,并不是只有order by和group by才会出现)。
例一:
例二:
EXPLAIN SELECT ads.id FROM ads, city WHERE city.city_id = 8005
AND ads.status = 'online' AND city.ads_id=ads.id ORDER BY ads.id desc
id select_type table type possible_keys key key_len ref rows filtered Extra
------ ----------- ------ ------ -------------- ------- ------- -------------------- ------ -------- -------------------------------
1 SIMPLE city ref ads_id,city_id city_id 4 const 2838 100.00 Using temporary; Using filesort
1 SIMPLE ads eq_ref PRIMARY PRIMARY 4 city.ads_id 1 100.00 Using where
这条语句会使用using temporary,而下面这条语句则不会
EXPLAIN SELECT ads.id FROM ads, city WHERE city.city_id = 8005 AND ads.status = 'online' AND city.ads_id=ads.id ORDER BYcity.ads_id desc
id select_type table type possible_keys key key_len ref rows filtered Extra
------ ----------- ------ ------ -------------- ------- ------- -------------------- ------ -------- ---------------------------
1 SIMPLE city ref ads_id,city_id city_id 4 const 2838 100.00 Using where; Using filesort
1 SIMPLE ads eq_ref PRIMARY PRIMARY 4 city.ads_id 1 100.00 Using where
这是为什么呢?他俩之间只是一个order by不同,MySQL 表关联的算法是
Nest Loop Join,是通过驱动表的结果集作为循环基础数据,然后一条一条
地通过该结果集中的数据作为过滤条件到下一个表中查询数据,然后合并结
果。EXPLAIN 结果中,第一行出现的表就是驱动表(Important!)以上两个
查询语句,驱动表都是 city,如上面的执行计划所示!对驱动表可以直接排
序,对非驱动表(的字段排序)需要对循环查询的合并结果(临时表)进行
排序因此,order by ads.id desc 时,就要先 using temporary 了!
* Using where(需要回表查询)
使用了WHERE从句来限制哪些行将与下一张表匹配或者是返回
给用户。Extra列出现Using where表示MySQL服务器将存储引擎
返回服务层以后再应用WHERE条件过滤。
* Using index(不需要回表查询)
表示相应的select操作中使用了覆盖索引(Covering Index),
避免了访问表的数据行,效率高如果同时出现Using where,表
明索引被用来执行索引键值的查找(参考 Using temporary中的
图)如果没用同时出现Using where,表明索引用来读取数据而
非执行查找动作
* Using index condition
这是MySQL 5.6出来的新特性,叫做“索引条件推送”。简单说一
点就是MySQL原来在索引上是不能执行如like这样的操作的,但
是现在可以了,这样减少了不必要的IO操作,但是只能用在二级
索引上,详情点这里。
SQL优化的例子:
例一,根据Type和Extra优化SQL
创建一张表:
CREATE TABLE IF NOT EXISTS `article` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`author_id` int(10) unsigned NOT NULL,
`category_id` int(10) unsigned NOT NULL,
`views` int(10) unsigned NOT NULL,
`comments` int(10) unsigned NOT NULL,
`title` varbinary(255) NOT NULL,
`content` text NOT NULL,
PRIMARY KEY (`id`)
);
插几条数据:
INSERT INTO `article`
(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES
(1, 1, 1, 1, '1', '1'),
(2, 2, 2, 2, '2', '2'),
(1, 1, 3, 3, '3', '3');
需求:查询 category_id 为 1 且 comments 大于 1 的情况下,
views最多的 article_id。
执行:
EXPLAIN
SELECT author_id
FROM `article`
WHERE category_id = 1 AND comments > 1
ORDER BY views DESC
LIMIT 1\G
看看部分输出结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: article
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 3
Extra: Using where; Using filesort
1 row in set (0.00 sec)
type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,
也是最坏的情况。优化是必须的。
加索引,为where 之后使用的 category_id,comments,
views 三个字段三个字段加联合索引。
ALTER TABLE `article` ADD INDEX x ( `category_id` , `comments`, `views` );
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: article
type: range
possible_keys: x
key: x
key_len: 8
ref: NULL
rows: 1
Extra: Using where; Using filesort
1 row in set (0.00 sec)
type 变成了 range,这是可以忍受的。但是 extra 里使用
Using filesort仍是无法接受的。但是我们已经建立了索引,为啥
没用呢?这是因为按照BTree 索引的工作原理,先排序
category_id,如果遇到相同的category_id则再排序 comments,
如果遇到相同的 comments 则再排序views。当comments 字段
在联合索引里处于中间位置时,因comments > 1 条件是一个范
围值(断点),MySQL 无法利用索引再对后面的 views 部分进
行检索,即 range 类型查询字段后面的索引无效。
那么我们需要抛弃 comments,删除旧索引:
DROP INDEX x ON article;
建立新索引:
ALTER TABLE `article` ADD INDEX y ( `category_id` , `views` ) ;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: article
type: ref
possible_keys: y
key: y
key_len: 4
ref: const
rows: 1
Extra: Using where
1 row in set (0.00 sec)
可以看到,type 变为了 ref,Extra 中的 Using filesort 也消失
了,结果非常理想。
例二,LEFT JOIN、RIGHT JOIN、INNERJOIN优化技巧。
多表连接优化的时候,inner join 和 left join 相似,需要优
化右表。而 right join 需要优化左表。
定义三个表:
CREATE TABLE IF NOT EXISTS `class` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`card` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `book` (
`bookid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`card` int(10) unsigned NOT NULL,
PRIMARY KEY (`bookid`)
);
CREATE TABLE IF NOT EXISTS `phone` (
`phoneid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`card` int(10) unsigned NOT NULL,
PRIMARY KEY (`phoneid`)
) engine = innodb;
然后再分别插入大量数据。插入数据的php脚本:
Left Join优化
Left Join优化,右表需要建索引而左表不需要。
因为Left Join 条件用于确定如何从右表搜索行,左边
一定都有,所以优化的重点在右边。所以有了
“Left Join优化,右表需要建索引而左表不需要”这个结
论。
例子,一个左连接查询:
explain select * from class left join book on class.card = book.card\G
执行结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: class
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: book
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
2 rows in set (0.00 sec)
显然第二个 ALL 是需要我们进行优化的。
建立索引:
ALTER TABLE `book` ADD INDEX y ( `card`);
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: class
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: book
type: ref
possible_keys: y
key: y
key_len: 4
ref: test.class.card
rows: 1000
Extra:
2 rows in set (0.00 sec)
可以看到第二行的 type 变为了 ref,rows 也变成了
1741*18,优化比较明显。
Left Join的反向测试,左表建立索引,右表不建:
删除原索引:
DROP INDEX y ON book;
建立新索引:
ALTER TABLE `class` ADD INDEX x ( `card`);
SQL不变,执行结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: class
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: book
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
2 rows in set (0.00 sec)
可见性能并未提升
Right Join优化:
Right Join优化和Left Join相反,右边的表数据全
部都有,左边的表只筛选满足条件的,所以优化的
重点在右边的表。所以左边的表需要加索引而右边
的表不需要加索引。
explain select * from class right join book on class.card = book.card;
执行结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: book
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: class
type: ref
possible_keys: x
key: x
key_len: 4
ref: test.book.card
rows: 1000
Extra:
2 rows in set (0.00 sec)
优化较明显。
注:左表的索引是在“Left Join优化那一节”已经加了
的,所以此步骤没有在索引。
inner join优化:
inner join和left join相似都需要优化右表
删除左表索引:
DROP INDEX x ON class;
建立新索引:
ALTER TABLE `book` ADD INDEX y ( `card`);
执行如下代码:
explain select * from class inner join book on class.card = book.card;
执行结果如下:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: book
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: class
type: ref
possible_keys: x
key: x
key_len: 4
ref: test.book.card
rows: 1000
Extra:
2 rows in set (0.00 sec)
删除旧索引:
DROP INDEX y ON book;
执行相同代码,结果如下:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: class
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: book
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
2 rows in set (0.00 sec)
建立新索引:
ALTER TABLE `class` ADD INDEX x ( `card`);
执行相同代码,结果如下:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: class
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: book
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
2 rows in set (0.00 sec)
由此可见,inner join需要优化右边表。
三个表查询优化的例子:
首先删除所有索引然后执行如下两段代码
ALTER TABLE `phone` ADD INDEX z ( `card`);
ALTER TABLE `book` ADD INDEX y ( `card`);
explain select * from class
left join book on class.card=book.card
left join phone on book.card = phone.card;
执行结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: class
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: book
type: ref
possible_keys: y
key: y
key_len: 4
ref: test.class.card
rows: 1000
Extra:
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: phone
type: ref
possible_keys: z
key: z
key_len: 4
ref: test.book.card
rows: 260
Extra: Using index
3 rows in set (0.00 sec)
后 2 行的 type 都是 ref 且总 rows 优化很好,效果不错。
4.MyISAM和InnoDB的区别
参考文章:https://blog.csdn.net/UFO___/article/details/80519309
可间,MyISAM主要用作查询,MyISAM和InnoDB的区别总结如下:
(1)MyISAM不支持事务,而InnoDB支持
(2)InnoDB支持数据行锁定,MyISAM不支持行锁定,只支持锁定整个表
(3)InnoDB支持外键,MyISAM不支持
(4)InnoDB的主键范围更大,最大是MyISAM的2倍。
(5)InnoDB不支持全文索引,而MyISAM支持
(6)没有where的count(*)使用MyISAM要比InnoDB快得多。因为MyISAM内置了一
个计数器,count(*)时它直接从计数器中读,而InnoDB必须扫描全表。
(7)MyISAM支持GIS数据,InnoDB不支持
5.mysql和oracle比较
(1)简单版
https://www.cnblogs.com/jeffen/p/6000198.html
(2)详细了解
https://www.cnblogs.com/wangwangever/p/7809788.html
https://www.cnblogs.com/TaoLeonis/p/7043543.html
然后在百度一些东西。
6.mysql大数据量翻页
(1)问题的产生
假设有一个千万量级的表,取1到10条数据;
select * from table limit 0,10;
select * from table limit 1000,10;
这两条语句查询时间应该在毫秒级完成。但是,下面这条语句执行时间
大约要5秒
select * from table limit 3000000,10;
为什么相差这么大?这是因为mysql并没有你想的那么智能,比如你要查
询 300w开始后面10条数据,mysql会读取300w加10条这么多的数据,
只不过过滤后返回最后10条而已。
(2)解决方案
(i)业务解决,比如百度,你翻76页就不让你翻了。
(ii)如果你的table的主键id是自增的,并且中间没有删除和断点,可以
使用如下这种方式——在查询下一页的的时候把上一页的行作为参数
传递给客户端。如下例:
select * from table where id>3000000 limit 10;
这条语句执行也是在毫秒级完成的,id>300w其实就是让mysql直接跳到这
里了,不用依次在扫描全面所有的行。
如果你的table的主键id是自增的,并且中间没有删除和断点,比如100页的
10条数据,如下
select * from table where id>100*10 limit 10;
这种方式因为索引扫描,所以速度会很快。有朋友提出: 因为数据
查询出来并不是按照pk_id排序的,所以会有漏掉数据的情况,只
能用如下方法:
基于索引的在排序:
SELECT * FROM 表名称 WHERE id_pk > (pageNum*10) ORDER BY id_pk ASC LIMIT M
(iii)延迟关联(基于子查询)
我们在来分析一下这条语句为什么慢,慢在哪里
select * from table limit 3000000,10;
玄机就处在这个 * 里面,这个表除了id主键肯定还有其他字段 比如 name、
age之类的,因为select * 所以mysql在沿着id主键走的时候要回行拿数据,
走一下拿一下数据。
如果把语句改成:
select id from table limit 3000000,10;
你会发现时间缩短了一半;然后我们在拿id分别去取10条数据就行了。
语句就改成这样了:
select table.* from table inner join ( select id from table limit 3000000,10 ) as tmp on tmp.id=table.id;
这三种方法最先考虑第一种 其次第二种,第三种是别无选择
(iv)基于索引使用prepare(第一个问号表示pageNum,第二个?表
示每页元组数)
PREPARE stmt_name FROM SELECT * FROM 表名称 WHERE id_pk > (?* ?) ORDER BY id_pk ASC LIMIT M
mysql处理prepare的原理:
https://www.cnblogs.com/justfortaste/p/3920140.html
7.truncate和delete的区别
(1)DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该
行的删除操作作为事务记录在日志中保存以便进行进行回滚操作;
TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的
删除操作记录记入日志保存,删除行是不能恢复的,并且在删除的过
程中不会激活与表有关的删除触发器。执行速度快。
(2)当表被TRUNCATE 后,这个表和索引所占用的空间会恢复到初始大
小;DELETE操作不会减少表或索引所占用的空间。
(3)TRUNCATE 只能对TABLE; DELETE可以是table和view
8.mysql锁会通过索引锁定
(1)行锁有时候会锁大于返回记录的行
InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB
访问的行数,从而减少锁的数量。但这只有当InnoDB在存储引擎层能
够过滤掉所有不需要的行时才有效。如果索引无法过滤掉无效的行,
那么在InnoDB检索到数据并返回给服务器层以后,MySQL服务器才能
应用WHERE子句。这时已经无法避免锁定行了:InnoDB已经锁住了这
些行,到适当的时候才释放。在MySQL5.1和更新的版本中,InnoDB可
以在服务器端过滤掉行后就释放锁,但是在早期的MySQL版本中,
InnoDB只有在事务提交后才能释放锁。
这个查询仅仅会返回2~4之间的行,但是实际上获取了1~4之间的行的排
他锁。InnoDB会锁住第1行,这是因为MySQL为该查询选择的执行计划
是索引范围扫描:
换句话说,底层存储引擎的操作是“从索引的开头开始获取满足条件
actor_id < 5的记录“,服务器并没有告诉InnoDB可以过滤第1行的
WHERE条件。注意到EXPLAIN的Extra列出现了”Using where“,这表示
MySQL服务器将存储引擎返回行以后再应用WHERE过滤条件。下面的
第2个查询就能证明第1行确实已经被锁定了,尽管第1个查询的结果中并
没有这个第1行。保持第1个连接打开,然后开启第2个连接并执行如下查
询:
就像这个例子显示的,即使使用了索引,InnoDB也可能锁定一些不需要
的数据。如果不能使用索引查找和锁定行的话问题可能会更糟糕,
MySQL会做全表扫描并锁住所有的行,而不管是不是需要。关于InnoDB、
索引和锁有一些很少有人知道的细节:
InnoDB在二级索引上使用共享(读)锁,但访问主键索引需要排他
(写)锁。这消除了使用覆盖索引的可能性,并且使得
SELECT FOR UPDATE比LOCK IN SHAREMODE或非锁定查询要慢得多。
关于一级索引和二级索引(聚簇索引和非聚簇索引):
每个InnoDB表具有一个特殊的索引称为聚簇索引(也叫聚集索引,
聚类索引,簇集索引,注:这只有InnoDB才有的)。如果表上定义有主
键,该主键索引就是聚簇索引。如果未定义主键,MySQL取第一个唯一
索引(unique)而且只含非空列(NOT NULL)作为主键,InnoDB使用
它作为聚簇索引。如果没有这样的列,InnoDB就自己产生一个这样的ID
值,它有六个字节,而且是隐藏的,使其作为聚簇索引。表中的聚簇索
引(clustered index )就是一级索引,除此之外,表上的其他非聚簇索
引都是二级索引,又叫辅助索引(secondary indexes)。主键是聚集索
引,唯一索引、普通索引、前缀索引等都是二级索引(辅助索引)。
Mysql中InnoDB引擎的主键索引为聚簇索引,MyISAM存储引擎采用
非聚集索引聚簇索引的叶子节点存的是数据,而非聚簇索引的叶子节点
存的是主键。实例如下图:
聚簇索引(主键索引)
从图中我们可以看到,索引数据和存储数据都是在一颗树上,存在一
起的。通过定位索引就直接可以查找到数据。这棵树是根据主键进行
创建的。如果查找id=16的编程语言,
select id, plname, ranking from pl_ranking where id=16;
则只需要读取3个磁盘块,就可以获取到数据。
二级索引(辅助索引)
从上图中我们发现,该B*tree根据plname列进行构建的,只存储索引
数据,plname 和 id 的映射。比如查找 编程语言为“Java”的数据。
select id, plname, ranking from pl_ranking where plname='Java';
首先通过二级索引树中找到 Java 对应的主键id 为 “16”(读取2个磁盘
块)。然后在去主键索引中查找id为“16” 的数据。(读取3个磁盘块)
select id, plname, ranking from pl_ranking where id=16; 所以,
select id, plname, ranking from pl_ranking where plname='Java';
根据编程语言名称查询需要读取5个磁盘块
结论一:
主键索引(聚餐索引)查询效率比非主键索引查询效率更高。如果能
使用主键查找的,就尽量使用主键索引进行查找。
结论二:
主键定义的长度越小,二级索引的大小就越小,这样每个磁盘块存储
的索引数据越多,查询效率就越高。
(2)表锁出现的死锁
在MySQL中,行级锁并不是直接锁记录,而是锁索引。索引分为主键
索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就
会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先
锁定该非主键索引,再锁定相关的主键索引。
案例一:
tab_test 结构如下:
id:主键;
state:状态;
time:时间;
索引:idx_1(state,time)
出现死锁的2条sql语句:
update tab_test set state=1064,time=now() where state=1061
and time < date_sub(now(), INTERVAL 30 minute);
update tab_test set state=1067,time=now () where id in (9921180)
原因分析:
当“update tab_test set state=1064,time=now() where
state=1061 and time < date_sub(now(), INTERVAL 30 minute)”
执行时,MySQL会使用idx_1索引,因此首先锁定相关的索引记
录,因为idx_1是非主键索引,为执行该语句,MySQL还会锁定
主键索引。假设“update tab_test set state=1067,time=now ()
where id in (9921180)”几乎同时执行时,本语句首先锁定主键索
引,由于需要更新state的值,所以还需要锁定idx_1的某些索引记
录。这样第一条语句锁定了idx_1的记录,等待主键索引,而第二
条语句则锁定了主键索引记录,而等待idx_1的记录,这样死锁就
产生了。在第一条语句给主键加锁前,第二条语句已经给主键加
了锁,所以在高并发的数据操作时,死锁的情况
就容易产生InnoDB 会自动检测一个事务的死锁并回滚一个或多
个事务来防止死锁。Innodb会选择代价比较小的事务回滚,此次
事务(1)解锁并回滚,语句(2)继续运行直至事务结束。
解决方案:
拆分第一条sql,先查出符合条件的主键值,再按照主键更新记录:
select id from tab_test where state=1061 and time <
date_sub(now(), INTERVAL 30 minute);
update tab_test state=1064,time=now() where id in(......);
其它死锁情况参见:https://www.cnblogs.com/phpfans/p/4649883.html
9.数据库锁
(1)共享锁和排它锁(原文:http://www.cnblogs.com/boblogsbo/p/5602122.html)
mysql锁机制分为表级锁和行级锁
共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于
同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所
并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获
取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可
以对数据就行读取和修改。
共享锁:又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一
数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
排它锁:又称为写锁,独占锁,简称X锁,顾名思义,排他锁就是不能与其
他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不
能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的
事务是可以对数据就行读取和修改。
对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据,对于
排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁
住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排
他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其
他的锁,mysql InnoDB引擎默认的修改数据语句,update,delete,insert
都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型(
网上说默认会加S锁,取决于隔离级别),如果加排他锁可以使用
select ...for update语句,加共享锁可以使用select ... lock in share mode
语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通
过for update和lock in share mode锁的方式查询数据,但可以直接通过
select ...from...查询数据,因为普通查询没有任何锁机制;在加了共享锁的
语句上,可以在加共享锁,但是不能加排它锁。实验如下图:
我们看到是可以查询数据的,但加排他锁就查不到,因为排他锁与共享锁不
能存在同一数据上。
(2)悲观锁和乐观锁
悲观锁:在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,
Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制
的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。
如果一个事务执行的操作都某行数据应用了锁,那只有当这个事
务把锁释放,其他事务才能够执行与该锁冲突的操作。悲观并发
控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁
保护数据的成本要低于回滚事务的成本的环境中。悲观锁的实现,
往往依靠数据库提供的锁机制。悲观锁的执行流程如下:
(i)在对任意记录进行修改前,先尝试为该记录加上排他锁
(exclusive locking)。
(ii)如果加锁失败,说明该记录正在被修改,那么当前查询
可能要等待或者抛出异常。 具体响应方式由开发者根据
实际需要决定。
(iii)如果成功加锁,那么就可以对记录做修改,事务完成后
就会解锁了。
(iv)其间如果有其他对该记录做修改或加排他锁的操作,都
会等待我们解锁或直接抛出异常。
优点与不足:
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处
理的安全提供了保证。但是在效率方面,处理加锁的机制会让
数据库产生额外的开销,还有增加产生死锁的机会;另外,在
只读型事务处理中由于不会产生冲突,也没必要使用锁,这样
做只能增加系统负载;还有会降低了并行性,一个事务如果锁
定了某行数据,其他事务就必须等待该事务处理完才可以处理
那行数据。
MySQL InnoDB中使用悲观锁:
要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,
因为MySQL默认使用autocommit模式,也就是说,当你执行
一个更新操作后,MySQL会立刻将结果进行提交。
set autocommit=0;
//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;/commit work;
上面的查询语句中,我们使用了select…for update的方式,这样就通过
开启排他锁的方式实现了悲观锁。此时在t_goods表中,id为1的 那条数
据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这
样我们可以保证当前的数据不会被其它事务修改。
上面我们提到,使用select…for update会把数据给锁住,不过我们
需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于
索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表
级锁把整张表锁住,这点需要注意。
乐观锁:在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,
Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。
它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不
产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每
个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。
如果其他事务有更新的话,正在提交的事务会进行回滚。乐观锁
( Optimistic Locking ) 是相对悲观锁而言,乐观锁假设认为数据一般情
况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的
冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户
决定如何去做。相对于悲观锁,在对数据库进行处理的时候,乐观锁并不
会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识
的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交
更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版
本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值
相等,则予以更新,否则认为是过期数据。
实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间
戳。
使用版本号实现乐观锁:
1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
使用时间戳实现乐观锁:有时间在查。
优点与不足
(3)行锁死锁
死锁分行锁死锁和表锁死锁,常见的是行锁死锁,本节介绍行锁死锁
下一节介绍表锁死锁。
(a)在学习行锁死锁之前要补充两个知识点:
(i)在MySQL中,行级锁并不是直接锁记录,而是锁索引。
索引分为主键索引和非主键索引两种,如果一条sql语句操作
了主键索引,MySQL就会锁定这条主键索引;如果一条语句操
作了非主键索引,MySQL会先锁该非主键索引,然后在定再锁
定相关的主键索引。
(ii)行锁是逐行获取的。
比如表A有50行记录,语句一逐行获取锁,获取了1到50条
的锁,语句2逐条获取锁,获取了100到51条的锁,当语句1要
获取第51条记录的锁,语句2要获取第50条记录 的锁,这个时
候就出现了互相等待的状况。
(iii)除了单个SQL组成的事务外,锁是逐步获取的
(b)四种死锁情况
(i)不同表的相同记录行锁冲突
案例:两个表、两行记录,交叉获得和申请互斥锁
条件:
- 两事务分别操作两个表、相同表的同一行记录
- 申请的锁互斥
- 申请的顺序不一致
(ii)主键索引锁冲突
案例:本文案例,产生冲突在主键索引锁上
条件:
- 两sql语句即两事务操作同一个表、使用不同索引
- 申请的锁互斥
- 操作多行记录
- 查找到记录的顺序不一致
案例:
tab_test 结构如下:
id:主键;
state:状态;
time:时间;
索引:idx_1(state,time)
出现死锁的2条sql语句:
update tab_test set state=1064,time=now() where state=1061
and time < date_sub(now(), INTERVAL 30 minute);
update tab_test set state=1067,time=now () where id in (9921180)
原因分析:
当“update tab_test set state=1064,time=now()
where state=1061 and
time < date_sub(now(), INTERVAL 30 minute)”
执行时,MySQL会使用idx_1索引,因此首先锁定相关
的索引记录,因为idx_1是非主键索引,为执行该语句,
MySQL还会锁定主键索引。假设“update tab_test set
state=1067,time=now () where id in (9921180)”几乎同
时执行时,本语句首先锁定主键索引,由于需要更新
state的值,所以还需要锁定idx_1的某些索引记录。这
样第一条语句锁定了idx_1的记录,等待主键索引,而
第二条语句则锁定了主键索引记录,而等待idx_1的记
录,这样死锁就产生了。在第一条语句给主键加锁前,
第二条语句已经给主键加了锁,所以在高并发的数据
操作时,死锁的情况就容易产生InnoDB 会自动检测一
个事务的死锁并回滚一个或多个事务来防止死锁。Innodb
会选择代价比较小的事务回滚,此次事务。
解决方案:
拆分第一条sql,先查出符合条件的主键值,再按
照主键更新记录:
select id from tab_test where state=1061 and time <
date_sub(now(), INTERVAL 30 minute);
update tab_test state=1064,time=now() where id in(......);
(iii)主键索引锁与非聚簇索引锁冲突
直接看案例:
teamUser表的表结构如下:
PRIMARY KEY (`uid`,`Id`),
KEY `k_id_titleWeight_score` (`Id`,`titleWeight`,`score`),
ENGINE=InnoDB
出现死锁的两条SQL语句如下:
insert into teamUser_20110121 select * from teamUser
DELETE FROM teamUser WHERE teamId=$teamId AND titleWeight<32768 AND joinTime<'$daysago_1week'
原因分析:
在innodb默认的事务隔离级别下,普通的SELECT是不
需要加行锁的,只有LOCK IN SHARE MODE、
FOR UPDATE及高串行化级别中的SELECT都要加锁。但
是此案例中的,第一条语句这种情况
insert into teamUser_20110121 select * from teamUser会
对表teamUser_20110121(ENGINE= MyISAM)加表锁,
并对teamUser表所有行的主键索引(即聚簇索引)加共享
锁。默认对其使用主键索引。而第二条语句
DELETE FROM teamUser WHERE teamId=$teamId
AND titleWeight<32768 AND joinTime<'$daysago_1week'
为删除操作,会对选中行的主键索引加排他锁。由于此语句
还使用了非聚簇索引
KEY `k_teamid_titleWeight_score` (`teamId`,`titleWeight`,`score`)
的前缀索引,于是,还会对相关行的此非聚簇索引加排他锁。
由于共享锁与排他锁是互斥的,当一方拥有了某行记录的
排他锁后,另一方就不能其拥有共享锁,同样,一方拥有了某
行共享锁后,另一方也无法得到其排他锁(可见,元凶是锁是
逐行获取的,比如teamUser表有50行记录,语句一逐行获取锁,
获取了1到50条的锁,语句2逐条获取锁,获取了100到51条的
锁,当语句1要获取第51条记录的锁,语句2要获取第50条记录
的锁,这个时候就出现了互相等待的状况)。所以两条语句同
时行时,相当于两个事务会同时申请某相同记录行的锁资源,
于是会产生锁冲突。由于两个事务都会申请主键索引,锁冲突
只会发生在主键索引上。
解决方案:
InnoDB给MySQL提供了具有提交,回滚和崩溃恢复能力
的事务安全(ACID兼容)存储引擎。InnoDB锁定在行级并且
也在SELECT语句提供非锁定读。这些特色增加了多用户部署
和性能。但其行锁的机制也带来了产生死锁的风险,这就需
要在应用程序设计时避免死锁的发生。以单个SQL语句组成的
隐式事务来说,建议的避免死锁的方法如下:
-- 如果使用insert…select语句备份表格且数据量较大,在单
独的时间点操作,避免与其他sql语句争夺资源,或使用
select into outfile加上load data infile代替 insert…select,
这样不仅快,而且不会要求锁定
-- 一个锁定记录集的事务,其操作结果集应尽量简短,以免
一次占用太多资源,与其他事务处理的记录冲突。
-- 更新或者删除表格数据,sql语句的where条件都是主键或
都是索引,避免两种情况交叉,造成死锁。对于where子
句较复杂的情况,将其单独通过sql得到后,再在更新语句
中使用。
-- sql语句的嵌套表格不要太多,能拆分就拆分,避免占有资
源同时等待资源,导致与其他事务冲突。
-- 对定点运行脚本的情况,避免在同一时间点运行多个对同
一表进行读写的脚本,特别注意加锁且操作数据量比较大的
语句。
-- 应用程序中增加对死锁的判断,如果事务意外结束,重新运
行该事务,减少对功能的影响。
(iv)锁升级造成锁队列阻塞
提交:
- 两事务操作同一行记录
- 一事务对某一记录先申请共享锁,再升级为排他锁
- 另一事务在过程中申请这一记录的排他锁
例如,用户A查询一条纪录,然后修改该条纪录;这时用户B修
改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升
到独占锁,而用户B里的独占锁由于A有共享锁存在所以必须等A释
放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释
放共享锁,于是出现了死锁。这种死锁由于比较隐蔽,但在稍大点
的项目中经常发生。
一般更新模式由一个事务组成,此事务读取记录,获取资源
(页或行)的共享 (S) 锁,然后修改行,此操作要求锁转换为排它 (X)
锁。如果两个事务获得了资源上的共享模式锁,然后试图同时更新
数据,则一个事务尝试将锁转换为排它 (X) 锁。共享模式到排它锁
的转换必须等待一段时间,因为一个事务的排它锁与其它事务的共
享模式锁不兼容;发生锁等待。第二个事务试图获取排它 (X) 锁以
进行更新。由于两个事务都要转换为排它 (X) 锁,并且每个事务都
等待另一个事务释放共享模式锁,因此发生死锁。
解决方法:
-- 使用乐观锁进行控制
-- 使用悲观锁进行控制
(4)表锁死锁
出现原因:
一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B访
问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经
锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A
释放表A才能继续,这就死锁就产生了。
解决方法:
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的
逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操作时,
尽量按照相同的顺序进行处理,尽量避免同时锁定两个资源,如操作A
和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,
要保证在任何时刻都应该按照相同的顺序来锁定资源。
(b)并发修改同一记录
(c)参见4中的(2)
(4)mysql行级锁、表级锁、页级锁
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最
高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最
低,并发度也最高。
行锁的死锁:参见4中的(2)
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表
锁和行锁之间,并发度一般。
mysql行锁、表锁一些需要注意的点:
(i)在不通过索引条件查询的时候,InnoDB 确实使用的是表锁,而不是行锁。
(ii)由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽
然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲
突的。应用设计的时候要注意这一点。
(iii)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,
另外,不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行
锁来对数据加锁。
(iv)即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL
通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率
更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB
将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的
执行计划,以确认是否真正使用了索引。
什么时候使用表锁?
对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往
往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用
表级锁。
第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认
的行锁,不仅这个事务执行效率低,而且可能造成其他事务长
时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该
事务的执行速度。
第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务
回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避
免死锁、减少数据库因事务回滚带来的开销。
表锁和行锁应用场景:
表级锁使用与并发性不高,以查询为主,少量更新的应用,比如小型的
web应用;而行级锁适用于高并发环境下,对事务完整性要求较高的系
统,如在线事务处理系统。
10.数据库三范式和五大约束
(1)三范式
第一范式(1NF):当关系模式R的所有属性都不能在分解为更基本的数据单
位时,称R是满足第一范式的,简记为1NF。满足第一范
式是关系模式规范化的最低要求,否则,将有很多基本操
作在这样的关系模式中实现不了。通俗的说就是:
(a)数据表中的每一列(每个字段)必须是不可拆分的最
小单元,也就是确保每一列的原子性;
(b)两列的属性相近或相似或一样,尽量合并属性一样的
列,确保不产生冗余数据。
第二范式(2NF):如果关系模式R满足第一范式,并且R得所有非主属性都
完全依赖于R的每一个候选关键属性,称R满足第二范式,
简记为2NF。通俗的说是每一行的数据只能与其中一列
相关,即一行数据只做一件事。只要数据列中出现数据重
复,就要把表拆分开来。例如,一个人同时订几个房间,
就会出来一个订单号多条数据,这样子联系人都是重复的,
就会造成数据冗余。我们应该把他拆开来。
第三范式(3NF):设R是一个满足第一范式条件的关系模式,X是R的任意属
性集,如果X非传递依赖于R的任意一个候选关键字,称R
满足第三范式,简记为3NF。通俗的说就是“数据不能存在
传递关系,即每个属性都跟主键有直接关系而不是间接关
系”。例如,Student表(学号,姓名,年龄,性别,所在
院校,院校地址,院校电话)这样一个表结构,就存在上
述关系。 学号--> 所在院校 --> (院校地址,院校电话)这样
的表结构,我们应该拆开来,如下:(学号,姓名,年龄,
性别,所在院校)--(所在院校,院校地址,院校电话)
(2)五大约束
(a)主键约束(Primay Key Coustraint) 唯一性,非空性
添加主键约束(将stuNo作为主键)
alter table stuInfo
add constraint PK_stuNo primary key (stuNo)
(b)唯一约束 (Unique Counstraint)唯一性,可以空,但只能有一个
添加唯一约束(身份证号唯一,因为每个人的都不一样)
alter table stuInfo
add constraint UQ_stuID unique(stuID)
(c)检查约束 (Check Counstraint) 对该列数据的范围、格式的限制(如:年
龄、性别等)
添加检查约束 (对年龄加以限定 15-40岁之间)
alter table stuInfo
add constraint CK_stuAge check (stuAge between 15 and 40)
(d)默认约束 (Default Counstraint) 该数据的默认值
添加默认约束(如果地址不填 默认为“地址不详”)
alter table stuInfo
add constraint DF_stuAddress default (‘地址不详’) for stuAddress
(e)外键约束 (Foreign Key Counstraint) 需要建立两表间的关系并引用主表
的列
添加外键约束 (主表stuInfo和从表stuMarks建立关系,关联字段stuNo)
alter table stuInfo
add constraint FK_stuNo foreign key(stuNo)references stuinfo(stuNo)
二:数据库的分库分表
1.互联网行业传统架构中分库分表(水平分库分表)
(1)水平分库分表产生的业务场景
对于大型的互联网应用来说,数据库单表的记录行数可能达到千万
级甚至是亿级,并且数据库面临着极高的并发访问。采用Master-Slave
复制模式的MySQL架构,只能够对数据库的读进行扩展,而对数据库的
写入操作还是集中在Master上,并且单个Master挂载的Slave也不可能无
限制多,Slave的数量受到Master能力和负载的限制。
(2)分表
对于访问极为频繁且数据量巨大的单表来说,我们首先要做的就是
减少单表的记录条数,以便减少数据查询所需要的时间,提高数据库的
吞吐,这就是所谓的分表!
在分表之前,首先需要选择适当的分表策略,使得数据能够较为均
衡地分不到多张表中,并且不影响正常的查询!
对于互联网企业来说,大部分数据都是与用户关联的,因此,用户
id是最常用的分表字段。因为大部分查询都需要带上用户id,这样既不
影响查询,又能够使数据较为均衡地分布到各个表中(当然,有的场景
也可能会出现冷热数据分布不均衡的情况),如下图:
注:拆分后表的数量一般为2的n次方。
假设有一张表记录用户购买信息的订单表order,由于order表记
录条数太多,将被拆分成256张表。拆分的记录根据user_id%256取
得对应的表进行存储,前台应用则根据对应的user_id%256,找到对
应订单存储的表进行访问。这样一来,user_id便成为一个必需的查询
条件,否则将会由于无法定位数据存储的表而无法对数据进行访问。
这样一来,user_id便成为一个必需的查询条件,否则将会由于无
法定位数据存储的表而无法对数据进行访问。
举例说明user_id为必须查询的字段
假设表结构为:
create table order_(
order_id bigint(20) primary key auto_increment,
user_id bigint(20),
user_nick varchar(50),
auction_id bigint(20),
auction_title bigint(20),
price bigint(20),
auction_cat varchar(200),
seller_id bigint(20),
seller_nick varchar(50)
)
那么分表以后,假设user_id = 257,并且auction_id = 100,需要根
据auction_id来查询对应的订单信息,则对应的SQL语句如下:
select * from order_1 where user_id=257 and auction_id = 100;
其中,order_1是根据257%256计算得出,表示分表之后的第一
张order表
(3)分库
分表能够解决单表数据量过大带来的查询效率下降的问题,但是,
却无法给数据库的并发处理能力带来质的提升。面对高并发的读写访
问,当数据库master服务器无法承载写操作压力时,不管如何扩展
slave服务器,此时都没有意义了。因此,我们必须换一种思路,对数
据库进行拆分,从而提高数据库写入能力,这就是所谓的分库!
与分表策略相似,分库可以采用通过一个关键字取模的方式,来
对数据访问进行路由,如下图所示:
还是之前的订单表,假设user_id 字段的值为258,将原有的单库
分为256个库,那么应用程序对数据库的访问请求将被路由到第二个
库(258%256 = 2)。
(3)分库分表
(a)为什么要分库分表?
有时数据库可能既面临着高并发访问的压力,又需要面
对海量数据的存储问题,这时需要对数据库既采用分表策略,
又采用分库策略,以便同时扩展系统的发处理能力,以及提升
单表的查询性能,这就是所谓的分库分表。
(b)分库分表策略
注:下面案例中,库的编号是一个自然序列,且从0开始
中间变量=userid%(库的数量*每个库表的数量)
库=“中间变量/每个库的表的数量” 然后在取整 。
表=中间变量%库中的表数量
注意:上面公式的第一步,求的是所有表的自然序列;
第二步,如果库的下标是从0开始,向下取整,
如果库的编号从1开始像上取整。
假设将原来的单库单表order拆分成256个库,每个库包含1024
个表,那么按照前面所提到的路由策略,对于user_id=262145
的访问,路由的计算过程如下:
* 中间变量 = 262145 % (256 * 1024) = 1
* 库 = 取整 (1/1024) = 0
* 表 = 1 % 1024 = 1
结论,user_id=262145 的订单记录的查询和修改,将被路由到
第0个库的第1个order_1表中执行
(4)垂直分表
上面介绍的分表方式是水平的,下面介绍垂直分表。
(a)为什么要垂直分表?
如果一个表的字段非常多,有可能造成数据库的跨页存储,
这就导致了性能的下降,垂直分表正式为了解决这个问题产生
的。
(b)垂直分表
垂直分表就是将一张表中不常用的字段拆分到另一张表中,
从而保证第一章表中的字段较少,避免出现数据库跨页存储的
问题,从而提升查询效率。而另一张表中的数据通过外键与第
一张表进行关联,如下图所示:
2. 微服务架构中的分库(垂直分库)
垂直分库即是将一个完整的数据库根据业务功能拆分成多个独立的数据
库,这些数据库可以运行在不同的服务器上,从而提升数据库整体的数
据读写性能。这种方式在微服务架构中非常常用。微服务架构的核心思
想是将一个完整的应用按照业务功能拆分成多个可独立运行的子系统,
这些子系统称为“微服务”,各个服务之间通过RPC接口通信,这样的结
构使得系统耦合度更低、更易于扩展。垂直分库的理念与微服务的理念
不谋而合,可以将原本完整的数据按照微服务拆分系统的方式,拆分成
多个独立的数据库,使得每个微服务系统都有各自独立的数据库,从而
可以避免单个数据库节点压力过大,影响系统的整体性能,如下图所示
3. 微服务分库(垂直分库)跨库join的几种解决方案
(1)全局表
所谓全局表,就是有可能系统中所有模块都可能会依赖到的一些
表。比较类似我们理解的“数据字典”。为了避免跨库join查询,我们
可以将这类表在其他每个数据库中均保存一份。同时,这类数据通常
也很少发生修改(甚至几乎不会),所以也不用太担心“一致性”问题。
(2)字段冗余
这是一种典型的反范式设计,在互联网行业中比较常见,通常是
为了性能来避免join查询。
举个电商业务中很简单的场景:
“订单表”中保存“卖家Id”的同时,将卖家的“Name”字段也冗
余,这样查询订单详情的时候就不需要再去查询“卖家用户表”。
字段冗余能带来便利,是一种“空间换时间”的体现。但其适用场
景也比较有限,比较适合依赖字段较少的情况。最复杂的还是数
据一致性问题,这点很难保证,可以借助数据库中的触发器或者
在业务代码层面去保证。当然,也需要结合实际业务场景来看一
致性的要求。就像上面例子,如果卖家修改了Name之后,是否
需要在订单信息中同步更新呢?
(3)数据同步
定时A库中的tab_a表和B库中tbl_b有关联,可以定时将指定
的表做同步。当然,同步本来会对数据库带来一定的影响,需要
性能影响和数据时效性中取得一个平衡。这样来避免复杂的跨库
查询。笔者曾经在项目中是通过ETL工具来实施的。
(4)系统层组装
(原文出自博客:https://www.open-open.com/lib/view/open1473820694158.html#articleHeader4
的系统层组装章节中,如果博客被删了,百度“跨库 系统层组装”。
其实(1)、(2)、(3)、(4)点都来自这篇博客)
在系统层面,通过调用不同模块的组件或者服务,获取到数据并
进行字段拼装。说起来很容易,但实践起来可真没有这么简单,尤其
是数据库设计上存在问题但又无法轻易调整的时候。具体情况通常会
比较复杂。下面笔者结合以往实际经验,并通过伪代码方式来描述。
(a)简单的列表查询的情况
伪代码很容易理解,先获取“我的提问列表”数据,然后再根
据列表中的UserId去循环调用依赖的用户服务获取到用户的
RealName,拼装结果并返回。有经验的读者一眼就能看出上诉
伪代码存在效率问题。循环调用服务,可能会有循环RPC,循
环查询数据库…不推荐使用。再看看改进后的:
这种实现方式,看起来要优雅一点,其实就是把循环调用改成
一次调用。当然,用户服务的数据库查询中很可能是In查询,效率
方面比上一种方式更高。(坊间流传In查询会全表扫描,存在性能
问题,传闻不可全信。其实查询优化器都是基本成本估算的,经过
测试,在In语句中条件字段有索引的时候,条件较少的情况是会走
索引的。这里不细展开说明,感兴趣的朋友请自行测试)。
小结
简单字段组装的情况下,我们只需要先获取“主表”数据,
然后再根据关联关系,调用其他模块的组件或服务来获取
依赖的其他字段(如例中依赖的用户信息),最后将数据
进行组装。通常,我们都会通过缓存来避免频繁RPC通信
和数据库查询的开销。
(b)列表查询带条件过滤的情况
- 查出所有的问答数据,然后调用用户服务进行拼装数据,再根据过滤
字段state字段进行过滤,最后进行排序和分页并返回。这种方式能够
保证数据的准确性和完整性,但是性能影响非常大,不建议使用。
- 查询出state字段符合/不符合的UserId,在查询问答数据的时候使用
in/not in进行过滤,排序,分页等。过滤出有效的问答数据后,再调
用用户服务获取数据进行组装。
(c)跨库事务(分布式事务)的问题
按业务拆分数据库之后,不可避免的就是“分布式事务”的问题。以
往在代码中通过spring注解简单配置就能实现事务的,现在则需要花很
大的成本去保证一致性。这里不展开介绍,
1.自己比较懵懂的概念
(1)数据库分区
查看:https://blog.csdn.net/xhf852963/article/details/78896427
(2)数据库分片