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
CASE
dichiarazione. -
Una funzione pivot . PostgreSQL fornisce la
crosstab()
funzione nel modulo aggiuntivotablefunc
per 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
DROP
riceverai 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 TABLE
non 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