PostgreSQL Crosstab Query

196

¿Alguien sabe cómo crear consultas de tabla cruzada en PostgreSQL?
Por ejemplo tengo la siguiente tabla:

Section    Status    Count
A          Active    1
A          Inactive  2
B          Active    4
B          Inactive  5

Me gustaría que la consulta devuelva la siguiente tabla de referencias cruzadas:

Section    Active    Inactive
A          1         2
B          4         5

es posible?

Schone
fuente
1
Tenía una estructura ligeramente diferente y encontré este ejemplo un poco difícil de entender, así que documenté mi forma de pensar sobre este stackoverflow.com/q/49051959/808723 . Quizás sea útil para cualquiera.
GameScripting

Respuestas:

317

Instale el módulo adicional tablefunc una vez por base de datos, que proporciona la función crosstab(). Desde Postgres 9.1 puedes usar CREATE EXTENSIONpara eso:

CREATE EXTENSION IF NOT EXISTS tablefunc;

Caso de prueba mejorado

CREATE TABLE tbl (
   section   text
 , status    text
 , ct        integer  -- "count" is a reserved word in standard SQL
);

INSERT INTO tbl VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                    , ('C', 'Inactive', 7);  -- ('C', 'Active') is missing

Forma simple: no se ajusta a los atributos faltantes

crosstab(text)con 1 parámetro de entrada:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- needs to be "ORDER BY 1,2" here
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

Devoluciones:

Sección | Activo | Inactivo
--------- + -------- + ----------
 A | 1 | 2
 B | 4 | 5 5
 C |      7 | - !!
  • No hay necesidad de fundición y cambio de nombre.
  • Tenga en cuenta lo incorrecto resultado para C: el valor 7se completa para la primera columna. A veces, este comportamiento es deseable, pero no para este caso de uso.
  • La forma simple también está limitada a exactamente tres columnas en la consulta de entrada proporcionada: nombre_fila , categoría , valor . No hay espacio para columnas adicionales. como en la alternativa de 2 parámetros a continuación.

Forma segura

crosstab(text, text)con 2 parámetros de entrada:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- could also just be "ORDER BY 1" here

  , $$VALUES ('Active'::text), ('Inactive')$$
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

Devoluciones:

Sección | Activo | Inactivo
--------- + -------- + ----------
 A | 1 | 2
 B | 4 | 5 5
 C | El |        7   - !!
  • Tenga en cuenta el resultado correcto para C.

  • El segundo parámetro puede ser cualquier consulta que devuelva una fila por atributo que coincida con el orden de la definición de columna al final. A menudo querrá consultar atributos distintos de la tabla subyacente de esta manera:

    'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

    Eso está en el manual.

    Como debe deletrear todas las columnas en una lista de definición de columna de todos modos (excepto las variantes predefinidas ), generalmente es más eficiente proporcionar una lista corta en una expresión como la demostrada:crosstabN()VALUES

    $$VALUES ('Active'::text), ('Inactive')$$)

    O (no en el manual):

    $$SELECT unnest('{Active,Inactive}'::text[])$$  -- short syntax for long lists
  • Utilicé las cotizaciones en dólares para facilitar las cotizaciones.

  • Puede incluso columnas de salida con diferentes tipos de datos con crosstab(text, text)- siempre y cuando la representación de texto de la columna de valor es una entrada válida para el tipo de destino. De esta manera es posible que tenga los atributos de diferentes tipos y de salida text, date, numericetc para los atributos respectivos. Hay un ejemplo de código al final del capítulo.crosstab(text, text) en el manual .

db <> violín aquí

Ejemplos avanzados


\crosstabview en psql

Postgres 9.6 agregó este metacomando a su terminal interactiva predeterminada psql . Puede ejecutar la consulta que usaría como primer crosstab()parámetro y alimentarla \crosstabview(inmediatamente o en el siguiente paso). Me gusta:

db=> SELECT section, status, ct FROM tbl \crosstabview

Resultado similar al anterior, pero es una característica de representación exclusiva del lado del cliente . Las filas de entrada se tratan de forma ligeramente diferente, por ORDER BYlo tanto, no es necesario. Detalles para \crosstabviewen el manual. Hay más ejemplos de código en la parte inferior de esa página.

Respuesta relacionada en dba.SE por Daniel Vérité (el autor de la función psql):



La respuesta previamente aceptada está desactualizada.

  • La variante de la función crosstab(text, integer)está desactualizada. El segundo integerparámetro se ignora. Cito el manual actual :

    crosstab(text sql, int N) ...

    Versión obsoleta de crosstab(text). El parámetro Nahora se ignora, ya que la consulta de llamada siempre determina el número de columnas de valor.

  • Casting y cambio de nombre innecesarios.

  • Falla si una fila no tiene todos los atributos. Vea la variante segura con dos parámetros de entrada anteriores para manejar los atributos faltantes correctamente.

  • ORDER BYse requiere en la forma de un parámetro de crosstab(). El manual:

    En la práctica, la consulta SQL siempre debe especificar ORDER BY 1,2para garantizar que las filas de entrada estén ordenadas correctamente

Erwin Brandstetter
fuente
3
+1, buena crítica, gracias por notarloIn practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered
ChristopheD
Tengo algunos problemas al usar $$ VALUES .. $$. En su lugar, he usado 'VALUES (' '<attr>' ':: <type>), ..'
Marco Fantasia
¿Podemos especificar el enlace de parámetros en la consulta de tabla cruzada? Recibo este error => no se pudo determinar el tipo de datos del parámetro $ 2
Ashish
1
¿Es posible establecer el valor predeterminado para la columna en la consulta de tabla cruzada?
Ashish
2
@Ashish: Por favor, comience una nueva pregunta. Los comentarios no son el lugar. Siempre puede vincular a este para el contexto.
Erwin Brandstetter
30

Puede usar la crosstab()función del módulo adicional tablefunc , que debe instalar una vez por base de datos. Desde PostgreSQL 9.1 puede usar CREATE EXTENSIONpara eso:

CREATE EXTENSION tablefunc;

En su caso, creo que se vería así:

CREATE TABLE t (Section CHAR(1), Status VARCHAR(10), Count integer);

INSERT INTO t VALUES ('A', 'Active',   1);
INSERT INTO t VALUES ('A', 'Inactive', 2);
INSERT INTO t VALUES ('B', 'Active',   4);
INSERT INTO t VALUES ('B', 'Inactive', 5);

SELECT row_name AS Section,
       category_1::integer AS Active,
       category_2::integer AS Inactive
FROM crosstab('select section::text, status, count::text from t',2)
            AS ct (row_name text, category_1 text, category_2 text);
Jeremiah Peschka
fuente
En caso de que utilice un parámetro en la consulta de tabla cruzada, debe escapar de él correctamente. Ejemplo: (desde arriba) digamos que desea solo los activos: SELECCIONAR ... DESDE la tabla de referencias cruzadas ('seleccionar sección :: texto, estado, conteo :: texto de t donde estado =' 'activo' '', 2) AS. .. (observe las comillas dobles). En caso de que el usuario pase el parámetro en tiempo de ejecución (como un parámetro de función, por ejemplo), puede decir: SELECT ... FROM crosstab ('select section :: text, status, count :: text from t where status =' ' '|| par_active ||' '' ', 2) AS ... (¡comillas triples aquí!). En BIRT esto también funciona con el? marcador de posición
Wim Verhavert
26
SELECT section,
       SUM(CASE status WHEN 'Active' THEN count ELSE 0 END) AS active, --here you pivot each status value as a separate column explicitly
       SUM(CASE status WHEN 'Inactive' THEN count ELSE 0 END) AS inactive --here you pivot each status  value as a separate column explicitly

FROM t
GROUP BY section
araqnid
fuente
1
¿Alguien puede explicar qué agrega la función de tabla de referencias cruzadas en el módulo tablefunc a esta respuesta, que hace el trabajo a la vez y que, en mi opinión, es más fácil de entender?
John Powell
44
@ JohnBarça: Un caso simple como este se puede resolver fácilmente con declaraciones CASE. Sin embargo, esto se vuelve difícil de manejar muy rápidamente con más atributos y / u otros tipos de datos además de enteros. Por otro lado: este formulario usa la función de agregado sum(), sería mejor usar min()o max()y no, lo ELSEque texttambién funciona. Pero esto tiene efectos sutilmente diferentes que corosstab(), que solo usan el "primer" valor por atributo. No importa mientras solo pueda haber uno. Finalmente, el rendimiento también es relevante. crosstab()está escrito en C y optimizado para la tarea.
Erwin Brandstetter
Esto no funciona para mí, para postgresql. Me sale el errorERROR: 42803: aggregate function calls may not be nested
Audrey
1
@Audrey, ¿no estás ejecutando el mismo SQL entonces?
2
Considere agregar una explicación versus solo un bloque de código
Daniel L. VanDenBosch
10

Solución con agregación JSON:

CREATE TEMP TABLE t (
  section   text
, status    text
, ct        integer  -- don't use "count" as column name.
);

INSERT INTO t VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                   , ('C', 'Inactive', 7); 


SELECT section,
       (obj ->> 'Active')::int AS active,
       (obj ->> 'Inactive')::int AS inactive
FROM (SELECT section, json_object_agg(status,ct) AS obj
      FROM t
      GROUP BY section
     )X
Milos
fuente
Gracias, esto me ayudó con un problema relacionado.
JeffCharter
1

Lo siento, esto no está completo porque no puedo probarlo aquí, pero puede llevarte en la dirección correcta. Estoy traduciendo de algo que uso que hace una consulta similar:

select mt.section, mt1.count as Active, mt2.count as Inactive
from mytable mt
left join (select section, count from mytable where status='Active')mt1
on mt.section = mt1.section
left join (select section, count from mytable where status='Inactive')mt2
on mt.section = mt2.section
group by mt.section,
         mt1.count,
         mt2.count
order by mt.section asc;

El código con el que estoy trabajando es:

select m.typeID, m1.highBid, m2.lowAsk, m1.highBid - m2.lowAsk as diff, 100*(m1.highBid - m2.lowAsk)/m2.lowAsk as diffPercent
from mktTrades m
   left join (select typeID,MAX(price) as highBid from mktTrades where bid=1 group by typeID)m1
   on m.typeID = m1.typeID
   left join (select typeID,MIN(price) as lowAsk  from mktTrades where bid=0 group by typeID)m2
   on m1.typeID = m2.typeID
group by m.typeID, 
         m1.highBid, 
         m2.lowAsk
order by diffPercent desc;

que devolverá un typeID, la oferta de precio más alto y el precio más bajo pedido y la diferencia entre los dos (una diferencia positiva significaría que algo se podría comprar por menos de lo que se puede vender).

LanceH
fuente
1
Te falta una cláusula from, de lo contrario, esto es correcto. Los planes de explicación son muy diferentes en mi sistema: la función de tabla cruzada tiene un costo de 22.5, mientras que el enfoque LEFT JOIN es aproximadamente 4 veces más costoso con un costo de 91.38. También produce aproximadamente el doble de lecturas físicas y realiza uniones hash, lo que puede ser bastante costoso en comparación con otros tipos de unión.
Jeremiah Peschka
Gracias Jeremiah, es bueno saberlo. He votado a favor de la otra respuesta, pero vale la pena guardar tu comentario, así que no eliminaré esta.
LanceH
-1

CrosstabLa función está disponible bajo la tablefuncextensión. Tendrá que crear esta extensión una vez para la base de datos.

CREAR EXTENSIÓN tablefunc;

Puede usar el siguiente código para crear una tabla dinámica utilizando la tabla cruzada:

create table test_Crosstab( section text,
<br/>status text,
<br/>count numeric)

<br/>insert into test_Crosstab values ( 'A','Active',1)
                <br/>,( 'A','Inactive',2)
                <br/>,( 'B','Active',4)
                <br/>,( 'B','Inactive',5)

select * from crosstab(
<br/>'select section
    <br/>,status
    <br/>,count
    <br/>from test_crosstab'
    <br/>)as ctab ("Section" text,"Active" numeric,"Inactive" numeric)
Lekshmi Kurup
fuente
1
Esta respuesta no agrega nada sobre las respuestas preexistentes.
Erwin Brandstetter