sqlpostgresqlwindow-functionsanalytics

WIDTH_BUCKET en SQL: histogramas de ancho fijo y distribuciones

Como WIDTH_BUCKET reparte valores en intervalos de ancho fijo, gestiona los bordes y construye distribuciones con GROUP BY.

3 min de lecturaReferencesql · postgresql · window-functions · analytics · histogram

WIDTH_BUCKET responde a una pregunta sencilla: en que tramo del histograma cae un numero si dividimos un rango en partes de igual anchura. Es la herramienta ideal cuando necesitas agrupar un valor continuo (importe de un pedido, salario, puntuacion) en cubos uniformes y construir una distribucion.

Firma y logica

La llamada canonica en PostgreSQL recibe cuatro argumentos:

SELECT WIDTH_BUCKET(score, 0, 100, 10) AS bucket;  -- bucket 1..10

Esto significa: toma el rango de 0 a 100, cortalo en 10 partes iguales de anchura 10 cada una y devuelve el numero de tramo para score. Un valor en 0..10 cae en el cubo 1, 10..20 en el cubo 2, etcetera. El limite inferior se incluye y el superior se excluye.

La caracteristica clave son los dos cubos extra:

  • si el valor es menor que el limite inferior, obtienes 0 (underflow);
  • si el valor es igual o mayor que el limite superior, obtienes n + 1 (overflow).

Asi nunca pierdes filas en silencio: todo lo que se sale del rango se apila ordenadamente en los bordes.

Distribuciones con GROUP BY

El uso mas habitual es un histograma de importes de pedido. Reparte amount de 0 a 1000 en 10 cubos y cuenta cada uno:

SELECT
  WIDTH_BUCKET(amount, 0, 1000, 10) AS bucket,
  COUNT(*) AS orders,
  ROUND(MIN(amount), 2) AS lo,
  ROUND(MAX(amount), 2) AS hi
FROM orders
WHERE status = 'paid'
GROUP BY bucket
ORDER BY bucket;

El cubo 0 recoge devoluciones e importes nulos, y el cubo 11 recoge los pedidos enormes por encima de 1000. Para convertir el numero en una etiqueta legible, calcula los limites dentro de la consulta:

SELECT
  bucket,
  (bucket - 1) * 100 AS range_lo,
  bucket * 100       AS range_hi,
  COUNT(*)           AS orders
FROM (
  SELECT WIDTH_BUCKET(amount, 0, 1000, 10) AS bucket
  FROM orders
  WHERE status = 'paid'
) t
GROUP BY bucket
ORDER BY bucket;

El mismo patron sirve para salarios: reparte employees.salary en bandas y veras al instante en que franjas se concentra la plantilla.

SELECT
  WIDTH_BUCKET(salary, 30000, 150000, 6) AS band,
  dept,
  COUNT(*) AS headcount
FROM employees
GROUP BY band, dept
ORDER BY band, dept;

WIDTH_BUCKET frente a NTILE

Estas dos funciones se confunden con facilidad, pero dividen los datos en ejes distintos:

  • WIDTH_BUCKET corta el eje de valores en segmentos de igual anchura. Los cubos pueden quedar vacios o desbordados, y eso esta bien, porque la forma de la distribucion es justo lo que quieres ver.
  • NTILE(n) corta el conjunto de filas en n grupos de tamano parecido (cuantiles). La anchura de cada intervalo varia, pero cada grupo contiene casi la misma cantidad de filas.

Usa WIDTH_BUCKET cuando importan los umbrales fijos ("pedidos 0-100, 100-200..."). Usa NTILE cuando necesitas percentiles ("el 10% de clientes que mas gasta"):

SELECT
  user_id,
  SUM(amount) AS lifetime,
  NTILE(4) OVER (ORDER BY SUM(amount)) AS quartile
FROM orders
GROUP BY user_id;

Trampa: limites y NULL

Algunos escollos en los que es facil tropezar:

  • El limite superior se excluye. Exactamente WIDTH_BUCKET(100, 0, 100, 10) devuelve 11, no 10. Para meter el maximo en el ultimo cubo real, amplia el limite superior o usa LEAST(amount, 999.99).
  • Una entrada NULL produce una salida NULL, asi que esas filas desaparecen de la distribucion. Filtralas de forma explicita o intagralas con COALESCE.
  • Puedes pasar los limites al reves (superior < inferior), lo que invierte la numeracion y hace que los cubos cuenten hacia atras. Rara vez es lo que buscas, asi que revisa bien el orden.

Diferencias entre motores

  • PostgreSQL y Oracle admiten directamente la forma de cuatro argumentos de WIDTH_BUCKET.
  • MySQL no tiene WIDTH_BUCKET. Emulalo con aritmetica: FLOOR((amount - 0) / 100) + 1, y gestiona el underflow y el overflow a mano.
  • ClickHouse ofrece roundDown(amount, [0, 100, 200, ...]) y el simple floor(amount / 100) para el mismo efecto.

La regla de oro: fija min, max y el numero de cubos como umbrales de negocio deliberados en lugar de ajustarlos a los datos actuales, o dos informes vecinos dejaran de ser comparables.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador