Skip to content

Design Primary Keys

Choose effective primary keys for your tables.

Primary Key Principles

Primary key attributes:

  • Uniquely identify each entity
  • Cannot be NULL
  • Cannot be changed after insertion
  • Are inherited by dependent tables via foreign keys

The primary key is how a table enforces entity integrity โ€” the one-to-one correspondence between rows and the real-world entities they represent. Every principle below follows from that responsibility.

Natural Keys

A key is natural when it identifies the entity outside the database โ€” printed on labels, written in lab notebooks, or spoken in conversation. Use one whenever it exists:

@schema
class Subject(dj.Manual):
    definition = """
    subject_id : varchar(16)    # Lab-assigned ID like 'M001'
    ---
    species : varchar(32)
    """

Good candidates: - Lab-assigned IDs - Standard identifiers (NCBI accession, DOI) - Meaningful codes with enforced uniqueness

Composite Keys

Combine attributes when a single attribute isn't unique:

@schema
class Session(dj.Manual):
    definition = """
    -> Subject
    session_idx : int32        # Session number within subject
    ---
    session_date : date
    """

The primary key is (subject_id, session_idx).

Surrogate Keys

Use UUIDs when natural keys don't exist:

@schema
class Experiment(dj.Manual):
    definition = """
    experiment_id : uuid
    ---
    description : varchar(500)
    """

Generate UUIDs:

import uuid

Experiment.insert1({
    'experiment_id': uuid.uuid4(),
    'description': 'Pilot study'
})

Why DataJoint Avoids Auto-Increment

DataJoint discourages auto_increment for primary keys:

  1. Encourages lazy design โ€” Users treat it as "row number" rather than thinking about what uniquely identifies the entity in their domain.

  2. Incompatible with composite keys โ€” DataJoint schemas routinely use composite keys like (subject_id, session_idx, trial_idx). MySQL allows only one auto_increment column per table, and it must be first in the key.

  3. Breaks reproducibility โ€” Auto_increment values depend on insertion order. Rebuilding a pipeline produces different IDs.

  4. No client-server handshake โ€” The client discovers the ID only after insertion, complicating error handling and concurrent access.

  5. Meaningless foreign keys โ€” Downstream tables inherit opaque integers rather than traceable lineage.

Instead, use: - Natural keys that identify entities in your domain - UUIDs when no natural identifier exists - Composite keys combining foreign keys with sequence numbers

Foreign Keys in Primary Key

Foreign keys above the --- become part of the primary key:

@schema
class Trial(dj.Manual):
    definition = """
    -> Session                  # In primary key
    trial_idx : int32          # In primary key
    ---
    -> Stimulus                  # NOT in primary key
    outcome : enum('hit', 'miss')
    """

Key Design Guidelines

Keep Keys Small

The primary key is copied into every dependent table, index, and join, so an oversized key multiplies across the whole schema. Use the smallest type that covers the range, and don't reach for a wide string key when a compact integer will do:

# Good: a compact integer key
scan_id : int32             # up to ~2.1 billion scans

# Avoid: a 200-character string key where an int32 would do โ€”
# this key is copied into every child table, index, and join
scan_id : varchar(200)

# Avoid: a wider integer than the range needs
scan_id : int64             # wastes space, slower joins

Avoid Floating-Point Keys

Never use float or double in a primary key: equality comparison on floating-point values is unreliable because of rounding, so lookups and joins on the key can silently miss. Use decimal (fixed-point) โ€” or an integer โ€” instead:

# Bad: float equality is fraught with rounding error โ€” key lookups can miss
dose_mg : float64

# Good: exact fixed-point value
dose_mg : decimal(6, 3)

A date or datetime is perfectly good key material when the entity is genuinely identified by that time (a daily summary, or a session dated by day). Add a sequence number only when the date alone doesn't identify the entity.

Migration Considerations

Once a table has data, primary keys cannot be changed. Plan carefully:

# Consider future needs
@schema
class Scan(dj.Manual):
    definition = """
    -> Session
    scan_idx : int16           # Might need int32 for high-throughput
    ---
    ...
    """

See Also

  • Entity Integrity โ€” why every table needs a primary key and how it enforces the one-to-one correspondence with entities
  • Normalization โ€” organizing attributes so each fact lives in exactly one place
  • Define Tables โ€” Table definition syntax
  • Model Relationships โ€” Foreign key patterns