Timescale 官方出品的 pg-aiguide 是一個 Claude Code Skill,讓 AI 在幫你生成 PostgreSQL 程式碼時,自動套用官方推薦的最佳實踐。這篇文章從裡面精選 10 個最實用的 Best Practice,寫給每天在跟 Postgres 打交道的後端工程師。
# 資料型態
# 1. 用 TEXT 取代 VARCHAR(n)
這是最常被忽略的一條。
PostgreSQL 的 TEXT 和 VARCHAR 底層完全一樣,都是 varlena 儲存,效能沒有任何差異。
-- ❌ 常見寫法
email VARCHAR(255)
-- ✅ 推薦寫法
email TEXT NOT NULL
-- 如果真的需要限制長度:
email TEXT NOT NULL CHECK (LENGTH(email) <= 255)
用 TEXT 的好處:
- 不用猜「這欄位要多長」(50?100?255?)
- 以後不用跑
ALTER TABLE改長度 - 用
CHECK限制比VARCHAR(n)更語意清楚
規則: 也不要用 CHAR(n),Postgres 裡沒有任何理由使用它。
# 2. 時間欄位一律用 TIMESTAMPTZ
這個踩坑的人超多。
-- ❌ TIMESTAMP(沒有時區)
INSERT INTO t VALUES ('2026-03-23 16:00:00');
-- 存進去就是 16:00:00,不管你在哪個時區
-- 台灣查是 16:00,美國查也是 16:00 → 時間錯亂
-- ✅ TIMESTAMPTZ(有時區)
INSERT INTO t VALUES ('2026-03-23 16:00:00+08');
-- 存的是 UTC 08:00:00
-- 台灣查:自動轉成 16:00:00+08 ✅
-- 美國查:自動轉成 04:00:00-04 ✅
TIMESTAMPTZ 把時間統一存成 UTC,查詢時根據 session 時區自動轉換。只要伺服器、DB、後端時區設定不一致,TIMESTAMP 就會出問題。
規則:
- ✅
TIMESTAMPTZ(有時區) - ❌
TIMESTAMP(沒時區) - ❌
TIMETZ(有時區的 TIME,基本上沒人用)
# 3. 金額欄位用 NUMERIC,絕對不用 FLOAT
-- ❌ 浮點數有精度問題
price FLOAT
-- ✅ 精確十進位
price NUMERIC(10, 2) NOT NULL CHECK (price > 0)
浮點數(FLOAT、REAL、DOUBLE PRECISION)是近似值,存 0.1 + 0.2 可能得到 0.30000000000000004。金融、電商場景絕對不能用,用 NUMERIC 才能保證精確計算。
Postgres 還有一個 MONEY 型態,同樣不推薦使用,有本地化問題。
# 設計原則
# 4. Primary Key 用 BIGINT GENERATED ALWAYS AS IDENTITY
-- ❌ 舊寫法(SERIAL 底層是 sequence,但有一些陷阱)
id SERIAL PRIMARY KEY
-- ✅ 推薦寫法
user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
SERIAL 是舊語法,GENERATED ALWAYS AS IDENTITY 是 SQL 標準,語意更清楚,也更安全(ALWAYS 防止外部手動插入 ID)。
什麼時候用 UUID?只在需要「全域唯一性」或「不透明 ID」(不想讓外部猜到資料筆數)時。Postgres 18+ 推薦用 uuidv7(),較舊版本用 gen_random_uuid()。
# 5. FK 欄位要手動加 Index
這是 Postgres 最容易踩的坑之一:
PostgreSQL 不會自動幫 Foreign Key 欄位建 Index。
CREATE TABLE orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id),
...
);
-- ✅ 必須手動加!
CREATE INDEX ON orders (user_id);
沒有這個 Index,當你刪除 users 裡的一筆資料時,Postgres 要全表掃 orders 確認沒有關聯資料,直接 lock 住。
# 6. NOT NULL + DEFAULT 要養成習慣
CREATE TABLE users (
user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
每個欄位只要語意上不應該是 NULL,就加 NOT NULL。搭配有意義的 DEFAULT 值,不只防資料污染,也讓查詢條件不用一直處理 NULL 的三態邏輯。
# 7. 先正規化,再考慮反正規化
最後一條是設計哲學。
Postgres 的 Join 很快,不要一開始就為了「減少 Join」把所有東西塞進一張大表。
正規化(3NF)的好處:
- 資料一致性:同一筆資料只存一份,不會更新到一半
- 維護容易:Schema 清楚,查詢直覺
只有在以下情況才考慮反正規化:
- 實際測量到 Join 是效能瓶頸(不是猜的)
- 讀取頻率遠大於寫入
- 可以接受額外的維護成本
過早反正規化是技術債的溫床。
# 索引 & 進階
# 8. 索引策略:只建「你真的會查」的 Index
Index 不是越多越好,每個 Index 都會拖慢 INSERT/UPDATE 速度。
幾個重點:
-- 1. 複合 Index:最常過濾的欄位放最左邊
CREATE INDEX ON orders (user_id, status);
-- 可支援: WHERE user_id = ?
-- 可支援: WHERE user_id = ? AND status = ?
-- 不支援: WHERE status = ? (沒有 user_id 的條件)
-- 2. Partial Index:只索引你關心的子集
CREATE INDEX ON orders (user_id) WHERE status = 'PENDING';
-- 3. Expression Index:索引計算後的值
CREATE UNIQUE INDEX ON users (LOWER(email));
-- 讓 WHERE LOWER(email) = 'xxx' 走 Index
# 9. JSONB 給半結構化資料,核心資料還是放欄位
CREATE TABLE profiles (
user_id BIGINT PRIMARY KEY REFERENCES users(user_id),
-- 核心資料放欄位
theme TEXT GENERATED ALWAYS AS (attrs->>'theme') STORED,
-- 彈性的半結構化資料放 JSONB
attrs JSONB NOT NULL DEFAULT '{}'
);
-- JSONB 要搭配 GIN Index
CREATE INDEX profiles_attrs_gin ON profiles USING GIN (attrs);
JSONB(Binary JSON)是 Postgres 最強大的功能之一,但不是萬能的。核心欄位(常做條件查詢、JOIN 的)還是要放正規欄位,JSONB 留給「有時候有、有時候沒有」的彈性屬性。
另外注意:用 JSONB 不要用 JSON。除非你需要保留 key 的原始順序,否則 JSONB 在索引、查詢效能上都遠優於 JSON。
# 10. Schema 變更要先想好順序(Safe Migration)
加欄位不難,但加 NOT NULL 欄位要小心:
-- ❌ 危險:volatile default 會觸發全表 rewrite(鎖表!)
ALTER TABLE users ADD COLUMN session_id UUID NOT NULL DEFAULT gen_random_uuid();
-- ✅ 安全做法(Expand-Contract Pattern):
-- Step 1: 先加 nullable 欄位
ALTER TABLE users ADD COLUMN session_id UUID;
-- Step 2: Backfill
UPDATE users SET session_id = gen_random_uuid() WHERE session_id IS NULL;
-- Step 3: 加 NOT NULL constraint
ALTER TABLE users ALTER COLUMN session_id SET NOT NULL;
同樣的,要建 Index 時用 CONCURRENTLY 避免鎖表:
CREATE INDEX CONCURRENTLY ON users (created_at);
# 完整 Table 範例
-- Users 表
CREATE TABLE users (
user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX ON users (LOWER(email));
CREATE INDEX ON users (created_at);
-- Orders 表
CREATE TABLE orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id),
status TEXT NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'PAID', 'CANCELED')),
total NUMERIC(10,2) NOT NULL CHECK (total > 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON orders (user_id);
CREATE INDEX ON orders (created_at);
# 安裝 pg-aiguide Skill
如果你在用 Claude Code,可以直接安裝 Timescale 官方的 pg-aiguide:
/plugin marketplace add timescale/pg-aiguide
/plugin install pg
裝完後 Claude Code 在幫你設計 Postgres Schema 時,會自動套用這些最佳實踐,不用每次都手動提醒。
參考來源:timescale/pg-aiguide — MCP server and Claude plugin for Postgres skills and documentation.