TRUNCATE 語句到底因何而慢?

作者通過源碼分析 truncate 語句形成慢 SQL 的原因和解決方案,並與 MySQL 5.7 就相關實現邏輯進行對比。

作者:李錫超

一個愛笑的江蘇蘇寧銀行 數據庫工程師,主要負責數據庫日常運維、自動化建設、DMP 平臺運維。擅長 MySQL、Python、Oracle,愛好騎行、研究技術。

本文來源:原創投稿

  • 愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。

問題現象

收到反饋某測試環境執行批量操作時,有 truncate 語句存在於慢查詢日誌中。擔心上線後可能影響數據庫,請求 DBA 配合分析。

關鍵配置

配置項 說明
數據庫版本 MySQL 5.7
參數 long_query_time <br/> 慢查詢閾值,單位爲秒 0.1(100 毫秒)
參數 innodb_adaptive_hash_index ON

問題分析總結

總結下來主要有如何幾個問題:

Q1: TRUNCATE 語句是如何執行的?fd 句柄不變化?爲什麼執行時間長?

TRUNCATE 語句如何執行?

關鍵堆棧:

關鍵操作 debug:

爲什麼執行時間長?

從以上堆棧可以看到,耗時過程主要是 row_drop_table_for_mysqlos_file_delete_func

其中:row_drop_table_for_mysql 主要是調用 btr_drop_ahi_for_table 執行 AHI 的 page 頁的刪除。os_file_delete_func 主要調用 unlink 執行文件的清理。

句柄爲什麼不變化?

假如需要 truncate 的表分配的 fd 爲 43,truncate 過程中,會先將表 rename。這個時候這個 fd 會被關閉,43 就被釋放了。然後執行 create table 操作。一般這個間隙過程很短,因此新建立的表可以使用被釋放的 43 了,所以會看到 fd 沒有變化。

如果 rename 之後,在內部執行 create table 之前,又打開了新文件,那這時候 fd 43 就會被其它打開的文件持有,truncate 之後表的 fd 也就會發生變化。

注意:MySQL 8.0 是真正使用 rename + create + drop實現的 truncate,但 MySQL 5.7 是通過文件的 truncate 實現的。

Q2: 如何分析 TRUNCATE 慢的問題?

方式一:慢日誌?

只能看到慢的結果,無法確認原因。

方式二:執行計劃?

不支持 truncate 語句。

方式三:profile

profile 結果來看,對於 truncate 語句,只能看到耗時過程都在System lock 上,無法看到更近一步的原因。

方式四:DEBUG

// 推薦設置
// 其中 T 其實是 MySQL 支持(在 trace 中打印時間)的,但官方文檔中缺少了說明。已提交bug說明:Bug #111174

set global debug='d:t:T:i:n:N:o,/tmp/debug_3306.trace.f';
set global debug='';

  • ① 表示 show processlist 的線程 ID
  • ② 執行時間
  • ③ 函數調用層級
  • ④ 函數名稱

MySQL 8.0 切換對比

// TRUNCATE
// 默認規範配置
// innodb_flush_method = on & innodb_flush_method = O_DIRECT
([email protected]) [eolbimsdb] 08:44:46 15> truncate table t5;
Query OK, 0 rows affected (0.98 sec)

// 設置 innodb_adaptive_hash_index = off
([email protected]) [eolbimsdb] 08:52:03 5> truncate table t5;
Query OK, 0 rows affected (0.03 sec)

// 設置 innodb_flush_method = fsync
([email protected]) [eolbimsdb] 09:03:34 28> truncate table t5;
Query OK, 0 rows affected (1.04 sec)

// 設置  innodb_adaptive_hash_index = off & innodb_flush_method = fsync
([email protected]) [eolbimsdb] 09:20:24 5> truncate table t5;
Query OK, 0 rows affected (0.22 sec)


// DROP
// 默認規範配置
// innodb_flush_method = on & innodb_flush_method = O_DIRECT
([email protected]) [eolbimsdb] 10:05:41 9> drop table t5;
Query OK, 0 rows affected (0.94 sec)


// 設置 innodb_adaptive_hash_index = off & innodb_flush_method = O_DIRECT
([email protected]) [eolbimsdb] 09:44:24 5> drop table t5;
Query OK, 0 rows affected (0.01 sec)

// 設置 innodb_flush_method = on & innodb_flush_method = fsync
([email protected]) [eolbimsdb] 09:32:15 13> drop table t5;
Query OK, 0 rows affected (1.13 sec)

// 設置  innodb_adaptive_hash_index = off & innodb_flush_method = fsync
([email protected]) [eolbimsdb] 09:25:10 14> drop table t5;
Query OK, 0 rows affected (0.19 sec)

Q3: 能否優化?慢在哪裏?post_ddl 如何調用?

從 Q1 的結果中可以看出,執行的主要耗時在 row_drop_table_for_mysqlos_file_delete_func

MySQL 8.0 的優化措施

  1. row_drop_table_for_mysql 慢的問題,可以通過設置 innodb_adaptive_hash_index = off 進行優化;
  2. os_file_delete_func 慢的問題,可以設置 innodb_flush_method = O_DIRECT 或者配置表的 HARD LINK 進行優化。

MySQL 5.7 的優化措施

詳見後面 3-Q1、3-Q4 部分。

post_ddl 如何調用?

MySQL 8.0 引入了 scope guard 功能:當定義了 scope guard 之後,會創建 Scope_guard 對象。正常情況下,當執行 return 操作前,會執行 scope guard 定義的邏輯。除非在函數結束前執行 Scope_guard 對象的 commit 操作。文件的刪除功能實在 scope guard 的 cleanup_base 階段調用是現的。

Q4: 生產執行 TRUNCATE 是否存在隱患?

從實現機制來看,主要有以下風險:

IO 壓力

當觸發 truncate 操作後,需要在短時間由數據庫線程將文件 unlinktruncate,如果被處理的文件很大,服務器的 IO 壓力可能會影響正常的數據庫請求。

內存併發

在執行 truncatedrop 的過程中,由於需要對內存的數據進行清理,特別是對 LRU 和 flush_LRU 進行掃描,並釋放對應的數據塊。這個過程是需要逐個根據 buffer pool instance 獲取 mutex 資源的。如果在業務高峯期,特別是 buffer pool 較大時,可能會影響正常的業務情況。

同時,執行 create drop table 操作時需要 dict_operation_lock 的 X 鎖(RW_X_LATCH),而一些其他後臺線程,比如 Main Thread 檢查 dict cache 時,也需要獲取 dict_operation_lock 的 X 鎖,因此被阻塞。然後用戶線程可能由於獲取不到鎖而處於掛起狀態,當無法立刻獲得鎖時。更多參考:《Drop Table 對 MySQL 的性能影響分析》

Q5: 不同版本對於 TRUNCATE 的實現是否存在差異?

通過對比 2-Q1 與 3-Q4:

MySQL 8.0 的 truncate 實現方式基本和 drop 實現方式相同,包括主要的耗時位置(都在 row_drop_table_for_mysqlos_file_delete_func)都是相同的。

MySQL 5.7 的 truncatedrop 實現差異較大,整個實現過程幾乎是完全獨立的代碼。truncate 使用 row_truncate_table_for_mysqldrop 使用 row_drop_table_for_mysqltruncate 操作的主要的耗時有 dict_drop_index_treeos_file_truncate

DROP TABLE 優化失敗分析

下面來看一個 MySQL 5.7 測試環境上線 DROP TABLE 優化方案失敗問題。

Q1:上線爲什麼會失敗?HARD LINK 爲什麼不生效?AHI 爲什麼不生效?

  • 當 MySQL 5.7 使用規範配置啓動時,從 debug-trace 過程來看,在row_drop_single_table_tablespacerow_drop_table_from_cache 函數執行期間根本沒有耗時,所以實施優化方案後,沒有效果;
  • 耗時的過程在 que_eval_sql: query: PROCEDURE DROP_TABLE_PROC ---> dict_drop_index_tree
  • row_drop_single_table_tablespace 的耗時被 MySQL 5.7 配置innodb_flush_method=O_DIRECT 優化了。

Q2:該優化是否適用於 MySQL 8.0?

設置 innodb_flush_method=O_DIRECT 的優化操作,同樣適用於 MySQL 8.0。

Q3:MySQL 8.0 如何解決 DROP TABLE 時執行 DROP_TABLE_PROC 慢的問題?

  • WL#9536: InnoDB_New_DD: Support crash-safe DDL;
  • 依賴於自 Version 8.0.3 的 NEW DD;
  • 整個 drop 慢的 que_eval_sqlDROP_TABLE_PROC 被整體砍掉;
  • 包括 dict_drop_index_tree 在內的整個函數,都被砍了;
  • 具體實現機制,參考分析 NEW DD 實現方法。

Q4:MySQL 5.7 DROP TABLE 和 TRUNCATE 在實現機制、優化措施有何區別呢?

  • 執行 truncate 操作的耗時,仍然是在 dict_drop_index_treeos_file_truncate 這兩個階段;
  • os_file_truncate 的耗時:可以通過設置 innodb_flush_method=O_DIRECT 時間進行優化(不可以通過 hard link 進行優化);
  • dict_drop_index_tree 的耗時,暫時沒有優化思路。瞭解更多:InnoDB 文件系統之文件物理結構

Q5:5.7 慢查詢爲什麼有時記錄 TRUNCATE 執行慢,有時不記錄?

根據源碼,MySQL 是否記錄慢查詢判斷時,主要有兩個維度:一個是執行時間(不包括 utime_alter_lock);一個是執行掃描的行數,並對特殊的語句(如 call)進行了忽略。對於 truncate 操作而言,無論執行時間是多少,掃描行數都是 0。當配置了 min_examined_row_limit 大於 0 之後,一般 truncate 操作由於不滿足該條件,都不會被記錄到慢查詢。

但是當 truncate 操作位於存儲過程中時,在 truncate 操作之前有其它 DML 操作(如 insert selecct),這時候由於位於同一個 THD 下,在 MySQL 5.7 版本里面 thd->get_examined_row_count() 返回的結果其實是上一個 DML 語句的(這裏應該是缺陷)。如果此時 truncate 操作的執行時間又超過了 long_query_time,那麼此時這個 truncate 語句就會被記錄慢查詢。

同時,在 MySQL 8.0 針對 call 的語句,將不在單獨記錄記錄的語句。而是記錄爲統一的 call 語句裏面。需要看存儲過程裏面的語句執行情況,可以用 show profiles 查看。

慢查詢記錄堆棧

測試存儲過程

DROP PROCEDURE truncate_test;
DELIMITER //
CREATE PROCEDURE truncate_test()
BEGIN
  insert into t1 select * from t1_bak;
  truncate table t1;
END
//
DELIMITER ;

call truncate_test();

mysql> call truncate_test();
Query OK, 0 rows affected (1 min 59.58 sec)
# Time: 2023-06-08T00:28:30.969993+08:00
# User@Host: root[root] @ localhost []  Id:     2
# Schema: db2  Last_errno: 0  Killed: 0
# Query_time: 119.177518  Lock_time: 0.000233  Rows_sent: 0  Rows_examined: 131072  Rows_affected: 131072
# Bytes_sent: 0
# Stored_routine: db2.truncate_test
use db2;
SET timestamp=1686155310;
insert into t1 select * from t1_bak;
# Time: 2023-06-08T00:28:31.375873+08:00
# User@Host: root[root] @ localhost []  Id:     2
# Schema: db2  Last_errno: 0  Killed: 0
# Query_time: 0.405734  Lock_time: 0.003310  Rows_sent: 0  Rows_examined: 131072  Rows_affected: 0
# Bytes_sent: 0
# Stored_routine: db2.truncate_test
SET timestamp=1686155311;
truncate table t1;

與 MySQL 8.0 對比

mysql> call truncate_test();
Query OK, 0 rows affected (2 min 28.51 sec)

# Time: 2023-06-07T17:18:39.215632Z
# User@Host: root[root] @ localhost []  Id:     8
# Query_time: 148.516478  Lock_time: 0.000372 Rows_sent: 0  Rows_examined: 172032
use testdb;
SET timestamp=1686158318;
call truncate_test();

MySQL 8.0 如何跟蹤?

mysql> call truncate_test();
Query OK, 0 rows affected (2 min 24.84 sec)

mysql> 
mysql> 
mysql> show profiles;
+----------+--------------+-----------------------------------------+
| Query_ID | Duration     | Query                                   |
+----------+--------------+-----------------------------------------+
|        1 | 144.55113600 | insert into ltb2 select * from ltb3_bak |
|        2 |   0.29312375 | truncate table ltb2                     |
+----------+--------------+-----------------------------------------+
2 rows in set, 1 warning (0.00 sec)

以上包括 truncate 執行慢的分析,如針對細節有任何疑問和建議,歡迎留言交流。

關於 SQLE

愛可生開源社區的 SQLE 是一款面向數據庫使用者和管理者,支持多場景審覈,支持標準化上線流程,原生支持 MySQL 審覈且數據庫類型可擴展的 SQL 審覈工具。

SQLE 獲取

類型 地址
版本庫 https://github.com/actiontech/sqle
文檔 https://actiontech.github.io/sqle-docs/
發佈信息 https://github.com/actiontech/sqle/releases
數據審覈插件開發文檔 https://actiontech.github.io/sqle-docs-cn/3.modules/3.7_auditplugin/auditplugin_development.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章