index merge的数据结构和成本评估
前面以案例的形式介绍了什么是index merge,以及它的使用场景。本文将介绍index merge实现的主要数据结构以及MySQL如何评估index merge的成本。在开始本文之前,需要先理解Range访问相关的数据结构介绍:SEL_ARG结构,SEL_TREE结构。 1. 概述:index merge的
前面以案例的形式介绍了什么是index merge,以及它的使用场景。本文将介绍index merge实现的主要数据结构以及MySQL如何评估index merge的成本。在开始本文之前,需要先理解Range访问相关的数据结构介绍:SEL_ARG结构,SEL_TREE结构。
1. 概述:index merge的数据结构
index merge的主要数据结构仍然是存放在SEL_TREE中:
class SEL_TREE :public Sql_alloc { ... List<sel_imerge> merges; ... };</sel_imerge>
在merges这个list中存放了所有可能的index merge。本文将从几个案例,来看看SEL_TREE/SEL_IMERGE如何代表一个index merge访问方式。本文将不再重复介绍SEL_ARG/SEL_TREE的Range相关结构。
SEL_IMERGE的主要成员是一个SEL_TREE的链表,每一个SEL_TREE代表了一个独立的索引条件,这个链表中多个条件共同构成这个index merge。我们通过两个案例看看一个SEL_TREE如何表示一个index merge。(这里需要注意,SEL_TREE既可以代表一个RANGE条件,也可以代表一个index merge;代表Range时,其merges成员为空)。
2. 案例1:简单的index merge
SELECT * FROM tmp_sel_tree where ( key1_part1 = 1 or (key1_part2 = 2 and key2_part1 = 3) ) or ( key3_part1 = 5 )
这是一个多个索引的index merge,且没有任何的range可以使用。or条件的三个分支,分表可以使用一个独立的索引,其构成的SEL_TREE结构如下:
SEL_TREE | |-->List<sel_imerge> merges; | | / SEL_TREE-> SEL_ARG(key1_part1 = 1) \ SEL_IMERGE1 | SEL_TREE-> SEL_ARG(key2_part1 = 3) \ SEL_TREE-> SEL_ARG(key3_part1 = 5)</sel_imerge>
3. 案例2:单个查询多个index merge
SELECT * FROM tmp_sel_tree where ( key1_part1 = 1 or (key1_part2 = 2 and key2_part1 = 3) ) and ( key3_part1 = 5 or key2_part1 = 5)
这个案例中,And条件两边都可以各自使用index merge,MySQL可以选择其中任何一个执行。对应的SEL_TREE中,将会有多个SEL_IMERGE对象,每个SEL_IMERGE对象里面存储了多个独立的可以使用索引的条件(有独立的SEL_TREE表示):
SEL_TREE | \-->List<sel_imerge> merges; | | / SEL_TREE-> SEL_ARG(key1_part1 = 1) | SEL_IMERGE1 | SEL_TREE-> SEL_ARG(key2_part1 = 3) | \ SEL_TREE-> SEL_ARG(key3_part1 = 5) | | / SEL_TREE-> SEL_ARG(key2_part1 = 5) \ SEL_IMERGE2 | \ SEL_TREE-> SEL_ARG(key3_part1 = 5)</sel_imerge>
MySQL会在选择执行计划时,逐一评估每个SEL_IMERGE的成本,然后选择最优的执行计划。
4. 成本计算
MySQL在计算index merge的成本时,分开考虑了ROR和non-ROR的场景。所以这里先单独介绍一下什么是ROR,后面再介绍MySQL如何区别对待ROR的成本计算。
4.1 什么是Rowid-Ordered Retrieval
Rowid-Ordered Retrieval简称ROR。看下面的说明。有基于索引的查询:
"key_1=c_1 AND ... AND key_n=c_n"
该索引定义为:(key_1, ..., key_N [,a_1, ..., a_m]),且主键列为(a_1, ..., a_m, b1, ..., b_k),并且n >= N。
那么这个查询就是一个ROR查询。简单说明:对于该索引左前缀(key_1,...key_n)都是定值,对应该值的子树顺序是按照剩余索引列来排序的,而剩余的索引列又都是主键最左前缀,所以子树的顺序恰好同主键顺序相同。
(这一段可以参考函数is_key_scan_ror的注释和实现部分)
示例:
CREATE TABLE `tmp_index_merge` ( `id` int(11) NOT NULL, `key1_part1` int(11) NOT NULL, `key1_part2` int(11) NOT NULL, `key2_part1` int(11) NOT NULL, `key2_part2` int(11) NOT NULL, `key2_part3` int(11) NOT NULL, `key3_part1` int(11) NOT NULL DEFAULT '4', PRIMARY KEY (`id`), KEY `ind2` (`key2_part1`,`key2_part2`,`key2_part3`), KEY `ind1` (`key1_part1`,`key1_part2`,`id`), KEY `ind3` (`key3_part1`,`id`) ) ENGINE=InnoDB; explain select * from tmp_index_merge where (key1_part1 = 4333 and key1_part2 = 1657) or (key3_part1 = 2877); j+----+-------------+-----------------+-------------+---------------+-----------+---------+------+------+-------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-----------------+-------------+---------------+-----------+---------+------+------+-------------------------------------+ | 1 | SIMPLE | tmp_index_merge | index_merge | ind1,ind3 | ind1,ind3 | 8,4 | NULL | 2 | Using union(ind1,ind3); Using where | +----+-------------+-----------------+-------------+---------------+-----------+---------+------+------+-------------------------------------+ 这就是一个ROR的index查询。ROR在Explain的执行计划中并没有任何体现,通过在代码中设置断点可以观察到。在函数get_best_disjunct_quick中,代码会跳到标签skip_to_ror_scan处执行。
在对index merge的成本评估时,只有所有的SEL_TREE子树都是ROR的,对应的SEL_IMERGE才是ROR的。后面我们将看看ROR和non-ROR在成本评估上的不同。
4.2 成本概述
一个index merge是由多个SEL_TREE子树组成,每个SEL_TREE对应一个range操作(参考),所以每个SEL_TREE成本仍然会按照range操作类似各自计算成本,并累加。
各个SEL_TREE子树各自获取ROWIDs后,MySQL需要对这些ROWID进行去重,最后根据ROWID获取所有数据。去重操作其实是一个对多组ROWID归并排序的问题。对于ROR和non-ROR场景归并排序复杂度略有不同。对于non-ROR的场景,需要先进行分组排序,然后合并。而对于ROR,因为ROWID是顺序的,所以前面的分组排序就省略了,直接做合并操作,这让non-ROR和ROR在成本计算上有较大的不同。
在完成去重之后,最后是根据ROWID取出主键的成本(对应的二级索引里面取出的ROWID)。
一个细节:如果某个SEL_TREE对应的索引恰好是主键索引时,那么MySQL会在其他SEL_TREE子树扫描时,直接判断扫描出来的ROWID是否在主键对应的SEL_TREE的range内,如果这个ROWID已经存在,则不在记录。这样可以尽可能的减少归并排序的元素个数。我们称这部分成本味"二级ROWID过滤成本"。
4.3 SEL_TREE子树的成本
这部分成本计算与range成本计算相同(参考),这里会将多个子树成本单独计算并累加。
for (every SEL_TREE IN SEL_IMERGE){ cur_child= get_key_scans_params(param, *ptree, TRUE, FALSE, read_time); imerge_cost += (*cur_child)->read_cost; ...... }
4.4 non-ROR场景的成本计算
这里通过排序进行去重,是典型的归并排序,如果超过MySQL排序内存的限制,则是典型的外排序。先分组做红黑树排序,然后进行合并。成本分为几部分:创建红黑树、外排时磁盘读写、最后顺序读取排序结果。
4.4.1 去重复成本计算概述
这部分的成本可以完整的参考函数Unique::get_use_cost,这里做一个较为详细的补充说明。
对这个问题做一个简单的抽象:有两部分数据,第一部分有cpk_scan_records条,已排序。第二部分有non_cpk_scan_records未排序,现在需要返回去重后所有数据。单条数据大小为key_size,可用内存为max_in_memory_size。因为前面对第二部数据做了"二级ROWID过滤",所以这部分ROWID跟第一部分没有重复。因此,仅这里的第二部分数据需要进行去重。去重通过一个排序实现。
简单的说,需要对non_cpk_scan_records条记录进行外排序,最大可用内存是max_in_memory_size,单条记录大小是key_size。排序分成两部分,对部分数据做排序,然后合并。
4.4.2 二级ROWID过滤成本
如果有子树SEL_TREE是对应主键聚簇索引,另一部分子树SEL_TREE对应二级索引,那么在遍历二级索引时将取出对应的ROWID,看看是否再聚簇索引的SEL_TREE子树中,如果是,那么可以忽略这个ROWID,以免重复计算(减少后面Unique操作)。这部分的成本计算为:
imerge_cost += non_cpk_scan_records / TIME_FOR_COMPARE_ROWID;
另外,这里记cpk_scan_records为主键聚簇索引对应的SEL_TREE返回的ROWID数量,non_cpk_scan_records为二级索引对应的所有SEL_TREE返回的ROWID数量。
4.4.3 排序比较成本
需要进行N=non_cpk_scan_records*key_size/max_in_memory_size次排序。在每次排序过程中,如果已经排序好的记录树m个,那么新增一条记录平均需要做log2(m+1)次比较操作,m取值是从1,2...N。比较操作的成本为log2((m+1)!),MySQL使用了如下公式计算log2((m+1)!):
\[n! \approx \sqrt{2{\pi}n}(\frac{n}{e})^n\]
\[\log{n!} \approx \log{\sqrt{2{\pi}n}} + n*\log{\frac{n}{e}} \]
这里log是2为底数,再使用\[log_{n}{m} = \frac{\lg{n}}{\lg{m}}\] 通过此公式底数都可以转换为10进行运算(这一部并不是必须的,不过MySQL是这样计算的)。
阶乘转换参考:斯特靈公式(口味略重,慎入)。
对应的代码段:
result+= n_full_trees * log2_n_fact(max_elements_in_tree + 1.0); result /= TIME_FOR_COMPARE_ROWID;
4.4.4 外排序时候的磁盘IO成本
在外排的时候,需要对所有的数据进行一次IO操作,成本计算如下:
293 result += DISK_SEEK_BASE_COST * n_full_trees * max_elements_in_tree / IO_SIZE; 295 result += DISK_SEEK_BASE_COST * key_size * last_tree_elems / IO_SIZE;
第一行是完整树的IO成本,第二部分是最后一个可能不完整树的IO成本。
4.4.5 合并成本
最后是合并成本,这是一个典型的归并排序,是对K个有序列表进行归并,时间复杂度为:
\[O(N*\lg{K})\]
归并过程中有一次读写操作,IO和比较成本加起来就是合并的成本:
\[\frac{total\_buf\_elems*\log(n\_buffers)}{TIME\_FOR\_COMPARE\_ROWID*\log2} + 2*\frac{total\_buf\_elems*elem\_size}{IO\_SIZE} \]
total_buf_elems是总元素个数;n_buffers子树数量;elem_size为单个元素大小。
未尽的细节:MySQL一次最多对15(MERGEBUFF2)颗子树做归并。
4.4.6 最后的读取
这时,完成了所有的排序操作,最后是读取结果到内存的成本:
result += ceil((double)key_size*nkeys/IO_SIZE);
4.4.7 根据ROWID取出记录的成本
所有非聚簇索引扫描获得ROWID后,最后仍然需要根据这些ROWID获取记录。
对于索引组织表(聚簇索引,InnoDB),这部分的成本计算较为简单,假设聚簇索引的总page为total_pages,这里二级索引取出的rowid数量为rows,该表的总记录树为total_rows,那么成本为:
(rows / total_rows) *total_pages
代码参考:
imerge_cost += get_sweep_read_cost(param, non_cpk_scan_records);
4.5 ROR场景的成本计算
ROR的时候,去重时则少了对子队列的排序,直接是对多个已经排列好的队列做合并排序。所以这里的成本计算相对简单:索引读取,合并排序,最后是根据ROWID取出所有记录的成本。
4.5.1 索引读取成本
这部分计算与索引覆盖扫描计算相同。假设单个索引块大小为BS,索引字段长度味KL,ROWID长度为RL,总是假设索引块有50%为空,如果需要扫描的记录数为RS,那么这部分成本计算为:
\[\frac{RS}{\frac{1}{2}\frac{BS}{(KL+RL)}}\]
参考函数get_index_only_read_time的实现。
4.5.2 合并排序
这次合并排序,是对多个有序列表的合并。若有K个有序列表,总记录数味N,那么其成本为:
\[O(N*\lg{K})\]
这里N为各个SEL_TREE子树对应found_records之和(MySQL这里的计算略微不同)。
4.5.3 根据ROWID取出记录的成本
这部分成本于NON-ROR场景相同,对于索引组织表(聚簇索引,InnoDB),这部分的成本计算较为简单,假设聚簇索引的总page为total_pages,这里二级索引取出的rowid数量为rows,该表的总记录树为total_rows,那么成本为:
(rows / total_rows) *total_pages
在MySQL中,对于上面表达式的rows计算做了一些不一样的处理。这里说一下主要思想,MySQL假设每个SEL_TREE完全独立,总记录数味R,如果有三个SEL_TREE子树,记对应的记录数为R(1),R(2),R(3)。如果数据都均匀分布,那么去重后总记录数为:
(R(1)+R(2)+R(3)) - R(a)*(R(1)*R(2)+R(2)+R(3)+R(1)*R(3))/R(a)^2 + R(a)*((R(1)*R(2)*R3)/R(a)^3)
MySQL这里做了一个近似:
(R(1)+R(2)+R(3)) - R(a)*((R(1)*R(2)*R3)/R(a)^3)
MySQL利用这个近似值作为上面公式的rows。到这里ROR部分成本就完成了。
5 最后
最后,如果index merge的成本比其他执行计划的成本要更小的话,那么MySQL就会选择改执行计划。案例可以参考index merge介绍。
原文地址:index merge的数据结构和成本评估, 感谢原作者分享。

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Java中比較複雜資料結構時,使用Comparator提供靈活的比較機制。具體步驟包括:定義比較器類,重寫compare方法定義比較邏輯。建立比較器實例。使用Collections.sort方法,傳入集合和比較器實例。

資料結構與演算法是Java開發的基礎,本文深入探討Java中的關鍵資料結構(如陣列、鍊錶、樹等)和演算法(如排序、搜尋、圖演算法等)。這些結構透過實戰案例進行說明,包括使用陣列儲存分數、使用鍊錶管理購物清單、使用堆疊實現遞歸、使用佇列同步執行緒以及使用樹和雜湊表進行快速搜尋和身份驗證等。理解這些概念可以編寫高效且可維護的Java程式碼。

引用類型在Go語言中是一種特殊的資料類型,它們的值並非直接儲存資料本身,而是儲存資料的位址。在Go語言中,引用型別包括slices、maps、channels和指標。深入了解引用類型對於理解Go語言的記憶體管理和資料傳遞方式至關重要。本文將結合具體的程式碼範例,介紹Go語言中引用類型的特點和使用方法。 1.切片(Slices)切片是Go語言中最常用的引用類型之一

AVL樹是一種平衡二元搜尋樹,確保快速且有效率的資料操作。為了實現平衡,它執行左旋和右旋操作,調整違反平衡的子樹。 AVL樹利用高度平衡,確保樹的高度相對於節點數始終較小,從而實現對數時間複雜度(O(logn))的查找操作,即使在大型資料集上也能保持資料結構的效率。

Java集合框架概述Java集合框架是Java程式語言的重要組成部分,它提供了一系列可以儲存和管理資料的容器類別庫。這些容器類別庫具有不同的資料結構,可以滿足不同場景下的資料儲存和處理需求。集合框架的優點在於它提供了統一的接口,使得開發人員可以使用相同的方式來操作不同的容器類別庫,從而降低了開發難度。 Java集合框架的資料結構Java集合框架中包含多種資料結構,每種資料結構都有其獨特的特性和適用場景。以下是幾種常見的Java集合框架資料結構:1.List:List是一個有序的集合,它允許元素重複。 Li

利用雜湊表可最佳化PHP數組交集和並集計算,將時間複雜度從O(n*m)降低到O(n+m),具體步驟如下:使用雜湊表將第一個數組的元素映射到布林值,以快速找出第二個陣列中元素是否存在,提高交集計算效率。使用雜湊表將第一個陣列的元素標記為存在,然後逐一新增第二個陣列的元素,忽略已存在的元素,提高並集計算效率。

PHPSPL資料結構庫概述PHPSPL(標準php庫)資料結構庫包含一組類別和接口,用於儲存和操作各種資料結構。這些資料結構包括數組、鍊錶、堆疊、佇列和集合,每個資料結構都提供了一組特定的方法和屬性,用於操縱資料。數組在PHP中,數組是儲存一系列元素的有序集合。 SPL數組類別提供了對原生的PHP數組進行加強的功能,包括排序、過濾和映射。以下是使用SPL陣列類別的範例:useSplArrayObject;$array=newArrayObject(["foo","bar","baz"]);$array

文字分類是自然語言處理(NLP)任務之一,它旨在將文字歸類到預先定義的類別中。文字分類有許多實際應用,例如電子郵件過濾、垃圾郵件偵測、情緒分析和問答系統等。使用pythonNLTK庫完成文字分類的任務可以分為以下幾個步驟:資料預處理:首先,需要對資料進行預處理,包括移除標點符號、轉換成小寫、移除空格等。特徵提取:接下來,需要從預處理後的文字中提取特徵。特徵可以是字詞、詞組或句子。模型訓練:然後,需要使用擷取的特徵來訓練一個分類模型。通常使用的分類模型包括樸素貝葉斯、支援向量機和決策樹等。評估:最後
