GROUP BY的原理与优化
GROUP BY的基本概念
GROUP BY是PostgreSQL中用于分组聚合的核心子句,它将表中具有相同值的行归为一组,然后对每组计算聚合函数(如sum、avg、count)。例如,我们有一张记录商品销售的test1表:
CREATE TABLE test1 (x TEXT, y INT);
INSERT INTO test1 VALUES ('a', 3), ('c', 2), ('b', 5), ('a', 1);
若要计算每个x对应的y之和,可使用:
SELECT x, sum(y) FROM test1 GROUP BY x;
结果会按x分组,返回每组的求和结果:
x | sum
---+-----
a | 4
b | 5
c | 2
关键规则:SELECT列表中的列要么在GROUP BY中(分组键),要么被聚合函数包裹(否则会因“非分组列无法确定唯一值”报错)。
功能依赖与GROUP BY简化
PostgreSQL支持功能依赖(Functional Dependency)优化:若GROUP BY的列是表的主键或唯一约束,则其他依赖于该列的列(即主键能唯一确定的列)无需加入GROUP BY。例如,products表的product_id是主键,name和price依赖于product_id:
CREATE TABLE products (
product_id INT PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10,2) NOT NULL
);
CREATE TABLE sales (
sale_id INT PRIMARY KEY,
product_id INT REFERENCES products(product_id),
units INT NOT NULL,
sale_date DATE NOT NULL
);
查询每个产品的总销售额时,无需将name和price加入GROUP BY:
SELECT
p.product_id,
p.name, -- 依赖于product_id,无需GROUP BY
sum(s.units * p.price) AS total_sales
FROM products p
LEFT JOIN sales s ON p.product_id = s.product_id
GROUP BY p.product_id; -- 仅需分组主键
这一优化减少了分组的复杂度,因为PostgreSQL知道主键能唯一确定其他列的值,无需额外分组检查。
GROUPING SETS、CUBE与ROLLUP的高效聚合
当需要生成多个分组的聚合结果时(如同时按“品牌”“尺寸”“总合计”分组),多次查询会重复扫描数据,而GROUPING SETS、CUBE、ROLLUP能一次性生成多组聚合,大幅提升效率。
以items_sold表为例:
CREATE TABLE items_sold (brand TEXT, size TEXT, sales INT);
INSERT INTO items_sold VALUES
('Foo', 'L', 10), ('Foo', 'M', 20), ('Bar', 'M', 15), ('Bar', 'L', 5);
若要同时按brand、size和“总合计”分组,使用GROUPING SETS:
SELECT brand, size, sum(sales)
FROM items_sold
GROUP BY GROUPING SETS ((brand), (size), ()); -- 三组分组
结果会返回三组聚合:
brand | size | sum
-------+------+-----
Foo | | 30 -- 按brand分组
Bar | | 20
| L | 15 -- 按size分组
| M | 35
| | 50 -- 总合计(空分组)
CUBE(a, b):生成a、b、a+b、空分组的所有组合(即“立方体”聚合);ROLLUP(a, b):生成a+b、a、空分组的层级聚合(如“省份+城市”“省份”“全国”)。
这些扩展避免了多次全表扫描,是处理多维度分析的高效工具。
HAVING与WHERE的区别
HAVING用于过滤分组后的结果,而WHERE用于过滤原始行。例如:
-- 过滤“sum(y) > 3”的分组
SELECT x, sum(y) FROM test1 GROUP BY x HAVING sum(y) > 3;
-- 过滤“x < 'c'”的原始行,再分组
SELECT x, sum(y) FROM test1 WHERE x < 'c' GROUP BY x;
注意:HAVING可以使用聚合函数,WHERE不能(WHERE过滤的是未分组的行,聚合函数尚未计算)。
ORDER BY的优化策略
索引与排序的关系
ORDER BY的性能核心是是否能利用索引避免排序。PostgreSQL的索引是有序的(如B-tree索引),若ORDER BY的列顺序与索引完全一致(包括ASC/DESC),则可直接通过索引获取有序数据,跳过Sort操作。
例如,orders表的order_date列有索引:
CREATE INDEX idx_orders_order_date ON orders(order_date DESC);
查询最新订单时,执行计划会使用Index Scan而非Seq Scan + Sort:
SELECT * FROM orders ORDER BY order_date DESC LIMIT 10;
执行计划示例:
Limit (cost=0.29..1.04 rows=10 width=44)
-> Index Scan using idx_orders_order_date on orders (cost=0.29..74.29 rows=1000 width=44)
若ORDER BY的列没有索引,PostgreSQL会进行内存排序(in-memory sort),若数据量超过work_mem(默认4MB),则会写入临时文件(外部排序),性能骤降。
Top-N查询的优化
Top-N查询(如“取最新10条数据”)是ORDER BY的常见场景,PostgreSQL会使用Top-N Heapsort优化:只需维护一个大小为N的堆(如10),遍历数据时不断替换堆中最小的元素,无需排序整个结果集。
例如,查询销量最高的5个产品:
SELECT product_id, sum(units) AS total_units
FROM sales
GROUP BY product_id
ORDER BY total_units DESC
LIMIT 5;
若sum(units)无法用索引,Top-N Heapsort仍比全排序高效——因为堆的大小远小于总数据量。
内存与外部排序
work_mem参数控制PostgreSQL用于排序、哈希等操作的内存上限。若排序数据量超过work_mem,会触发外部排序(将数据分成多个块,每个块内存排序后写入临时文件,最后合并块),性能下降明显。
优化方法:
- 临时调整
work_mem(会话级别,不影响全局):SET work_mem = '64MB'; -- 将排序内存提升至64MB - 创建合适的索引:避免排序(推荐)。
- 减少排序数据量:使用
WHERE过滤不必要的行,或LIMIT限制结果数。
窗口函数OVER()的效率提升
窗口函数的执行时机
窗口函数(如row_number()、sum() OVER())用于计算每行的“窗口内”聚合(如累计销售额、排名),其执行顺序在GROUP BY之后、SELECT之前:
FROM → WHERE → GROUP BY → HAVING → 窗口函数 → SELECT → ORDER BY
例如,计算每个产品的累计销售额:
SELECT
s.product_id,
s.sale_date,
s.units * p.price AS daily_sales,
sum(s.units * p.price) OVER (
PARTITION BY s.product_id -- 按产品分区
ORDER BY s.sale_date -- 按日期排序
) AS running_total
FROM sales s
JOIN products p ON s.product_id = p.product_id;
PARTITION BY将数据分成多个“窗口”(如每个产品一组),ORDER BY定义窗口内的行顺序,sum() OVER()计算窗口内的累计和。
窗口定义的索引优化
窗口函数的性能取决于窗口内数据的有序性。若PARTITION BY和ORDER BY的列有复合索引,PostgreSQL可快速分区并排序,避免额外的Sort操作。
例如,为sales表创建product_id + sale_date的复合索引:
CREATE INDEX idx_sales_product_date ON sales(product_id, sale_date);
上述累计销售额查询的执行计划会跳过排序,直接使用索引获取有序数据:
WindowAgg (cost=0.56..1.71 rows=100 width=56)
-> Index Scan using idx_sales_product_date on sales s (cost=0.29..1.21 rows=100 width=28)
Join Filter: (s.product_id = p.product_id)
窗口复用与合并
若多个窗口函数使用完全相同的PARTITION BY和ORDER BY,可通过WINDOW子句定义窗口并复用,减少重复计算。
例如,同时计算累计销售额和排名:
SELECT
product_id,
sale_date,
daily_sales,
sum(daily_sales) OVER w AS running_total, -- 复用窗口w
row_number() OVER w AS rank -- 复用窗口w
FROM (
SELECT
s.product_id,
s.sale_date,
s.units * p.price AS daily_sales
FROM sales s
JOIN products p ON s.product_id = p.product_id
) AS subquery
WINDOW w AS (PARTITION BY product_id ORDER BY sale_date); -- 定义窗口w
WINDOW子句将窗口逻辑集中定义,PostgreSQL只需计算一次窗口,提升效率。
Frame Clause的选择
窗口函数的Frame Clause(如ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)定义窗口内的行范围,不同类型的Frame性能差异显著:
- ROWS Frame:基于行位置(如“前所有行到当前行”),计算最快(直接按顺序累加)。
- RANGE Frame:基于值范围(如“所有sale_date ≤ 当前行的行”),需比较值,性能较慢。
例如,累计销售额应使用ROWS Frame(默认即ROWS UNBOUNDED PRECEDING):
sum(daily_sales) OVER (PARTITION BY product_id ORDER BY sale_date) -- 默认ROWS
若需按值范围计算(如“最近7天的销售额”),则需使用RANGE Frame,但需注意性能:
sum(daily_sales) OVER (
PARTITION BY product_id
ORDER BY sale_date
RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW
)
课后Quiz
问题1:为什么GROUP BY主键列时,不需要将其他依赖列加入GROUP BY子句?
答案:因为主键列具有功能依赖性——主键的值唯一确定了其他列的值(如product_id确定name和price)。PostgreSQL支持这一优化,允许SELECT列表中包含依赖列,无需将它们加入GROUP BY,减少分组的复杂度和计算量。
问题2:如何优化包含ORDER BY和LIMIT的Top-N查询?
答案:
- 创建匹配的索引:使
ORDER BY的列顺序与索引完全一致(包括ASC/DESC),直接通过索引获取有序数据。 - 利用Top-N Heapsort:LIMIT的行数越小,HeapSort的优势越明显(无需排序全表)。
- 避免表达式排序:若
ORDER BY使用表达式(如LOWER(name)),需为表达式创建索引(如CREATE INDEX idx_name_lower ON users(LOWER(name)))。
问题3:窗口函数的PARTITION BY和ORDER BY如何影响性能?
答案:
PARTITION BY的列若有索引,可快速将数据分成不同窗口,避免额外的分组操作。ORDER BY的列若有索引,可直接获取窗口内的有序数据,跳过Sort操作。- 复合索引(
PARTITION BY列 + ORDER BY列)能最大化窗口函数的性能,因为无需任何额外排序或分组。
常见报错解决方案
报错1:ERROR: column "table.column" must appear in the GROUP BY clause or be used in an aggregate function
原因:SELECT列表中的列既不在GROUP BY中,也未被聚合函数包裹(违反分组规则)。
解决方法:
- 将列加入
GROUP BY(如GROUP BY x, y)。 - 对列使用聚合函数(如
sum(y))。 - 利用功能依赖(若列依赖于
GROUP BY的主键,无需加入)。
预防建议:分组时尽量只包含必要的列,优先使用主键作为分组键。
报错2:ERROR: window function requires an OVER clause
原因:使用了窗口函数(如row_number())但未指定OVER子句(窗口函数必须通过OVER定义窗口)。
解决方法:为窗口函数添加OVER子句,指定PARTITION BY和ORDER BY(如row_number() OVER (PARTITION BY product_id ORDER BY sale_date))。
预防建议:编写窗口函数时,确保每个函数都有对应的OVER子句。
报错3:ERROR: could not sort because work_mem exceeded
原因:排序数据量超过work_mem,触发外部排序(写入临时文件)。
解决方法:
- 临时调整
work_mem:SET work_mem = '64MB'(会话级别,不影响全局)。 - 创建索引:避免排序(优先选择)。
- 减少排序数据量:使用
WHERE过滤或LIMIT限制结果数。
预防建议:根据查询需求合理调整work_mem,避免不必要的排序。
参考链接
参考链接:www.postgresql.org/docs/17/que…
参考链接:www.postgresql.org/docs/17/que…
参考链接:www.postgresql.org/docs/17/ind…
参考链接:www.postgresql.org/docs/17/sql…
参考链接:www.postgresql.org/docs/17/run…
往期文章归档
- PostgreSQL里的子查询和CTE居然在性能上“掐架”?到底该站哪边? - cmdragon's Blog
- PostgreSQL选Join策略有啥小九九?Nested Loop/Merge/Hash谁是它的菜? - cmdragon's Blog
- PostgreSQL新手SQL总翻车?这7个性能陷阱你踩过没? - cmdragon's Blog
- PostgreSQL索引选B-Tree还是GiST?“瑞士军刀”和“多面手”的差别你居然还不知道? - cmdragon's Blog
- 想知道数据库怎么给查询“算成本选路线”?EXPLAIN能帮你看明白? - cmdragon's Blog
- PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机? - cmdragon's Blog
- PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前? - cmdragon's Blog
- 转账不翻车、并发不干扰,PostgreSQL的ACID特性到底有啥魔法? - cmdragon's Blog
- 银行转账不白扣钱、电商下单不超卖,PostgreSQL事务的诀窍是啥? - cmdragon's Blog
- PostgreSQL里的PL/pgSQL到底是啥?能让SQL从“说目标”变“讲步骤”? - cmdragon's Blog
- PostgreSQL视图不存数据?那它怎么简化查询还能递归生成序列和控制权限? - cmdragon's Blog
- PostgreSQL索引这么玩,才能让你的查询真的“飞”起来? - cmdragon's Blog
- PostgreSQL的表关系和约束,咋帮你搞定用户订单不混乱、学生选课不重复? - cmdragon's Blog
- PostgreSQL查询的筛子、排序、聚合、分组?你会用它们搞定数据吗? - cmdragon's Blog
- PostgreSQL数据类型怎么选才高效不踩坑? - cmdragon's Blog
- 想解锁PostgreSQL查询从基础到进阶的核心知识点?你都get了吗? - cmdragon's Blog
- PostgreSQL DELETE居然有这些操作?返回数据、连表删你试过没? - cmdragon's Blog
- PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
- PostgreSQL插入数据还在逐条敲?批量、冲突处理、返回自增ID的技巧你会吗? - cmdragon's Blog
- PostgreSQL的“仓库-房间-货架”游戏,你能建出电商数据库和表吗? - cmdragon's Blog
- PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
- 能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
- 给接口加新字段又不搞崩老客户端?FastAPI的多版本API靠哪三招实现? - cmdragon's Blog
- 流量突增要搞崩FastAPI?熔断测试是怎么防系统雪崩的? - cmdragon's Blog
- FastAPI秒杀库存总变负数?Redis分布式锁能帮你守住底线吗 - cmdragon's Blog
- FastAPI的CI流水线怎么自动测端点,还能让Allure报告美到犯规? - cmdragon's Blog
- 如何用GitHub Actions为FastAPI项目打造自动化测试流水线? - cmdragon's Blog
- 如何用Git Hook和CI流水线为FastAPI项目保驾护航? - cmdragon's Blog
- FastAPI如何用契约测试确保API的「菜单」与「菜品」一致?
- 为什么TDD能让你的FastAPI开发飞起来? - cmdragon's Blog
- 如何用FastAPI玩转多模块测试与异步任务,让代码不再“闹脾气”? - cmdragon's Blog
- 如何在FastAPI中玩转“时光倒流”的数据库事务回滚测试?
- 如何在FastAPI中优雅地模拟多模块集成测试? - cmdragon's Blog
- 多环境配置切换机制能否让开发与生产无缝衔接? - cmdragon's Blog
- 如何在 FastAPI 中巧妙覆盖依赖注入并拦截第三方服务调用? - cmdragon's Blog
- 为什么你的单元测试需要Mock数据库才能飞起来? - cmdragon's Blog
- 如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼? - cmdragon's Blog
- 如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼? - cmdragon's Blog
- 测试覆盖率不够高?这些技巧让你的FastAPI测试无懈可击! - cmdragon's Blog
- 为什么你的FastAPI测试覆盖率总是低得让人想哭? - cmdragon's Blog
- 如何让FastAPI测试不再成为你的噩梦? - cmdragon's Blog
- FastAPI测试环境配置的秘诀,你真的掌握了吗? - cmdragon's Blog
- 全链路追踪如何让FastAPI微服务架构的每个请求都无所遁形? - cmdragon's Blog
- 如何在API高并发中玩转资源隔离与限流策略? - cmdragon's Blog
- 任务分片执行模式如何让你的FastAPI性能飙升? - cmdragon's Blog
免费好用的热门在线工具
- Mermaid 在线编辑器 - 应用商店 | By cmdragon
- 数学求解计算器 - 应用商店 | By cmdragon
- 智能提词器 - 应用商店 | By cmdragon
- 魔法简历 - 应用商店 | By cmdragon
- Image Puzzle Tool - 图片拼图工具 | By cmdragon
- 字幕下载工具 - 应用商店 | By cmdragon
- 歌词生成工具 - 应用商店 | By cmdragon
- 网盘资源聚合搜索 - 应用商店 | By cmdragon
- ASCII字符画生成器 - 应用商店 | By cmdragon
- JSON Web Tokens 工具 - 应用商店 | By cmdragon
- Bcrypt 密码工具 - 应用商店 | By cmdragon
- GIF 合成器 - 应用商店 | By cmdragon
- GIF 分解器 - 应用商店 | By cmdragon
- 文本隐写术 - 应用商店 | By cmdragon
- CMDragon 在线工具 - 高级AI工具箱与开发者套件 | 免费好用的在线工具
- 应用商店 - 发现1000+提升效率与开发的AI工具和实用程序 | 免费好用的在线工具
- CMDragon 更新日志 - 最新更新、功能与改进 | 免费好用的在线工具
- 支持我们 - 成为赞助者 | 免费好用的在线工具
- AI文本生成图像 - 应用商店 | 免费好用的在线工具
- 临时邮箱 - 应用商店 | 免费好用的在线工具
- 二维码解析器 - 应用商店 | 免费好用的在线工具
- 文本转思维导图 - 应用商店 | 免费好用的在线工具
- 正则表达式可视化工具 - 应用商店 | 免费好用的在线工具
- 文件隐写工具 - 应用商店 | 免费好用的在线工具
- IPTV 频道探索器 - 应用商店 | 免费好用的在线工具
- 快传 - 应用商店 | 免费好用的在线工具
- 随机抽奖工具 - 应用商店 | 免费好用的在线工具
- 动漫场景查找器 - 应用商店 | 免费好用的在线工具
- 时间工具箱 - 应用商店 | 免费好用的在线工具
- 网速测试 - 应用商店 | 免费好用的在线工具
- AI 智能抠图工具 - 应用商店 | 免费好用的在线工具
- 背景替换工具 - 应用商店 | 免费好用的在线工具
- 艺术二维码生成器 - 应用商店 | 免费好用的在线工具
- Open Graph 元标签生成器 - 应用商店 | 免费好用的在线工具
- 图像对比工具 - 应用商店 | 免费好用的在线工具
- 图片压缩专业版 - 应用商店 | 免费好用的在线工具
- 密码生成器 - 应用商店 | 免费好用的在线工具
- SVG优化器 - 应用商店 | 免费好用的在线工具
- 调色板生成器 - 应用商店 | 免费好用的在线工具
- 在线节拍器 - 应用商店 | 免费好用的在线工具
- IP归属地查询 - 应用商店 | 免费好用的在线工具
- CSS网格布局生成器 - 应用商店 | 免费好用的在线工具
- 邮箱验证工具 - 应用商店 | 免费好用的在线工具
- 书法练习字帖 - 应用商店 | 免费好用的在线工具
- 金融计算器套件 - 应用商店 | 免费好用的在线工具
- 中国亲戚关系计算器 - 应用商店 | 免费好用的在线工具
- Protocol Buffer 工具箱 - 应用商店 | 免费好用的在线工具
- IP归属地查询 - 应用商店 | 免费好用的在线工具
- 图片无损放大 - 应用商店 | 免费好用的在线工具
- 文本比较工具 - 应用商店 | 免费好用的在线工具
- IP批量查询工具 - 应用商店 | 免费好用的在线工具
- 域名查询工具 - 应用商店 | 免费好用的在线工具
- DNS工具箱 - 应用商店 | 免费好用的在线工具
- 网站图标生成器 - 应用商店 | 免费好用的在线工具
- XML Sitemap