1. 程式人生 > 程式設計 >Mysql索引

Mysql索引

概念

官方定義:索引是幫助mysql高效獲取資料的資料結構。所以索引的本質就是資料結構,可以理解為排好序的快速查詢的資料結構
資料本身之外,資料庫還維護著一個滿足特定查詢演演算法的資料結構,這些資料結構以某種方式指向資料,這樣就可以在這些資料結構的基礎上實現高階查詢演演算法,這種資料結構就是索引。

優勢

  • 類似於圖書館書目索引,提高資料檢索效率,降低資料庫的io成本
  • 通過索引列對資料進行排序,降低資料排序的成本,降低cpu的消耗

劣勢

雖然索引大大提高了查詢速度,同時卻會降低更新表的速度,如對錶進行INSERT、UPDATE和DELETE。因為更新表時,MySQL不僅要更新資料,還要更新一下索引檔案每次更新添加了索引列的欄位,都會調整因為更新所帶來的鍵值變化後的索引資訊。同時索引還會佔用一定的磁碟空間。

分類

  • 單值索引
    一個索引只包含單個列,一個表可以有多個單列索引
  • 唯一索引
    索引列的值必須唯一,但允許空值
  • 複合索引
    即一個索引包含多個列

語法

  • 建立
    1. CREATE [UNIQUE] INDEX indexName ON tableName(欄位名...);
    2. ALTER TABLE tableName ADD [UNIQUE] INDEX indexName (欄位名...);
  • 刪除
    DROP INDEX indexName ON tableName;
  • 檢視
    SHOW INDEX FROM tableName

索引的最佳實踐

哪些情況建立索引

  1. 主鍵自動建立唯一索引
  2. 頻繁作為查詢條件的欄位應該建立索引(where 後面的語句或者ORDER BY 語句中出現的列)
  3. 查詢中與其他表關聯的欄位,外來鍵關係建立索引
  4. 單鍵、組合索引的選擇問題(在高併發下推薦建立組合索引)
  5. 查詢中排序的欄位,排序欄位若通過索引去訪問將大大提高排序速度
  6. 查詢中統計或者分組的欄位

哪些情況不建立索引

  1. 表記錄太少
  2. 經常增刪改的表
  3. 資料重複且分佈平均的表字段,因此只為最經常查詢和最經常排序的資料列建立索引。注意,如果某個資料列包含許多重複的內容,為他建立索引就沒有太大的實際效果。(比如14億中國人的國籍都是中國,這種型別欄位就可以不建立索引,或者性別)
  4. Where條件裡用不到的欄位不建立索引

效能分析

使用EXPLAIN關鍵字可以模擬優化器執行sql查詢語句,從而知道mysql是如何處理你的sql語句。分析你的查詢語句或是表結構的效能瓶頸。

語法

explain sql語句

相關欄位

每列的定義如下

  • id: SELECT 查詢的識別符號. 每個 SELECT 都會自動分配一個唯一的識別符號
    1. id相同,執行順序由上至下
    2. id不同,如果是子查詢,id的序號會遞增,id值越大優先順序越高,越先被執行
  • select_type: SELECT 查詢的型別.主要是用於區別普通查詢、聯合查詢、子查詢等的複雜查詢
    1. SIMPLE:簡單的 select 查詢,查詢中不包含子查詢或者UNION
    2. PRIMARY:查詢中若包含任何複雜的子部分,最外層查詢則被標記為Primary
    3. DERIVED:在FROM列表中包含的子查詢被標記為DERIVED(衍生)MySQL會遞迴執行這些子查詢,把結果放在臨時表裡。
    4. SUBQUERY:在SELECT或WHERE列表中包含了子查詢
    5. UNION:若第二個SELECT出現在UNION之後,則被標記為UNION;若UNION包含在FROM子句的子查詢中,外層SELECT將被標記為:DERIVED
    6. UNCACHEABLE SUBQUREY:無法被快取的子查詢
    7. DEPENDENT SUBQUERY:在SELECT或WHERE列表中包含了子查詢,子查詢基於外層
    8. UNION RESULT:從UNION表獲取結果的SELECT
  • table: 查詢的是哪個表,顯示這一行的資料是關於哪張表的
  • type: join 型別(決定效能的指標) 顯示查詢使用了何種型別,從最好到最差依次是: system>const>eq_ref>ref>range>index>ALL
    1. system:表只有一行記錄(等於系統表),這是const型別的特列,平時不會出現,這個也可以忽略不計
    2. const:表示通過索引一次就找到了,const用於比較primary key或者unique索引。因為只匹配一行資料,所以很快如將主鍵置於where列表中,MySQL就能將該查詢轉換為一個常量
    3. eq_ref:唯一性索引掃描,對於每個索引鍵,表中只有一條記錄與之匹配。常見於主鍵或唯一索引掃描
    4. ref:非唯一性索引掃描,返回匹配某個單獨值的所有行.本質上也是一種索引訪問,它返回所有匹配某個單獨值的行,然而,它可能會找到多個符合條件的行,所以他應該屬於查詢和掃描的混合體(例如:根據姓名查詢人的資訊,姓名是可以重複的,並不是唯一的欄位)
    5. range:只檢索給定範圍的行,使用一個索引來選擇行。key 列顯示使用了哪個索引 一般就是在你的where語句中出現了between、<、>、in等的查詢這種範圍掃描索引掃描比全表掃描要好,因為它只需要開始於索引的某一點,而結束語另一點,不用掃描全部索引。
    6. Index:Full IndexScan,index與ALL區別為index型別只遍歷索引樹。這通常比ALL快,因為索引檔案通常比資料檔案小。(也就是說雖然all和Index都是讀全表,但index是從索引中讀取的,而all是從硬碟中讀的)
    7. All:Full Table Scan,將遍歷全表以找到匹配的行
      備註:一般來說,得保證查詢至少達到range級別,最好能達到ref。
  • possible_keys: 此次查詢中可能選用的索引顯示可能應用在這張表中的索引,一個或多個。查詢涉及到的欄位上若存在索引,則該索引將被列出,但不一定被查詢實際使用
  • key: 此次查詢中確切使用到的索引
  • Key_len: 表示索引中使用的位元組數,可通過該列計算查詢中使用的索引的長度。key_len欄位能夠幫你檢查是否充分的利用上了索引
  • ref: 哪個欄位或常數與key一起被使用.顯示索引的哪一列被使用了,如果可能的話,是一個常數。哪些列或常量被用於查詢索引列上的值
  • rows: 顯示此查詢一共掃描了多少行. 這個是一個估計值.(找到所需記錄掃描的行數)
  • filtered: 表示此查詢條件所過濾的資料的百分比
  • extra: 包含不適合在其他列中顯示但十分重要的額外資訊
    1. Using filesort:說明mysql會對資料使用一個外部的索引排序,而不是按照表內的索引順序進行讀取。MySQL中無法利用索引完成的排序操作稱為“檔案排序”
    2. Using where:表明使用了where過濾
    3. Using Index :表示索引覆蓋(Covering Index),不會回表查詢("覆蓋索引掃描",表示查詢在索引樹中就可查詢所需資料,不用掃描表資料檔案,往往說明效能不錯)
    4. Using temporary: 使了用臨時表儲存中間結果,MySQL在對查詢結果排序時使用臨時表。常見於排序 order by 和分組查詢 group by。
    5. Covering Index:覆蓋索引,就是查詢的列要被所建的索引覆蓋

主要欄位:Id/type/key/rows/Extra

查詢優化

索引的失效情況以及索引的優化

測試sql

CREATE TABLE staffs (
 id INT PRIMARY KEY AUTO_INCREMENT,NAME VARCHAR (24) NULL DEFAULT '' COMMENT '姓名',age INT NOT NULL DEFAULT 0 COMMENT '年齡',pos VARCHAR (20) NOT NULL DEFAULT '' COMMENT '職位',add_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入職時間'
) CHARSET utf8 COMMENT '員工記錄表' ;

INSERT INTO staffs(NAME,age,pos,add_time) VALUES('z3',22,'manager',NOW());
INSERT INTO staffs(NAME,add_time) VALUES('July',23,'dev',add_time) VALUES('2000',add_time) VALUES(null,NOW());
SELECT * FROM staffs;

ALTER TABLE staffs ADD INDEX idx_staffs_nameAgePos(name,pos);
複製程式碼
  1. 全值匹配我最愛
    索引 idx_staffs_nameAgePos 建立索引時 以 name , age ,pos 的順序建立的。全值匹配表示 按順序匹配的
    EXPLAIN SELECT * FROM staffs WHERE NAME = 'July';

可以看到使用到了索引,並且是ref級別的
EXPLAIN SELECT * FROM staffs WHERE NAME = 'July' AND age = 25;

EXPLAIN SELECT * FROM staffs WHERE NAME = 'July' AND age = 25 AND pos = 'dev';

2. 最佳左字首法則
如果索引了多列,要遵守最左字首法則。指的是查詢從索引的最左前列開始並且不跳過索引中的列。
and 忽略左右關係。既即使沒有沒有按順序 由於優化器的存在,會自動優化。
經過試驗結論建立了 idx_nameAge 索引id 為主鍵
當使用覆蓋索引的方式時,(select name/age/id from staffs where age=10 (後面沒有其他沒有索引的欄位條件)),即使不是以 name 開頭,也會使用 idx_nameAge 索引。 既 select 後的欄位 有索引,where 後的欄位也有索引,則無關執行順序。
除開上述條件 才滿足最左字首法則。

EXPLAIN SELECT * FROM staffs WHERE age = 25 AND pos = 'dev';

可以看到並沒有使用到索引,原因是查詢條件中跳過了索引的第一個欄位。
EXPLAIN SELECT * FROM staffs WHERE pos = 'dev';

3. 不在索引列上做任何操作(計算、函式、(自動or手動)型別轉換),會導致索引失效而轉向全表掃描
EXPLAIN SELECT * FROM staffs WHERE left(NAME,4) = 'July';

4. 儲存引擎不能使用索引中範圍條件右邊的列
EXPLAIN SELECT * FROM staffs WHERE name = 'z3' and age > 20 and pos = 'manmger';
5. 儘量使用覆蓋索引(只訪問索引的查詢(索引列和查詢列一致)),減少select *

可以看出使用覆蓋索引效能會更好
6. mysql 在使用不等於(!= 或者<>)的時候無法使用索引會導致全表掃描
使用 != 和 <> 的欄位索引失效(!= 針對數值型別。 <> 針對字元型別
前提 where and 後的欄位在混合索引中的位置比當前欄位靠後 where age != 10 and name='xxx',這種情況下,mysql自動優化,將 name='xxx' 放在 age !=10 之前,name 依然能使用索引。只是 age 的索引失效)
7. is not null 也無法使用索引,但是is null是可以使用索引的
8. like以萬用字元開頭('%abc...')mysql索引失效會變成全表掃描的操作
這種情況會導致索引失效全表掃描,而abc%這種情況下是可以使用索引的
解決方案:這種情況下儘量使用覆蓋索引(即select後面的欄位和所建立的索引的欄位一致),部分情況下可以解決這種索引失效問題
9. 字串不加單引號索引失效
例如某欄位name的名稱為100,是VARCHAR型別的但是作為查詢條件100並沒有加''號,底層進行轉換使索引失效,使用了函式造成索引失效
10. 少用or,用它來連線時會索引失效

一般性建議

  • 對於單鍵索引,儘量選擇針對當前query過濾性更好的索引
  • 在選擇組合索引的時候,當前Query中過濾性最好的欄位在索引欄位順序中,位置越靠前越好。(避免索引過濾性好的索引失效)
  • 在選擇組合索引的時候,儘量選擇可以能夠包含當前query中的where字句中更多欄位的索引
  • 儘可能通過分析統計資訊和調整query的寫法來達到選擇合適索引的目的

單表查詢優化

sql

CREATE TABLE IF NOT EXISTS `article` (
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY 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
);

INSERT INTO `article`(`author_id`,`category_id`,`views`,`comments`,`title`,`content`) VALUES
(1,1,'1','1'),(2,2,'2','2'),(1,3,'3','3');

SELECT * FROM article;
複製程式碼
  • 查詢 category_id 為1 且 comments 大於 1 的情況下,views 最多的 article_id。
    EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;

結論:很顯然,type 是 ALL,即最壞的情況。Extra裡還出現了Usingfilesort,也是最壞的情況。優化是必須的。
開始優化:
1.1 新建索引+刪除索引
ALTER TABLE article ADD INDEX idx_article_ccv ( category_id,comments,views );
create index idx_article_ccv on article(category_id,comments,views);
DROP INDEX idx_article_ccv ON article

1.2 第2次EXPLAIN
EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments >1 ORDER BY views DESC LIMIT 1;

結論: type 變成了 range,這是可以忍受的。但是 extra 裡使用 Using filesort 仍是無法接受的。 但是我們已經建立了索引,為啥沒用呢?
這是因為按照 BTree 索引的工作原理,先排序 category_id,如果遇到相同的 category_id 則再排序 comments,如果遇到相同的 comments 則再排序 views。 當 comments 欄位在聯合索引裡處於中間位置時,因comments > 1 條件是一個範圍值(所謂 range),MySQL 無法利用索引再對後面的 views 部分進行檢索,即 range 型別查詢欄位後面的索引無效。

1.3 刪除第一次建立的索引 DROP INDEX idx_article_ccv ON article;

1.4 第2次新建索引
ALTER TABLE article ADD INDEX idx_article_cv ( category_id,views ) ; create index idx_article_cv on article(category_id,views);

1.5 第3次EXPLAIN
EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;

結論:可以看到,type 變為了 ref,Extra 中的 Using filesort 也消失了,結果非常理想。 DROP INDEX idx_article_cv ON article;

關聯查詢優化

sql

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,PRIMARY KEY (`bookid`)
);


INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));


INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));

複製程式碼
  • 下面開始explain分析
    EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;

結論:type有All
新增索引優化
ALTER TABLE book ADD INDEX Y ( card);
第2次explain
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;

可以看到第二行的type變為了ref,rows也變成了優化比較明顯。
這是由左連線特性決定的。LEFT JOIN條件用於確定如何從右表搜尋行,左邊一定都有,所以右邊是我們的關鍵點,一定需要建立索引。

刪除舊索引+新建+第3次explain
DROP INDEX Y ON book;
ALTER TABLE class ADD INDEX X (card);
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;

建議

  • 保證被驅動表的join欄位已經被索引
  • left join 時,選擇小表作為驅動表,大表作為被驅動表。
  • inner join 時,mysql會自己幫你把小結果集的表選為驅動表。
  • 子查詢儘量不要放在被驅動表,有可能使用不到索引。

子查詢優化

用in 還是 exists
有索引 大表驅動小表
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
select sql_no_cache sum(sal) from emp where exists (select 1 from dept where emp.deptno=dept.deptno);
用 exists 是否存在,存在返回一條記錄,exists 是作為一個查詢判斷用,所以 select 後返回什麼不重要。
select sql_no_cache sum(sal) from emp inner join dept on emp.deptno=dept.deptno;

有索引 小表驅動大表
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e where exists (select 1 from emp where e.deptno=emp.deptno);
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e inner join (select distinct deptno from emp) m on m.deptno=e.deptno;
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);

有索引小驅動大表效能優於大表驅動小表
無索引 小表驅動大表
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e where exists (select 1 from emp where e.deptno=emp.deptno);
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e inner join (select distinct deptno from emp) m on m.deptno=e.deptno;
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);

無索引大表驅動小表
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
select sql_no_cache sum(sal) from emp where exists (select 1 from dept where emp.deptno=dept.deptno);
select sql_no_cache sum(sal) from emp inner join dept on emp.deptno=dept.deptno;
結論
有索引的情況下 用 inner join 是最好的 其次是 in ,exists最糟糕
無索引的情況下用
小表驅動大表 因為join 方式需要distinct ,沒有索引distinct消耗效能較大
所以 exists效能最佳 in其次 join效能最差?
無索引的情況下大表驅動小表
in 和 exists 的效能應該是接近的 都比較糟糕 exists稍微好一點 超不過5% 但是inner join 優於使用了 join buffer 所以快很多
如果left join 則最慢

order by關鍵字優化

ORDER BY子句,儘量使用Index方式排序,避免使用FileSort方式排序
MySQL支援二種方式的排序,FileSort和Index,Index效率高. 它指MySQL掃描索引本身完成排序。FileSort方式效率較低。
ORDER BY滿足兩情況,會使用Index方式排序:

  • ORDER BY 語句使用索引最左前列
  • 使用Where子句與Order BY子句條件列組合滿足索引最左前列
  • where子句中如果出現索引的範圍查詢(即explain中出現range)會導致order by 索引失效。

儘可能在索引列上完成排序操作,遵照索引建的最佳左字首
如果不在索引列上,filesort有兩種演演算法:mysql就要啟動雙路排序和單路排序
雙路排序

  • MySQL 4.1之前是使用雙路排序,字面意思就是兩次掃描磁碟,最終得到資料, 讀取行指標和orderby列,對他們進行排序,然後掃描已經排序好的列表,按照列表中的值重新從列表中讀取對應的資料輸出。多路排序需要藉助 磁碟來進行排序。所以 取資料,排好了取資料。兩次 io操作。比較慢單路排序 ,將排好的資料存在記憶體中,省去了一次io操作,所以比較快,但是需要記憶體空間足夠。
  • 從磁碟取排序欄位,在buffer進行排序,再從磁碟取其他欄位。

單路排序

  • 取一批資料,要對磁碟進行了兩次掃描,眾所周知,I\O是很耗時的,所以在mysql4.1之後,出現了第二種改進的演演算法,就是單路排序。從磁碟讀取查詢需要的所有列,按照orderby列在buffer對它們進行排序,然後掃描排序後的列表進行輸出,它的效率更快一些,避免了第二次讀取資料。並且把隨機IO變成了順序IO,但是它會使用更多的空間,因為它把每一行都儲存在記憶體中了。

單路排序引申出的問題
在sort_buffer中,方法B比方法A要多佔用很多空間,因為方法B是把所有欄位都取出,所以有可能取出的資料的總大小超出了sort_buffer的容量,導致每次只能取sort_buffer容量大小的資料,進行排序(建立tmp檔案,多路合併),排完再取取sort_buffer容量大小,再排……從而多次I/O。本來想省一次I/O操作,反而導致了大量的I/O操作,反而得不償失。

優化策略

  • 增大sort_buffer_size引數的設定
    用於單路排序的記憶體大小
  • 增大max_length_for_sort_data引數的設定
    單次排序欄位大小。(單次排序請求)
  • 去掉select 後面不需要的欄位
    select後的多了,排序的時候也會帶著一起,很佔記憶體,所以去掉沒有用的

GROUP BY關鍵字優化

  • group by實質是先排序後進行分組,遵照索引建的最佳左字首
  • 當無法使用索引列,增大max_length_for_sort_data引數的設定+增大sort_buffer_size引數的設定
  • where高於having,能寫在where限定的條件就不要去having限定了。

去重優化

儘量不要使用distinct關鍵字去重
例如:
t_mall_sku 表   id  shp_id      kcdz                
------  ------ --------------------
     3       1    北京市昌平區  
     4       1    北京市昌平區  
     5       5    北京市昌平區  
     6       3       重慶              
     8       8     天津              
例子:
select kcdz form t_mall_sku where id in( 3,4,5,6,8 ) 將產生重複資料,
select distinct kcdz form t_mall_sku where id in( 3,8 ) 使用 distinct 關鍵字去重消耗效能
優化: select  kcdz form t_mall_sku where id in( 3,8 )  group by kcdz 能夠利用到索引

案例

。。。。。。