width_bucket()用于等宽分桶,返回值所在桶编号(从1开始),边界值归入下一桶;动态分桶可用数组+unnest()+generate_series()构建区间,需确保边界升序无重复。

用 width_bucket() 实现固定宽度分桶
width_bucket() 是 PostgreSQL 原生支持的分桶函数,适合等宽区间(如每10岁为一档、每100元为一档)。它返回输入值落在第几个桶中,桶编号从 1 开始,超出范围则返回 0 或 n+1。
常见错误是忽略边界行为:当值等于右边界时,会被划入下一桶。例如 width_bucket(10, 0, 10, 5) 表示将 [0,10) 分成 5 等份,每份宽 2,那么值为 10 会返回 6(溢出),不是 5。
- 语法:
width_bucket(value, low_bound, high_bound, bucket_count),注意low_bound≤valuehigh_bound 才有效 - 若需包含右边界,可把
high_bound往上提一点(如high_bound + 0.001),或改用case when显式定义 - 聚合时通常配合
group by和min()/max()还原区间:min(value), max(value), count(*)
SELECT width_bucket(age, 0, 100, 5) AS bucket_id, CONCAT((bucket_id-1)*20, '-', bucket_id*20, '岁') AS age_range, COUNT(*) FROM users GROUP BY bucket_id ORDER BY bucket_id;
用 case when 实现任意自定义区间(非等宽)
当分桶逻辑不规则(比如 0–18、19–35、36–59、60+)时,width_bucket() 失效,必须手写 case when。这是最常用也最容易出错的方式——漏写 else 导致 NULL 桶,或区间重叠/留空。
关键点在于明确每个区间的开闭性,并统一处理边界值。推荐全部用左闭右开(>= AND ),最后一档用 <code>>= 收尾。
- 避免用
BETWEEN,它默认闭区间,容易和相邻桶重复(如BETWEEN 19 AND 35和BETWEEN 35 AND 59会让 35 落入两桶 - 所有分支必须覆盖全集,加
ELSE 'unknown'捕获异常数据 - 别在
case里写聚合函数;先算桶标签,再group by标签字段
SELECT
CASE
WHEN age >= 0 AND age < 18 THEN '0-17'
WHEN age >= 18 AND age < 36 THEN '18-35'
WHEN age >= 36 AND age < 60 THEN '36-59'
WHEN age >= 60 THEN '60+'
ELSE 'invalid'
END AS age_group,
COUNT(*)
FROM users
GROUP BY age_group;
用数组 + unnest() + generate_series() 动态建桶边界
当桶配置来自外部(如配置表、JSON 参数),硬编码 case when 不可持续。此时可把边界存成数组,用 unnest() 展开,再用 lag() 或自连接生成区间对。
典型陷阱是数组未排序或含重复值,导致区间倒置或为零宽;还有忘记 OFFSET 1 对齐前后边界。
- 边界数组建议升序存储,如
ARRAY[0,18,36,60,120] - 用
generate_series(1, array_length(boundaries,1)-1)控制桶数量 - 用
boundaries[i]和boundaries[i+1]构造每档上下界
WITH buckets AS (
SELECT i, boundaries[i] AS low, boundaries[i+1] AS high
FROM (SELECT ARRAY[0,18,36,60,120] AS boundaries) t,
generate_series(1, array_length(t.boundaries,1)-1) AS i
)
SELECT
CONCAT(b.low, '-', b.high-1) AS range,
COUNT(u.*)
FROM buckets b
LEFT JOIN users u ON u.age >= b.low AND u.age < b.high
GROUP BY b.low, b.high;
性能与 NULL 处理的隐性坑
分桶查询慢,往往不是因为逻辑复杂,而是没走索引或 NULL 值干扰了分组。尤其当 age 字段有大量 NULL 时,case when 默认不匹配任何分支,全进 ELSE 桶——但如果你忘了写 ELSE,它们就变成 NULL 组,而 GROUP BY NULL 会把所有 NULL 合并成一行,掩盖数据质量问题。
- 对分桶字段建索引能加速
WHERE过滤,但对GROUP BY case_when_expr无直接帮助 - 若桶数少且固定,考虑用部分索引(如
CREATE INDEX idx_users_age_0_18 ON users(age) WHERE age >= 0 AND age ) - 显式过滤或补值:
WHERE age IS NOT NULL,或用COALESCE(age, -1)并在case中单独处理
自定义分桶真正难的不是写法,而是边界定义是否反映业务语义、NULL 是否被有意归类、以及桶配置能否随业务变化快速调整——这些都比 SQL 本身更值得花时间设计。

















