patternsqlMinor
Select orphaned rows (in a generic way)
Viewed 0 times
rowsgenericorphanedwayselect
Problem
I have a table
I would like to select all rows of
AFAIK, it should be possible to do this in a generic way (with some introspection magic).
my_tables which gets referenced as foreign key in several tables.I would like to select all rows of
my_table which are not referenced in an other table.AFAIK, it should be possible to do this in a generic way (with some introspection magic).
Solution
Quoting the manual:
A foreign key must reference columns that either are a primary key or
form a unique constraint.
So that's not necessarily limited to the PK. But if we start with
Using the object identifiers type
Basic query
Produces a query of the form:
Which returns all rows that are not currently referenced by any FK constraint.
If
Full automation
To make this work for any input table dynamically, create a polymorphic function taking a row value of the table:
Call (important)!
Or:
Related:
A foreign key must reference columns that either are a primary key or
form a unique constraint.
So that's not necessarily limited to the PK. But if we start with
pg_constraint, we get all FK constraints pointing to the target table automatically. No need to provide any key columns - except if you want to limit to certain FKs.Using the object identifiers type
regclass and table aliases, we can keep the function short and the result safe and unambiguous:Basic query
SELECT format(E'SELECT * FROM %s t\nWHERE NOT EXISTS (SELECT FROM %s'
, c.confrelid::regclass
, string_agg(format('%s WHERE %s = %s)', c.conrelid::regclass, src.cols, tgt.cols)
, E'\nAND NOT EXISTS (SELECT FROM '))
FROM pg_constraint c
, cardinality(c.conkey) AS col_ct
, LATERAL (
SELECT concat(CASE WHEN col_ct > 1 THEN '(' END
, string_agg(quote_ident(attname), ', ' ORDER BY fld.ord) -- original order
, CASE WHEN col_ct > 1 THEN ')' END) AS cols
FROM unnest(c.conkey) WITH ORDINALITY fld(attnum, ord) -- possibly n cols
JOIN pg_catalog.pg_attribute a ON (a.attrelid, a.attnum) = (c.conrelid, fld.attnum)
) src
, LATERAL (
SELECT concat(CASE WHEN col_ct > 1 THEN '(' END -- parentheses for multiple columns
, string_agg('t.' || quote_ident(attname), ', t.' ORDER BY fld.ord)
, CASE WHEN col_ct > 1 THEN ')' END) AS cols
FROM unnest(c.confkey) WITH ORDINALITY fld(attnum, ord)
JOIN pg_catalog.pg_attribute a ON (a.attrelid, a.attnum) = (c.confrelid, fld.attnum)
) tgt
WHERE c.confrelid = 'my_table'::regclass -- target table name, optionally schema-qualified
AND c.contype = 'f' -- FK constraints
GROUP BY c.confrelid;Produces a query of the form:
SELECT * FROM my_table t
WHERE NOT EXISTS (SELECT FROM schema1.tbl1 WHERE col1 = t.id)
AND NOT EXISTS (SELECT FROM "tB-l2" WHERE ("COL2", col3) = (t.col4, t.col5));Which returns all rows that are not currently referenced by any FK constraint.
If
cardinality(c.conkey) > 1 then it's safe to also assume cardinality(c.confkey) > 1. So only count once to decide whether to add parentheses.Full automation
To make this work for any input table dynamically, create a polymorphic function taking a row value of the table:
CREATE OR REPLACE FUNCTION f_orphans(_tbl anyelement)
RETURNS SETOF anyelement AS
$func$
BEGIN
RETURN QUERY EXECUTE ( -- exactly the query from above
SELECT format(E'SELECT * FROM %s t\nWHERE NOT EXISTS (SELECT FROM %s'
, c.confrelid::regclass
, string_agg(format('%s WHERE %s = %s)', c.conrelid::regclass, src.cols, tgt.cols)
, E'\nAND NOT EXISTS (SELECT FROM '))
FROM pg_constraint c
, cardinality(c.conkey) AS col_ct
, LATERAL (
SELECT concat(CASE WHEN col_ct > 1 THEN '(' END
, string_agg(quote_ident(attname), ', ' ORDER BY fld.ord)
, CASE WHEN col_ct > 1 THEN ')' END) AS cols
FROM unnest(c.conkey) WITH ORDINALITY fld(attnum, ord)
JOIN pg_catalog.pg_attribute a ON (a.attrelid, a.attnum) = (c.conrelid, fld.attnum)
) src
, LATERAL (
SELECT concat(CASE WHEN col_ct > 1 THEN '(' END
, string_agg('t.' || quote_ident(attname), ', t.' ORDER BY fld.ord)
, CASE WHEN col_ct > 1 THEN ')' END) AS cols
FROM unnest(c.confkey) WITH ORDINALITY fld(attnum, ord)
JOIN pg_catalog.pg_attribute a ON (a.attrelid, a.attnum) = (c.confrelid, fld.attnum)
) tgt
WHERE c.confrelid = pg_typeof(_tbl)::text::regclass -- input goes here!
AND c.contype = 'f'
GROUP BY c.confrelid
);
END
$func$ LANGUAGE plpgsql;Call (important)!
SELECT * FROM f_orphans(NULL::my_table);Or:
SELECT * FROM f_orphans(NULL::myschema.my_table);Related:
- PostgreSQL: Pass table as argument in function
- Retrieving all PK and FK
- SQL order by query results in any arbitrary (but reproducible) manner
- How to preserve the original order of elements in an unnested array?
Code Snippets
SELECT format(E'SELECT * FROM %s t\nWHERE NOT EXISTS (SELECT FROM %s'
, c.confrelid::regclass
, string_agg(format('%s WHERE %s = %s)', c.conrelid::regclass, src.cols, tgt.cols)
, E'\nAND NOT EXISTS (SELECT FROM '))
FROM pg_constraint c
, cardinality(c.conkey) AS col_ct
, LATERAL (
SELECT concat(CASE WHEN col_ct > 1 THEN '(' END
, string_agg(quote_ident(attname), ', ' ORDER BY fld.ord) -- original order
, CASE WHEN col_ct > 1 THEN ')' END) AS cols
FROM unnest(c.conkey) WITH ORDINALITY fld(attnum, ord) -- possibly n cols
JOIN pg_catalog.pg_attribute a ON (a.attrelid, a.attnum) = (c.conrelid, fld.attnum)
) src
, LATERAL (
SELECT concat(CASE WHEN col_ct > 1 THEN '(' END -- parentheses for multiple columns
, string_agg('t.' || quote_ident(attname), ', t.' ORDER BY fld.ord)
, CASE WHEN col_ct > 1 THEN ')' END) AS cols
FROM unnest(c.confkey) WITH ORDINALITY fld(attnum, ord)
JOIN pg_catalog.pg_attribute a ON (a.attrelid, a.attnum) = (c.confrelid, fld.attnum)
) tgt
WHERE c.confrelid = 'my_table'::regclass -- target table name, optionally schema-qualified
AND c.contype = 'f' -- FK constraints
GROUP BY c.confrelid;SELECT * FROM my_table t
WHERE NOT EXISTS (SELECT FROM schema1.tbl1 WHERE col1 = t.id)
AND NOT EXISTS (SELECT FROM "tB-l2" WHERE ("COL2", col3) = (t.col4, t.col5));CREATE OR REPLACE FUNCTION f_orphans(_tbl anyelement)
RETURNS SETOF anyelement AS
$func$
BEGIN
RETURN QUERY EXECUTE ( -- exactly the query from above
SELECT format(E'SELECT * FROM %s t\nWHERE NOT EXISTS (SELECT FROM %s'
, c.confrelid::regclass
, string_agg(format('%s WHERE %s = %s)', c.conrelid::regclass, src.cols, tgt.cols)
, E'\nAND NOT EXISTS (SELECT FROM '))
FROM pg_constraint c
, cardinality(c.conkey) AS col_ct
, LATERAL (
SELECT concat(CASE WHEN col_ct > 1 THEN '(' END
, string_agg(quote_ident(attname), ', ' ORDER BY fld.ord)
, CASE WHEN col_ct > 1 THEN ')' END) AS cols
FROM unnest(c.conkey) WITH ORDINALITY fld(attnum, ord)
JOIN pg_catalog.pg_attribute a ON (a.attrelid, a.attnum) = (c.conrelid, fld.attnum)
) src
, LATERAL (
SELECT concat(CASE WHEN col_ct > 1 THEN '(' END
, string_agg('t.' || quote_ident(attname), ', t.' ORDER BY fld.ord)
, CASE WHEN col_ct > 1 THEN ')' END) AS cols
FROM unnest(c.confkey) WITH ORDINALITY fld(attnum, ord)
JOIN pg_catalog.pg_attribute a ON (a.attrelid, a.attnum) = (c.confrelid, fld.attnum)
) tgt
WHERE c.confrelid = pg_typeof(_tbl)::text::regclass -- input goes here!
AND c.contype = 'f'
GROUP BY c.confrelid
);
END
$func$ LANGUAGE plpgsql;SELECT * FROM f_orphans(NULL::my_table);SELECT * FROM f_orphans(NULL::myschema.my_table);Context
StackExchange Database Administrators Q#205684, answer score: 7
Revisions (0)
No revisions yet.