Skip to content

COUNT / SUM / AVG / MAX / MIN ​

count、sum、avg、max、min 是 SQL 中最基础的五类聚合函数,但在真实报表里它们几乎每一处都会踩到 NULL 语义与类型提升的坑:avg 的分母是「非 NULL 计数」而非总行数,全 NULL 的组 sum 返回 NULL 而非 0,sum(int4) 会悄悄变成 bigint 再变成 numeric。本节把这五个函数的语义、NULL 行为、类型规则与条件聚合写法讲透,并给出一次扫描产出多维指标的标准写法。

前置知识

阅读本节前,建议先了解:OVERLAPS 时间重叠

基础概念 ​

聚合函数(Aggregate Function) 把一组输入行归约为单个值。与标量函数「一行进一行出」不同,聚合函数「多行进一行出」,输出行数由 GROUP BY 决定。

概念术语说明
聚合函数Aggregate Function把多行归约为一个值的函数,如 sum / avg
组GroupGROUP BY 划分出的行集合,聚合在组内独立计算
分组键Grouping Key参与分组的列或表达式
计数星号count(*)统计行数,含 NULL 行,是唯一不忽略 NULL 的计数
条件计数count(col)统计该列非 NULL 的个数
去重计数count(DISTINCT col)统计该列不同非 NULL 值的个数
条件聚合Conditional Aggregate用 FILTER 或 CASE 只聚合满足条件的行
类型提升Type Promotionsum(int4) → bigint、sum(bigint) → numeric
忽略 NULLNULL-Ignoring标准聚合函数跳过 NULL,不参与计算
空组结果Empty Group Result全 NULL 组中 sum / avg / max / min 返回 NULL

五者的语义签名与 NULL 行为:

函数返回类型忽略 NULL全 NULL 组返回空表(无 GROUP BY)
count(*)bigint不适用(计所有行)00
count(expr)bigint是00
sum(expr)见下文类型表是NULLNULL
avg(expr)numeric / double precision是NULLNULL
max(expr) / min(expr)输入类型是NULLNULL

一句话记忆:只有 count 返回 0 而不是 NULL。这是「sum 加空组得到 NULL 而不是 0」这一经典陷阱的根源。

语法与用法 ​

sql
-- 聚合函数通用形态
聚合函数(expr)                     -- 普通聚合
聚合函数(expr) FILTER (WHERE 条件) -- 条件聚合(PostgreSQL 9.4+)
聚合函数(DISTINCT expr)            -- 去重聚合(仅 count / sum / avg 支持)

-- 典型查询骨架
SELECT 分组键, count(*) AS 行数, sum(amount) AS 合计
FROM t
WHERE 行级条件
GROUP BY 分组键
HAVING 组级条件
ORDER BY 合计 DESC;

最小可运行示例,覆盖五个函数与 NULL 行为:

sql
SELECT
  count(*)                AS all_rows,     -- 计所有行
  count(city)             AS city_filled,  -- 跳过 NULL
  count(DISTINCT city)    AS city_uniq,    -- 去重后计数
  sum(amount)             AS total_amount,
  round(avg(amount), 2)   AS avg_amount,
  min(amount)             AS min_amount,
  max(amount)             AS max_amount
FROM (VALUES
  ('北京', 100.00),
  ('北京', 200.00),
  ('上海',  50.00),
  (NULL,   150.00)        -- city 为 NULL,amount 有效
) AS t(city, amount);
text
 all_rows | city_filled | city_uniq | total_amount | avg_amount | min_amount | max_amount
----------+-------------+-----------+--------------+------------+------------+------------
        4 |           3 |         2 |       500.00 |     125.00 |      50.00 |     200.00
(1 row)

count(*) 是 4(含 city 为 NULL 的那行),count(city) 是 3——两者之差就是 NULL 行数。

详细说明 ​

count(*) 与 count(col) ​

这是最常用的两个计数,差别只在 NULL:

写法统计对象NULL 行是否计入
count(*)全部行计入
count(1) / count(0)全部行(常量非 NULL)计入
count(col)col IS NOT NULL 的行不计入
count(DISTINCT col)col 的非 NULL 不同值不计入
sql
-- 三者在同一份数据上的差异
SELECT
  count(*)              AS star,
  count(1)              AS one,
  count(city)           AS col,
  count(DISTINCT city)  AS dist,
  count(*) - count(city) AS null_rows
FROM (VALUES ('北京'), ('北京'), ('上海'), (NULL), (NULL)) AS t(city);
text
 star | one | col | dist | null_rows
------+-----+-----+------+-----------
    5 |   5 |   3 |    2 |         2
(1 row)

count(1) 与 count(*) 语义与计划完全相同(优化器都展开为 count(*) 节点),但 count(1) 会让读者猜测用意,推荐一律写 count(*)。

count(*) 在大表上必须全扫,代价是 O(行数)。PostgreSQL 的 MVCC 让「行数」不便于增量维护,因此没有 MySQL InnoDB 那种缓存计数。近似值可以从统计信息拿:

sql
-- 精确计数:需要全扫
SELECT count(*) FROM orders;

-- 近似计数:读 pg_class 的统计估算,O(1),误差通常 < 5%
SELECT reltuples::BIGINT AS approx_rows
FROM pg_class
WHERE oid = 'orders'::REGCLASS;

需要精确总数时也有技巧:用窗口函数一次查出「数据 + 总数」:

sql
-- count(*) OVER () 在过滤后的结果集上算总数,与明细同时返回
SELECT id, order_no, count(*) OVER () AS total_rows
FROM orders
ORDER BY id
LIMIT 20;

avg 的分母是「非 NULL 计数」 ​

这是统计类报表最经典的陷阱。avg(x) 的等价定义是:

text
avg(x) ≡ sum(x) / count(x)     -- 注意分母是 count(x),不是 count(*)
写法分母语义
avg(amount)count(amount) 即非 NULL 行数只对填了金额的行求平均
sum(amount) / count(*)总行数把 NULL 当作缺失分母,结果偏小
sum(amount) / nullif(count(amount), 0)非 NULL 行数与 avg 等价,且能防除零
sql
-- 5 行数据,其中 2 行 amount 为 NULL
SELECT
  count(*)                                          AS total_rows,
  count(amount)                                     AS amount_rows,
  avg(amount)                                       AS avg_ignores_null,
  sum(amount) / count(*)                            AS avg_includes_null,
  sum(amount) / nullif(count(amount), 0)            AS avg_explicit
FROM (VALUES
  (100.00), (200.00), (300.00), (NULL), (NULL)
) AS t(amount);
text
 total_rows | amount_rows | avg_ignores_null | avg_includes_null | avg_explicit
------------+-------------+------------------+-------------------+--------------
          5 |           3 |           200.00 |            120.00 |       200.00
(1 row)

avg(amount) 得 200.00,而 sum/count(*) 得 120.00。当业务口径是「所有订单的平均客单价」时,要先决定 NULL 金额是否算作 0——这是需求层面必须问清的问题,不是 SQL 能替你决定的。

加权平均不能直接用 avg,必须手写:

sql
-- 错误!avg(price) 是算术平均,不等于按销量加权的平均成交价
SELECT avg(unit_price) AS wrong_weighted
FROM order_items;

-- 正确:加权平均 = sum(单价 × 数量) / sum(数量)
SELECT sum(unit_price * qty) / nullif(sum(qty), 0) AS weighted_avg_price
FROM order_items;

sum 的全 NULL 返回 NULL 与类型提升 ​

sum 有两个必须记住的行为。

行为一:全 NULL 或空组返回 NULL,不是 0。

sql
-- 空集与全 NULL 的 sum 都是 NULL
SELECT
  sum(amount)                    AS raw_sum,        -- NULL
  coalesce(sum(amount), 0)       AS safe_sum        -- 0
FROM (VALUES (NULL::NUMERIC), (NULL::NUMERIC)) AS t(amount);
text
 raw_sum | safe_sum
---------+----------
         |     0.00
(1 row)

报表里凡是「没有数据时应该显示 0」的位置,都必须包 coalesce(sum(x), 0)。左侧 /、avg 等对 NULL 的处理也一致。

行为二:类型提升,且不会溢出。

输入类型sum 返回类型说明
smallint / integerbigint提升一级,避免 32 位溢出
bigintnumeric提升为 numeric,避免 64 位溢出
numericnumeric保持
real / double precisiondouble precision浮点累加,有精度损失
moneymoney—
intervalinterval间隔累加
sql
SELECT
  pg_typeof(sum(x_int))    AS sum_int4,     -- bigint
  pg_typeof(sum(x_bigint)) AS sum_int8,     -- numeric
  pg_typeof(sum(x_num))    AS sum_numeric,  -- numeric
  pg_typeof(sum(x_float))  AS sum_float8    -- double precision
FROM (VALUES (1, 1::BIGINT, 1::NUMERIC, 1::DOUBLE PRECISION)) AS t(x_int, x_bigint, x_num, x_float);
text
  sum_int4  | sum_int8 | sum_numeric | sum_float8
------------+----------+-------------+------------
 bigint     | numeric  | numeric     | double precision
(1 row)

结论:整数求和不会溢出,因为 PostgreSQL 自动升为 bigint / numeric。但 double precision 求和仍会有浮点误差,金融场景务必用 numeric。

max / min 的比较规则与索引优化 ​

max / min 对文本按排序规则(Collation) 比较,不是按字节:

sql
SELECT
  max(name) AS max_name,
  min(name) AS min_name
FROM (VALUES ('apple'), ('Banana'), ('cherry'), ('_under')) AS t(name);
text
 max_name | min_name
----------+----------
 _under   | apple
(1 row)

apple < Banana 是因为 en_US.UTF-8 排序规则忽略大小写差异(a 在 B 之前),而下划线 _ 的排序权重最低。需要字节序比较时显式指定 COLLATE "C":

sql
SELECT max(name COLLATE "C") AS byte_max FROM (VALUES ('apple'), ('Banana')) AS t(name);
text
 byte_max
----------
 apple
(1 row)

SELECT max(id) FROM t 可以走 B-tree 索引,只读索引最右端一个元组,代价 O(log n):

sql
CREATE INDEX idx_orders_id ON orders (id);
EXPLAIN (COSTS OFF) SELECT max(id) FROM orders;
text
              QUERY PLAN
--------------------------------------
 Result
   InitPlan 1 (returns $0)
     ->  Limit
           ->  Index Only Scan Backward using idx_orders_id on orders
                 Index Cond: (id IS NOT NULL)
(5 rows)

对比 count(*) 必须走 Aggregate 全扫——这解释了为什么「取最新一条」用 ORDER BY id DESC LIMIT 1 很快,而「看总数」很慢。

两种分组物理计划:HashAggregate 与 GroupAggregate ​

带 GROUP BY 的聚合有两种物理实现,EXPLAIN 会直接写出来,读计划时必须能分辨:

计划节点是否要求输入有序内存特征适合场景
HashAggregate否,先扫一遍建哈希表分组数多时占内存,超 work_mem 会落盘分组数适中(几千到几十万)
GroupAggregate是,输入须按分组键有序逐组流式累加,内存占用小分组数极大,或分组键上已有索引
sql
-- 分组数少:优化器通常选 HashAggregate
EXPLAIN (COSTS OFF) SELECT city, count(*) FROM orders GROUP BY city;
text
             QUERY PLAN
------------------------------------
 HashAggregate
   Group Key: city
   ->  Seq Scan on orders
(3 rows)
sql
-- 分组键上建了索引后,可能改走 GroupAggregate,省掉显式排序
CREATE INDEX idx_orders_city ON orders (city);
EXPLAIN (COSTS OFF) SELECT city, count(*) FROM orders GROUP BY city;
text
                    QUERY PLAN
---------------------------------------------------
 GroupAggregate
   Group Key: city
   ->  Index Only Scan using idx_orders_city on orders
(3 rows)

若分组键上没有索引,GroupAggregate 前面会多出一个 Sort 节点——此时它比 HashAggregate 更贵,优化器多半不会选它。反过来,当分组数极大(例如按 user_id 分组上百万组)时,HashAggregate 的哈希表可能撑爆 work_mem,日志里出现 Sort Method: external merge Disk 或哈希批次落盘,这时用索引喂给 GroupAggregate 反而更稳。

HashAggregate 还支持并行:计划里会出现 Partial HashAggregate + Finalize HashAggregate 两级,各 worker 先局部聚合,再由领导者合并。

聚合与 WHERE / GROUP BY / HAVING 的执行顺序 ​

顺序子句作用对象能否引用聚合
1FROM / JOIN原始行否
2WHERE原始行否
3GROUP BY原始行否
4聚合函数每组—
5HAVING分组能
6SELECT结果行能
7ORDER BY结果行能
sql
-- 错误!WHERE 中不能出现聚合函数
SELECT city, count(*) FROM orders WHERE count(*) > 1 GROUP BY city;
text
ERROR:  aggregate functions are not allowed in WHERE
LINE 1: SELECT city, count(*) FROM orders WHERE count(*) > 1 GROUP BY...

需要在分组前过滤行,把条件放进 WHERE;需要过滤分组,放进 HAVING。条件能前移就前移——先在 WHERE 减少输入行,分组代价随之下降。

FILTER 做条件聚合 ​

FILTER (WHERE cond) 让单个聚合只处理满足条件的行,是「一次扫描产出多列指标」的核心手段(PostgreSQL 9.4+):

sql
-- 一次扫描拿到多个维度的统计
SELECT
  count(*)                                    AS total_cnt,
  count(*) FILTER (WHERE status = 'paid')     AS paid_cnt,
  sum(amount) FILTER (WHERE status = 'paid')  AS paid_amount,
  count(*) FILTER (WHERE status = 'refunded') AS refunded_cnt
FROM orders;

旧版本可用 CASE 模拟,但必须补 ELSE 兜底,否则 sum 会因 NULL 得出错误结果:

sql
-- 等价但更啰嗦,且必须写 ELSE 0
SELECT
  count(CASE WHEN status = 'paid' THEN 1 END)          AS paid_cnt,
  sum(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS paid_amount
FROM orders;

聚合函数不能嵌套 ​

sum(count(*)) 这类写法直接报错,因为聚合的输入必须是原始行:

sql
-- 错误!aggregate function calls cannot be nested
SELECT sum(count(*)) FROM orders GROUP BY city;
text
ERROR:  aggregate function calls cannot be nested
LINE 1: SELECT sum(count(*)) FROM orders GROUP BY city;

需要「对聚合结果再聚合」时,先分层:用子查询或 CTE 把内层聚合固化,再在外层聚合。

sql
-- 正确:先按城市算订单数,再对外层求平均
WITH city_cnt AS (
  SELECT city, count(*) AS cnt FROM orders GROUP BY city
)
SELECT round(avg(cnt), 2) AS avg_orders_per_city, sum(cnt) AS total_orders
FROM city_cnt;

实战示例 ​

场景:电商订单看板。要求一条 SQL 从同一张表产出行数、去重用户数、GMV、平均客单价、最大最小金额,并在城市维度上分组。

第 1 步:建表并造数 ​

sql
DROP TABLE IF EXISTS orders;

CREATE TABLE orders (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_no    TEXT           NOT NULL UNIQUE,
  user_name   TEXT           NOT NULL,
  city        TEXT,                       -- 允许 NULL:未填写城市
  amount      NUMERIC(12, 2),             -- 允许 NULL:待定价订单
  status      TEXT           NOT NULL CHECK (status IN ('paid', 'pending', 'canceled', 'refunded')),
  created_at  TIMESTAMPTZ    NOT NULL DEFAULT now()
);

INSERT INTO orders (order_no, user_name, city, amount, status, created_at) VALUES
  ('SO-1001', 'alice', '北京', 1200.00, 'paid',     TIMESTAMPTZ '2026-01-05 10:00:00+08'),
  ('SO-1002', 'bob',   '上海',  350.50, 'paid',     TIMESTAMPTZ '2026-01-06 11:30:00+08'),
  ('SO-1003', 'alice', '北京',  880.00, 'pending',  TIMESTAMPTZ '2026-01-07 09:15:00+08'),
  ('SO-1004', 'carol', '广州', 1200.00, 'paid',     TIMESTAMPTZ '2026-01-08 14:20:00+08'),
  ('SO-1005', 'bob',   '上海',  220.00, 'canceled', TIMESTAMPTZ '2026-01-09 16:45:00+08'),
  ('SO-1006', 'dave',  '北京',  450.00, 'refunded', TIMESTAMPTZ '2026-01-10 08:05:00+08'),
  ('SO-1007', 'carol', '广州', 1200.00, 'paid',     TIMESTAMPTZ '2026-01-11 12:40:00+08'),
  ('SO-1008', 'eve',   '深圳',  760.00, 'paid',     TIMESTAMPTZ '2026-01-12 15:10:00+08'),
  ('SO-1009', 'frank', NULL,    NULL,   'pending',  TIMESTAMPTZ '2026-01-13 10:00:00+08');
text
INSERT 0 9

注意第 9 行:city 与 amount 都是 NULL,用来演示 NULL 对各个聚合的影响。

第 2 步:整体指标(含 NULL 陷阱对照) ​

sql
SELECT
  count(*)                                  AS 订单数,        -- 9,含 NULL 行
  count(city)                               AS 已填城市数,    -- 8
  count(amount)                             AS 已定价数,      -- 8
  count(DISTINCT user_name)                 AS 下单用户数,    -- 6
  sum(amount)                               AS 已定价总额,
  coalesce(sum(amount), 0)                  AS 总额_安全,     -- NULL 兜底
  round(avg(amount), 2)                     AS 平均额_非空,
  round(sum(amount) / nullif(count(*), 0), 2) AS 平均额_含空
FROM orders;
text
 订单数 | 已填城市数 | 已定价数 | 下单用户数 | 已定价总额 | 总额_安全 | 平均额_非空 | 平均额_含空
--------+------------+----------+------------+------------+-----------+-------------+-------------
      9 |          8 |        8 |          6 |    6260.50 |   6260.50 |      782.56 |      695.61
(1 row)

平均额_非空 = 6260.50 / 8 = 782.56,平均额_含空 = 6260.50 / 9 = 695.61。同一份数据,两种口径差 87 元——报表口径必须先统一。

第 3 步:按城市分组 + 条件聚合 ​

sql
SELECT
  coalesce(city, '【未填写】')                     AS 城市,
  count(*)                                         AS 订单数,
  count(DISTINCT user_name)                        AS 用户数,
  coalesce(sum(amount), 0)                         AS 总额,
  round(avg(amount), 2)                            AS 平均额,
  min(amount)                                      AS 最小额,
  max(amount)                                      AS 最大额,
  count(*) FILTER (WHERE status = 'paid')          AS 已付款数,
  coalesce(sum(amount) FILTER (WHERE status = 'paid'), 0) AS 已付款额
FROM orders
GROUP BY city
ORDER BY 总额 DESC;
text
    城市    | 订单数 | 用户数 |  总额  | 平均额 | 最小额 | 最大额 | 已付款数 | 已付款额
------------+--------+--------+--------+--------+--------+--------+----------+----------
 北京       |      3 |      2 |2530.00 | 843.33 | 450.00 |1200.00 |        1 |  1200.00
 广州       |      2 |      1 |2400.00 |1200.00 |1200.00 |1200.00 |        2 |  2400.00
 深圳       |      1 |      1 | 760.00 | 760.00 | 760.00 | 760.00 |        1 |   760.00
 上海       |      2 |      1 | 570.50 | 285.25 | 220.00 | 350.50 |        1 |   350.50
 【未填写】 |      1 |      1 |   0.00 |        |        |        |        0 |     0.00
(5 rows)

看最后一行:金额 为 NULL 的那组,sum 经 coalesce 变 0.00,而 avg / min / max 全为空白(NULL)。这直观地展示了「只有 count 与兜底后的 sum 能安全显示」。

第 4 步:验证 count(*) 与 count(col) 的差异来源 ​

sql
SELECT
  count(*)                      AS 全部行,
  count(city)                   AS 有城市,
  count(amount)                 AS 有金额,
  count(*) - count(city)        AS 城市为空行,
  count(*) - count(amount)      AS 金额为空行
FROM orders;
text
 全部行 | 有城市 | 有金额 | 城市为空行 | 金额为空行
--------+--------+--------+------------+------------
      9 |      8 |      8 |          1 |          1

第 5 步:用 EXPLAIN 看近似计数与精确计数的差异 ​

sql
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM orders;
text
              QUERY PLAN
---------------------------------------
 Aggregate (actual time=0.045..0.045 rows=1 loops=1)
   ->  Seq Scan on orders (actual time=0.010..0.014 rows=9 loops=1)
 Planning Time: 0.090 ms
 Execution Time: 0.070 ms

表越大,Seq Scan 的代价越明显——这正是需要精确总数时应改用「维护计数表」而非实时 count(*) 的原因。

第 6 步:清理 ​

sql
DROP TABLE IF EXISTS orders;

注意事项 ​

  • avg 的分母是 count(expr) 而非 count(*)。NULL 多时平均值会偏高,务必与业务确认口径:
sql
-- 这两种写法结果不同,必须显式决定用哪一种
SELECT avg(amount), sum(amount) / count(*) FROM orders;
  • 全 NULL 的组 sum 返回 NULL,不是 0。用于减法、除法、比较前必须兜底:
sql
-- 错误!没有数据时得到 NULL,前端显示为空白
SELECT sum(amount) FROM orders WHERE status = 'no_such_status';

-- 推荐:显式兜底
SELECT coalesce(sum(amount), 0) FROM orders WHERE status = 'no_such_status';
  • count(1) 与 count(*) 等价但不推荐:语义相同、计划相同,但会让阅读者误以为有特殊意图。

  • count(col) 忽略 NULL,count(*) 不忽略。若用 count(col) 判断「是否有数据」,NULL 行会被漏计。

  • 聚合函数不能出现在 WHERE 中,只能用 HAVING 或外层子查询。

  • 聚合函数不能嵌套,sum(count(*)) 直接报错,必须分层用 CTE 或子查询。

  • max / min 对文本按排序规则比较,跨语言环境结果可能与预期不符。需要确定性字节序时加 COLLATE "C"。

  • sum(double precision) 有浮点误差,金融金额一律用 numeric:

sql
-- 不推荐:浮点累加
SELECT sum(amount::DOUBLE PRECISION) FROM orders;

-- 推荐:十进制精确求和
SELECT sum(amount) FROM orders;
  • count(DISTINCT ...) 在大表上代价高(需排序或哈希去重),且不支持 FILTER 之外的多列去重语法糖(多列需 count(DISTINCT (a, b)) 行构造器写法)。

  • 聚合结果无顺序保证,报表必须显式 ORDER BY。

最佳实践 ​

  • 报表里凡是「求和」都包 coalesce,这是唯一能保证「无数据时显示 0」的做法:
sql
SELECT city, coalesce(sum(amount), 0) AS total
FROM orders
GROUP BY city
ORDER BY total DESC;
  • avg 与「总额 / 单数」二选一时,先确认分母。若业务口径是「所有订单」,用 sum(amount) / nullif(count(*), 0);若是「有效金额订单」,直接用 avg(amount) 并写注释说明。

  • 多维指标用 FILTER 一条 SQL 完成,避免同一张表被扫多遍:

sql
SELECT
  count(*)                                              AS 订单数,
  count(*) FILTER (WHERE status = 'paid')               AS 已付款,
  sum(amount) FILTER (WHERE status = 'paid')            AS 已付款额,
  count(*) FILTER (WHERE status = 'refunded')           AS 已退款,
  round(avg(amount) FILTER (WHERE status = 'paid'), 2)  AS 平均客单价
FROM orders;
  • 精确总数用计数表或物化视图维护,不要在每次写入时对全表 count(*) 现算。千万级表上的 count(*) 需要走索引全扫,高并发下会成为热点:
sql
-- 用触发器或应用逻辑维护计数表
CREATE TABLE order_stats (
  stat_date date PRIMARY KEY,
  order_cnt bigint NOT NULL DEFAULT 0
);

-- 报表优先读计数表
SELECT coalesce(sum(order_cnt), 0) AS 累计订单数 FROM order_stats;
  • max / min 走索引即是最优解。若查询形如 SELECT max(created_at) FROM orders WHERE user_id = ?,建 (user_id, created_at) 复合索引可让优化器直接读索引末端,无需扫描整组:
sql
CREATE INDEX idx_orders_user_created ON orders (user_id, created_at DESC);
  • count(DISTINCT ...) 尽量换成 GROUP BY 子查询或 EXISTS。当只需要「有多少个不同值」且业务可接受近似时,pg_statistic 或 HLL 扩展比精确去重便宜得多;必须精确时,把它拆到独立子查询避免与其它聚合争抢排序资源。

  • 给聚合结果起英文或中文别名时保持全库一致。本项目统一使用中文列别名(如 AS 订单数),便于报表层直接映射;混用中英文会让下游字段名难以对齐。

  • 用 EXPLAIN (ANALYZE, BUFFERS) 验证「一次扫描」是否成立。若执行计划出现多次 Seq Scan,说明优化器没把多个聚合合并,应改写为 FILTER 形式或检查统计信息是否过期(ANALYZE orders)。

下一节

继续学习:STRING_AGG / ARRAY_AGG / JSONB_AGG

参考链接 ​