MySQL中的函式索引(Generated Column)及一次SQL優化
MySQL 中是沒有 Oracle 的函式索引功能的,把 MySQL 的 Generated Column 稱為“函式索引”並不準確,但可以和函式索引達到同樣的效果,也有人把這個特性稱為“衍生列”。
Generated Column 是什麼
Generated Column 的值是根據其定義的表示式所計算而來的,下面使用官方文件中的例子做個簡單介紹。
有一張表儲存直角三角形的三條邊長,大家都知道,根據直角三角形的邊長公式,斜邊的長度可以通過另外兩條邊長計算得到,這樣就可以在表中只儲存兩條直角邊,而斜邊通過 Generated Column 定義,建立這張表並插入一條資料:
CREATE TABLE triangle ( sidea DOUBLE, sideb DOUBLE, sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb)) ); INSERT INTO triangle (sidea, sideb) VALUES(1,1),(3,4),(6,8);
sidea 和 sideb 是兩條直角邊,sidec 是斜邊,insert 時只需要插入兩條直角邊,也就是說 Generated Column 不能人為操作(插入、更新、刪除),會自動根據其定義表示式計算得到。
查詢這張表:
mysql> SELECT * FROM triangle; +-------+-------+--------------------+ | sidea | sideb | sidec | +-------+-------+--------------------+ | 1 | 1 | 1.4142135623730951 | | 3 | 4 | 5 | | 6 | 8 | 10 | +-------+-------+--------------------+
Generated Column 定義語法
Generated Column 的定義語法如下:
col_name data_type [GENERATED ALWAYS] AS (expr)
[VIRTUAL | STORED] [NOT NULL | NULL]
[UNIQUE [KEY]] [[PRIMARY] KEY]
[COMMENT 'string']
關鍵字“AS”指明瞭這個欄位是衍生的,是 Generated Column,AS 後面就是用以計算的表示式。GENERATED ALWAYS 使定義更明確,可以省略。
VIRTUAL 和 STORED 是 Generated Column 的兩種型別,指明該欄位的值如何儲存:
- VIRTUAL: Virtual Generated Column 的值不會持久化到磁碟,只儲存在資料字典中(表的元資料),每次讀取時在 BEFORE 觸發器後就會立即計算。
- STORED:Stored Generated Column 的值會持久化到磁碟上,而不是每次讀取時計算。
如果不指明的話,MySQL 會預設以 VIRTUAL 的形式實現,STORED 需要更多的磁碟空間,效能也沒有明顯的優勢,所以一般使用 VIRTUAL。
Generated Column 定義要點
- 一般情況下,Generated Column 可以使用內建函式及操作符定義。如果給定相同的資料,多次呼叫會產生相同的結果,這樣的定義是明確被允許的。否則,定義會失敗,例如使用
NOW()
、CURRENT_USER()
、CONNECTION_ID()
的定義會失敗。 - 自定義的函式和儲存過程,不允許使用。
- 變數,例如系統變數、自定義變數等不允許使用。
- 子查詢不允許使用。
- Generated Column 的定義中可以依賴其他 Generated Column 欄位,但所依賴的衍生欄位必須定義在它的前面。如果只依賴非衍生欄位,則定義順序沒有要求。
- 自增長
AUTO_INCREMENT
不允許使用。 - 自增長的列,不能用到 Generated Column 的定義中。
- 從 MySQL 5.7.10 開始,如果表示式計算導致截斷或給函式提供了不正確的輸入,則create table語句將終止,並返回DDL操作。
一次SQL優化
通過慢查詢日誌找到一條慢SQL,執行計劃如下:
mysql> EXPLAIN
SELECT
c.id,
b.customer_status
FROM
t_core_customer c
INNER JOIN t_core_customer_bizinfo b ON c.id = b.customer_id AND b.biz_id = 'maintain'
WHERE
REPLACE ( REPLACE ( c.customer_name, '(', '(' ), ')', ')' ) = '天津買斯扣科技有限公司';
+----+-------------+-------+------------+--------+----------------------------------+---------+---------+--------------------------------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+----------------------------------+---------+---------+--------------------------------+---------+----------+-------------+
| 1 | SIMPLE | b | NULL | ALL | idx_core_customer_bizinfo_cidbid | NULL | NULL | NULL | 1263918 | 10.00 | Using where |
| 1 | SIMPLE | c | NULL | eq_ref | PRIMARY | PRIMARY | 110 | b.customer_id | 1 | 100.00 | Using where |
+----+-------------+-------+------------+--------+----------------------------------+---------+---------+--------------------------------+---------+----------+-------------+
2 rows in set (0.05 sec)
客戶表中有117萬行資料,這條SQL執行耗時4秒多,通過執行計劃可以看到,客戶表沒有走索引而進行全表掃描,customer_name 欄位的索引由於 replace 函式沒有被利用到。
增加 Generated Column :
ALTER TABLE `t_core_customer`
ADD COLUMN `customer_name_replaced` varchar(200) AS (REPLACE(REPLACE(customer_name, '(', '(' ), ')', ')' ));
建立索引:
ALTER TABLE `t_core_customer`
ADD INDEX `customer_name_replaced`(`customer_name_replaced`) USING BTREE;
優化後再看執行計劃:
mysql> EXPLAIN
SELECT
c.id,
b.customer_status
FROM
t_core_customer c
INNER JOIN t_core_customer_bizinfo b ON c.id = b.customer_id AND b.biz_id = 'maintain'
WHERE
REPLACE ( REPLACE ( c.customer_name, '(', '(' ), ')', ')' ) = '天津買斯扣科技有限公司';
+----+-------------+-------+------------+------+----------------------------------+----------------------------------+---------+-----------------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+----------------------------------+----------------------------------+---------+-----------------------------+------+----------+-------+
| 1 | SIMPLE | c | NULL | ref | PRIMARY,customer_name_replaced | customer_name_replaced | 603 | const | 1 | 100.00 | NULL |
| 1 | SIMPLE | b | NULL | ref | idx_core_customer_bizinfo_cidbid | idx_core_customer_bizinfo_cidbid | 222 | c.id,const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+----------------------------------+----------------------------------+---------+-----------------------------+------+----------+-------+
2 rows in set (0.40 sec)
執行計劃正常,利用了索引,SQL耗時到了10毫秒以內。