Ervan uitgaande dat Postgres 9.1 of later.
Ik heb uw basisquery vereenvoudigd/geoptimaliseerd om de nieuwste waarden op te halen:
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;
Ik bevraag de PostgreSQL-catalogus in plaats van het standaardinformatieschema omdat dat sneller is. Let op de speciale cast voor ::regclass
.
Dat geeft je nu een tabel . U wilt alle waarden voor één unique_id
op een rij .
Om dat te bereiken heb je in principe drie opties:
-
Eén subselect (of join) per kolom. Duur en onhandig. Maar een geldige optie voor slechts een paar kolommen.
-
Een grote
CASE
verklaring. -
Een draaifunctie . PostgreSQL biedt de
crosstab()
functie in de aanvullende moduletablefunc
daarvoor.
Basisinstructies:- PostgreSQL-kruistabelquery
Basis draaitabel met crosstab()
Ik heb de functie volledig herschreven:
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 !!! --
);
Ik heb het opzoeken van de catalogus gescheiden van de waardequery, zoals de crosstab()
functie met twee parameters biedt kolomnamen afzonderlijk. Ontbrekende waarden (geen invoer in wijzigingen) worden vervangen door NULL
automatisch. Een perfecte match voor deze use case!
Ervan uitgaande dat attname
komt overeen met column_name
. Exclusief unique_id
, die een speciale rol speelt.
Volledige automatisering
Uw opmerking aanpakken:Er is een manier om de kolomdefinitielijst automatisch aan te leveren. Het is echter niet voor bangeriken.
Ik gebruik hier een aantal geavanceerde Postgres-functies:crosstab()
, plpgsql-functie met dynamische SQL, verwerking van samengestelde typen, geavanceerde dollarquotering, opzoeken in catalogus, aggregatiefunctie, vensterfunctie, type objectidentificatie, ...
Testomgeving:
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
Geautomatiseerde functie voor één tafel
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;
De tabel instances
is hard gecodeerd, schema gekwalificeerd om ondubbelzinnig te zijn. Let op het gebruik van het tabeltype als retourtype. Er is automatisch een rijtype geregistreerd voor elke tabel in PostgreSQL. Dit komt ongetwijfeld overeen met het retourtype van de crosstab()
functie.
Dit bindt de functie aan het type tabel:
- Je krijgt een foutmelding als je probeert te
DROP
de tafel - Uw functie zal mislukken na een
ALTER TABLE
. Je moet het opnieuw maken (zonder wijzigingen). Ik beschouw dit als een bug in 9.1.ALTER TABLE
mag de functie niet stilzwijgend verbreken, maar een foutmelding geven.
Dit presteert erg goed.
Bel:
SELECT * FROM f_curr_instance(3);
unique_id | col1 | col2
----------+-------+-----
3 |<NULL> | bar3x
Merk op hoe col1
is NULL
hier.
Gebruik in een query om een instantie met de nieuwste waarden weer te geven:
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;
Volledige automatisering voor elke tafel
(Toegevoegd 2016. Dit is dynamiet.)
Vereist Postgres 9.1 of later. (Kan worden gemaakt om te werken met pg 8.4, maar ik nam niet de moeite om te backpatchen.)
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;
Aanroepen (geef het tabeltype op met NULL::public.instances
:
SELECT * FROM f_curr_instance(3, NULL::public.instances);
Gerelateerd:
- Refactor een PL/pgSQL-functie om de uitvoer van verschillende SELECT-query's te retourneren
- Hoe de waarde van het samengestelde variabele veld in te stellen met dynamische SQL