傳統關係型設計中,用戶地址、遊戲積分記錄等一對多場景需建立獨立表。本文介紹一種「反範式」設計思路:將多行數據序列化為單字段 JSON 字符串,以空間換時間、減少表關聯,適合特定業務場景。
CREATE TABLE user_address (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
name VARCHAR(50) NOT NULL,
phone VARCHAR(20) NOT NULL,
detail VARCHAR(200) NOT NULL,
is_default TINYINT DEFAULT 0,
KEY idx_user_id (user_id)
);
用戶增刪改查地址 → 5 個接口(列表、新增、修改、刪除、設置默認)
-- 在用戶表直接添加 JSON 字段
ALTER TABLE `user` ADD COLUMN `addresses` JSON
COMMENT '地址列表:[{"name":"","phone":"","detail":"","is_default":1}]';
或 MySQL 5.7 前版本使用 TEXT 存儲 JSON 字符串。
// 數據結構
struct Address {
var name: String
var phone: String
var detail: String
var isDefault: Bool
}
// 讀取:解析 JSON 陣列
let addresses = try JSONDecoder().decode([Address].self, from: data)
// 修改/新增/刪除/設置默認:內存操作陣列後,整體序列化回傳
let updatedData = try JSONEncoder().encode(addresses)
API.updateAddresses(userId, addressesJson: updatedData)
// 僅需兩個接口
// 1. 獲取
SELECT `addresses` FROM `user` WHERE `id` = ?;
// 2. 全量覆蓋更新
UPDATE `user` SET `addresses` = ? WHERE `id` = ?;
// 參數為客戶端傳來的完整 JSON 字符串


String account = request.getParameter("account");
String loadType = request.getParameter("LoadType"); // 0=讀取, 1=寫入
if ("0".equals(loadType)) {
// 讀取
ResultSet rs = MySqlHelper.executeQuery("SELECT addresses FROM user WHERE account=?", account);
// 封裝 JSON 返回...
} else {
// 寫入:ReplaceAllStr 為客戶端傳來的完整 JSON
String replaceAllStr = request.getParameter("ReplaceAllStr");
int result = MySqlHelper.executeUpdate(
"UPDATE user SET addresses=? WHERE account=?", replaceAllStr, account);
// 返回成功/失敗...
}

| 維度 | 傳統設計 | 壓縮設計 |
|---|---|---|
| 表數量 | +1 張關聯表 | 零新增表(複用主表) |
| 接口數量 | 5+ 個 CRUD 接口 | 2 個接口(讀/寫) |
| 查詢性能 | 需 JOIN 或子查詢 | 單表主鍵查詢,極快 |
| 寫入性能 | 單行 INSERT/UPDATE | 全量覆蓋,鎖定主表行 |
| 數據一致性 | 外鍵約束、事務保障 | 應用層自行保證 JSON 結構 |
| 擴展性 | 靈活添加字段/索引 | 需客戶端同步升級解析邏輯 |
| 統計分析 | SQL 直接聚合 | 需導出解析或物化視圖 |
| 存儲空間 | 行開銷 + 索引 | JSON 字符串開銷,總體通常更小 |
JSON 類型,支援 JSON_EXTRACT、JSON_CONTAINS 等函數,保留部分 SQL 查詢能力max_allowed_packet / LONGTEXT 限制JSONB、MongoDB 文檔模型原生更適合此類場景這種設計並非「銀彈」,而是針對特定約束條件下的工程權衡。核心原則:在犧牲關係模型查詢靈活性的前提下,換取極致的讀寫性能與開發效率。選型前請完成完整的訪問模式分析與壓力測試驗證。