在現代資料驅動的企業環境中,資料庫設計的品質直接影響系統的可維護性、效能表現與資料可靠度。從資料型別的精準選擇到資料載入流程的自動化管理,每個環節都需要深思熟慮的設計決策。台灣企業在數位轉型過程中,面臨著日益龐大的資料處理需求,無論是氣象觀測系統的即時資料收集、製造業的生產履歷追蹤,或是選舉投票系統的選民資料管理,都需要建立在穩固的資料庫架構基礎之上。
資料型別的選擇是資料庫設計的第一道關卡,看似簡單的決定往往對系統長遠發展產生深遠影響。以台灣的氣象觀測系統為例,氣象站的經緯度座標需要精確儲存至小數點後四位,若使用不當的資料型別,可能導致觀測站定位偏差數百公尺,進而影響氣象預報的準確性。時間日期資料的處理更是充滿挑戰,MySQL 雖然提供了豐富的時間型別,但在處理非標準格式的時間字串時,開發者需要熟練運用轉換函式才能確保資料正確儲存。
資料載入與驗證流程是確保資料品質的關鍵機制。當系統需要定期從外部來源匯入大量資料時,手動操作不僅效率低落,更容易因人為疏失導致資料錯誤。透過 Bash 指令碼搭配 MySQL 的 LOAD DATA INFILE 指令,可以建立自動化的資料載入管道,並整合驗證邏輯確保資料完整性。在台灣的公部門應用中,選舉投票系統的資料庫設計更展現了資料完整性約束的重要性,從選民資格驗證到選票記錄追蹤,每個環節都需要嚴謹的資料庫結構支撐。本文將從實務角度深入探討這些關鍵技術,為讀者建構完整的資料庫工程知識體系。
MySQL 資料型別選擇的精確度考量
資料型別的選擇遠非僅是符合資料範圍這麼簡單,更需要考慮精確度要求、儲存空間消耗、查詢效能影響等多重因素。在處理地理座標資料時,這個議題特別關鍵。台灣中央氣象署的氣象觀測網路遍布全台,每個觀測站都需要精確記錄其地理位置。經度範圍約為東經 118 度至 122 度,緯度範圍約為北緯 21 度至 25 度,若要達到公尺級的定位精確度,小數點後至少需要保留四位數字。
DECIMAL 資料型別在此場景下成為最佳選擇。與 FLOAT 或 DOUBLE 等浮點數型別不同,DECIMAL 採用定點數儲存方式,能夠精確表示指定範圍內的數值,不會產生浮點數運算常見的精確度誤差。對於經度欄位,DECIMAL(7,4) 的定義表示總共可儲存七位數字,其中四位為小數,這樣的設定可以精確儲存從 -180 度到 180 度範圍內的座標,並保持四位小數的精確度。緯度欄位則使用 DECIMAL(6,4),因為緯度範圍較小,整數部分只需兩位即可。
在氣象資料的數值欄位設計上,同樣需要仔細評估精確度需求。氣溫通常以攝氏度為單位,台灣的氣溫範圍大約在零下五度到四十度之間,使用 DECIMAL(5,2) 即可滿足需求,這個定義允許儲存從 -999.99 到 999.99 的數值範圍,足以涵蓋所有可能的氣溫讀數並保留兩位小數精確度。濕度百分比則使用 DECIMAL(5,2),儲存範圍從 0.00 到 100.00。氣壓資料通常以百帕(hPa)為單位,正常範圍約在 950 到 1050 百帕之間,DECIMAL(6,2) 可以提供足夠的範圍與精確度。
-- 氣象觀測資料表結構設計範例
-- 展示如何精確選擇資料型別以確保資料品質與查詢效能
CREATE TABLE weather_observation (
-- 觀測站識別碼:使用 INT 型別作為主鍵
-- AUTO_INCREMENT 確保每個觀測站都有唯一的識別編號
station_id INT PRIMARY KEY AUTO_INCREMENT,
-- 觀測站名稱:台灣氣象站名稱通常不超過 50 字元
-- NOT NULL 約束確保每個觀測站都必須有名稱
station_name VARCHAR(50) NOT NULL,
-- 觀測站所在縣市:使用 CHAR(10) 固定長度儲存
-- 台灣縣市名稱長度相對固定,使用 CHAR 可提升查詢效能
station_city VARCHAR(20) NOT NULL,
-- 觀測站所在行政區:最多 20 字元涵蓋所有台灣鄉鎮市區名稱
station_district VARCHAR(20) NOT NULL,
-- 觀測站緯度:DECIMAL(6,4) 提供精確度至小數點後四位
-- 範圍涵蓋北緯 21 度至 25 度,精確度達公尺級
station_latitude DECIMAL(6,4) NOT NULL,
-- 觀測站經度:DECIMAL(7,4) 提供精確度至小數點後四位
-- 範圍涵蓋東經 118 度至 122 度,精確度達公尺級
station_longitude DECIMAL(7,4) NOT NULL,
-- 觀測站海拔高度:以公尺為單位,DECIMAL(6,2) 涵蓋台灣所有海拔高度
-- 從海平面以下(負值)到玉山頂峰(約 3952 公尺)
station_elevation DECIMAL(6,2),
-- 當前氣溫:攝氏度,DECIMAL(5,2) 儲存範圍 -99.99 到 99.99
-- 涵蓋台灣所有可能的氣溫範圍並保持兩位小數精確度
current_temperature DECIMAL(5,2),
-- 體感溫度:考慮風速與濕度的綜合溫度指標
feels_like_temperature DECIMAL(5,2),
-- 風速:以公尺/秒為單位,DECIMAL(5,2) 儲存至小數點後兩位
-- 範圍涵蓋微風到颱風級強風
wind_speed DECIMAL(5,2),
-- 風向:使用 VARCHAR(10) 儲存方位名稱(如東北風、西南風)
-- 或使用角度值(0-360 度)的文字表示
wind_direction VARCHAR(10),
-- 降雨量:以毫米為單位,DECIMAL(6,2) 可記錄從微量到豪雨的所有降雨量
precipitation DECIMAL(6,2) DEFAULT 0.00,
-- 氣壓:以百帕(hPa)為單位,DECIMAL(6,2) 涵蓋正常氣壓變化範圍
atmospheric_pressure DECIMAL(6,2),
-- 能見度:以公里為單位,DECIMAL(5,2) 記錄從濃霧到晴空的能見度
visibility DECIMAL(5,2),
-- 相對濕度:百分比值,DECIMAL(5,2) 儲存範圍 0.00 到 100.00
relative_humidity DECIMAL(5,2),
-- 天氣描述:文字描述當前天氣狀況(晴天、多雲、雨天等)
weather_description VARCHAR(100),
-- 日出時間:使用 TIME 型別儲存,格式為 HH:MM:SS
sunrise_time TIME,
-- 日落時間:使用 TIME 型別儲存,格式為 HH:MM:SS
sunset_time TIME,
-- 觀測時間:使用 DATETIME 型別記錄精確的觀測時間點
-- 包含年月日與時分秒,支援時間範圍查詢與趨勢分析
observation_datetime DATETIME NOT NULL,
-- 資料建立時間:記錄資料寫入資料庫的時間
-- DEFAULT CURRENT_TIMESTAMP 自動填入當前時間
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 資料更新時間:記錄資料最後修改的時間
-- ON UPDATE CURRENT_TIMESTAMP 在資料更新時自動更新時間戳記
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- 建立複合索引以加速常見的查詢操作
-- 依觀測站與時間查詢是最常見的需求
INDEX idx_station_datetime (station_id, observation_datetime),
-- 地理位置索引,支援空間查詢(需要 MySQL 5.7 以上版本)
-- 可用於查詢特定範圍內的觀測站
INDEX idx_location (station_latitude, station_longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='氣象觀測站即時資料表,儲存台灣各地氣象站的觀測數據';
時間日期處理是資料庫設計中另一個需要特別注意的環節。MySQL 提供了多種時間相關的資料型別,包括 DATE、TIME、DATETIME、TIMESTAMP 等,每種型別都有其適用場景。DATETIME 型別可以儲存從西元 1000 年到 9999 年的日期與時間,精確度到秒,適合用於記錄觀測時間、交易時間等需要完整日期時間資訊的欄位。TIMESTAMP 型別則自動處理時區轉換,並且佔用的儲存空間較小,適合用於記錄資料的建立時間與更新時間。
在處理外部資料來源時,時間格式的轉換經常成為挑戰。氣象觀測資料可能以 YYYYMMDD HH:MM 的格式提供,這種格式並非 MySQL 原生支援的格式,需要透過 STR_TO_DATE() 函式進行轉換。此函式接受兩個參數,第一個是待轉換的時間字串,第二個是格式指定字串,透過 %Y、%m、%d、%H、%i 等格式符號,MySQL 可以正確解析並轉換為 DATETIME 型別。在台灣的實務應用中,考慮到台灣位於東八區,處理時間資料時需特別注意時區設定,確保儲存的時間戳記與實際觀測時間一致。
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 160
package "資料型別設計決策流程" {
card "資料需求分析" as Analysis {
[識別資料屬性]
[確定精確度要求]
[評估資料範圍]
[分析查詢模式]
}
card "型別選擇評估" as Selection {
[數值資料評估]
[時間資料評估]
[文字資料評估]
[空間資料評估]
}
card "儲存最佳化" as Storage {
[計算儲存空間]
[評估索引策略]
[考量壓縮選項]
}
card "效能驗證" as Performance {
[執行查詢測試]
[分析執行計畫]
[監控系統資源]
}
database "資料庫架構" as DB {
frame "數值欄位" {
[DECIMAL 定點數]
[INT 整數]
[BIGINT 長整數]
}
frame "時間欄位" {
[DATETIME 日期時間]
[TIMESTAMP 時間戳記]
[DATE 純日期]
}
frame "文字欄位" {
[VARCHAR 變長字串]
[CHAR 固定長度]
[TEXT 長文字]
}
}
}
[識別資料屬性] --> [確定精確度要求]
[確定精確度要求] --> [評估資料範圍]
[評估資料範圍] --> [分析查詢模式]
Analysis --> Selection
[數值資料評估] --> [DECIMAL 定點數]
[數值資料評估] --> [INT 整數]
[時間資料評估] --> [DATETIME 日期時間]
[時間資料評估] --> [TIMESTAMP 時間戳記]
[文字資料評估] --> [VARCHAR 變長字串]
[文字資料評估] --> [CHAR 固定長度]
Selection --> Storage
[計算儲存空間] --> [評估索引策略]
[評估索引策略] --> [考量壓縮選項]
Storage --> Performance
[執行查詢測試] --> [分析執行計畫]
[分析執行計畫] --> [監控系統資源]
Performance --> DB
note right of Analysis
深入分析資料特性是
正確選擇資料型別的
關鍵第一步
end note
note bottom of DB
合適的資料型別選擇
直接影響系統的效能、
儲存成本與資料品質
end note
@enduml自動化 ETL 資料載入與驗證機制
資料載入流程的自動化是確保資料時效性與準確性的關鍵。在台灣的氣象觀測系統中,各地觀測站每小時產生新的觀測資料,這些資料需要及時載入至中央資料庫供後續分析使用。手動處理這樣的工作負荷既不實際也容易出錯,透過 Bash 指令碼搭配 MySQL 的 LOAD DATA INFILE 指令,可以建立穩定可靠的自動化載入管道。
LOAD DATA INFILE 是 MySQL 提供的高效率批次載入工具,相較於逐筆執行 INSERT 陳述式,批次載入的效能可以提升數十倍甚至上百倍。這個指令可以直接讀取 CSV 格式的資料檔案,並根據指定的欄位分隔符號與換行符號解析資料內容。在使用時需要注意檔案權限設定,MySQL 伺服器需要有讀取來源檔案的權限。若檔案位於客戶端而非伺服器端,則需要使用 LOAD DATA LOCAL INFILE 指令,並在連線時啟用 local_infile 選項。
資料驗證是載入流程中不可或缺的環節。原始資料可能存在格式錯誤、數值超出範圍、必要欄位缺失等問題,若直接載入至正式資料表,可能污染整個資料集。較穩健的做法是採用兩階段載入策略:首先將資料載入至暫存表,執行完整的資料驗證與清洗作業,確認無誤後再複製至正式資料表。這種方式雖然增加了處理步驟,但大幅提升了資料品質的可靠性。驗證邏輯可以包括數值範圍檢查、必要欄位完整性驗證、參照完整性檢查等,透過 SQL 查詢即可實作。
#!/bin/bash
##############################################################################
# 氣象資料自動化載入處理腳本
#
# 功能說明:
# 1. 檢查待處理的氣象觀測資料檔案是否存在
# 2. 執行資料載入至暫存表
# 3. 驗證載入的資料品質
# 4. 將驗證通過的資料複製至正式資料表
# 5. 歸檔已處理的資料檔案
#
# 執行方式:透過 crontab 設定定時執行
# 範例:*/5 * * * * /opt/weather/scripts/load_weather_data.sh
#
# 作者:玄貓(BlackCat)
# 版本:1.0
# 最後更新:2025-12-01
##############################################################################
# 設定工作目錄為氣象資料存放路徑
WORK_DIR="/opt/weather/data"
cd "${WORK_DIR}" || exit 1
# 設定資料檔案名稱
DATA_FILE="weather_observation.csv"
# 設定日誌檔案路徑
LOG_DIR="/var/log/weather"
LOAD_LOG="${LOG_DIR}/load_weather.log"
COPY_LOG="${LOG_DIR}/copy_weather.log"
ERROR_LOG="${LOG_DIR}/error_weather.log"
# 設定 MySQL 連線參數
# 實務上應使用環境變數或配置檔管理敏感資訊
MYSQL_HOST="127.0.0.1"
MYSQL_PORT="3306"
MYSQL_DB="weather_system"
MYSQL_USER="weather_app"
# 密碼應透過 mysql_config_editor 或環境變數設定,此處僅為示範
MYSQL_PASS="SecurePassword123"
# 建立 MySQL 連線指令基本參數
MYSQL_CMD="mysql --host=${MYSQL_HOST} --port=${MYSQL_PORT} \
--database=${MYSQL_DB} --user=${MYSQL_USER} \
--password=${MYSQL_PASS} --silent"
# 記錄腳本執行開始時間
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 開始執行資料載入流程" >> "${ERROR_LOG}"
# 檢查資料檔案是否存在
if [ ! -f "${DATA_FILE}" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 警告:資料檔案 ${DATA_FILE} 不存在,結束執行" >> "${ERROR_LOG}"
exit 0
fi
# 檢查資料檔案是否為空
if [ ! -s "${DATA_FILE}" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 警告:資料檔案 ${DATA_FILE} 為空檔案,結束執行" >> "${ERROR_LOG}"
exit 0
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 發現資料檔案:${DATA_FILE}" >> "${ERROR_LOG}"
# 執行第一階段:載入資料至暫存表
# 使用 --local-infile=1 啟用本地檔案載入功能
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 階段一:載入資料至暫存表" >> "${ERROR_LOG}"
${MYSQL_CMD} --local-infile=1 <<EOF > "${LOAD_LOG}" 2>&1
-- 清空暫存表,確保乾淨的載入環境
TRUNCATE TABLE weather_observation_staging;
-- 使用 LOAD DATA LOCAL INFILE 批次載入資料
-- IGNORE 1 ROWS 跳過 CSV 檔案的標題列
LOAD DATA LOCAL INFILE '${WORK_DIR}/${DATA_FILE}'
INTO TABLE weather_observation_staging
FIELDS TERMINATED BY ',' -- 欄位分隔符號為逗號
OPTIONALLY ENCLOSED BY '"' -- 文字欄位可選用雙引號包圍
LINES TERMINATED BY '\n' -- 換行符號為 Unix 格式
IGNORE 1 ROWS -- 跳過標題列
(station_id, station_name, station_city, station_district,
@latitude, @longitude, @elevation,
@temperature, @feels_like, @wind_speed, wind_direction,
@precipitation, @pressure, @visibility, @humidity,
weather_description, @sunrise, @sunset, @obs_datetime)
SET
-- 使用 STR_TO_DATE 函式轉換時間格式
observation_datetime = STR_TO_DATE(@obs_datetime, '%Y%m%d %H:%i'),
-- 數值欄位轉換,處理可能的空值情況
station_latitude = NULLIF(@latitude, ''),
station_longitude = NULLIF(@longitude, ''),
station_elevation = NULLIF(@elevation, ''),
current_temperature = NULLIF(@temperature, ''),
feels_like_temperature = NULLIF(@feels_like, ''),
wind_speed = NULLIF(@wind_speed, ''),
precipitation = NULLIF(@precipitation, ''),
atmospheric_pressure = NULLIF(@pressure, ''),
visibility = NULLIF(@visibility, ''),
relative_humidity = NULLIF(@humidity, ''),
sunrise_time = STR_TO_DATE(@sunrise, '%H:%i:%s'),
sunset_time = STR_TO_DATE(@sunset, '%H:%i:%s');
-- 顯示載入過程中的警告訊息
SHOW WARNINGS;
EOF
# 檢查載入日誌是否包含錯誤訊息
if [ -s "${LOAD_LOG}" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 警告:資料載入過程中出現訊息,詳見 ${LOAD_LOG}" >> "${ERROR_LOG}"
cat "${LOAD_LOG}" >> "${ERROR_LOG}"
# 若發現嚴重錯誤,終止執行
if grep -qi "error" "${LOAD_LOG}"; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 錯誤:資料載入失敗,終止執行" >> "${ERROR_LOG}"
exit 1
fi
fi
# 執行第二階段:資料驗證與複製至正式表
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 階段二:驗證資料並複製至正式表" >> "${ERROR_LOG}"
${MYSQL_CMD} <<EOF > "${COPY_LOG}" 2>&1
-- 資料品質驗證查詢
-- 檢查必要欄位是否有空值
SELECT
COUNT(*) as null_station_count
FROM weather_observation_staging
WHERE station_id IS NULL
OR station_name IS NULL
OR observation_datetime IS NULL;
-- 檢查數值範圍是否合理
SELECT
COUNT(*) as invalid_temperature_count
FROM weather_observation_staging
WHERE current_temperature < -50
OR current_temperature > 60;
-- 若驗證通過,將資料插入正式表
-- 使用 INSERT IGNORE 避免重複資料造成錯誤
INSERT IGNORE INTO weather_observation
SELECT * FROM weather_observation_staging
WHERE station_id IS NOT NULL
AND station_name IS NOT NULL
AND observation_datetime IS NOT NULL
AND (current_temperature IS NULL
OR (current_temperature >= -50 AND current_temperature <= 60));
-- 回報處理結果
SELECT ROW_COUNT() as rows_inserted;
EOF
# 檢查複製結果
if [ $? -eq 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 成功:資料已複製至正式表" >> "${ERROR_LOG}"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 錯誤:資料複製失敗" >> "${ERROR_LOG}"
exit 1
fi
# 歸檔已處理的資料檔案
# 使用時間戳記重新命名,保留處理記錄
ARCHIVE_DIR="/opt/weather/archive"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
ARCHIVE_FILE="${ARCHIVE_DIR}/weather_observation_${TIMESTAMP}.csv"
mkdir -p "${ARCHIVE_DIR}"
mv "${DATA_FILE}" "${ARCHIVE_FILE}"
if [ $? -eq 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 資料檔案已歸檔:${ARCHIVE_FILE}" >> "${ERROR_LOG}"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 警告:資料檔案歸檔失敗" >> "${ERROR_LOG}"
fi
# 清理超過 30 天的歷史歸檔檔案
find "${ARCHIVE_DIR}" -name "weather_observation_*.csv" -mtime +30 -delete
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 資料載入流程執行完成" >> "${ERROR_LOG}"
echo "----------------------------------------" >> "${ERROR_LOG}"
exit 0
排程執行機制讓資料載入流程能夠完全自動化運作。在 Linux 系統中,cron 服務提供了強大的定時任務管理功能。透過 crontab 設定,可以精確控制腳本的執行時間與頻率。氣象資料通常每小時更新一次,因此可以設定每小時的第五分鐘執行載入腳本,確保資料及時進入系統。crontab 的時間格式由五個欄位組成,分別代表分鐘、小時、日期、月份、星期,透過不同的組合可以實現各種複雜的排程需求。
在實務應用中,腳本的錯誤處理與日誌記錄同樣重要。腳本應該能夠處理各種異常狀況,例如檔案不存在、資料庫連線失敗、磁碟空間不足等,並將錯誤資訊詳細記錄至日誌檔案。日誌檔案應包含時間戳記、操作類型、處理結果等資訊,方便事後追蹤與除錯。對於關鍵性的資料載入作業,還應該設定告警機制,當載入失敗或資料異常時,立即通知系統管理員處理。
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 150
participant "cron 排程器" as Cron
participant "Bash 載入腳本" as Script
participant "檔案系統" as FileSystem
database "暫存資料表" as StagingTable
database "正式資料表" as ProductionTable
participant "日誌系統" as Logger
participant "告警服務" as Alert
activate Cron
Cron -> Script: 定時觸發執行\n(每小時第 5 分鐘)
activate Script
Script -> FileSystem: 檢查資料檔案\nweather_observation.csv
activate FileSystem
alt 檔案存在且非空
FileSystem --> Script: 回傳檔案路徑
Script -> Script: 建立 MySQL 連線\n設定載入參數
Script -> StagingTable: TRUNCATE TABLE\n清空暫存表
activate StagingTable
StagingTable --> Script: 清空完成
Script -> StagingTable: LOAD DATA LOCAL INFILE\n批次載入 CSV 資料
StagingTable -> StagingTable: 解析 CSV 格式\n轉換資料型別\n處理時間欄位
StagingTable --> Script: 載入完成\n回傳 WARNINGS
Script -> Logger: 記錄載入結果\n(筆數、警告訊息)
activate Logger
Script -> StagingTable: 執行資料驗證\n檢查必要欄位\n驗證數值範圍
StagingTable --> Script: 回傳驗證結果
alt 驗證通過
Script -> ProductionTable: INSERT IGNORE\nSELECT FROM staging
activate ProductionTable
ProductionTable -> ProductionTable: 檢查重複資料\n執行資料插入\n更新索引
ProductionTable --> Script: 插入成功\n影響列數
Script -> Logger: 記錄成功訊息\n插入筆數統計
Script -> FileSystem: 歸檔資料檔案\nmv to archive/
FileSystem --> Script: 歸檔完成
Script -> Logger: 記錄完整流程\n執行時間統計
deactivate ProductionTable
else 驗證失敗
Script -> Logger: 記錄驗證錯誤\n詳細錯誤內容
Script -> Alert: 發送告警通知\n資料品質問題
activate Alert
Alert -> Alert: 發送電子郵件\nSlack 訊息推播
deactivate Alert
Script -> FileSystem: 移動至錯誤目錄\nmv to error/
end
deactivate StagingTable
deactivate Logger
else 檔案不存在或為空
FileSystem --> Script: 檔案不存在
Script -> Logger: 記錄警告訊息\n無資料需要處理
activate Logger
deactivate Logger
end
deactivate FileSystem
Script -> Script: 清理暫存檔案\n釋放系統資源
Script --> Cron: 執行完成\n回傳狀態碼
deactivate Script
deactivate Cron
note over Cron, Alert
完整的 ETL 自動化流程包含排程觸發、
檔案檢查、資料載入、品質驗證、
錯誤處理、日誌記錄與告警通知,
確保資料處理的可靠性與可追溯性
end note
@enduml選舉投票系統的資料庫架構設計
選舉投票系統的資料庫設計是資料完整性約束的最佳實踐範例。在台灣的民主選舉中,從總統大選到地方議員選舉,每次投票都涉及數百萬選民的資料管理與數十萬張選票的記錄追蹤。資料庫架構必須確保每一筆資料的準確性與可追溯性,同時滿足選務資訊系統對效能與安全性的嚴格要求。這種系統的設計展現了關聯式資料庫的核心價值:透過主鍵、外鍵、唯一性約束等機制,從結構層面保障資料的正確性。
選民資料表是整個系統的基礎,儲存每位選民的基本資訊與投票資格相關資料。主鍵 voter_id 採用自動遞增整數,確保每位選民都有唯一的系統識別碼。選民登記號碼是另一個關鍵欄位,這個號碼由戶政機關核發,具有唯一性,因此設定 UNIQUE 約束確保不會有重複登記的情況。選民的戶籍資料包括縣市、選區、投開票所等資訊,這些資料決定選民應該在哪裡投票、可以投哪些候選人的票。政黨傾向欄位設定為可空值,因為選民有權不表明政黨傾向。
選票資料表記錄每一張投出的選票資訊。每張選票都與一位選民關聯,透過 voter_id 外鍵確保參照完整性。選票號碼由投票機器產生,用於追蹤實體選票與電子記錄的對應關係。投票時間與投票方式(親自投票或通訊投票)的記錄對於選務稽核極為重要。外鍵約束確保只有合法登記的選民才能產生選票記錄,若嘗試插入不存在的 voter_id,資料庫會拒絕該操作。
-- 選舉投票系統資料庫架構完整設計
-- 展示主鍵、外鍵、唯一性約束、檢查約束等資料完整性機制
-- 建立投票系統資料庫
CREATE DATABASE IF NOT EXISTS election_system
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci
COMMENT '台灣選舉投票管理系統';
USE election_system;
-- 選民資料表:儲存所有合格選民的基本資訊與投票資格
CREATE TABLE voter (
-- 選民系統識別碼:自動遞增主鍵,確保每位選民的唯一性
voter_id INT PRIMARY KEY AUTO_INCREMENT
COMMENT '選民唯一識別碼',
-- 選民姓名:依據個人資料保護法,姓名屬於敏感資訊需加密儲存
voter_name VARCHAR(50) NOT NULL
COMMENT '選民姓名(實務上應加密儲存)',
-- 戶籍地址:完整戶籍地址,決定投票資格與選區
voter_address VARCHAR(200) NOT NULL
COMMENT '戶籍地址',
-- 縣市:台灣行政區劃第一層級
voter_county VARCHAR(20) NOT NULL
COMMENT '戶籍所在縣市',
-- 選區:立委或議員選舉的選區編號
voter_district VARCHAR(10) NOT NULL
COMMENT '選舉區代碼',
-- 投開票所:選民應前往投票的投開票所編號
voter_precinct VARCHAR(10) NOT NULL
COMMENT '投開票所編號',
-- 政黨傾向:選民登記的政黨傾向,可為空值(選民有權不表態)
voter_party VARCHAR(30) NULL
COMMENT '政黨傾向(可為空)',
-- 投票地點:投開票所的完整地址
voting_location VARCHAR(150) NOT NULL
COMMENT '投票地點地址',
-- 選民登記號碼:由戶政機關核發的唯一選民編號
-- UNIQUE 約束確保不會有重複登記
voter_registration_number VARCHAR(20) NOT NULL UNIQUE
COMMENT '選民登記證號碼(唯一)',
-- 選民資格狀態:啟用或停權(例如因刑事判決停權)
voter_status ENUM('active', 'suspended', 'deceased')
NOT NULL DEFAULT 'active'
COMMENT '選民資格狀態',
-- 資料建立時間
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
COMMENT '資料建立時間',
-- 資料更新時間
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
COMMENT '資料最後更新時間',
-- 建立複合索引加速依縣市、選區查詢
INDEX idx_county_district (voter_county, voter_district),
-- 投開票所索引,方便統計各所選民人數
INDEX idx_precinct (voter_precinct)
) ENGINE=InnoDB
COMMENT='選民基本資料表,儲存所有合格選民資訊';
-- 選票資料表:記錄每一張投出的選票
CREATE TABLE ballot (
-- 選票系統識別碼
ballot_id INT PRIMARY KEY AUTO_INCREMENT
COMMENT '選票唯一識別碼',
-- 選民識別碼:外鍵關聯至選民表
-- UNIQUE 約束確保一位選民只能投一張票
voter_id INT NOT NULL UNIQUE
COMMENT '投票選民 ID(外鍵,唯一)',
-- 選票號碼:實體選票上的序號,用於對帳
ballot_number VARCHAR(30) NOT NULL UNIQUE
COMMENT '選票序號(唯一)',
-- 投票時間:精確記錄投票的時間點
cast_datetime DATETIME NOT NULL
COMMENT '投票時間',
-- 投票方式:親自投票或通訊投票
cast_method ENUM('in_person', 'absentee') NOT NULL
COMMENT '投票方式',
-- 資料建立時間
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
COMMENT '記錄建立時間',
-- 外鍵約束:確保投票的選民必須存在於選民表中
CONSTRAINT fk_ballot_voter
FOREIGN KEY (voter_id)
REFERENCES voter(voter_id)
ON DELETE RESTRICT -- 禁止刪除已投票的選民記錄
ON UPDATE CASCADE, -- 選民 ID 更新時同步更新
-- 投票時間索引,方便分析投票時段分布
INDEX idx_cast_datetime (cast_datetime)
) ENGINE=InnoDB
COMMENT='選票記錄表,儲存每張選票的投票資訊';
-- 選舉項目資料表:定義本次選舉包含的各項選舉
CREATE TABLE race (
-- 選舉項目識別碼
race_id INT PRIMARY KEY AUTO_INCREMENT
COMMENT '選舉項目唯一識別碼',
-- 選舉項目名稱:例如「台北市市長」、「立法委員第一選區」
race_name VARCHAR(100) NOT NULL UNIQUE
COMMENT '選舉項目名稱(唯一)',
-- 選舉類型:總統、立委、縣市長、議員等
race_type ENUM('president', 'legislator', 'mayor', 'councilor', 'other')
NOT NULL
COMMENT '選舉類型',
-- 允許投票數:單選為 1,複選則為可投票數
votes_allowed INT NOT NULL DEFAULT 1
COMMENT '允許投票數(單選=1)',
-- 選舉日期
election_date DATE NOT NULL
COMMENT '選舉投票日',
-- 資料建立時間
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
COMMENT '資料建立時間',
-- 檢查約束:確保允許投票數為正整數
CONSTRAINT chk_votes_allowed
CHECK (votes_allowed > 0)
) ENGINE=InnoDB
COMMENT='選舉項目定義表,列舉本次選舉的所有選舉項目';
-- 候選人資料表:儲存參選人資訊
CREATE TABLE candidate (
-- 候選人識別碼
candidate_id INT PRIMARY KEY AUTO_INCREMENT
COMMENT '候選人唯一識別碼',
-- 選舉項目識別碼:候選人參選的項目
race_id INT NOT NULL
COMMENT '參選項目 ID(外鍵)',
-- 候選人姓名
candidate_name VARCHAR(50) NOT NULL
COMMENT '候選人姓名',
-- 候選人號次:選票上的號碼
candidate_number INT NOT NULL
COMMENT '候選人號次',
-- 候選人地址
candidate_address VARCHAR(200) NOT NULL
COMMENT '候選人戶籍地址',
-- 政黨:候選人所屬政黨,無黨籍可為空
candidate_party VARCHAR(30) NULL
COMMENT '所屬政黨(無黨籍可空)',
-- 現任標誌:是否為現任民意代表
incumbent_flag BOOLEAN DEFAULT FALSE
COMMENT '現任標誌',
-- 資料建立時間
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
COMMENT '資料建立時間',
-- 外鍵約束:確保候選人參選的項目必須存在
CONSTRAINT fk_candidate_race
FOREIGN KEY (race_id)
REFERENCES race(race_id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
-- 唯一性約束:同一選舉項目中候選人號次不可重複
CONSTRAINT uk_race_number
UNIQUE (race_id, candidate_number),
-- 選舉項目索引
INDEX idx_race (race_id)
) ENGINE=InnoDB
COMMENT='候選人資料表,儲存參選人基本資訊';
-- 選票明細資料表:記錄選票上勾選的候選人
CREATE TABLE ballot_candidate (
-- 選票識別碼
ballot_id INT NOT NULL
COMMENT '選票 ID(外鍵)',
-- 候選人識別碼
candidate_id INT NOT NULL
COMMENT '候選人 ID(外鍵)',
-- 勾選時間:記錄該候選人被勾選的時間
selected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
COMMENT '勾選時間',
-- 複合主鍵:確保同一張選票不會重複勾選同一位候選人
PRIMARY KEY (ballot_id, candidate_id),
-- 外鍵約束:確保選票必須存在
CONSTRAINT fk_bc_ballot
FOREIGN KEY (ballot_id)
REFERENCES ballot(ballot_id)
ON DELETE CASCADE -- 選票刪除時一併刪除明細
ON UPDATE CASCADE,
-- 外鍵約束:確保候選人必須存在
CONSTRAINT fk_bc_candidate
FOREIGN KEY (candidate_id)
REFERENCES candidate(candidate_id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
-- 候選人索引,方便統計得票數
INDEX idx_candidate (candidate_id)
) ENGINE=InnoDB
COMMENT='選票明細表,記錄選票上勾選的候選人';
-- 選民資料變更歷程表:追蹤選民資料的所有變更記錄
CREATE TABLE voter_change_log (
-- 變更記錄識別碼
log_id BIGINT PRIMARY KEY AUTO_INCREMENT
COMMENT '變更記錄唯一識別碼',
-- 選民識別碼
voter_id INT NOT NULL
COMMENT '變更的選民 ID',
-- 變更類型:新增、更新、刪除
change_type ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL
COMMENT '變更類型',
-- 變更欄位名稱
field_name VARCHAR(50) NULL
COMMENT '變更的欄位名稱',
-- 變更前值
old_value VARCHAR(200) NULL
COMMENT '變更前的值',
-- 變更後值
new_value VARCHAR(200) NULL
COMMENT '變更後的值',
-- 變更原因
change_reason VARCHAR(200) NULL
COMMENT '變更原因說明',
-- 變更執行者
changed_by VARCHAR(50) NOT NULL
COMMENT '執行變更的使用者',
-- 變更時間
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
COMMENT '變更執行時間',
-- 選民索引
INDEX idx_voter (voter_id),
-- 變更時間索引
INDEX idx_changed_at (changed_at)
) ENGINE=InnoDB
COMMENT='選民資料變更歷程表,記錄所有資料異動';
候選人資料表與選舉項目資料表的設計展現了資料正規化的原則。選舉項目表定義本次選舉包含哪些項目,例如總統選舉、立法委員選舉、縣市長選舉等。每個選舉項目都有其允許投票數,單選項目為一票,複選項目則可能允許投多票。候選人表透過 race_id 外鍵關聯至選舉項目表,確保每位候選人都參選特定的項目。複合唯一性約束 (race_id, candidate_number) 確保同一選舉項目中不會有重複的候選人號次。
選票明細表採用多對多關聯的設計模式,連接選票與候選人兩個實體。這個表的主鍵是 ballot_id 與 candidate_id 的組合,確保一張選票不會重複勾選同一位候選人。透過這個架構,系統可以精確記錄每張選票選了哪些候選人,同時也能快速統計每位候選人的得票數。外鍵的刪除與更新規則設定也很關鍵:選票刪除時應該連帶刪除其明細記錄(CASCADE),但候選人不能隨意刪除(RESTRICT),確保資料的歷史完整性。
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 13
skinparam minClassWidth 140
entity "voter\n選民資料表" as voter {
* voter_id : INT <<PK>>
--
* voter_name : VARCHAR(50)
* voter_address : VARCHAR(200)
* voter_county : VARCHAR(20)
* voter_district : VARCHAR(10)
* voter_precinct : VARCHAR(10)
voter_party : VARCHAR(30)
* voting_location : VARCHAR(150)
* voter_registration_number : VARCHAR(20) <<UK>>
* voter_status : ENUM
created_at : TIMESTAMP
updated_at : TIMESTAMP
}
entity "ballot\n選票資料表" as ballot {
* ballot_id : INT <<PK>>
--
* voter_id : INT <<FK>> <<UK>>
* ballot_number : VARCHAR(30) <<UK>>
* cast_datetime : DATETIME
* cast_method : ENUM
created_at : TIMESTAMP
}
entity "race\n選舉項目表" as race {
* race_id : INT <<PK>>
--
* race_name : VARCHAR(100) <<UK>>
* race_type : ENUM
* votes_allowed : INT
* election_date : DATE
created_at : TIMESTAMP
}
entity "candidate\n候選人資料表" as candidate {
* candidate_id : INT <<PK>>
--
* race_id : INT <<FK>>
* candidate_name : VARCHAR(50)
* candidate_number : INT
* candidate_address : VARCHAR(200)
candidate_party : VARCHAR(30)
incumbent_flag : BOOLEAN
created_at : TIMESTAMP
}
entity "ballot_candidate\n選票明細表" as ballot_candidate {
* ballot_id : INT <<PK,FK>>
* candidate_id : INT <<PK,FK>>
--
selected_at : TIMESTAMP
}
entity "voter_change_log\n選民變更記錄表" as log {
* log_id : BIGINT <<PK>>
--
* voter_id : INT
* change_type : ENUM
field_name : VARCHAR(50)
old_value : VARCHAR(200)
new_value : VARCHAR(200)
change_reason : VARCHAR(200)
* changed_by : VARCHAR(50)
changed_at : TIMESTAMP
}
voter ||--o{ ballot : "一位選民\n投一張票"
ballot ||--|{ ballot_candidate : "一張選票\n選多位候選人"
candidate ||--|{ ballot_candidate : "一位候選人\n被多張票選"
race ||--|{ candidate : "一個項目\n多位候選人"
voter ||--o{ log : "記錄所有\n資料變更"
note top of voter
選民表是系統的核心
主鍵確保選民唯一性
登記號碼具有業務唯一性
狀態欄位管理投票資格
end note
note top of ballot
選票表記錄投票行為
voter_id 外鍵且唯一
確保一人一票原則
選票號碼用於對帳
end note
note bottom of race
選舉項目表定義本次
選舉包含的所有項目
votes_allowed 控制
單選或複選規則
end note
note bottom of candidate
候選人表儲存參選人資訊
race_id 外鍵關聯選舉項目
複合唯一約束確保
號次不重複
end note
note right of ballot_candidate
選票明細表實現
多對多關聯設計
複合主鍵防止
重複勾選
end note
note right of log
變更記錄表提供
完整的稽核軌跡
記錄所有資料異動
支援爭議追溯
end note
}
}
}
}
}
@enduml資料變更追蹤機制是選舉系統不可或缺的功能。當選民遷移戶籍、變更政黨登記,或因任何原因需要修改資料時,系統必須詳細記錄變更前後的值、變更原因、執行變更的人員與時間。這些歷史記錄在選舉爭議處理、資料稽核、法律訴訟等場景中都可能成為關鍵證據。實作方式可以透過資料庫觸發器(Trigger)自動捕捉變更,或在應用程式層面明確寫入變更日誌表。觸發器的優勢在於不會遺漏任何資料庫層級的變更,即使是直接透過 SQL 指令修改資料也能被記錄。應用程式層面的記錄則提供更大的彈性,可以加入業務邏輯判斷、變更原因說明等額外資訊。
MySQL 資料庫的架構設計與資料處理自動化是確保系統穩定運作的基石。從資料型別的精確選擇到外鍵約束的合理設定,從批次載入的效能最佳化到變更追蹤的完整記錄,每個環節都需要深入的技術理解與實務經驗。台灣企業在推動數位轉型的過程中,應該重視資料庫工程的專業性,投入足夠的資源建立穩固的資料基礎設施。氣象觀測系統展示了如何處理高頻率的時序資料,選舉投票系統則展現了資料完整性約束在關鍵業務中的應用價值。透過自動化的 ETL 流程,系統能夠可靠地處理日常的資料載入需求,減少人為錯誤並提升運作效率。這些技術與經驗不僅適用於特定領域,更是各行各業在建構資料驅動型應用時的重要參考。