web-dev-qa-db-fra.com

Lignes de transposition SQL en colonnes

J'ai une énigme intéressante qui, je crois, peut être résolue en purement SQL. J'ai des tableaux similaires aux suivants:

responses:

user_id | question_id | body
----------------------------
1       | 1           | Yes
2       | 1           | Yes
1       | 2           | Yes
2       | 2           | No
1       | 3           | No
2       | 3           | No


questions:

id | body
-------------------------
1 | Do you like apples?
2 | Do you like oranges?
3 | Do you like carrots?

et je voudrais obtenir la sortie suivante

user_id | Do you like apples? | Do you like oranges? | Do you like carrots?
---------------------------------------------------------------------------
1       | Yes                 | Yes                  | No
2       | Yes                 | No                   | No

Je ne sais pas combien de questions il y aura et elles seront dynamiques, donc je ne peux pas simplement coder pour chaque question. J'utilise PostgreSQL et je crois que cela s'appelle la transposition, mais je n'arrive pas à trouver quoi que ce soit qui dise la façon standard de le faire en SQL. Je me souviens avoir fait cela dans ma classe de base de données au collège, mais c'était dans MySQL et je ne me souviens vraiment pas comment nous l'avons fait.

Je suppose que ce sera une combinaison de jointures et d'un GROUP BY, mais je ne sais même pas par où commencer.

Quelqu'un sait comment faire ça? Merci beaucoup!

Édition 1: J'ai trouvé des informations sur l'utilisation d'un tableau croisé qui semble être ce que je veux, mais j'ai des problèmes donner un sens à cela. Des liens vers de meilleurs articles seraient grandement appréciés!

37
Topher Fangio

Utilisation:

  SELECT r.user_id,
         MAX(CASE WHEN r.question_id = 1 THEN r.body ELSE NULL END) AS "Do you like apples?",
         MAX(CASE WHEN r.question_id = 2 THEN r.body ELSE NULL END) AS "Do you like oranges?",
         MAX(CASE WHEN r.question_id = 3 THEN r.body ELSE NULL END) AS "Do you like carrots?"
    FROM RESPONSES r
    JOIN QUESTIONS q ON q.id = r.question_id
GROUP BY r.user_id

Il s'agit d'une requête pivot standard, car vous "faites pivoter" les données des lignes vers les données en colonnes.

48
OMG Ponies

J'ai implémenté une fonction vraiment dynamique pour gérer ce problème sans avoir à coder en dur une classe spécifique de réponses ou à utiliser des modules/extensions externes. Il donne également un contrôle total sur l'ordre des colonnes et prend en charge plusieurs colonnes de clé et de classe/attribut.

Vous pouvez le trouver ici: https://github.com/jumpstarter-io/colpivot

Exemple qui résout ce problème particulier:

begin;

create temporary table responses (
    user_id integer,
    question_id integer,
    body text
) on commit drop;

create temporary table questions (
    id integer,
    body text
) on commit drop;

insert into responses values (1,1,'Yes'), (2,1,'Yes'), (1,2,'Yes'), (2,2,'No'), (1,3,'No'), (2,3,'No');
insert into questions values (1, 'Do you like apples?'), (2, 'Do you like oranges?'), (3, 'Do you like carrots?');

select colpivot('_output', $$
    select r.user_id, q.body q, r.body a from responses r
        join questions q on q.id = r.question_id
$$, array['user_id'], array['q'], '#.a', null);

select * from _output;

rollback;

Cela produit:

 user_id | 'Do you like apples?' | 'Do you like carrots?' | 'Do you like oranges?' 
---------+-----------------------+------------------------+------------------------
       1 | Yes                   | No                     | Yes
       2 | Yes                   | No                     | No
12
Hannes Landeholm

Vous pouvez résoudre cet exemple avec la fonction tableau croisé de cette manière

drop table if exists responses;
create table responses (
user_id integer,
question_id integer,
body text
);

drop table if exists questions;
create table questions (
id integer,
body text
);

insert into responses values (1,1,'Yes'), (2,1,'Yes'), (1,2,'Yes'), (2,2,'No'), (1,3,'No'), (2,3,'No');
insert into questions values (1, 'Do you like apples?'), (2, 'Do you like oranges?'), (3, 'Do you like carrots?');

select * from crosstab('select responses.user_id, questions.body, responses.body from responses, questions where questions.id = responses.question_id order by user_id') as ct(userid integer, "Do you like apples?" text, "Do you like oranges?" text, "Do you like carrots?" text);

Tout d'abord, vous devez installer l'extension tablefunc. Depuis la version 9.1, vous pouvez le faire en utilisant l'extension create:

CREATE EXTENSION tablefunc;
6
Francisco Puga

J'ai écrit une fonction pour générer la requête dynamique. Il génère le sql pour le tableau croisé et crée une vue (le supprime d'abord s'il existe). Vous pouvez alors sélectionner dans la vue pour obtenir vos résultats.

Voici la fonction:

CREATE OR REPLACE FUNCTION public.c_crosstab (
  eavsql_inarg varchar,
  resview varchar,
  rowid varchar,
  colid varchar,
  val varchar,
  agr varchar
)
RETURNS void AS
$body$
DECLARE
    casesql varchar;
    dynsql varchar;    
    r record;
BEGIN   
 dynsql='';

 for r in 
      select * from pg_views where lower(viewname) = lower(resview)
  loop
      execute 'DROP VIEW ' || resview;
  end loop;   

 casesql='SELECT DISTINCT ' || colid || ' AS v from (' || eavsql_inarg || ') eav ORDER BY ' || colid;
 FOR r IN EXECUTE casesql Loop
    dynsql = dynsql || ', ' || agr || '(CASE WHEN ' || colid || '=''' || r.v || ''' THEN ' || val || ' ELSE NULL END) AS ' || agr || '_' || r.v;
 END LOOP;
 dynsql = 'CREATE VIEW ' || resview || ' AS SELECT ' || rowid || dynsql || ' from (' || eavsql_inarg || ') eav GROUP BY ' || rowid;
 RAISE NOTICE 'dynsql %1', dynsql; 
 EXECUTE dynsql;
END

$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;

Et voici comment je l'utilise:

SELECT c_crosstab('query_txt', 'view_name', 'entity_column_name', 'attribute_column_name', 'value_column_name', 'first');

Exemple: le poing que vous exécutez:

SELECT c_crosstab('Select * from table', 'ct_view', 'usr_id', 'question_id', 'response_value', 'first');

Que:

Select * from ct_view;
2
SunWuKung

Il y a un exemple de ceci dans contrib/tablefunc/.

0
Peter Eisentraut