Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 49 additions & 52 deletions lints/0001_unindexed_foreign_keys.sql
Original file line number Diff line number Diff line change
@@ -1,58 +1,55 @@
create view "0001_unindexed_foreign_keys" as

with foreign_keys as (
select
cl.oid::regclass as table_,
ct.conname as fkey_name,
ct.conkey col_attnums
from
pg_constraint ct
join pg_class cl -- fkey owning table
on ct.conrelid = cl.oid
left join pg_depend d
on d.objid = cl.oid
and d.deptype = 'e'
where
ct.contype = 'f' -- foreign key constraints
and d.objid is null -- exclude tables that are dependencies of extensions
and cl.relnamespace::regnamespace::text not in (
'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'extensions'
)
),
index_ as (
select
indrelid::regclass as table_,
indexrelid::regclass as index_,
string_to_array(indkey::text, ' ')::smallint[] as col_attnums
from
pg_index
where
indisvalid
)
select
'unindexed_foreign_key' as name,
'WARN' as level,
'INTERNAL' as facing,
'Foreign keys without indexes can degrade performance on joins.' as description,
'0001_unindexed_foreign_keys' as name,
'INFO' as level,
'EXTERNAL' as facing,
'Identifies foreign key constraints without a covering index, which can impact database performance.' as description,
format(
'The foreign key on table %I.%I involving columns (%s) is not indexed.',
ns.nspname,
cl.relname,
string_agg(a.attname, ', ' order by a.attnum)
) as detail,
format(
'create index on %I.%I(%s);',
ns.nspname,
cl.relname,
string_agg(a.attname, ', ' order by a.attnum)
) as remediation,
'{}'::jsonb as metadata,
format(
'unindexed_foreign_key_%s_%s_%s',
ns.nspname,
cl.relname,
string_agg(a.attname, '_' order by a.attnum)
) as cache_key
'Table "%s" has a foreign key "%s" without a covering index. This can lead to suboptimal query performance.',
fk.table_, fk.fkey_name
) as detail,
null as remediation,
jsonb_build_object(
'table', fk.table_,
'fkey_name', fk.fkey_name,
'fkey_columns', fk.col_attnums
) as metadata,
format('0001_unindexed_foreign_keys_%s_%s', fk.table_, fk.fkey_name) as cache_key
from
pg_constraint ct
join pg_class cl
on ct.conrelid = cl.oid
join pg_namespace ns
on cl.relnamespace = ns.oid
join pg_attribute a
on a.attrelid = cl.oid
and a.attnum = any(ct.conkey)
left join pg_index ix
on ix.indrelid = ct.conrelid
left join lateral (
select array_agg(i) as indkeys
from unnest(ix.indkey) with ordinality as u(i, ord)
) ix_keys on true
left join pg_depend d
on d.refobjid = cl.oid
and d.deptype = 'e'
left join pg_extension e
on e.oid = d.objid
foreign_keys fk
left join index_ idx on fk.table_ = idx.table_ and fk.col_attnums = idx.col_attnums
where
ct.contype = 'f' -- foreign key constraints
and ns.nspname not in ('pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'extensions')
and e.oid is null -- exclude tables that are dependencies of extensions
and (ix.indrelid is null or not (ct.conkey <@ ix_keys.indkeys))
group by
ns.nspname,
cl.relname,
ct.oid
having
bool_or(ix.indrelid is null) -- check if there's no index covering all columns of the foreign key;

idx.index_ is null
order by
fk.table_,
fk.fkey_name;
61 changes: 61 additions & 0 deletions test/expected/0001_unindexed_foreign_keys.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
begin;
savepoint a;
-- Simple Case
-- No index on bbb.aaa_id produces an error
create table aaa(
id int primary key
);
create table bbb(
id int primary key,
aaa_id int references aaa(id) -- no index
);
select * from "0001_unindexed_foreign_keys";
name | level | facing | description | detail | remediation | metadata | cache_key
-----------------------------+-------+----------+-----------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------+-------------+-----------------------------------------------------------------------+-------------------------------------------------
0001_unindexed_foreign_keys | INFO | EXTERNAL | Identifies foreign key constraints without a covering index, which can impact database performance. | Table "bbb" has a foreign key "bbb_aaa_id_fkey" without a covering index. This can lead to suboptimal query performance. | | {"table": "bbb", "fkey_name": "bbb_aaa_id_fkey", "fkey_columns": [2]} | 0001_unindexed_foreign_keys_bbb_bbb_aaa_id_fkey
(1 row)

-- When a covering index is created, the error goes away
create index on bbb(aaa_id);
select * from "0001_unindexed_foreign_keys";
name | level | facing | description | detail | remediation | metadata | cache_key
------+-------+--------+-------------+--------+-------------+----------+-----------
(0 rows)

rollback to savepoint a;
-- Multi-column Case
-- No index on bbb(foo, bar)
create table aaa(
foo int,
bar int,
primary key (foo, bar)
);
create table bbb(
id int primary key,
foo int,
bar int,
foreign key (foo, bar) references aaa(foo, bar)
);
select * from "0001_unindexed_foreign_keys";
name | level | facing | description | detail | remediation | metadata | cache_key
-----------------------------+-------+----------+-----------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+-------------+---------------------------------------------------------------------------+--------------------------------------------------
0001_unindexed_foreign_keys | INFO | EXTERNAL | Identifies foreign key constraints without a covering index, which can impact database performance. | Table "bbb" has a foreign key "bbb_foo_bar_fkey" without a covering index. This can lead to suboptimal query performance. | | {"table": "bbb", "fkey_name": "bbb_foo_bar_fkey", "fkey_columns": [2, 3]} | 0001_unindexed_foreign_keys_bbb_bbb_foo_bar_fkey
(1 row)

-- Confirm that an index on the correct columns but in the wrong order
-- does NOT resolve the issue
create index on bbb(bar, foo);
select * from "0001_unindexed_foreign_keys";
name | level | facing | description | detail | remediation | metadata | cache_key
-----------------------------+-------+----------+-----------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+-------------+---------------------------------------------------------------------------+--------------------------------------------------
0001_unindexed_foreign_keys | INFO | EXTERNAL | Identifies foreign key constraints without a covering index, which can impact database performance. | Table "bbb" has a foreign key "bbb_foo_bar_fkey" without a covering index. This can lead to suboptimal query performance. | | {"table": "bbb", "fkey_name": "bbb_foo_bar_fkey", "fkey_columns": [2, 3]} | 0001_unindexed_foreign_keys_bbb_bbb_foo_bar_fkey
(1 row)

-- When we create a multi-column index in the correct order the issue is resolved
create index on bbb(foo, bar);
select * from "0001_unindexed_foreign_keys";
name | level | facing | description | detail | remediation | metadata | cache_key
------+-------+--------+-------------+--------+-------------+----------+-----------
(0 rows)

rollback;
55 changes: 55 additions & 0 deletions test/sql/0001_unindexed_foreign_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
begin;

savepoint a;

-- Simple Case
-- No index on bbb.aaa_id produces an error
create table aaa(
id int primary key
);

create table bbb(
id int primary key,
aaa_id int references aaa(id) -- no index
);

select * from "0001_unindexed_foreign_keys";


-- When a covering index is created, the error goes away
create index on bbb(aaa_id);
select * from "0001_unindexed_foreign_keys";


rollback to savepoint a;


-- Multi-column Case
-- No index on bbb(foo, bar)
create table aaa(
foo int,
bar int,
primary key (foo, bar)
);

create table bbb(
id int primary key,
foo int,
bar int,
foreign key (foo, bar) references aaa(foo, bar)
);

select * from "0001_unindexed_foreign_keys";

-- Confirm that an index on the correct columns but in the wrong order
-- does NOT resolve the issue

create index on bbb(bar, foo);
select * from "0001_unindexed_foreign_keys";

-- When we create a multi-column index in the correct order the issue is resolved
create index on bbb(foo, bar);
select * from "0001_unindexed_foreign_keys";


rollback;