Skip to content

fix(#1512): index foreign-key columns on PostgreSQL (coverage-aware) - #1526

Open
dimitri-yatsenko wants to merge 3 commits into
masterfrom
fix/1512-postgres-fk-index
Open

fix(#1512): index foreign-key columns on PostgreSQL (coverage-aware)#1526
dimitri-yatsenko wants to merge 3 commits into
masterfrom
fix/1512-postgres-fk-index

Conversation

@dimitri-yatsenko

Copy link
Copy Markdown
Member

Closes #1512.

Why

PostgreSQL never auto-indexes a foreign key's referencing (child) columns and offers no server setting to change it — only MySQL/InnoDB does, implicitly. So on Postgres a non-unique FK was left with no supporting index, a performance cliff for cascade deletes, joins on the FK, and parent-existence checks on large child tables — and an asymmetry with MySQL.

What

declare.py's compile_foreign_key() now emits an explicit index on the FK columns on the PostgreSQL DDL path only, coverage-aware:

  • Skip when the FK columns are already a left-prefix of the child's primary key — a leading primary FK, whose lookups the PK index already serves.
  • Emit otherwise — a secondary FK (not in the PK) or a non-leading primary FK (e.g. PK (b_id, a_id) with a_id from the FK), neither covered by the PK index.

The coverage test is exactly primary_key[:len(fk_attrs)] == fk_attrs. MySQL continues to rely on InnoDB's implicit index (nothing emitted); the unique FK case keeps its UNIQUE INDEX on both backends. The existing Postgres DDL path already converts INDEX (...) entries to CREATE INDEX statements, so no new plumbing.

Per the issue decision: index creation only — the index lifecycle on FK drop / .alter() is deferred to the schema-changes design.

Tests

tests/integration/test_fk_index.py (PostgreSQL-specific; MySQL cases skip since the index is InnoDB's, not ours):

  • secondary FK → supporting index present;
  • non-leading primary FK → supporting index present;
  • leading primary FK → no redundant index.

Verified failing without the fix (negative control: the two positive tests fail) and passing with it; full test_declare.py + test_cascade_delete.py green on MySQL 8.0 and PostgreSQL 15 (52 passed, no regressions).

Follow-up (not in this PR)

datajoint-docs #227 currently advises users to declare an explicit index(...) on Postgres; once this lands, that guidance flips back to "the database indexes FK columns." I'll pair the docs update with the spec reshape.

Draft pending review.

PostgreSQL never auto-indexes a foreign key's referencing (child) columns and
offers no server setting to change that, so non-unique FKs were left unindexed —
a performance cliff on cascades/joins and an asymmetry with MySQL/InnoDB.

declare.py now emits an explicit index on the FK columns on the PostgreSQL DDL
path, coverage-aware: skip when the FK columns are already a left-prefix of the
child's primary key (a leading primary FK, served by the PK index), emit
otherwise (secondary FKs and non-leading primary FKs). MySQL keeps InnoDB's
implicit index; the unique-FK case keeps its UNIQUE INDEX. Creation only —
FK-drop/alter lifecycle deferred.

Tests (PostgreSQL-specific; verified failing without the fix).
@dimitri-yatsenko dimitri-yatsenko added the enhancement Indicates new improvements label Jul 31, 2026
The inline check only compared against the primary key (and couldn't see indexes
declared after the FK line). Move the decision to a post-parse pass in
prepare_declare, where the full primary key and index list are known: emit an FK
support index unless its columns are a left-prefix of the primary key, of a
declared index, or of a longer FK-support index already emitted (longest-first).
compile_foreign_key now only records candidates. Never under-indexes; no longer
creates redundant indexes.

Adds a test for the declared-index coverage case.
@dimitri-yatsenko
dimitri-yatsenko marked this pull request as ready for review July 31, 2026 19:21
Replace the `adapter.backend == "postgresql"` string check in declare.py with a
new adapter capability, `auto_indexes_foreign_keys` (MySQL True, PostgreSQL
False), mirroring `supports_inline_indexes`. The backend *policy* (who indexes FK
columns) now lives on the adapter; the coverage/redundancy logic stays in
declare.py as backend-agnostic schema-structure reasoning. Scales to new backends
without touching declare.py.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Indicates new improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PostgreSQL: foreign-key columns left unindexed (declare.py emits an FK index only for unique FKs)

1 participant