一文讀懂MySQL的索引結(jié)構(gòu)及查詢優(yōu)化

(同時再次強調(diào)俏让,這幾篇關(guān)于MySQL的探究都是基于5.7版本酵幕,相關(guān)總結(jié)與結(jié)論不一定適用于其他版本)

MySQL官方文檔中(https://dev.mysql.com/doc/refman/5.7/en/optimization-indexes.html)有這樣一段描述:

The best way to improve the performance of SELECT operations is to create indexes on one or more of the columns that are tested in the query. But unnecessary indexes waste space and waste time for MySQL to determine which indexes to use. Indexes also add to the cost of inserts, updates, and deletes because each index must be updated. You must find the right balance to achieve fast queries using the optimal set of indexes.

就是說提高查詢性能最直接有效的方法就是建立索引映琳,但是不必要的索引會浪費空間呐馆,同時也增加了額外的時間成本去判斷應(yīng)該走哪個索引掩驱,此外浅萧,索引還會增加插入沉填、更新醇锚、刪除數(shù)據(jù)的成本,因為做這些操作的同時還要去維護(更新)索引樹吩案。因此霍殴,應(yīng)該學(xué)會使用最佳索引集來優(yōu)化查詢媒惕。

什么是索引#

在MySQL中,索引(Index)是幫助高效獲取數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)来庭。這種數(shù)據(jù)結(jié)構(gòu)MySQL中最常用的就是B+樹(B+Tree)妒蔚。

Indexes are used to find rows with specific column values quickly. Without an index, MySQL must begin with the first row and then read through the entire table to find the relevant rows.

就好比給你一本書和一篇文章標(biāo)題,如果沒有目錄月弛,讓你找此標(biāo)題對應(yīng)的文章面睛,可能需要從第一頁翻到最后一頁;如果有目錄大綱尊搬,你可能只需要在目錄頁尋找此標(biāo)題,然后迅速定位文章土涝。

這里我們可以把書(book)看成是MySQL中的table佛寿,把文章(article)看成是table中的一行記錄,即row但壮,文章標(biāo)題(title)看成row中的一列column冀泻,目錄自然就是對title列建立的索引index了,這樣根據(jù)文章標(biāo)題從書中檢索文章就對應(yīng)sql語句select * from book where title = ?蜡饵,相應(yīng)的弹渔,書中每增加一篇文章(即insert into book (title, ...) values ('華山論劍', ...)),都需要維護一下目錄溯祸,這樣才能從目錄中找到新增的文章華山論劍肢专,這一操作對應(yīng)的是MySQL中每插入(insert)一條記錄需要維護title列的索引樹(B+Tree)。

為什么使用B+Tree#

首先需要澄清的一點是焦辅,MySQL跟B+樹沒有直接的關(guān)系博杖,真正與B+樹有關(guān)系的是MySQL的默認(rèn)存儲引擎InnoDB,MySQL中存儲引擎的主要作用是負(fù)責(zé)數(shù)據(jù)的存儲和提取筷登,除了InnoDB之外剃根,MySQL中也支持比如MyISAM等其他存儲引擎(詳情見https://dev.mysql.com/doc/refman/5.7/en/storage-engine-setting.html)作為表的底層存儲引擎。

Copy

mysql> show engines;+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+| Engine? ? ? ? ? ? | Support | Comment? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | Transactions | XA? | Savepoints |+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+| MRG_MYISAM? ? ? ? | YES? ? | Collection of identical MyISAM tables? ? ? ? ? ? ? ? ? ? ? ? ? | NO? ? ? ? ? | NO? | NO? ? ? ? || CSV? ? ? ? ? ? ? ? | YES? ? | CSV storage engine? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | NO? ? ? ? ? | NO? | NO? ? ? ? || PERFORMANCE_SCHEMA | YES? ? | Performance Schema? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | NO? ? ? ? ? | NO? | NO? ? ? ? || BLACKHOLE? ? ? ? ? | YES? ? | /dev/null storage engine (anything you write to it disappears) | NO? ? ? ? ? | NO? | NO? ? ? ? || InnoDB? ? ? ? ? ? | DEFAULT | Supports transactions, row-level locking, and foreign keys? ? | YES? ? ? ? ? | YES? | YES? ? ? ? || MyISAM? ? ? ? ? ? | YES? ? | MyISAM storage engine? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | NO? ? ? ? ? | NO? | NO? ? ? ? || ARCHIVE? ? ? ? ? ? | YES? ? | Archive storage engine? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | NO? ? ? ? ? | NO? | NO? ? ? ? || MEMORY? ? ? ? ? ? | YES? ? | Hash based, storedinmemory, usefulfortemporary tables? ? ? | NO? ? ? ? ? | NO? | NO? ? ? ? || FEDERATED? ? ? ? ? | NO? ? ? | Federated MySQL storage engine? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | NULL? ? ? ? | NULL | NULL? ? ? |+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+

提到索引前方,我們可能會立馬想到下面幾種數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)狈醉。

(1) 哈希表

哈希雖然能夠提供O(1)的單數(shù)據(jù)行的查詢性能廉油,但是對于范圍查詢和排序卻無法很好支持,需全表掃描苗傅。

(2) 紅黑樹

紅黑樹(Red Black Tree)是一種自平衡二叉查找樹抒线,在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能金吗。

一般來說十兢,索引本身也很大,往往不可能全部存儲在內(nèi)存中摇庙,因此索引往往以索引文件的形式存儲的磁盤上旱物。這樣的話,索引查找過程中就要產(chǎn)生磁盤I/O消耗卫袒,相對于內(nèi)存存取宵呛,I/O存取的消耗遠(yuǎn)遠(yuǎn)高于內(nèi)存,所以評價一個數(shù)據(jù)結(jié)構(gòu)作為索引的優(yōu)劣最重要的指標(biāo)就是查找過程中磁盤I/O次數(shù)夕凝。換句話說宝穗,索引的結(jié)構(gòu)組織要盡量減少查找過程中磁盤I/O的次數(shù)。

在這里码秉,磁盤I/O的次數(shù)取決于樹的高度逮矛,所以,在數(shù)據(jù)量較大時转砖,紅黑樹會因樹的高度較大而造成磁盤IO較多须鼎,從而影響查詢效率。

(3) B-Tree

B樹中的B代表平衡(Balance)府蔗,而不是二叉(Binary)晋控,B樹是從平衡二叉樹演化而來的。

為了降低樹的高度(也就是減少磁盤I/O次數(shù))姓赤,把原來瘦高的樹結(jié)構(gòu)變得矮胖赡译,B樹會在每個節(jié)點存儲多個元素(紅黑樹每個節(jié)點只會存儲一個元素),并且節(jié)點中的元素從左到右遞增排列不铆。如下圖所示:

B-Tree在查詢的時候比較次數(shù)其實不比二叉查找樹少蝌焚,但在內(nèi)存中的大小比較、二分查找的耗時相比磁盤IO耗時幾乎可以忽略誓斥。?B-Tree大大降低了樹的高度综看,所以也就極大地提升了查找性能。

(4) B+Tree

B+Tree是在B-Tree基礎(chǔ)上進一步優(yōu)化岖食,使其更適合實現(xiàn)存儲索引結(jié)構(gòu)红碑。InnoDB存儲引擎就是用B+Tree實現(xiàn)其索引結(jié)構(gòu)。

B-Tree結(jié)構(gòu)圖中可以看到每個節(jié)點中不僅包含數(shù)據(jù)的key值,還有data值析珊。而每一個節(jié)點的存儲空間是有限的羡鸥,如果data值較大時將會導(dǎo)致每個節(jié)點能存儲的key的數(shù)量很小,這樣會導(dǎo)致B-Tree的高度變大忠寻,增加了查詢時的磁盤I/O次數(shù)惧浴,進而影響查詢性能。在B+Tree中奕剃,所有data值都是按照鍵值大小順序存放在同一層的葉子節(jié)點上衷旅,而非葉子節(jié)點上只存儲key值信息,這樣可以增大每個非葉子節(jié)點存儲的key值數(shù)量纵朋,降低B+Tree的高度柿顶,提高效率。

這里補充一點相關(guān)知識?在計算機中操软,磁盤往往不是嚴(yán)格按需讀取嘁锯,而是每次都會預(yù)讀,即使只需要一個字節(jié)聂薪,磁盤也會從這個位置開始家乘,順序向后讀取一定長度的數(shù)據(jù)放入內(nèi)存。這樣做的理論依據(jù)是計算機科學(xué)中著名的局部性原理:

當(dāng)一個數(shù)據(jù)被用到時藏澳,其附近的數(shù)據(jù)也通常會馬上被使用仁锯。

由于磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉(zhuǎn)時間)翔悠,因此對于具有局部性的程序來說扑馁,預(yù)讀可以提高I/O效率。預(yù)讀的長度一般為頁(page)的整數(shù)倍凉驻。

頁是計算機管理存儲器的邏輯塊,硬件及操作系統(tǒng)往往將主存和磁盤存儲區(qū)分割為連續(xù)的大小相等的塊复罐,每個存儲塊稱為一頁(許多操作系統(tǒng)的頁默認(rèn)大小為4KB)涝登,主存和磁盤以頁為單位交換數(shù)據(jù)。當(dāng)程序要讀取的數(shù)據(jù)不在主存中時效诅,會觸發(fā)一個缺頁異常胀滚,此時操作系統(tǒng)會向磁盤發(fā)出讀盤信號,磁盤會找到數(shù)據(jù)的起始位置并向后連續(xù)讀取一頁或幾頁載入內(nèi)存中乱投,然后異常返回咽笼,程序繼續(xù)運行。(如下命令可以查看操作系統(tǒng)的默認(rèn)頁大小)

Copy

$ getconf PAGE_SIZE

4096

數(shù)據(jù)庫系統(tǒng)的設(shè)計者巧妙利用了磁盤預(yù)讀原理戚炫,將一個節(jié)點的大小設(shè)為操作系統(tǒng)的頁大小的整數(shù)倍剑刑,這樣每個節(jié)點只需要一次I/O就可以完全載入。

InnoDB存儲引擎中也有頁(Page)的概念,頁是其磁盤管理的最小單位施掏。InnoDB存儲引擎中默認(rèn)每個頁的大小為16KB钮惠。

Copy

mysql> show variables like'innodb_page_size';+------------------+-------+| Variable_name? ? | Value |+------------------+-------+| innodb_page_size | 16384 |+------------------+-------+1 rowinset(0.01 sec)

一般表的主鍵類型為INT(占4個字節(jié))或BIGINT(占8個字節(jié)),指針類型也一般為4或8個字節(jié)七芭,也就是說一個頁(B+Tree中的一個節(jié)點)中大概存儲16KB/(8B+8B)=1K個鍵值(因為是估值素挽,為方便計算,這里的K取值為10^3)狸驳。也就是說一個深度為3的B+Tree索引可以維護10^3 * 10^3 * 10^3 = 10億條記錄预明。

B+Tree的高度一般都在2到4層。mysql的InnoDB存儲引擎在設(shè)計時是將根節(jié)點常駐內(nèi)存的耙箍,也就是說查找某一鍵值的行記錄時最多只需要1到3次磁盤I/O操作撰糠。

隨機I/O對于MySQL的查詢性能影響會非常大,而順序讀取磁盤中的數(shù)據(jù)會很快究西,由此我們也應(yīng)該盡量減少隨機I/O的次數(shù)窗慎,這樣才能提高性能。在B-Tree中由于所有的節(jié)點都可能包含目標(biāo)數(shù)據(jù)卤材,我們總是要從根節(jié)點向下遍歷子樹查找滿足條件的數(shù)據(jù)行遮斥,這會帶來大量的隨機I/O,而B+Tree所有的數(shù)據(jù)行都存儲在葉子節(jié)點中扇丛,而這些葉子節(jié)點通過雙向鏈表依次按順序連接术吗,當(dāng)我們在B+樹遍歷數(shù)據(jù)(比如說范圍查詢)時可以直接在多個葉子節(jié)點之間進行跳轉(zhuǎn),保證順序帆精、倒序遍歷的性能较屿。

另外,對以上提到的數(shù)據(jù)結(jié)構(gòu)不熟悉的朋友卓练,這里推薦一個在線數(shù)據(jù)結(jié)構(gòu)可視化演示工具隘蝎,有助于快速理解這些數(shù)據(jù)結(jié)構(gòu)的機制:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

主鍵索引#

上面也有提及,在MySQL中襟企,索引屬于存儲引擎級別的概念嘱么。不同存儲引擎對索引的實現(xiàn)方式是不同的,這里主要看下MyISAM和InnoDB兩種存儲引擎的索引實現(xiàn)方式顽悼。

MyISAM索引實現(xiàn)#

MyISAM引擎使用B+Tree作為索引結(jié)構(gòu)時葉子節(jié)點的data域存放的是數(shù)據(jù)記錄的地址曼振。如下圖所示:

由上圖可以看出:MyISAM索引文件和數(shù)據(jù)文件是分離的,索引文件僅保存數(shù)據(jù)記錄的地址蔚龙,因此MyISAM的索引方式也叫做非聚集的冰评,之所以這么稱呼是為了與InnoDB的聚集索引區(qū)分。

InnoDB索引實現(xiàn)#

InnoDB的主鍵索引也使用B+Tree作為索引結(jié)構(gòu)時的實現(xiàn)方式卻與MyISAM截然不同木羹。InnoDB的數(shù)據(jù)文件本身就是索引文件甲雅。在InnoDB中,表數(shù)據(jù)文件本身就是按B+Tree組織的一個索引結(jié)構(gòu),這棵樹的葉子節(jié)點data域保存了完整的數(shù)據(jù)記錄务荆,這個索引的key是數(shù)據(jù)表的主鍵妆距,因此InnoDB表數(shù)據(jù)文件本身就是主索引。

InnoDB存儲引擎中的主鍵索引(primary key)又叫做聚集索引(clustered index)函匕。因為InnoDB的數(shù)據(jù)文件本身要按主鍵聚集娱据,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定盅惜,則MySQL系統(tǒng)會自動選擇一個可以唯一標(biāo)識數(shù)據(jù)記錄的列作為主鍵中剩,如果不存在這種列,則MySQL自動為InnoDB表生成一個隱含字段作為主鍵抒寂,這個字段長度為6個字節(jié)结啼,類型為長整形。(詳情見官方文檔:https://dev.mysql.com/doc/refman/5.7/en/innodb-index-types.html)

聚集索引這種實現(xiàn)方式使得按主鍵搜索十分高效屈芜,直接能查出整行數(shù)據(jù)郊愧。

在InnoDB中,用非單調(diào)遞增的字段作為主鍵不是個好主意井佑,因為InnoDB數(shù)據(jù)文件本身是一棵B+Tree属铁,非單增的主鍵會造成在插入新記錄時數(shù)據(jù)文件為了維持B+Tree的特性而頻繁的分裂調(diào)整,十分低效躬翁,因而使用遞增字段作為主鍵則是一個很好的選擇焦蘑。

非主鍵索引#

MyISAM索引實現(xiàn)#

MyISAM中,主鍵索引和非主鍵索引(Secondary key盒发,也有人叫做輔助索引)在結(jié)構(gòu)上沒有任何區(qū)別例嘱,只是主鍵索引要求key是唯一的,而輔助索引的key可以重復(fù)宁舰。這里不再多加敘述拼卵。

InnoDB索引實現(xiàn)#

InnoDB的非主鍵索引data域存儲相應(yīng)記錄主鍵的值。換句話說蛮艰,InnoDB的所有非主鍵索引都引用主鍵的值作為data域腋腮。如下圖所示:

由上圖可知:使用非主鍵索引搜索時需要檢索兩遍索引,首先檢索非主鍵索引獲得主鍵(primary key)印荔,然后用主鍵到主鍵索引樹中檢索獲得完整記錄。

那么為什么非主鍵索引結(jié)構(gòu)葉子節(jié)點存儲的是主鍵值详羡,而不像主鍵索引那樣直接存儲完整的一行數(shù)據(jù)仍律,這樣就能避免回表二次檢索?顯然实柠,這樣做一方面節(jié)省了大量的存儲空間水泉,另一方面多份冗余數(shù)據(jù),更新數(shù)據(jù)的效率肯定低下,另外保證數(shù)據(jù)的一致性是個麻煩事草则。

到了這里钢拧,也很容易明白為什么不建議使用過長的字段作為主鍵,因為所有的非主鍵索引都引用主鍵值炕横,過長的主鍵值會讓非主鍵索引變得過大源内。

聯(lián)合索引#

官方文檔:https://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html

比如INDEX idx_book_id_hero_name (book_id, hero_name) USING BTREE,即對book_id, hero_name兩列建立了一個聯(lián)合索引份殿。

A multiple-column index can be considered a sorted array, the rows of which contain values that are created by concatenating the values of the indexed columns.

聯(lián)合索引是多列按照次序一列一列比較大小膜钓,拿idx_book_id_hero_name這個聯(lián)合索引來說,先比較book_id卿嘲,book_id小的排在左邊颂斜,book_id大的排在右邊,book_id相同時再比較hero_name拾枣。如下圖所示:

了解了聯(lián)合索引的結(jié)構(gòu)沃疮,就能引入最左前綴法則:

If the table has a multiple-column index, any leftmost prefix of the index can be used by the optimizer to look up rows. For example, if you have a three-column index on (col1, col2, col3), you have indexed search capabilities on (col1), (col1, col2), and (col1, col2, col3).

就是說聯(lián)合索引中的多列是按照列的次序排列的,如果查詢的時候不能滿足列的次序梅肤,比如說where條件中缺少col1 = ?司蔬,直接就是col2 = ? and col3 = ?,那么就走不了聯(lián)合索引凭语,從上面聯(lián)合索引的結(jié)構(gòu)圖應(yīng)該能明顯看出葱她,只有col2列無法通過索引樹檢索符合條件的數(shù)據(jù)。

根據(jù)最左前綴法則似扔,我們知道對INDEX idx_book_id_hero_name (book_id, hero_name)來說吨些,where book_id = ? and hero_name = ?的查詢來說,肯定可以走索引炒辉,但是如果是where hero_name = ? and book_id = ?呢豪墅,表面上看起來不符合最左前綴法則啊,但MySQL優(yōu)化器會根據(jù)已有的索引黔寇,調(diào)整查詢條件中這兩列的順序偶器,讓它符合最左前綴法則,走索引缝裤,這里也就回答了上篇《一文學(xué)會MySQL的explain工具》中為什么用show warnings命令查看時屏轰,where中的兩個過濾條件hero_name、book_id先后順序被調(diào)換了憋飞。

至于對聯(lián)合索引中的列進行范圍查詢等各種情況霎苗,都可以先想聯(lián)合索引的結(jié)構(gòu)是如何創(chuàng)建出來的,然后看過濾條件是否滿足最左前綴法則榛做。比如說范圍查詢時唁盏,范圍列可以用到索引(必須是最左前綴)内狸,但是范圍列后面的列無法用到索引。同時厘擂,索引最多用于一個范圍列昆淡,因此如果查詢條件中有兩個范圍列則無法全用到索引。

優(yōu)化建議#

主鍵的選擇#

在使用InnoDB存儲引擎時刽严,如果沒有特別的需要昂灵,盡量使用一個與業(yè)務(wù)無關(guān)的遞增字段作為主鍵,主鍵字段不宜過長港庄。原因上面在講索引結(jié)構(gòu)時已提過倔既。比如說常用雪花算法生成64bit大小的整數(shù)(占8個字節(jié),用BIGINT類型)作為主鍵就是一個不錯的選擇鹏氧。

索引的選擇#

(1) 表記錄比較少的時候渤涌,比如說只有幾百條記錄的表,對一些列建立索引的意義可能并不大把还,所以表記錄不大時酌情考慮索引实蓬。但是業(yè)務(wù)上具有唯一特性的字段,即使是多個字段的組合吊履,也建議使用唯一索引(UNIQUE KEY)安皱。

(2) 當(dāng)索引的選擇性非常低時,索引的意義可能也不大艇炎。所謂索引的選擇性(Selectivity)酌伊,是指不重復(fù)的索引值(也叫基數(shù)Cardinality)與表記錄數(shù)的比值,即count(distinct 列名)/count(*)缀踪,常見的場景就是有一列status標(biāo)識數(shù)據(jù)行的狀態(tài)居砖,可能status非0即1,總數(shù)據(jù)100萬行有50萬行status為0驴娃,50萬行status為1奏候,那么是否有必要對這一列單獨建立索引呢?

An index is best used when you need to select a small number of rows in comparison to the total rows.

這句話我摘自stackoverflow上《MySQL: low selectivity columns = how to index?》下面一個人的回答唇敞。(詳情見:https://stackoverflow.com/questions/2386852/mysql-low-cardinality-selectivity-columns-how-to-index)

對于上面說的status非0即1蔗草,而且這兩種情況分布比較均勻的情況,索引可能并沒有實際意義疆柔,實際查詢時咒精,MySQL優(yōu)化器在計算全表掃描和索引樹掃描代價后,可能會放棄走索引旷档,因為先從status索引樹中遍歷出來主鍵值模叙,再去主鍵索引樹中查最終數(shù)據(jù),代價可能比全表掃描還高彬犯。

但是如果對于status為1的數(shù)據(jù)只有1萬行向楼,其他99萬行數(shù)據(jù)status為0的情況呢,你怎么看谐区?歡迎有興趣的朋友在文章下面留言討論湖蜕!

補充: 關(guān)于MySQL如何選擇走不走索引或者選擇走哪個最佳索引,可以使用MySQL自帶的trace工具一探究竟宋列。具體使用見下面的官方文檔未檩。

https://dev.mysql.com/doc/internals/en/optimizer-tracing.html

https://dev.mysql.com/doc/refman/5.7/en/information-schema-optimizer-trace-table.html

使用方法:

Copy

mysql>setsession optimizer_trace="enabled=on",end_markers_in_json=on;mysql> select * from tb_herowherehero_id = 1;mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE;

注意:開啟trace工具會影響MySQL性能冀宴,所以只能臨時分析sql使用,用完之后應(yīng)當(dāng)立即關(guān)閉

Copy

mysql>setsession optimizer_trace="enabled=off";

(3) 在varchar類型字段上建立索引時,建議指定索引長度术吝,有些時候可能沒必要對全字段建立索引,根據(jù)實際文本區(qū)分度決定索引長度即可【說明:索引的長度與區(qū)分度是一對矛盾體灸撰,一般對字符串類型數(shù)據(jù)伞插,長度為20的索引,區(qū)分度會高達90%以上艇纺,可以使用count(distinct left(列名, 索引長度))/count(*)來確定區(qū)分度】怎静。

這種指定索引長度的索引叫做前綴索引(詳情見https://dev.mysql.com/doc/refman/5.7/en/column-indexes.html#column-indexes-prefix)。

With col_name(N) syntax in an index specification for a string column, you can create an index that uses only the first N characters of the column. Indexing only a prefix of column values in this way can make the index file much smaller. When you index a BLOB or TEXT column, you must specify a prefix length for the index.

前綴索引語法如下:

Copy

mysql> alter table tb_hero add index idx_hero_name_skill2 (hero_name, skill(2));

前綴索引兼顧索引大小和查詢速度黔衡,但是其缺點是不能用于group by和order by操作蚓聘,也不能用于covering index(即當(dāng)索引本身包含查詢所需全部數(shù)據(jù)時,不再訪問數(shù)據(jù)文件本身)盟劫。

(4) 當(dāng)查詢語句的where條件或group by夜牡、order by含多列時,可根據(jù)實際情況優(yōu)先考慮聯(lián)合索引(multiple-column index)侣签,這樣可以減少單列索引(single-column index)的個數(shù)塘装,有助于高效查詢。

If you specify the columns in the right order in the index definition, a single composite index can speed up several kinds of queries on the same table.

建立聯(lián)合索引時要特別注意column的次序硝岗,應(yīng)結(jié)合上面提到的最左前綴法則以及實際的過濾氢哮、分組、排序需求型檀。區(qū)分度最高的建議放最左邊冗尤。

說明:

order by的字段可以作為聯(lián)合索引的一部分,并且放在最后胀溺,避免出現(xiàn)file_sort的情況裂七,影響查詢性能。正例:where a=? and b=? order by c會走索引idx_a_b_c仓坞,但是WHERE a>10 order by b卻無法完全使用上索引idx_a_b背零,只會使用上聯(lián)合索引的第一列a

存在非等號和等號混合時,在建聯(lián)合索引時无埃,應(yīng)該把等號條件的列前置徙瓶。如:where c>? and d=?那么即使c的區(qū)分度更高毛雇,也應(yīng)該把d放在索引的最前列,即索引idx_d_c

如果where a=? and b=?侦镇,如果a列的幾乎接近于唯一值灵疮,那么只需要建立單列索引idx_a即可

order by與group by#

盡量在索引列上完成分組、排序壳繁,遵循索引最左前綴法則震捣,如果order by的條件不在索引列上,就會產(chǎn)生Using filesort闹炉,降低查詢性能蒿赢。

分頁查詢#

MySQL分頁查詢大多數(shù)寫法可能如下:

Copy

mysql> select * from tb_herolimitoffset,N;

MySQL并不是跳過offset行,而是取offset+N行渣触,然后返回放棄前offset行羡棵,返回N行,那當(dāng)offset特別大的時候嗅钻,效率就非常的低下晾腔。

可以對超過特定閾值的頁數(shù)進行SQL改寫如下:

先快速定位需要獲取的id段,然后再關(guān)聯(lián)

Copy

mysql> select a.* from tb_hero a, (select hero_id from tb_herowhere條件limit100000,20 ) bwherea.hero_id = b.hero_id;

或者這種寫法

Copy

mysql> select a.* from tb_hero a inner join (select hero_id from tb_herowhere條件limit100000,20) b on a.hero_id = b.hero_id;

多表join#

(1) 需要join的字段啊犬,數(shù)據(jù)類型必須絕對一致灼擂;

(2) 多表join時,保證被關(guān)聯(lián)的字段有索引

覆蓋索引#

利用覆蓋索引(covering index)來進行查詢操作觉至,避免回表剔应,從而增加磁盤I/O。換句話說就是语御,盡可能避免select *語句峻贮,只選擇必要的列,去除無用的列应闯。

An index that includes all the columns retrieved by a query. Instead of using the index values as pointers to find the full table rows, the query returns values from the index structure, saving disk I/O. InnoDB can apply this optimization technique to more indexes than MyISAM can, because InnoDB secondary indexes also include the primary key columns. InnoDB cannot apply this technique for queries against tables modified by a transaction, until that transaction ends.

Any column index or composite index could act as a covering index, given the right query. Design your indexes and queries to take advantage of this optimization technique wherever possible.

當(dāng)索引本身包含查詢所需全部列時纤控,無需回表查詢完整的行記錄。對于InnoDB來說碉纺,非主鍵索引中包含了所有的索引列以及主鍵值船万,查詢的時候盡量用這種特性避免回表操作,數(shù)據(jù)量很大時骨田,查詢性能提升很明顯耿导。

in和exsits#

原則:小表驅(qū)動大表,即小的數(shù)據(jù)集驅(qū)動大的數(shù)據(jù)集

(1) 當(dāng)A表的數(shù)據(jù)集大于B表的數(shù)據(jù)集時态贤,in優(yōu)于exists

Copy

mysql> select * from Awhereidin(select id from B)

(2) 當(dāng)A表的數(shù)據(jù)集小于B表的數(shù)據(jù)集時舱呻,exists優(yōu)于in

Copy

mysql> select * from Awhereexists (select 1 from BwhereB.id = A.id)

like#

索引文件具有B+Tree最左前綴匹配特性,如果左邊的值未確定悠汽,那么無法使用索引箱吕,所以應(yīng)盡量避免左模糊(即%xxx)或者全模糊(即%xxx%)芥驳。

Copy

mysql> select * from tb_herowherehero_name like'%無%';+---------+-----------+--------------+---------+| hero_id | hero_name | skill? ? ? ? | book_id |+---------+-----------+--------------+---------+|? ? ? 3 | 張無忌? ? | 九陽神功? ? |? ? ? 3 ||? ? ? 5 | 花無缺? ? | 移花接玉? ? |? ? ? 5 |+---------+-----------+--------------+---------+2 rowsinset(0.00 sec)mysql> explain select * from tb_herowherehero_name like'%無%';+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+| id | select_type | table? | partitions |type| possible_keys | key? | key_len | ref? | rows | filtered | Extra? ? ? |+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+|? 1 | SIMPLE? ? ? | tb_hero | NULL? ? ? | ALL? | NULL? ? ? ? ? | NULL | NULL? ? | NULL |? ? 6 |? ? 16.67 | Usingwhere|+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+1 rowinset, 1 warning (0.00 sec)

可以看出全模糊查詢時全表掃了,這個時候使用覆蓋索引的特性茬高,只選擇索引字段可以有所優(yōu)化晚树。如下:

Copy

mysql> explain select book_id, hero_name from tb_herowherehero_name like'%無%';+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+| id | select_type | table? | partitions |type| possible_keys | key? ? ? ? ? ? ? ? ? | key_len | ref? | rows | filtered | Extra? ? ? ? ? ? ? ? ? ? |+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+|? 1 | SIMPLE? ? ? | tb_hero | NULL? ? ? | index | NULL? ? ? ? ? | idx_book_id_hero_name | 136? ? | NULL |? ? 6 |? ? 16.67 | Usingwhere; Using index |+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+1 rowinset, 1 warning (0.00 sec)

count(*)#

阿里巴巴Java開發(fā)手冊中有這樣的規(guī)約:

不要使用count(列名)或count(常量)來替代count(*),count(*)是SQL92定義的標(biāo)準(zhǔn)統(tǒng)計行數(shù)的語法雅采,跟數(shù)據(jù)庫無關(guān),跟NULL和非NULL無關(guān)【說明:count(*)會統(tǒng)計值為NULL的行慨亲,而count(列名)不會統(tǒng)計此列為NULL值的行】婚瓜。

count(distinct col)計算該列除NULL之外的不重復(fù)行數(shù),注意count(distinct col1, col2)如果其中一列全為NULL刑棵,那么即使另一列有不同的值巴刻,也返回為0

截取一段官方文檔對count的描述(具體見:https://dev.mysql.com/doc/refman/5.7/en/aggregate-functions.html#function_count)

COUNT(expr): Returns a count of the number of non-NULL values of expr in the rows.The result is a BIGINT value.If there are no matching rows, COUNT(expr) returns 0.

COUNT(*) is somewhat different in that it returns a count of the number of rows, whether or not they contain NULL values.

Prior to MySQL 5.7.18, InnoDB processes SELECT?COUNT(*)?statements by scanning the clustered index. As of MySQL 5.7.18, InnoDB processes SELECT COUNT(*) statements by traversing the smallest available secondary index unless an index or optimizer hint directs the optimizer to use a different index. If a secondary index is not present, the clustered index is scanned.

可見5.7.18之前,MySQL處理count(*)會掃描主鍵索引蛉签,5.7.18之后從非主鍵索引中選擇較小的合適的索引掃描胡陪。可以用explain看下執(zhí)行計劃碍舍。

Copy

mysql> select version();+-----------+| version() |+-----------+| 5.7.18? ? |+-----------+1 rowinset(0.00 sec)mysql> explain select count(*) from tb_hero;+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+| id | select_type | table? | partitions |type| possible_keys | key? ? ? | key_len | ref? | rows | filtered | Extra? ? ? |+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+|? 1 | SIMPLE? ? ? | tb_hero | NULL? ? ? | index | NULL? ? ? ? ? | idx_skill | 15? ? ? | NULL |? ? 6 |? 100.00 | Using index |+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+1 rowinset, 1 warning (0.00 sec)mysql> explain select count(1) from tb_hero;+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+| id | select_type | table? | partitions |type| possible_keys | key? ? ? | key_len | ref? | rows | filtered | Extra? ? ? |+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+|? 1 | SIMPLE? ? ? | tb_hero | NULL? ? ? | index | NULL? ? ? ? ? | idx_skill | 15? ? ? | NULL |? ? 6 |? 100.00 | Using index |+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+1 rowinset, 1 warning (0.00 sec)

有人糾結(jié)count(*)柠座、count(1)到底哪種寫法更高效,從上面的執(zhí)行計劃來看都一樣片橡,如果你還不放心的話妈经,官方文檔中也明確指明了InnoDB對count(*)、count(1)的處理完全一致捧书。

InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.

其他#

索引列上做任何操作(表達式吹泡、函數(shù)計算、類型轉(zhuǎn)換等)時無法使用索引會導(dǎo)致全表掃描

實戰(zhàn)#

前幾周測試同事對公司的某產(chǎn)品進行壓測经瓷,某單表寫入了近2億條數(shù)據(jù)爆哑,過程中發(fā)現(xiàn)配的報表有幾個數(shù)據(jù)查詢時間太長,所以重點看了幾個慢查詢SQL舆吮。避免敏感信息揭朝,這里對其提取簡化做個記錄。

Copy

mysql> select count(*) from tb_alert;

+-----------+

| count(*)? |

+-----------+

| 198101877 |

+-----------+

表join慢#

表join后色冀,取前10條數(shù)據(jù)就花了15秒萝勤,看了下SQL執(zhí)行計劃,如下:

Copy

mysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_idlimit10;10 rowsinset(15.46 sec)mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_idlimit10;+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+| id | select_type | table? ? ? ? ? ? ? | partitions |type| possible_keys | key? | key_len | ref? | rows? ? ? | filtered | Extra? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+|? 1 | SIMPLE? ? ? | tb_alert? ? ? ? ? | NULL? ? ? | ALL? | NULL? ? ? ? ? | NULL | NULL? ? | NULL | 190097118 |? 100.00 | NULL? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ||? 1 | SIMPLE? ? ? | tb_situation_alert | NULL? ? ? | ALL? | NULL? ? ? ? ? | NULL | NULL? ? | NULL |? 8026988 |? 100.00 | Usingwhere; Using join buffer (Block Nested Loop) |+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+2 rowsinset, 1 warning (0.00 sec)

可以看出join的時候沒有用上索引呐伞,tb_situation_alert表上聯(lián)合主鍵是這樣的PRIMARY KEY (situation_id, alert_id)敌卓,參與表join字段是alert_id,原來是不符合聯(lián)合索引的最左前綴法則伶氢,僅從這條sql看趟径,解決方案有兩種瘪吏,一種是對tb_situation_alert表上的alert_id單獨建立索引,另外一種是調(diào)換聯(lián)合主鍵的列的次序蜗巧,改為PRIMARY KEY (alert_id, situation_id)掌眠。當(dāng)然不能因為多配一張報表,就改其他產(chǎn)線的表的主鍵索引幕屹,這并不合理蓝丙。在這里,應(yīng)該對alert_id列單獨建立索引望拖。

Copy

mysql> create index idx_alert_id on tb_situation_alert (alert_id);mysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_idlimit100;100 rowsinset(0.01 sec)mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_idlimit100;+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+| id | select_type | table? ? ? ? ? ? ? | partitions |type| possible_keys | key? ? ? ? ? | key_len | ref? ? ? ? ? ? ? ? ? ? ? ? ? ? | rows? ? ? | filtered | Extra |+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+|? 1 | SIMPLE? ? ? | tb_alert? ? ? ? ? | NULL? ? ? | ALL? | NULL? ? ? ? ? | NULL? ? ? ? | NULL? ? | NULL? ? ? ? ? ? ? ? ? ? ? ? ? ? | 190097118 |? 100.00 | NULL? ||? 1 | SIMPLE? ? ? | tb_situation_alert | NULL? ? ? | ref? | idx_alert_id? | idx_alert_id | 8? ? ? | tb_alert.alert_id |? ? ? ? 2 |? 100.00 | NULL? |+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+2 rowsinset, 1 warning (0.00 sec)

優(yōu)化后渺尘,執(zhí)行計劃可以看出join的時候走了索引,查詢前100條0.01秒说敏,和之前的取前10條數(shù)據(jù)就花了15秒天壤之別鸥跟。

分頁查詢慢#

從第10000000條數(shù)據(jù)往后翻頁時,25秒才能出結(jié)果盔沫,這里就能使用上面的分頁查詢優(yōu)化技巧了医咨。上面講優(yōu)化建議時,沒看執(zhí)行計劃架诞,這里正好看一下拟淮。

Copy

mysql> select * from tb_alertlimit10000000, 10;10 rowsinset(25.23 sec)mysql> explain select * from tb_alertlimit10000000, 10;+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+| id | select_type | table? ? | partitions |type| possible_keys | key? | key_len | ref? | rows? ? ? | filtered | Extra |+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+|? 1 | SIMPLE? ? ? | tb_alert | NULL? ? ? | ALL? | NULL? ? ? ? ? | NULL | NULL? ? | NULL | 190097118 |? 100.00 | NULL? |+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+1 rowinset, 1 warning (0.00 sec)

再看下使用上分頁查詢優(yōu)化技巧的sql的執(zhí)行計劃

Copy

mysql> select * from tb_alert a inner join (select alert_id from tb_alertlimit10000000, 10) b on a.alert_id = b.alert_id;10 rowsinset(2.29 sec)mysql> explain select * from tb_alert a inner join (select alert_id from tb_alert a2limit10000000, 10) b on a.alert_id = b.alert_id;+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+| id | select_type | table? ? ? | partitions |type| possible_keys | key? ? ? ? ? | key_len | ref? ? ? | rows? ? ? | filtered | Extra? ? ? |+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+|? 1 | PRIMARY? ? | | NULL? ? ? | ALL? ? | NULL? ? ? ? ? | NULL? ? ? ? ? | NULL? ? | NULL? ? ? |? 10000010 |? 100.00 | NULL? ? ? ? ||? 1 | PRIMARY? ? | a? ? ? ? ? | NULL? ? ? | eq_ref | PRIMARY? ? ? | PRIMARY? ? ? | 8? ? ? | b.alert_id |? ? ? ? 1 |? 100.00 | NULL? ? ? ? ||? 2 | DERIVED? ? | a2? ? ? ? | NULL? ? ? | index? | NULL? ? ? ? ? | idx_processed | 5? ? ? | NULL? ? ? | 190097118 |? 100.00 | Using index |+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+3 rowsinset, 1 warning (0.00 sec)

分組聚合慢#

分析SQL后,發(fā)現(xiàn)根本上并非分組聚合慢谴忧,而是掃描聯(lián)合索引后惩歉,回表導(dǎo)致性能低下,去除不必要的字段俏蛮,使用覆蓋索引撑蚌。

這里避免敏感信息,只演示分組聚合前的簡化SQL搏屑,主要問題也是在這争涌。

表上有聯(lián)合索引KEY idx_alert_start_host_template_id ( alert_start, alert_host, template_id),優(yōu)化前的sql為

Copy

mysql> select alert_start, alert_host, template_id, alert_service from tb_alertwherealert_start > {ts'2019-06-05 00:00:10.0'}limit10000;10000 rowsinset(1 min 5.22 sec)

使用覆蓋索引辣恋,去掉template_id列亮垫,就能避免回表,查詢時間從1min多變?yōu)?.03秒伟骨,如下:

Copy

mysql> select alert_start, alert_host, template_id from tb_alertwherealert_start > {ts'2019-06-05 00:00:10.0'}limit10000;10000 rowsinset(0.03 sec)mysql> explain select alert_start, alert_host, template_id from tb_alertwherealert_start > {ts'2019-06-05 00:00:10.0'}limit10000;+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+| id | select_type | table? ? | partitions |type| possible_keys? ? ? ? ? ? ? ? ? ? ? | key? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | key_len | ref? | rows? ? | filtered | Extra? ? ? ? ? ? ? ? ? ? |+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+|? 1 | SIMPLE? ? ? | tb_alert | NULL? ? ? | range | idx_alert_start_host_template_id? | idx_alert_start_host_template_id? | 9? ? ? | NULL | 95048559 |? 100.00 | Usingwhere; Using index |+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+1 rowinset, 1 warning (0.01 sec)

總結(jié)#

任何不考慮應(yīng)用場景的設(shè)計都不是最好的設(shè)計饮潦,就比如說表結(jié)構(gòu)的設(shè)計、索引的創(chuàng)建携狭,都應(yīng)該權(quán)衡數(shù)據(jù)量大小继蜡、查詢需求、數(shù)據(jù)更新頻率等。

1)寧濫勿缺稀并。認(rèn)為一個查詢就需要建一個索引

2)寧缺勿濫仅颇。認(rèn)為索引會消耗空間、嚴(yán)重拖慢記錄的更新以及行的新增速度

最后碘举,小編想說:我是一名python開發(fā)工程師忘瓦, 整理了一套最新的python系統(tǒng)學(xué)習(xí)教程, 想要這些資料的可以關(guān)注私信小編“01”即可(免費分享哦)希望能 對你有所幫助.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末引颈,一起剝皮案震驚了整個濱河市耕皮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蝙场,老刑警劉巖凌停,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異李丰,居然都是意外死亡,警方通過查閱死者的電腦和手機逼泣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門趴泌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拉庶,你說我怎么就攤上這事嗜憔。” “怎么了氏仗?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵吉捶,是天一觀的道長。 經(jīng)常有香客問我皆尔,道長呐舔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任慷蠕,我火速辦了婚禮珊拼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘流炕。我一直安慰自己澎现,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布每辟。 她就那樣靜靜地躺著剑辫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪渠欺。 梳的紋絲不亂的頭發(fā)上妹蔽,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音,去河邊找鬼讹开。 笑死盅视,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旦万。 我是一名探鬼主播闹击,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼成艘!你這毒婦竟也來了赏半?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤淆两,失蹤者是張志新(化名)和其女友劉穎断箫,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秋冰,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡仲义,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了剑勾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片埃撵。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖虽另,靈堂內(nèi)的尸體忽然破棺而出暂刘,到底是詐尸還是另有隱情,我是刑警寧澤捂刺,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布谣拣,位于F島的核電站,受9級特大地震影響族展,放射性物質(zhì)發(fā)生泄漏森缠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一仪缸、第九天 我趴在偏房一處隱蔽的房頂上張望辅鲸。 院中可真熱鬧,春花似錦腹殿、人聲如沸独悴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刻炒。三九已至,卻和暖如春自沧,著一層夾襖步出監(jiān)牢的瞬間坟奥,已是汗流浹背树瞭。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留爱谁,地道東北人晒喷。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像访敌,于是被迫代替她去往敵國和親凉敲。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345