Альтернатива Self Join

10

Я задал вопрос здесь: /programming/43807566/how-to-divide-two-values-from-the-same-column-but-at-different-rows

о делении значений из одной и той же таблицы, в одном столбце, но в разных строках. Теперь у меня есть проблема, где у меня есть больше числителей и знаменателей (с разными uns). Все еще self joinхороший способ решить эту проблему с Postgres или есть лучшие решения?

Пример:

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

Результат должен быть:

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

Где значение сгруппировано по почтовому индексу, а формула (значение с uns):

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

Обратите внимание, чтобы избежать возможного деления на ноль. Формула может быть еще более сложной, но это хороший пример.

Randomize
источник
есть ли в вашей таблице какое-либо поле, обозначающее, какие строки являются числителями и знаменателями?
McNets
нет, знаменатель - это сумма значений с uns 56, 57, 58.
Рандомизировать
Похоже, лучшим решением было бы повернуть данные так, чтобы unsимена столбцов стали - оттуда, любая формула, использующая значения, должна стать работоспособной. Будет ли формула жестко закодирована или как-то получена динамически?
RDFozz
Есть несколько формул (~ 30), которые потребуются для создания слишком большого количества таблиц
Randomize

Ответы:

3

По сути, это проблема сводной / кросс-таблицы , как Майкл уже точно диагностировал .

Если вы не знакомы с tablefuncмодулем в Postgres, прочтите основные инструкции здесь:

Запрос становится простым и очень быстрым (быстрее, чем другие решения, представленные здесь):

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 предотвратить деление на ноль.

dbfiddle здесь

Эрвин Брандштеттер
источник
6

Вы можете объединить все пары uns / value в объект JSON, а затем использовать его для доступа к значениям UNS по имени. Это требует некоторого приведения, поскольку значения могут быть извлечены только как текст из объекта JSON, но тогда формула выглядит очень похоже на ваше описание:

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;

Я разделил агрегацию, оценку знаменателя и делителя и окончательное деление на три этапа, чтобы сделать его более читабельным.

Пример в сети: http://rextester.com/IZYT54566


Вы можете упростить формулу, создав функцию:

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;
a_horse_with_no_name
источник
4

Модель PIVOT будет работать для этого. Он преобразует значения строк в столбцы в одной строке в соответствии с их общим ключом. Есть несколько способов реализовать это. Некоторые требуют только одного сканирования таблицы.

После PIVOT у вас будет таблица с одной строкой на почтовый индекс и столбцом на значение. Оставшаяся часть запроса будет записана так, как будто она ссылается на одну таблицу.

Майкл Грин
источник
3

Предполагая, что (postcode, uns)это UNIQUE(возможно, PK), шаблон PIVOT, как уже прокомментировал @ michael-green, может быть реализован переносимо с использованием следующего запроса:

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 ;

Проверьте это в SQLFiddle .

joanolo
источник
3

Предполагая, что (postcode, uns)это UNIQUE(возможно, PK), возможно, самый простой способ, возможно, самый переносимый, хотя, вероятно, не оптимальный: используйте столько подвыборов, сколько необходимо :

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 ;

Проверьте в SQLFiddle .

joanolo
источник