SQL for QA — 從 SELECT 到 JOIN、抓 bug 必備 30 個 query 範本
「我看不到 DB 就不能驗 bug」是 QA 的痛。會 SQL 的 QA 不用等 dev 查資料、自己 30 秒拿到答案。這篇給你 QA 角度的 SQL 入門到實戰。
QA 為什麼必須會 SQL
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 基本
-- 列所有欄位
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 — 過濾
-- 等於
SELECT * FROM users WHERE email = '[email protected]';
-- 不等於
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
-- 多少 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 — 分組
-- 每個 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 最常卡這裡)
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
-- 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 用(最常用)
-- 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
-- 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
-- 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
-- 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. 效能 / 統計
-- 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)
-- 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
-- 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. 時間操作
-- 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 — 看為什麼慢
-- 30. 分析 query plan
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 1042;
如果看到 Seq Scan(全表掃描)→ 該欄位該加 index。
安全紅線(拜託千萬別踩)
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 做這些
- ❌
DELETE FROM users WHERE ... - ❌
UPDATE users SET ... - ❌
TRUNCATE - ❌
DROP TABLE - ❌
ALTER TABLE
改資料只能透過 migration script + code review + 跑過 staging。
Read-only 也要小心
-- ❌ 大 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 的方法
-- 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 合作的禮儀
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 的時機
- UPDATE / DELETE / migration
- 你的 query 跑了 30 秒還沒回
- 大量 lock 風險(thousand rows)
- 不確定 schema 語意
SQL 練習資源
- SQL Bolt — 互動式教學
- HackerRank SQL — 題目
- LeetCode Database — 進階
- PostgreSQL Tutorial — 完整 ref
- DB Fiddle — 在線跑 SQL
反模式
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 句
- 會 SQL 的 QA = 自己解決 80% 資料問題
- 永遠 read-only、永遠 LIMIT、永遠看 EXPLAIN
- JOIN 是分水嶺、學會就上一個 level
- 第一週進公司讀 schema、後面省一年
- prod 寫操作一律走 migration、不走 query
最後
SQL 是 QA 最高 ROI 的硬技能 — 一週能上手、十年用得到、薪資 +20%。從每天看一張 schema、每週寫 5 個 query 開始,三個月後你會發現自己變「會解資料問題的 QA」、不是「只會點 button 的 QA」。