---
title: SQL for QA — 從 SELECT 到 JOIN、抓 bug 必備 30 個 query 範本
description: 給 QA 的 SQL 完整指南。為什麼 QA 要會 SQL、SELECT/WHERE/JOIN 基本、debug 用 query 範本、效能注意事項、安全紅線（千萬別在 prod 跑 DELETE）。
category: manual
tags: [sql, database, qa-tools, debugging, data-validation]
date: 2026-06-12
---

# SQL for QA — 從 SELECT 到 JOIN、抓 bug 必備 30 個 query 範本

「我看不到 DB 就不能驗 bug」是 QA 的痛。**會 SQL 的 QA 不用等 dev 查資料、自己 30 秒拿到答案**。這篇給你 QA 角度的 SQL 入門到實戰。

## QA 為什麼必須會 SQL

```mermaid
flowchart LR
    QA[QA] --> N1{驗 bug}
    QA --> N2{看 test data}
    QA --> N3{驗 migration}
    QA --> N4{找 edge case}
    QA --> N5{效能監控}

    N1 --> Q1["User 說我訂單沒了<br>SELECT * FROM orders WHERE..."]
    N2 --> Q2["測試帳號被誰用過?<br>SELECT * FROM sessions"]
    N3 --> Q3["新欄位有沒有 backfill?<br>SELECT COUNT(*) WHERE col IS NULL"]
    N4 --> Q4["有沒有負值 / NULL / 重複?<br>SELECT WHERE val < 0"]
    N5 --> Q5["哪個 query 慢?<br>EXPLAIN"]

    style QA fill:#06b6d4,color:#fff
```

不會 SQL = 每件事都要 Slack 一個 dev。

## 30 分鐘從 0 到能用

### 1. SELECT 基本

```sql
-- 列所有欄位
SELECT * FROM users;

-- 列指定欄位
SELECT id, email, created_at FROM users;

-- 限制筆數（一定要加！）
SELECT * FROM users LIMIT 10;

-- 排序
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
```

⚠️ **看大 table 沒加 LIMIT 會炸 production**。

### 2. WHERE — 過濾

```sql
-- 等於
SELECT * FROM users WHERE email = 'qa@test.com';

-- 不等於
SELECT * FROM users WHERE status != 'active';

-- 比較
SELECT * FROM orders WHERE total > 1000;

-- 範圍
SELECT * FROM orders WHERE total BETWEEN 100 AND 500;

-- IN（多選）
SELECT * FROM users WHERE country IN ('TW', 'JP', 'US');

-- LIKE（模糊）
SELECT * FROM users WHERE email LIKE '%@example.com';

-- NULL
SELECT * FROM users WHERE deleted_at IS NULL;

-- 時間
SELECT * FROM orders WHERE created_at >= '2026-06-01';

-- 組合
SELECT * FROM orders
WHERE status = 'pending'
  AND created_at > NOW() - INTERVAL '24 hours'
  AND total > 0;
```

### 3. COUNT / SUM / AVG

```sql
-- 多少 user
SELECT COUNT(*) FROM users;

-- 多少 active user
SELECT COUNT(*) FROM users WHERE status = 'active';

-- 訂單總額
SELECT SUM(total) FROM orders WHERE status = 'paid';

-- 平均訂單
SELECT AVG(total) FROM orders;

-- 最大 / 最小
SELECT MAX(created_at), MIN(created_at) FROM orders;
```

### 4. GROUP BY — 分組

```sql
-- 每個 status 多少 user
SELECT status, COUNT(*) FROM users GROUP BY status;

-- 每個 country 訂單數 + 總額
SELECT country, COUNT(*), SUM(total)
FROM orders
GROUP BY country
ORDER BY SUM(total) DESC;

-- 每天訂單數
SELECT DATE(created_at), COUNT(*)
FROM orders
WHERE created_at > '2026-06-01'
GROUP BY DATE(created_at)
ORDER BY DATE(created_at);
```

### 5. JOIN — 連表（QA 最常卡這裡）

```mermaid
flowchart LR
    Tables[兩個 table] --> Inner[INNER JOIN<br>兩邊都有的]
    Tables --> Left[LEFT JOIN<br>左邊全部 + 右邊有的]
    Tables --> Right[RIGHT JOIN<br>右邊全部 + 左邊有的]
    Tables --> Full[FULL JOIN<br>兩邊全部]

    Inner --> I1["users JOIN orders<br>只看有訂單的 user"]
    Left --> L1["users LEFT JOIN orders<br>所有 user + 訂單(或 NULL)"]

    style Inner fill:#06b6d4,color:#fff
    style Left fill:#10b981,color:#fff
    style Right fill:#a855f7,color:#fff
    style Full fill:#f59e0b,color:#fff
```

```sql
-- INNER JOIN：兩邊都有
SELECT u.email, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.created_at > '2026-06-01';

-- LEFT JOIN：所有 user、有沒有訂單都顯示
SELECT u.email, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.email
ORDER BY order_count DESC;
```

**90% 的 bug 驗證會用 LEFT JOIN**：「這個 user 有沒有相關記錄」。

## QA 必備 30 個 query 範本

### A. 驗 bug 用（最常用）

```sql
-- 1. 查特定 user 的訂單
SELECT * FROM orders WHERE user_id = 1042 ORDER BY created_at DESC LIMIT 20;

-- 2. 查最近 1 小時的失敗訂單
SELECT * FROM orders
WHERE status = 'failed'
  AND created_at > NOW() - INTERVAL '1 hour';

-- 3. 重複訂單（防止重複扣款 bug）
SELECT user_id, amount, COUNT(*)
FROM orders
WHERE created_at > NOW() - INTERVAL '5 minutes'
GROUP BY user_id, amount
HAVING COUNT(*) > 1;

-- 4. 該 user 的 session 歷史
SELECT * FROM sessions
WHERE user_id = 1042
ORDER BY created_at DESC LIMIT 10;

-- 5. 查 audit log
SELECT * FROM audit_logs
WHERE user_id = 1042
  AND action = 'password_change'
ORDER BY created_at DESC;
```

### B. 驗 data integrity

```sql
-- 6. 找孤兒記錄（user_id 指向不存在的 user）
SELECT o.* FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.id IS NULL;

-- 7. 找重複 email
SELECT email, COUNT(*) FROM users
GROUP BY email
HAVING COUNT(*) > 1;

-- 8. 找 NULL 在不該 NULL 的地方
SELECT COUNT(*) FROM orders WHERE user_id IS NULL;

-- 9. 找負值
SELECT * FROM orders WHERE total < 0 LIMIT 10;

-- 10. 找未來時間（資料污染）
SELECT * FROM orders WHERE created_at > NOW();
```

### C. 驗 migration

```sql
-- 11. 新欄位 backfill 進度
SELECT
  COUNT(*) AS total,
  COUNT(new_column) AS filled,
  COUNT(*) - COUNT(new_column) AS pending
FROM users;

-- 12. 欄位最小 / 最大 / NULL 比例
SELECT
  MIN(age), MAX(age), AVG(age),
  COUNT(*) - COUNT(age) AS null_count
FROM users;

-- 13. Enum 值分佈
SELECT status, COUNT(*) FROM orders GROUP BY status;
```

### D. 找 edge case

```sql
-- 14. 找 trial 過期但還 active 的 user
SELECT * FROM users
WHERE trial_ends_at < NOW()
  AND status = 'trial_active';

-- 15. 找買了 5+ 訂單的 VIP
SELECT u.email, COUNT(o.id) as cnt
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.email
HAVING COUNT(o.id) >= 5;

-- 16. 找從沒登入過的 user
SELECT * FROM users WHERE last_login_at IS NULL;

-- 17. 邊界值搜尋
SELECT * FROM orders WHERE total IN (0, 0.01, 99999.99);
```

### E. 效能 / 統計

```sql
-- 18. 每日新註冊
SELECT DATE(created_at), COUNT(*)
FROM users
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY DATE(created_at);

-- 19. Top 10 客戶
SELECT u.email, SUM(o.total) as revenue
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.email
ORDER BY revenue DESC LIMIT 10;

-- 20. P95 訂單金額（PostgreSQL）
SELECT PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY total)
FROM orders;
```

### F. JSON 欄位查詢（PostgreSQL）

```sql
-- 21. JSON 內某 key
SELECT * FROM users WHERE settings->>'theme' = 'dark';

-- 22. JSON nested
SELECT * FROM users WHERE settings->'notifications'->>'email' = 'true';

-- 23. JSON 含某 array item
SELECT * FROM users WHERE settings->'tags' ? 'qa';
```

### G. 進階：CTE + Window

```sql
-- 24. CTE — 多步驟查詢
WITH recent_orders AS (
  SELECT user_id, total FROM orders
  WHERE created_at > NOW() - INTERVAL '7 days'
)
SELECT u.email, SUM(r.total) as week_total
FROM users u
JOIN recent_orders r ON u.id = r.user_id
GROUP BY u.email;

-- 25. Window — 每個 user 的訂單排序
SELECT
  user_id,
  total,
  ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY total DESC) as rank
FROM orders;

-- 26. 找每 user 最近一筆訂單
SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at DESC;
```

### H. 時間操作

```sql
-- 27. 時區轉換
SELECT created_at AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Taipei'
FROM orders LIMIT 10;

-- 28. 今天 / 昨天 / 本週
SELECT * FROM orders WHERE created_at >= CURRENT_DATE;
SELECT * FROM orders WHERE created_at >= CURRENT_DATE - INTERVAL '1 day';
SELECT * FROM orders WHERE created_at >= DATE_TRUNC('week', NOW());

-- 29. 時間差
SELECT
  AVG(EXTRACT(EPOCH FROM (paid_at - created_at)))
FROM orders
WHERE status = 'paid';
```

### I. EXPLAIN — 看為什麼慢

```sql
-- 30. 分析 query plan
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 1042;
```

如果看到 `Seq Scan`（全表掃描）→ 該欄位該加 index。

## 安全紅線（拜託千萬別踩）

```mermaid
flowchart TD
    Red[在 prod 跑 SQL] --> Q1{是讀 還是寫?}
    Q1 -->|讀| Read[SELECT only<br>+ LIMIT 100]
    Q1 -->|寫| Stop[🛑 STOP]

    Stop --> S1["UPDATE / DELETE<br>絕對不要在 prod 跑"]
    Stop --> S2["要改資料 → 找 DBA / Dev<br>透過正式 migration / script"]

    Read --> R1["先加 LIMIT 1 跑"]
    R1 --> R2["看 plan"]
    R2 --> R3["再放大 LIMIT"]

    style Red fill:#f59e0b,color:#fff
    style Stop fill:#ef4444,color:#fff
    style Read fill:#10b981,color:#fff
```

### 永遠不要在 prod 做這些

1. ❌ `DELETE FROM users WHERE ...`
2. ❌ `UPDATE users SET ...`
3. ❌ `TRUNCATE`
4. ❌ `DROP TABLE`
5. ❌ `ALTER TABLE`

**改資料只能透過 migration script + code review + 跑過 staging**。

### Read-only 也要小心

```sql
-- ❌ 大 table 沒 LIMIT
SELECT * FROM orders;   -- 1000 萬筆 → 連線 timeout

-- ✅ 永遠加 LIMIT
SELECT * FROM orders LIMIT 100;

-- ❌ 沒 index 的 LIKE
SELECT * FROM orders WHERE notes LIKE '%abc%';  -- 全表掃描

-- ✅ 看 plan 再跑
EXPLAIN SELECT * FROM orders WHERE notes LIKE '%abc%';
```

### 用 read replica 不要打 master

如果公司有 read replica（slave），用 replica connection。

## 工具推薦

| 工具 | 用途 | 跨平台 |
|------|------|--------|
| **TablePlus** | GUI、新手友善 | ✓ |
| **DBeaver** | 免費、強大 | ✓ |
| **DataGrip** | JetBrains、強 | ✓ |
| **Postico** | macOS、PG 友善 | macOS |
| **psql / mysql CLI** | Terminal | ✓ |
| **Metabase / Redash** | Dashboard、不能改 | ✓（推薦給 QA） |
| **Beekeeper Studio** | open source | ✓ |

**強烈建議 QA 申請 read-only DB account + Metabase / Redash**。

## QA 看 schema 的方法

```sql
-- PostgreSQL：列所有 table
\dt

-- 看某 table 結構
\d orders

-- MySQL
SHOW TABLES;
DESCRIBE orders;

-- 看欄位資訊
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'orders';

-- 看 index
SELECT * FROM pg_indexes WHERE tablename = 'orders';
```

**不熟 schema 寫不出對的 query**。第一週進公司花 1 天讀 schema 是值得的。

## 跟 dev 合作的禮儀

```mermaid
flowchart LR
    QA[QA 想查資料] --> Q1{該查嗎?}
    Q1 -->|自己能解| Self[Read-only 自己查]
    Q1 -->|要改資料| Dev[找 dev / DBA]
    Q1 -->|複雜 query| Pair[Pair with dev]
    Q1 -->|大量資料| Tool[用 Metabase 等工具]

    style Self fill:#10b981,color:#fff
    style Dev fill:#f59e0b,color:#fff
    style Tool fill:#06b6d4,color:#fff
```

### 該找 dev 的時機

1. UPDATE / DELETE / migration
2. 你的 query 跑了 30 秒還沒回
3. 大量 lock 風險（thousand rows）
4. 不確定 schema 語意

## SQL 練習資源

- **SQL Bolt** — 互動式教學
- **HackerRank SQL** — 題目
- **LeetCode Database** — 進階
- **PostgreSQL Tutorial** — 完整 ref
- **DB Fiddle** — 在線跑 SQL

## 反模式

```mermaid
flowchart TD
    Anti[QA SQL 反模式] --> A1["在 prod 跑 UPDATE"]
    Anti --> A2["SELECT * 沒 LIMIT"]
    Anti --> A3["不看 schema 就猜"]
    Anti --> A4["JOIN 全 table 沒 WHERE"]
    Anti --> A5["用 master 不用 replica"]
    Anti --> A6["改了資料沒 backup"]

    style A1 fill:#ef4444,color:#fff
    style A2 fill:#ef4444,color:#fff
    style A3 fill:#ef4444,color:#fff
    style A4 fill:#ef4444,color:#fff
    style A5 fill:#ef4444,color:#fff
    style A6 fill:#ef4444,color:#fff
```

## 給 QA 的 5 句

1. **會 SQL 的 QA = 自己解決 80% 資料問題**
2. **永遠 read-only、永遠 LIMIT、永遠看 EXPLAIN**
3. **JOIN 是分水嶺、學會就上一個 level**
4. **第一週進公司讀 schema、後面省一年**
5. **prod 寫操作一律走 migration、不走 query**

## 最後

SQL 是 QA 最高 ROI 的硬技能 — **一週能上手、十年用得到、薪資 +20%**。從每天看一張 schema、每週寫 5 個 query 開始，三個月後你會發現自己變「會解資料問題的 QA」、不是「只會點 button 的 QA」。
