Skip to content

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

Description

@dimitri-yatsenko

Summary

On the PostgreSQL backend, DataJoint does not create a secondary index on a child table's foreign-key columns except in the unique case. MySQL/InnoDB creates one implicitly as part of enforcing every foreign key; PostgreSQL does not, and there is no server setting to make it. This leaves an unindexed-FK performance gap on Postgres for a subset of foreign keys.

Background

  • MySQL/InnoDB requires and auto-creates an index on the referencing (child) columns of every FK.
  • PostgreSQL auto-indexes only the referenced (parent) side (the PK/unique constraint, already indexed). It never indexes the referencing (child) columns, and this is not configurable via postgresql.conf — it is a deliberate design choice, left to the DBA.

Current DataJoint behavior

compile_foreign_key() appends the constraint but emits an index only when the FK is unique:

# src/datajoint/declare.py (~288-307)
foreign_key_sql.append(
    f"FOREIGN KEY ({fk_cols}) REFERENCES {ref_table_name} ({pk_cols}) ON UPDATE CASCADE ON DELETE RESTRICT"
)
# declare unique index
if is_unique:
    index_cols = ", ".join(adapter.quote_identifier(attr) for attr in ref.primary_key)
    index_sql.append(f"UNIQUE INDEX ({index_cols})")

So on Postgres a non-unique FK gets no supporting index from DataJoint or from the engine.

When this actually bites (and when it doesn't)

Because a DataJoint FK inherits the parent's primary key into the child, the FK columns are often a left-prefix of the child's primary key — in which case the child's PK index already covers FK lookups on both backends, and no extra index is needed.

The gap is real only when the FK columns are not a usable left-prefix of an existing index, i.e.:

  • secondary (non-primary) foreign keys — FK attributes not in the child PK; and
  • FK columns that sit in a non-leading position of a composite PK (e.g. PK (a, b) where b comes from the FK), so lookups/cascades filtering on the FK alone can't use the PK index.

In those cases, on Postgres, cascade deletes (ON DELETE), joins on the FK, and the parent-existence check scan the child without index support — a performance cliff on large child tables, and an asymmetry vs. MySQL.

Proposal

On the Postgres DDL path, have declare.py emit an explicit CREATE INDEX on the FK columns when they are not already covered by a left-prefix of the PK or an existing index. MySQL can continue to rely on InnoDB's implicit index (avoid duplicating it). Points to work through:

  • Skip when FK columns are a left-prefix of the child PK (or an existing declared index) to avoid redundant indexes.
  • Idempotency with alter() (don't churn/duplicate indexes on redeclare).
  • Index naming.
  • The unique FK case already emits a UNIQUE INDEX; keep that.

References

  • src/datajoint/declare.pycompile_foreign_key() (~L288-307); PostgreSQL index handling (~L494-510).
  • Adapters: adapters/mysql.py (InnoDB implicit index), adapters/postgres.py.
  • Docs note added in datajoint-docs Added set_password and show_definition #227 (explanation/referential-integrity.md §5) currently advises users to declare an explicit index(...) on Postgres; if this is fixed in the framework, that guidance flips back to "the database indexes them."

Metadata

Metadata

Labels

enhancementIndicates new improvementsneeds-discussionIssues requiring further development review and verify impact.

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions