MySQL的MyISAM、InnoDB引擎默认均使用B+树索引(查询时都显示为“BTREE”),本文讨论两个问题:html
索引结构的选择基于这样一个性质:大数据量时,索引没法所有装入内存。node
为何索引没法所有装入内存?假设使用树结构组织索引,简单估算一下:mysql
假设索引存储在内存中。也就是说,每在物理盘上保存2G的数据,就要占用200MB的内存,索引:数据的占用比
约为1/10。1/10的占用比算不算大呢?物理盘比内存廉价的多,以一台内存16G硬盘1T的服务器为例,若是要存满1T的硬盘,至少须要100G的内存,远大于16G。git
考虑到一个表上可能有多个索引、联合索引、数据行占用更小等状况,实际的占用比一般大于1/10,某些时候能达到1/3。在基于索引的存储架构中,索引:数据的占用比
太高,所以,索引没法所有装入内存。github
因为没法装入内存,则必然依赖磁盘(或SSD)存储。而内存的读写速度是磁盘的成千上万倍(与具体实现有关),所以,核心问题是“如何减小磁盘读写次数”。算法
首先不考虑页表机制,假设每次读、写都直接穿透到磁盘,那么:sql
BST、AVL、RBT很好的将读写次数从O(n)优化到O(log2(n));其中,AVL和RBT都比BST多了自平衡的功能,将读写次数降到最大O(log2(n))。数据库
假设使用自增主键,则主键自己是有序的,树结构的读写次数可以优化到树高,树高越低读写次数越少;自平衡保证了树结构的稳定。若是想进一步优化,能够引入B树和B+树。缓存
不少文章将B树误称为B-(减)树,这多是对其英文名“B-Tree”的误解(更有甚者,将B树称为二叉树或二叉搜索树)。特别是与B+树一块儿讲的时候。想固然的认为有B+(加)树就有B-(减)树,实际上B+树的英文名是“B+-Tree”。服务器
若是抛开维护操做,那么B树就像一棵“m叉搜索树”(m是子树的最大个数),时间复杂度为O(logm(n))。然而,B树设计了一种高效简单的维护操做,使B树的深度维持在约log(ceil(m/2))(n)~logm(n)之间,大大下降树高。
再次强调:
不要纠结于时间复杂度,与单纯的算法不一样,磁盘IO次数才是更大的影响因素。读者能够推导看看,B树与AVL的时间复杂度是相同的,但因为B树的层数少,磁盘IO次数少,实践中B树的性能要优于AVL等二叉树。
同二叉搜索树相似,每一个节点存储了多个key和子树,子树与key按顺序排列。
页表的目的是扩展内存+加速磁盘读写。一个页(Page)一般4K(等于磁盘数据块block的大小,见inode与block的分析),从磁盘读写的角度出发,操做系统每次以页为单位将内容从磁盘加载到内存(以摊分寻道成本),修改页后,再择期将该页写回磁盘。考虑到页表的良好性质,可使每一个节点的大小约等于一个页(使m很是大),这每次加载的一个页就能完整覆盖一个节点,以便选择下一层子树;对子树同理。对于页表来讲,AVL(或RBT)至关于1个key+2个子树的B树,因为逻辑上相邻的节点,物理上一般不相邻,所以,读入一个4k页,页面内绝大部分空间都将是无效数据。
假设key、子树节点指针均占用4B,则B树节点最大m * (4 + 4) = 8m B
;页面大小4KB。则m = 4 * 1024 / 8 = 512
,一个512叉的B树,1000w的数据,深度最大 log(512/2)(10^7) = 3.02 ~= 4
。对比二叉树如AVL的深度为log(2)(10^7) = 23.25 ~= 24
,相差了5倍以上。震惊!B树索引深度居然如此!
另外,B树对局部性原理很是友好。若是key比较小(好比上面4B的自增key),则除了页表的加成,缓存还能进一步预读加速。美滋滋~
然而,若是要实际应用到数据库的索引中,B树还有一些问题:
数据表的记录有多个字段,仅仅定位到主键是不够的,还须要定位到数据行。有3个方案解决:
方案1中,数据行一般很是大,存储数据行将减小页面中的子树个数,m减少树高增大。假设数据行占用200B,可忽略组织B树的指针,则新的m = 4 * 1024 / 200 = 20.48 ~= 21
,深度最大 log(21/2)(10^7) ~= 7
。增长了一倍以上的IO,不考虑。
方案2中,节点增长了一个字段。假设是4B的指针,则新的m = 4 * 1024 / 12 = 341.33 ~= 341
,深度最大 log(341/2)(10^7) = 3.14 ~= 4
。与3差异不大,能够考虑。
方案3的节点m与深度不变,但时间复杂度变为稳定的O(logm(n))。考虑。
实际业务中,范围查询的频率很是高,B树只能定位到一个索引位置(可能对应多行),很难处理范围查询。给出2种方案:
乍一看感受方案1比方案2好——时间复杂度和常数项都同样,方案1还不须要改动。可是别忘了局部性原理,无论节点中存储的是数据行仍是数据行位置,方案2的好处在于,叶子节点连续存储,对页表和缓存友好。而方案1则面临节点逻辑相邻、物理分离的缺点。
综上,问题1的方案2与问题2的方案1可整合为一种方案(基于B树的索引),问题1的方案3与问题2的方案2可整合为一种(基于B+树的索引)。实际上,数据库、文件系统有些采用了B树,有些采用B+树。
因为某些猴子暂未明白的缘由,包括MySQL在内的主流数据库多选择了B+树。即:
主要变更如上所述:
B树的增删过程暂时可参考从B树、B+树、B*树谈到R 树的“六、B树的插入、删除操做”小节,B+树的增删同理。此处暂不赘述。
根据B+树的性质,很容易理解各类常见的MySQL索引优化思路。
暂不考虑不一样引擎之间的区别。
前面的分析中,假设用4B的自增key做为索引,则m可达到512,层高仅有3。使用自增的key有两个好处:
m = 4 * 1024 / 54m = 75.85 ~= 76
,深度最大 log(76/2)(10^7) = 4.43 ~= 5
,再加上cache缺失、字符串比较的成本,时间成本增长较大。同时,key由4B增加到50B,整棵索引树的空间占用增加也是极为恐怖的(若是二级索引使用主键定位数据行,则空间增加更加严重)。优化经历:
猴子曾使用varchar(100)的列作过主键,存储containerId,过了三、4天100G的数据库就满了,DBA小姐姐邮件里委婉表示了对个人鄙视。。。以后增长了自增列做为主键,containerId做为unique的二级索引,时间、空间优化效果至关显著。
索引能够简单如一个列(a),也能够复杂如多个列(a, b, c, d),即联合索引
。若是是联合索引,那么key也由多个列组成,同时,索引只能用于查找key是否存在(相等),遇到范围查询(>、<、between、like左匹配)等就不能进一步匹配了,后续退化为线性查找。所以,列的排列顺序决定了可命中索引的列数。
若有索引(a, b, c, d),查询条件a = 1 and b = 2 and c > 3 and d = 4
,则会在每一个节点依次命中a、b、c,没法命中d。也就是最左前缀匹配原则。
不须要考虑=、in等的顺序,mysql会自动优化这些条件的顺序,以匹配尽量多的索引列。
若有索引(a, b, c, d),查询条件c > 3 and b = 2 and a = 1 and d < 4
与a = 1 and c > 3 and b = 2 and d < 4
等顺序都是能够的,MySQL会自动优化为a = 1 and b = 2 and c > 3 and d < 4
,依次命中a、b、c。
有索引列参与计算的查询条件对索引不友好(甚至没法使用索引),如from_unixtime(create_time) = '2014-05-29'
。
缘由很简单,如何在节点中查找到对应key?若是线性扫描,则每次都须要从新计算,成本过高;若是二分查找,则须要针对from_unixtime方法肯定大小关系。
所以,索引列不能参与计算。上述from_unixtime(create_time) = '2014-05-29'
语句应该写成create_time = unix_timestamp('2014-05-29')
。
若是已有索引(a),想创建索引(a, b),尽可能选择修改索引(a)为索引(a, b)。
新建索引的成本很容易理解。而基于索引(a)修改成索引(a, b)的话,MySQL能够直接在索引a的B+树上,通过分裂、合并等修改成索引(a, b)。
若是已有索引(a, b),则不须要再创建索引(a),可是若是有必要,则仍然需考虑创建索引(b)。
很容易理解。如,用性别做索引,那么索引仅能将1000w行数据划分为两部分(如500w男,500w女),索引几乎无效。
区分度
的公式是count(distinct <col>) / count(*)
,表示字段不重复的比例,比例越大区分度越好。惟一键的区分度是1,而一些状态、性别字段可能在大数据面前的区分度趋近于0。
这个值很难肯定,通常须要join的字段要求是0.1以上,即平均1条扫描10条记录。
参考:
本文连接:浅谈MySQL的B树索引与索引优化
做者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,可是必须保留本文的署名及连接。