PostgreSQL Field Guide

查询工具箱

从过滤与 JOIN 到 CTE、窗口函数和安全参数化

可维护查询的基本形状

SELECT
  o.id,
  c.email,
  o.total_cents,
  o.placed_at
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
WHERE o.status = $1
  AND o.placed_at >= $2
ORDER BY o.placed_at DESC, o.id DESC
LIMIT $3;

这条查询明确了输出、连接条件、参数、稳定排序和上限。$1$2$3 由驱动绑定;不要用字符串拼接用户输入。

JOIN 的决策

目标使用
只保留两边匹配行INNER JOIN / JOIN
保留左表全部行LEFT JOIN
判断相关行是否存在EXISTS,常比“JOIN 后 DISTINCT”更清楚
找没有关联行的数据NOT EXISTS,避免 NOT INNULL 语义陷阱
SELECT c.id, c.email
FROM customers AS c
WHERE NOT EXISTS (
  SELECT 1 FROM orders AS o WHERE o.customer_id = c.id
);

聚合与窗口不是一回事

GROUP BY 把多行折叠为一行;窗口函数保留明细行,同时在窗口内计算。

SELECT
  customer_id,
  id AS order_id,
  total_cents,
  row_number() OVER (
    PARTITION BY customer_id
    ORDER BY placed_at DESC, id DESC
  ) AS recency_rank,
  sum(total_cents) OVER (PARTITION BY customer_id) AS lifetime_cents
FROM orders;

CTE 的用途

CTE 应为复杂查询命名阶段,而不是自动优化按钮。

WITH recent_paid AS (
  SELECT customer_id, total_cents
  FROM orders
  WHERE status = 'paid'
    AND placed_at >= now() - interval '30 days'
)
SELECT customer_id, sum(total_cents) AS paid_cents
FROM recent_paid
GROUP BY customer_id;

分页

大结果集优先 keyset pagination:

SELECT id, placed_at, total_cents
FROM orders
WHERE (placed_at, id) < ($1, $2)
ORDER BY placed_at DESC, id DESC
LIMIT 50;

与很大的 OFFSET 相比,它不需要不断跳过前面的行,并在并发写入时更稳定。游标必须包含排序的全部键。

写查询前的检查

  • 输出列是否稳定并最小化?
  • 每个 JOIN 是否可能放大行数?
  • NULL 的含义是否明确?
  • 排序是否有唯一的最终 tie-breaker?
  • 参数是否由驱动绑定?
  • 是否需要超时和结果行数上限?

Last updated on

On this page