Alternativa a la autounión

10

He hecho una pregunta aquí: /programming/43807566/how-to-divide-two-values-from-the-same-column-but-at-different-rows

sobre dividir valores de la misma tabla, en la misma columna pero en diferentes filas. Ahora tengo el problema donde tengo más numeradores y denominadores (con diferentes uns). ¿Sigue siendo la self joinmejor manera de resolver este problema con Postgres o hay mejores soluciones?

Ejemplo:

| postcode | value | uns |
|----------|-------|-----|
|       AA |    40 |  53 |
|       BB |    20 |  53 |
|       AA |    10 |  54 |
|       AA |    20 |  55 |
|       AA |    10 |  56 |
|       AA |    30 |  57 |
|       AA |    50 |  58 |
|       BB |    10 |  54 |
|       BB |    10 |  55 |
|       BB |    70 |  56 |
|       BB |    80 |  57 |
|       BB |    10 |  58 |

El resultado debe ser:

| postcode | formula    |
|----------|------------|
|       AA | 18.888...  |
|       BB | 14.375     |

Donde los valores se agrupan por código postal y la fórmula es (valor con uns):

(V53 * V56 + V54 * V57 + V55 * V58) / (V56 + V57 + V58)

Prestando atención para evitar la eventual división por cero. La fórmula puede ser aún más compleja, pero ese es un buen ejemplo.

Aleatorizar
fuente
¿Hay algún campo en su tabla que indique qué filas son numeradores y denominadores?
McNets
no, el denominador es la suma de valores con uns 56, 57, 58.
Aleatorizar
Parece que la mejor solución sería pivotar los datos para que se unsconviertan en nombres de columna; a partir de ahí, cualquier fórmula que use los valores debería ser viable. ¿La fórmula estará codificada o se derivará dinámicamente de alguna manera?
RDFozz
hay algunas fórmulas (~ 30) que se necesitarán para crear demasiadas tablas
aleatorizar

Respuestas:

3

Este es un problema de pivote / tabla cruzada en su núcleo, como Michael ya diagnosticó con precisión.

Si no está familiarizado con el tablefuncmódulo en Postgres, lea las instrucciones básicas aquí:

La consulta se vuelve simple y muy rápida (más rápida que otras soluciones presentadas aquí):

SELECT (v53 * v56 + v54 * v57 + v55 * v58) / NULLIF(v56 + v57 + v58, 0)
FROM   crosstab(
   'SELECT postcode, uns, value FROM tbl ORDER BY 1'
 , 'SELECT generate_series(53,58)'
   ) AS ct (postcode text
          , v53 numeric, v54 numeric, v55 numeric
          , v56 numeric, v57 numeric, v58 numeric);

NULLIF para evitar la división por cero.

dbfiddle aquí

Erwin Brandstetter
fuente
6

Puede agregar todos los pares uns / value en un objeto JSON, luego usarlo para acceder a los valores UNS por nombre. Esto requiere un poco de conversión ya que los valores solo se pueden extraer como texto del objeto JSON, pero la fórmula se ve muy similar a su descripción entonces:

with vals(postcode, v) as (
  select postcode, json_object_agg(uns, value)
  from x
  group by postcode
), factors (postcode, denominator, divisor) as (
  select postcode, 
         (v->>'53')::decimal * (v->>'56')::decimal + (v->>'54')::decimal * (v->>'57')::decimal + (v->>'55')::decimal * (v->>'58')::decimal,
         (v->>'56')::decimal + (v->>'57')::decimal + (v->>'58')::decimal
  from vals
)
select postcode, 
       denominator / nullif(divisor, 0)
from factors;

He dividido la agregación, la evaluación del denominador y el divisor y la división final en tres pasos para que sea más legible.

Ejemplo en línea: http://rextester.com/IZYT54566


Puede simplificar la fórmula creando una función:

create function val(p_vals json, p_uns text)
  returns decimal
as $$
  select (p_vals ->> p_uns)::decimal;
$$
language sql;

with vals (postcode, v) as (
  select postcode, json_object_agg(uns, value)
  from x
  group by postcode
), factors (postcode, denominator, divisor) as (
  select postcode, 
         val(v, '53') * val(v, '56') + val(v, '54') * val(v, '57') + val(v, '55') * val(v, '58'),
         val(v, '56') + val(v, '57') + val(v, '58')
  from vals
)
select postcode, 
       denominator / nullif(divisor, 0)
from factors;
un caballo sin nombre
fuente
4

El patrón PIVOT funcionaría para esto. Convierte los valores de las filas en columnas en una sola fila, de acuerdo con su clave común. Hay algunas formas de implementar esto. Algunos requieren solo un escaneo de una sola tabla.

Después del PIVOT, tendría una tabla con una fila por código postal y una columna por valor. El resto de la consulta se escribiría como si hiciera referencia a una sola tabla.

Michael Green
fuente
3

Suponiendo que (postcode, uns)son UNIQUE(probablemente, un PK), se puede implementar el patrón PIVOT, como ya comentó @ michael-green, portátil utilizando la siguiente consulta:

SELECT
     postcode, 
     CAST(V53 * V56 + V54 * V57 + V55 * V58 AS numeric) 
         / nullif(V56 + V57 + V58, 0) AS formula
FROM
    (SELECT
         postcode,
         sum(case when uns=53 then value end) AS v53,     
         sum(case when uns=54 then value end) AS v54,     
         sum(case when uns=55 then value end) AS v55,     
         sum(case when uns=56 then value end) AS v56,
         sum(case when uns=57 then value end) AS v57,
         sum(case when uns=58 then value end) AS v58
    FROM
         t
    GROUP BY
         postcode
    ) AS s
ORDER BY
    postcode ;

Compruébelo en SQLFiddle .

joanolo
fuente
3

Suponiendo que (postcode, uns)son UNIQUE(probablemente, un PK), probablemente la forma más simple , probablemente la más portátil, aunque probablemente no sea la óptima: use tantas subselecciones como sea necesario :

SELECT
    postcode,
    ((SELECT value FROM t WHERE t.uns = 53 AND t.postcode = p.postcode) *
     (SELECT value FROM t WHERE t.uns = 56 AND t.postcode = p.postcode) +
     (SELECT value FROM t WHERE t.uns = 54 AND t.postcode = p.postcode) *
     (SELECT value FROM t WHERE t.uns = 57 AND t.postcode = p.postcode) +
     (SELECT value FROM t WHERE t.uns = 55 AND t.postcode = p.postcode) *
     (SELECT value FROM t WHERE t.uns = 58 AND t.postcode = p.postcode)
    )::double precision / 
     nullif( (SELECT sum(value) FROM t 
              WHERE t.uns IN (56, 57, 58) AND t.postcode = p.postcode), 0)
    AS formula
FROM
    (SELECT DISTINCT postcode FROM t) AS p
ORDER BY
    postcode ;

Verifique en SQLFiddle .

joanolo
fuente