資料正規化完整指南:從 JSON 數據到結構化資料
更新日期: 2025 年 3 月 4 日
本文為 資料庫正規化 基本介紹系列文,第 2 篇:
在處理數據時,尤其是來自 JSON 檔案 的資料,經常會遇到 巢狀結構、不規則的欄位名稱,以及重複資訊。
如果數據沒有經過良好的整理與規範,不僅難以閱讀,也不易存入關聯式資料庫(如 MySQL、PostgreSQL)。
因此,我們需要透過 資料正規化(Data Normalization),將數據轉換成 有組織的結構,以利於後續查詢與分析。
本指南將 逐步解析 JSON 正規化的過程,並以 實際案例 為例,帶領你完成整個數據整理流程!
確認 JSON 資料結構
在進行正規化前,第一步是先 檢視 JSON 檔案的結構,確認它是 扁平結構(key-value)還是巢狀結構。
檢查要點
- 是否為單層 JSON?
- 例如:
{ "id": 1, "name": "Alice" }
- 例如:
- 是否為巢狀結構?
- 例如:
{ "user_id": 1, "name": "Alice", "orders": [ {"order_id": 101, "total": 50}, {"order_id": 102, "total": 30} ] }
- 例如:
- 是否有重複資訊?
- 若
location
出現在多個地方,可能需要拆分成獨立表格。
- 若
解析 JSON 的方法
- 使用 JSON Formatter(如 jsonformatter.org)視覺化數據。
- 使用 Python 美化輸出 JSON:
import json
with open("data.json") as f:
data = json.load(f)
print(json.dumps(data, indent=2))
- 用
pandas.json_normalize()
攤平成表格:
import pandas as pd
df = pd.json_normalize(data)
print(df.head())
找出數據間的關聯性
資料正規化的目標是 消除重複資訊,建立數據之間的關聯性,因此我們需要 識別主鍵(PK)與外鍵(FK)。
主要步驟
- 確認主鍵(Primary Key, PK)
- 找出唯一識別碼(如
UID
、id
)。
- 找出唯一識別碼(如
- 識別外鍵(Foreign Key, FK)
- 若
user_id
出現在orders
內,就應該成為外鍵。
- 若
- 拆分巢狀結構
- 例如
orders
內的資料應拆分到獨立表格。
- 例如
🎯 解析範例
原始 JSON
{
"UID": "669eab3226b3240c380463f1",
"title": "新觀點音樂會",
"showInfo": [
{
"time": "2025/02/19 19:30:00",
"location": "高雄市鳳山區三多一路1號",
"locationName": "衛武營國家藝術文化中心表演廳"
}
]
}
正規化後
1️⃣ events
表:
UID | title |
---|---|
669eab3 | 新觀點音樂會 |
2️⃣ show_info
表:
showID | UID | time | location | locationName |
---|---|---|---|---|
1 | 669eab3 | 2025-02-19 19:30 | 高雄市鳳山區 | 衛武營 |
這樣一來,每場 showInfo
獨立存放,避免冗餘數據。
清理 & 重構數據
數據整理後,我們還需要確保:
- 欄位名稱一致(避免
UserID
vsuser_id
)。 - 避免重複存放數據(
locationName
不應存於show_info
)。 - 統一數據格式(如
日期格式 YYYY-MM-DD HH:MM:SS
)。
轉換成適合的結構
數據整理完成後,可將其轉換為不同格式:
- 存入 MySQL/PostgreSQL
CREATE TABLE events (
UID VARCHAR(255) PRIMARY KEY,
title TEXT
);
CREATE TABLE show_info (
showID INT PRIMARY KEY AUTO_INCREMENT,
UID VARCHAR(255),
time DATETIME,
location TEXT,
FOREIGN KEY (UID) REFERENCES events(UID)
);
- 轉為 DataFrame(方便分析)
df = pd.DataFrame(data)
- 存為 CSV 或 SQL
df.to_csv("data.csv", index=False) # 存成 CSV
df.to_sql("table_name", conn, if_exists="replace") # 存進 SQL
資料正規化:以「音樂表演資訊 | 政府資料開放平臺 (data.gov.tw)」為例
接下來,我們將以政府資料開放平臺(data.gov.tw)所提供的音樂表演資訊為範例。
逐步展示如何將 JSON 數據正規化,從第一正規化(1NF)到第三正規化(3NF),讓數據變得更加結構化,方便儲存、查詢與分析。
原始資料
政府資料開放平臺提供的 JSON 檔案包含了音樂表演資訊,其中包含演出名稱、演出時間、地點、售票資訊等,例如:
{
"UID": "669eab3226b3240c380463f1",
"title": "新觀點.新世界~Kimball Gallagher 2024台灣巡迴音樂會",
"category": "1",
"showInfo": [
{
"time": "2025/02/19 19:30:00",
"location": "高雄市鳳山區三多一路1號",
"locationName": "衛武營國家藝術文化中心表演廳",
"onSales": "Y",
"price": "",
"latitude": "22.6230179238508",
"longitude": "120.342434118507",
"endTime": "2025/02/19 21:05:00"
}
],
"startDate": "2025/02/19",
"endDate": "2025/02/19",
"webSales": "https://www.opentix.life/program/1811063063948378113"
}
這段 JSON 包含了一場音樂表演的基本資訊,但有一些問題需要正規化來解決:
- 巢狀結構(Nested Structure):
showInfo
是一個陣列,包含演出時間、地點等資訊,這在關聯式資料庫中不易直接存放。 - 冗餘資訊:如果多場表演在相同地點,則
locationName
、latitude
和longitude
可能會重複存儲。 - 部分函數依賴(Partial Dependency):
locationName
依賴於location
,而不是UID
,這違反 2NF。 - 傳遞依賴(Transitive Dependency):
latitude
和longitude
依賴location
,這違反 3NF。
我們將按照 1NF → 2NF → 3NF 的方式來優化這組數據。
第一正規化(1NF)
條件
✅ 每個欄位只能有單一值,不能有巢狀結構。
問題點
"showInfo": [
{
"time": "2025/02/19 19:30:00",
"location": "高雄市鳳山區三多一路1號",
"locationName": "衛武營國家藝術文化中心表演廳",
"onSales": "Y",
"latitude": "22.6230179238508",
"longitude": "120.342434118507"
}
]
🔴 showInfo
為巢狀陣列,違反 1NF。
修正後
將 showInfo
拆分為獨立的 show_info
表,確保每筆資料為單一值:
UID | show_time | location | locationName | onSales | latitude | longitude |
---|---|---|---|---|---|---|
669eab3226b3240c380463f1 | 2025/02/19 19:30:00 | 高雄市鳳山區三多一路1號 | 衛武營國家藝術文化中心表演廳 | Y | 22.6230179238508 | 120.342434118507 |
第二正規化(2NF)
條件
✅ 非主鍵欄位需完全依賴主鍵,而非部分主鍵。
問題點
在原始的 show_info
表中:
UID | show_time | location | locationName |
---|---|---|---|
669eab3 | 2025/02/19 19:30 | 高雄市鳳山區三多一路1號 | 衛武營國家藝術文化中心表演廳 |
UID
(演出活動的唯一識別碼)是show_info
的主鍵。show_time
這個欄位 完全依賴UID
,因為每個表演場次都是與某個活動UID
相關的。- 問題點:
locationName
並不是直接依賴UID
,而是依賴location
。- 錯誤的依賴關係:
UID
→location
→locationName
- 正確的做法:我們應該把
locationName
移到locations
表,這樣show_info
只需存locationID
。
- 錯誤的依賴關係:
修正後
拆分 locations
表:
locationID | location | locationName |
---|---|---|
LOC_001 | 高雄市鳳山區三多一路1號 | 衛武營國家藝術文化中心表演廳 |
更新 show_info
表:
UID | show_time | locationID | onSales |
---|---|---|---|
669eab3226b3240c380463f1 | 2025/02/19 19:30:00 | LOC_001 | Y |
這樣,locationName
就不會依賴 UID
,確保所有非主鍵欄位都完全依賴於 UID
。
這樣的修正確保:
show_info
表中的所有非主鍵欄位都依賴UID
,不再有部分函數依賴的問題。locationName
現在完全依賴於locationID
,存放於locations
表,符合 2NF 規範。
第三正規化(3NF)
條件
✅ 消除傳遞依賴,所有非主鍵欄位應完全依賴主鍵。
問題點
💡 在 2NF 階段,我們已經拆分了 locations 表
在 第二正規化(2NF) 時,我們發現 locationName
並不是 依賴 UID
,而是依賴 location
,所以我們把 locationName
獨立成 locations
表,如下:
📌 修正後的 2NF 表結構
show_info
表
UID | show_time | locationID | onSales |
---|---|---|---|
669eab3 | 2025/02/19 19:30 | LOC_001 | Y |
locations
表
locationID | location | locationName |
---|---|---|
LOC_001 | 高雄市鳳山區三多一路1號 | 衛武營國家藝術文化中心表演廳 |
這樣 locationName
不再依賴 UID
,而是依賴 locationID
,因此符合 2NF。
💡 但 2NF 仍然存在問題
雖然 locationName
被拆開了,但 latitude
和 longitude
仍然存放在 show_info
表中,如下:
2NF 階段的 show_info
表
UID | show_time | locationID | onSales | latitude | longitude |
---|---|---|---|---|---|
669eab3 | 2025/02/19 19:30 | LOC_001 | Y | 22.623018 | 120.342434 |
📌 問題出現了
latitude
和longitude
並不是依賴UID
,而是依賴locationID
,因為同一個locationID
(相同地點)會有相同的經緯度。- 這樣的設計 違反了 3NF,因為
latitude
和longitude
不應該存放在show_info
表,而應該移到locations
表中。
修正後
進一步拆分 locations
表:
在 第三正規化(3NF) 中,我們將 latitude
和 longitude
從 show_info
表移到 locations
表,讓它們只依賴 locationID
。
🔹 show_info
表(修正後)
UID | show_time | locationID | onSales |
---|---|---|---|
669eab3 | 2025/02/19 19:30 | LOC_001 | Y |
🔹 locations
表(修正後)
locationID | location | locationName | latitude | longitude |
---|---|---|---|---|
LOC_001 | 高雄市鳳山區三多一路1號 | 衛武營國家藝術文化中心表演廳 | 22.623018 | 120.342434 |
這樣,latitude
和 longitude
不再存於 show_info
表,而是存於 locations
表,確保所有非主鍵欄位都僅依賴於 locationID
,從而符合 3NF。
補充:2NF 和 3NF 差異
📌 主要差異點:
正規化階段 核心問題 關鍵區別 2NF(第二正規化) 某個欄位依賴表內的「非主鍵欄位」,而不是整個主鍵 這個欄位沒有獨特 ID,所以我們需要「建立新的表」來存放它 3NF(第三正規化) 某個欄位依賴表內的「非主鍵欄位」,但這個非主鍵欄位本身已經有 ID 這個欄位有 ID(如 locationID),但還是間接依賴主鍵,所以「應該移動到正確的表」 🎯 關鍵差異
- 2NF 處理「部分函數依賴」:
- 某些欄位 沒有 ID,但不應該依賴
UID
。- 需要 創建新的表 來存放這些欄位(例如
locations
表)。- 3NF 處理「傳遞依賴」:
- 某些欄位 已經有 ID(例如
locationID
),但還是間接依賴UID
。- 需要 移動欄位到正確的表,避免不必要的依賴。
📌 具體案例分析
假設我們有這張
show_info
表:
UID show_time location locationName latitude longitude 669eab3 2025-02-19 19:30 高雄市鳳山區三多一路1號 衛武營國家藝術文化中心表演廳 22.623018 120.342434 這張表的 主鍵(Primary Key, PK)是
UID
,但有些欄位存在部分函數依賴和傳遞依賴的問題。📌 🎯 第二正規化(2NF)
問題
locationName
依賴location
,但location
不是show_info
的主鍵UID
。- 這代表
locationName
不應該存放在show_info
,因為它不是由UID
決定的,而是由location
決定的。解決方案
✅ 創建
locations
表,把locationName
拆出去。
locationID location locationName LOC_001 高雄市鳳山區三多一路1號 衛武營
UID show_time locationID 669eab3 2025-02-19 19:30 LOC_001 📌 🎯 第三正規化(3NF)
問題
latitude
和longitude
依賴locationID
,而locationID
又依賴UID
,這就是 傳遞依賴。UID → locationID → latitude, longitude
這種 間接依賴 代表latitude
和longitude
不應該存放在show_info
表,而應該存放在locations
表。解決方案
✅ 把
latitude
和longitude
移到locations
表,讓show_info
只存locationID
。
locationID location locationName latitude longitude LOC_001 高雄市鳳山區三多一路1號 衛武營 22.623018 120.342434
UID show_time locationID 669eab3 2025-02-19 19:30 LOC_001
📌 用簡單的話來解釋
💡 2NF
如果一個欄位(如
locationName
)依賴location
,但location
不是show_info
的主鍵。那麼這是部分函數依賴,我們應該 創建新的
locations
表,讓show_info
只存locationID
。💡 3NF
如果一個欄位(如
latitude
和longitude
)依賴locationID
,但locationID
又依賴UID
,這就是傳遞依賴。我們應該 把
latitude
和longitude
移到locations
表,確保show_info
沒有任何location
相關資訊,只存locationID
。
最終結果
events
表(活動基本資訊)
UID | title | category | startDate | endDate | webSales |
---|---|---|---|---|---|
669eab3226b3240c380463f1 | 新觀點.新世界~Kimball Gallagher 2024台灣巡迴音樂會 | 1 | 2025/02/19 | 2025/02/19 | 網址 |
show_info
表(演出資訊)
showID | UID | show_time | locationID | onSales |
---|---|---|---|---|
1 | 669eab3226b3240c380463f1 | 2025/02/19 19:30:00 | LOC_001 | Y |
locations
表(場地資訊)
locationID | location | locationName | latitude | longitude |
---|---|---|---|---|
LOC_001 | 高雄市鳳山區三多一路1號 | 衛武營國家藝術文化中心表演廳 | 22.6230179238508 | 120.342434118507 |
結論
層級 | 修正的問題 | 具體變化 |
---|---|---|
1NF | showInfo 為巢狀陣列 | 拆開 showInfo,每場次獨立存放 |
2NF | locationName 依賴 location,非 UID | 建立 locations 表 |
3NF | latitude 依賴 location,非 UID | locations 表內存 latitude |
透過這些步驟,數據變得更整潔、更有組織,方便後續儲存與分析! 🎶🎻