Functional indexes
coreadvancedIndex(Lower("title"), name="...") indexes the RESULT of an expression, not the raw column — the database precomputes and indexes Lower(title) for every row, so a query filtering on Lower(title) (or __iexact) can actually use the index instead of computing the expression fresh for every row on every query.
Think of it as
A plain models.Index(fields=["title"]) speeds up queries that filter/sort on title AS STORED — but a query like filter(title__iexact="hello") has to lowercase EVERY row's title to compare, and no plain index on the raw column helps with that, since the index is built from the raw, not-lowercased values. A functional index flips this: it indexes Lower("title") directly, so the database already has the lowercased value precomputed and indexed — the moment a query's WHERE clause matches that same expression, the index becomes usable again, exactly like an ordinary column index would be.
What we're doing: Speed up case-insensitive email lookups (a very common auth pattern) with a functional index on the lowercased email.
- 6
- Lower("email") as a positional argument (not fields=["email"]) is what makes this a functional index — the database indexes the LOWERCASED value of every row's email.
- 7
- name="email_lower_idx" is required here — a functional index cannot auto-generate a name the way a plain field index can.
Why this works: Without this index, User.objects.filter(email__iexact="Person@Example.com") has to lowercase every single row's email before comparing — with a large user table, that means a full table scan on every login attempt; the functional index lets the database use an actual index lookup instead, since the lowercased value is already precomputed and indexed.
Adding a plain field index and expecting it to accelerate a case-insensitive (__iexact) query
Wrong
Better
What you see: EXPLAIN on the __iexact query still shows a sequential scan even with a plain index on the same column already in place — the index technically exists but the planner has no way to use it for this comparison.
Why: A plain index is built from the column's RAW stored values — a case-insensitive comparison needs to transform both sides (the stored value and the search term) before comparing, and a plain index has no transformed values to match against. Only a functional index built on the SAME transformation (Lower()) gives the planner something it can actually use for an __iexact lookup.
- every row's email — "Person@Example.com"
- Lower(email) precomputed — stored in the index itself
- filter(email__iexact=...) — matches the same expression — index used
Plain index vs functional index
Together
Remember: A functional index (Index(Lower("field"), name="...")) indexes an EXPRESSION's result, not the raw column — it only accelerates a query using that exact same expression (e.g. __iexact needs Lower(), not a plain field index). name= is required for any expression-based index.
See also: constraints and indexes · db default and integrity philosophy · indexes

