When should a Django model use a UUIDField primary key instead of BigAutoField, and what does that choice cost in storage and index performance?
answer
- unguessable, mergeable identifiers
- a callable, not a value
- wider keys everywhere they are copied
- random versus time-ordered inserts
basics
~20 sUse a UUID key when ids must be unguessable or created outside the database. It costs a wider key in every index and foreign key, char(32) off PostgreSQL and MariaDB, and random uuid4 values scatter inserts across the index; time-ordered UUIDv7 avoids that.
solid answer
~40 sA UUID key suits ids that appear in URLs and must not be enumerable, ids generated before the row exists (offline clients, several services), and data merged across databases. Declare it as `id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)` with the callable, not `uuid.uuid4()`. Costs: PostgreSQL and MariaDB store 16-byte `uuid`; other backends use `char(32)`, and the width is repeated in every index and foreign key pointing at the table. Random v4 values land anywhere in the primary key index, so inserts touch scattered pages instead of appending. Django 6.1's `UUID7()` database function, usable as `db_default` on PostgreSQL 18+, gives time-ordered keys that insert near the end. A common compromise keeps a `BigAutoField` key and adds a unique `UUIDField` as the public id.
code
python · 15 linesimport uuid
from django.db import models
from django.db.models.functions import UUID7
class Order(models.Model):
# Time-ordered UUID key generated by PostgreSQL 18+ (Django 6.1)
id = models.UUIDField(primary_key=True, db_default=UUID7(), editable=False)
class Invoice(models.Model):
# Integer key for joins, random UUID for URLs and APIs
public_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
order = models.ForeignKey(Order, on_delete=models.PROTECT)go deeper
Remember how to declare a UUID primary key and that default takes the callable uuid.uuid4, without parentheses.
Explain the storage per backend and why wider keys also widen every foreign key and index that references the table.
Argue the choice from real needs, exposure, offline creation or merging, and bring in index locality, UUIDv7 and the integer-plus-public-UUID pattern.
Set a key policy across services, which tables expose ids, where ids are minted and how merging works, and weigh it against storage and write cost.
## Why teams choose UUID keys A Django model's primary key defaults to a `BigAutoField`: a 64-bit integer the database assigns in increasing order. A **`UUIDField`** primary key replaces it with a 128-bit universally unique identifier. The reasons are about **where ids are created and who sees them**: - **Unguessable public ids** — `/orders/42/` invites a visitor to try `/orders/43/`; a UUID in the URL does not reveal volume or neighbours. Authorization is still required, but enumeration is harder. - **Ids created before insert** — a mobile client working offline, or several services creating related records, can mint ids without asking the database. - **Merging data** — rows from several databases or shards can be combined without renumbering. ## Declaring it correctly The database does not generate the value for `UUIDField` by default, so the Django docs recommend a Python-side default: ```python import uuid from django.db import models class Order(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) ``` Two details matter: 1. **Pass the callable** — `default=uuid.uuid4`, not `default=uuid.uuid4()`. The call with parentheses runs once at import time, so every new row would get the same UUID and the second insert fails on the primary key. 2. **It cannot be the project default** — `DEFAULT_AUTO_FIELD` must name an `AutoField` subclass, so UUID keys are declared model by model. Since **Django 6.1**, the database functions **`UUID4()`** and **`UUID7()`** can generate values in SQL, for example as `db_default=UUID7()`. `UUID4` is available on PostgreSQL, SQLite, MariaDB 11.7+ and recent Oracle; `UUID7` on PostgreSQL 18+, MariaDB 11.7+, and SQLite when running on Python 3.14+. ## What it costs | Cost | `BigAutoField` | `UUIDField` primary key | |---|---|---| | Key width | 8 bytes | 16 bytes as `uuid` on PostgreSQL/MariaDB; `char(32)` on other backends | | Copies | every index and foreign key column referencing it | the same copies, each wider | | Insert position in the key index | always at the end | random for v4; near the end for v7 | | Readability in logs and support tickets | short numbers | long strings | **Width multiplies.** Every `ForeignKey` pointing at `Order` stores the key too, and every index on those columns repeats it. On large child tables such as order lines, the difference shows up in table size, index size and memory. **Random order hurts inserts.** A primary key is backed by an ordered index. Increasing integers always add entries at the right-hand edge, so recent pages stay hot. Random v4 UUIDs land anywhere, so each insert may touch a different page, which lowers cache efficiency and causes more page splits as tables grow. **Version 7** UUIDs start with a timestamp, so new keys sort after older ones and inserts behave much like an increasing integer while staying globally unique. ## Designs that balance both - **UUIDv7 keys** — on PostgreSQL 18+ with Django 6.1, `UUIDField(primary_key=True, db_default=UUID7())` keeps locality with UUID benefits. - **Integer key plus public UUID** — keep `BigAutoField` as the primary key for joins and foreign keys, and add `public_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)` for URLs and APIs. Internal joins stay narrow; outsiders never see sequential ids. - **Plain `BigAutoField`** — right for internal tables that never appear in URLs and are created only by the application's own database. ## Django details that come with UUID keys - **URLs** — the built-in `uuid` path converter, as in `path("orders/<uuid:pk>/", ...)`, matches the hyphenated form and passes a `uuid.UUID` to the view. - **Lookups** — on PostgreSQL and MariaDB, where the column is a native `uuid`, the text lookups `iexact`, `contains`, `icontains`, `startswith`, `istartswith`, `endswith` and `iendswith` do not match values written without hyphens. - **Fixtures and serialisation** — keys appear as strings, so hand-written fixtures must use valid UUID text. - **Ordering** — `order_by("id")` on random v4 keys returns an arbitrary-looking order; sort by a timestamp column instead, or use v7 keys where time order is wanted. ## Judging it in an interview A strong answer names the reason for the UUID (exposure, offline creation or merging), shows the callable default, quantifies the width and index effects, and offers v7 or the integer-plus-public-id pattern. Changing the key type of an existing, populated table is a separate and much harder migration problem.
- What goes wrong with UUIDField(primary_key=True, default=uuid.uuid4())?The parentheses call `uuid4()` once, when the model module is imported, so the default is a single fixed UUID. The first insert succeeds and the second fails with an integrity error on the primary key. Pass the callable `uuid.uuid4` so Django calls it for each new instance.
- Does a UUID primary key remove the need for permission checks on a detail URL?No. It makes ids hard to guess, but anyone who obtains a link can still use it, and ids leak through logs, referrers and shared screenshots. Access control must still check that the requester may see the object; the UUID only removes easy enumeration.
Random UUID keys are like filing each new folder at a random spot in an already full cabinet, forcing drawers to be reshuffled; increasing or time-ordered keys are like always adding the new folder at the back of the last drawer.
saying these in an interview costs you the question
- default=uuid.uuid4() is the right way to generate a new UUID per row.
- UUID keys cost nothing extra because a UUID is stored as a number.
- Random v4 UUIDs insert into the key index as efficiently as increasing integers.
- A UUID key in the URL makes authorization checks unnecessary.
- Every backend stores UUIDField in a native 16-byte uuid column.