MySQL8.0 · 優化器新特性 · Cost Model, 直方圖及優化器開銷優化

MySQL當前已經發布到MySQL8.0版本,在新的版本中,能夠看到MySQL以前被人詬病的優化器部分作了不少的改動,因爲筆者以前的工做環境是5.6,最近切換到最新的8.0版本,本文涵蓋了一些本人感興趣的和優化器相關的部分,主要包括MySQL5.7的cost model以及MySQL8.0的直方圖功能。html

本文基於當前最新的MySQL8.0.12版本,主要是講下cost model 和 histogram的用法和相關代碼mysql

Cost Model

Configurable cost constants

爲何須要配置cost model常量 ? 咱們知道MySQL已經發展了好幾十年的歷史,可是在優化器中依然使用了hardcode的權重值來衡量io, cpu等資源狀況,而這些權重值其實是基於多年前甚至十來年前的經驗設定的。想一想看,這麼多年硬件的發展多麼迅速。幾十上百個核心的服務器不在少數甚至在某些大型公司大規模使用,ssd早就成爲主流,NVME也在崛起。高速RDMA網絡正在走入尋常百姓家。這一切甚至影響到數據庫系統的實現和變革。顯而易見,那些hardcode的權值已通過時了,咱們須要提供給用戶可定義的方式,甚至更進一步的,可以智能的根據硬件環境自動設定。sql

MySQL5.7引入兩個新的系統表, 經過這兩個系統表暴露給用戶來進行更新,以下:數據庫

root@(none) 04:05:24>select * from mysql.server_cost;
+------------------------------+------------+---------------------+---------+---------------+
| cost_name                    | cost_value | last_update         | comment | default_value |
+------------------------------+------------+---------------------+---------+---------------+
| disk_temptable_create_cost   |       NULL | 2018-04-23 13:55:20 | NULL    |            20 |
| disk_temptable_row_cost      |       NULL | 2018-04-23 13:55:20 | NULL    |           0.5 |
| key_compare_cost             |       NULL | 2018-04-23 13:55:20 | NULL    |          0.05 |
| memory_temptable_create_cost |       NULL | 2018-04-23 13:55:20 | NULL    |             1 |
| memory_temptable_row_cost    |       NULL | 2018-04-23 13:55:20 | NULL    |           0.1 |
| row_evaluate_cost            |       NULL | 2018-04-23 13:55:20 | NULL    |           0.1 |
+------------------------------+------------+---------------------+---------+---------------+
6 rows in set (0.00 sec)

其中default_value是generated column,其表達式已經固定死了默認值:

`default_value` float GENERATED ALWAYS AS (
(case `cost_name` 
when _utf8mb3'disk_temptable_create_cost' then 20.0 
when _utf8mb3'disk_temptable_row_cost' then 0.5 
when _utf8mb3'key_compare_cost' then 0.05 
when _utf8mb3'memory_temptable_create_cost' then 1.0 
when _utf8mb3'memory_temptable_row_cost' then 0.1 
when _utf8mb3'row_evaluate_cost' then 0.1 else NULL end)) VIRTUAL

root@(none) 04:05:35>select * from mysql.engine_cost;
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+
| engine_name | device_type | cost_name              | cost_value | last_update         | comment | default_value |
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+
| default     |           0 | io_block_read_cost     |       NULL | 2018-04-23 13:55:20 | NULL    |             1 |
| default     |           0 | memory_block_read_cost |       NULL | 2018-04-23 13:55:20 | NULL    |          0.25 |
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+

你能夠經過update語句來進行更新, 例如:json

root@(none) 04:05:52>update mysql.server_cost set cost_value = 40 where cost_name = 'disk_temptable_create_cost';
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

root@(none) 04:07:13>select * from mysql.server_cost where cost_name = 'disk_temptable_create_cost';
+----------------------------+------------+---------------------+---------+---------------+
| cost_name                  | cost_value | last_update         | comment | default_value |
+----------------------------+------------+---------------------+---------+---------------+
| disk_temptable_create_cost |         40 | 2018-06-23 16:07:05 | NULL    |            20 |
+----------------------------+------------+---------------------+---------+---------------+
1 row in set (0.00 sec)


//更新後執行一次flush optimizer_costs操做來更新內存
//但老的session仍是會用老的cost數據
root@(none) 10:10:12>flush optimizer_costs;
Query OK, 0 rows affected (0.00 sec)

能夠看到用法也很是簡單,上面包含了兩張表:server_cost及engine_cost,分別對server層和引擎層進行配置api

相關代碼:

全局cache Cost_constant_cache

全局cache維護了一個當前的cost model信息, 用戶線程在lex_start時會去判斷其有沒有初始化本地指針,若是沒有的話就去該cache中將指針拷貝到本地數組

初始化全局cache:服務器

Cost_constant_cache::init
:

建立Cost_model_constants, 其中包含了兩類信息: server層cost model和引擎層cost model, 類結構以下:


Cost_constant_cache ----> Cost_model_constants
                            ---> Server_cost_constants
                                //server_cost
                            ---> Cost_model_se_info 
                                --->SE_cost_constants
                                //engine_cost 若是存儲引擎提供了接口函數get_cost_constants的話,則從存儲引擎那取

從系統表讀取配置,適用於初始化和flush optimizer_costs並更新cache:網絡

read_cost_constants()
|--> read_server_cost_constants
|--> read_engine_cost_constants

因爲用戶能夠動態的更新系統表,執行完flush optimizer_costs後,有可能老的版本還在被某些session使用,所以須要引用計數,老的版本ref counter被降爲0後才能被釋放session

線程cost model初始化

  • Cost_model_server

在每一個線程的thd上,掛了一個Cost_model_server的對象THD::m_cost_model, 在lex_start()時,若是發現線程的m_cost_model沒有初始化,就會去獲取全局的指針,存儲到本地:

Cost_model_server::init

const Cost_model_constants *m_cost_constants = cost_constant_cache->get_cost_constants();
// 會增長一個引用計數,以確保不會在引用時被刪除

const Server_cost_constants *m_server_cost_constants = m_cost_constants->get_server_cost_constants();
// 一樣獲取的是全局指針

可見thd不建立本身的cost model, 只引用cache中的指針

Table Cost Model

struct TABLE::m_cost_model, 類型:Cost_model_table

其值取自上述thd中存儲的cost model對象

Cost_estimate

統一的對象類型cost_estimate來存儲計算的cost結果,包含四個維度:

double io_cost;      ///< cost of I/O operations
  double cpu_cost;     ///< cost of CPU operations
  double import_cost;  ///< cost of remote operations
  double mem_cost;     ///< memory used (bytes)

將來

目前來看,除非根據工做負載,通過充分的測試才能得出合理的配置值,但如何配置,什麼是合理的值,我的認爲應該是能夠自動調整配置的。關鍵是找出配置和硬件條件的對應關係。 這也是咱們將來能夠努力的一個方向。

reference:

1. Cost Model官方文檔
2. 官方博客1:The MySQL Optimizer Cost Model Project
3. 官方博客2: A new dimension to MySQL query optimizations 
4. Optimizer Cost Model Improvements in MySQL 5.7.5 DMR
5.Slide: MySQL Cost Model

Related Worklog:
WL#7182: Optimizer Cost Model API 
WL#7209: Handler interface changes for new cost model
WL#7276: Configuration data base for Optimizer Cost Model
WL#7315 Optimizer cost model: main memory management of cost constants
WL#7316 Optimizer cost model: Command for online updating of cost model constants

Histogram

直方圖也是MySQL一個萬衆期待的功能了,這個功能實際上在其餘數據庫產品中是很常見的,能夠很好的指導優化器選擇執行路徑。利用直方圖存儲了指定列的數據分佈。MariaDB從很早的10.0.2版本支持這個功能, 而MySQL在最新的8.0版本中也開始支持

使用

MySQL裏使用直方圖是經過ANALYZE TABLE語法來執行:

ANALYZE [NO_WRITE_TO_BINLOG | LOCAL]
    TABLE tbl_name
    UPDATE HISTOGRAM ON col_name [, col_name] ...
        [WITH N BUCKETS]
        
ANALYZE [NO_WRITE_TO_BINLOG | LOCAL]
    TABLE tbl_name
    DROP HISTOGRAM ON col_name [, col_name] ...

舉個簡單的例子:

咱們以普通的sysbench表爲例:

root@sb1 05:16:33>show create table sbtest1\G
*************************** 1. row ***************************
       Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `k` int(11) NOT NULL DEFAULT '0',
  `c` char(120) NOT NULL DEFAULT '',
  `pad` char(60) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=200001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.01 sec)


# 建立直方圖並存儲到數據詞典中

root@sb1 05:16:38>ANALYZE TABLE sbtest1 UPDATE HISTOGRAM ON k with 10 BUCKETS;
+-------------+-----------+----------+----------------------------------------------+
| Table       | Op        | Msg_type | Msg_text                                     |
+-------------+-----------+----------+----------------------------------------------+
| sb1.sbtest1 | histogram | status   | Histogram statistics created for column 'k'. |
+-------------+-----------+----------+----------------------------------------------+
1 row in set (0.55 sec)

root@sb1 05:17:03>ANALYZE TABLE sbtest1 UPDATE HISTOGRAM ON k,pad with 10 BUCKETS;
+-------------+-----------+----------+------------------------------------------------+
| Table       | Op        | Msg_type | Msg_text                                       |
+-------------+-----------+----------+------------------------------------------------+
| sb1.sbtest1 | histogram | status   | Histogram statistics created for column 'k'.   |
| sb1.sbtest1 | histogram | status   | Histogram statistics created for column 'pad'. |
+-------------+-----------+----------+------------------------------------------------+
2 rows in set (7.98 sec)

刪除pad列上的histogram:
root@sb1 05:17:51>ANALYZE TABLE sbtest1 DROP HISTOGRAM ON pad;
+-------------+-----------+----------+------------------------------------------------+
| Table       | Op        | Msg_type | Msg_text                                       |
+-------------+-----------+----------+------------------------------------------------+
| sb1.sbtest1 | histogram | status   | Histogram statistics removed for column 'pad'. |
+-------------+-----------+----------+------------------------------------------------+
1 row in set (0.06 sec)

root@sb1 05:58:12>ANALYZE TABLE sbtest1 DROP HISTOGRAM ON k;
+-------------+-----------+----------+----------------------------------------------+
| Table       | Op        | Msg_type | Msg_text                                     |
+-------------+-----------+----------+----------------------------------------------+
| sb1.sbtest1 | histogram | status   | Histogram statistics removed for column 'k'. |
+-------------+-----------+----------+----------------------------------------------+
1 row in set (0.08 sec)


# 若是不指定bucket的話,默認Bucket的數量是100

root@sb1 05:58:27>ANALYZE TABLE sbtest1 UPDATE HISTOGRAM ON k;
+-------------+-----------+----------+----------------------------------------------+
| Table       | Op        | Msg_type | Msg_text                                     |
+-------------+-----------+----------+----------------------------------------------+
| sb1.sbtest1 | histogram | status   | Histogram statistics created for column 'k'. |
+-------------+-----------+----------+----------------------------------------------+
1 row in set (0.56 sec)

直方圖統計信息存儲於InnoDB數據詞典中,能夠經過information_schema表來獲取

root@information_schema 05:34:49>SHOW CREATE TABLE INFORMATION_SCHEMA.COLUMN_STATISTICS\G
*************************** 1. row ***************************
                View: COLUMN_STATISTICS
         Create View: CREATE ALGORITHM=UNDEFINED DEFINER=`mysql.infoschema`@`localhost` SQL SECURITY DEFINER VIEW `COLUMN_STATISTICS` AS select `mysql`.`column_statistics`.`schema_name` AS `SCHEMA_NAME`,`mysql`.`column_statistics`.`table_name` AS `TABLE_NAME`,`mysql`.`column_statistics`.`column_name` AS `COLUMN_NAME`,`mysql`.`column_statistics`.`histogram` AS `HISTOGRAM` from `mysql`.`column_statistics` where can_access_table(`mysql`.`column_statistics`.`schema_name`,`mysql`.`column_statistics`.`table_name`)
character_set_client: utf8
collation_connection: utf8_general_ci
1 row in set (0.00 sec)

從column_statistics表的定義能夠看到,有一個名爲mysql.column_statistics系統表,但被隱藏了,沒有對外暴露

如下舉個簡單的例子:

root@sb1 05:58:55>ANALYZE TABLE sbtest1 UPDATE HISTOGRAM ON k WITH 4 BUCKETS;
+-------------+-----------+----------+----------------------------------------------+
| Table       | Op        | Msg_type | Msg_text                                     |
+-------------+-----------+----------+----------------------------------------------+
| sb1.sbtest1 | histogram | status   | Histogram statistics created for column 'k'. |
+-------------+-----------+----------+----------------------------------------------+
1 row in set (0.63 sec)

# 查詢表上的直方圖信息

root@sb1 06:00:43>SELECT JSON_PRETTY(HISTOGRAM) FROM INFORMATION_SCHEMA.COLUMN_STATISTICS WHERE SCHEMA_NAME='sb1' AND TABLE_NAME = 'sbtest1'\G
*************************** 1. row ***************************
JSON_PRETTY(HISTOGRAM): {
  "buckets": [
    [
      38671,
      99756,
      0.249795,
      17002
    ],
    [
      99757,
      100248,
      0.500035,
      492
    ],
    [
      100249,
      100743,
      0.749945,
      495
    ],
    [
      100744,
      172775,
      1.0,
      16630
    ]
  ],
  "data-type": "int",
  "null-values": 0.0,
  "collation-id": 8,
  "last-updated": "2018-09-22 09:59:30.857797",
  "sampling-rate": 1.0,
  "histogram-type": "equi-height",
  "number-of-buckets-specified": 4
}
1 row in set (0.00 sec)

從輸出的json能夠看到,在執行了上述語句後產生的直方圖,有4個bucket,數據類型爲Int, 類型爲equi-height,即等高直方圖(另一種是等寬直方圖,即SINGLETON)。每一個Bucket中,描述的信息包括:數值的上界和下界, 頻率以及不一樣值的個數。經過這些信息能夠得到比較精確的數據分佈狀況,從而優化器來根據這些統計信息決定更優的執行計劃。

若是列上存在大量的重複值,那麼MySQL也可能選擇等寬直方圖,例如上例,咱們將列k上的值更新爲一半10一半爲20, 那麼出來的直方圖數據以下:

root@sb1 10:41:17>SELECT JSON_PRETTY(HISTOGRAM) FROM INFORMATION_SCHEMA.COLUMN_STATISTICS WHERE SCHEMA_NAME='sb1' AND TABLE_NAME = 'sbtest1'\G
*************************** 1. row ***************************
JSON_PRETTY(HISTOGRAM): {
  "buckets": [
    [
      10,
      0.499995
    ],
    [
      20,
      1.0
    ]
  ],
  "data-type": "int",
  "null-values": 0.0,
  "collation-id": 8,
  "last-updated": "2018-09-22 14:41:17.312601",
  "sampling-rate": 1.0,
  "histogram-type": "singleton",
  "number-of-buckets-specified": 100
}
1 row in set (0.00 sec)

如上,對於SINGLETON類型,每一個bucket只包含兩個值:列值,及對應的累計頻率(即百分之多少的數據比當前Bucket裏的值要小或相等)

注意這裏的sampling-rate, 這裏的值爲1,表示讀取了表上全部的數據來進行統計,但一般對於大表而言,咱們可能不但願讀太多的數據,由於可能產生過分的內存消耗,所以MySQL還提供了一個參數histogram_generation_max_mem_size來限制內存的使用上限。

若是表上的DML很少,那直方圖基本是穩定的,但頻繁寫入的話,那咱們就可能須要去按期更新直方圖,MySQL自己不會去主動更新。

優化器經過histogram來計算列的過濾性,大多數的謂詞均可以使用到。具體參閱官方文檔

關於直方圖影響查詢計劃,這篇博客 及 這篇博客

相關代碼

代碼結構:
以MySQL8.0.12爲例,主要代碼在sql/histogram目錄下:

ls sql/histograms/
equi_height_bucket.cc  
equi_height_bucket.h  
equi_height.cc  
equi_height.h  histogram.cc  
histogram.h  singleton.cc  
singleton.h  
value_map.cc  
value_map.h  
value_map_type.h


類結構:

namespace histograms
|---> Histogram  //基類
        |--> Equi_height //等高直方圖,模板類,實例化參數爲數據類型,須要針對類型顯示定義
        // 見文件 "equi_height.cc"
        |--> Singleton
        //等寬直方圖,只有值和其出現的頻度被存儲下來

建立及存儲histogram:

處理histogram的相關函數和堆棧以下:

Sql_cmd_analyze_table::handle_histogram_command
|--> update_histogram  //更新histogram
   |-->histograms::update_histogram  //調用namespace內的接口函數
        a. 判斷各個列:
        //histograms::field_type_to_value_map_type:  檢查列類型是否支持
        //covered_by_single_part_index: 若是列是Pk或者uk,不會爲其建立histogram
        //若是是generated column, 則找到其依賴的列加入到set中
        b. 判斷取樣的半分比,這主要受參數histogram_generation_max_mem_size限制,若是設的足夠大,則會去讀取全表數據進行分析
        |-> fill_value_maps   //開始從表上讀取須要分析的列數據
            |->ha_sample_init
            |->ha_sample_next
                |-->  handler::sample_next //讀取下一條記錄,經過隨機數的方式來進行取樣
            Value_map<T>::add_values // 將讀到的數據加入到map中
            |->...
            |->ha_sample_end
        
        |-> build_histogram //建立histogram對象
        a. 肯定histogram類型:若是值的個數小於桶的個數,則使用Singleton,不然使用Equi_height類型
            |->Singleton<T>::build_histogram
            |->Equi_height<T>::build_histogram
        
        |-> Histogram::store_histogram //將histogram信息存儲到column_statistic表中
            |-> dd::cache::Dictionary_client::update<dd::Column_statistics>

|--> drop_histogram //刪除直方圖

使用histogram

使用的方式就比較簡單了:

首先在表對象TABLE_SHARE中,增長成員m_histograms,其結構爲一個unordered map,key值爲field index, value爲相應的histogram對象

獲取列值過濾性的相關堆棧以下:

get_histogram_selectivity
    |-->Histogram::get_selectivity
        |->get_equal_to_selectivity_dispatcher
        |->get_greater_than_selectivity_dispatcher
        |->get_less_than_selectivity_dispatcher
    |-->write_histogram_to_trace // 寫到optimizer_trace中

MySQL支持多種操做類型對直方圖的使用,包括:

col_name = constant
col_name <> constant
col_name != constant
col_name > constant
col_name < constant
col_name >= constant
col_name <= constant
col_name IS NULL
col_name IS NOT NULL
col_name BETWEEN constant AND constant
col_name NOT BETWEEN constant AND constant
col_name IN (constant[, constant] ...)
col_name NOT IN (constant[, constant] ...)

經過直方圖,咱們能夠根據列上的條件判斷出列值的過濾性,來輔助選擇更優的執行計劃。在沒有直方圖以前咱們須要經過在列上創建索引來得到相對精確的列值分佈。但咱們知道索引是有很大的維護開銷的,而直方圖則能夠靈活的按需建立。

reference

WL#5384 PERFORMANCE_SCHEMA, HISTOGRAMS
WL#8706 Persistent storage of Histogram data
WL#8707 Classes/structures for Histograms
WL#8943 Extend ANALYZE TABLE with histogram support
WL#9223 Using histogram statistics in the optimizer

其餘

優化rec_per_key

相關worklog:
WL#7338: Interface for improved records per key estimates
WL#7339 Use improved records per key estimate interface in optimizer

MySQL經過rec_per_key 接口來估算記錄的個數(暗示每一個索引Key對應的記錄個數),但在早前版本中這個數字是整數,對於小數會取整,不能表示準確的rec_per_key,從而影響到索引的選擇,所以在5.7版本中,將其記錄的值改爲了float類型

引入數據cache狀態計算開銷

相關worklog:

WL#7168 API for estimates for how much of table and index data that is in memory buffer
WL#7170: InnoDB buffer estimates for tables and indexes
WL#7340 IO aware cost estimate function for data access

在以前的版本中,優化器是沒法知道數據的狀態,是不是cache在內存中,仍是須要從磁盤讀出來的,缺少這部分信息,致使優化器統一認爲數據屬於磁盤的來計算開銷。這可能致使低效的執行計劃。

相關代碼:

server層新增api,用於獲取表或索引上有百分之多少的數據是存儲在cache中的

handler::table_in_memory_estimate
 handler::index_in_memory_estimate

而在innodb層,增長了一個全局變量buf_stat_per_index (對應類型爲buf_stat_per_index_t) 來維護每一個索引在內存中的leaf page個數, 其內部實現了一個lock-free的hash結構,Key值爲(m_space_id) << 32 | m_index_id), 在讀入page時或者內存中建立新page時, 若是對應的page是leaf page,就遞增計數;當從page hash中移除時,則遞減計數。

爲了減小性能的影響,計數器是經過lock-free hash的結構存儲的,對應的結構爲ut_lock_free_hash_t
基本的實現思路是:hash是一個定長的數組,數組元素爲(key, val), 根據Key計算一個hash值再模上array size, 找到對應的槽位, 若是槽位被佔用了,則向右查找一個空閒的slot。
當數組滿了的時候,會建立一個新的更大的數組,在數據還沒Move到這個新hash以前,全部的search都須要查詢兩個數組。當全部的記錄到遷移到新數組,而且沒有線程訪問老的數組時,就能夠把老的hash刪除掉了。

在hash中存儲的counter自己,也考慮到多核和numa架構,避免同時更新引發的cpu cache失效。在大量core的場景下這個問題可能很明顯。Innodb封裝計數操做到類ut_lock_free_cnt_t中,使用數組維護counter, 按照cpu no做爲index更新,須要獲取counter值時則累加數組中的值。

這個Lock free hash並非個通用場景的hash結構:例如處理衝突的時候,可能佔用其餘key的槽位,hash不夠用時,須要遷移到新的array中。實際上mysql自己實現了一個lf_hash,在擴展Hash時無需遷移數據,有空單獨開篇博客講一下。

你能夠從information_schema.innodb_cached_indexes表中讀取到每一個索引cache的page個數。

當定義好接口,而且Innodb提供相應的統計數據後,優化器就能夠利用這些信息來計算開銷:

  • Cost_model_table::page_read_cost
  • Cost_model_table::page_read_cost_index


原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索