group by本身不绘图,仅生成直方图所需分桶数据;需配合width_bucket()等函数实现等宽分桶,并通过left join补全空桶以确保绘图准确。

GROUP BY 本身不画图,但能生成直方图所需的数据桶
SQL 没有内置绘图能力,GROUP BY 只负责把原始数据按区间或类别“分桶”,输出每组的计数(COUNT(*))或聚合值。真正画图得靠外部工具(如 Python 的 matplotlib、Excel、BI 工具),但第一步必须让 SQL 输出结构清晰的频数表。
常见错误是直接 SELECT COUNT(*) FROM table GROUP BY column 却没意识到:如果 column 是连续数值(比如年龄、价格),默认分组会按精确值拆,导致几百个离散桶,根本不是直方图需要的等宽区间。
- 想画年龄分布?不能
GROUP BY age,要先用FLOOR(age / 10) * 10或WIDTH_BUCKET()(PostgreSQL)做区间映射 - MySQL 8.0+ 可用
CEIL(age / 10) * 10 - 9 AS age_range构造 [1–10]、[11–20] 这类左闭右闭区间 - SQLite 没窗口函数和分桶函数,得靠
CASE WHEN手动定义区间,例如:CASE WHEN age BETWEEN 0 AND 10 THEN '0-10' ... END
用 WIDTH_BUCKET()(PostgreSQL)快速生成等宽直方图桶
WIDTH_BUCKET() 是最接近“直方图专用函数”的标准 SQL 扩展,它把连续值线性划分为指定数量的等宽桶,并返回桶编号(从 1 开始)。注意:它不自动处理边界外数据,超出范围的值会归入桶 0 或桶 n+1。
示例:对 price 字段划分 5 个等宽桶(范围 0–1000):
SELECT WIDTH_BUCKET(price, 0, 1000, 5) AS bucket_id, COUNT(*) AS freq FROM products WHERE price IS NOT NULL GROUP BY WIDTH_BUCKET(price, 0, 1000, 5) ORDER BY bucket_id;
结果得到 5 行,bucket_id 为 1–5,对应区间 [0,200)、[200,400)、[400,600)、[600,800)、[800,1000](右边界包含)。容易踩的坑:
- 第三个参数是桶数量,不是桶宽度;宽度由 (max-min)/n 自动算出
- 若
price出现NULL或 1000 的值,WIDTH_BUCKET()返回 0 或 6,需额外HAVING bucket_id BETWEEN 1 AND 5过滤 - 不同数据库语法差异大:Oracle 支持同名函数;SQL Server 无等价函数;MySQL 完全不支持
MySQL 8.0+ 用 CTE + ROW_NUMBER() 模拟分桶(替代 WIDTH_BUCKET)
MySQL 没 WIDTH_BUCKET(),但可用变量或 CTE 计算全局 min/max 后手工缩放。更稳的方式是用 NTILE() 做等频分桶(每桶记录数大致相等),但它不是等宽直方图——适合探索性分析,不适合看真实分布形态。
如果坚持要等宽,推荐两步法:
WITH bounds AS (
SELECT MIN(price) AS lo, MAX(price) AS hi FROM products
),
bucketed AS (
SELECT
FLOOR((price - (SELECT lo FROM bounds)) / ((SELECT hi FROM bounds) - (SELECT lo FROM bounds) + 1) * 5) + 1 AS bucket_id
FROM products
WHERE price IS NOT NULL
)
SELECT bucket_id, COUNT(*) AS freq
FROM bucketed
GROUP BY bucket_id
ORDER BY bucket_id;
关键点:
- 分母加
+1防止除零(当所有 price 相同时) -
FLOOR(... * 5) + 1确保桶号为 1–5,而非 0–4 - 这种写法在大数据集上性能较差,因为子查询被多次执行;生产环境建议先算出 min/max 存为变量,或用应用层预计算
直方图 SQL 输出后,别忘了补全空桶
真实数据常有空缺区间,比如价格分布中 [600,800) 区间没商品,GROUP BY 就不会输出该桶的 0 行。下游绘图时若缺失这一行,柱状图就会错位或漏段。
解决方案:构造一个完整桶序列,再用 LEFT JOIN 补零。以 PostgreSQL 为例:
SELECT b.bucket_id, COALESCE(freq, 0) AS freq FROM ( SELECT generate_series(1, 5) AS bucket_id ) b LEFT JOIN ( SELECT WIDTH_BUCKET(price, 0, 1000, 5) AS bucket_id, COUNT(*) AS freq FROM products WHERE price BETWEEN 0 AND 1000 GROUP BY WIDTH_BUCKET(price, 0, 1000, 5) ) g ON b.bucket_id = g.bucket_id ORDER BY b.bucket_id;
这一步极易被忽略。很多工程师导出 CSV 后直接画图,发现柱子数量不对,回头查才发现是空桶丢失了。补桶逻辑必须在 SQL 层完成,而不是靠 Excel 手动填 0。











