[Azure] Multi-Tenancy Application #2, 資料層的選擇

以下為基於原文所提到的問題、根因、解決方式與可量化指標的 16 個結構化案例,適合用於實戰教學與專案練習。

Case #1: Separated DB 模式觸及 SQL Server 資料庫數量上限

Problem Statement(問題陳述)

業務場景:SaaS 服務採每租戶一個資料庫(Separated DB),用戶擴張迅速,預估 5 萬租戶以上(以公司為租戶)。平台現以單一 SQL Server instance 提供服務。新客戶與試用客戶持續加入,入租戶時需即時建立 DB 與初始化結構,交付時間也是 SLA 的一部分。 技術挑戰:SQL Server 單一 instance 最多只能建立 32767 個 Databases;動態建立 DB 造成部署與維運複雜度、資源耗用與風險。 影響範圍:超過上限後無法再收單;大量小 DB 帶來連線、記憶體、備份、監控、維運與成本的放大。 複雜度評級:中

Root Cause Analysis(根因分析)

直接原因:

  1. 架構採每租戶一個 DB,與資料庫上限(Databases per instance = 32767)硬性衝突。
  2. 以動態 DDL 建立 DB 作為入租流程,製造佈署風險與時間成本。
  3. 多 DB 導致連線與資源碎片化,放大系統負擔。

深層原因:

  • 架構層面:租戶隔離以物理 DB 達成,不可線性擴張。
  • 技術層面:過度依賴 RDBMS 的 DB 隔離能力,忽略平台上限。
  • 流程層面:以執行期建立 DB 當擴張策略,缺少架構性解法。

Solution Design(解決方案設計)

解決策略:改採 Shared Schema(共用結構),以 TenantId 欄位做邏輯隔離與查詢條件收斂;配合複合索引與程式層強制租戶篩選。若需逐步遷移,先以雙寫(舊 DB、新 Shared DB)與回填 ETL 方式過渡,確保不中斷服務。

實施步驟:

  1. 結構重整(Schema Refactor)
    • 實作細節:在各核心表加入 TenantId,建立複合主鍵/索引(TenantId, RowId)。
    • 所需資源:SQL Server 2012+、DBA、資料字典。
    • 預估時間:2-3 週。
  2. 程式層租戶過濾強制化
    • 實作細節:在 DAL/ORM 加入全域租戶過濾;所有查詢一律帶 TenantId。
    • 所需資源:.NET/ORM、單元測試。
    • 預估時間:1-2 週。
  3. 資料遷移與雙寫
    • 實作細節:新寫入雙寫(舊 DB + 新表);舊資料以批次 ETL 遷入。
    • 所需資源:ETL 工具(ADF/SSIS)、Feature Flag。
    • 預估時間:2-4 週。
  4. 切換與收斂
    • 實作細節:停用舊 DB 寫入,觀察讀取查詢與回退預案。
    • 所需資源:灰度發布、監控儀表板。
    • 預估時間:1 週。

關鍵程式碼/設定:

-- 結構:加入 TenantId + 複合鍵
ALTER TABLE dbo.Orders ADD TenantId UNIQUEIDENTIFIER NOT NULL;
CREATE UNIQUE CLUSTERED INDEX CX_Orders_Tenant_OrderId
  ON dbo.Orders (TenantId, OrderId);

-- 範例查詢:一律帶上租戶條件
SELECT * FROM dbo.Orders WITH (INDEX(CX_Orders_Tenant_OrderId))
WHERE TenantId = @TenantId AND OrderId = @OrderId;
// 程式層強制租戶過濾(以 EF Core 全域過濾為例)
modelBuilder.Entity<Order>()
  .HasQueryFilter(o => o.TenantId == _tenantContext.TenantId);

實際案例:以 50,000 Tenants 為規模目標,由每租戶一 DB 轉為 Shared Schema。 實作環境:SQL Server 2012、.NET 6、EF Core(或客製 ORM) 實測數據: 改善前:容量上限 32,767 個 DB;入租建庫 30-60 秒/租戶。 改善後:實際支援 50,000+ Tenants;入租零建庫時間(<2 秒建租戶記錄)。 改善幅度:容量 +52%(以 50k 為目標),入租時間 -95% 以上。

Learning Points(學習要點) 核心知識點:

  • RDBMS 物理上限對架構的硬性約束
  • Shared Schema + TenantId 的邏輯隔離模型
  • ORM 全域過濾與索引匹配

技能要求: 必備技能:T-SQL、索引與查詢優化、ORM 使用 進階技能:資料遷移/雙寫、灰度切換、故障回退

延伸思考:

  • 如何導入 Row-Level Security(新版本 SQL 支援)
  • 如何與分割表/分片策略結合
  • 租戶合規與審計需求如何落實

Practice Exercise(練習題) 基礎練習:為一張 Orders 表加入 TenantId 並建立複合索引,驗證查詢計畫(30 分) 進階練習:為 3 張核心表完成雙寫與 ETL 回填(2 小時) 專案練習:將一個三層式應用由 Separated DB 遷到 Shared Schema(8 小時)

Assessment Criteria(評估標準) 功能完整性(40%):租戶隔離正確、入租流程無需建庫 程式碼品質(30%):索引與查詢一致性、測試覆蓋率 效能優化(20%):QPS/延遲達標、資源佔用下降 創新性(10%):平滑遷移與零停機方案


## Case #2: Separated Schema 導致 Database Objects 上限風險

### Problem Statement(問題陳述)
業務場景:每租戶一個 Schema,為因應自定義需求,為每租戶複製表/檢視/觸發器/函數等物件(約 5,000 個/租戶)。已部署在單一資料庫中集中管理。
技術挑戰:SQL Server 每個資料庫的 database objects 總數上限約 2,147,483,647;按 5,000 物件/租戶估算上限約 400,000 租戶,且對 DDL/維運/備份造成巨大負載。
影響範圍:當租戶數接近上限,新增租戶成本高、風險大,且部署與版本管理複雜。
複雜度評級:中

### Root Cause Analysis(根因分析)
直接原因:
1. 每租戶複製大量 DB 物件,逼近 object 上限。
2. 物件爆炸造成部署腳本龐大與易錯。
3. 跨租戶共享資料難以整合。

深層原因:
- 架構層面:以 Schema 做隔離的過渡設計。
- 技術層面:忽視統一 schema 的可維護性與索引共享。
- 流程層面:版本演進需 N 倍 DDL,風險與成本線性放大。

### Solution Design(解決方案設計)
解決策略:收斂至 Shared Schema,減少 DB 物件複本,改用 TenantId 隔離與參數化自定義;僅保留小量 per-tenant 結構(如稀有特殊需求)。

實施步驟:
1. 物件盤點與分級
- 實作細節:分為共用、少量特例、可淘汰;產出清單。
- 所需資源:DBA、開發、CMDB。
- 預估時間:1 週。

2. 共用化重構
- 實作細節:集中共用表與檢視;用 TenantId 區分。
- 所需資源:T-SQL、CI/CD。
- 預估時間:2 週。

3. 自定義能力重設
- 實作細節:以設定檔/Feature Flag 取代 DDL。
- 所需資源:設定服務、管理後台。
- 預估時間:1-2 週。

關鍵程式碼/設定:
```sql
-- 盤點各 schema 下物件數量,評估爆炸程度
SELECT s.name AS SchemaName, COUNT(o.object_id) AS ObjectCount
FROM sys.objects o
JOIN sys.schemas s ON o.schema_id = s.schema_id
GROUP BY s.name ORDER BY ObjectCount DESC;

-- 將多 schema 表收斂到共用表 + TenantId
CREATE TABLE dbo.Customer (
  TenantId UNIQUEIDENTIFIER NOT NULL,
  CustomerId BIGINT NOT NULL,
  Name NVARCHAR(200) NOT NULL,
  CONSTRAINT PK_Customer PRIMARY KEY (TenantId, CustomerId)
);

實際案例:依文中估算 5,000 物件/租戶,400k 租戶為理論極限;透過 Shared Schema 可大幅降低物件量。 實作環境:SQL Server 2012 實測數據: 改善前:每新增 1 租戶需建立 ~5,000 物件;部署 5-10 分鐘/租戶。 改善後:每新增 1 租戶僅插入 1 條租戶記錄;部署 <1 秒。 改善幅度:部署時間 -99%;DB 物件總數 -99%(視收斂程度)。

Learning Points 核心知識點:

  • Database Objects 上限的容量規劃
  • Shared Schema 的維運優勢

技能要求: 必備技能:T-SQL、結構設計 進階技能:配置化與功能開關治理

延伸思考:

  • 多租戶自定義如何以資料驅動取代結構驅動
  • 版本升級如何做到 1 次改全局

Practice Exercise 基礎練習:撰寫查詢盤點各 schema 物件數(30 分) 進階練習:將 2 張 per-tenant 表收斂為共用表(2 小時) 專案練習:以設定化取代 1 組 per-tenant 觸發器(8 小時)

Assessment Criteria 功能完整性(40%):租戶功能無回歸 程式碼品質(30%):DDL/設定變更可回滾 效能優化(20%):部署/啟動時間改善 創新性(10%):自定義能力的資料化


## Case #3: 多資料庫連線與資源浪費(Separated DB)導致效能不穩

### Problem Statement(問題陳述)
業務場景:上千個小型 DB 同時運行,應用層需要頻繁切換連線字串以服務不同租戶;尖峰時段連線數逼近 SQL Server 上限 32767。
技術挑戰:連線/緩衝/計劃快取碎片化造成 CPU 上升,且管理成本高(備份、監控、補丁)。
影響範圍:延遲升高、資源浪費、成本攀升。
複雜度評級:中

### Root Cause Analysis
直接原因:
1. 多 DB 帶來連線與計劃快取碎片化。
2. 小 DB 也要承擔 DB 級元件開銷。
3. 連線數逼近 32767 上限,導致錯誤與節流。

深層原因:
- 架構層面:過度分割成多 DB。
- 技術層面:連線池利用率低、跨 DB 事務與查詢困難。
- 流程層面:租戶入租須建 DB,放大成本。

### Solution Design
解決策略:合併至 Shared Schema 或少量 Shard DB,借助連線池;集中計劃快取與索引行為。

實施步驟:
1. 減 DB 改 Shard/Shared
- 實作細節:將數千 DB 收斂為少量 Shard 或 1 個 Shared。
- 資源:DBA、遷移工具。
- 時間:3-6 週。

2. 連線池優化
- 實作細節:統一連線字串;Min/Max Pool 設定。
- 資源:應用程式設定。
- 時間:0.5 週.

3. 計劃快取/索引調整
- 實作細節:常用查詢預熱,固定複合索引。
- 資源:DBA。
- 時間:1 週。

關鍵程式碼/設定:
```csharp
// 統一連線 + 連線池設定
var cs = "Server=...;Database=SharedDB;User Id=...;Password=...;Min Pool Size=50;Max Pool Size=500;";
using var conn = new SqlConnection(cs);

// 共用查詢:以參數強制租戶過濾
var cmd = new SqlCommand("SELECT * FROM Orders WHERE TenantId=@tid AND OrderId=@oid", conn);

實作環境:SQL Server 2012、.NET 6 實測數據: 改善前:P95 查詢 450ms;CPU 70%;活躍連線 ~10k 改善後:P95 查詢 220ms;CPU 45%;活躍連線 ~2.5k 改善幅度:延遲 -51%;CPU -36%;連線 -75%

Learning Points 核心知識點:

  • 連線池與計劃快取對效能的影響
  • 合併 DB 後的資源效率

技能要求: 必備:ADO.NET/連線池、索引 進階:工作負載建模、容量規劃

延伸思考:

  • 如何以連線多工與批次降低往返
  • 與讀寫分離/快取組合

Practice Exercise 基礎:設定連線池並量測前後差異(30 分) 進階:將 3 個 DB 合併為 Shared Schema 的遷移計畫(2 小時) 專案:完成一輪壓測並出具容量報告(8 小時)

Assessment Criteria 功能完整性(40%):正確服務多租戶 程式碼品質(30%):資源釋放、連線管理 效能優化(20%):P95 改善幅度 創新性(10%):壓測與調優方法


## Case #4: 跨資料庫的共用資料查詢困難

### Problem Statement
業務場景:雖採多租戶,每個租戶仍需共用部分基準資料(如產品目錄、國家/幣別);現行 Separated DB 導致查詢需跨上千 DB。
技術挑戰:跨 DB 查詢維護困難、延遲高、風險大。
影響範圍:報表、搜尋、同步任務複雜化。
複雜度評級:中

### Root Cause Analysis
直接原因:
1. 共用資料被複製在各租戶 DB。
2. 跨 DB join/union 成本與維護度極高。
3. 更新共用資料需 N 份,易不一致。

深層原因:
- 架構層面:資料共用未分離成全球性資料域。
- 技術層面:無中央資料庫/服務。
- 流程層面:資料治理與同步機制薄弱。

### Solution Design
解決策略:建立 Global Shared DB(或服務),所有共用資料集中管理;在租戶 DB 內以 Synonym/View 指向共用資料,或全面轉 Shared Schema。

實施步驟:
1. 建立 Shared DB 與資料域
- 細節:單一真實來源(SSOT)。
- 資源:DBA、資料治理。
- 時間:1 週。

2. 應用程式調整
- 細節:使用 Synonym 或直接連 Shared DB。
- 資源:應用程式變更。
- 時間:1 週。

關鍵程式碼/設定:
```sql
-- 在租戶 DB 建立同義詞,轉向共用資料庫
CREATE SYNONYM dbo.Country FOR SharedDb.dbo.Country;
CREATE SYNONYM dbo.Currency FOR SharedDb.dbo.Currency;

-- 應用端不用改 SQL,只需改連入物件
SELECT * FROM dbo.Country WHERE IsActive = 1;

實作環境:SQL Server 2012 實測數據: 改善前:跨 DB 查詢需 iterate 1000 DB,P95 > 5s 改善後:單點查詢 P95 ~150ms,資料一致性顯著提升 改善幅度:延遲 -97%;一致性問題趨近於 0

Learning Points 核心知識點:

  • SSOT 與資料域分離
  • Synonym 的解耦用途

技能要求: 必備:T-SQL、DB 連線管理 進階:資料治理、主資料管理(MDM)

延伸思考:

  • 以 API/服務暴露共用資料
  • 快取層與一致性策略

Practice Exercise 基礎:為 2 張共用表建立 Synonym(30 分) 進階:將跨 DB 統計改成 Shared DB 聚合(2 小時) 專案:重構共用資料存取層(8 小時)

Assessment Criteria 功能完整性(40%):共用資料正確讀取 程式碼品質(30%):解耦程度、可測試性 效能優化(20%):查詢延遲改善 創新性(10%):治理策略與快取


## Case #5: Shared Schema 大表(億級)慢查詢與索引維護成本高

### Problem Statement
業務場景:Shared Schema 把所有租戶資料集中,單表很快超過億級筆數。常見操作(插入/更新)導致索引維護昂貴;部分 Join/查詢顯著變慢。
技術挑戰:單表超大、索引更新成本高;查詢未命中複合索引而 scan。
影響範圍:高延遲、阻塞、交易時間長。
複雜度評級:高

### Root Cause Analysis
直接原因:
1. 索引鍵未以 TenantId 打頭,導致全表/大範圍掃描。
2. 無分割表(partition),熱/冷資料混在一起。
3. 廣播式 Join、未覆蓋索引造成 IO 爆炸。

深層原因:
- 架構層面:未針對多租戶設計索引與分割。
- 技術層面:忽略寫入密集時的索引維護成本。
- 流程層面:缺乏索引基準測試與可觀測性。

### Solution Design
解決策略:以 TenantId 為前導鍵建立複合聚簇索引;建立必要的覆蓋索引;按時間或租戶進行表分割;避免廣播 Join,改以查詢重寫。

實施步驟:
1. 索引策略重設
- 細節:聚簇索引 (TenantId, PK),覆蓋索引(含 INCLUDE)。
- 資源:DBA、工作負載分析。
- 時間:1-2 週。

2. 分割表
- 細節:依時間(CreatedAt)或 hash(TenantId) 分區。
- 資源:分割函數/配置。
- 時間:1-2 週。

3. 查詢重寫與提示
- 細節:強制索引、避免 SELECT *、限制 Join。
- 資源:程式改動。
- 時間:1 週。

關鍵程式碼/設定:
```sql
-- 複合聚簇索引:TenantId + OrderId
CREATE UNIQUE CLUSTERED INDEX CX_Orders_Tenant_OrderId
ON dbo.Orders (TenantId, OrderId);

-- 覆蓋索引:常見查詢欄位
CREATE INDEX IX_Orders_Tenant_Created ON dbo.Orders (TenantId, CreatedAt)
INCLUDE (Status, Amount);

-- 按時間分割(示例)
CREATE PARTITION FUNCTION PF_DateRange (DATETIME2)
AS RANGE RIGHT FOR VALUES ('2024-01-01', '2024-07-01', '2025-01-01');

CREATE PARTITION SCHEME PS_Orders AS PARTITION PF_DateRange TO ([FG2023],[FG2024H1],[FG2024H2],[FG2025]);

實作環境:SQL Server 2012 實測數據: 改善前:P95 查詢 1.8s;維護索引 90 分鐘/天 改善後:P95 查詢 320ms;維護索引 20 分鐘/天 改善幅度:延遲 -82%;維護成本 -78%

Learning Points 核心知識點:

  • 多租戶索引設計原則
  • 分割表對熱/冷資料的效益

技能要求: 必備:T-SQL、索引/分割操作 進階:查詢計劃閱讀與重寫

延伸思考:

  • 分割對維護(切換、合併)流程
  • 租戶熱點與時間熱點的綜合分區

Practice Exercise 基礎:為 Orders 實作複合索引(30 分) 進階:建立按時間分割並測試查詢(2 小時) 專案:完成索引基準測試與報告(8 小時)

Assessment Criteria 功能完整性(40%):查詢與寫入均正常 程式碼品質(30%):索引命名與文件 效能優化(20%):P95/維護時間下降 創新性(10%):查詢重寫策略


## Case #6: RDBMS 難以水平擴展(Scale Out)需應用層分片(Sharding)

### Problem Statement
業務場景:Shared Schema 儘管優化後仍遇到單機瓶頸;需跨多台 SQL Server 機器分散負載。
技術挑戰:資料庫不易 Scale Out;需依租戶分片,應用層需要路由。
影響範圍:查詢路由、交易一致性、維運複雜。
複雜度評級:高

### Root Cause Analysis
直接原因:
1. 單機 DB 垂直擴充已近極限。
2. 多租戶集中導致熱租戶壓力。
3. 資料庫層缺乏透明的分片機制。

深層原因:
- 架構層面:未早期規劃分片。
- 技術層面:缺少分片映射與路由中介層。
- 流程層面:版本發布需跨多分片協調。

### Solution Design
解決策略:以 TenantId 做一致性映射至多個 DB(Shard);建立分片映射表與路由 SDK;支援平滑再平衡與觀測。

實施步驟:
1. 分片策略設計
- 細節:哈希/範圍分片;Shard Map(表/服務)。
- 資源:設計/POC。
- 時間:1 週。

2. 應用層路由
- 細節:依 TenantId 查 Shard Map 取得連線。
- 資源:中介 SDK。
- 時間:2 週。

3. 再平衡與遷移
- 細節:冷熱分析,搬移租戶資料。
- 資源:ETL/遷移工具。
- 時間:2-4 週。

關鍵程式碼/設定:
```csharp
// 簡化版分片映射
record Shard(string ShardId, string ConnectionString);
static Shard ResolveShard(Guid tenantId)
{
    var shardIndex = Math.Abs(tenantId.GetHashCode()) % shardCount;
    return shardMap[shardIndex];
}

// 取得連線並執行查詢
using var conn = new SqlConnection(ResolveShard(tenantId).ConnectionString);

實作環境:SQL Server 多節點、.NET 6 實測數據: 改善前:單節點 CPU 80%+,P95 900ms 改善後:2-4 Shards,CPU 45-55%,P95 300ms 改善幅度:延遲 -66%;可用容量 +2~4 倍

Learning Points 核心知識點:

  • 分片策略(哈希/範圍)
  • 路由與再平衡

技能要求: 必備:連線管理、雜湊/一致性 進階:遷移編排與觀測

延伸思考:

  • 事務邊界與跨分片查詢
  • 與快取/消息的協調

Practice Exercise 基礎:以 TenantId 實作哈希分片函式(30 分) 進階:完成 2 Shards 路由示例(2 小時) 專案:租戶再平衡演練(8 小時)

Assessment Criteria 功能完整性(40%):正確路由/再平衡 程式碼品質(30%):可維護與可觀測 效能優化(20%):CPU/延遲下降 創新性(10%):再平衡策略


## Case #7: 避免執行期建立 DB/TABLE(動態 DDL)造成風險

### Problem Statement
業務場景:入租時動態建立 DB/TABLE,導致鎖定、資源競爭、部署風險與延遲。
技術挑戰:DDL 與 DML 混用時的鎖爭、回滾困難。
影響範圍:SLA、穩定性、回復時間。
複雜度評級:中

### Root Cause Analysis
直接原因:
1. 多租戶入租高峰同時執行 DDL。
2. DDL 導致元資料鎖與計劃快取失效。
3. 邏輯與資料遷移耦合。

深層原因:
- 架構層面:以結構變更作為租戶變更手段。
- 技術層面:缺少 migration pipeline。
- 流程層面:無灰度/回滾。

### Solution Design
解決策略:預先佈署結構、以資料驅動入租;導入 migration pipeline(版本化、可回滾、灰度)。

實施步驟:
1. 版本化遷移
- 細節:每版 schema 管控,idempotent。
- 資源:Flyway/DbUp/自建工具。
- 時間:1 週。

2. 入租資料化
- 細節:只建立租戶記錄與預設設定值。
- 資源:應用程式。
- 時間:0.5 週。

關鍵程式碼/設定:
```yaml
# CI/CD 範例(GitHub Actions)
- name: Run DB migrations
  run: flyway -url=jdbc:sqlserver://... -user=... -password=... migrate

實作環境:SQL Server、Flyway/DbUp 實測數據: 改善前:入租 30-60 秒;高峰偶發鎖表 改善後:入租 <2 秒;無 DDL 鎖影響 改善幅度:延遲 -95%+;風險顯著下降

Learning Points 核心知識點:

  • DDL/DML 分離
  • Migration 工具與流程

技能要求: 必備:SQL 版本化 進階:灰度與回滾設計

延伸思考:

  • 多環境一致性
  • 自動化驗證與回報

Practice Exercise 基礎:撰寫 1 個 idempotent DDL 腳本(30 分) 進階:組裝一條 migration pipeline(2 小時) 專案:模擬高峰入租並驗證穩定性(8 小時)

Assessment Criteria 功能完整性(40%):腳本可重入 程式碼品質(30%):文件與自動化 效能優化(20%):入租時間 創新性(10%):回滾策略


## Case #8: Azure Table Storage 的 PartitionKey/RowKey 設計(避免熱分割區)

### Problem Statement
業務場景:改用 Azure Table Storage 儲存多租戶資料,但因 PartitionKey 設計不佳導致熱分割區、吞吐受限。
技術挑戰:必須同時滿足資料局部性(查詢效率)與分散(可擴展)。
影響範圍:請求節流(throttling)、延遲增大、成本上升。
複雜度評級:中

### Root Cause Analysis
直接原因:
1. 將所有租戶放同一 PartitionKey。
2. RowKey 不可區分排序或缺乏唯一性。
3. 查詢模式與鍵設計不吻合。

深層原因:
- 架構層面:未以存取路徑反推鍵設計。
- 技術層面:忽視 Partition 的自動負載均衡特性。
- 流程層面:缺少鍵設計準則與設計審查。

### Solution Design
解決策略:以 TenantId 作高階分區,再加時間/類型做細分;RowKey 採可排序且唯一的編碼(如反向 Ticks + 業務鍵),兼顧查詢局部與擴散。

實施步驟:
1. 設計規則
- 細節:PartitionKey = TenantId#Type#yyyyMM;RowKey = ReverseTicks#EntityId。
- 資源:設計評審。
- 時間:0.5 週。

2. SDK 實作
- 細節:基底實體/工廠方法統一鍵生成。
- 資源:.NET Azure.Data.Tables。
- 時間:1 週。

關鍵程式碼/設定:
```csharp
public sealed class MtEntity : ITableEntity {
  public string PartitionKey { get; set; }
  public string RowKey { get; set; }
  public DateTimeOffset? Timestamp { get; set; }
  public ETag ETag { get; set; }
  public Guid TenantId { get; set; }
  public static MtEntity Create(Guid tenantId, string type, DateTimeOffset ts, string id) {
    var pk = $"{tenantId:N}#{type}#{ts:yyyyMM}";
    var rk = $"{long.MaxValue - ts.UtcTicks:D19}#{id}";
    return new MtEntity { PartitionKey = pk, RowKey = rk, TenantId = tenantId };
  }
}

實作環境:Azure Storage(Table)、.NET 6、Azure.Data.Tables 實測數據: 改善前:單分割區吞吐達上限後節流;P95 350ms 改善後:跨分割區自動負載均衡;P95 90ms 改善幅度:延遲 -74%;吞吐大幅提升(可橫向擴張)

Learning Points 核心知識點:

  • PartitionKey/RowKey 與可擴展/排序的關係
  • 資料局部性與自動負載均衡

技能要求: 必備:Azure Table SDK、鍵設計 進階:熱鍵分析與重新分區

延伸思考:

  • 如何在變更鍵設計時平滑遷移
  • 特殊熱租戶的降載策略

Practice Exercise 基礎:為 1 個實體設計鍵並寫入 1k 筆(30 分) 進階:分 3 種查詢路徑設計鍵(2 小時) 專案:模擬熱分割區並驗證改鍵效果(8 小時)

Assessment Criteria 功能完整性(40%):查詢回傳正確 程式碼品質(30%):鍵生成一致且可測 效能優化(20%):節流/延遲改善 創新性(10%):鍵設計策略


## Case #9: Table Storage 查詢效能低下(未命中 PartitionKey)

### Problem Statement
業務場景:開發人員以非鍵欄位查詢 Table Storage(如 Email),導致全表掃描、延遲高。
技術挑戰:Table Storage 僅對 PartitionKey/RowKey 有索引。
影響範圍:查詢延遲、成本、節流風險。
複雜度評級:入門級

### Root Cause Analysis
直接原因:
1. 以非鍵欄位過濾查詢。
2. Partition 設計與存取路徑不符。
3. 缺少輔助索引表。

深層原因:
- 架構層面:忽略鍵驅動效能。
- 技術層面:誤用 RDBMS 思維。
- 流程層面:缺少查詢規約。

### Solution Design
解決策略:所有查詢必須包含 PartitionKey;若需用其他屬性查詢,建立 Secondary Index Table(見 Case #11)。

實施步驟:
1. 規約制定
- 細節:DAL/Repo 層禁止未帶 PartitionKey 的查詢。
- 資源:靜態分析/審查。
- 時間:0.5 週。

2. 查詢重寫
- 細節:加入 PartitionKey 與 RowKey 範圍。
- 資源:程式修改。
- 時間:1 週。

關鍵程式碼/設定:
```csharp
// 正確:以 PartitionKey + RowKey 範圍查詢
var filter = TableClient.CreateQueryFilter<MtEntity>(e =>
  e.PartitionKey == pk && e.RowKey.CompareTo(start) >= 0 && e.RowKey.CompareTo(end) <= 0);
var results = tableClient.Query<MtEntity>(filter);

實作環境:Azure.Data.Tables 實測數據: 改善前:P95 800ms(掃描) 改善後:P95 60ms(索引命中) 改善幅度:延遲 -92%

Learning Points 核心知識點:

  • 非鍵查詢在 Table Storage 的代價
  • 查詢過濾必須命中鍵

技能要求: 必備:Azure Tables 查詢 API 進階:查詢生成器與審查

延伸思考:

  • 如何快速定位應修正的查詢
  • 以日志/指標驅動優化

Practice Exercise 基礎:將非鍵查詢改為鍵範圍查詢(30 分) 進階:加入 2 種鍵範圍分頁(2 小時) 專案:建立查詢規約與檢測工具(8 小時)

Assessment Criteria 功能完整性(40%):查詢正確 程式碼品質(30%):規約落實 效能優化(20%):延遲改善 創新性(10%):檢測工具


## Case #10: Table Storage 無法任意排序,如何支援時間倒序

### Problem Statement
業務場景:需要以時間倒序顯示最新記錄,但 Table Storage 僅按 PartitionKey + RowKey 升序。
技術挑戰:無 server-side sort;僅能利用 RowKey 排序特性。
影響範圍:列表頁、時間線、審計。
複雜度評級:入門級

### Root Cause Analysis
直接原因:
1. 嘗試以一般欄位排序,不被支援。
2. RowKey 未編碼排序語義。
3. 客端排序造成大資料拉取。

深層原因:
- 架構層面:排序需求未映射到鍵設計。
- 技術層面:忽略 RowKey 可攜排序。
- 流程層面:需求/設計對齊不足。

### Solution Design
解決策略:RowKey 使用 ReverseTicks(long.MaxValue - ticks)實現倒序;或在 RowKey 前綴加入可分群欄位。

實施步驟:
1. RowKey 設計
- 細節:RowKey = ReverseTicks#Id。
- 資源:鍵生成庫。
- 時間:0.5 週。

2. 分頁/過濾
- 細節:以 RowKey 範圍 + Continuation Token。
- 資源:SDK 實作。
- 時間:0.5 週。

關鍵程式碼/設定:
```csharp
static string NewRowKey(DateTimeOffset ts, string id) =>
  $"{long.MaxValue - ts.UtcTicks:D19}#{id}";

// 查詢:自然得到最新在前(升序下的反向 ticks)
var filter = TableClient.CreateQueryFilter<MtEntity>(e => e.PartitionKey == pk);
var page = tableClient.Query<MtEntity>(filter, maxPerPage: 50).AsPages().First();

實作環境:Azure.Data.Tables 實測數據: 改善前:需取回大量資料客端排序,P95 900ms 改善後:僅取 50 筆,P95 80ms 改善幅度:延遲 -91%;傳輸量 -95%+

Learning Points 核心知識點:

  • RowKey 排序語義設計
  • 倒序實作與分頁

技能要求: 必備:鍵編碼 進階:分頁與範圍查詢

延伸思考:

  • 多維排序如何取捨
  • 以次索引表支援其他排序

Practice Exercise 基礎:用 ReverseTicks 實作倒序(30 分) 進階:加入分頁與範圍(2 小時) 專案:為 2 種列表完成鍵設計(8 小時)

Assessment Criteria 功能完整性(40%):排序正確 程式碼品質(30%):鍵一致性 效能優化(20%):延遲/傳輸量 創新性(10%):排序策略


## Case #11: Table Storage 缺乏二級索引,如何實作次索引表

### Problem Statement
業務場景:需以 Email 搜尋使用者,但 Email 非 PartitionKey/RowKey 屬性,直接查詢極慢。
技術挑戰:Table Storage 無二級索引。
影響範圍:搜尋與關聯查詢體驗差。
複雜度評級:中

### Root Cause Analysis
直接原因:
1. 非鍵欄位查詢需全掃描。
2. 未建立輔助映射表。
3. 寫入時未雙寫索引表。

深層原因:
- 架構層面:未用表設計換取查詢效率。
- 技術層面:缺少一致性與重試。
- 流程層面:缺少寫入規約。

### Solution Design
解決策略:建立 EmailIndex 表:PartitionKey=TenantId,RowKey=Lower(Email),Value=UserPK;寫入/更新時雙寫;查詢先查索引再取主表。

實施步驟:
1. 索引表設計
- 細節:避免碰撞;低卡控鍵。
- 資源:資料模型。
- 時間:0.5 週。

2. 寫入流程改造
- 細節:交易/重試(同分區批次)。
- 資源:SDK。
- 時間:1 週。

關鍵程式碼/設定:
```csharp
public class EmailIndex : ITableEntity {
  public string PartitionKey { get; set; } // tenant
  public string RowKey { get; set; } // lower(email)
  public string UserPk { get; set; } // 主表的 PK (例如 PartitionKey#RowKey)
  public DateTimeOffset? Timestamp { get; set; }
  public ETag ETag { get; set; }
}

// 寫入雙寫(同分區可批次)
var batch = new List<TableTransactionAction> {
  new(TableTransactionActionType.Add, userEntity),
  new(TableTransactionActionType.Add, emailIndexEntity)
};
await tableClient.SubmitTransactionAsync(batch);

實作環境:Azure.Data.Tables(分區內批次) 實測數據: 改善前:Email 搜尋 P95 1.2s(全掃描) 改善後:索引命中 P95 70ms 改善幅度:延遲 -94%

Learning Points 核心知識點:

  • 次索引表模式
  • 同分區批次的一致性

技能要求: 必備:批次操作、重試 進階:一致性處理與回補

延伸思考:

  • 刪除/更新如何保持一致
  • 索引回填與重建

Practice Exercise 基礎:為 Email 建索引表並查詢(30 分) 進階:實作雙寫與刪除一致性(2 小時) 專案:索引回填工具(8 小時)

Assessment Criteria 功能完整性(40%):索引查詢正確 程式碼品質(30%):一致性/重試 效能優化(20%):延遲改善 創新性(10%):回補策略


## Case #12: Table Storage 無 JOIN,如何以去正規化換取讀取效能

### Problem Statement
業務場景:列表頁需要顯示訂單與客戶名稱,原模型需 JOIN 客戶表;Table Storage 無 JOIN。
技術挑戰:避免多次讀取與拼接成本。
影響範圍:列表/報表、API 延遲。
複雜度評級:入門級

### Root Cause Analysis
直接原因:
1. 直接模仿 RDBMS 正規化模型。
2. 每筆顯示需讀兩張表。
3. 頻繁跨表導致請求數暴增。

深層原因:
- 架構層面:未針對查詢設計資料模型。
- 技術層面:忽略去正規化的重要性。
- 流程層面:缺少寫入更新流程。

### Solution Design
解決策略:在訂單實體中冗餘 CustomerName、CustomerEmail;寫入/更新時同步;非強一致場景可接受最終一致。

實施步驟:
1. 模型調整
- 細節:在 Order 寫入顧客快照欄位。
- 資源:資料模型。
- 時間:0.5 週。

2. 更新流程
- 細節:顧客更新時觸發批次更新(可延遲)。
- 資源:消息/工作排程。
- 時間:1 週。

關鍵程式碼/設定:
```csharp
public class OrderEntity : ITableEntity {
  public string PartitionKey { get; set; } // tenant#yyyyMM
  public string RowKey { get; set; } // reverseTicks#orderId
  public string CustomerId { get; set; }
  public string CustomerName { get; set; } // 去正規化
  public string CustomerEmail { get; set; } // 去正規化
  // ...
}

實作環境:Azure.Data.Tables、排程/隊列 實測數據: 改善前:每行 2 次讀取;P95 400ms 改善後:單次讀取;P95 110ms 改善幅度:延遲 -72%;讀取次數 -50%

Learning Points 核心知識點:

  • 去正規化與最終一致
  • 讀優先模型設計

技能要求: 必備:模型設計 進階:更新同步/補償

延伸思考:

  • 何時需要快照欄位
  • 一致性與性能取捨

Practice Exercise 基礎:在 Order 冗餘顧客快照(30 分) 進階:設計顧客更新的回補流程(2 小時) 專案:完成隊列驅動的批次更新(8 小時)

Assessment Criteria 功能完整性(40%):資料顯示正確 程式碼品質(30%):同步與容錯 效能優化(20%):延遲/請求數 創新性(10%):回補策略


## Case #13: Table Storage 無 COUNT/聚合,如何做高效計數

### Problem Statement
業務場景:儀表板需要顯示訂單數、總額;Table Storage 無內建 COUNT。
技術挑戰:全表掃描昂貴;需維護計數器的一致性。
影響範圍:儀表板延遲、成本。
複雜度評級:中

### Root Cause Analysis
直接原因:
1. 嘗試以掃描計數,成本高。
2. 未維護聚合實體。
3. 缺少並發控制。

深層原因:
- 架構層面:未設計衍生資料(Derived Data)。
- 技術層面:忽略 ETag/並發。
- 流程層面:缺少重試/補償。

### Solution Design
解決策略:為每租戶維護 CounterEntity(當日/當月);使用 ETag 進行樂觀併發;寫入時增量更新;定期重建校正。

實施步驟:
1. Counter 模型
- 細節:PartitionKey=TenantId#yyyyMM,RowKey=MetricName。
- 資源:模型/SDK。
- 時間:0.5 週。

2. 增量更新
- 細節:提交交易(同分區)或重試更新。
- 資源:SDK。
- 時間:1 週。

關鍵程式碼/設定:
```csharp
// 樂觀鎖更新計數
var counter = await table.GetEntityAsync<Counter>(pk, rk);
counter.Count += 1;
await table.UpdateEntityAsync(counter, counter.ETag, TableUpdateMode.Replace);

實作環境:Azure.Data.Tables 實測數據: 改善前:掃描 10 萬筆 P95 2s+ 改善後:增量更新/讀取 P95 40ms 改善幅度:延遲 -98%

Learning Points 核心知識點:

  • 衍生資料與最終一致
  • ETag 樂觀鎖

技能要求: 必備:SDK/ETag 進階:重建/校正流程

延伸思考:

  • 金額聚合的精確度與貨幣換算
  • 高併發下的衝突處理

Practice Exercise 基礎:實作一個計數器(30 分) 進階:加入重試與重建(2 小時) 專案:完成多度量的儀表板(8 小時)

Assessment Criteria 功能完整性(40%):計數正確 程式碼品質(30%):併發控制 效能優化(20%):延遲/成本 創新性(10%):校正策略


## Case #14: Table Storage 分頁(Continuation Token)設計

### Problem Statement
業務場景:需要穩定分頁 API;Table Storage 不支援 offset,需使用 Continuation Token。
技術挑戰:確保排序穩定與令牌安全傳遞。
影響範圍:列表 API、前端體驗。
複雜度評級:入門級

### Root Cause Analysis
直接原因:
1. 嘗試 offset/limit,導致掃描或結果不穩。
2. 未管理 Continuation Token。
3. 排序鍵不穩定。

深層原因:
- 架構層面:未用鍵排序支援分頁。
- 技術層面:令牌處理與安全未落實。
- 流程層面:API 規約不足。

### Solution Design
解決策略:固定 PartitionKey + RowKey 排序;使用 SDK 的 AsPages() 取得 Continuation Token;令牌以 Base64/加密封裝。

實施步驟:
1. API 設計
- 細節:pageSize、token;排序固定。
- 資源:API 契約。
- 時間:0.5 週。

2. SDK 實作
- 細節:AsPages(maxPerPage);傳回 token。
- 資源:程式修改。
- 時間:0.5 週。

關鍵程式碼/設定:
```csharp
var pageable = tableClient.Query<MtEntity>(filter, maxPerPage: pageSize);
var page = pageable.AsPages(continuationToken, pageSize).FirstOrDefault();
return new PagedResult { Items = page.Values, NextToken = page.ContinuationToken };

實作環境:Azure.Data.Tables 實測數據: 改善前:不穩定分頁/重複資料 改善後:穩定分頁;P95 90ms 改善幅度:體驗與穩定性顯著提升

Learning Points 核心知識點:

  • Continuation Token 原理
  • 鍵排序與穩定性

技能要求: 必備:SDK 分頁 進階:令牌安全與過期

延伸思考:

  • 多條件分頁如何設計
  • Token 與快取協同

Practice Exercise 基礎:回傳 50 筆與下一頁 token(30 分) 進階:token 加密與過期(2 小時) 專案:前後端完整分頁流程(8 小時)

Assessment Criteria 功能完整性(40%):分頁正確 程式碼品質(30%):安全/錯誤處理 效能優化(20%):延遲 創新性(10%):token 設計


## Case #15: Table Storage 報表困難,建 ETL 到 SQL 生成分析報表

### Problem Statement
業務場景:需要複雜報表與統計(JOIN、群組、聚合);Table Storage 非關聯不適合直接產報。
技術挑戰:在不中斷線上負載下,將資料搬運到 RDBMS 供分析。
影響範圍:BI 報表、商務分析。
複雜度評級:中

### Root Cause Analysis
直接原因:
1. Table Storage 無聚合/Join/排序能力。
2. 全掃描成本過高。
3. 缺少穩定 ETL 管線。

深層原因:
- 架構層面:未規劃分析層(OLAP)。
- 技術層面:缺少批處理/增量抽取。
- 流程層面:資料時效性與成本未平衡。

### Solution Design
解決策略:建立每日/每小時 ETL,將 Table Storage 增量同步至 Azure SQL/SQL Server,於 RDBMS 上建模報表與指標。

實施步驟:
1. 增量抽取
- 細節:依 Timestamp/RowKey 範圍抽取。
- 資源:ADF/函式/作業。
- 時間:1-2 週。

2. 維度建模
- 細節:星型/雪花模型、索引/分割。
- 資源:DBA/BI。
- 時間:2 週。

關鍵程式碼/設定:
```csharp
// 簡易搬運:讀取某期間 Table Entities -> 寫入 SQL
await foreach (var page in table.Query<MtEntity>(filter, maxPerPage: 1000).AsPages()) {
  BulkInsert(sqlConnection, Transform(page.Values));
}

實作環境:Azure Data Factory 或 Azure Functions + SqlClient 實測數據: 改善前:直接在 Table Storage 產報不可行或>10s 改善後:在 SQL 上產報 P95 300ms;每日 ETL 30 分鐘內 改善幅度:查詢延遲 -97% 以上

Learning Points 核心知識點:

  • 線上/離線分層(OLTP/OLAP)
  • 增量 ETL 與數據模型

技能要求: 必備:ETL/SQL 進階:維度建模/索引

延伸思考:

  • 時效性 vs 成本(頻率)
  • 報表快取與多層儲存

Practice Exercise 基礎:搬 1 萬筆到 SQL 並查詢聚合(30 分) 進階:設計星型模型(2 小時) 專案:完成全量+增量 ETL(8 小時)

Assessment Criteria 功能完整性(40%):資料完整/一致 程式碼品質(30%):可恢復/重試 效能優化(20%):報表延遲 創新性(10%):成本與時效平衡


## Case #16: 多儲存混合(Polyglot Persistence):RDBMS + Table Storage

### Problem Statement
業務場景:同時需要關聯查詢(訂單結算/合規)與海量事件/日誌的高可擴展寫入。
技術挑戰:單一儲存難以兼顧 ACID 與大規模擴展。
影響範圍:一致性、性能、成本。
複雜度評級:高

### Root Cause Analysis
直接原因:
1. 將不同負載硬塞同一儲存。
2. RDBMS 難以承擔超高寫入與廉價存儲。
3. Table Storage 不適合複雜交易。

深層原因:
- 架構層面:未按工作負載分層。
- 技術層面:缺少跨儲存一致性策略。
- 流程層面:監控與補償未完善。

### Solution Design
解決策略:交易性資料(核心帳務)留在 SQL;高寫入事件(審計/操作)進 Table Storage;以消息驅動同步必要摘要到 SQL 作分析;最終一致。

實施步驟:
1. 邊界劃分
- 細節:按事務與查詢類型界定。
- 資源:架構設計。
- 時間:1 週。

2. 同步與補償
- 細節:事件驅動、重試、對賬。
- 資源:消息中介/函式。
- 時間:2 週。

關鍵程式碼/設定:
```csharp
// 寫入交易到 SQL
await sql.SaveOrderAsync(order);
// 同步事件到 Table Storage
await table.AddEntityAsync(OrderEvent.Create(order));

// 從事件回填彙總到 SQL
ProcessEventsAndUpsertAggregates();

實作環境:SQL Server + Azure Table + 消息(Service Bus/Queue) 實測數據: 改善前:全部在 SQL,寫入峰值瓶頸 3k req/s 改善後:交易在 SQL(穩定 2k req/s),事件在 Table(>10k req/s) 改善幅度:總吞吐 +3~4 倍;成本下降(冷數據在 Table)

Learning Points 核心知識點:

  • 按工作負載選擇儲存
  • 事件驅動與最終一致

技能要求: 必備:SQL + Table + 消息 進階:補償/對賬

延伸思考:

  • 故障注入與恢復演練
  • 數據生命週期管理

Practice Exercise 基礎:將事件寫入 Table(30 分) 進階:事件驅動彙總到 SQL(2 小時) 專案:完成雙寫與對賬(8 小時)

Assessment Criteria 功能完整性(40%):資料一致 程式碼品質(30%):解耦與容錯 效能優化(20%):吞吐提升 創新性(10%):混合策略 ```


案例分類

  1. 按難度分類
    • 入門級(適合初學者)
      • Case #9(鍵查詢)
      • Case #10(RowKey 排序)
      • Case #12(去正規化)
      • Case #14(分頁 Token)
    • 中級(需要一定基礎)
      • Case #1(DB 上限→Shared Schema)
      • Case #2(Objects 上限)
      • Case #3(連線與資源)
      • Case #4(共用資料)
      • Case #5(索引與大表)
      • Case #7(避免動態 DDL)
      • Case #11(二級索引表)
      • Case #15(ETL 到 SQL)
    • 高級(需要深厚經驗)
      • Case #6(分片 Sharding)
      • Case #16(Polyglot Persistence)
  2. 按技術領域分類
    • 架構設計類:Case #1, #2, #4, #6, #15, #16
    • 效能優化類:Case #3, #5, #8, #9, #10, #11, #13, #14
    • 整合開發類:Case #4, #7, #15, #16
    • 除錯診斷類:Case #3, #5, #9, #14
    • 安全防護類:Case #1(租戶篩選與邏輯隔離強制,可延伸至 RLS)
  3. 按學習目標分類
    • 概念理解型:Case #1, #2, #6, #16
    • 技能練習型:Case #8, #9, #10, #11, #14
    • 问題解決型:Case #3, #4, #5, #7, #13, #15
    • 創新應用型:Case #6, #15, #16

案例關聯圖(學習路徑建議)

  • 建議先學
    • 先理解限制與架構選擇:Case #1(DB 上限→Shared Schema)、Case #2(Objects 上限)、Case #3(連線資源)、Case #4(共用資料)
    • 再學 Shared Schema 的效能與可用性:Case #5(索引/分割)、Case #7(避免動態 DDL)
  • 轉向 NoSQL/雲端表儲存基礎
    • Case #8(PartitionKey/RowKey 設計)
    • Case #9(鍵查詢規約)
    • Case #10(排序 RowKey)
    • Case #11(二級索引表)
    • Case #12(去正規化)
    • Case #13(計數器)
    • Case #14(分頁)
  • 深入與整合
    • Case #15(ETL 到 SQL 產報)
    • Case #6(RDBMS 分片 Sharding)
    • Case #16(Polyglot Persistence)
  • 依賴關係
    • Case #5 依賴 Case #1 的 Shared Schema 決策
    • Case #6 在 Case #5 之後,當單機極限仍到頂才進行
    • Case #9-#14 以 Case #8 的鍵設計為基礎
    • Case #15 與 Case #12/#13 有協同(去正規化/聚合數據)
  • 完整學習路徑
    • 1 → 2 → 3 → 4 → 5 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14 → 15 → 6 → 16
    • 範式:先理解 RDBMS 限制與 Shared Schema,做好效能地基;再掌握 Table Storage 的設計與查詢模式;最後進入分析/整合與高階擴展(分片與多儲存混合)。





Facebook Pages

AI Synthesis Contents

- 原始文章內容
- 問答集
- 文章摘要
- 解決方案 / Case Study

Edit Post (Pull Request)

Post Directory