sql >> Base de Datos >  >> RDS >> PostgreSQL

Alternativa dinámica al pivote con CASE y GROUP BY

Si no ha instalado el módulo adicional tablefunc , ejecute este comando una vez por base de datos:

CREATE EXTENSION tablefunc;

Respuesta a la pregunta

Una solución de tabulación cruzada muy básica para su caso:

SELECT * FROM crosstab(
  'SELECT bar, 1 AS cat, feh
   FROM   tbl_org
   ORDER  BY bar, feh')
 AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

La dificultad especial aquí está, que no hay categoría (cat ) en la tabla base. Para la forma básica de 1 parámetro simplemente podemos proporcionar una columna ficticia con un valor ficticio que sirva como categoría. El valor se ignora de todos modos.

Este es uno de los casos raros donde el segundo parámetro para la crosstab() la función no es necesaria , porque todo NULL los valores solo aparecen en columnas colgantes a la derecha por definición de este problema. Y el orden puede ser determinado por el valor .

Si tuviéramos una categoría real columna con nombres que determinan el orden de los valores en el resultado, necesitaríamos el formulario de 2 parámetros de crosstab() . Aquí sintetizo una columna de categoría con la ayuda de la función de ventana row_number() , para basar crosstab() en:

SELECT * FROM crosstab(
   $$
   SELECT bar, val, feh
   FROM  (
      SELECT *, 'val' || row_number() OVER (PARTITION BY bar ORDER BY feh) AS val
      FROM tbl_org
      ) x
   ORDER BY 1, 2
   $$
 , $$VALUES ('val1'), ('val2'), ('val3')$$         -- more columns?
) AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

El resto es bastante común y corriente. Encuentre más explicaciones y enlaces en estas respuestas estrechamente relacionadas.

Conceptos básicos:
Lea esto primero si no está familiarizado con crosstab() función!

  • Consulta de tabulación cruzada de PostgreSQL

Avanzado:

  • Pivote en múltiples columnas usando Tablefunc
  • Fusionar una tabla y un registro de cambios en una vista en PostgreSQL

Configuración adecuada de la prueba

Así es como debe proporcionar un caso de prueba para comenzar:

CREATE TEMP TABLE tbl_org (id int, feh int, bar text);
INSERT INTO tbl_org (id, feh, bar) VALUES
   (1, 10, 'A')
 , (2, 20, 'A')
 , (3,  3, 'B')
 , (4,  4, 'B')
 , (5,  5, 'C')
 , (6,  6, 'D')
 , (7,  7, 'D')
 , (8,  8, 'D');

¿Tabla cruzada dinámica?

No muy dinámico , sin embargo, como comentó @Clodoaldo. Los tipos de retorno dinámicos son difíciles de lograr con plpgsql. Pero hay hay formas de evitarlo - con algunas limitaciones .

Entonces, para no complicar más el resto, lo demuestro con un más simple caso de prueba:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);

Llamar:

SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2')
AS ct (row_name text, val1 int, val2 int, val3 int);

Devoluciones:

 row_name | val1 | val2 | val3
----------+------+------+------
 A        | 10   | 20   |
 B        |  3   |  4   |
 C        |  5   |      |
 D        |  6   |  7   |  8

Característica integrada de tablefunc módulo

El módulo tablefunc proporciona una infraestructura simple para crosstab() genéricos llamadas sin proporcionar una lista de definición de columna. Varias funciones escritas en C (normalmente muy rápido):

crosstabN()

crosstab1() - crosstab4() están predefinidos. Un punto menor:requieren y devuelven todo el text . Así que necesitamos convertir nuestro integer valores. Pero simplifica la llamada:

SELECT * FROM crosstab4('SELECT row_name, attrib, val::text  -- cast!
                         FROM tbl ORDER BY 1,2')

Resultado:

 row_name | category_1 | category_2 | category_3 | category_4
----------+------------+------------+------------+------------
 A        | 10         | 20         |            |
 B        | 3          | 4          |            |
 C        | 5          |            |            |
 D        | 6          | 7          | 8          |

Personalizar crosstab() función

Para más columnas o otros tipos de datos , creamos nuestro propio tipo compuesto y función (una vez).
Escriba:

CREATE TYPE tablefunc_crosstab_int_5 AS (
  row_name text, val1 int, val2 int, val3 int, val4 int, val5 int);

Función:

CREATE OR REPLACE FUNCTION crosstab_int_5(text)
  RETURNS SETOF tablefunc_crosstab_int_5
AS '$libdir/tablefunc', 'crosstab' LANGUAGE c STABLE STRICT;

Llamar:

SELECT * FROM crosstab_int_5('SELECT row_name, attrib, val   -- no cast!
                              FROM tbl ORDER BY 1,2');

Resultado:

 row_name | val1 | val2 | val3 | val4 | val5
----------+------+------+------+------+------
 A        |   10 |   20 |      |      |
 B        |    3 |    4 |      |      |
 C        |    5 |      |      |      |
 D        |    6 |    7 |    8 |      |

Uno función polimórfica y dinámica para todos

Esto va más allá de lo que cubre tablefunc módulo.
Para hacer que el tipo de retorno sea dinámico, uso un tipo polimórfico con una técnica detallada en esta respuesta relacionada:

  • Refactorice una función PL/pgSQL para devolver el resultado de varias consultas SELECT

Forma de 1 parámetro:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L) t(%s)'
                , _qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

Sobrecarga con esta variante para el formulario de 2 parámetros:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _cat_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L, %L) t(%s)'
                , _qry, _cat_qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

pg_typeof(_rowtype)::text::regclass :hay un tipo de fila definido para cada tipo compuesto definido por el usuario, de modo que los atributos (columnas) se enumeran en el catálogo del sistema pg_attribute . La vía rápida para obtenerlo:emita el tipo registrado (regtype ) a text y lanza este text a regclass .

Crear tipos compuestos una vez:

Debe definir una vez cada tipo de retorno que va a utilizar:

CREATE TYPE tablefunc_crosstab_int_3 AS (
    row_name text, val1 int, val2 int, val3 int);

CREATE TYPE tablefunc_crosstab_int_4 AS (
    row_name text, val1 int, val2 int, val3 int, val4 int);

...

Para llamadas ad-hoc, también puede simplemente crear una tabla temporal al mismo efecto (temporal):

CREATE TEMP TABLE temp_xtype7 AS (
    row_name text, x1 int, x2 int, x3 int, x4 int, x5 int, x6 int, x7 int);

O use el tipo de una tabla existente, vista o vista materializada si está disponible.

Llamar

Usando los tipos de fila anteriores:

Forma de 1 parámetro (sin valores perdidos):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1,2'
 , NULL::tablefunc_crosstab_int_3);

Forma de 2 parámetros (pueden faltar algunos valores):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1'
 , $$VALUES ('val1'), ('val2'), ('val3')$$
 , NULL::tablefunc_crosstab_int_3);

Esta una función funciona para todos los tipos de devolución, mientras que crosstabN() marco proporcionado por tablefunc módulo necesita una función separada para cada uno.
Si ha nombrado sus tipos en secuencia como se muestra arriba, solo tiene que reemplazar el número en negrita. Para encontrar el número máximo de categorías en la tabla base:

SELECT max(count(*)) OVER () FROM tbl  -- returns 3
GROUP  BY row_name
LIMIT  1;

Eso es lo más dinámico posible si desea columnas individuales . Matrices como las demostradas por @Clocoaldo o una representación de texto simple o el resultado envuelto en un tipo de documento como json o hstore puede funcionar para cualquier número de categorías dinámicamente.

Descargo de responsabilidad:
Siempre es potencialmente peligroso cuando la entrada del usuario se convierte en código. Asegúrese de que esto no se pueda usar para la inyección de SQL. No acepte entradas de usuarios que no sean de confianza (directamente).

Llamada para pregunta original:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);