常用查詢優化
1: max()優化: 在相應列上添加索引
2: count()優化:count(*) 會算出包含null記錄的數量, count(field_name)只包含不含 null的數量(這也是很多時候兩種count方式結果不一致的原因), count()的時候盡量用後一種, count(null)返回0,即不會記錄null記錄數量
3: 子查詢優化=====》(改為)聯接查詢(如果1對多的關系,注意重復記錄)
4: group by優化 如果包含子查詢,在子查詢裡面使用where條件和group by過濾, 避免在復雜查詢的最外層使用group by(如果最外層使用會用到臨時表)
5: order by , limit 優化:
方式1:盡量使用主鍵或有索引的列order by;
方式2: 使用自增型的字段: 記錄上一次返回的主鍵或者自增列(此種方式該字段不能有空值,否則會出現有的頁面數量不足的問題, 解決的方式是添加附加的index_id, 自增且索引), 過濾時先用大於上一次主鍵值且小於上一次的主鍵值+每頁的數量, 過濾該字段,然後order by 和limit
PS: 復合索引有效條件:
1: where 條件中依次過濾(最左前綴)
2:排序時: 索引字段有正有反的時候不能使用
3:排序時: 某列有范圍查詢的時候該列右側的字段不能使用索引
優化的思路就是盡量避免掃描過多的記錄。
創建索引的原則:
1: where, order by ,group by, on從句中的字段
2:索引字段越小越好
3:聯合索引時把離散程度高的字段放前面
表級優化
1: 表的范式優化
2: 適當增減一些冗余, 做反范式優化(以空間換取時間)
3: 表的列非常多的時候使用垂直拆分
原則:
1: 把不常用的單獨字段放到一個表中
2: 把大字段獨立存放到一個表中
3: 把經常一起用的字段放在一起
4: 表的數據量非常大的時候使用水平拆分
方法:
1: 根據某個字段進行hash預算, 如果要拆分成5個表, 用取余的方式取到0-4,分表保到相應的表中
2: 針對不同的hashID把數據存到不同的表中
問題:
1: 跨分區查詢的問題
2: 統計及後台報表操作
(前台使用分表查詢, 後台使用匯總表查詢做匯總報表操作).
一、 通過查詢緩沖提高查詢速度
一般我們使用SQL語句進行查詢時,數據庫服務器每次在收到客戶端發來SQL後,都會執行這條SQL語句。但當在一定間隔內(如1分鐘內),接到完全一樣的SQL語句,也同樣執行它。雖然這樣可以保證數據的實時性,但在大多數時候,數據並不要求完全的實時,也就是說可以有一定的延時。如果是這樣的話,在短時間內執行完全一樣的SQL就有些得不償失。
幸好MySQL為我們提供了查詢緩沖的功能(只能在MySQL 4.0.1及以上版本使用查詢緩沖)。我們可以通過查詢緩沖在一定程度上提高查詢性能。
1、我們可以通過在MySQL安裝目錄中的my.ini文件設置查詢緩沖:
設置也非常簡單,只需要將query_cache_type設為1即可。在設置了這個屬性後,MySQL在執行任何SELECT語句之前,都會在它的緩沖區中查詢是否在相同的SELECT語句被執行過,如果有,並且執行結果沒有過期,那麼就直接取查詢結果返回給客戶端。但在寫SQL語句時注意,MySQL的查詢緩沖是區分大小寫的。如下列的兩條SELECT語句:
SELECT * FROM TABLE1
SELECT * FROM TABLE1
上面的兩條SQL語句對於查詢緩沖是完全不同的SELECT。而且查詢緩沖並不自動處理空格,因此,在寫SQL語句時,應盡量減少空格的使用,尤其是在SQL首和尾的空格(因為,查詢緩沖並不自動截取首尾空格)。
2、臨時關閉查詢緩沖方法:
雖然不設置查詢緩沖,有時可能帶來性能上的損失,但有一些SQL語句需要實時地查詢數據,或者並不經常使用(可能一天就執行一兩次)。這樣就需要把緩沖關了。當然,這可以通過設置query_cache_type的值來關閉查詢緩沖,但這就將查詢緩沖永久地關閉了。
在MySQL 5.0中提供了一種可以臨時關閉查詢緩沖的方法:SQL_NO_CACHE。
SELECT SQL_NO_CACHE field1, field2 FROM TABLE1
以上的SQL語句由於使用了SQL_NO_CACHE,因此,不管這條SQL語句是否被執行過,服務器都不會在緩沖區中查找,每次都會執行它。
3、臨時開啟查詢緩沖方法:
我們還可以將my.ini中的query_cache_type設成2,這樣只有在使用了SQL_CACHE後,才使用查詢緩沖。
SELECT SQL_CALHE * FROM TABLE1
二、MySQL對查詢的自動優化
索引對於數據庫是非常重要的。在查詢時可以通過索引來提高性能。但有時使用索引反而會降低性能。我們可以看如下的SALES表:
CREATETABLE SALES
(
ID INT(10) UNSIGNED NOTNULL AUTO_INCREMENT,
NAME VARCHAR(100) NOTNULL,
PRICE FLOATNOTNULL,
SALE_COUNT INTNOTNULL,
SALE_DATE DATE NOTNULL,
PRIMARYKEY(ID),
INDEX (NAME),
INDEX (SALE_DATE)
)
假設這個表中保存了數百萬條數據,而我們要查詢商品號為1000的商品在2004年和2005年的平均價格。我們可以寫如下的SQL語句:
SELECT AVG(PRICE) FROM SALES
WHERE ID=1000 AND SALE_DATE BETWEEN '2004-01-01' AND '2005-12-31';
如果這種商品的數量非常多,差不多占了SALES表的記錄的50%或更多。那麼使用SALE_DATE字段上索引來計算平均數就有些慢。因為如果使用索引,就得對索引進行排序操作。當滿足條件的記錄非常多時(如占整個表的記錄的50%或更多的比例),速度會變慢,這樣還不如對整個表進行掃描。因此,MySQL會自動根據滿足條件的數據占整個表的數據的比例自動決定是否使用索引進行查詢。
對於MySQL來說,上述的查詢結果占整個表的記錄的比例是30%左右時就不使用索引了,這個比例是MySQL的開發人員根據他們的經驗得出的。然而,實際的比例值會根據所使用的數據庫引擎不同而不同。
三、 基於索引的排序
MySQL的弱點之一是它的排序。雖然MySQL可以在1秒中查詢大約15,000條記錄,但由於MySQL在查詢時最多只能使用一個索引。因此,如果WHERE條件已經占用了索引,那麼在排序中就不使用索引了,這將大大降低查詢的速度。我們可以看看如下的SQL語句:
SELECT*FROM SALES WHERE NAME = 'name' ORDERBY SALE_DATE DESC;
在以上的SQL的WHERE子句中已經使用了NAME字段上的索引,因此,在對SALE_DATE進行排序時將不再使用索引。為了解決這個問題,我們可以對SALES表建立復合索引:
ALTERTABLE SALES DROPINDEX NAME, ADDINDEX (NAME, SALE_DATE)
這樣再使用上述的SELECT語句進行查詢時速度就會大副提升。但要注意,在使用這個方法時,要確保WHERE子句中沒有排序字段,在上例中就是不能用SALE_DATE進行查詢,否則雖然排序快了,但是SALE_DATE字段上沒有單獨的索引,因此查詢又會慢下來。
SELECT*FROM SALES WHERE NAME = 'name1' AND NAME = 'name2'
以上的查詢語句要查找NAME既等於name1又等於name2的記錄。很明顯,這是一個不可達的查詢,WHERE條件一定是假。MySQL在執行SQL 語句之前,會先分析WHERE條件是否是不可達的查詢,如果是,就不再執行這條SQL語句了。為了驗證這一點。我們首先對如下的SQL使用EXPLAIN 進行測試:
EXPLAIN SELECT*FROM SALES WHERE NAME = ’name1'
上面的查詢是一個正常的查詢,我們可以看到使用EXPLAIN返回的執行信息數據中table項是SALES。這說明MySQL對SALES進行操作了。再看看下面的語句:
EXPLAIN SELECT*FROM SALES WHERE NAME = ’name1' AND NAME = 'name2'
我們可以看到,table項是空,這說明MySQL並沒有對SALES表進行操作。
四、 使用各種查詢選擇來提高性能
SELECT語句除了正常的使用外,MySQL還為我們提供了很多可以增強查詢性能的選項。如上面介紹的用於控制查詢緩沖的SQL_NO_CACHE和SQL_CACHE就是其中兩個選項。在這一部分,我將介紹幾個常用的查詢選項。
1、STRAIGHT_JOIN:強制連接順序
當我們將兩個或多個表連接起來進行查詢時,我們並不用關心MySQL先連哪個表,後連哪個表。而這一切都是由MySQL內部通過一系列的計算、評估,最後得出的一個連接順序決定的。如下列的SQL語句中,TABLE1和TABLE2並不一定是誰連接誰:
SELECT TABLE1.FIELD1, TABLE2.FIELD2 FROM TABLE1 ,TABLE2 WHERE …
如果開發人員需要人為地干預連接的順序,就得使用STRAIGHT_JOIN關鍵字,如下列的SQL語句:
SELECT TABLE1.FIELD1, TABLE2.FIELD2 FROM TABLE1 STRAIGHT_JOIN TABLE2 WHERE …
由上面的SQL語句可知,通過STRAIGHT_JOIN強迫MySQL按TABLE1、TABLE2的順序連接表。如果你認為按自己的順序比MySQL推薦的順序進行連接的效率高的話,就可以通過STRAIGHT_JOIN來確定連接順序。
2、干預索引使用,提高性能
在上面已經提到了索引的使用。一般情況下,在查詢時MySQL將自己決定是否使用索引,使用哪一個索引。
但在一些特殊情況下,我們希望MySQL只使用一個或幾個索引,或者不希望使用某個索引。這就需要使用MySQL的控制索引的一些查詢選項。
(1)限制使用索引的范圍:
有時我們在數據表裡建立了很多索引,當MySQL對索引進行選擇時,這些索引都在考慮的范圍內。但有時我們希望MySQL只考慮幾個索引,而不是全部的索引,這就需要用到USE INDEX對查詢語句進行設置。
SELECT*FROM TABLE1 USEINDEX (FIELD1, FIELD2) …
從以上SQL語句可以看出,無論在TABLE1中已經建立了多少個索引,MySQL在選擇索引時,只考慮在FIELD1和FIELD2上建立的索引。
(2)限制不使用索引的范圍:
如果我們要考慮的索引很多,而不被使用的索引又很少時,可以使用IGNORE INDEX進行反向選取。在上面的例子中是選擇被考慮的索引,而使用IGNORE INDEX是選擇不被考慮的索引。
SELECT*FROM TABLE1 IGNORE INDEX (FIELD1, FIELD2) …
在上面的SQL語句中,TABLE1表中只有FIELD1和FIELD2上的索引不被使用。
(3)強迫使用某一個索引:
上面的兩個例子都是給MySQL提供一個選擇,也就是說MySQL並不一定要使用這些索引。而有時我們希望MySQL必須要使用某一個索引(由於MySQL在查詢時只能使用一個索引,因此只能強迫MySQL使用一個索引)。這就需要使用FORCE INDEX來完成這個功能。
SELECT*FROM TABLE1 FORCE INDEX (FIELD1) …
以上的SQL語句只使用建立在FIELD1上的索引,而不使用其它字段上的索引。
3. 使用臨時表提供查詢性能
當我們查詢的結果集中的數據比較多時,可以通過SQL_BUFFER_RESULT選項強制將結果集放到臨時表中,這樣就可以很快地釋放MySQL的表鎖(這樣其它的SQL語句就可以對這些記錄進行查詢了),並且可以長時間地為客戶端提供大記錄集。
SELECT SQL_BUFFER_RESULT * FROM TABLE1 WHERE …
和SQL_BUFFER_RESULT選項類似的還有SQL_BIG_RESULT,這個選項一般用於分組或DISTINCT關鍵字,這個選項通知MySQL,如果有必要,就將查詢結果放到臨時表中,甚至在臨時表中進行排序。
SELECT SQL_BUFFER_RESULT FIELD1, COUNT(*) FROM TABLE1 GROUPBY FIELD1
五、MYSQL查詢優化:使用索引
MySQL有幾種使用索引的方式:
· 如上所述,索引被用於提高WHERE條件的數據行匹配或者執行聯結操作時匹配其它表的數據行的搜索速度。
· 對於使用了MIN()或MAX()函數的查詢,索引數據列中最小或最大值可以很快地找到,不用檢查每個數據行。
· MySQL利用索引來快速地執行ORDER BY和GROUP BY語句的排序和分組操作。
· 有時候MySQL會利用索引來讀取查詢得到的所有信息。假設你選擇了MyISAM表中的被索引的數值列,那麼就不需要從該數據表中選擇其它的數據列。在這種情況下,MySQL從索引文件中讀取索引值,它所得到的值與讀取數據文件得到的值是相同的。沒有必要兩次讀取相同的值,因此沒有必要考慮數據文件。
索引創建規則:
1、表的主鍵、外鍵必須有索引;
2、數據量超過300的表應該有索引;
3、經常與其他表進行連接的表,在連接字段上應該建立索引;
4、經常出現在Where子句中的字段,特別是大表的字段,應該建立索引;
5、索引應該建在選擇性高的字段上;
6、索引應該建在小字段上,對於大的文本字段甚至超長字段,不要建索引;
7、復合索引的建立需要進行仔細分析;盡量考慮用單字段索引代替:
A、正確選擇復合索引中的主列字段,一般是選擇性較好的字段;
B、復合索引的幾個字段是否經常同時以AND方式出現在Where子句中?單字段查詢是否極少甚至沒有?如果是,則可以建立復合索引;否則考慮單字段索引;
C、如果復合索引中包含的字段經常單獨出現在Where子句中,則分解為多個單字段索引;
D、如果復合索引所包含的字段超過3個,那麼仔細考慮其必要性,考慮減少復合的字段;
E、如果既有單字段索引,又有這幾個字段上的復合索引,一般可以刪除復合索引;
8、頻繁進行數據操作的表,不要建立太多的索引;
9、刪除無用的索引,避免對執行計劃造成負面影響;
以上是一些普遍的建立索引時的判斷依據。一言以蔽之,索引的建立必須慎重,對每個索引的必要性都應該經過仔細分析,要有建立的依據。
因為太多的索引與不充分、不正確的索引對性能都毫無益處:在表上建立的每個索引都會增加存儲開銷,索引對於插入、刪除、更新操作也會增加處理上的開銷。
另外,過多的復合索引,在有單字段索引的情況下,一般都是沒有存在價值的;相反,還會降低數據增加刪除時的性能,特別是對頻繁更新的表來說,負面影響更大。
查詢優化之explain的深入解析
下面來舉一個例子來說明下 explain 的用法。
先來一張表:
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 條件是一個范圍值(所謂 range),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 也消失了,結果非常理想。
再來看一個多表查詢的例子。
首先定義 3個表 class 和 room。
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腳本:
<?php
$link = mysql_connect("localhost","root","870516");
mysql_select_db("test",$link);
for($i=0;$i<10000;$i++)
{
$j = rand(1,20);
$sql = " insert into class(card) values({$j})";
mysql_query($sql);
}
for($i=0;$i<10000;$i++)
{
$j = rand(1,20);
$sql = " insert into book(card) values({$j})";
mysql_query($sql);
}
for($i=0;$i<10000;$i++)
{
$j = rand(1,20);
$sql = " insert into phone(card) values({$j})";
mysql_query($sql);
}
mysql_query("COMMIT");
?>
然後來看一個左連接查詢:
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`);
結果
*************************** 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)
基本無變化。
然後來看一個右連接查詢:
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)
優化較明顯。這是因為 RIGHT JOIN 條件用於確定如何從左表搜索行,右邊一定都有,所以左邊是我們的關鍵點,一定需要建立索引。
刪除舊索引:
DROP INDEX x ON class;
建立新索引。
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: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 20000
Extra:
2 rows in set (0.00 sec)
基本無變化。
最後來看看 inner join 的情況:
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 和 left join 差不多,都需要優化右表。而 right 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 優化很好,效果不錯。