Timescale 官方出品的 pg-aiguide 是一個 Claude Code Skill,讓 AI 在幫你生成 PostgreSQL 程式碼時,自動套用官方推薦的最佳實踐。這篇文章從裡面精選 10 個最實用的 Best Practice,寫給每天在跟 Postgres 打交道的後端工程師。


# 資料型態

# 1. 用 TEXT 取代 VARCHAR(n)

這是最常被忽略的一條。

PostgreSQL 的 TEXTVARCHAR 底層完全一樣,都是 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)

浮點數(FLOATREALDOUBLE 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.