MySQL 表定義主鍵不是必須的,並且直到今天(MySQL 版本 8.3.0)都是這樣。不過,在 MGR 和 PXC 架構中不允許使用沒有主鍵的表。如果數據表沒有主鍵,會有許多衆所周知的負面性能影響,其中最痛苦的是複製速度很糟糕。
今天,我想快速說明一下 需要使用主鍵的另一個原因:磁盤空間!
創建一個非常簡單的示例表:
mysql > show create table test1\G
*************************** 1. row ***************************
Table: test1
Create Table: CREATE TABLE `test1` (
`a` int NOT NULL,
`b` bigint DEFAULT NULL,
KEY `a` (`a`),
KEY `b` (`b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
填充 10M 測試行,需要 748M 磁盤空間。現在,假設我的測試表的 a
列具有唯一值:
mysql > select count(*) from test1;
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (1.34 sec)
mysql > select count(DISTINCT(a)) from test1;
+--------------------+
| count(DISTINCT(a)) |
+--------------------+
| 10000000 |
+--------------------+
1 row in set (5.25 sec)
下面我將把索引類型更改爲主鍵:
mysql > alter table test1 add primary key(a), drop key a;
Query OK, 0 rows affected (48.90 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql > show create table test1\G
*************************** 1. row ***************************
Table: test1
Create Table: CREATE TABLE `test1` (
`a` int NOT NULL,
`b` bigint DEFAULT NULL,
PRIMARY KEY (`a`),
KEY `b` (`b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
結果,該表被重新創建,其磁盤大小減少到 588M,非常顯着!
爲什麼會發生這種情況?我們擁有完全相同的數據,並且在兩種情況下都對兩列都建立了索引!讓我們檢查一下更改前後該表的更多詳細信息。
之前,在沒有主鍵的情況下,當兩列都通過輔助鍵建立索引時,我們可以看到以下內容:
mysql > select SPACE,INDEX_ID,i.NAME as index_name, t.NAME as table_name, CLUST_INDEX_SIZE, OTHER_INDEX_SIZE from information_schema.INNODB_INDEXES i JOIN information_schema.INNODB_TABLESPACES t USING(space) JOIN information_schema.INNODB_TABLESTATS ts WHERE t.NAME=ts.NAME AND t.NAME='db1/test1'\G
*************************** 1. row ***************************
SPACE: 50
INDEX_ID: 232
index_name: a
table_name: db1/test1
CLUST_INDEX_SIZE: 24699
OTHER_INDEX_SIZE: 22242
*************************** 2. row ***************************
SPACE: 50
INDEX_ID: 231
index_name: b
table_name: db1/test1
CLUST_INDEX_SIZE: 24699
OTHER_INDEX_SIZE: 22242
*************************** 3. row ***************************
SPACE: 50
INDEX_ID: 230
index_name: GEN_CLUST_INDEX
table_name: db1/test1
CLUST_INDEX_SIZE: 24699
OTHER_INDEX_SIZE: 22242
3 rows in set (0.00 sec)
竟然還有第三個索引!通過 innodb_ruby 工具可以更詳細地查看每個索引,可以看到它的大小是最大的(id=230):
$ innodb_space -f msb_8_3_0/data/db1/test1.ibd space-indexes
id name root fseg fseg_id used allocated fill_factor
230 4 internal 3 27 27 100.00%
230 4 leaf 4 24634 24672 99.85%
231 5 internal 5 21 21 100.00%
231 5 leaf 6 12627 12640 99.90%
232 6 internal 7 13 13 100.00%
232 6 leaf 8 9545 9568 99.76%
這就是 InnoDB 引擎的工作原理;如果沒有定義明確的主鍵,它將添加一個名爲 的內部主鍵 GEN_CLUST_INDEX
。由於它包含整個數據行,因此其大小開銷非常大。
將二級索引替換爲顯式主鍵後,就不再需要隱藏索引了。因此,我們總共剩下兩個索引:
mysql > select SPACE,INDEX_ID,i.NAME as index_name, t.NAME as table_name, CLUST_INDEX_SIZE,OTHER_INDEX_SIZE from information_schema.INNODB_INDEXES i JOIN information_schema.INNODB_TABLESPACES t USING(space) JOIN information_schema.INNODB_TABLESTATS ts WHERE t.NAME=ts.NAME AND t.NAME='db1/test1'\G
*************************** 1. row ***************************
SPACE: 54
INDEX_ID: 237
index_name: b
table_name: db1/test1
CLUST_INDEX_SIZE: 23733
OTHER_INDEX_SIZE: 13041
*************************** 2. row ***************************
SPACE: 54
INDEX_ID: 236
index_name: PRIMARY
table_name: db1/test1
CLUST_INDEX_SIZE: 23733
OTHER_INDEX_SIZE: 13041
2 rows in set (0.01 sec)
$ innodb_space -f msb_8_3_0/data/db1/test1.ibd space-indexes
id name root fseg fseg_id used allocated fill_factor
236 4 internal 3 21 21 100.00%
236 4 leaf 4 20704 23712 87.31%
237 5 internal 5 17 17 100.00%
237 5 leaf 6 11394 13024 87.48%
GEN_CLUST_INDEX vs GIPK
每個 InnoDB 表都有一個聚集鍵,因此不定義聚集鍵不會節省任何磁盤空間,有時甚至相反,如上所示。因此,即使有問題的表中沒有任何現有列是唯一的,最好還是添加另一個唯一列作爲主鍵。內部 GEN_CLUST_INDEX 不暴露給 MySQL 上層,只有 InnoDB 引擎知道它,因此對於複製速度來說沒有用處。因此,顯式主鍵始終是更好的解決方案。
但是,如果由於遺留應用程序問題而無法添加新的主鍵列,建議使用不可見的主鍵(GIPK)來當作主鍵。這樣,您將獲得性能優勢,同時對應用程序是不可見的。
mysql > set sql_require_primary_key=1;
Query OK, 0 rows affected (0.00 sec)
mysql > create table nopk (a int);
ERROR 3750 (HY000): Unable to create or change a table without a primary key, when the system variable 'sql_require_primary_key' is set. Add a primary key to the table or unset this variable to avoid this message. Note that tables without a primary key can cause performance problems in row-based replication, so please consult your DBA before changing this setting.
mysql > set sql_generate_invisible_primary_key=1;
Query OK, 0 rows affected (0.00 sec)
mysql > create table nopk (a int);
Query OK, 0 rows affected (0.02 sec)
mysql > show create table nopk\G
*************************** 1. row ***************************
Table: nopk
Create Table: CREATE TABLE `nopk` (
`my_row_id` bigint unsigned NOT NULL AUTO_INCREMENT /*!80023 INVISIBLE */,
`a` int DEFAULT NULL,
PRIMARY KEY (`my_row_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
mysql > select * from nopk;
+------+
| a |
+------+
| 100 |
+------+
1 row in set (0.00 sec)
因此,我們的應用程序根本不知道新列。但如果需要,我們仍然可以使用它,例如,輕鬆地將表讀取或寫入分成可預測的塊:
mysql > select my_row_id,a from nopk;
+-----------+------+
| my_row_id | a |
+-----------+------+
| 1 | 100 |
+-----------+------+
1 row in set (0.00 sec)
請注意,對於缺少主鍵的架構,在強制執行變量 sql_require_primary_key
之前,最好首先啓用 sql_generate_invisible_primary_key
並使用邏輯備份和恢復重新創建數據。簡單的表優化不會增加不可見主鍵。無論如何,對於遺留的應用來說,擁有不可見主鍵(GIPK)應該是一個雙贏的解決方案。
更多技術文章,請訪問:https://opensource.actionsky.com/
關於 SQLE
SQLE 是一款全方位的 SQL 質量管理平臺,覆蓋開發至生產環境的 SQL 審覈和管理。支持主流的開源、商業、國產數據庫,爲開發和運維提供流程自動化能力,提升上線效率,提高數據質量。