PostgreSQL 高级 SQL 查询实战:窗口函数、CTE、递归与 JSONB 深度操作

详解 PostgreSQL 超越基础 CRUD 的高级查询能力:窗口函数(ROW_NUMBER/RANK/LEAD/LAG)、递归 CTE、LATERAL JOIN、数组操作、JSONB 高级查询、正则表达式匹配与全文搜索。每个技巧配完整示例和适用场景。

PostgreSQL 的强大不仅在于存储数据,更在于查询数据的能力。从基础的 SELECT ... WHERE 到声明式窗口函数、递归 CTE、LATERAL 连接,PostgreSQL 的 SQL 引擎几乎实现了所有高级分析能力——这些能力在 MySQL 和 MongoDB 中要么缺失,要么要等好几个版本才补齐。

一句话总结:掌握 CTE + 窗口函数这两个利器,你的查询能力就跨越了 80% 开发者。


一、窗口函数(Window Functions)

窗口函数允许在不聚合行的情况下对结果集中的某个"窗口"进行计算。通俗理解:给每行数据一个"还能看到哪些邻居"的视角。

1.1 排名类窗口函数

-- ROW_NUMBER:每行唯一排名(不并列)
SELECT
  name,
  sales,
  ROW_NUMBER() OVER (ORDER BY sales DESC) AS rank_num,
  RANK() OVER (ORDER BY sales DESC) AS rank_equal,         -- 并列跳号
  DENSE_RANK() OVER (ORDER BY sales DESC) AS rank_dense    -- 并列不跳号
FROM employees;

-- 结果:
-- name    | sales  | rank_num | rank_equal | rank_dense
-- Alice   | 1000   | 1        | 1          | 1
-- Bob     | 1000   | 2        | 1          | 1
-- Charlie | 800    | 3        | 3          | 2

1.2 值类窗口函数

-- LEAD/LAG:查看前/后行的值(无上/下行返回 NULL)
SELECT
  date,
  revenue,
  LAG(revenue, 1) OVER (ORDER BY date) AS prev_revenue,
  revenue - LAG(revenue, 1) OVER (ORDER BY date) AS delta,
  LEAD(revenue, 1) OVER (ORDER BY date) AS next_revenue
FROM daily_sales;

-- FIRST_VALUE / LAST_VALUE:窗口内的首尾值
SELECT
  department,
  name,
  salary,
  FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC) AS top_earner,
  LAST_VALUE(name) OVER (
    PARTITION BY department ORDER BY salary DESC
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS lowest_earner
FROM employees;

1.3 聚合类窗口函数

-- 计算部门平均与员工差异
SELECT
  department,
  name,
  salary,
  AVG(salary) OVER (PARTITION BY department) AS dept_avg,
  salary - AVG(salary) OVER (PARTITION BY department) AS diff_from_avg,
  SUM(salary) OVER (PARTITION BY department) AS dept_total,
  salary / SUM(salary) OVER (PARTITION BY department) * 100 AS pct_of_dept
FROM employees;

1.4 窗口函数常用场景

场景函数说明
分页去重ROW_NUMBER() OVER (PARTITION BY x ORDER BY y)取每组最新一行
同比环比LAG(x, 12) OVER (ORDER BY month)与去年同期对比
累计求和SUM(x) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING)运行总计
移动平均AVG(x) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)7 日均线
分类占比x / SUM(x) OVER (PARTITION BY group)组内百分比

二、CTE(公用表表达式):让查询可组合

CTE 用 WITH 关键字把子查询命名化,让复杂 SQL 可读性提升数倍。

2.1 非递归 CTE

-- 找出每个类目中评分最高的 3 本书
WITH ranked_books AS (
  SELECT
    id, title, category, rating,
    ROW_NUMBER() OVER (PARTITION BY category ORDER BY rating DESC, id) AS rn
  FROM books
)
SELECT * FROM ranked_books WHERE rn <= 3;

2.2 多 CTE 串联

WITH
  monthly_sales AS (
    SELECT DATE_TRUNC('month', order_date) AS month, SUM(amount) AS total
    FROM orders
    GROUP BY 1
  ),
  growth AS (
    SELECT
      month,
      total,
      LAG(total) OVER (ORDER BY month) AS prev_total,
      (total - LAG(total) OVER (ORDER BY month)) / NULLIF(LAG(total) OVER (ORDER BY month), 0) * 100 AS growth_rate
    FROM monthly_sales
  )
SELECT * FROM growth WHERE month >= '2025-01-01';

2.3 递归 CTE:遍历层级结构

-- 查找某员工的全部下属(组织架构)
WITH RECURSIVE org_tree AS (
  -- 锚点:从 CEO 开始
  SELECT id, name, manager_id, 0 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- 递归步:找到下属的下属
  SELECT e.id, e.name, e.manager_id, ot.level + 1
  FROM employees e
  JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT id, name, level, REPEAT('  ', level) || name AS indented_name
FROM org_tree
ORDER BY level, name;

-- 结果:
-- id | name    | level | indented_name
-- 1  | Alice   | 0     | Alice
-- 2  | Bob     | 1     |   Bob
-- 3  | Charlie | 1     |   Charlie
-- 4  | Dave    | 2     |     Dave

2.4 递归 CTE 查找路径

-- 文件系统路径拼接(层级目录树)
WITH RECURSIVE path_tree AS (
  SELECT id, name, parent_id, name AS full_path
  FROM folders
  WHERE parent_id IS NULL

  UNION ALL

  SELECT f.id, f.name, f.parent_id, pt.full_path || '/' || f.name
  FROM folders f
  JOIN path_tree pt ON f.parent_id = pt.id
)
SELECT full_path FROM path_tree WHERE id = 42;
-- 结果:Root/Projects/Backend/Database

三、LATERAL JOIN:子查询当列用

LATERAL JOIN 允许子查询引用主表的列,相当于"为每行执行一次子查询"。

3.1 获取每个用户最近 3 篇博客

-- 不用 LATERAL:自连接 + 子查询,复杂且慢
-- 用 LATERAL:清晰直观
SELECT u.id, u.name, p.title, p.created_at
FROM users u
LEFT JOIN LATERAL (
  SELECT id, title, created_at
  FROM posts
  WHERE author_id = u.id
  ORDER BY created_at DESC
  LIMIT 3
) p ON true
ORDER BY u.id, p.created_at DESC;

3.2 LATERAL + 计算列

-- 计算每个订单的运费(按重量分区对应不同费率)
SELECT o.id, o.total, o.weight,
       COALESCE(fr.rate * o.weight, 0) AS shipping_cost,
       o.total + COALESCE(fr.rate * o.weight, 0) AS grand_total
FROM orders o
LEFT JOIN LATERAL (
  SELECT rate FROM freight_rates
  WHERE o.weight >= min_weight AND o.weight < max_weight
  LIMIT 1
) fr ON true;

四、数组操作:列级集合处理

PostgreSQL 的数组类型支持完整的集合运算。

4.1 数组 CRUD

-- 创建含数组列的表
CREATE TABLE articles (
  id SERIAL PRIMARY KEY,
  title TEXT,
  tags TEXT[]
);

-- 插入数组
INSERT INTO articles (title, tags) VALUES
  ('Postgres Arrays', ARRAY['sql', 'advanced', 'postgres']),
  ('Window Functions', '{sql, window}');  -- 两种语法

-- 追加/删除元素
UPDATE articles SET tags = array_append(tags, 'tutorial') WHERE id = 1;
UPDATE articles SET tags = array_remove(tags, 'old') WHERE id = 1;

-- 包含某个标签
SELECT * FROM articles WHERE tags @> ARRAY['sql'];

-- 包含任意标签(交集)
SELECT * FROM articles WHERE tags && ARRAY['window', 'advanced'];

-- 数组长度
SELECT title, array_length(tags, 1) AS tag_count FROM articles;

-- 展开为行(unnest)
SELECT unnest(tags) AS tag FROM articles;
-- 结果 → sql, advanced, postgres, sql, window

-- 唯一标签统计
SELECT unnest(tags) AS tag, COUNT(*) AS cnt
FROM articles
GROUP BY tag
ORDER BY cnt DESC;

4.2 数组 + GIN 索引

CREATE INDEX idx_articles_tags ON articles USING GIN(tags);

-- 含 'postgres' 标签的查询用 GIN 索引
SELECT * FROM articles WHERE tags @> ARRAY['postgres'];

五、JSONB 高级查询

PostgreSQL JSONB 不只是存储,它支持完整的查询与索引。

5.1 JSONB 操作速查

操作符含义示例
->取 JSONB 子对象data->'user'
取文本值data->>'name'
#>按路径取对象data#>'{address,city}'
按路径取文本data#>>'{address,city}'
@>包含(左包含右)data @> '{"active": true}'
<@被包含'{"role":"admin"}' <@ data
?键存在data ? 'email'
?任意键存在
?&所有键都存在data ?& ARRAY['email', 'phone']

5.2 JSONB 深度查询

-- 查找订单中购买了苹果产品的
SELECT * FROM orders
WHERE items @> '[{"product": {"brand": "Apple"}}]';

-- 按嵌套属性排序
SELECT * FROM products
ORDER BY (metadata->'specs'->>'rating')::NUMERIC DESC;

-- JSONB 聚合(多条记录聚合为单个 JSONB)
SELECT user_id,
  JSONB_AGG(
    JSONB_BUILD_OBJECT('title', title, 'created_at', created_at)
    ORDER BY created_at DESC
  ) AS post_history
FROM posts
GROUP BY user_id;

-- JSONB 列值批量更新(给所有用户的 metadata 加字段)
UPDATE users
SET metadata = metadata || '{"newsletter_opt_in": true}'::jsonb;

5.3 JSONB + GIN 路径索引

-- 为特定 JSONB 路径建索引,避免全表扫描
CREATE INDEX idx_orders_items ON orders (
  (items->0->>'product_id')
);

-- 更通用的 GIN 索引
CREATE INDEX idx_products_metadata ON products USING GIN(metadata jsonb_path_ops);

六、正则表达式与文本搜索

6.1 正则表达式匹配

-- POSIX 正则
SELECT * FROM users WHERE email ~ '^[a-zA-Z0-9._%+-]+@gmail\.com$';

-- 不区分大小写
SELECT * FROM users WHERE name ~* '^alice';

-- 正则替换
SELECT REGEXP_REPLACE(phone, '^(\d{3})-(\d{4})$', '(\1) \2-XXXX');

-- 分组提取
SELECT REGEXP_MATCHES(email, '(.+)@(.+)', 'i') FROM users;

6.2 全文搜索进阶

-- 多语言混合搜索(中文需要额外扩展)
SELECT id, title, ts_rank(tsv, query) AS rank
FROM articles,
  to_tsquery('english', 'postgres & optimization') query
WHERE tsv @@ query
ORDER BY rank DESC;

-- 创建加权 tsvector(标题权重 A,正文 B)
ALTER TABLE articles ADD COLUMN search_vector tsvector
  GENERATED ALWAYS AS (
    SETWEIGHT(to_tsvector('english', COALESCE(title, '')), 'A') ||
    SETWEIGHT(to_tsvector('english', COALESCE(body, '')), 'B')
  ) STORED;

CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);

-- 高亮摘要
SELECT id, title,
  ts_headline('english', body, plainto_tsquery('english', 'database')) AS highlight
FROM articles
WHERE search_vector @@ plainto_tsquery('english', 'database');

七、综合案例:构建报表查询

7.1 用户活跃度漏斗分析

WITH monthly_stats AS (
  SELECT
    DATE_TRUNC('month', created_at) AS month,
    user_id,
    COUNT(*) AS session_count
  FROM sessions
  GROUP BY 1, 2
),
engagement AS (
  SELECT
    month,
    COUNT(DISTINCT user_id) AS total_users,
    COUNT(DISTINCT CASE WHEN session_count >= 5 THEN user_id END) AS power_users,
    COUNT(DISTINCT CASE WHEN session_count = 1 THEN user_id END) AS one_time_users,
    AVG(session_count)::NUMERIC(10,2) AS avg_sessions
  FROM monthly_stats
  GROUP BY month
)
SELECT
  month,
  total_users,
  power_users,
  ROUND(power_users::NUMERIC / total_users * 100, 2) AS power_user_rate,
  one_time_users,
  avg_sessions
FROM engagement
ORDER BY month;

7.2 商品类目层级路径与深度

WITH RECURSIVE category_path AS (
  SELECT id, name, parent_id, name AS path, 0 AS depth
  FROM categories WHERE parent_id IS NULL

  UNION ALL

  SELECT c.id, c.name, c.parent_id, cp.path || ' > ' || c.name, cp.depth + 1
  FROM categories c
  JOIN category_path cp ON c.parent_id = cp.id
)
SELECT * FROM category_path ORDER BY path;

常见问题(FAQ)

窗口函数能用 CTE 替代吗?

不能。CTE 让查询结构化,窗口函数让每行获得邻行视角,两者互补。复杂报表通常是 CTE 定义中间集 + 窗口函数计算排名/占比。

递归 CTE 会无限循环吗?

可能,如果层级数据成环(A 管 B,B 又管 A)。PostgreSQL 默认有 max_recursive_iterations 限制,或通过 CYCLE 检测:WITH RECURSIVE ... CYCLE id SET is_cycle USING path

LATERAL JOIN 和普通 JOIN 有什么区别?

普通 JOIN 先求两个表的笛卡尔积后过滤;LATERAL JOIN 先取左表一行,再执行右子查询(可引用左表列),最后连接。适合"每行一个动态子集"的应用。


相关阅读

← 上一篇

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章