4.1 Query Profiling用法詳解
Query Profiling是MySQL數據庫提供的一種SQL性能診斷工具, 本節通過對Profiling的簡介,原理,使用方法以及案例的介紹,讓讀者學會如何在SQL優化的過程中,合理的去使用它。
4.1.1 Query Profiling簡介
Query Profiling是MySQL數據庫中提供的一種SQL性能診斷方法,用戶可以在開啓profiling的情況下,查看當前會話執行SQL的時間消耗分佈,CPU用戶時間和系統時間,以及涉及到的關鍵函數,所在的源代碼的文件和行數等等。
4.1.2 開啓和設置profiling
和Query Profiling相關的參數以及他們的含義如下:
- have_profiling 表示此數據庫是否支持profiling,YES表示支持,NO表示不支持。
- profiling 此參數表示當前是否開啓了profiling的功能,on表示開啓,off表示關閉,默認爲關閉狀態。
- profiling_history_size 表示保留當前會話多少條SQL的 profile的記錄,默認爲15條。
通過如下命令開啓profiling功能
set session profiling=on;
切記不要在生產環境使用
set global profiling=on
,這會打開全局的profiling設置,會對生成環境數據庫的性能造成很大的影響。
然後執行如下SQL:
mysql> select * from order_list order by insert_time limit 1;
+----+----------+---------------------+--------------+
| id | order_id | insert_time | order_detail |
+----+----------+---------------------+--------------+
| 1 | 1 | 2019-05-03 21:46:00 | |
+----+----------+---------------------+--------------+
1 row in set (2.16 sec)
查看當前會話歷史的profiles信息
mysql> show profiles;
+----------+------------+-------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+-------------------------------------------------------+
| 1 | 2.15071400 | select * from order_list order by insert_time limit 1 |
+----------+------------+-------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
查看id爲1的SQL的詳細profile信息
mysql> show profiles;
+----------+------------+-------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+-------------------------------------------------------+
| 1 | 2.15071400 | select * from order_list order by insert_time limit 1 |
+----------+------------+-------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show profile for query 1;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000050 |
| checking permissions | 0.000006 |
| Opening tables | 0.000012 |
| init | 0.000014 |
| System lock | 0.000006 |
| optimizing | 0.000004 |
| statistics | 0.000008 |
| preparing | 0.000024 |
| Sorting result | 0.000003 |
| executing | 0.000002 |
| Sending data | 0.000006 |
| Creating sort index | 2.150458 |
| end | 0.000014 |
| query end | 0.000007 |
| closing tables | 0.000007 |
| freeing items | 0.000024 |
| logging slow query | 0.000057 |
| cleaning up | 0.000014 |
+----------------------+----------+
18 rows in set, 1 warning (0.01 sec)
此時,數據庫內部執行此SQL語句的每一個階段的時間消耗都能看到,然後可以針對耗時較長的地方進行分析了。比如這條SQL的時間消耗主要在Creating sort index階段,證明此SQL沒有使用索引排序。
使用profiling還可以查看各個階段的IO情況,如下:
mysql> show profile BLOCK IO for query 1;
+----------------------+----------+--------------+---------------+
| Status | Duration | Block_ops_in | Block_ops_out |
+----------------------+----------+--------------+---------------+
| starting | 0.000050 | 0 | 0 |
| checking permissions | 0.000006 | 0 | 0 |
| Opening tables | 0.000012 | 0 | 0 |
| init | 0.000014 | 0 | 0 |
| System lock | 0.000006 | 0 | 0 |
| optimizing | 0.000004 | 0 | 0 |
| statistics | 0.000008 | 0 | 0 |
| preparing | 0.000024 | 0 | 0 |
| Sorting result | 0.000003 | 0 | 0 |
| executing | 0.000002 | 0 | 0 |
| Sending data | 0.000006 | 0 | 0 |
| Creating sort index | 2.150458 | 124224 | 0 |
| end | 0.000014 | 0 | 0 |
| query end | 0.000007 | 0 | 0 |
| closing tables | 0.000007 | 0 | 0 |
| freeing items | 0.000024 | 0 | 0 |
| logging slow query | 0.000057 | 0 | 8 |
| cleaning up | 0.000014 | 0 | 0 |
+----------------------+----------+--------------+---------------+
18 rows in set, 1 warning (0.00 sec)
如果讀者對MySQL源代碼感興趣,想了解某一個階段究竟是如何執行的,可以通過如下命令,查看某個階段對應的源碼文件,行號,函數名稱信息,如下
mysql> show profile SOURCE for query 1;
+----------------------+----------+-----------------------+----------------------+-------------+
| Status | Duration | Source_function | Source_file | Source_line |
+----------------------+----------+-----------------------+----------------------+-------------+
| starting | 0.000050 | NULL | NULL | NULL |
| checking permissions | 0.000006 | check_access | sql_authorization.cc | 802 |
| Opening tables | 0.000012 | open_tables | sql_base.cc | 5714 |
| init | 0.000014 | handle_query | sql_select.cc | 121 |
| System lock | 0.000006 | mysql_lock_tables | lock.cc | 323 |
| optimizing | 0.000004 | optimize | sql_optimizer.cc | 151 |
| statistics | 0.000008 | optimize | sql_optimizer.cc | 367 |
| preparing | 0.000024 | optimize | sql_optimizer.cc | 475 |
| Sorting result | 0.000003 | make_tmp_tables_info | sql_select.cc | 3842 |
| executing | 0.000002 | exec | sql_executor.cc | 119 |
| Sending data | 0.000006 | exec | sql_executor.cc | 195 |
| Creating sort index | 2.150458 | sort_table | sql_executor.cc | 2614 |
| end | 0.000014 | handle_query | sql_select.cc | 199 |
| query end | 0.000007 | mysql_execute_command | sql_parse.cc | 4946 |
| closing tables | 0.000007 | mysql_execute_command | sql_parse.cc | 4998 |
| freeing items | 0.000024 | mysql_parse | sql_parse.cc | 5610 |
| logging slow query | 0.000057 | log_slow_do | log.cc | 1713 |
| cleaning up | 0.000014 | dispatch_command | sql_parse.cc | 1924 |
+----------------------+----------+-----------------------+----------------------+-------------+
18 rows in set, 1 warning (0.00 sec)
要想在編譯數據庫期間就禁用掉profiling的功能,可以在cmake階段添加如下選項
-DENABLED_PROFILING=OFF ##此選項默認爲ON
此後編譯得到的數據庫版本就不再包含profiling的功能。
查看數據庫profiling相關的變量,將會看到如下結果
mysql> show global variables like '%profiling%';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| have_profiling | NO |
+----------------+-------+
1 row in set (0.01 sec)
have_profiling爲NO表示此數據庫版本中已經沒有了profiling的功能。這就是通過源碼編譯MySQL數據庫的優點所在,通過在編譯期間的參數控制,減少運行期間的各種判斷,提高數據庫的運行效率。
4.1.3 Query Profiling的原理
本節將會帶領讀者深入MySQL源代碼,用來簡單介紹Query Profiling在MySQL中是如何工作的。
最主要完成Query Profiling工作的類有如下三個,這部分代碼集中在sql_profile.cc中,涉及到如下幾個類。
class PROF_MEASUREMENT;
class QUERY_PROFILE;
class PROFILING;
class THD;
其中跟用戶線程交互的類爲PROFILING,和MySQL數據庫中的主要類THD之間的關係爲
[外鏈圖片轉存失敗(img-8xHl1npn-1562559590686)(http://note.youdao.com/yws/res/12428/0A078E0DE3C84088BD9F2ABD8E9C6E49)]
THD類通過調用PROFILING類的公有成員函數,來實現Query Profiling的功能。
在用戶連接MySQL數據庫,做THD的初始化時,會調用PROFILING::set_thd(THD *thd_arg),這個函數只做了一件事情,就是設置私有成員指針變量thd
inline void set_thd(THD *thd_arg) { thd= thd_arg; };
完整的線程函數調用堆棧如下
::pfs_spawn_thread(void *)
::handle_connection(void *)
init_new_thd(Channel_info*)
Channel_info_local_socket::create_thd()
Channel_info::create_thd()
THD::THD(bool)
THD::THD(bool)
PROFILING::set_thd(THD*)
在用戶下發SQL命令後,會調用PROFILING::start_new_query(char const*)函數去開始一個新的SQL profile信息的記錄:
void PROFILING::start_new_query(const char *initial_state)
{
DBUG_ENTER("PROFILING::start_new_query");
/* 除非人爲的修改了代碼,不然不可能出現這種情況 */
if (unlikely(current != NULL))
{
DBUG_PRINT("warning", ("profiling code was asked to start a new query "
"before the old query was finished. This is "
"probably a bug."));
finish_current_query();
}
//判斷用戶線程變量的profiling是否開啓,如果開啓的話,enabled則爲true,否則爲false
enabled= ((thd->variables.option_bits & OPTION_PROFILING) != 0);
//如果爲false,也就是說用戶沒有開啓profiling的功能,則直接返回
if (! enabled) DBUG_VOID_RETURN;
DBUG_ASSERT(current == NULL);
//否則初始化current,關於QUERY_PROFILE類的構造函數,讀者可以自行查閱。通過上文的介紹中
//讀者應該清除,profiling是可以記錄當前session多條SQL的profile信息的,而current表示當
//執行SQL的profile。
current= new QUERY_PROFILE(this, initial_state);
//返回上層調用
DBUG_VOID_RETURN;
}
隨後,如果用戶輸入的命令類型爲COM_QUERY的話,則調用PROFILING::set_query_source(char const*, unsigned long)函數,設置此次記錄的profile信息對應的SQL語句,如下:
void PROFILING::set_query_source(const char *query_source_arg, size_t query_length_arg)
{
DBUG_ENTER("PROFILING::set_query_source");
//如果用戶沒有開啓profiling,則不做任何操作,直接返回
if (! enabled)
DBUG_VOID_RETURN;
//通過QUERY_PROFILE::set_query_source(char const*, unsigned long)保存當前用戶的SQL語句。這部分需要讀者自行展開閱讀。
if (current != NULL)
current->set_query_source(query_source_arg, query_length_arg);
else
DBUG_PRINT("info", ("no current profile to send query source to"));
DBUG_VOID_RETURN;
}
在設置完SQL內容之後,進入到SQL執行的其它不同階段中,通過PROFILING去記錄相應的時間消耗,對應的入口函數,IO信息,源代碼的位置等等。這裏不再列舉所有階段的信息獲取,只以權限檢測這個階段爲例,權限檢測對應show profile for query結果中的checking permissions。獲取這些信息通過調用函數PROFILING::status_change(char const*, char const*, char const*, unsigned int)來實現,THD通過THD::enter_stage函數來調用它。 具體的函數調用關係如下:
dispatch_command(THD*, COM_DATA const*, enum_server_command)
mysql_parse(THD*, Parser_state*)
mysql_execute_command(THD*, bool)
Opt_trace_start::Opt_trace_start(THD*, TABLE_LIST*, enum_sql_command, List<set_var_base>*, char const*, unsigned long, sp_printable*, charset_info_st const*)
Opt_trace_start::Opt_trace_start(THD*, TABLE_LIST*, enum_sql_command, List<set_var_base>*, char const*, unsigned long, sp_printable*, charset_info_st const*)
check_access(THD*, unsigned long, char const*, unsigned long*, st_grant_internal_info*, bool, bool)
THD::enter_stage(PSI_stage_info_v1 const*, PSI_stage_info_v1*, char const*, char const*, unsigned int)
PROFILING::status_change(char const*, char const*, char const*, unsigned int)
status_change函數實現:
void PROFILING::status_change(const char *status_arg,
const char *function_arg,
const char *file_arg, unsigned int line_arg)
{
DBUG_ENTER("PROFILING::status_change");
//status_arg表示當前的執行階段
if (status_arg == NULL)
DBUG_VOID_RETURN;
//表示當前profiling並沒有開啓,或者當前的Query Profile已經被丟棄。
if (current == NULL)
DBUG_VOID_RETURN;
//如果用戶開啓了profiling,則記錄上一個階段的消耗,分析見下文
if (unlikely(enabled))
current->new_status(status_arg, function_arg, file_arg, line_arg);
DBUG_VOID_RETURN;
}
函數QUERY_PROFILE::new_status(char const*, char const*, char const*, unsigned int)如下
void QUERY_PROFILE::new_status(const char *status_arg,
const char *function_arg, const char *file_arg,
unsigned int line_arg)
{
PROF_MEASUREMENT *prof;
DBUG_ENTER("QUERY_PROFILE::status");
DBUG_ASSERT(status_arg != NULL);
//根據參數的不同,調用不同的PROF_MEASUREMENT構造函數,在構造函數中,會去收集當前時間,以及通過系統函數getrusage,來獲取進程的相關資源信息,這其中包括了cpu的用戶和系統開銷時間,接收的信號量等。
if ((function_arg != NULL) && (file_arg != NULL))
prof= new PROF_MEASUREMENT(this, status_arg, function_arg, base_name(file_arg), line_arg);
else
prof= new PROF_MEASUREMENT(this, status_arg);
//遞增m_seq
prof->m_seq= m_seq_counter++;
//將獲取到的時間賦值給m_end_time_usecs,表示查詢截止時間。
m_end_time_usecs= prof->time_usecs;
//將此階段的相關信息加入到隊列entries中
entries.push_back(prof);
/* 如果隊列已經超過了MAX_QUERY_HISTORY的大小,則需要將隊首元素刪除 */
while (entries.elements > MAX_QUERY_HISTORY)
delete entries.pop();
//返回上層調用
DBUG_VOID_RETURN;
}
直到整個查詢的結束,最後調用到PROFILING::finish_current_query(),結束一次profiling操作。代碼如下:
void PROFILING::finish_current_query()
{
DBUG_ENTER("PROFILING::finish_current_profile");
if (current != NULL)
{
/* The last fence-post, so we can support the span before this. */
//記錄SQL執行最後一個階段消耗的時間
status_change("ending", NULL, NULL, 0);
if ((enabled) &&
((thd->variables.option_bits & OPTION_PROFILING) != 0) &&
(current->m_query_source.str != NULL) &&
(! current->entries.is_empty()))
{
//分配一個profiling_query_id,此值在當前線程內單調遞增。
current->profiling_query_id= next_profile_id();
//將當前查詢的profile信息添加到history隊列中
history.push_back(current);
last= current;
current= NULL;
}
else
{
delete current;
current= NULL;
}
}
/* 對應到參數profiling_history_size,如果隊列中元素個數超過了設置的大小,則剔除隊首元素*/
while (history.elements > thd->variables.profiling_history_size)
delete history.pop();
//返回上層調用。
DBUG_VOID_RETURN;
}
Query Profiling的工作原理大致如上所示,可見,在開啓profiling的情況下,增加了很多系統調用。讀者可以嘗試在開啓全局profiling的情況下,通過程序去壓測數據庫,然後通過perf工具去抓取mysqld進程的函數調用,可以發現如下圖所示的情況:
儘管profiling對性能影響較大,但是由於它使用方便,並且支持線程級別的設置,所以在做SQL診斷和優化時,還是可以發揮很大的作用的。