Alternativa à auto-junção

10

Eu fiz uma pergunta aqui: /programming/43807566/how-to-divide-two-values-from-the-same-column-but-at-different-rows

sobre como dividir valores da mesma tabela, na mesma coluna, mas em linhas diferentes. Agora eu tenho o problema em que tenho mais numeradores e denominadores (com diferentes uns). Ainda é self joinuma boa maneira de resolver esse problema com o Postgres ou existem soluções melhores?

Exemplo:

| 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 |

O resultado deve ser:

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

Onde o valor é agrupado por código postal e a fórmula é (valor com uns):

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

Preste atenção para evitar eventual divisão por zero. A fórmula pode ser ainda mais complexa, mas esse é um bom exemplo.

Aleatória
fonte
existe algum campo em sua tabela que sinalize quais linhas são numeradores e denominadores?
McNets 5/05
não, denominador é a soma dos valores com uns 56, 57, 58.
Randomize
Parece que a melhor solução seria dinamizar os dados para que os unsnomes das colunas se tornem - a partir daí, qualquer fórmula que use os valores se tornará viável. A fórmula será codificada ou derivada dinamicamente de alguma forma?
RDFozz
existem algumas fórmulas (~ 30) que serão necessárias para criar muitas tabelas #
Randomize

Respostas:

3

Este é um problema de pivô / crosstab em sua essência, como Michael já diagnosticou com precisão.

Se você não está familiarizado com o tablefuncmódulo no Postgres, leia as instruções básicas aqui:

A consulta se torna simples e muito rápida (mais rápida que outras soluções apresentadas aqui):

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 impedir a divisão por zero.

dbfiddle aqui

Erwin Brandstetter
fonte
6

Você pode agregar todos os pares uns / value em um objeto JSON e usá-lo para acessar os valores UNS por nome. Isso requer alguma conversão, já que os valores só podem ser extraídos como texto do objeto JSON, mas a fórmula é muito semelhante à sua descrição:

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;

Dividi a agregação, a avaliação do denominador e divisor e a divisão final em três etapas para torná-lo mais legível.

Exemplo online: http://rextester.com/IZYT54566


Você pode simplificar a fórmula criando uma função:

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;
um cavalo sem nome
fonte
4

O padrão PIVOT funcionaria para isso. Ele converte os valores das linhas em colunas em uma única linha, de acordo com sua chave comum. Existem algumas maneiras de implementar isso. Alguns exigem apenas uma única verificação de tabela.

Após o PIVOT, você teria uma tabela com uma linha por código postal e uma coluna por valor. O restante da consulta seria gravado como se referenciasse uma única tabela.

Michael Green
fonte
3

Supondo que (postcode, uns)sejam UNIQUE(provavelmente, um PK), o padrão PIVOT, como já comentado por @ michael-green, pode ser implementado de forma portável, usando a seguinte 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 ;

Verifique no SQLFiddle .

joanolo
fonte
3

Supondo que (postcode, uns)seja UNIQUE(provavelmente, um PK), provavelmente o caminho mais simples , provavelmente o mais portátil, embora provavelmente não seja o ideal: use o número de sub-seleções necessário :

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 no SQLFiddle .

joanolo
fonte