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 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:
select 1 from T where not exists (select 1 from child1 c1 where c1.fkcols = T.pkcols union all select 1 from child2 c2 where c1.fkcols = T.pkcols union all ...)