Supponendo che Postgres 9.1 o versioni successive.
Ho semplificato/ottimizzato la query di base per recuperare i valori più recenti:
SELECT DISTINCT ON (1,2)
c.unique_id, a.attname AS col, c.value
FROM pg_attribute a
LEFT JOIN changes c ON c.column_name = a.attname
AND c.table_name = 'instances'
-- AND c.unique_id = 3 -- uncomment to fetch single row
WHERE a.attrelid = 'instances'::regclass -- schema-qualify to be clear?
AND a.attnum > 0 -- no system columns
AND NOT a.attisdropped -- no deleted columns
ORDER BY 1, 2, c.updated_at DESC;
Interrogo il catalogo PostgreSQL invece dello schema di informazioni standard perché è più veloce. Nota il cast speciale di ::regclass .
Ora, questo ti dà una tabella . Vuoi tutti i valori per un unique_id in una riga .
Per raggiungere questo obiettivo hai fondamentalmente tre opzioni:
-
Una sottoselezione (o join) per colonna. Costoso e ingombrante. Ma un'opzione valida solo per poche colonne.
-
Un grande
CASEdichiarazione. -
Una funzione pivot . PostgreSQL fornisce la
crosstab()funzione nel modulo aggiuntivotablefuncper questo.
Istruzioni di base:- Query a campi incrociati PostgreSQL
Tabella pivot di base con crosstab()
Ho riscritto completamente la funzione:
SELECT *
FROM crosstab(
$x$
SELECT DISTINCT ON (1, 2)
unique_id, column_name, value
FROM changes
WHERE table_name = 'instances'
-- AND unique_id = 3 -- un-comment to fetch single row
ORDER BY 1, 2, updated_at DESC;
$x$,
$y$
SELECT attname
FROM pg_catalog.pg_attribute
WHERE attrelid = 'instances'::regclass -- possibly schema-qualify table name
AND attnum > 0
AND NOT attisdropped
AND attname <> 'unique_id'
ORDER BY attnum
$y$
)
AS tbl (
unique_id integer
-- !!! You have to list all columns in order here !!! --
);
Ho separato la ricerca nel catalogo dalla query del valore, come crosstab() la funzione con due parametri fornisce i nomi delle colonne separatamente. I valori mancanti (nessuna voce nelle modifiche) vengono sostituiti con NULL automaticamente. Una corrispondenza perfetta per questo caso d'uso!
Supponendo che attname corrisponde a column_name . Escluso unique_id , che svolge un ruolo speciale.
Automazione completa
Rispondere al tuo commento:C'è un modo per fornire automaticamente l'elenco delle definizioni delle colonne. Non è per i deboli di cuore, però.
Uso una serie di funzionalità avanzate di Postgres qui:crosstab() , funzione plpgsql con SQL dinamico, gestione del tipo composito, quotazione avanzata del dollaro, ricerca nel catalogo, funzione di aggregazione, funzione finestra, tipo di identificatore di oggetto, ...
Ambiente di prova:
CREATE TABLE instances (
unique_id int
, col1 text
, col2 text -- two columns are enough for the demo
);
INSERT INTO instances VALUES
(1, 'foo1', 'bar1')
, (2, 'foo2', 'bar2')
, (3, 'foo3', 'bar3')
, (4, 'foo4', 'bar4');
CREATE TABLE changes (
unique_id int
, table_name text
, column_name text
, value text
, updated_at timestamp
);
INSERT INTO changes VALUES
(1, 'instances', 'col1', 'foo11', '2012-04-12 00:01')
, (1, 'instances', 'col1', 'foo12', '2012-04-12 00:02')
, (1, 'instances', 'col1', 'foo1x', '2012-04-12 00:03')
, (1, 'instances', 'col2', 'bar11', '2012-04-12 00:11')
, (1, 'instances', 'col2', 'bar17', '2012-04-12 00:12')
, (1, 'instances', 'col2', 'bar1x', '2012-04-12 00:13')
, (2, 'instances', 'col1', 'foo2x', '2012-04-12 00:01')
, (2, 'instances', 'col2', 'bar2x', '2012-04-12 00:13')
-- NO change for col1 of row 3 - to test NULLs
, (3, 'instances', 'col2', 'bar3x', '2012-04-12 00:13');
-- NO changes at all for row 4 - to test NULLs
Funzione automatizzata per una tabella
CREATE OR REPLACE FUNCTION f_curr_instance(int, OUT t public.instances) AS
$func$
BEGIN
EXECUTE $f$
SELECT *
FROM crosstab($x$
SELECT DISTINCT ON (1,2)
unique_id, column_name, value
FROM changes
WHERE table_name = 'instances'
AND unique_id = $f$ || $1 || $f$
ORDER BY 1, 2, updated_at DESC;
$x$
, $y$
SELECT attname
FROM pg_catalog.pg_attribute
WHERE attrelid = 'public.instances'::regclass
AND attnum > 0
AND NOT attisdropped
AND attname <> 'unique_id'
ORDER BY attnum
$y$) AS tbl ($f$
|| (SELECT string_agg(attname || ' ' || atttypid::regtype::text
, ', ' ORDER BY attnum) -- must be in order
FROM pg_catalog.pg_attribute
WHERE attrelid = 'public.instances'::regclass
AND attnum > 0
AND NOT attisdropped)
|| ')'
INTO t;
END
$func$ LANGUAGE plpgsql;
La tabella instances è codificato, lo schema è qualificato per essere non ambiguo. Notare l'uso del tipo di tabella come tipo restituito. Esiste un tipo di riga registrato automaticamente per ogni tabella in PostgreSQL. Questo è destinato a corrispondere al tipo restituito di crosstab() funzione.
Questo lega la funzione al tipo di tabella:
- Se provi a
DROPriceverai un messaggio di errore la tavola - La tua funzione fallirà dopo un
ALTER TABLE. Devi ricrearlo (senza modifiche). Considero questo un bug in 9.1.ALTER TABLEnon dovrebbe interrompere silenziosamente la funzione, ma generare un errore.
Funziona molto bene.
Chiama:
SELECT * FROM f_curr_instance(3);
unique_id | col1 | col2
----------+-------+-----
3 |<NULL> | bar3x
Nota come col1 è NULL qui.
Utilizzare in una query per visualizzare un'istanza con i suoi valori più recenti:
SELECT i.unique_id
, COALESCE(c.col1, i.col1)
, COALESCE(c.col2, i.col2)
FROM instances i
LEFT JOIN f_curr_instance(3) c USING (unique_id)
WHERE i.unique_id = 3;
Automazione completa per qualsiasi tabella
(Aggiunto nel 2016. Questa è dinamite.)
Richiede Postgres 9.1 o più tardi. (Potrebbe funzionare con pg 8.4, ma non mi sono preoccupato di eseguire il backpatch.)
CREATE OR REPLACE FUNCTION f_curr_instance(_id int, INOUT _t ANYELEMENT) AS
$func$
DECLARE
_type text := pg_typeof(_t);
BEGIN
EXECUTE
(
SELECT format
($f$
SELECT *
FROM crosstab(
$x$
SELECT DISTINCT ON (1,2)
unique_id, column_name, value
FROM changes
WHERE table_name = %1$L
AND unique_id = %2$s
ORDER BY 1, 2, updated_at DESC;
$x$
, $y$
SELECT attname
FROM pg_catalog.pg_attribute
WHERE attrelid = %1$L::regclass
AND attnum > 0
AND NOT attisdropped
AND attname <> 'unique_id'
ORDER BY attnum
$y$) AS ct (%3$s)
$f$
, _type, _id
, string_agg(attname || ' ' || atttypid::regtype::text
, ', ' ORDER BY attnum) -- must be in order
)
FROM pg_catalog.pg_attribute
WHERE attrelid = _type::regclass
AND attnum > 0
AND NOT attisdropped
)
INTO _t;
END
$func$ LANGUAGE plpgsql;
Chiama (fornendo il tipo di tabella con NULL::public.instances :
SELECT * FROM f_curr_instance(3, NULL::public.instances);
Correlati:
- Refactoring di una funzione PL/pgSQL per restituire l'output di varie query SELECT
- Come impostare il valore di un campo variabile composto utilizzando SQL dinamico