MySQL 8.0 新特性之函式索引
文章目錄
原文地址:MySQL 8.0 Reference Manual
通常來說索引使用的是列值或者列值的字首部分。例如,在下表 t1 中,索引包含了欄位 col1 的值,以及欄位 col2 的前 10 個位元組:
CREATE TABLE t1 (
col1 VARCHAR(10),
col2 VARCHAR(20),
INDEX (col1, col2(10))
);
MySQL 8.0.13 以及更高版本支援函式索引(functional key parts),也就是將表示式的值作為索引的內容,而不是列值或列值字首。 將函式作為索引鍵可以用於索引那些沒有在表中直接儲存的內容。例如:
CREATE TABLE t1 (col1 INT, col2 INT, INDEX func_index ((ABS(col1))));
CREATE INDEX idx1 ON t1 ((col1 + col2));
CREATE INDEX idx2 ON t1 ((col1 + col2), (col1 - col2), col1);
ALTER TABLE t1 ADD INDEX ((col1 * 40) DESC);
多列索引可以同時包含非函式列和函式列。
函式索引支援ASC
和DESC
選項。
函式索引必須遵循以下規則。如果索引鍵中包含了不允許的內容,建立索引時將會產生錯誤。
-
在索引定義中,需要將表示式放入括號之中,以便與列值索引或者字首索引進行區分。例如,以下索引表示式使用了括號:
INDEX ((col1 + col2), (col3 - col4))
下面是一個錯誤的寫法,表示式沒有位於括號之中:
INDEX (col1 + col2, col3 - col4)
-
函式索引不能只包含一個單獨的列名。例如,以下寫法是錯誤的:
INDEX ((col1), (col2))
但是,可以使用非函式索引的方式進行定義:
INDEX (col1, col2)
-
函式索引中的表示式不能使用列的字首。可以使用 SUBSTRING() 和 CAST() 函式作為一個替代方案,參考後文。
-
外來鍵不支援函式索引。
對於CREATE TABLE ... LIKE
語句,新建的表中將會保留源表中的函式索引。
函式索引實際上是使用隱藏的虛擬計算列來實現,因此存在以下限制:
-
每個函式索引都會算作一個列數,參與計算表的總列數限制;參考 Section C.10.4, “Limits on Table Column Count and
Row Size”。 -
函式索引同樣遵循計算列的所有限制。例如:
- 只有那些能夠用於計算列的函式才能夠用於建立函式索引。
- 函式索引中不允許使用子查詢、引數、變數、儲存函式以及自定義函式。
更多相關限制的資訊,可以參考 Section 13.1.20.8, “CREATE TABLE and Generated Columns”,以及 Section 13.1.9.2, “ALTER TABLE and Generated Columns”。
函式索引支援UNIQUE
選項。但是,主鍵不能包含函式列。主鍵只能使用儲存的計算列,但是函式索引使用虛擬計算列實現,而不是儲存計算列。
SPATIAL 索引和 FULLTEXT 索引不支援函式索引。
如果某個表中沒有主鍵,InnoDB 儲存引擎自動將第一個 UNIQUE NOT NULL 索引提升為主鍵。但是對於包含函式列的 UNIQUE NOT NULL 索引不會進行提升。
對於非函式索引,如果建立重複的索引,系統會提示一個警告。建立重複的函式索引不會提示任何資訊。
如果要刪除一個在函式索引中使用的欄位,必須先刪除該索引;否則將會產生錯誤。
雖然非函式索引支援字首索引,但是函式索引不支援使用欄位的字首。替代的方法就是使用 SUBSTRING() 函式(或者後文中的 CAST() 函式)。如果使用 SUBSTRING() 函式定義索引列,要想在查詢中使用該索引,必須在WHERE 子句中使用同樣的 SUBSTRING() 函式。以下示例中,只有第二個SELECT
能夠使用索引,因為它的查詢中使用了和索引定義相同的 SUBSTRING() 函式和引數:
CREATE TABLE tbl (
col1 LONGTEXT,
INDEX idx1 ((SUBSTRING(col1, 1, 10)))
);
SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 9) = '123456789';
SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 10) = '1234567890';
函式索引能夠支援其他方式無法使用的資料型別,例如 JSON 資料。不過,使用時需要特別小心。例如,以下建立索引的語法不會生效:
CREATE TABLE employees (
data JSON,
INDEX ((data->>'$.name'))
);
ERROR 3757 (HY000): Cannot create a functional index on an expression that returns a BLOB or TEXT. Please consider using CAST.
該語法的問題在於:
- 運算子 ->> 等價於 JSON_UNQUOTE(JSON_EXTRACT(…))。
- JSON_UNQUOTE() 函式返回 LONGTEXT 型別的資料,因此相應的隱藏計算列也具有這種資料型別。
- MySQL 不支援非字首的 LONGTEXT 列索引,而函式索引又不支援字首索引。兩者互相矛盾。
如果需要為 JSON 列建立索引,可以嘗試使用 CAST() 函式:
CREATE TABLE employees (
data JSON,
INDEX ((CAST(data->>'$.name' AS CHAR(30))))
);
相應的隱藏計算列被轉換為 VARCHAR(30) 型別,這種資料型別可以進行索引。但是這種方法帶來了一個新的使用上的問題:
- CAST() 函式返回的字串使用 utf8mb4_0900_ai_ci 排序規則(伺服器預設設定)。
- JSON_UNQUOTE() 函式返回的字串使用 utf8mb4_bin 排序規則(硬編碼,不能修改)。
結果就是,索引定義中的字元排序與以下查詢中的 WHERE 子句中的字元排序不一致:
SELECT * FROM employees WHERE data->>'$.name' = 'James';
以上查詢不會使用索引。為了支援這種情況下能夠使用函式索引,優化器查詢索引時自動排除索引中的 CAST() 函式的影響,但是隻有當索引表示式的排序規則能夠匹配查詢表示式的排序規則時才會這樣處理。為了能夠使用這種函式索引,可以採用以下兩種解決方案之一(它們之間存在一些差異):
解決方案 1:為索引表示式指定一個與 JSON_UNQUOTE() 相同的字元排序規則:
CREATE TABLE employees (
data JSON,
INDEX idx ((CAST(data->>"$.name" AS CHAR(30)) COLLATE utf8mb4_bin))
);
INSERT INTO employees VALUES
('{ "name": "james", "salary": 9000 }'),
('{ "name": "James", "salary": 10000 }'),
('{ "name": "Mary", "salary": 12000 }'),
('{ "name": "Peter", "salary": 8000 }');
SELECT * FROM employees WHERE data->>'$.name' = 'James';
運算子 ->> 等價於 JSON_UNQUOTE(JSON_EXTRACT(…)) ,而 JSON_UNQUOTE() 返回的字串使用 utf8mb4_bin 排序規則。因此,查詢條件區分大小寫,只返回一條記錄:
+------------------------------------+
| data |
+------------------------------------+
| {"name": "James", "salary": 10000} |
+------------------------------------+
解決方案 2:在查詢條件中指定完整的表示式:
CREATE TABLE employees (
data JSON,
INDEX idx ((CAST(data->>"$.name" AS CHAR(30))))
);
INSERT INTO employees VALUES
('{ "name": "james", "salary": 9000 }'),
('{ "name": "James", "salary": 10000 }'),
('{ "name": "Mary", "salary": 12000 }'),
('{ "name": "Peter", "salary": 8000 }');
SELECT * FROM employees WHERE CAST(data->>'$.name' AS CHAR(30)) = 'James';
CAST() 函式返回的字串使用的是 utf8mb4_0900_ai_ci 排序規則,因此查詢條件不區分大小寫,返回兩條記錄:
+------------------------------------+
| data |
+------------------------------------+
| {"name": "james", "salary": 9000} |
| {"name": "James", "salary": 10000} |
+------------------------------------+
需要注意的是,雖然優化器支援計算列索引中的自動 CAST() 去除處理, 不能使用以下方法實現 JSON 資料的索引,因為這種方法對於存在索引時和不存在索引時返回的結果不同(Bug#27337092):
mysql> CREATE TABLE employees (
data JSON,
generated_col VARCHAR(30) AS (CAST(data->>'$.name' AS CHAR(30)))
);
Query OK, 0 rows affected, 1 warning (0.03 sec)
mysql> INSERT INTO employees (data)
VALUES ('{"name": "james"}'), ('{"name": "James"}');
Query OK, 2 rows affected, 1 warning (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 1
mysql> SELECT * FROM employees WHERE data->>'$.name' = 'James';
+-------------------+---------------+
| data | generated_col |
+-------------------+---------------+
| {"name": "James"} | James |
+-------------------+---------------+
1 row in set (0.00 sec)
mysql> ALTER TABLE employees ADD INDEX idx (generated_col);
Query OK, 0 rows affected, 1 warning (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 1
mysql> SELECT * FROM employees WHERE data->>'$.name' = 'James';
+-------------------+---------------+
| data | generated_col |
+-------------------+---------------+
| {"name": "james"} | james |
| {"name": "James"} | James |
+-------------------+---------------+
2 rows in set (0.01 sec)
人生本來短暫,你又何必匆匆!點個贊再走吧!