【系統設計 30 概念】#04 資料庫優化與擴展 — 索引、複製、分片與快取

測驗:資料庫優化與擴展 — 索引、複製、分片與快取

共 5 題,點選答案後會立即顯示結果

1. 資料庫索引(Index)最主要的作用是什麼?

  • A. 減少資料庫的儲存空間
  • B. 加速 SELECT 查詢,讓資料庫不用逐筆掃描
  • C. 加速 INSERT 和 UPDATE 操作
  • D. 自動將資料備份到多台伺服器

2. Replication(複製)和 Sharding(分片)最核心的差異是什麼?

  • A. Replication 用於讀取,Sharding 用於寫入
  • B. Replication 適合小型系統,Sharding 適合大型系統
  • C. Replication 是每台都有全部資料,Sharding 是每台只有一部分資料
  • D. Replication 不需要 Primary,Sharding 需要 Primary

3. 在 Cache-Aside 模式中,當快取沒有命中(Cache Miss)時,系統會怎麼做?

  • A. 直接回傳空值,等快取自動更新
  • B. 去資料庫查詢,查到後存進快取,再回傳結果
  • C. 通知所有 Replica 同步最新資料
  • D. 等待快取過期後重新載入資料

4. 一個使用者更新大頭照後立刻重新整理頁面,卻看到舊的大頭照。這最可能是哪個概念造成的問題?

  • A. 索引沒有及時更新
  • B. Sharding Key 選擇不當
  • C. 反正規化導致資料不一致
  • D. Replication Lag — 寫入 Primary 後 Replica 還沒同步完成

5. 遇到資料庫效能問題時,文章建議的處理優先順序為何?

  • A. 索引 → 快取 → 讀寫分離(Replication) → 垂直分割/反正規化 → 分片(Sharding)
  • B. 分片(Sharding) → 快取 → 索引 → 讀寫分離 → 反正規化
  • C. 快取 → 反正規化 → 索引 → 分片 → 讀寫分離
  • D. 讀寫分離 → 分片 → 索引 → 快取 → 垂直分割

**系列**:系統設計 30 概念(第 4 篇,共 6 篇)
**難度**:L2-進階
**前置知識**:本系列第 2 篇(API 設計與資料儲存)、第 3 篇(系統擴展)
**影片來源**:*System Design was HARD until I Learned these 30 Concepts* — Ashish Pratap Singh (YouTube)

這篇在講什麼

上一篇我們聊了「系統怎麼擴展」。但擴展只是把機器加多,真正要讓系統變快,還得從資料庫下手。這篇涵蓋系統設計 30 概念中的第 15-20 個,全部圍繞同一個主題:怎麼讓資料庫又快又穩

讀完這篇你會知道:

  • Database Indexing 如何加速查詢,以及它的代價
  • Replication 與 Sharding 的差異 — 一個是「複製」,一個是「切割」
  • Caching 和 Denormalization 如何犧牲一致性換取讀取效能
  • Vertical Partitioning 按欄位拆分資料表的概念

概念 15:Database Indexing(資料庫索引)

一句話說明

索引就是資料庫的「目錄」,讓查詢不用一筆一筆翻。

最小範例

-- 沒有索引:資料庫要掃描整張表,一行一行找
SELECT * FROM users WHERE email = '[email protected]';
-- 如果有 100 萬筆,最差要看 100 萬次

-- 建立索引
CREATE INDEX idx_users_email ON users(email);

-- 有索引後:同樣的查詢,資料庫直接「翻目錄」找到位置
SELECT * FROM users WHERE email = '[email protected]';
-- 不管有幾百萬筆,幾乎瞬間找到
Code language: JavaScript (javascript)

用書來比喻

想像一本 1000 頁的書:

情境 沒有索引 有索引
找「快取」在哪一頁 從第 1 頁翻到第 1000 頁 翻到目錄,直接跳到第 487 頁
資料庫操作 Full Table Scan(全表掃描) Index Lookup(索引查找)
速度 O(n) — 資料越多越慢 O(log n) — 幾乎不受資料量影響

索引的代價

索引不是免費的。每次你新增、修改、刪除資料,資料庫都要同步更新索引

-- 每次 INSERT,資料庫要做兩件事:
-- 1. 寫入資料到 users 表
-- 2. 更新 idx_users_email 索引
INSERT INTO users (name, email) VALUES ('Bob', '[email protected]');

-- 索引越多,寫入越慢
-- 如果你對 5 個欄位都建了索引,每次 INSERT 要更新 5 個索引
Code language: JavaScript (javascript)

白話翻譯:索引就像書的目錄。目錄讓你查東西很快,但每次書的內容改了,目錄也要跟著改。目錄越多,改起來越累。

什麼時候該建索引

建索引的好時機:
- WHERE 子句常用的欄位(如 email、user_id)
- JOIN 的欄位
- ORDER BY 的欄位

不需要索引的情況:
- 很少查詢的欄位
- 經常大量 INSERT 的表(索引拖慢寫入)
- 資料量本身就很小的表(全掃也很快)

Vibe Coder 檢查點

看到資料庫相關程式碼時確認:

  • 查詢很慢?先檢查有沒有對 WHERE 條件的欄位建索引
  • AI 幫你建了一堆索引?想想那些欄位是不是真的常被查
  • 寫入變慢了?檢查是不是索引太多

概念 16:Replication(複製)

一句話說明

把同一份資料複製到多台伺服器,讀取分散、掛了不怕。

運作原理

                 ┌──── Replica 1 ◄── 讀取請求
                 │     (完整副本)
寫入請求 ──► Primary ──┤
             (主庫)    │
                 └──── Replica 2 ◄── 讀取請求
                       (完整副本)

白話翻譯

  • Primary(主庫):唯一負責「寫入」的資料庫
  • Replica(副本):Primary 的完整複製品,只負責「讀取」
  • 所有寫入都送到 Primary,然後 Primary 把變更同步到所有 Replica

最小範例

用一個 Web 應用的設定來理解:

# 虛擬碼:應用程式怎麼使用 Replication
DATABASE = {
    "primary": "db-primary.example.com:5432",    # 寫入用這個
    "replicas": [
        "db-replica-1.example.com:5432",          # 讀取用這些
        "db-replica-2.example.com:5432",
    ]
}

def create_user(name, email):
    # 寫入 → 送到 Primary
    primary_db.execute("INSERT INTO users ...")

def get_user(user_id):
    # 讀取 → 送到任一 Replica(分散負載)
    replica = random.choice(replicas)
    return replica.execute("SELECT * FROM users WHERE id = ?", user_id)
Code language: PHP (php)

為什麼要 Replication

好處 說明
讀取效能提升 3 個 Replica 分擔讀取,等於讀取能力 x3
高可用性 Primary 掛了,Replica 可以升級為新的 Primary
資料備份 資料存在多台機器,不怕硬碟壞掉

要注意的坑:Replication Lag

時間點 1:使用者更新大頭照
  → 寫入 Primary ✓

時間點 2:使用者立刻重新整理頁面
  → 讀取 Replica... 還是舊的大頭照!
  → 因為 Primary 的變更還沒同步到 Replica(延遲幾毫秒到幾秒)

時間點 3:幾秒後再重新整理
  → Replica 同步完成,看到新的大頭照 ✓

這就是 Eventual Consistency(最終一致性):資料「最終」會一致,但中間有一小段時間可能不同步。

Vibe Coder 檢查點

看到系統架構圖出現多個資料庫時確認:

  • 讀寫是否分離?寫入是不是都走 Primary?
  • 有沒有考慮 Replication Lag 造成的資料不一致?
  • 使用者剛寫入就要讀取的場景,是否直接讀 Primary?

概念 17:Sharding(分片)

一句話說明

把一張表的資料「切成好幾塊」,分散存到不同伺服器。

Replication vs. Sharding — 一張圖搞懂

Replication(複製):每台都有「全部」資料
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ Primary     │  │ Replica 1   │  │ Replica 2   │
│ 用戶 1-100萬│  │ 用戶 1-100萬│  │ 用戶 1-100萬│
└─────────────┘  └─────────────┘  └─────────────┘

Sharding(分片):每台只有「一部分」資料
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ Shard 1     │  │ Shard 2     │  │ Shard 3     │
│ 用戶 1-33萬 │  │ 用戶 34-66萬│  │ 用戶 67-100萬│
└─────────────┘  └─────────────┘  └─────────────┘

白話翻譯

  • Replication = 影印整本書,發給三個人各一本(每人都有完整的書)
  • Sharding = 把書撕成三份,一人拿一份(每人只有部分內容)

Sharding Key 怎麼選

Sharding 的核心問題是:按什麼規則切? 這個規則就是 Sharding Key。

Sharding Key = user_id

查詢 user_id = 42 的資料:
  → 42 % 3 = 0 → 去 Shard 0 找
  → 只查一台伺服器,速度快

查詢 user_id = 100 的資料:
  → 100 % 3 = 1 → 去 Shard 1 找
  → 同樣只查一台
# 虛擬碼:根據 Sharding Key 決定去哪個 Shard
def get_shard(user_id, total_shards=3):
    shard_number = user_id % total_shards
    return shards[shard_number]

# 查詢時
shard = get_shard(user_id=42)
user = shard.execute("SELECT * FROM users WHERE id = 42")
Code language: PHP (php)

Sharding 的挑戰

挑戰 說明
跨 Shard 查詢 「找出所有 25 歲的用戶」→ 要查全部 Shard,然後合併結果
資料不均勻 如果按地區分片,熱門地區的 Shard 壓力大(Hot Spot)
重新分片 從 3 個 Shard 擴展到 5 個,要搬移大量資料
JOIN 困難 兩筆需要 JOIN 的資料可能在不同 Shard

Vibe Coder 檢查點

看到 Sharding 相關設計時確認:

  • Sharding Key 選得合理嗎?大多數查詢都會帶這個欄位嗎?
  • 有沒有跨 Shard 查詢的需求?如果有,效能可能很差
  • 資料分布均勻嗎?會不會某個 Shard 特別忙?

概念 18:Vertical Partitioning(垂直分割)

一句話說明

按「欄位」把一張大表拆成好幾張小表。

最小範例

-- 原本:一張什麼都放的大表
CREATE TABLE users (
    id INT,
    name VARCHAR(100),       -- 每次登入都要讀
    email VARCHAR(200),      -- 每次登入都要讀
    avatar BLOB,             -- 很大,但不常讀
    bio TEXT,                -- 個人介紹,偶爾讀
    preferences JSON,        -- 設定偏好,很少讀
    created_at TIMESTAMP     -- 每次登入都要讀
);

-- 垂直分割後:按讀取頻率拆成兩張表
CREATE TABLE users_core (        -- 常讀的欄位
    id INT,
    name VARCHAR(100),
    email VARCHAR(200),
    created_at TIMESTAMP
);

CREATE TABLE users_profile (     -- 不常讀的欄位
    user_id INT,
    avatar BLOB,
    bio TEXT,
    preferences JSON
);
Code language: JavaScript (javascript)

為什麼要這樣拆

查詢「用戶登入資訊」:

拆之前:SELECT * FROM users WHERE id = 42
→ 讀出 name, email, avatar(2MB), bio, preferences, created_at
→ avatar 很大但根本用不到,浪費 I/O

拆之後:SELECT * FROM users_core WHERE id = 42
→ 只讀 name, email, created_at
→ 資料量小,查詢快

白話翻譯:就像把衣櫃分成「每天穿的」和「偶爾穿的」兩個區域。每天早上只翻常穿區,不用把整個衣櫃翻一遍。

Vertical vs. Horizontal Partitioning

原始表:
┌────┬───────┬───────┬──────────┐
│ idnameemailavatar   │
├────┼───────┼───────┼──────────┤
│  1 │ Alicea@... │ [2MB]    │
│  2 │ Bob   │ b@... │ [1.5MB]  │
│  3 │ Carol │ c@... │ [3MB]    │
└────┴───────┴───────┴──────────┘

Vertical(垂直):按欄位切
表A: id, name, email     表B: id, avatar
┌────┬───────┬───────┐   ┌────┬──────────┐
│  1 │ Alice │ a@... │   │  1 │ [2MB]    │
│  2 │ Bob   │ b@... │   │  2 │ [1.5MB]  │
│  3 │ Carol │ c@... │   │  3 │ [3MB]    │
└────┴───────┴───────┘   └────┴──────────┘

Horizontal(水平)= Sharding:按列切
Shard A: id 1-2          Shard B: id 3
┌────┬───────┬───────┐   ┌────┬───────┬───────┐
│  1 │ Alice │ a@... │   │  3 │ Carol │ c@... │
│  2 │ Bob   │ b@... │   └────┴───────┴───────┘
└────┴───────┴───────┘
Code language: CSS (css)

Vibe Coder 檢查點

看到資料表設計時確認:

  • 這張表是不是塞了太多欄位?有些欄位很少一起用?
  • 有沒有很大的欄位(BLOB、TEXT)拖慢常規查詢?
  • 拆表後 JOIN 的成本可以接受嗎?

概念 19:Caching(快取)

一句話說明

把常用資料存在記憶體裡,下次直接拿,不用再查資料庫。

為什麼快取這麼重要

讀取速度比較:
  記憶體(RAM)讀取  ≈ 0.1 毫秒
  SSD 磁碟讀取      ≈ 0.1 毫秒(隨機讀取可達數毫秒)
  資料庫查詢(含網路)≈ 1-10 毫秒
  跨機房網路請求     ≈ 50-150 毫秒

快取命中 = 省下資料庫查詢的時間

最小範例:Cache-Aside 模式

這是最常見的快取策略,也叫 Lazy Loading:

import redis

cache = redis.Redis(host='localhost', port=6379)

def get_user(user_id):
    # 第一步:先去快取找
    cached = cache.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)     # 快取命中!直接回傳

    # 第二步:快取沒有,去資料庫查
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)

    # 第三步:查到後存進快取,下次就不用再查資料庫
    cache.set(f"user:{user_id}", json.dumps(user), ex=300)  # ex=300 表示 300 秒後過期

    return user
Code language: PHP (php)

白話翻譯

要找某筆資料?
  → 先翻快取(記憶體)
    → 有找到(Cache Hit)→ 直接回傳,超快
    → 沒找到(Cache Miss)→ 去資料庫查,查到後順便存進快取

三種常見快取策略

1. Cache-Aside(旁路快取)
   讀:先查快取 → 沒有就查 DB → 存進快取
   寫:直接寫 DB → 刪掉快取(下次讀時會重新載入)
   特點:最常見,應用程式自己管快取

2. Write-Through(穿透寫入)
   讀:先查快取 → 沒有就查 DB → 存進快取
   寫:同時寫 DB 和快取
   特點:快取永遠是最新的,但寫入較慢

3. Write-Behind(背後寫入)
   讀:先查快取
   寫:只寫快取 → 過一段時間再批次寫入 DB
   特點:寫入超快,但如果快取掛了可能丟資料

快取失效 — 電腦科學的兩大難題之一

“There are only two hard things in Computer Science: cache invalidation and naming things.”
— Phil Karlton

問題場景:

1. 用戶 A 的資料存在快取中(name = "Alice"2. 用戶 A 改了名字(name = "Alicia")→ 資料庫更新了
3. 但快取裡還是舊的 "Alice"
4. 其他人查用戶 A → 拿到快取裡的舊資料 "Alice" ❌

常見解法:
- TTL(Time To Live):設過期時間,到期自動清除
- 主動失效:資料更新時,順便刪掉對應的快取
- 版本號:快取 key 帶版本號,更新時版本 +1
Code language: JavaScript (javascript)

Vibe Coder 檢查點

看到快取相關程式碼時確認:

  • 有設 TTL(過期時間)嗎?沒有的話快取會永遠不更新
  • 資料更新時有沒有清除快取?(否則會讀到舊資料)
  • 快取掛了的時候,系統還能運作嗎?(快取應該是加速用的,不是必需品)

概念 20:Denormalization(反正規化)

一句話說明

刻意在資料表中存重複資料,用空間換時間,避免 JOIN。

最小範例

-- 正規化設計(教科書寫法):資料不重複,用 JOIN 取得
-- orders 表
| order_id | user_id | product_id | quantity |
|----------|---------|------------|----------|
| 1        | 42      | 101        | 2        |

-- users 表
| user_id | name  |
|---------|-------|
| 42      | Alice |

-- products 表
| product_id | name    | price |
|------------|---------|-------|
| 101        | T-Shirt | 500   |

-- 查詢訂單詳情要 JOIN 三張表
SELECT o.order_id, u.name, p.name, p.price, o.quantity
FROM orders o
JOIN users u ON o.user_id = u.user_id
JOIN products p ON o.product_id = p.product_id;
-- 反正規化設計:資料有重複,但不需要 JOIN
-- orders_denormalized 表
| order_id | user_id | user_name | product_id | product_name | price | quantity |
|----------|---------|-----------|------------|--------------|-------|----------|
| 1        | 42      | Alice     | 101        | T-Shirt      | 500   | 2        |

-- 查詢訂單詳情,一張表搞定
SELECT order_id, user_name, product_name, price, quantity
FROM orders_denormalized
WHERE order_id = 1;

正規化 vs. 反正規化

比較 正規化(Normalization) 反正規化(Denormalization)
資料重複 無重複 有刻意的重複
讀取速度 慢(要 JOIN) 快(一張表搞定)
寫入速度 快(只改一處) 慢(多處都要改)
儲存空間
資料一致性 容易維持 要小心維護
適用場景 寫多讀少 讀多寫少

什麼時候用反正規化

適合反正規化的場景:
- 電商的訂單紀錄(建立後幾乎不改,但常常被查詢)
- 社群媒體的貼文(讀取量遠大於寫入量)
- 報表系統(只讀,不寫)

不適合的場景:
- 經常變動的資料(如用戶即時狀態)
- 需要嚴格一致性的系統(如銀行帳戶)

白話翻譯:正規化就像把資料整理得井然有序,每樣東西只放一個地方。反正規化就像在每個房間都放一把剪刀 — 雖然浪費剪刀,但你再也不用跑來跑去找了。

Vibe Coder 檢查點

看到資料表設計時確認:

  • 如果讀取遠多於寫入,有沒有考慮反正規化來減少 JOIN?
  • 反正規化的資料,更新時有沒有同步所有副本?
  • 重複的資料量大嗎?儲存成本能接受嗎?

六個概念的關係 — 全局觀

這六個概念不是互斥的,它們經常一起使用

一個典型的高流量系統可能同時用到:

                    ┌─────────────┐
  用戶請求 ──────►  │   快取層     │  ← Concept 19: Caching
                    │  (Redis)    │     先查快取,命中就回
                    └──────┬──────┘
                           │ Cache Miss
                    ┌──────▼──────┐
                    │   讀取副本   │  ← Concept 16: Replication
                    │  (Replica)  │     讀取走副本,分散壓力
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
       ┌──────▼──┐  ┌──────▼──┐  ┌──────▼──┐
       │ Shard 1 │  │ Shard 2 │  │ Shard 3 │  ← Concept 17: Sharding
       │用戶1-33萬│  │用戶34-66萬│ │用戶67-100萬│  資料分散到多個分片
       └────┬────┘  └────┬────┘  └────┬────┘
            │            │            │
         索引加速       索引加速      索引加速     ← Concept 15: Indexing
                                                    每個分片內用索引加速

同時:

  • 每張表按欄位做了 Vertical Partitioning(概念 18),常用欄位和不常用欄位分開
  • 讀多寫少的表做了 Denormalization(概念 20),減少 JOIN

決策順序

遇到資料庫效能問題時,通常按這個順序處理:

1. 先加索引(Indexing)           ← 成本最低,效果最明顯
2. 加快取(Caching)              ← 減少資料庫壓力
3. 讀寫分離(Replication)         ← 分散讀取壓力
4. 垂直分割 / 反正規化            ← 優化資料結構
5. 最後才考慮分片(Sharding)      ← 成本最高,複雜度最大

重點整理

概念 一句話 解決什麼問題 代價
Indexing 資料庫的目錄 查詢太慢 寫入變慢、佔空間
Replication 複製整個資料庫 讀取壓力大、要高可用 Replication Lag
Sharding 切割資料分散存 單台存不下、單台扛不住 跨 Shard 查詢困難
Vertical Partitioning 按欄位拆表 欄位太多拖慢查詢 JOIN 成本增加
Caching 存記憶體加速讀取 資料庫被讀爆 快取失效問題
Denormalization 存重複資料免 JOIN JOIN 太多太慢 寫入要同步多處

Vibe Coder 總檢查點

當你在 AI 生成的程式碼或架構圖中看到這些概念時:

  • [ ] 索引:是根據實際查詢模式建的,不是隨便加的嗎?
  • [ ] 複製:讀寫有分離嗎?有考慮 Lag 嗎?
  • [ ] 分片:Sharding Key 選得合理嗎?大多數查詢都帶這個 Key 嗎?
  • [ ] 快取:有設過期時間嗎?資料更新時會清快取嗎?
  • [ ] 反正規化:重複的資料有同步更新機制嗎?
  • [ ] 整體:是否按「索引 → 快取 → 複製 → 分片」的順序處理?別一開始就上 Sharding

**下一篇預告**:#05 分散式系統核心挑戰 — 我們會進入 CAP 定理、一致性模型、分散式交易等「分散式系統的硬核問題」。

進階測驗:資料庫優化與擴展 — 索引、複製、分片與快取

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你負責一個電商平台的訂單系統。訂單表有 500 萬筆資料,以下查詢執行時間超過 5 秒: 情境題

SELECT * FROM orders WHERE user_id = 12345 AND status = ‘pending’; — 目前 orders 表沒有任何索引(除了 Primary Key) — 系統以讀取為主,寫入量不大

根據文章建議的決策順序,你應該優先採取什麼措施?

  • A. 導入 Redis 快取層,把常查的訂單存在記憶體裡
  • B. 對 user_idstatus 建立索引
  • C. 把訂單表做 Sharding,按 user_id 分片到多台伺服器
  • D. 設定讀寫分離,增加 Replica 分散讀取壓力

2. 你的社群媒體應用要顯示貼文列表,每篇貼文需要顯示作者名稱和大頭照。目前的設計是: 情境題

— 每次載入貼文列表都要 JOIN 兩張表 SELECT p.content, p.created_at, u.name, u.avatar_url FROM posts p JOIN users u ON p.user_id = u.user_id ORDER BY p.created_at DESC LIMIT 20; — 這個查詢每秒被呼叫上萬次 — 用戶很少改名字或換大頭照

為了提升這個查詢的效能,最適合的做法是?

  • A. 對 posts 表做 Sharding,按 user_id 分片
  • B. 把 posts 表做 Vertical Partitioning,把 content 欄位分出去
  • C. 對 posts 表做反正規化,直接在 posts 表存入 user_name 和 avatar_url
  • D. 增加更多 Replica 來分散 JOIN 查詢的壓力

3. 你的 users 表包含以下欄位,每次用戶登入都要查整張表: 情境題

CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100), — 每次登入都讀 email VARCHAR(200), — 每次登入都讀 password_hash VARCHAR(256), — 每次登入都讀 avatar BLOB, — 3-5MB,只在個人頁面讀 resume BLOB, — 1-10MB,只在求職頁讀 preferences JSON, — 只在設定頁讀 activity_log JSON — 只在管理後台讀 ); — 登入查詢: SELECT * FROM users WHERE email = ‘[email protected]’;

登入查詢很慢,最可能的原因和最佳改善方式是?

  • A. 缺少索引,應對所有欄位建索引
  • B. SELECT * 讀取了大量 BLOB 資料(avatar、resume),應做垂直分割,將不常用的大欄位拆到另一張表
  • C. 資料量太大,應立即做 Sharding 分散到多台伺服器
  • D. 應導入 Write-Behind 快取策略,把登入資料寫入快取

4. 團隊為了加速查詢,在一張高頻寫入的日誌表(log table)上加了大量索引。以下是 AI 生成的 schema: 錯誤診斷

CREATE TABLE access_logs ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT, action VARCHAR(50), ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); — 每秒有數千筆 INSERT — AI 建議的索引: CREATE INDEX idx_logs_user ON access_logs(user_id); CREATE INDEX idx_logs_action ON access_logs(action); CREATE INDEX idx_logs_ip ON access_logs(ip_address); CREATE INDEX idx_logs_ua ON access_logs(user_agent(100)); CREATE INDEX idx_logs_time ON access_logs(created_at); CREATE INDEX idx_logs_user_action ON access_logs(user_id, action); CREATE INDEX idx_logs_user_time ON access_logs(user_id, created_at);

上線後發現寫入效能嚴重下降,INSERT 延遲從 1ms 飆升到 50ms。最可能的原因是?

  • A. 表的欄位太多,應該做 Vertical Partitioning
  • B. 沒有使用 Replication 做讀寫分離
  • C. 索引太多(7 個),每次 INSERT 都要同步更新全部索引,在高頻寫入的表上代價極大
  • D. 應該用 Write-Through 快取策略取代資料庫寫入

5. 團隊在系統中導入了 Redis 快取,但使用者反映「改了個人資料後,別人看到的還是舊的」。以下是快取相關程式碼: 錯誤診斷

def get_user_profile(user_id): cached = cache.get(f”user:{user_id}”) if cached: return json.loads(cached) user = db.query(“SELECT * FROM users WHERE id = ?”, user_id) cache.set(f”user:{user_id}”, json.dumps(user)) # 存入快取 return user def update_user_profile(user_id, new_data): db.execute(“UPDATE users SET … WHERE id = ?”, user_id) # 更新完成,回傳成功 return {“status”: “ok”}

這段程式碼的問題在哪裡?

  • A. get_user_profile 應該直接查資料庫,不要用快取
  • B. 應該用 Write-Through 策略,在 cache.set 時同時寫入資料庫
  • C. get_user_profile 缺少錯誤處理,應加 try-catch
  • D. update_user_profile 更新資料庫後沒有刪除或更新快取,且 cache.set 沒有設定 TTL(過期時間),導致舊資料永遠留在快取裡

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *