Grouping & aggregation

Bucket values into a histogram

3 min read

A histogram is just a GROUP BY on a bucketed version of the value. The portable form works in every dialect — divide by the bucket width, FLOOR it:

-- portable everywhere: fixed-width buckets of 10, starting at 0
SELECT
  FLOOR(amount / 10) * 10 AS bucket_start,
  COUNT(*) AS n
FROM orders
GROUP BY FLOOR(amount / 10)
ORDER BY bucket_start;

PostgreSQL, DuckDB and Snowflake have a dedicated WIDTH_BUCKET function that also handles the bucket count directly:

-- WIDTH_BUCKET(value, min, max, bucket_count) — 10 equal-width buckets between 0 and 100
SELECT
  WIDTH_BUCKET(amount, 0, 100, 10) AS bucket,
  COUNT(*) AS n
FROM orders
GROUP BY bucket
ORDER BY bucket;

Dialect notes:

For uneven bucket widths, a portable CASE beats both of the above:

SELECT
  CASE
    WHEN amount < 25  THEN '0-24'
    WHEN amount < 50  THEN '25-49'
    WHEN amount < 100 THEN '50-99'
    ELSE '100+'
  END AS bucket,
  COUNT(*) AS n
FROM orders
GROUP BY 1
ORDER BY MIN(amount);

Sort text buckets by MIN(amount), not the label. '100+' sorts before '25-49' alphabetically — ordering by the bucket’s actual minimum value keeps the histogram in numeric order regardless of how the labels read. GROUP BY 1 (the ordinal position) works in all six dialects, including BigQuery.

Open the full version (with copy buttons) ↗

← All recipes