SQL技巧:一步步分析爲什麼B+樹適合作爲索引的結構 以及索引原理

mysql的B+樹索引 查找使用了二分查找,redis 跳錶也使用了二分查找法,kafka查詢消息日誌也使用了二分查找法,二分查找法時間複雜度O(logn);

參考:redis的索引底層的 跳錶原理 實現 聊聊Mysql索引和redis跳錶 ---redis的跳錶原理 時間複雜度O(logn)(阿里)

參考:kafka如何實現高併發存儲-如何找到一條需要消費的數據(阿里)

參考:二分查找法:各種排序算法的時間複雜度和空間複雜度(阿里)

在MySQL中,主要有四種類型的索引,分別爲:B-Tree索引,Hash索引,Fulltext索引(MyISAM 表)和R-Tree索引,本文講的是B-Tree索引。

後面的索引原理一定要看,太重要了,阿里兩個人都問這個mysql的索引原理

mysql使用了 B+索引:

B樹:有序數組+平衡多叉樹; 
B+樹:有序數組鏈表+平衡多叉樹;

一、Mysql索引主要有兩種結構:B+Tree索引和Hash索引 

(a) Inodb存儲引擎 默認是 B+Tree索引

(b) MyISAM 存儲引擎 默認是Fulltext索引;

(c)Memory 存儲引擎 默認 Hash索引;

Hash索引

mysql中,只有Memory(Memory表只存在內存中,斷電會消失,適用於臨時表)存儲引擎顯示支持Hash索引,是Memory表的默認索引類型,儘管Memory表也可以使用B+Tree索引。Hash索引把數據以hash形式組織起來,因此當查找某一條記錄的時候,速度非常快。但是因爲hash結構,每個鍵只對應一個值,而且是散列的方式分佈。所以它並不支持範圍查找和排序等功能。

 

B+Tree索引

B+Tree是mysql使用最頻繁的一個索引數據結構,是Inodb和Myisam存儲引擎模式的索引類型。相對Hash索引,B+Tree在查找單條記錄的速度比不上Hash索引,但是因爲更適合排序等操作,所以它更受歡迎。畢竟不可能只對數據庫進行單條記錄的操作。

帶順序訪問指針的B+Tree

B+Tree所有索引數據都在葉子節點上,並且增加了順序訪問指針,每個葉子節點都有指向相鄰葉子節點的指針。

這樣做是爲了提高區間效率,例如查詢key爲從18到49的所有數據記錄,當找到18後,只要順着節點和指針順序遍歷就可以以此向訪問到所有數據節點,極大提高了區間查詢效率。

大大減少磁盤I/O讀取

數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設爲等於一個頁,這樣每個節點需要一次I/O就可以完全載入。 

 

什麼是索引

索引(Index)是幫助數據庫高效獲取數據的數據結構。索引是在基於數據庫表創建的,它包含一個表中某些列的值以及記錄對應的地址,並且把這些值存儲在一個數據結構中。最常見的就是使用哈希表、B+樹作爲索引。

一般的應用系統,讀寫比例在10:1左右,而且插入操作和一般的更新操作很少出現性能問題,在生產環境中,我們遇到最多的,也是最容易出問題的,還是一些複雜的查詢操作,因此對查詢語句的優化顯然是重中之重。說起加速查詢,就不得不提到索引了。

 

爲什麼要使用索引

我們知道,數據庫查詢是數據庫最主要的功能之一。而查詢速度當然是越快越好。而當數據量越來越大的時候,查詢花費的時間會隨之增長。而索引,可以加速數據的查詢。因爲索引是有序排列的。

舉個例子來說,假設我們有一個數據庫表Employee,這個表分別有三個字段:name,age,address。假設表中有1000條記錄。

假如沒有使用索引,當我們查詢名爲“Jesus”的僱員的時候,即調用:

select name,age,address from Employee where name = 'Jesus';

此時數據庫不得不在Employee表中對這1000條記錄一條一條的進行判斷name字段是否爲“Jesus”。這也就是所謂的全表掃描。

而當我們在Employee表上的name字段上創建索引時,當我們查詢名爲“Jesus”的僱員時,會通過索引查找去查詢名爲“Jesus”的僱員,因爲該索引已經按照字母順序排列,因此要查找名爲“Jesus”的記錄時會快很多,因爲名字首字母爲“J”的僱員都是排列在一起的。通過該索引,能獲取到表中對應的記錄。

舉例說明使用索引的好處

假設索引(索引是一種數據結構)是鏈表結構。每個節點存儲的是關鍵字字段(這個例子中對應的是name屬性)以及該關鍵字字段在數據庫表的對應的記錄的地址。而這些節點是根據name屬性排序的(即根據字母順序排序)。因此,當我們執行上面說的查找名爲“Jesus”的sql語句時,數據庫會通過該索引來查詢,因爲該鏈表是有序排列的,在我們找到第一個name屬性爲“Jesus”的節點後,繼續往後找,當遇到name屬性不爲“Jesus”的節點時,就無需再往後查找了,因爲節點是根據name屬性有序排列的啊。假設第一個name=“Jesus”的節點是第499個節點,最後一個name=“Jesus”的節點是第500個節點,那麼只需要遍歷501個節點就可以了。當發現第501個節點的name字段不爲“Jesus”,後面的499個節點也就無需遍歷了。通過索引,我們就找到了name爲“Jesus”的節點,而通過該節點的另一個屬性(關鍵字字段在數據庫表的對應的記錄的地址),我們就能獲取到Employee表中滿足條件name=“Jesus”的記錄了。

通過使用索引,查詢判斷的次數就從1000次縮小到了501次了。起到了加速了查詢效率。但實際上數據庫中索引的結構,並不是鏈表結構。

數據庫中使用什麼數據結構作爲索引

數據庫中實際使用的索引並不會是鏈表結構,因爲效率太低了。 
我們知道鏈表的查詢效率是O(n)。就像上面的例子,遍歷了501次才找到第一條符合條件的記錄,這是很低效的。而我們知道,數組+二分查找的效率是O(lgn),但是數組的插入元素以及刪除元素的效率很低,因此使用數組做爲索引結構並不合適。

另外,在選擇數據庫索引的結構的時候,要考慮到另一個問題。索引是存在於磁盤中,當索引非常大的時候,達到幾個G的時候,無法一次加載到內存中。

考慮到上面兩個因素,數據庫中索引使用的是樹形結構。

各種樹的名字

有這麼幾種樹:

B-Tree
B+-Tree
B*-Tree

首先要明白三種樹名中的“-”起到的是分隔的作用,並不是“減”的意思。 

因此正確的翻譯應該是B樹,B+樹,B*樹。而不是B-樹,B+樹,B*樹。因此,當你聽到別人說“B減樹”的時候,要明白它指的是B-Tree。即B樹和B-樹是同一種樹。

爲什麼要強調上面這一點呢,因爲有的博文中寫的是:B樹是二叉樹,B-樹是多路搜索樹。

然而B樹和B-樹都是指B-Tree。引用維基百科上的話:

B-tree 
Not to be confused with Binary tree.

 

也就是說,B-Tree並不是Binart tree。B-Tree的中文名是平衡多路搜索樹。 
(B樹的相關介紹在下面)

平衡二叉樹

樹形結構是計算機系統裏最重要的數據結構。

我們知道,二叉樹的查找的時間複雜度是O(log2N),其查找效率與深度有關,而普通的二叉樹可能由於內部節點排列問題退化成鏈表,這樣查找效率就會很低。因此平衡二叉樹是更好的選擇,因爲它保持平衡,即通過旋轉調整結構保持最小的深度。其查找的時間複雜度也是O(log2N)。

但實際上,數據庫中索引的結構也並非AVL樹或更優秀的紅黑樹,儘管它的查詢的時間複雜度很低。

爲什麼平衡二叉樹也不適合作爲索引

之前說了平衡樹的查找時間複雜度是O(log2N),已經很不錯了,但還是不適合作爲索引結構。那麼肯定是有一種更適合作爲索引的數據結構。那麼這個更適合作爲索引的數據結構,難道是查找的時間複雜度更低嗎?並不是。這種作爲索引的數據結構的查找的時間複雜度也近似O(log2N)。

那爲什麼平衡二叉樹不適合作爲索引呢?

索引是存在於索引文件中,是存在於磁盤中的。因爲索引通常是很大的,因此無法一次將全部索引加載到內存當中,因此每次只能從磁盤中讀取一個磁盤頁的數據到內存中。而這個磁盤的讀取的速度較內存中的讀取速度而言是差了好幾個級別。

注意,我們說的平衡二叉樹結構,指的是邏輯結構上的平衡二叉樹,其物理實現是數組。然後由於在邏輯結構上相近的節點在物理結構上可能會差很遠。因此,每次讀取的磁盤頁的數據中有許多是用不上的。因此,查找過程中要進行許多次的磁盤讀取操作。

而適合作爲索引的結構應該是儘可能少的執行磁盤IO操作,因爲執行磁盤IO操作非常的耗時。因此,平衡二叉樹並不適合作爲索引結構。

B-Tree適合作爲索引

平衡二叉樹不適合作爲索引。那麼什麼才適合作爲索引——B樹。

平衡二叉樹沒能充分利用磁盤預讀功能,而B樹是爲了充分利用磁盤預讀功能來而創建的一種數據結構,也就是說B樹就是爲了作爲索引才被髮明出來的的。

來看看關於“局部性原理與磁盤預讀”的知識: 

局部性原理與磁盤預讀:

由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分分之一,因此爲了提高效率,要儘量減少磁盤I/O。爲了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向後讀取一定長度的數據放入內存。這樣做的理論依據是計算機科學中著名的局部性原理: 
當一個數據被用到時,其附近的數據也通常會馬上被使用。 
程序運行期間所需要的數據通常比較集中。 
由於磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有局部性的程序來說,預讀可以提高I/O效率。

搞清楚上面的意思。磁盤預讀是具體實現,其理論依據是局部性原理。

爲什麼說紅黑樹沒能充分利用磁盤預讀功能,引用一篇博文的一段話: 

紅黑樹這種結構,h明顯要深的多。由於邏輯上很近的節點(父子)物理上可能很遠,無法利用局部性,所以紅黑樹的I/O漸進複雜度也爲O(h),效率明顯比B-Tree差很多。

 

也就是說,使用紅黑樹(平衡二叉樹)結構的話,每次磁盤預讀中的很多數據是用不上的數據。因此,它沒能利用好磁盤預讀的提供的數據。然後又由於深度大(較B樹而言),所以進行的磁盤IO操作更多。

B樹的每個節點可以存儲多個關鍵字,它將節點大小設置爲磁盤頁的大小,充分利用了磁盤預讀的功能。每次讀取磁盤頁時就會讀取一整個節點。也正因每個節點存儲着非常多個關鍵字,樹的深度就會非常的小。進而要執行的磁盤讀取操作次數就會非常少,更多的是在內存中對讀取進來的數據進行查找。

B樹的查詢,主要發生在內存中,而平衡二叉樹的查詢,則是發生在磁盤讀取中。因此,雖然B樹查詢查詢的次數不比平衡二叉樹的次數少,但是相比起磁盤IO速度,內存中比較的耗時就可以忽略不計了。因此,B樹更適合作爲索引。

比B樹更適合作爲索引的結構——B+樹

比B樹更適合作爲索引的結構是B+樹。MySQL中也是使用B+樹作爲索引。它是B樹的變種,因此是基於B樹來改進的。爲什麼B+樹會比B樹更加優秀呢?

B樹:有序數組+平衡多叉樹; 
B+樹:有序數組鏈表+平衡多叉樹;

B+樹的關鍵字全部存放在葉子節點中,非葉子節點用來做索引,而葉子節點中有一個指針指向一下個葉子節點。做這個優化的目的是爲了提高區間訪問的性能。而正是這個特性決定了B+樹更適合用來存儲外部數據。

引用一段話: 

 
走進搜索引擎的作者梁斌老師針對B樹、B+樹給出了他的意見(爲了真實性,特引用其原話,未作任何改動): “B+樹還有一個最大的好處,方便掃庫,B樹必須用中序遍歷的方法按序掃庫,而B+樹直接從葉子結點挨個掃一遍就完了,B+樹支持range-query非常方便,而B樹不支持。這是數據庫選用B+樹的最主要原因。 
比如要查 5-10之間的,B+樹一把到5這個標記,再一把到10,然後串起來就行了,B樹就非常麻煩。B樹的好處,就是成功查詢特別有利,因爲樹的高度總體要比B+樹矮。不成功的情況下,B樹也比B+樹稍稍佔一點點便宜。 
B樹比如你的例子中查,17的話,一把就得到結果了, 
有很多基於頻率的搜索是選用B樹,越頻繁query的結點越往根上走,前提是需要對query做統計,而且要對key做一些變化。 
另外B樹也好B+樹也好,根或者上面幾層因爲被反覆query,所以這幾塊基本都在內存中,不會出現讀磁盤IO,一般已啓動的時候,就會主動換入內存。”

舉個例子來對比。 
B樹: 
比如說,我們要查找關鍵字範圍在3到7的關鍵字,在找到第一個符合條件的數字3後,訪問完第一個關鍵字所在的塊後,得遍歷這個B樹,獲取下一個塊,直到遇到一個不符合條件的關鍵字。遍歷的過程是比較複雜的。

B+樹(葉節點保存數據,其他的節點 全部存放索引): 

相比之下,B+樹的基於範圍的查詢簡潔很多。由於葉子節點有指向下一個葉子節點的指針,因此從塊1到塊2的訪問,通過塊1指向塊2的指針即可。從塊2到塊3也是通過一個指針即可。

引用一篇博文中網友評論的一段話: 

數據庫索引採用B+樹的主要原因是B樹在提高了磁盤IO性能的同時並沒有解決元素遍歷的效率低下的問題。正是爲了解決這個問題,B+樹應運而生。
B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷。而且在數據庫中基於範圍的查詢是非常頻繁的,而B樹不支持這樣的操作(或者說效率太低)。

正如上面所說,在數據庫中基於範圍的查詢是非常頻繁的,因此MySQL最終選擇的索引結構是B+樹而不是B樹。 

 

二、索引的原理

一 索引原理

索引的目的在於提高查詢效率,與我們查閱圖書所用的目錄是一個道理:先定位到章,然後定位到該章下的一個小節,然後找到頁數。相似的例子還有:查字典,查火車車次,飛機航班等

本質都是:通過不斷地縮小想要獲取數據的範圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是說,有了這種索引機制,我們可以總是用同一種查找方式來鎖定數據。

數據庫也是一樣,但顯然要複雜的多,因爲不僅面臨着等值查詢,還有範圍查詢(>、<、between、in)、模糊查詢(like)、並集查詢(or)等等。數據庫應該選擇怎麼樣的方式來應對所有的問題呢?我們回想字典的例子,能不能把數據分成段,然後分段查詢呢?最簡單的如果1000條數據,1到100分成第一段,101到200分成第二段,201到300分成第三段......這樣查第250條數據,只要找第三段就可以了,一下子去除了90%的無效數據。但如果是1千萬的記錄呢,分成幾段比較好?稍有算法基礎的同學會想到搜索樹,其平均複雜度是lgN,具有不錯的查詢性能。但這裏我們忽略了一個關鍵的問題,複雜度模型是基於每次相同的操作成本來考慮的。而數據庫實現比較複雜,一方面數據是保存在磁盤上的,另外一方面爲了提高性能,每次又可以把部分數據讀入內存來計算,因爲我們知道訪問磁盤的成本大概是訪問內存的十萬倍左右,所以簡單的搜索樹難以滿足複雜的應用場景。

 二 磁盤IO與預讀

考慮到磁盤IO是非常高昂的操作,計算機操作系統做了一些優化,當一次IO時,不光把當前磁盤地址的數據,而是把相鄰的數據也都讀取到內存緩衝區內,因爲局部預讀性原理告訴我們,當計算機訪問一個地址的數據的時候,與其相鄰的數據也會很快被訪問到。每一次IO讀取的數據我們稱之爲一頁(page)。具體一頁有多大數據跟操作系統有關,一般爲4k或8k,也就是我們讀取一頁內的數據時候,實際上才發生了一次IO,這個理論對於索引的數據結構設計非常有幫助。

三、索引的數據結構

任何一種數據結構都不是憑空產生的,一定會有它的背景和使用場景,我們現在總結一下,我們需要這種數據結構能夠做些什麼,其實很簡單,那就是:每次查找數據時把磁盤IO次數控制在一個很小的數量級,最好是常數數量級。那麼我們就想到如果一個高度可控的多路搜索樹是否能滿足需求呢?就這樣,b+樹應運而生。

如上圖,是一顆b+樹,關於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並不真實存在於數據表中。

###b+樹的查找過程
如圖所示,如果要查找數據項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,顯然成本非常非常高。

###b+樹性質
1.索引字段要儘量的小:通過上面的分析,我們知道IO次數取決於b+數的高度h,假設當前數據表的數據爲N,每個磁盤塊的數據項的數量是m,則有h=㏒(m+1)N,當數據量N一定的情況下,m越大,h越小;而m = 磁盤塊的大小 / 數據項的大小,磁盤塊的大小也就是一個數據頁的大小,是固定的,如果數據項佔的空間越小,數據項的數量越多,樹的高度越低。這就是爲什麼每個數據項,即索引字段要儘量的小,比如int佔4字節,要比bigint8字節少一半。這也是爲什麼b+樹要求把真實的數據放到葉子節點而不是內層節點,一旦放到內層節點,磁盤塊的數據項會大幅度下降,導致樹增高。當數據項等於1時將會退化成線性表。
2.索引的最左匹配特性(即從左往右匹配):當b+樹的數據項是複合的數據結構,比如(name,age,sex)的時候,b+數是按照從左到右的順序來建立搜索樹的,比如當(張三,20,F)這樣的數據來檢索的時候,b+樹會優先比較name來確定下一步的所搜方向,如果name相同再依次比較age和sex,最後得到檢索的數據;但當(20,F)這樣的沒有name的數據來的時候,b+樹就不知道下一步該查哪個節點,因爲建立搜索樹的時候name就是第一個比較因子,必須要先根據name來搜索才能知道下一步去哪裏查詢。比如當(張三,F)這樣的數據來檢索時,b+樹可以用name來指定搜索方向,但下一個字段age的缺失,所以只能把名字等於張三的數據都找到,然後再匹配性別是F的數據了, 這個是非常重要的性質,即索引的最左匹配特性。

這也是經常考察的,比如 我定義了 A,B,C的聯合索引,如果 我只傳遞了 A,B 能走索引嗎?答案是能,因爲最左側原理(百度問過) 

補充一下. 全文索引(FULLTEXT)=mysql的 myISAM搜索引擎默認的索引類型

      MySQL從3.23.23版開始支持全文索引和全文檢索,fulltext索引僅可用於 MyISAM 表;他們可以從CHAR、VARCHAR或TEXT列中作爲CREATE TABLE語句的一部分被創建,或是隨後使用ALTER TABLE 或CREATE INDEX被添加。////對於較大的數據集,將你的資料輸入一個沒有FULLTEXT索引的表中,然後創建索引,其速度比把資料輸入現有FULLTEXT索引的速度更爲快。不過切記對於大容量的數據表,生成全文索引是一個非常消耗時間非常消耗硬盤空間的做法。
       文本字段上的普通索引只能加快對出現在字段內容最前面的字符串(也就是字段內容開頭的字符)進行檢索操作。如果字段裏存放的是由幾個、甚至是多個單詞構成的較大段文字,普通索引就沒什麼作用了。這種檢索往往以LIKE %word%的形式出現,這對MySQL來說很複雜,如果需要處理的數據量很大,響應時間就會很長。 
  這類場合正是全文索引(full-text index)可以大顯身手的地方。在生成這種類型的索引時,MySQL將把在文本中出現的所有單詞創建爲一份清單,查詢操作將根據這份清單去檢索有關的數據記錄。全文索引即可以隨數據表一同創建,也可以等日後有必要時再使用下面這條命令添加: 
  ALTER TABLE table_name ADD FULLTEXT(column1, column2) 
  有了全文索引,就可以用SELECT查詢命令去檢索那些包含着一個或多個給定單詞的數據記錄了。下面是這類查詢命令的基本語法: 
  SELECT * FROM table_name 
  WHERE MATCH(column1, column2) AGAINST('word1', 'word2', 'word3') 
  上面這條命令將把column1和column2字段裏有word1、word2和word3的數據記錄全部查詢出來。 

參考:Mysql索引詳解及優化(key和index區別) 

四,索引使用注意事項

1,不要濫用索引

①,索引提高查詢速度,卻會降低更新表的速度,因爲更新表時,mysql不僅要更新數據,保存數據,還要更新索引,保存索引

②,索引會佔用磁盤空間 

2,索引不會包含含有NULL值的列

複合索引只要有一列含有NULL值,那麼這一列對於此符合索引就是無效的,因此我們在設計數據庫設計時不要讓字段的默認值爲NULL。 

3,MySQL查詢只是用一個索引

如果where字句中使用了索引的話,那麼order by中的列是不會使用索引的 

4,like

like '%aaa%'不會使用索引而like "aaa%"可以使用索引


二、選擇索引的數據類型

Mysql支持很多數據類型,選擇合適的數據類型存儲數據對性能有很大的影響。

(1)越小的數據類型通常更好:越小的數據類型通常在磁盤、內存和cpu緩存中都需要更少的空間,處理起來更快。

(2)簡單的數據類型更好:整形數據比起字符,處理開銷更小,因爲字符串的比較更復雜。在MySQL中,應用內置的日期和時間數據類型,而不是字符串來存儲時間;以及用整形數據存儲IP地址。

(3)儘量避免NULL:應該制定列爲NOT NULL,除非你想存儲NULL。在MySQL中,含有空值的列很難進行查詢優化,因爲他們使得索引、索引的統計信息以及比較運算更加複雜。

三、MySQL常見索引有:主鍵索引、唯一索引、普通索引、全文索引、組合索引

1,INDEX(普通索引):ALTER TABLE 'table_name' ADD INDEX index_name('col')

最基本的索引,沒有任何限制 

2,UNIQUE(唯一索引):ALTER TABLE 'table_name' ADD UNIQUE('col')

與“普通索引”類似,不同的就是:索引列的值必須唯一,但允許有空值。 

3,PRIMARY KEY(主鍵索引):ALTER TABLE 'table_name' ADD PRIMARY KEY('col')

是一種特殊的唯一索引,不允許有空值。 

4,FULLTEXT(全文索引):ALTER TABLE 'table_name' ADD FULLTEXT('col')

僅可用於MyISAM和InoDB,針對較大的數據,生成全文索引很耗時耗空間

組合索引:ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3')

爲了更多的提高mysql效率可建立組合索引,遵循“最左前綴”原則。創建複合索引應該將最常用(頻率)做限制條件的列放在最左邊,一次遞減。組合索引最左字段用in是可以用到索引的。相當於建立了col1,col1col2,col1col2col3三個索引

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章