表
0. 概述
本篇博客對其中結構性的內容並沒有進行深究,同時認爲深究的意義不是很大,如果各位有興趣,可以參閱《MySQL技術內幕》。
1. 索引組織表
在InnoDB
存儲引擎中,表都是根據主鍵順序組織存放的,這種存儲方式的表稱爲索引組織表。
如果在創建表時沒有顯示定義主鍵,則InnoDB
按照如下方式選擇或創建主鍵。
- 如果存在非空且唯一性索引,則將該列作爲主鍵。
- 如果不存在,則
MySQL
會自動創建一個 6 字節大小的指針作爲主鍵。
2. InnoDB 邏輯存儲結構
InnoDB
的邏輯結構中,所有數據都被邏輯地存放在一個空間中,稱爲表空間(tablespace
)。表空間又由段(segment
)、區(extent
)、頁(page
)組成。
2.1 表空間
默認情況下,InnoDB
有一個共享表空間ibdata1
,所有數據都存放在這個表空間內。
2.2 段
表空間由各個段組成,常見的段有數據段、索引段、回滾段等。
由於InnoDB
表是索引組織的,因此數據即索引,索引即數據。數據段即爲 B+
樹的葉子節點,索引段即爲B+
樹的非葉子節點。
2.3 區
區是由連續頁組成的空間,在任何情況下每個區的大小都爲 1 MB。
默認情況下,InnoDB
頁的大小爲 16 KB
,即一個區中一共有 64 個連續的頁。
2.4 頁
頁是InnoDB
磁盤管理的最小單元,1.2 X
版本後,可以將其設置爲4k
、8k
、16k
。
2.5 行
InnoDB
中數據是按行存放的,每頁最多存放 7992 行記錄。
3. InnoDB 行記錄格式
InnoDB
提供了 Compact
和 Redundant
兩種格式來存放行記錄數據。
在 MySQL 5.1
版本中,默認設置爲 Compact
行格式。
3.1 Compact 行記錄格式
Compact
格式,其設計目標是高效地存儲數據。即一個頁中存放的行數據越多,其性能就越高。
Compact
行記錄格式的首部是一個非 NULL 變長字段長度列表,並且其是按照列的順序逆序放置的。用1
字節或者2
字節表示。
NULL
標誌位,該位指示了該行數據是否有 NULL
值,有則用 1
表示。用 1
字節表示。
3.2 Redundant 行記錄格式
不同於 Compact
行記錄格式,Redundant
行記錄格式的首部是一個字段長度偏移列表,同樣是按照列的順序逆序放置的。
3.3 行溢出數據
InnoDB
可以將一條記錄中的某些數據存儲在真正的數據頁面之外,這部分數據稱爲行溢出數據。
3.4 Compressed 和 Dynamic 行記錄格式
InnoDB 1.0.x
引入新的文件格式 Barracuda
,該文件格式擁有兩種新的行記錄格式:Compressed
和 Dynamic
。
新的兩種記錄格式對於存放在 BLOB 中的數據採用了完全的行溢出的方式,如圖所示,在數據頁中只存放20個字節的指針,實際的數據都存放在 Off Page 中,而之前的 Compact 和Redundant 兩種格式會存放 768 個前綴字節。
Compressed
的另一個功能就是,存儲在其中的行數據會以 zlib 算法進行壓縮。
3.5 CHAR 的行結構存儲
從 MySQL 4.1
開始,CHAR(N)
中的 N
指的是字符的長度,而不是之前版本的字節長度。
對於 UTF8
下 CHAR(10)
類型的列,其最小可以存儲 10
字節的字符,而最大可以存儲30
字節的字符。
因此,對於多字節字符編碼的 CHAR 數據類型的存儲,InnoDB 存儲引擎在內部將其視爲變長字符類型,因此可以認爲多字節字符集的情況下,CHAR和VARCHAR的實際行存儲基本是沒有區別的,對於未能佔滿長度的字符還是填充 0x20。
4. InnoDB 數據頁結構
4.1 File Header
File Header
用來記錄頁的一些頭信息。
InnoDB
存儲引擎中頁的類型:
4.2 Page Header
Page Header
用來記錄數據頁的狀態信息。
4.3 Infimum 和 Supremum Record
在 InnoDB
中,每個數據頁中有兩個虛擬的行記錄,用來限定記錄的邊界。Infimum
記錄是比該頁中任何主鍵值都要小的值,Supremum
比任何可能大的值還要大的值。這兩個值在頁創建時被建立,並且在任何情況下都不會被刪除。
4.4 User Record 和 Free Space
User Record
實際存儲行記錄的內容。
Free Space
指的是空閒空間,是個鏈表數據結構。在一條記錄被刪除後,該空間會被加入到空閒列表中。
4.5 Page Direction
Page Directory
(頁目錄)中存放了記錄的相對位置。
B+樹索引本身並不能查找到具體的的一條記錄,能找到的只是該記錄所在的頁。數據庫把頁載入到內存,然後通過 Page Directory
再進行二叉查找。
4.6 File Trailer
通過頁中的 File Trailer
部分,可以檢測頁是否已經完整地寫入磁盤(如寫入過程中磁盤損壞、機器關閉等)。
在默認配置下,InnoDB
存儲引擎每次從磁盤讀取一個頁就會檢測該頁的完整性。
5. Named File Formats 機制
Name File Formats
機制用來解決不同版本下頁結構兼容性的機制。
6. 約束
6.1 數據完整性
關係型數據庫系統和文件系統的一個不同點是,關係數據庫本身能保證存儲數據的完整性。
約束的作用便是用來保證數據完整性。
數據完整性有以下三種形式:
- 實體完整性保證表中有一個主鍵
- 域完整性保證數據每列的值滿足特定的條件
選擇合適的數據類型確保一個數據值滿足特定條件;
外鍵(Foreign Key
)約束;
編寫觸發器;
還可以考慮用 DEFAULT
約束作爲強制域完整性的一個方面。
- 參照完整性保證兩證表之間的關係
InnoDB
提供以下幾種約束:
- Primary Key
- Unique Key
- Foreign Key
- DEFAULT
- NOT NULL
6.2 約束的創建和查找
約束的創建可以採用以下兩種方式:
- 表建立時進行約束定義
- 利用
ALTER TABLE
命令進行創建約束
6.3 約束和索引的區別
約束更一個邏輯的概念,用來保證數據的完整性,而索引是一個數據結構,即有邏輯上的概念,在數據庫中還代表着物理存儲的方式。
6.4 對錯誤數據的約束
MySQL
並沒有對非法數據的插入或更新進行約束,如果需要插入非法數據時,選擇報錯,必須設置參數sql_mode
,來嚴格審覈輸入的參數。
6.5 ENUM 和 SET 約束
MySQL
不想SQLServer
那樣,擁有CHECK
約束,,只能提供離散的約束方式ENUM
和SET
來解決一部分需求。
CREATE TABLE a(
id INT,
sex SET ('male','fenake');
);
ENUM
與 SET
的區別:
SET
約束可以從裏面取多個值,而ENUM
只能取1個。
6.6 觸發器與約束
觸發器的作用是在執行INSERT
、DELETE
和UPDATE
命令之前或之後自動調用SQL
命令或存儲過程。
6.7 外鍵約束
外鍵用來保證參照完整性。
對父表的外鍵進行DELETE
和UPDATE
操作時,子表有如下四種操作:
- CASCADE
- SET NULL
- NO ACTION
- RESTRICT
CASCADE
表示當父表發生 DELETE
或 UPDATE
操作時,對相應的子表中的數據也進行 DELETE
或 UPDATE
操作。
SET NULL
表示當父表發生 DELETE 或 UPDATE 操作時,相應的子表中的數據被更新爲 NULL
值,但是子表中相應的列必須允許設置爲 NULL
值。
NO ACTION
表示當父表發生 DELETE
或 UPDATE
操作時,拋出錯誤,不允許這類操作發生。
RESTRICT
表示當父表發生 DELETE
或 UPDATE
操作時,拋出錯誤,不允許這類操作發生。
RESTRICT
是在修改或者刪除之前去檢查從表中是否有對應的數據,如果有,拒絕操作,而NO ACTION
是在修改或者刪除完以後去檢查從表中是否有對應的數據,如果有,拒絕操作,但是在MySQL中,外鍵約束都會立即檢查,所以兩者等價。
RESTRICT 是默認的外鍵設置。
7. 視圖
視圖是一個命名的虛表,與持久表不同的是,視圖中的數據沒有實際的物理存儲。
7.1 視圖的作用
在一定程度上起到一個安全層的作用:程序本身不需要關心基表(base table)的結構,只需要按照視圖定義來讀取數據或更新數據。
8. 分區表
8.1 分區概述
分區指將一個表或索引分解爲多個更小、更可管理的部分。
就訪問數據庫的應用而言,從邏輯上講,只有一個表或一個索引,但是在物理上這個表或索引可能由數十個物理分區組成。
MySQL 數據庫支持的分區類型爲水平分區(指將同一表中不同行的記錄分配到不同的物理文件中),並不支持垂直分區(指將同一表中不同列的記錄分配到不同的物理文件中)。
分區可能會給某些 SQL 語句性能帶來提高,但是分區主要用於數據庫高可用性的管理。在OLTP
應用中,對於分區的使用應該非常小心。
8.2 分區類型
8.2.1 RANGE 分區
行數據基於一個給定連續區間的列值被放入分區。
啓用分區之後,表不再由一個 ibd 文件組成了,而是由建立分區時的各個分區 ibd 文件組成。
創建一個 id
列的區間分區表。當 id
小於 10
時,數據插入 p0
分區。當 id
大於等於 10
小於 20
時,數據插入 p1
分區。
create table t(
id INT
)ENGINE = INNODB
PARTITION BY RANGE (id(
PARTITION P0 VALUES LESS THAN(10),
PARTITION P1 VALUES LESS THAN(10));
)
當插入一個不再分區中定義的值時,MySQL
會拋出一個異常。
RANGE
分區主要用於日期列的分區,例如對於銷售類的表,可以根據年來分區存放銷售記錄。
8.2.2 LIST 分區
和RANGE
分區類型,只是LIST
分區面向的是離散的值,而非連續的。
CREATE TABLE t(
a INT,
b INT
)ENGINE = INNODB
PARTITION BY RANGE (b)(
PARTITION p0 VALUES IN (1,3,5,7,9),
PARTITION p1 VALUES IN (0,2,4,6,8));
在用 INSERT
插入多個行數據的過程中遇到分區未定義的值時,MyISAM
和 InnoDB
的處理完全不同。MyISAM
會將之前的行數據都插入,但之後的數據不會被插入。而InnoDB
將其視爲一個事務,因此沒有任何數據插入。
8.2.3 HASH 分區
根據用戶自定義的表達式的返回值來進行分區,返回值不能爲負數。
HASH
分區的目的是將數據均勻地分佈到預先定義地各個分區中,保證各分區地數據數量大致是一樣的。
要使用 HASH
分區來分割一個表,要在 CHEATE TABLE
語句添加一個 PARTITION BY HASH(expr)
子句,其中 “expr
” 是返回一個整數的表達式。
CREATE table t_hash(
a INT,
b DATETIME
)ENGINE = InnoDB
PARTITION BY HASH (YEAR(b))
PARTITIONS 4;
8.2.4 KEY 分區
根據MySQL
數據庫提供的哈希函數進行分區。
KEY
分區和 HASH
分區相似,不同在於 HASH
分區使用用戶定義的函數進行分區,KEY
分區使用 MySQL
數據庫提供的函數進行分區。
8.2.5 COLUMN 分區
對於前四種分區,分區的條件是:整型(integer
),如果不是整型,那應該需要通過函數將其轉換爲整性。
COLUMNS
分區可以直接使用非整型的數據進行分區,分區根據類型直接比較而得,不需要轉換爲整性。
8.3 子分區
子分區是在分區得基礎上再進行分區,MySQL
是在分區的基礎上再進行分區,有時也稱這種分區爲符合分區。MySQL
數據庫允許再RANGE
和LIST
的分區上再進行HASH
或KEY
的子分區。
CREATE TABLE ts(
a INT,
b DATE
)
PARTITION BY RANGE ( YEAR(b))
SUBPARTITIONS 2(
PARTITION p0 VALUES LESS THAN (1990),
PARTITION p1 VALUES LESS THAN (2000),
PARTITION p2 VALUES LESS THAN MAXVALUE
);
表 ts
先根據 b
列進行了 RANGE
分區,然後又進行了一次 HASH
分區,所以分區的數量應該爲(3*2=6)個
8.4 分區中的 NULL 值
MySQL
允許對NULL
值做分區。
MySQL
數據庫得分區總是視 NULL
值小於任何得一個非 NULL
值。
對於 RANGE 分區
,如果向分區列插入了 NULL
值,則 MySQL
數據庫會將該值放入最左邊的分區。
在 LIST
分區下要使用 NULL
值,則必須顯示地指出那個分區中放入 NULL
值,否則會報錯。
HASH
和 KEY
分區對於 NULL
的處理方式和 RANGE
分區、LIST
分區不一樣。任何分區函數都會將含有 NULL
值得記錄返回爲 0
。
8.5 分區和性能
對於OLAP
(在線分析處理)的應用,分區的確是可以很好地提高查詢地性能,因爲OLAP
應用大多數查詢需要頻繁地掃描一張很大的表。
對於OLTP
(在線事務處理)的應用,則未必,如果涉及不好的分區會帶來嚴重的性能問題。