MySQL -- order by
CREATE TABLE `t` ( `id` INT(11) NOT NULL, `city` VARCHAR(16) NOT NULL, `name` VARCHAR(16) NOT NULL, `age` INT(11) NOT NULL, `addr` VARCHAR(128) DEFAULT NULL, PRIMARY KEY (`id`), KEY `city` (`city`) ) ENGINE=InnoDB;
查詢語句
SELECT city,name,age FROM t WHERE city='杭州' ORDER BY name LIMIT 1000;
儲存過程
DELIMITER ;; CREATE PROCEDURE idata() BEGIN DECLARE i INT; SET i=0; WHILE i<4000 DO INSERT INTO t VALUES (i,'杭州',concat('zhongmingmao',i),'20','XXX'); SET i=i+1; END WHILE; END;; DELIMITER ; CALL idata();
全欄位排序
city索引樹
滿足city=’杭州’的行,主鍵為 ID_X ~ ID_(X+N)
sort buffer
mysql> EXPLAIN SELECT city,name,age FROM t FORCE INDEX(city) WHERE city='杭州' ORDER BY name LIMIT 1000\G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t partitions: NULL type: ref possible_keys: city key: city key_len: 50 ref: const rows: 4000 filtered: 100.00 Extra: Using index condition; Using filesort
-
rows=4000
:EXPLAIN是 不考慮LIMIT的 ,代表 匹配條件的總行數 -
Using index condition
:表示使用了 索引下推 -
Using filesort
:表示需要 排序 ,MySQL會為每個 執行緒 分配一塊記憶體用於排序,即sort buffer
-- 1048576 Bytes = 1 MB mysql> SHOW VARIABLES LIKE '%sort_buffer%'; +-------------------------+----------+ | Variable_name| Value| +-------------------------+----------+ | innodb_sort_buffer_size | 67108864 | | myisam_sort_buffer_size | 8388608| | sort_buffer_size| 1048576| +-------------------------+----------+
執行過程
- 初始化
sort buffer
,確定放入三個欄位:city
、name
、age
- 從 city索引樹 找到第一個滿足city=’杭州’的主鍵ID,即ID_X
- 然後拿著ID_X 回表 取出整行,將
city
、name
、age
這三個欄位的值都存入sort buffer
- 回到 city索引樹 取下一條記錄,重複上述動作,直至city的值不滿足條件為止,即ID_Y
- 對
sort buffer
中的資料按照name
欄位進行排序- 排序過程可能使用 內部排序 ( 記憶體 , 首選 , 快速排序 ),也可能使用 外部排序 ( 磁碟 , 次選 , 歸併排序 )
- 這取決於 排序所需要的記憶體 是否小於
sort_buffer_size
(預設 1 MB )
- 按照排序結果取前1000行返回給客戶端
觀察指標
-- 開啟慢查詢日誌 SET GLOBAL slow_query_log=ON; SET long_query_time=0; -- 查詢optimizer_trace時需要用到臨時表,internal_tmp_disk_storage_engine預設值為InnoDB -- 採用預設值時,把資料從臨時表取出來的時候,會將Innodb_rows_read+1,因此修改為MyISAM,減少干擾資訊 SET GLOBAL internal_tmp_disk_storage_engine=MyISAM; -- 將sort buffer設定為最小值,這是為了構造外部排序的場景,如果是內部排序則無需執行該語句 SET sort_buffer_size=32768; -- 開啟optimizer_trace,只對本執行緒有效 SET optimizer_trace='enabled=on'; -- @a 儲存Innodb_rows_read的初始值 SELECT VARIABLE_VALUE INTO @a FROMperformance_schema.session_status WHERE variable_name = 'Innodb_rows_read'; -- 執行語句 SELECT city,name,age FROM t FORCE INDEX(city) WHERE city='杭州' ORDER BY name LIMIT 1000; -- 檢視optimizer_trace輸出 SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G; -- @b 儲存Innodb_rows_read的當前值 SELECT VARIABLE_VALUE INTO @b FROM performance_schema.session_status WHERE variable_name = 'Innodb_rows_read'; -- 計算Innodb_rows_read差值 -- MyISAM為4000,InnoDB為4001 SELECT @b-@a;
外部排序
慢查詢日誌
# Time: 2019-02-10T07:19:38.347053Z # User@Host: root[root] @ localhost []Id:8 # Query_time: 0.012832Lock_time: 0.000308 Rows_sent: 1000Rows_examined: 5000 SET timestamp=1549783178; SELECT city,name,age FROM t FORCE INDEX(city) WHERE city='杭州' ORDER BY name LIMIT 1000;
OPTIMIZER_TRACE
"filesort_summary": { "memory_available": 32768, "key_size": 32, "row_size": 140, "max_rows_per_buffer": 234, "num_rows_estimate": 16912, "num_rows_found": 4000, "num_examined_rows": 4000, "num_initial_chunks_spilled_to_disk": 9, "peak_memory_used": 35096, "sort_algorithm": "std::stable_sort", "sort_mode": "<fixed_sort_key, packed_additional_fields>" }
In optimizer trace output, num_tmp_files
did not actually indicate number of files.
It has been renamed to num_initial_chunks_spilled_to_disk
and indicates the number of chunks before any merging has occurred .
-
num_initial_chunks_spilled_to_disk=9
,說明採用了 外部排序 ,使用了 磁碟臨時檔案 -
peak_memory_used > memory_available
: sort buffer空間不足 - 如果
sort_buffer_size
越小,num_initial_chunks_spilled_to_disk
的值就越大 - 如果
sort_buffer_size
足夠大,那麼num_initial_chunks_spilled_to_disk=0
,採用 內部排序 -
num_examined_rows=4000
: 參與排序的行數 -
sort_mode
含有的packed_additional_fields
:排序過程中對 字串 做了 緊湊 處理- 欄位name為
VARCHAR(16)
,在排序過程中還是按照 實際長度 來分配空間
- 欄位name為
掃描行數
整個執行過程中總共 掃描 了4000行(如果 internal_tmp_disk_storage_engine=InnoDB
,返回4001)
mysql> SELECT @b-@a; +-------+ | @b-@a | +-------+ |4000 | +-------+
內部排序
慢查詢日誌
Query_time
為0.007517,為採用外部排序的 59%
# Time: 2019-02-10T07:36:36.442679Z # User@Host: root[root] @ localhost []Id:8 # Query_time: 0.007517Lock_time: 0.000242 Rows_sent: 1000Rows_examined: 5000 SET timestamp=1549784196; SELECT city,name,age FROM t FORCE INDEX(city) WHERE city='杭州' ORDER BY name LIMIT 1000;
OPTIMIZER_TRACE
"filesort_information": [ { "direction": "asc", "table": "`t` FORCE INDEX (`city`)", "field": "name" } ], "filesort_priority_queue_optimization": { "limit": 1000, "chosen": true }, ... "filesort_summary": { "memory_available": 1048576, "key_size": 32, "row_size": 138, "max_rows_per_buffer": 1001, "num_rows_estimate": 16912, "num_rows_found": 1001, "num_examined_rows": 4000, "num_initial_chunks_spilled_to_disk": 0, "peak_memory_used": 146146, "sort_algorithm": "std::stable_sort", "unpacked_addon_fields": "using_priority_queue", "sort_mode": "<fixed_sort_key, additional_fields>" }
-
num_initial_chunks_spilled_to_disk=0
,說明採用了內部排序,排序直接在sort buffer
中完成 -
peak_memory_used < memory_available
: sort buffer空間充足 -
num_examined_rows=4000
: 參與排序的行數 -
filesort_priority_queue_optimization
:採用 優先順序佇列優化 ( 堆排序 )
掃描行數
mysql> SELECT @b-@a; +-------+ | @b-@a | +-------+ |4000 | +-------+
效能
- 全欄位排序:對 原表 資料讀一遍(覆蓋索引的情況除外),其餘操作都在
sort buffer
和 臨時檔案 中進行 - 如果查詢要 返回的欄位很多 ,那麼
sort buffer
中能同時放下的行就會變得很少 - 這時會分成 很多個臨時檔案 , 排序效能就會很差
- 解決方案:採用 rowid排序
- 單行的長度 不超過
max_length_for_sort_data
: 全欄位排序 - 單行的長度 超過
max_length_for_sort_data
: rowid排序
- 單行的長度 不超過
mysql> SHOW VARIABLES LIKE '%max_length_for_sort_data%'; +--------------------------+-------+ | Variable_name| Value | +--------------------------+-------+ | max_length_for_sort_data | 4096| +--------------------------+-------+
rowid排序
city
、 name
和 age
三個欄位的總長度最少為36,執行 SET max_length_for_sort_data=16;
執行過程
- 初始化
sort buffer
,確定放入兩個欄位:name
(需要排序的欄位)、id
(索引組織表,主鍵) - 從 city索引樹 找到第一個滿足city=’杭州’的主鍵ID,即ID_X
- 然後拿著ID_X 回表 取出整行,將
name
和ID
這兩個欄位的值存入sort buffer
- 回到 city索引樹 取下一條記錄,重複上述動作,直至city的值不滿足條件為止,即ID_Y
- 對
sort buffer
中的資料按照name
欄位進行排序(當然也有可能仍然是 外部排序 ) - 遍歷排序結果,取出前1000行,並按照主鍵id的值 回表 取出
city
,name
和age
三個欄位返回給客戶端- 其實,結果集只是一個 邏輯概念 ,MySQL服務端在sort buffer排序完成後,不會再耗費記憶體來儲存回表取回的內容
- 實際上,MySQL服務端從排序後的
sort buffer
中依次取出id,回表取回內容後, 直接返回給客戶端
觀察指標
-- 採用外部排序 + rowid排序 SET sort_buffer_size=32768; SET max_length_for_sort_data=16;
慢查詢日誌
# Time: 2019-02-10T08:23:59.068672Z # User@Host: root[root] @ localhost []Id:8 # Query_time: 0.012047Lock_time: 0.000479 Rows_sent: 1000Rows_examined: 5000 SET timestamp=1549787039; SELECT city,name,age FROM t FORCE INDEX(city) WHERE city='杭州' ORDER BY name LIMIT 1000;
OPTIMIZER_TRACE
"filesort_information": [ { "direction": "asc", "table": "`t` FORCE INDEX (`city`)", "field": "name" } ], "filesort_priority_queue_optimization": { "limit": 1000 }, ... "filesort_summary": { "memory_available": 32768, "key_size": 36, "row_size": 36, "max_rows_per_buffer": 910, "num_rows_estimate": 16912, "num_rows_found": 4000, "num_examined_rows": 4000, "num_initial_chunks_spilled_to_disk": 6, "peak_memory_used": 35008, "sort_algorithm": "std::stable_sort", "unpacked_addon_fields": "max_length_for_sort_data", "sort_mode": "<fixed_sort_key, rowid>" }
-
num_initial_chunks_spilled_to_disk
,9->6,說明外部排序所需要的 臨時檔案變少 了 -
sort_mode
含有的rowid
:採用 rowid排序 -
num_examined_rows=4000
: 參與排序的行數
掃描行數
掃描的行數變成了5000行(多出了1000行是 回表 操作)
mysql> SELECT @b-@a; +-------+ | @b-@a | +-------+ |5000 | +-------+
全欄位排序 vs rowid排序
- MySQL只有在擔心由於 sort buffer太小而影響排序效率 的時候,才會考慮使用rowid排序,rowid排序的優缺點如下
- 優點:排序過程中, 一次排序可以排序更多的行
- 缺點:增加 回表 次數, 與LIMIT N成正相關
- MySQL如果認為
sort buffer
足夠大,會 優先選擇全欄位排序- 把需要的所有欄位都放到
sort buffer
,排序完成後 直接從記憶體返回查詢結果 , 無需回表 - 體現了MySQL的一個設 計思路
- 儘量使用記憶體,減少磁碟訪問
- 把需要的所有欄位都放到
- MySQL排序是一個比較 成本較高 的操作,進一步的優化方案: 聯合索引 、 覆蓋索引
- 目的: 移除
Using filesort
- 目的: 移除
優化方案
聯合索引
ALTER TABLE t ADD INDEX city_user(city, name);
city_user索引樹
explain
mysql> EXPLAIN SELECT city,name,age FROM t FORCE INDEX(city_user) WHERE city='杭州' ORDER BY name LIMIT 1000\G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t partitions: NULL type: ref possible_keys: city_user key: city_user key_len: 50 ref: const rows: 4000 filtered: 100.00 Extra: Using index condition
-
Extra
裡面已經移除了Using filesort
,說明MySQL 不需要排序 操作了 - 聯合索引
city_user
本身就是 有序 的,因此無需將4000行都掃描一遍,只需要掃描滿足條件的前 1000 條記錄即可 -
Using index condition
:表示使用了 索引下推
執行過程
- 從 city_user索引樹 找到第一個滿足city=’杭州’的主鍵ID,即ID_X
- 然後拿著ID_X 回表 取出整行,取
city
、name
和age
三個欄位的值,作為結果集的一部分 直接返回給客戶端 - 繼續取 city_user索引樹 的下一條記錄,重複上述步驟,直到查到1000條記錄或者不滿足city=’杭州’時結束迴圈
- 這個過程 不需要排序 (當然也不需要外部排序用到的 臨時檔案 )
觀察指標
慢查詢日誌
Rows_examined
為1000, Query_time
為上面全欄位排序(內部排序)的情況耗時的 49%
278 # Time: 2019-02-10T09:00:28.956622Z 279 # User@Host: root[root] @ localhost []Id:8 280 # Query_time: 0.003652Lock_time: 0.000569 Rows_sent: 1000Rows_examined: 1000 281 SET timestamp=1549789228; 282 SELECT city,name,age FROM t FORCE INDEX(city_user) WHERE city='杭州' ORDER BY name LIMIT 1000;
掃描行數
mysql> SELECT @b-@a; +-------+ | @b-@a | +-------+ |1000 | +-------+
覆蓋索引
覆蓋索引:索引上的資訊 足夠滿足查詢需求 , 無需再回表 ,但維護索引是有代價的,需要權衡
ALTER TABLE t ADD INDEX city_user_age(city, name, age);
explain
Using index
:表示使用 覆蓋索引
mysql> EXPLAIN SELECT city,name,age FROM t FORCE INDEX(city_user_age) WHERE city='杭州' ORDER BY name LIMIT 1000\G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t partitions: NULL type: ref possible_keys: city_user_age key: city_user_age key_len: 50 ref: const rows: 4000 filtered: 100.00 Extra: Using where; Using index
執行過程
- 從 city_user_age索引樹 找到第一個滿足city=’杭州’的記錄
- 直接取出
city
、name
和age
這三個欄位的值,作為結果集的一部分 直接返回給客戶端
- 直接取出
- 繼續取 city_user_age索引樹 的下一條記錄,重複上述步驟,直到查到1000條記錄或者不滿足city=’杭州’時結束迴圈
觀察指標
慢查詢日誌
Rows_examined
同樣為1000, Query_time
為上面使用聯合索引 city_user
耗時的 49%
# Time: 2019-02-10T09:16:20.911513Z # User@Host: root[root] @ localhost []Id:8 # Query_time: 0.001800Lock_time: 0.000366 Rows_sent: 1000Rows_examined: 1000 SET timestamp=1549790180; SELECT city,name,age FROM t FORCE INDEX(city_user_age) WHERE city='杭州' ORDER BY name LIMIT 1000;
掃描行數
mysql> SELECT @b-@a; +-------+ | @b-@a | +-------+ |1000 | +-------+
in語句優化
假設已有聯合索引city_user(city,name),查詢語句如下
SELECT * FROM t WHERE city IN ('杭州','蘇州') ORDER BY name LIMIT 100;
單個city內部,name是遞增的,但在匹配多個city時,name就不能保證是遞增的,因此這個SQL語句 需要排序
explain
依然有 Using filesort
mysql> EXPLAIN SELECT * FROM t FORCE INDEX(city_user) WHERE city IN ('杭州','蘇州') ORDER BY name LIMIT 100\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t partitions: NULL type: range possible_keys: city_user key: city_user key_len: 50 ref: NULL rows: 4001 filtered: 100.00 Extra: Using index condition; Using filesort mysql> EXPLAIN SELECT * FROM t FORCE INDEX(city_user) WHERE city IN ('杭州') ORDER BY name LIMIT 100\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t partitions: NULL type: ref possible_keys: city_user key: city_user key_len: 50 ref: const rows: 4000 filtered: 100.00 Extra: Using index condition
解決方案
- 拆分語句,包裝在同一個事務
-
SELECT * FROM t WHERE city='杭州' ORDER BY name LIMIT 100;
:不需要排序,客戶端用一個 記憶體陣列A 儲存結果 -
SELECT * FROM t WHERE city='蘇州' ORDER BY name LIMIT 100;
:不需要排序,客戶端用一個 記憶體陣列B 儲存結果 - 記憶體陣列A和記憶體陣列B 均為有序陣列 ,可以採用 記憶體中的歸併排序