Question: Indexes
Do I need an index on a foreign key?
Answered in the first paragraph. Last updated .
The referenced side is already indexed, because the constraint requires it. The referencing side is not, and the documentation explains why: there are too many reasonable ways to index it for the server to guess. Whether you need one has almost nothing to do with your joins and everything to do with a question most schemas never ask, which is what happens on the child table when a parent row is deleted or its key is updated.
The cost of not having one
Removing a parent row obliges the server to look for children that reference it. Without an index that is a full scan of the child table, per parent row removed, and with several child tables it is several scans. A delete of a few hundred rows from a small lookup table can therefore take minutes and read tens of gigabytes, which is one of the more surprising bills in this database.
The scan is not free of side effects either. Matching child rows are locked while the check runs, so a delete on the parent can block writers on a large child table for the duration. That lock is a row-level one rather than a table lock, which means it does not show up where people look first; lock modes covers what it conflicts with.
Cascading deletes multiply both effects, because the same check happens at every level of the tree.
When skipping it is defensible
A parent table nobody ever deletes from, and whose keys are never updated, does not pay the cost above. If the child side is also never filtered by that column, the index would sit there being maintained on every write and read by nothing, which is its own kind of waste. Unused and missing indexes covers measuring that rather than assuming it.
Two practical notes. A composite index whose leading column is the referencing column already satisfies the requirement, so you may have the coverage without a dedicated index. And on a partitioned child table the index has to exist in a form the planner can use per partition, or the scan comes back in a more expensive shape.
If the index exists and the delete is still slow, the planner may be choosing the table anyway because the estimate says most rows match, which is the ordinary sequential scan decision and is covered at length in why Postgres is not using my index.