數據庫關系模型設計
背景
目前公司內部主流數據庫是關系型數據庫MySQL,數據庫設計是對數據進行組織化和結構化的過程,即關系模型的設計。
對於項目規模小、用戶數量少的情況,處理數據庫中的表結構相對輕松;目前公司的發展速度快、用戶數量多、項目規模大、業務邏輯極其復雜;
相應的數據庫架構、關系模型表結構越來越復雜,這時我們往往會發現我們寫出來的SQL語句是很笨拙並且效率低下的。更可怕的是,由於表結構定義不合理,會導致對數據的增刪改查不方便不高效;最致命的是,擴展性極差,不能應對業務的變化。
此時對我們開發人員關系模式設計能力要求提高了,我們要學習和掌握數據庫的規范化流程,以指導我們更好的設計數據庫的表結構,減少冗余的數據,借此可以提高數據庫的存儲效率,數據完整性和可擴展性。
簡潔、結構明晰的表結構對數據庫的設計是相當重要的。規范化的表結構設計,在以后的數據維護中,不會發生插入(insert)、刪除(delete)和更新(update)時的異常。反之,數據庫表結構設計不合理,不僅會給數據庫的使用和維護帶來各種各樣的問題,而且可能存儲了大量不需要的冗余信息,浪費系統資源。
關於數據庫關系模型設計的問題,就是在實際項目中,我們應該構造幾個關系模型,每個關系(表)由哪些屬性(列)組成,不同關系之間有什么關聯。
規范化
一個低級范式的關系模型通過模式分解可以轉為若干個高一級別范式的關系模型的集合,這個過程就叫規范化。
為了說明方便,我們用一個訂單場景,來一步一步分析規范化的過程
如下只是為了演示,關系模型規范化的過程,存在不規范的地方
create table order_info (
order_no varchar(10) not null comment '訂單編號',
account_name varchar(10) not null comment '會員姓名',
account_address varchar(10) not null comment '會員地址',
product_name varchar(10) not null comment '商品',
product_address varchar(10) not null comment '產地',
product_num int unsigned not null default 0 comment '數量',
product_price double not null comment '單價',
sum_price double not null comment '總價',
primary key(order_no,account_name,product_name)
) engine=innodb default character set=utf8;
order_no | account_name | account_address | product_name | product_address | product_num | product_price | sum_price |
---|---|---|---|---|---|---|---|
201907080001 | 小明 | 北京 | bose降噪耳機 | 美國 | 1 | 2000 | 2000 |
201907080001 | 小明 | 北京 | 華為p20 | 中國 | 1 | 1000 | 1000 |
201907080002 | 小剛 | 上海 | bose降噪耳機 | 美國 | 1 | 2000 | 2000 |
201907080003 | 小明 | 北京 | iphone X | 美國 | 1 | 2000 | 2000 |
201907080004 | 小麗 | 唐山 | 華為p20 | 中國 | 1 | 1000 | 1000 |
這張表一共有8個字段,分析每個字段都有重復的值出現,也就是說,存在數據冗余問題。這將潛在地造成數據操作(比如刪除、更新等操作)時的異常情況,因此,需要進行規范化。
第一范式
參照范式的定義,我們發現,這張表已經滿足了第一范式的要求。
事實上在當前所有的關系數據庫管理系統(DBMS)中,都已經在建表的時候強制滿足第一范式,即關系型數據庫原生滿足第一范式,1NF是所有關系型數據庫的最基本要求。
從業務的角度分析可能認為這張表不滿足第一方式,因為地址可以再細化。
缺點
- 數據冗余:每一個字段都有值重復
- 更新復雜:更新用戶或商品的地址時需要更新多條記錄,即數據的一致性增加了成本
第二范式
參照范式的定義,我們發現,這張表不滿足第二范式的要求。
- 存在依賴部分主鍵
- (order_no,account_name,product_name) -> account_address
- (account_name) -> account_address
- (order_no,account_name,product_name) -> product_price
- (product_name) -> product_price
- (order_no,account_name,product_name) -> account_address
我們按照范式對該關系模式進行分解轉化:
account_info(用戶表)
create table account_info(
id int unsigned auto_increment comment '主鍵',
account_name varchar(10) comment '用戶姓名',
account_address varchar(10) comment '地址',
primary key(id)
)engine=innodb default charset=utf8
id | account_name | account_address |
---|---|---|
1 | 小明 | 北京 |
2 | 小剛 | 上海 |
3 | 小麗 | 唐山 |
product_info(商品表)
create table product_info(
id int unsigned auto_increment comment '主鍵',
product_name varchar(10) comment '商品名稱',
product_address varchar(10) comment '產址',
product_price double comment '單價',
primary key(id)
)engine=innodb default charset=utf8
id | product_name | product_address | product_price |
---|---|---|---|
1 | bose降噪耳機 | 美國 | 2000 |
2 | 華為p20 | 中國 | 1000 |
3 | iphone X | 美國 | 2000 |
order_info(訂單表)
create table order_info(
id int unsigned auto_increment comment '主鍵',
order_no varchar(10) comment '訂單號',
account_id int comment '用戶id',
primary key(id)
)engine=innodb default charset=utf8
id | order_no | account_id |
---|---|---|
1 | 201907080001 | 1 |
2 | 201907080002 | 2 |
3 | 201907080003 | 3 |
4 | 201907080004 | 1 |
order_detial_info(訂單商品表)
create table order_detial_info(
id int unsigned auto_increment comment '主鍵',
order_id int comment '訂單id',
product_id int comment '商品id',
product_num int comment '商品數量',
sum_price double comment '商品合計',
primary key(id)
)engine=innodb default charset=utf8
id | order_id | product_id | product_num | sum_price |
---|---|---|---|---|
1 | 1 | 1 | 1 | 2000 |
2 | 1 | 2 | 1 | 1000 |
3 | 2 | 1 | 1 | 2000 |
4 | 3 | 1 | 1 | 2000 |
5 | 4 | 3 | 1 | 1000 |
從第一范式到第二范式,一個訂單關系表衍生了多個關系表。
第三范式
參照范式的定義,我們發現,存在傳遞依賴
order_detial_info
- id -> product_id
- id -> product_num
- id -> sum_price
- (product_id,product_num) -> sum_price
order_info(訂單表)
create table order_info(
id int unsigned auto_increment comment '主鍵',
order_no varchar(10) comment '訂單號',
account_id int comment '用戶id',
sum_price double comment '商品合計',
primary key(id)
)engine=innodb default charset=utf8
id | order_no | account_id | sum_price |
---|---|---|---|
1 | 201907080001 | 1 | 3000 |
2 | 201907080002 | 2 | 2000 |
3 | 201907080003 | 3 | 2000 |
4 | 201907080004 | 1 | 1000 |
order_detial_info(訂單商品表)
create table order_detial_info(
id int unsigned auto_increment comment '主鍵',
order_id int comment '訂單id',
product_id int comment '商品id',
product_num int comment '商品數量',
primary key(id)
)engine=innodb default charset=utf8
id | order_id | product_id | product_num |
---|---|---|---|
1 | 1 | 1 | 1 |
2 | 1 | 2 | 1 |
3 | 2 | 1 | 1 |
4 | 3 | 1 | 1 |
5 | 4 | 3 | 1 |
我們通過進一步轉換,消除傳遞依賴,使之滿足第三范式。
總結
在本文描述的過程中,我們通過結合實例的方法,通俗地演繹了數據表規范化的過程,並展示了在此過程中數據冗余、數據庫操作異常等問題是如何得到解決的,但對於訂單信息的匯總,報表的形成增加了難度。
規范化的過程就是,一步步提升關系范式級別的過程;三大范式只是一般設計數據庫的基本理念,可是實際工作中我們根據業務場景(OLAP/OLTP)出於性能或擴展的考慮,出發點不一樣,在空間和時間上做的取舍不一樣,關系模型設計只滿足2NF或1NF,也就是反范式;但作為開發人員一定要了解關系模式設計規范化的過程。
范式
- 第一范式:當關系模式R的所有屬性都不能在分解為更基本的數據單位時,稱R是滿足第一范式的,簡記為1NF(列不可再分,無重復的列);
- 第二范式:如果關系模式R滿足第一范式,並且R得所有非主屬性都完全依賴於R的每一個候選關鍵屬性,稱R滿足- 第二范式,簡記為2NF(屬性依賴完全主鍵(主鍵/所有復合主鍵),不能依賴主鍵的一部分,消除部分依賴);
- 第三范式:設R是一個滿足第一范式條件的關系模式,X是R的任意屬性集,如果X非傳遞依賴於R的任意一個候選關鍵字,稱R滿足第三范式,簡記為3NF(屬性不依賴於其他非主屬性,消除傳遞依賴);
- 數據庫范式除了上述三個范式還有BCNF、4NF等,這里不再一一講解
知識拓展
第二范式(2NF)和第三范式(3NF)的概念很容易混淆,區分它們的關鍵點在於,2NF:非主鍵列是否完全依賴於主鍵,還是依賴於主鍵的一部分;3NF:非主鍵列是直接依賴於主鍵,還是直接依賴於非主鍵列。
候選碼:若關系(表)中某一屬性(列)組的值能唯一標識一個元組(行),而其子集不能,則稱該屬性組為候選碼(一個關系中可能存在多個候選碼,則選定其中一個作為主碼(主鍵 primary key))。
候選碼的各個屬性成為主屬性。不包含在任何候選碼中的屬性成為非主屬性或非碼屬性;
最簡單的情況下,候選碼只包含一個屬性;在最極端情況下,關系模式中的所有屬性是這個關系模型的屬性碼,成為全碼。