記一次關於 Mysql 中 text 類型和索引問題引發的慢查詢的定位及優化

最近有用戶反饋產品有些頁面加載比較慢,恰好我在學習 Mysql 相關知識,因此先從 Mysql 慢查詢日誌開始定位:前端

step1:經過慢查詢日誌定位具體 SQL

首先經過 SHOW VARIABLES like 查看當前 Mysql 服務器關於慢查詢的具體配置信息:mysql

slow_query_log = ON                  # 慢查詢日誌處於開啓狀態,因此能夠直接查詢
slow_query_type = 1                  # 根據運行時間將 SQL 語句中記錄到 slow log 中,而不考慮邏輯 IO 次數
long_query_time = 5.000000           # 凡是超過 5 秒以上的 SQL 都會記錄到 slow log 中
log_output = TABLE                   # slow log 記錄到 mysql.slow_log 表中
log_queries_not_using_indexes = OFF  # 沒有使用索引的 SQL 不會記錄到 slow_log 中,恰好咱們只關心查詢時間慢的 SQL
複製代碼

確認了 Mysql 服務器對慢查詢的配置知足需求,咱們不須要再修改任何配置,直接抓取對應時間點的慢查詢日誌:算法

-- 查看7月1日從9點半到10點半的slow log,並找出每條慢查詢SQL的最長查詢時間以及查詢次數,並按照查詢時間排序
SELECT
	db,
	start_time,
	max(query_time) AS max_query_time,
	CONVERT (sql_text USING utf8) AS sqlText,     -- sql_text 是 blob 類型,咱們須要 CONVERT 到 varchar 來識別具體 SQL
	count(1) AS count
FROM
	mysql.slow_log
WHERE
	start_time > "2019-07-01 09:30:00.000000"
AND start_time < "2019-07-01 10:30:00.000000"
GROUP BY
	sql_text
ORDER BY
	max_query_time DESC
複製代碼

最終咱們找到了服務器上四條不一樣的 slow log sql,最長查詢時間分別是 9秒,8秒,7秒,6秒:sql

image

step2:使用 explain 分析 SQL 執行計劃

恰好上週末寫了一篇 使用 explain 優化你的 mysql 性能,能夠直接上手,先對第一條 SQL 做分析:數據庫

mysql> EXPLAIN SELECT
	t.*, p.id AS projectId
FROM
	table_extract t
LEFT JOIN data_connection dc ON dc.id = t.data_connection_id
LEFT JOIN project p ON p.id = dc.project_id
WHERE
	p.id IN (
		700201361,
		700201360,
		700201359,
		700201358,
		700201357,
		700201356,
		700201354,
		700201353,
		700201351,
		700201350,
		700201347
	);
+----+-------------+-------+------------+--------+---------------------------------------------------------+---------+---------+------------------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys                                           | key     | key_len | ref                          | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+---------------------------------------------------------+---------+---------+------------------------------+------+----------+-------------+
| 1  | SIMPLE      | t     | NULL       | ALL    | NULL                                                    | NULL    | NULL    | NULL                         | 2159 | 100.00   | NULL        |
| 1  | SIMPLE      | dc    | NULL       | eq_ref | PRIMARY,index_data_connection_project_id,idx_project_id | PRIMARY | 4       | youdata.t.data_connection_id | 1    | 100.00   | Using where |
| 1  | SIMPLE      | p     | NULL       | eq_ref | PRIMARY                                                 | PRIMARY | 4       | youdata.dc.project_id        | 1    | 100.00   | Using index |
+----+-------------+-------+------------+--------+---------------------------------------------------------+---------+---------+------------------------------+------+----------+-------------+
3 行於數據集 (0.05 秒)
複製代碼

經過上述輸出結果沒發現什麼大的問題,兩次關聯查詢都使用了 type = eq_ref,而且都使用了索引,只是對於 table_extract 這張表的查詢數據庫走了全表掃描,這個確實沒辦法,咱們須要獲取該表中除了索引之外的其它字段,可是這張表的數據量也只有rows=2159行,因此理論上也不會有問題,因此這條 SQL 經過 explain 沒有發現什麼大問題,後面會繼續分析。緩存

接下來再看第二條 SQL:bash

mysql> EXPLAIN SELECT
	date(create_time) AS days,
	count(create_time) AS dayView
FROM
	resource_operation_record
WHERE
	resource_type IN ('NEW_REPORT', 'COCKPIT')
AND `action` = 'VIEW'
AND resource_id = 4539
AND create_time > '2019-06-25 00:00:00'
AND create_time < '2019-07-01 09:45:19'
GROUP BY
	days;
+----+-------------+---------------------------+------------+------+-----------------+------+---------+------+---------+----------+----------------------------------------------+
| id | select_type | table                     | partitions | type | possible_keys   | key  | key_len | ref  | rows    | filtered | Extra                                        |
+----+-------------+---------------------------+------------+------+-----------------+------+---------+------+---------+----------+----------------------------------------------+
| 1  | SIMPLE      | resource_operation_record | NULL       | ALL  | resource_id_idx | NULL | NULL    | NULL | 1729523 | 0.02     | Using where; Using temporary; Using filesort |
+----+-------------+---------------------------+------------+------+-----------------+------+---------+------+---------+----------+----------------------------------------------+
1 行於數據集 (0.05 秒)
複製代碼

首先 possible_keys 字段告訴咱們可能用到的索引 resource_id_idx,但是爲何 key 字段裏沒有真正用到索引呢?這應該是 Mysql 優化器認爲使用索引對該查詢優化空間不大,或者說可能會使性能更差。加上 Extra 字段裏還有 Using filesort,Using temporary,在將近 rows = 200萬 的數據裏進行全表掃描,查詢時間超過 5 秒再正常不過了。因此咱們查看一下索引信息來定位一下爲何沒有使用 resource_id_idx 索引:服務器

mysql> show index from resource_operation_record;
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table                     | Non_unique | Key_name        | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| resource_operation_record | 0          | PRIMARY         | 1            | id          | A         | 1646744     | NULL     | NULL   |      | BTREE      |         |               |
| resource_operation_record | 1          | creator_id_idx  | 1            | creator_id  | A         | 1169        | NULL     | NULL   | YES  | BTREE      |         |               |
| resource_operation_record | 1          | resource_id_idx | 1            | resource_id | A         | 4228        | NULL     | NULL   | YES  | BTREE      |         |               |
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
3 行於數據集 (0.04 秒)
複製代碼

resource_operation_record 這張表上一共三個索引,id 是自增主鍵,這個能夠先不用管。對於其它兩個索引 creator_id_idx 和 resource_id_idx,首先看到 Cardinality 這個值和 id 彙集索引差距好大,Cardinality 這個值表示索引中不重複的預估值,該值很關鍵,它和表中總行數比值越接近 1 越好,並且優化器會根據該值選擇是否使用索引優化,關於 InnoDB 索引和 Cardinality 相關內容能夠看 InnoDB 存儲引擎的索引和算法學習 這篇文章。resource_id_idx 的可選擇過小了,比例只有 0.0025,看來優化器不選擇該索引是正常的,因此咱們大部分狀況下要相信 Mysql 優化器。咱們也可使用 force index(resource_id_idx) 強制使用索引來觀察效果:網絡

SELECT
	date(create_time) AS days,
	count(create_time) AS dayView
FROM
	resource_operation_record
force index(resource_id_idx)  -- 使用 force index 強制使用索引
WHERE
 resource_id = 4539
AND	resource_type IN ('NEW_REPORT', 'COCKPIT')
AND `action` = 'VIEW'
AND create_time > '2019-06-25 00:00:00'
AND create_time < '2019-07-01 09:45:19'
GROUP BY
	days;
+------------+---------+
| days       | dayView |
+------------+---------+
| 2019-06-28 | 29      |
| 2019-06-29 | 2       |
| 2019-06-30 | 2       |
| 2019-07-01 | 5       |
+------------+---------+
4 行於數據集 (1.67 秒)
-- 查詢要 1.67 秒,相同狀況下,我不使用 force index 要 1.61 秒,比使用索引還要快,固然這個不一樣時間點查詢也有關係
-- 總之,使用索引確實沒有多大提高
複製代碼

再觀察上述查詢,咱們發現 select 和 where 條件中用到了 create_time,並且這個 create_time 是數據插入的時間,理論上不會有太多重複的,嘗試在 create_time 上建立索引:函數

-- 新建索引
ALTER TABLE `resource_operation_record` ADD INDEX `create_time_idx` USING BTREE (`create_time`);

-- 查看索引信息
show index from resource_operation_record;
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table                     | Non_unique | Key_name        | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| resource_operation_record | 0          | PRIMARY         | 1            | id          | A         | 1739371     | NULL     | NULL   |      | BTREE      |         |               |
| resource_operation_record | 1          | creator_id_idx  | 1            | creator_id  | A         | 1002        | NULL     | NULL   | YES  | BTREE      |         |               |
| resource_operation_record | 1          | resource_id_idx | 1            | resource_id | A         | 6988        | NULL     | NULL   | YES  | BTREE      |         |               |
| resource_operation_record | 1          | create_time_idx | 1            | create_time | A         | 1246230     | NULL     | NULL   | YES  | BTREE      |         |               |
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
4 行於數據集 (0.25 秒)

-- 查看執行計劃
mysql> EXPLAIN SELECT
	date(create_time) AS days,
	count(create_time) AS dayView
FROM
	resource_operation_record
WHERE
	resource_type IN ('NEW_REPORT', 'COCKPIT')
AND `action` = 'VIEW'
AND resource_id = 4539
AND create_time > '2019-06-25 00:00:00'
AND create_time < '2019-07-01 09:45:19'
GROUP BY
	days;
	
+----+-------------+---------------------------+------------+-------+---------------------------------+-----------------+---------+------+--------+----------+---------------------------------------------------------------------+
| id | select_type | table                     | partitions | type  | possible_keys                   | key             | key_len | ref  | rows   | filtered | Extra                                                               |
+----+-------------+---------------------------+------------+-------+---------------------------------+-----------------+---------+------+--------+----------+---------------------------------------------------------------------+
| 1  | SIMPLE      | resource_operation_record | NULL       | range | resource_id_idx,create_time_idx | create_time_idx | 5       | NULL | 210240 | 0.20     | Using index condition; Using where; Using temporary; Using filesort |
+----+-------------+---------------------------+------------+-------+---------------------------------+-----------------+---------+------+--------+----------+---------------------------------------------------------------------+
1 行於數據集 (0.19 秒)
複製代碼

創建索引的基礎上咱們再查看執行計劃和索引信息,create_time_idx 的 Cardinality 變爲 1246230,選擇性大於 0.7,優化器天然會選擇該索引,果真 explain 出來的結果是 type = range,使用了範圍索引查詢,而且 extra 裏增長了 Using index condition,表示會先條件過濾索引,過濾完索引後找到全部符合索引條件的數據行,隨後用 WHERE 子句中的其餘條件去過濾這些數據行。

最後執行查詢看一下優化後的效果,相比之前 1.7 秒,速度提高了 5 倍左右,這個優化到此爲止,由於這個表是對用戶訪問記錄的統計,後面能夠考慮針對時間分區進行優化。

SELECT
	date(create_time) AS days,
	count(create_time) AS dayView
FROM
	resource_operation_record
WHERE
 resource_id = 4539
AND	resource_type IN ('NEW_REPORT', 'COCKPIT')
AND `action` = 'VIEW'
AND create_time > '2019-06-25 00:00:00'
AND create_time < '2019-07-01 09:45:19'
GROUP BY
	days;
4 行於數據集 (0.35 秒)
複製代碼

接下來還有兩個慢查詢 SQL,這兩個慢查詢 SQL 和第一個 SQL 同樣經過 explain 輸出結果看不出什麼效果,因此接下來咱們經過 profile 查看這三個 SQL 性能

step3:使用 show profile 繼續定位

能夠經過文章 學習如何統計 Mysql 服務器狀態信息 來了解如何使用 SHOW STATUS,SHOW ENGINE INNODB STATUS,SHOW PROCESSLIST,SHOW PROFILE 來查看 Mysql 服務器狀態信息。

Mysql 5.1 版本開始支持 SHOW PROFILE 功能,它能夠高精度的記錄每一個查詢語句在運行過程當中各個操做的執行時間,這個功能可能會影響 Mysql 查詢性能,因此默認狀況下是關閉的,因爲咱們臨時定位問題,能夠短暫開啓該功能:

-- 開啓 profiling 功能
mysql> SET global profiling = ON;
Query OK, 0 rows affected, 1 warning (0.00 sec)

-- 執行第三條慢查詢 SQL
SELECT
	t.id AS id,
	t. NAME AS NAME,
	data_connection_id AS dataConnectionId,
	QUERY,
	init_sql AS initSql,
	t.produced AS produced,
	t.creator_id AS creatorId,
	t.create_time AS createTime,
	u.nick AS creatorName
FROM
	custom_table AS t
LEFT JOIN bigviz_user AS u ON t.creator_id = u.id
WHERE
	t.data_connection_id = 20;
800 行於數據集 (1.1 秒)	
	
-- 根據 show profiles 找到對應的 Query_Id = 8,對應執行時間爲 1.1 秒
show profiles

-- 具體查看每一步的耗時狀況
mysql> show profile for query 8;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000222 |
| checking permissions | 0.000030 |
| checking permissions | 0.000027 |
| Opening tables       | 0.000049 |
| init                 | 0.000062 |
| System lock          | 0.000035 |
| optimizing           | 0.000037 |
| statistics           | 0.000063 |
| preparing            | 0.000048 |
| executing            | 0.000025 |
| Sending data         | 1.101708 |
| end                  | 0.000090 |
| query end            | 0.000034 |
| closing tables       | 0.000088 |
| freeing items        | 0.000055 |
| logging slow query   | 0.000030 |
| Opening tables       | 0.000159 |
| System lock          | 0.000100 |
| cleaning up          | 0.000041 |
+----------------------+----------+
19 rows in set, 1 warning (0.00 sec)

複製代碼

經過 show profile 返回數據能夠發現,基本上全部的時間都花在了 「Sending data」 上,咱們查看 Mysql 官方文檔對 「Sending data」 的說明:

The thread is reading and processing rows for a SELECT statement, and sending data to the client. 
Because operations occurring during this state tend to perform large amounts of disk access (reads), 
it is often the longest-running state over the lifetime of a given query.
複製代碼

也就是說 「Sending data」 並非單純的發送數據,而是包括「收集 + 發送數據」,這個階段通常是 query 中最耗時的階段,那麼爲何這個只有 800 行的查詢會耗時這麼久呢,難道這 800 行中平均每行數據量都很大?因此看一下該表定義:

mysql> show create table custom_table\G;
*************************** 1. row ***************************
       Table: custom_table
Create Table: CREATE TABLE `custom_table` (
  `name` varchar(2000) DEFAULT NULL,
  `produced` varchar(255) DEFAULT 'UserDefinedSQL',
  `query` longtext,
  `project_id` int(11) DEFAULT NULL,
  `data_connection_id` int(11) DEFAULT NULL,
  `creator_id` int(11) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `modifier_id` int(11) DEFAULT NULL,
  `modify_time` datetime DEFAULT NULL,
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `init_sql` text COMMENT '初始化sql',
  `rely_list` text COMMENT '表依賴關係',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=27975 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
複製代碼

上述表結構定義中發現有三個字段 query,init_sql,rely_list 都是 text 類型字段,並且 query 字段仍是 longtext,從咱們懷疑出發,試着在 select 查詢中去掉 init_sql 和 query 的查詢後再觀察結果:

SELECT
	t.id AS id,
	t. NAME AS NAME,
	data_connection_id AS dataConnectionId,
	t.produced AS produced,
	t.creator_id AS creatorId,
	t.create_time AS createTime,
	u.nick AS creatorName
FROM
	custom_table AS t
LEFT JOIN bigviz_user AS u ON t.creator_id = u.id
WHERE
	t.data_connection_id = 20;
800 行於數據集 (0.04 秒)		
複製代碼

天哪,從 1 秒左右變成了 0.04 秒,徹底不是一個數量級的,看來 text 類型的字段對整個查詢影響太大了,咱們先不急追究爲何,先看如何在當前業務上優化查詢,因爲考慮到業務場景是前端獲取 custom_table 在某個 data_connection_id 下的列表,會返回 query,init_sql 這兩個字段,這個兩個字段用戶確實會用到,可是隻有在用戶點擊某個 custom_table 進行編輯或者查看詳情時纔會用到,那咱們爲何不考慮延遲獲取呢?只有當用戶須要查看詳情時再根據主鍵 ID 去獲取對應的信息,這個時候屬於 const 查詢且只有一行數據,代價很是小。

而對於第一個慢查詢 SQL,直接使用 select * 去查詢,這個表裏麪包好了多個 text 字段,而該業務需求其實只須要 id 和 used_memory(biginit) 兩個字段,因此咱們優化成只選擇其中兩個字段進行查詢。

對於第四個慢查詢 SQL,對應的表結構裏面也包含了一個 mediumtext 字段,前端界面上用戶須要根據該字段裏的文本信息進行搜索,可是該場景使用不多,只有當用戶切換到對應的」按照字段名稱搜索「時纔會用到該字段,默認狀況下不會用到該字段,因此咱們能夠在用戶切換到對應的搜索時再返回該字段,默認狀況下不返回便可。

針對以上三個慢查詢 SQL 咱們在不改變表結構的狀況下,經過修改業務處理邏輯都成功解決了問題,下面是對於 Mysql 在使用 text 和 blob 類型時以及索引查詢時的一些優化建議:

step4: 如何優化 Mysql 中 text 和 blob 類型:

什麼是行溢出數據?
  • InnoDB 會將一些大對象數據存放在數據頁以外的 BLOB 頁中,而後在查詢時根據指針去對應的 BLOB 頁中查詢。
  • 要不要將數據放在 BLOB 頁中,取決於當前頁中是否能夠存放下至少兩行數據,對於默認是 16 KB 大小的頁,這個閾值長度是 8098,大於該值的會存放在 BLOB 頁中。
  • BLOB 不僅存放 text 和 blob 類型,varchar 類型的數據也有可能被存放在 BLOB 頁中,而 blob 類型和 text 類型的數據也有可能不被存放在 BLOB 頁中。
  • 對於 Compact 和 Redundant 行存儲格式存放的數據,採用的是部分行溢出存儲,前 768 字節仍是會存放在當前數據頁中的。
  • 對於 Compressed 和 Dynamic 行存儲格式存放的數據,採用的徹底行溢出存儲,只用 20 個字節存放指針,其他全部數據都放在行溢出數據中。
爲何要儘可能少使用 text 和 blob 類型?
  • 首先對於 text 和 blob 類型,在遇到使用臨時表的狀況時,沒法使用內存臨時表,只能在磁盤上建立臨時表。
  • 對於行溢出數據,InnoDB 一次只會爲一個列分配一頁的空間,可是當該列超過 32 個頁後會一次性分配 64 個頁面,存儲空間有必定的浪費。
  • 行溢出數據禁用了自適應哈希索引,若是做爲 where 條件時必須完整的比較整個列。
  • 對於 text 和 blob 字段進行排序時,只能使用部分前綴進行排序,默認是 1024 字節,能夠經過 max_sort_length 進行設置。
  • 數據量太大,會致使 InnoDB 每一個數據頁中存放的行數減小,從而影響對頁面的緩存。
  • 若是存放在行溢出數據中,每次會根據指針去對應的溢出頁進行查詢,增長頁面訪問次數,並且每次查詢都是隨機 IO,text 字段越多查詢次數越多。
如何優化查詢?
  • 若是有許多大字段,能夠考慮合併這些字段到一個字段,存儲一個大的 200kb 比存儲 20 個 10kb 更高效,檢查隨機頁面訪問次數。
  • 查詢時儘可能避免對大字段查詢,尤爲是獲取列表時,杜絕使用 select * 查詢。
  • 能夠考慮將大字段專門放在另一張表中,只有在須要時再關聯查詢,增長 InnoDB 的當前表緩存命中率。
  • 若是隻須要獲取大字段的部分數據,可使用 SUBSTRING( ) 函數,這樣能夠避免使用磁盤臨時表。
  • 若是必須使用到磁盤臨時表,能夠考慮將磁盤臨時表指向在基於內存的文件系統中,能夠經過修改 tmpdir 參數實現。
  • 必要時能夠考慮對大字段進行壓縮後再存儲到表中。
  • 儘可能不要使用大字段做爲 where 中的查詢條件。

step5: 如何正確使用索引

  • 建立索引時儘可能選擇 Cardinality 值比較大的字段,你能夠經過 explain 觀察本身建立的索引到底有沒有被使用
  • order by 中的排序的列若是建了索引,則可使用直接索引進行排序,優化性能
  • 在使用索引時對應的索引列必須獨立,不能是表達式的一部分也不能是函數的參數,不然不能使用索引:
-- 雖然 id 上創建了索引,可是沒法使用索引優化
select id from user where id + 1 =5;
複製代碼
  • 當服務器出現多個列作 AND 操做查詢時,一般須要建了一個多列索引,而不是多個獨立的單列索引
  • 當不須要考慮排序和分組時,將選擇性最高的列放在前面一般是最好的,由於能夠很快的過濾出須要的行
  • 若是索引包含了須要查詢的全部字段值,那麼就是可使用覆蓋索引查詢,只須要讀取索引,極大地減小了數據訪問量,在 EXPLAIN 分析的 Extra 字段中能夠看到 「Using index」 信息
  • 若是查詢中某個列是範圍查詢,那麼其右邊的全部列將沒法使用索引優化,索引儘可能將範圍條件放在右邊或者使用多個等值條件來代替範圍查詢
  • 查詢時儘可能不要返回多餘的列,第一能夠減小網絡流量,第二增長使用覆蓋索引的可能性
  • 多列索引時只有當索引的列和 ORDER BY 子句的順序徹底一致且全部列的排序方向一致時才能使用索引作排序
  • 不要建立冗餘的索引,Mysql 不只須要單獨維護索引列,而且在優化器查詢時也須要逐個索引進行過濾,會影響性能,下面是建立冗餘索引的幾個例子:
- 建立了索引(A,B)再建立索引(A),那後者即是冗餘索引
- 建立索引擴展爲(A,ID),其中 ID 是主鍵,對於 InnoDB 來講主鍵已經包含在二級索引中了,因此這也是冗餘的
複製代碼
  • 有一些索引可能服務器永遠都不會用到,建議考慮刪除,在 percona 版本或 marida 中能夠經過 information_schea.index_statistics 查看獲得索引的使用狀況,在官方版本中 可使用 performance_schema.table_io_waits_summary_by_index_usage 查看索引使用狀況

參考文獻

相關文章
相關標籤/搜索