4
votes

Tableau croisé dynamique avec plusieurs colonnes de valeurs

J'ai une table Postgres avec des données produits de différents fabricants, voici la structure de table simplifiée:

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

Je dois sortir chaque fabricant pour chaque sku mais s'il y a plusieurs fabricants identiques pour le même sku j'ai besoin de sélectionner le fabricant avec le prix le plus bas (notez que je dois également inclure la colonne 'stock'), ici les résultats souhaités:

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

J'ai essayé d'utiliser 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
    );

Mais cela produit une table avec une seule colonne price . Et je n'ai pas trouvé de moyen d'inclure la colonne stock .

J'ai également essayé d'utiliser l'agrégation conditionnelle:

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

Et cette requête ne fonctionne pas non plus dans mon cas - elle sélectionne simplement le niveau de stock minimum - mais s'il y a peu de fabricants identiques pour le même sku mais avec des prix / stocks différents - cette requête sélectionne le prix minimum d'un fabricant et le stock minimum d'un autre.

Comment puis-je sortir le prix de chaque fabricant et le stock correspondant à partir de ce tableau?

PS Merci à tous pour ces réponses utiles. Ma table Postgres est plutôt petite - il n'y a pas plus de 15k de produits, (je ne sais pas si de tels chiffres peuvent être utiles pour une comparaison correcte) mais depuis qu'Erwin Brandstetter a demandé à comparer les performances de différentes requêtes, j'ai exécuté 3 requêtes avec EXPLAIN ANALYSER , voici leur temps d'exécution:

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

Encore une fois - je ne suis pas sûr que ces nombres puissent être utiles comme référence. Pour mon cas, j'ai choisi la version combinée des requêtes Kjetil S et Gordon Linoff mais les variantes Erwin Brandstetter et a_horse_with_no_name sont également très utiles et intéressants. Il est intéressant de noter que si ma table à l'avenir finissait par avoir plus que peu de fabricants - ajuster la requête et taper leurs noms à chaque fois serait fastidieux - et donc la requête de la réponse a_horse_with_no_name serait la plus pratique à utiliser.


5 commentaires

Votre résultat attendu ne correspond pas à l'exigence " Je dois sélectionner un fabricant avec un" prix "minimum " - vous affichez tous les fabricants sous forme de colonnes, pas celle avec le prix le plus bas.


Oh, je suis désolé - je vais le réparer, dans l'ensemble, je dois sortir chaque fabricant pour chaque sku, mais s'il y a un fabricant en double pour le même sku, je dois sélectionner l'enregistrement avec le prix le plus bas.


Ce sera très compliqué (pour ne pas dire impossible) car cela signifie que le nombre de colonnes peut changer pour chaque SKU par ex. un SKU où tous les fabricants ont le même prix se traduirait par une seule colonne, s'il y en a trois mais deux ont le même prix, alors ce serait deux colonnes et ainsi de suite.


Comment et où utilisez-vous ce résultat? Ce serait beaucoup plus facile si les informations sur le prix / le stock pouvaient être renvoyées par exemple. une seule colonne JSON.


Ce tableau est une version très simplifiée - les résultats finaux doivent inclure non seulement le prix et le stock, mais également quelques autres colonnes. La sortie est ensuite envoyée au framework JS (Ag-Grid qui génère des données sous forme de tableau croisé dynamique). Je pense que la sortie de données sous forme de colonne Json peut fonctionner dans ce cas - pouvez-vous montrer un échantillon?


4 Réponses :


3
votes

Votre dernière sélection fonctionne presque . Mais vous devez ajouter une condition where où les lignes avec des prix non minimaux par sku par fabricant sont supprimées. Cela produit le résultat attendu:

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 commentaires

Merci - cette requête produit exactement ce dont j'ai besoin.



1
votes

Je trouve l'utilisation d'un résultat JSON beaucoup plus facile de nos jours que l'utilisation d'un pivot compliqué. La production d'une seule valeur JSON agrégée ne rompt pas la restriction inhérente à SQL selon laquelle le nombre de colonnes doit être connu avant que la requête ne soit exécutée (et doit être le même pour toutes les lignes).

Vous pouvez utiliser quelque chose comme ceci:

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

Ce qui précède renvoie le résultat suivant en utilisant vos exemples de données:

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;

p>


2 commentaires

J'ai choisi une autre réponse car elle produit une table simple au format requis, mais j'aime vraiment votre suggestion d'utiliser Json pour de telles requêtes.


@Vlad: l'avantage du JSON est que vous n'avez pas besoin d'ajuster votre requête si vous obtenez plus de fabricants.



1
votes

J'utiliserais distinct sur pour limiter les données à un fabricant à un prix. Et j'aime la fonctionnalité filter dans Postgres. Donc:

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;


0 commentaires

0
votes

crosstab () doit fournir un Liste de définitions de colonne statique . Votre deuxième paramètre:

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

... fournit une liste dynamique de valeurs qui nécessiteraient une liste de définition de colonne dynamique . Cela ne fonctionnera pas - sauf par incidence.

Le problème principal de votre tâche est que crosstab () attend une colonne de valeur unique de la requête dans son premier paramètre. Mais vous souhaitez traiter deux colonnes de valeurs par ligne ( price et stock ).

Une façon de contourner ce problème consiste à regrouper plusieurs valeurs dans un type composite et à extraire les valeurs dans le SELECT externe .

Créez un type composite une fois:

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

Une table ou une vue temporaire remplit également cette fonction.
Ensuite:

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

Pour un test rapide, ou si la ligne de votre table sous-jacente n'est pas trop large, vous pouvez également simplement utiliser son type de ligne, sans créer de type personnalisé :

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

db fiddle ici

En relation :


3 commentaires

@Vlad: Si votre table est grande, je serais intéressé de savoir comment cela fonctionne par rapport à la requête de Kjetil. Pouvez-vous exécuter les deux avec EXPLAIN ANALYZE ou avec \ timing dans psql?


Merci @Erwin Brandstetter pour une autre excellente variante avec des explications utiles. Mon tableau est petit mais néanmoins - j'ai ajouté la comparaison des résultats à la question.


Merci d'avoir fourni des résultats. En règle générale, crosstab () est le plus rapide. La surcharge de la formation d'un type composite et de la suppression des valeurs en retour pèse cependant.