Files

5.2 KiB
Raw Permalink Blame History

id, title, type, domain, stack, tags, level, est_hours, related, week, order
id title type domain stack tags level est_hours related week order
backend.postgresql.schema-sql 表设计和 SQL:约束、JOIN、CTE、窗口函数、JSONB、UPSERT lesson backend
postgresql
SQL
表设计
约束
JOIN
窗口函数
JSONB
basic 4
backend.postgresql.index-tx-mvcc
backend.postgresql.interview
2
1

表设计和 SQL:约束、JOIN、CTE、窗口函数、JSONB、UPSERT

解释说明

表设计的基本功

  • 主键:一般用 BIGINT GENERATED ALWAYS AS IDENTITY(自增)或 UUID。对外暴露的 ID 用 UUID 或随机字符串可以避免被遍历。
  • 约束是最后一道防线:NOT NULL、UNIQUE、CHECK (amount > 0)、外键 REFERENCES。应用代码有 bug 时,数据库约束能挡住脏数据。
  • 范式和反范式:一般按第三范式拆表(一个事实只存一处);为了查询性能可以有意冗余(例如订单里冗余商品名称快照),但要清楚冗余字段谁负责更新。
  • 时间:用 TIMESTAMPTZ(带时区)存时间,统一存 UTC,展示时再转换。
  • 金额:用 NUMERIC(12,2) 或以“分”为单位的整数,不要用浮点数。

常用 SQL

  • JOIN:内连接只保留两边都有的行;左连接保留左表全部行,右边没有就是 NULL。
  • GROUP BY + 聚合函数;HAVING 过滤分组后的结果。
  • CTE(WITH ... AS):把复杂查询拆成几步,可读性好。
  • 窗口函数:ROW_NUMBER() OVER (PARTITION BY 用户 ORDER BY 时间 DESC) 不合并行就能做“每组取前 N 条”、排名、累计。
  • UPSERT:INSERT ... ON CONFLICT (键) DO UPDATE 一条语句完成“有则更新、无则插入”,并发下也安全(依赖唯一约束)。
  • RETURNING:插入、更新后直接返回新值,省一次查询。
  • JSONB:存半结构化数据(如商品的可变属性),支持索引(GIN)和查询运算符 ->>、@>。结构稳定、经常查询的字段还是建成普通列。

案例

电商后台的“每个用户最近 3 笔订单”“按月统计各渠道销售额”“商品扩展属性”都是上面这些语法的典型用途。下面用一个最小的订单库演示。

代码示例

-- 建表:约束写全
CREATE TABLE users (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email      TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id    BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
  channel    TEXT NOT NULL CHECK (channel IN ('web', 'app', 'mini')),
  amount     NUMERIC(12, 2) NOT NULL CHECK (amount > 0),
  attrs      JSONB NOT NULL DEFAULT '{}',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO users (email) VALUES ('a@example.com'), ('b@example.com');
INSERT INTO orders (user_id, channel, amount, attrs, created_at) VALUES
  (1, 'web', 99.00,  '{"coupon": "NEW10"}', now() - interval '3 day'),
  (1, 'app', 25.50,  '{}',                  now() - interval '2 day'),
  (1, 'app', 300.00, '{"gift": true}',      now() - interval '1 day'),
  (1, 'web', 12.00,  '{}',                  now()),
  (2, 'mini', 45.00, '{"coupon": "VIP"}',   now());

-- 1. 每个用户最近 3 笔订单(窗口函数)
SELECT * FROM (
  SELECT o.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
  FROM orders o
) t WHERE rn <= 3;

-- 2. 按渠道统计,只看销售额超过 50 的渠道(GROUP BY + HAVING)
SELECT channel, count(*) AS cnt, sum(amount) AS total
FROM orders GROUP BY channel HAVING sum(amount) > 50 ORDER BY total DESC;

-- 3. CTE:先算每个用户的消费总额,再关联用户表
WITH spend AS (
  SELECT user_id, sum(amount) AS total FROM orders GROUP BY user_id
)
SELECT u.email, COALESCE(s.total, 0) AS total
FROM users u LEFT JOIN spend s ON s.user_id = u.id;

-- 4. JSONB 查询:用了优惠券的订单
SELECT id, attrs->>'coupon' AS coupon FROM orders WHERE attrs ? 'coupon';

-- 5. UPSERT:每日统计表,重复执行不会出错
CREATE TABLE daily_sales (day DATE PRIMARY KEY, total NUMERIC(12, 2) NOT NULL);
INSERT INTO daily_sales (day, total)
SELECT created_at::date, sum(amount) FROM orders GROUP BY 1
ON CONFLICT (day) DO UPDATE SET total = EXCLUDED.total
RETURNING *;

运行:docker run -d --name pg -e POSTGRES_PASSWORD=pg -p 5432:5432 postgres:17,然后 psql -h localhost -U postgres 粘贴执行。

易错点

  • NOT IN 遇到 NULL:x NOT IN (子查询) 只要子查询结果里有一个 NULL,整个条件就不成立,结果为空;改用 NOT EXISTS。
  • count(列) 不统计 NULL,count(*) 统计所有行。
  • 浮点数存金额:0.1 + 0.2 的精度问题会出现在账目里。
  • LEFT JOIN 后在 WHERE 里过滤右表:会把左连接变回内连接,右表的条件要写在 ON 里。
  • 先查再插判断唯一:并发时两个请求都查不到、都插入成功;用唯一约束 + ON CONFLICT。
  • JSONB 什么都往里放:失去约束和类型检查,查询也难优化。

面试常问

见 PostgreSQL 面试题:范式、NOT IN 和 NULL、UPSERT 的并发安全、JSONB 的使用场景。