Alternative à l'auto-adhésion

10

J'ai posé une question ici: /programming/43807566/how-to-divide-two-values-from-the-same-column-but-at-different-rows

sur la division des valeurs de la même table, dans la même colonne mais sur des lignes différentes. Maintenant, j'ai le problème où j'ai plus de numérateurs et de dénominateurs (avec différents uns). Est-ce toujours le self joinbon moyen de résoudre ce problème avec Postgres ou existe-t-il de meilleures solutions?

Exemple:

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

Le résultat devrait être:

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

Où les valeurs sont regroupées par code postal et la formule est (valeur avec uns):

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

Faire attention à éviter une éventuelle division par zéro. La formule peut être encore plus complexe mais c'est un bon exemple.

Randomiser
la source
y a-t-il un champ sur votre table qui indique quelles lignes sont des numérateurs et des dénominateurs?
McNets
non, le dénominateur est la somme des valeurs avec uns 56, 57, 58.
Randomiser
Il semble que la meilleure solution serait de faire pivoter les données afin que les unsnoms de colonnes deviennent - à partir de là, quelle que soit la formule qui utilise les valeurs, elle devrait pouvoir fonctionner. La formule sera-t-elle codée en dur ou dérivée dynamiquement d'une manière ou d'une autre?
RDFozz
il y a quelques formules (~ 30) qui prendront pour créer trop de tableaux
Randomiser

Réponses:

3

Il s'agit d'un problème de pivot / tableau croisé à la base, comme Michael a déjà été diagnostiqué avec précision.

Si vous n'êtes pas familier avec le tablefuncmodule dans Postgres, lisez les instructions de base ici:

La requête devient simple et très rapide (plus rapide que les autres solutions présentées ici):

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 pour empêcher la division par zéro.

dbfiddle ici

Erwin Brandstetter
la source
6

Vous pouvez agréger toutes les paires uns / value dans un objet JSON, puis l'utiliser pour accéder aux valeurs UNS par leur nom. Cela nécessite une conversion car les valeurs ne peuvent être extraites que sous forme de texte à partir de l'objet JSON, mais la formule ressemble alors beaucoup à votre description:

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;

J'ai divisé l'agrégation, l'évaluation du dénominateur et du diviseur et la division finale en trois étapes pour le rendre plus lisible.

Exemple en ligne: http://rextester.com/IZYT54566


Vous pouvez simplifier la formule en créant une fonction:

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 cheval sans nom
la source
4

Le modèle PIVOT fonctionnerait pour cela. Il convertit les valeurs des lignes en colonnes sur une seule ligne, selon leur clé commune. Il existe plusieurs façons de mettre cela en œuvre. Certains ne nécessitent qu'une seule analyse de table.

Après le PIVOT, vous auriez un tableau avec une ligne par code postal et une colonne par valeur. Le reste de la requête serait écrit comme s'il faisait référence à une seule table.

Michael Green
la source
3

En supposant qu'il (postcode, uns)s'agit UNIQUE(probablement d'un PK), le modèle PIVOT, comme déjà commenté par @ michael-green, peut être implémenté de manière portable à l'aide de la requête suivante:

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 ;

Vérifiez-le sur SQLFiddle .

joanolo
la source
3

En supposant qu'il (postcode, uns)s'agit UNIQUE(probablement d'un PK), probablement de la manière la plus simple, probablement la plus portable, bien que probablement pas optimale: utilisez autant de sous-sélections que nécessaire :

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 ;

Vérifiez à SQLFiddle .

joanolo
la source