最近有用戶反饋產品有些頁面加載比較慢,恰好我在學習 Mysql 相關知識,因此先從 Mysql 慢查詢日誌開始定位:前端
首先經過 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
恰好上週末寫了一篇 使用 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 性能
能夠經過文章 學習如何統計 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 類型時以及索引查詢時的一些優化建議:
-- 雖然 id 上創建了索引,可是沒法使用索引優化
select id from user where id + 1 =5;
複製代碼
- 建立了索引(A,B)再建立索引(A),那後者即是冗餘索引
- 建立索引擴展爲(A,ID),其中 ID 是主鍵,對於 InnoDB 來講主鍵已經包含在二級索引中了,因此這也是冗餘的
複製代碼