資料庫正規化(Database normalization)簡單的講就是讓資料庫避免重複資料、減少空間浪費、避免資料不一致、讓資料更好管理的一系列規劃方式。本篇教學文章屬於概念性質,對於MariaDB、MySQL、MS SQL、Oracle Database、H2 Database等各種使用SQL的關聯式資料庫都是通用的概念。
第一正規化
第一正規化(First Normal Form, 1NF)是資料正規化中的第一個階段,它的目標是確保每個欄位只包含原子值,並消除重複的群組。按照第一正規化的規則,每個表格中的每個欄位都應該是原子的,不可再分。這意味著每個欄位中的資料應該是單一值,而不是包含多個值或者是一個值的集合。
修正前
以下模擬一間公司的員工名單,以及員工所在的部門,可以看到後面的「部門地點與時區」違反了第一正規化,應該要區分成兩個欄位;最後的語言技能也不是不可再分的,可是我們不確定需要多少個欄位存放語言技能1、語言技能2、語言技能3,所以最好的方式是直接拆出去變成另一個資料表。
員工ID | 員工姓名 | 部門ID | 部門人數 | 部門名稱 | 部門地點與時區 | 語言技能 |
---|---|---|---|---|---|---|
1 | Kyle | 1 | 2 | 業務部 | 台北-UTC+8 | 中文、英文、日文 |
2 | Stitch | 1 | 2 | 業務部 | 台北-UTC+8 | 中文 |
3 | Sato | 2 | 1 | 研發部 | 東京-UTC+9 | 英文、日文 |
這個例子中,因為部門地點與時區綁在一起,所以我們沒辦法單獨查詢UTF+8
的時區有哪些部門(比如新加坡)。我們也不能方便統計每個人會多少語言、每個語言有多少人會這些問題,必須用一些效率很差的方式才能統計出來,還容易出錯。
修正後
透過修正將部門地點與部門時區分開來,然後將語言技能獨立成另一張資料表,即可完成第一正規化。
員工ID | 員工姓名 | 部門ID | 部門人數 | 部門名稱 | 部門地點 | 部門時區 |
---|---|---|---|---|---|---|
1 | Kyle | 1 | 2 | 業務部 | 台北 | UTC+8 |
2 | Stitch | 1 | 2 | 業務部 | 台北 | UTC+8 |
3 | Sato | 2 | 1 | 研發部 | 東京 | UTC+9 |
員工ID | 語言 |
---|---|
1 | 中文 |
1 | 英文 |
1 | 日文 |
2 | 中文 |
3 | 英文 |
3 | 日文 |
員工語言表獨立之後我們就可以透過下列方式統計每個語言有多少人會。這邊使用到了SELECT
查詢與GROUP
統計,我會在之後的查詢與進階查詢中有詳細教學。
select 語言, count(*) as 人數 from 員工語言表 group by 語言;
或是統計每個人會多少種語言。這邊也是用了很多SQL查詢語法,看不懂也沒關係,先有個概念,之後我會再寫查詢與進階查詢中的詳細教學。
-- 使用子查詢(Subquery)統計
select 員工姓名, (select count(*) from 員工語言表 as b where a.員工ID=b.員工ID) as 語言數量
from 員工資料表 as a;
-- 使用JOIN與GROUP查詢,通常這個效能較好
select 員工姓名, count(b.語言) as 語言數量 from 員工資料表 as a
left join 員工語言表 as b on a.員工ID=b.員工ID group by a.員工ID;
這邊使用的是LEFT JOIN
,這樣才能查詢到什麼都語言都不會的員工,查詢結果會在語言數量顯示0。如果使用INNER JOIN
,就不會顯示語言數量是0的員工。雖然大部分時候不會有什麼語言都不會的員工,但是看到0我們就可以發現有人漏填語言選項了。
第二正規化
第二正規化(Second Normal Form, 2NF)的目標是消除非關鍵欄位對於主鍵的部分依賴,同時確保每個非關鍵欄位完全依賴於主鍵。按照第二正規化的規則,每個非關鍵欄位都應該完全依賴於主鍵,而不是部分依賴於主鍵。部分依賴指的是非關鍵欄位依賴於主鍵的部分而不是全部。
修正後
以我們剛才做完第一正規化的表格來看,可以發現員工跟部門資料放在一起不是好主意,我們可以拆出變成以下兩張表,分別是員工表和部門表。然後將在員工表部門ID,透過部門ID就可以知道這位員工在哪一個部門了。
員工ID | 員工姓名 | 部門ID |
---|---|---|
1 | Kyle | 1 |
2 | Stitch | 1 |
3 | Sato | 2 |
部門ID | 部門人數 | 部門名稱 | 部門地點 | 部門時區 |
---|---|---|---|---|
1 | 2 | 業務部 | 台北 | UTC+8 |
2 | 1 | 研發部 | 東京 | UTC+9 |
想要將員工姓名與部門名稱一起顯示可以透過JOIN
查詢。
SELECT 員工ID, 員工姓名, 部門名稱 FROM 員工資料表 u
JOIN 部門資料表 d ON d.部門ID=u.部門ID;
第三正規化
第三正規化(Third Normal Form, 3NF)的目標是消除非關鍵欄位之間的傳遞性依賴,確保每個非關鍵欄位都直接依賴於主鍵。按照第三正規化的規則,任何非關鍵欄位都不應該相互依賴,也不應該存在傳遞性依賴。傳遞性依賴指的是一個非關鍵欄位依賴於另一個非關鍵欄位。
修正後
以剛才的例子來說部門時區主要是跟部門地點有關,而非直接跟部門ID有關係,因此應該另外建立一個表來儲存地點與時區的關係。然後部門人數也跟部門沒有直接關係,反而要去數數看員工表的部門ID有幾個,因此要去除部門人數。
部門ID | 部門名稱 | 部門地點 |
---|---|---|
1 | 業務部 | 台北 |
2 | 研發部 | 東京 |
地點 | 時區 |
---|---|
台北 | UTC+8 |
東京 | UTC+9 |
要是直接在部門資料表紀錄人數,很可能發生員工資料表的人異動了,卻忘記一併修正部門資料表。因此當我們需要查詢每個部門的人數時,應該要直接透過SQL去統計,這樣才能獲得最正確的資料。
select 部門ID, count(*) as 人數 from 員工資料表 group by 部門ID;
反正規化
有一些特殊的情況下我們會刻意去違反第三正規化,把部門人數儲存起來當做快取資料,這是為了增加效能而做的反正規化。除非真的感受到效能瓶頸,透過其他優化手段速度還是不佳,不然不建議做反正規化。這是一種花費空間來換取時間的方式,可是之後卻要花很多開發時間與維護時間確保快取資料的正確性。
第四正規化
第四正規化(Fourth Normal Form, 4NF)的目標是消除多值相依性(Multivalued Dependency),確保每個欄位都只依賴於鍵(Key)。多值相依性指的是在一個關係表中,存在非鍵欄位之間的相互關係,這些非鍵欄位可以獨立於其他非鍵欄位存在多個值。4NF通常在第三正規化之後進行。(之後再補上例子)
其他正規化
通常進行到第四正規化就足夠了,如果還有興趣可以參考以下連結。
本篇教學的資料表結構
因為本篇文章主要是概念講解,所以前面介紹的時候忽略了 Table schema,如果各位有興趣的話以下是最一開始的資料表結構。
-- https://klab.tw/2023/05/database-teaching-3-4/
create table 員工資料表 (
員工ID varchar(20) primary key,
員工姓名 varchar(20),
部門ID varchar(20),
部門人數 int not null default 0,
部門名稱 varchar(20),
部門地點與時區 varchar(20),
語言技能 varchar(100)
);
以下是經過四次正規化最後修改完成的資料表結構。
-- https://klab.tw/2023/05/database-teaching-3-4/
create table 時區資料表 (
地點 varchar(20) primary key,
時區 varchar(20)
);
create table 部門資料表 (
部門ID int primary key,
部門名稱 varchar(20),
部門地點 varchar(20),
foreign key(部門地點) references 時區資料表(地點)
);
create table 員工資料表 (
員工ID int primary key,
員工姓名 varchar(20),
部門ID int,
foreign key(部門ID) references 部門資料表(部門ID)
);
create table 員工語言表 (
員工ID int,
語言 varchar(20),
primary key(員工ID, 語言),
foreign key(員工ID) references 員工資料表(員工ID)
);
insert into 時區資料表 values ('台北', 'UTC+8'), ('東京', 'UTC+9');
insert into 部門資料表 values (1, '業務部', '台北'), (2, '研發部', '東京');
insert into 員工資料表 values (1, 'Kyle', 1), (2, 'Stitch', 1), (3, 'Sato', 2);
insert into 員工語言表 values (1, '中文'), (1, '英文'), (1, '日文'), (2, '中文'), (3, '英文'), (3, '日文');