37、PostgreSQL高级查询:窗口函数、CTE、递归查询、JSONB操作

说实话,PostgreSQL 在高级查询这块,真的把很多商业数据库都比下去了。我最早接触它是在一个电商项目里,那时候 MySQL 处理复杂报表查询已经让我有点头疼了。后来换到 PostgreSQL,才发现原来 SQL 可以写得这么优雅。

今天咱们就聊聊四个硬核话题:窗口函数、CTE、递归查询、还有 JSONB 操作。这些功能,说白了就是让你用更少的代码,干更多的活。

窗口函数:不丢细节的聚合

普通聚合函数,比如 SUM、AVG,一用就把多行压成一行。窗口函数不一样,它能在保留每一行数据的同时,算出聚合值。

我举个例子。假设我们有个销售表:

CREATE TABLE sales (
    id SERIAL PRIMARY KEY,
    product TEXT,
    amount NUMERIC,
    sale_date DATE
);

INSERT INTO sales VALUES
(1, 'A', 100, '2024-01-01'),
(2, 'A', 200, '2024-01-02'),
(3, 'B', 150, '2024-01-01'),
(4, 'B', 250, '2024-01-02'),
(5, 'A', 300, '2024-01-03');

现在我想看每个产品的累计销售额。用窗口函数:

SELECT 
    product,
    sale_date,
    amount,
    SUM(amount) OVER (PARTITION BY product ORDER BY sale_date) AS running_total
FROM sales;

结果会保留每一行,同时多出一列 running_total,显示到当前行为止的累计值。我在项目中做财务流水对账时,这个功能帮了大忙。你想想看,如果用程序去算累计,还得写循环,多麻烦。

关键点:窗口函数中的 ORDER BY 控制窗口内的排序,PARTITION BY 控制分组。不写 PARTITION BY 就是对全表做窗口计算。

常用的窗口函数还有 ROW_NUMBER()、RANK()、DENSE_RANK()、LAG()、LEAD()。比如找每个产品销售额最高的那天:

SELECT * FROM (
    SELECT 
        product,
        sale_date,
        amount,
        ROW_NUMBER() OVER (PARTITION BY product ORDER BY amount DESC) AS rn
    FROM sales
) t WHERE rn = 1;

CTE:让复杂查询变清晰

CTE 就是公用表表达式,用 WITH 关键字定义。我个人习惯用它来拆解多层嵌套的子查询。你想想看,三层子查询嵌套在一起,读起来多痛苦。CTE 可以把每一层拆成独立的步骤。

WITH high_sales AS (
    SELECT product, SUM(amount) AS total
    FROM sales
    GROUP BY product
    HAVING SUM(amount) > 300
)
SELECT * FROM high_sales;

这只是一个简单的例子。实际项目中,我经常把 CTE 链起来用:

WITH 
step1 AS (
    SELECT product, SUM(amount) AS total
    FROM sales
    GROUP BY product
),
step2 AS (
    SELECT product, total,
           RANK() OVER (ORDER BY total DESC) AS rank
    FROM step1
)
SELECT * FROM step2 WHERE rank <= 3;

嗯,这里要注意:CTE 本质上就是个临时视图,不会物化数据。但如果同一个 CTE 被多次引用,PostgreSQL 会把它当作一个优化屏障,有时候反而影响性能。我曾经踩过这个坑,后来改用临时表才解决。

递归查询:搞定树形结构

递归 CTE 是 CTE 的进阶用法。它由两部分组成:一个非递归的初始查询,和一个递归部分,用 UNION ALL 连接。

举个例子,组织架构表:

CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name TEXT,
    manager_id INT REFERENCES employees(id)
);

INSERT INTO employees VALUES
(1, '张三', NULL),
(2, '李四', 1),
(3, '王五', 1),
(4, '赵六', 2),
(5, '钱七', 2);

我想查张三下面所有下属,包括间接下属:

WITH RECURSIVE org_tree AS (
    -- 非递归部分:找到根节点
    SELECT id, name, manager_id, 1 AS level
    FROM employees
    WHERE name = '张三'
    
    UNION ALL
    
    -- 递归部分:找下属
    SELECT e.id, e.name, e.manager_id, t.level + 1
    FROM employees e
    JOIN org_tree t ON e.manager_id = t.id
)
SELECT * FROM org_tree;

递归查询在 BOM 物料清单、评论回复树、菜单权限树这些场景下特别实用。我记得有一次做知识库系统,分类层级有 7 层深,用递归 CTE 一条 SQL 就搞定了。换成程序递归,得写几十行代码。

避坑指南:递归 CTE 必须要有终止条件。如果数据里有循环引用(比如 A 的上级是 B,B 的上级又是 A),递归会无限循环。我曾经在测试环境遇到过,直接把数据库跑挂了。解决方案是在递归部分加一个深度限制:WHERE t.level < 10。

JSONB 操作:灵活的数据存储

PostgreSQL 的 JSONB 支持,让我在关系型数据库里也能玩转 NoSQL 风格的数据。JSONB 是二进制格式,比 JSON 类型更快,还支持索引。

创建一个带 JSONB 字段的表:

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name TEXT,
    attributes JSONB
);

INSERT INTO products VALUES
(1, '手机', '{"color": "黑色", "storage": "128GB", "price": 2999}'),
(2, '手机', '{"color": "白色", "storage": "256GB", "price": 3499}'),
(3, '电脑', '{"cpu": "i7", "ram": "16GB", "price": 6999}');

查询 JSONB 字段里的值:

-- 获取所有手机的存储容量
SELECT name, attributes->>'storage' AS storage
FROM products
WHERE attributes @> '{"color": "黑色"}';

-- 价格大于 3000 的产品
SELECT * FROM products
WHERE (attributes->>'price')::NUMERIC > 3000;

JSONB 还支持更新部分字段:

-- 把黑色手机的价格改为 2799
UPDATE products
SET attributes = jsonb_set(attributes, '{price}', '2799')
WHERE attributes @> '{"color": "黑色"}';

我在一个电商项目中,用 JSONB 存储商品的动态属性。不同类别的商品属性差异很大,如果用传统的关系模型,得建几十张属性表。用 JSONB 一张表就搞定了,而且查询性能还不错——只要建了 GIN 索引。

小技巧:JSONB 字段上建 GIN 索引,可以加速 @>、?、?|、?& 这些操作符的查询。语法:CREATE INDEX idx_gin ON products USING GIN (attributes);

知识体系总览

下面这张图,把今天讲的四个知识点串起来了。你可以看到它们各自解决什么问题,以及在实际项目中如何配合使用。

PostgreSQL 高级查询知识体系 高级查询核心 窗口函数 CTE(公用表表达式) 递归查询 JSONB 操作 保留行级细节的聚合 ROW_NUMBER / RANK / LAG 拆解复杂嵌套查询 WITH 链式定义 树形结构遍历 组织架构 / BOM 清单 灵活存储动态属性 @> / ->> / jsonb_set 适用场景:报表统计、树形数据、灵活属性存储

这四个功能,单独用已经很强大,组合起来更是如虎添翼。比如在 CTE 里用窗口函数做排名,再结合 JSONB 字段过滤,一条 SQL 就能完成过去需要多步处理的任务。

好了,今天的内容就到这。这些技巧我在实际项目中反复用过,确实能提升开发效率。你可以在自己的 PostgreSQL 环境里试试,感受一下它们的威力。


公众号:蓝海资料掘金营,微信 deep3321