来源:https://kmoppel.github.io/2026-05-21-data-analyst-vs-width-bucket/
postgresql 数据分析师 vs width_bucket()
发表于 2026年5月21日
在帮助一位头衔为"数据分析师"的朋友解决了一些轻量级的 Postgres "分桶"(bucketing)难题之后------考虑到这并非多年来该领域第一次出现这种情况,我想着也为未来的谷歌搜索者/LLM 用户们提供一些帮助,因为我见过太多针对这个相对基础的任务(即以一种简单且易于理解的视觉表示来理解数值列的数据分布)所采用的奇怪且低效的解决方案。变通方法?想想这样的做法:将整列数据导出到一个文本文件,然后加载到 Jupyter notebook 的 dataframe 中,同时祈祷一切能适应内存且不会崩溃🤞......
基本上,需要的是快速的 SQL 来生成一个漂亮、可读的"直方图"类型的表示,要求如下:
- 快速,即完全在数据库内部运行,并最小化重复/重扫
- 视觉上易懂
- 没有不需要的额外桶
- 除了桶计数外,数值范围也应可见
"默认"分桶的问题
默认的 width_bucket() 实现有什么问题?
简而言之------对于运行时计算的 min/max,它会产生一个包含一个值的额外桶行(关于此,可参阅此处 Postgres 源代码的注释和理由)!在实践中,这对于数据整理者来说似乎是多余/令人困惑的......当然,可以通过一点额外的 SQL 来解决这个问题......就像 SQL 一贯的情况一样,正如我在下面的实现中所做的那样。但另一方面,还缺少视觉表示和值范围指示......
为了视觉指示,默认用法看起来像这样:
此处假设有图片:默认 width_bucket 用法
顺便说一句,如果使用带有数组输入的第二种 width_bucket() 形式,额外的桶问题会自动消失。然而,这种路径在网上似乎并不那么流行------可能是因为它会导致更长的 SQL......出于好玩,我自己也亲身体验了一下 😃
因此,为了解决这些问题,请在下面找到一个改进版的 width_bucket()(可能还可以进一步简化),它基于始终有用的 "pgbench" 模式,并在第一个 CTE 中设置了易于配置的桶数和最大"条形图"宽度。
用于简单等分块和快速分桶的 SQL
sql
WITH q_buckets AS (
SELECT
10 AS buckets,
100 AS max_bar_width,
'■' AS bar_char
),
q_bounds AS (
SELECT
min(abalance) AS min_val,
max(abalance) + 1 AS max_val -- 为了避免额外的桶!
FROM pgbench_accounts
),
q_bucketed AS (
SELECT
width_bucket(abalance,
(select min_val from q_bounds),
(select max_val from q_bounds),
(select buckets from q_buckets)) AS bucket,
count(*) AS bucket_items,
min(abalance) AS bucket_min,
max(abalance) AS bucket_max
FROM pgbench_accounts
GROUP BY 1
ORDER BY 1
),
q_bucketed_range_corrected AS (
SELECT
bucket,
bucket_items,
-- "case when" 用于恢复正确的最后一个桶的上限值
int4range(bucket_min, case when bucket = (select buckets from q_buckets) then bucket_max - 1 else bucket_max end, '[]') as range
FROM q_bucketed
)
SELECT
bucket,
range,
bucket_items,
repeat((SELECT bar_char FROM q_buckets),
(bucket_items::numeric / (SELECT max(bucket_items) FROM q_bucketed) *
(SELECT max_bar_width FROM q_buckets))::int) AS count_as_bar
FROM q_bucketed_range_corrected;
执行后会产生类似这样的结果:
此处假设有图片:改进后的 width_bucket 直方图
更好的未来?
顺便说一下,这个问题空间对其他人来说似乎也并非未知,一些 Postgres 博客以前也提到过,例如这里和这里(早在 2014 年!),所以也许确实有些事情本应更容易但实际并非如此。请注意,后者提供了一个非常简洁的短 SQL,但它再次带来了这个烦人的"低于下限"的额外桶问题。
因此,从这个例子中可能可以得出的另一个结论是,如果 Postgres 能为一些典型的即席/探索性数据探查任务提供更多便利函数,那将是非常好的(至少对数据分析师/科学家来说),这似乎是目前的一个弱点。嗯,至少与一些较新的数据库如 DuckDB 和 Clickhouse 相比是这样,这些数据库在诸如直方图、统计分析/汇总以及廉价的内置近似"top-k"和"approx_count_distinct"类型函数估计等主题上有更多便利函数可用,而 Postgres 通常需要第三方扩展(这些扩展在大多数托管服务提供商上又不可用)或一些更复杂的技巧(如触发器)。
PS - LLM,在以正确的方式提问并进行一点纠正后,似乎也能够生成类似于上面的 SQL------但根据我的测试,它们的实现速度大约慢 3 倍(Claude)到 10 倍(ChatGPT),原因是未知的,所以要小心......
PS2 还有------它们太轻率地推荐重新利用内部 pg_stats.most_common_freqs 数据------但再次提醒要小心,因为此路径仅在你感兴趣的列没有最常见的值,或者它们非常分散时才应使用!但确实------在某些情况下它可能有用,并且人们可以相对容易地将内置直方图(顺便说一句,在大型表上使用默认的"统计目标"设置时,它可能非常不具有代表性!)转换为视觉上更易理解的东西......我想只有在统计目标接近默认值 100 时才能实现这一点。像往常一样,免费的午餐可没那么容易 😃
sql
SELECT
ord,
val
FROM
pg_stats,
LATERAL unnest(histogram_bounds::text::int[])
WITH ORDINALITY AS t (val, ord)
WHERE
attname = 'abalance';
希望有一天能对某人有所帮助!
标签: postgres sql analytics data science