分頁、關聯查詢優化
文章目錄
一、分頁查詢示例
CREATE TABLE `employees` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年齡',
`position` varchar(20) NOT NULL DEFAULT '' COMMENT '職位',
`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入職時 間',
PRIMARY KEY (`id`),
KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
)
ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='員工記錄表';
1.分頁查詢優化
1.1 根據自增且連續的主鍵排序的分頁查詢
select * from employees limit 90000,5;
執行計劃
該 SQL 表示查詢從第 90001開始的五行資料,沒新增單獨 order by,表示通過主鍵排序。我們再看錶 employees ,因為主鍵是自增並且連續的,所以可以改寫成按照主鍵去查詢從第 90001開始的五行資料,如下:
select * from employees where id > 90000 limit 5;
執行計劃
分析:
顯然改寫後的 SQL 走了索引,而且掃描的行數大大減少,執行效率更高。 但是,這條 改寫的SQL 在很多場景並不實用,因為表中可能某些記錄被刪後,主鍵空缺,導致結果不一致,因此,如果主鍵不連續,不能使用上面描述的優化方法。 另外如果原 SQL 是 order by 非主鍵的欄位,按照上面說的方法改寫會導致兩條 SQL 的結果不一致。
所以這種改寫得滿 足以下兩個條件: 主鍵自增且連續、結果是按照主鍵排序的
1.2 根據非主鍵欄位排序的分頁查詢
EXPLAIN select * from employees ORDER BY name limit 90000,5;
發現並沒有使用 name 欄位的索引(key 欄位對應的值為 null),
**具體原因:**掃描整個索引並查詢到沒索引 的行(可能要遍歷多個索引樹)的成本比掃描全表的成本更高,所以優化器放棄使用索引。 知道不走索引的原因,那麼怎麼優化呢?
其實關鍵是讓排序時返回的欄位儘可能少,所以可以讓排序和分頁操作先查出主鍵,然後根據主鍵查到對應的記錄,SQL 改寫如下
mysql> select * from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id;
原 SQL 使用的是 filesort 排序,而優化後的 SQL 使用的是索引排序。
二、Join關聯查詢優化
1.Join關聯查詢示例表
CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`)
)
ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8;
create table t2 like t1;
假設t1表中一萬條資料,t2表中100條資料
2.常見表關聯演算法
Nested-Loop Join 演算法
Block Nested-Loop Join 演算法
2.1 巢狀迴圈連線 Nested-Loop Join(NLJ) 演算法
一次一行迴圈地從第一張表(稱為驅動表)中讀取行,在這行資料中取到關聯欄位,根據關聯欄位在另一張表(被驅動 表)裡取出滿足條件的行,然後取出兩張表的結果合集。
EXPLAIN select*from t1 inner join t2 on t1.a= t2.a;
從執行計劃中可以看到這些資訊:
-
驅動表是 t2,被驅動表是 t1。先執行的就是驅動表(執行計劃結果的id如果一樣則按從上到下順序執行sql);優化器一般會優先選擇小表做驅動表。所以使用 inner join 時,排在前面的表並不一定就是驅動表。
-
使用了 NLJ演算法。一般 join 語句中,如果執行計劃 Extra 中未出現 Using join buffer 則表示使用的 join 演算法是NLJ。
大致流程:
- 從表 t2 中讀取一行資料;
- 從第 1 步的資料中,取出關聯欄位 a,到表 t1 中查詢;
- 取出表 t1 中滿足條件的行,跟 t2 中獲取到的結果合併,作為結果返回給客戶端;
- 重複上面 3 步。
整個過程會讀取 t2 表的所有資料(掃描100行),然後遍歷這每行資料中欄位 a 的值,根據 t2 表中 a 的值索引掃描 t1 表 中的對應行 (掃描100次 t1 表的索引,1次掃描可以認為最終只掃描 t1 表一行完整資料,掃描非葉子節點的時間可以忽略不計,也就是總共 t1 表也掃描了100 行) 。因此整個過程掃描了 200 行。 如果被驅動表的關聯欄位沒索引,使用NLJ演算法效能會比較低(下面有詳細解釋),mysql會選擇Block Nested-Loop Join 演算法。
2.2 基於塊的巢狀迴圈連線 Block Nested-Loop Join(BNL)演算法
把驅動表的資料讀入到 join_buffer 中,然後掃描被驅動表,把被驅動表每一行取出來跟 join_buffer 中的資料做對比。
>EXPLAIN select*from t1 inner join t2 on t1.b= t2.b;
Extra 中 的Using join buffer (Block Nested Loop)說明該關聯查詢使用的是 BNL 演算法。
大致流程:
- 把 t2 的所有資料放入到 join_buffer 中
- 把表 t1 中每一行取出來,跟 join_buffer 中的資料做對比
- 返回滿足 join 條件的資料
整個過程對錶 t1 和 t2 都做了一次全表掃描,因此掃描的總行數為10000(表 t1 的資料總量) + 100(表 t2 的資料總量) = 10100。並且 join_buffer 裡的資料是無序的,因此對錶 t1 中的每一行,都要做 100 次判斷,所以記憶體中的判斷次數是 100 * 10000= 100 萬次。
被驅動表的關聯欄位沒索引為什麼要選擇使用 BNL 演算法而不使用 Nested-Loop Join 呢?
如果上面第二條sql使用 Nested-Loop Join,那麼掃描行數為 100 * 10000 = 100萬次,這個是磁碟掃描。
很顯然,用BNL磁碟掃描次數少很多,相比於磁碟掃描,BNL的記憶體計算會快得多。 因此MySQL對於被驅動表的關聯欄位沒索引的關聯查詢,一般都會使用 BNL 演算法。如果有索引一般選擇 NLJ 演算法,有 索引的情況下 NLJ 演算法比 BNL演算法效能更高
3.對於關聯sql的優化
關聯欄位加索引,讓mysql做join操作時儘量選擇NLJ演算法
小標驅動大表,寫多表連線sql時如果明確知道哪張表是小表可以用straight_join寫法固定連線驅動方式,省去 mysql優化器自己判斷的時間
straight_join解釋: straight_join功能同join類似,但能讓左邊的表來驅動右邊的表,能改表優化器對於聯表查詢的執 行順序
比如:
select * from t2 straight_join t1 on t2.a = t1.a;
代表制定mysql選著 t2 表作為驅動表。
- straight_join只適用於inner join,並不適用於left join,right join。(因為left join,right join已經代表指 定了表的執行順序)
- 儘可能讓優化器去判斷,因為大部分情況下mysql優化器是比人要聰明的。使用straight_join一定要慎重,因 為部分情況下人為指定的執行順序並不一定會比優化引擎要靠譜。
4.in和exsits優化
原則:小表驅動大表,即小的資料集驅動大的資料集
in: 當B表的資料集小於A表的資料集時,in優於exists
select * from A where id in (select id from B)
exists: 當A表的資料集小於B表的資料集時,exists優於in 將主查詢A的資料,放到子查詢B中做條件驗證,根據驗證結果(true或false)來決定主查詢的資料是否保留
select * from A where exists (select 1 from B where B.id = A.id)
1、EXISTS (subquery)只返回TRUE或FALSE,因此子查詢中的SELECT * 也可以用SELECT 1替換,官方說法是實際執行時會 忽略SELECT清單,因此沒有區別
2、EXISTS子查詢的實際執行過程可能經過了優化而不是我們理解上的逐條對比
3、EXISTS子查詢往往也可以用JOIN來代替,何種最優需要具體問題具體分析
三、count()查詢優化
EXPLAIN select count(1) from employees;
EXPLAIN select count(id) from employees;
EXPLAIN select count(name) from employees;
EXPLAIN select count(*) from employees;
四個sql的執行計劃一樣,說明這四個sql執行效率應該差不多,區別在於根據某個欄位count不會統計欄位為null值的資料行
為什麼mysql最終選擇輔助索引而不是主鍵聚集索引?
因為二級索引相對主鍵索引儲存資料更少,檢索效能應該更高
count()查詢優化方法
1、查詢mysql自己維護的總行數
對於myisam儲存引擎的表做不帶where條件的count查詢效能是很高的,因為myisam儲存引擎的表的總行數會被 mysql儲存在磁碟上,查詢不需要計算
對於innodb儲存引擎的表mysql不會儲存表的總記錄行數,查詢count需要實時計算
2、show table status
如果只需要知道表總行數的估計值可以用如下sql查詢,效能很高
show table status like ' employees';
3、將總數維護到Redis裡
插入或刪除表資料行的時候同時維護redis裡的表總行數key的計數值(用incr或decr命令),但是這種方式可能不準,很難 保證表操作和redis操作的事務一致性
4、增加計數表
插入或刪除表資料行的時候同時維護計數表,讓他們在同一個事務裡操作