Сводная таблица с несколькими столбцами значений - PullRequest
4 голосов
/ 22 марта 2019

У меня есть таблица Postgres с данными о товарах разных производителей, здесь упрощенная структура таблицы:

CREATE TABLE test_table (
  sku               text,
  manufacturer_name text,
  price             double precision,
  stock             int
);

INSERT INTO test_table
VALUES ('sku1', 'Manufacturer1', 110.00, 22),
       ('sku1', 'Manufacturer2', 120.00, 15),
       ('sku1', 'Manufacturer3', 130.00, 1),
       ('sku1', 'Manufacturer3', 30.00, 11),
       ('sku2', 'Manufacturer1', 10.00, 2),
       ('sku2', 'Manufacturer2', 9.00,  3),
       ('sku3', 'Manufacturer2', 21.00, 3),
       ('sku3', 'Manufacturer2', 1.00, 7),
       ('sku3', 'Manufacturer3', 19.00, 5);

Мне нужно вывести каждого производителя для каждого артикула, но если есть несколько идентичных производителей для одного артикулаМне нужно выбрать производителя с самой низкой ценой (обратите внимание, что мне также нужно включить столбец «сток»), здесь желаемые результаты:

| sku  | man1_price | man1_stock | man2_price | man2_stock | man3_price | man3_stock |
|------|------------|------------|------------|------------|------------|------------|
| sku1 | 110.0      | 22         | 120.0      | 15         | 30.0       | 11         |
| sku2 | 10.0       | 2          | 9.0        | 3          |            |            |
| sku3 |            |            | 1.0        | 7          | 19.0       | 5          |

Я пытался использовать Postgres crosstab():

SELECT *
FROM crosstab('SELECT sku, manufacturer_name, price
              FROM test_table
              ORDER BY 1,2',
              $$ SELECT DISTINCT manufacturer_name FROM test_table ORDER BY 1 $$
       )
       AS ct (sku text, "man1_price" double precision,
              "man2_price" double precision,
              "man3_price" double precision
    );

Но при этом создается таблица только с одним столбцом price.И я не нашел способа включить столбец stock.

Я также попытался использовать условное агрегирование:

SELECT sku,
   MIN(CASE WHEN manufacturer_name = 'Manufacturer1' THEN price END) as man1_price,
   MIN(CASE WHEN manufacturer_name = 'Manufacturer1' THEN stock END) as man1_stock,
   MIN(CASE WHEN manufacturer_name = 'Manufacturer2' THEN price END) as man2_price,
   MIN(CASE WHEN manufacturer_name = 'Manufacturer2' THEN stock END) as man2_stock,
   MIN(CASE WHEN manufacturer_name = 'Manufacturer3' THEN price END) as man3_price,
   MIN(CASE WHEN manufacturer_name = 'Manufacturer3' THEN stock END) as man3_stock
FROM test_table
GROUP BY sku
ORDER BY sku

И этот запрос также не работает в моем случае- он просто выбирает минимальный уровень запаса - но если есть несколько одинаковых производителей для одного и того же артикула, но с разными ценами / запасами - этот запрос выбирает минимальную цену от одного производителя и минимальный запас от другого.

Как я могуВывести price каждого производителя и соответствующие stock из этой таблицы?

PS Спасибо всем за такие полезные ответы.Моя таблица Postgres довольно мала - там не более 15 тыс. Продуктов (я не знаю, могут ли такие числа быть полезными для правильного сравнения), но поскольку Эрвин Брандштеттер попросил сравнить производительность разных запросов, я выполнил 3 запроса с EXPLAIN ANALYZE,вот их время выполнения:

Erwin Brandstetter query:        400 - 450 ms 
Kjetil S query:                  250 - 300 ms
Gordon Linoff query:             200 - 250 ms
a_horse_with_no_name query:      250 - 300 ms

Опять же - я не уверен, что эти цифры могут быть полезны в качестве ссылки.Для моего случая я выбрал комбинированную версию запросов Kjetil S и Gordon Linoff, но варианты Erwin Brandstetter и a_horse_with_no_name также очень полезны и интересны.Стоит отметить, что если в будущем в моей таблице будет больше, чем несколько производителей - корректировать запрос и вводить их имена каждый раз будет утомительно - и, следовательно, запрос из ответа a_horse_with_no_name будет наиболее удобным для использования.

Ответы [ 4 ]

2 голосов
/ 22 марта 2019

Ваш последний выбор почти работает.Но вы должны добавить условие «где», где удаляются строки с минимальными ценами на единицу товара для одного производителя.Это дает ожидаемый результат:

select
  sku,
  min( case when manufacturer_name='Manufacturer1' then price end ) man1_price,
  min( case when manufacturer_name='Manufacturer1' then stock end ) man1_stock,
  min( case when manufacturer_name='Manufacturer2' then price end ) man2_price,
  min( case when manufacturer_name='Manufacturer2' then stock end ) man2_stock,
  min( case when manufacturer_name='Manufacturer3' then price end ) man3_price,
  min( case when manufacturer_name='Manufacturer3' then stock end ) man3_stock
from test_table t
where not exists (
    select 1 from test_table
    where sku=t.sku
    and manufacturer_name=t.manufacturer_name
    and price<t.price
)
group by sku
order by 1;
1 голос
/ 22 марта 2019

Я бы использовал distinct on, чтобы ограничить данные для одного производителя одной ценой. И мне нравится filter функциональность в Postgres. Итак:

select sku,
       max(price) filter (where manufacturer_name = 'Manufacturer1') as man1_price,
       max(stock) filter (where manufacturer_name = 'Manufacturer1') as man1_stock,
       max(price) filter (where manufacturer_name = 'Manufacturer2') as man2_price,
       max(stock) filter (where manufacturer_name = 'Manufacturer2') as man2_stock,
       max(price) filter (where manufacturer_name = 'Manufacturer3') as man3_price,
       max(stock) filter (where manufacturer_name = 'Manufacturer3') as man3_stock
from (select distinct on (manufacturer_name, sku) t.*
      from test_table t
      order by manufacturer_name, sku, price
     ) t
group by sku
order by sku;
1 голос
/ 22 марта 2019

Я считаю, что использовать результаты в формате JSON в наши дни намного проще, чем использовать сложные точки.Создание единого агрегированного значения JSON не нарушает присущее SQL ограничение, заключающееся в том, что число столбцов должно быть известно до выполнения запроса (и должно быть одинаковым для всех строк).

Вы можете использовать что-то вроде этого:

select sku, 
       jsonb_object_agg(manufacturer_name, 
                          jsonb_build_object('price', price, 'stock', stock, 'isMinPrice', price = min_price)) as price_info
from (
  select sku, 
         manufacturer_name,
         price, 
         min(price) over (partition by sku) as min_price,
         stock
  from test_table
) t
group by sku;

Вышеприведенное возвращает следующий результат, используя ваши данные образца:

sku  | price_info                                                                                                                                                                                             
-----+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
sku1 | {"Manufacturer1": {"price": 110, "stock": 22, "isMinPrice": false}, "Manufacturer2": {"price": 120, "stock": 15, "isMinPrice": false}, "Manufacturer3": {"price": 30, "stock": 11, "isMinPrice": true}}
sku2 | {"Manufacturer1": {"price": 10, "stock": 2, "isMinPrice": false}, "Manufacturer2": {"price": 9, "stock": 3, "isMinPrice": true}}                                                                       
sku3 | {"Manufacturer2": {"price": 1, "stock": 7, "isMinPrice": true}, "Manufacturer3": {"price": 19, "stock": 5, "isMinPrice": false}}                                                                       
0 голосов
/ 22 марта 2019

crosstab() должен предоставить статический список определений столбцов .Ваш второй параметр:

$$ SELECT DISTINCT manufacturer_name FROM test_table ORDER BY 1 $$

... предоставляет список значений динамический , для которого потребуется список определений столбцов динамический .Это не сработает, за исключением случаев.

Основная проблема вашей задачи заключается в том, что crosstab() ожидает от столбца значения single из запроса в его первом параметре.Но вы хотите обработать два столбца значений на строку (price и stock).

Одним из способов решения этой проблемы является упаковка нескольких значений в составной тип и извлечение значений во внешнем SELECT.

Создание составного типа один раз:

CREATE TYPE price_stock AS (price float8, stock int);

Временная таблица или представление также служит цели.
Тогда:

SELECT sku
     , (man1).price, (man1).stock
     , (man2).price, (man2).stock
     , (man3).price, (man3).stock
FROM   crosstab(
   'SELECT sku, manufacturer_name, (price, stock)::price_stock
    FROM   test_table
    ORDER  BY 1,2'
  , $$VALUES ('Manufacturer1'),('Manufacturer2'),('Manufacturer3')$$
    )
       AS ct (sku text
            , man1 price_stock
            , man2 price_stock
            , man3 price_stock
    );

Для быстрого теста или если строка вашей базовой таблицы не слишком широка, вы также можете просто использовать ее тип строки, не создавая пользовательский тип:

SELECT sku
     , (man1).price, (man1).stock
     , (man2).price, (man2).stock
     , (man3).price, (man3).stock
FROM   crosstab(
   'SELECT sku, manufacturer_name, t
    FROM   test_table t
    ORDER  BY 1,2'
  , $$VALUES ('Manufacturer1'),('Manufacturer2'),('Manufacturer3')$$
    )
       AS ct (sku text
            , man1 test_table
            , man2 test_table
            , man3 test_table
    );

db <> fiddle здесь

Связанные:

...