Skip to content

Indexes & Foreign Keys

Indexes improve query performance by allowing the database to find rows faster.

PropertyDescription
NameAuto-generated: {table}_{columns}[_unique]_index
UniqueEnforces uniqueness across indexed columns
OrderSort order: ASC (ascending) or DESC (descending)
ColumnsOne or more columns (composite index)
WherePartial index condition (optional)
Primary KeyIndicates this is the primary key index (read-only)

TypeDescriptionExample Use Case
Single-columnIndex on one columnuserId for user lookups
CompositeIndex on multiple columns(userId, createdOn) for filtering
UniquePrevents duplicate valuesEmail addresses
PartialIndex only rows matching a WHERE conditionActive records only
Primary KeyUnique identifier for each row (cannot be deleted)id column

Use case: Looking up users by email

Table: user
Column: email
Unique: true

Generated name: user_email_unique_index

This speeds up queries like:

SELECT * FROM "user" WHERE "email" = 'john@example.com'

Use case: Filtering orders by user and date range

Table: order
Columns: userId, createdOn
Order: ASC, DESC

Generated name: order_userId_createdOn_index

This speeds up queries like:

SELECT * FROM "order"
WHERE "userId" = 123
AND "createdOn" > '2024-01-01'
ORDER BY "createdOn" DESC

Use case: Preventing duplicate email addresses

Table: user
Column: email
Unique: true

This enforces uniqueness at the database level, preventing duplicate registrations.

Use case: Indexing only active users

Table: user
Column: lastName
Where: "isActive" = true

This creates a smaller, faster index that only includes active users.


Use the Where Clause field to create partial indexes:

Index only active users:

"isActive" = true

Index only recent orders:

"createdOn" > '2024-01-01'

Index non-null emails:

"email" IS NOT NULL

Index pending and processing orders:

"status" IN ('PENDING', 'PROCESSING')

Index high-value orders:

"total" > 1000

Indexes are automatically named using this pattern:

{tableName}_{column1}_{column2}_[unique_]index

Examples:

  • user_email_unique_index
  • order_userId_createdOn_index
  • product_sku_unique_index
  • orderItem_orderId_index

  • Primary keys (automatic)
  • Foreign keys (automatic with *Id naming)
  • Unique constraints (email, username)
  • Columns used in WHERE clauses frequently
  • Columns used in JOIN conditions
  • Columns used in ORDER BY
  • Columns used in GROUP BY
  • Columns with high selectivity (many unique values)
  • Small tables (< 1000 rows)
  • Columns with low selectivity (few unique values)
  • Columns that are rarely queried
  • Tables with high write volume (indexes slow down writes)

Foreign keys enforce referential integrity between tables.

PropertyDescription
NameAuto-generated: {table}_{column}_{refTable}_{refColumn}_fk
ColumnThe column in your table (must be BIGINT/BIGSERIAL)
Ref TableThe table being referenced
Ref ColumnThe column being referenced (typically id)
On DeleteAction when the referenced row is deleted
On UpdateAction when the referenced row is updated

ActionDescriptionUse Case
NO ACTIONPrevent delete/update if references exist (default)Preserve data integrity
RESTRICTLike NO ACTION, but checked immediatelyStrict integrity enforcement
CASCADEAutomatically delete/update all referencing rowsDependent data (e.g., order items)
SET NULLSet the foreign key column to NULLOptional relationships
SET DEFAULTSet the foreign key column to its default valueFallback to default reference

NO ACTION (default):

  • Most relationships
  • When you want to prevent accidental deletions
  • When child records should be explicitly handled

CASCADE:

  • Parent-child relationships where children are meaningless without parent
  • Example: Delete order → delete all order items
  • Example: Delete user → delete all user sessions

SET NULL:

  • Optional relationships
  • Example: Delete manager → set employee.managerId to NULL
  • Example: Delete category → set product.categoryId to NULL

RESTRICT:

  • When you need immediate constraint checking
  • Rare in practice (NO ACTION is usually sufficient)

Table: order
Column: userId
Ref Table: user
Ref Column: id
On Delete: CASCADE
On Update: NO ACTION

When a user is deleted, all their orders are automatically deleted.

Table: orderItem
Column: orderId
Ref Table: order
Ref Column: id
On Delete: CASCADE
On Update: NO ACTION

When an order is deleted, all its items are automatically deleted.

Table: employee
Column: managerId
Ref Table: employee
Ref Column: id
On Delete: SET NULL
On Update: NO ACTION

When a manager is deleted, their employees’ managerId is set to NULL.

Table: product
Column: categoryId
Ref Table: category
Ref Column: id
On Delete: NO ACTION
On Update: NO ACTION

Cannot delete a category if products still reference it.

User table:

id: BIGSERIAL, primary key
firstName: VARCHAR(30)
email: VARCHAR(255), unique

Order table:

id: BIGSERIAL, primary key
userId: BIGINT, foreign key to user.id
total: DECIMAL(10,2)
status: ENUM('PENDING','COMPLETED')

Foreign key:

order.userId → user.id
On Delete: CASCADE

One user can have many orders. Deleting a user deletes all their orders.

Order table:

id: BIGSERIAL, primary key
userId: BIGINT
total: DECIMAL(10,2)

OrderItem table:

id: BIGSERIAL, primary key
orderId: BIGINT, foreign key to order.id
productId: BIGINT, foreign key to product.id
quantity: INTEGER
price: DECIMAL(10,2)

Foreign keys:

orderItem.orderId → order.id (CASCADE)
orderItem.productId → product.id (NO ACTION)

One order can have many items. Deleting an order deletes all its items.

User table:

id: BIGSERIAL, primary key
firstName: VARCHAR(30)
email: VARCHAR(255)

Role table:

id: BIGSERIAL, primary key
name: VARCHAR(50)
description: TEXT

UserRole junction table:

id: BIGSERIAL, primary key
userId: BIGINT, foreign key to user.id
roleId: BIGINT, foreign key to role.id

Foreign keys:

userRole.userId → user.id (CASCADE)
userRole.roleId → role.id (CASCADE)

Unique composite index:

Columns: userId, roleId
Unique: true

This prevents duplicate role assignments and enables efficient lookups.

Employee table:

id: BIGSERIAL, primary key
firstName: VARCHAR(30)
managerId: BIGINT, nullable, foreign key to employee.id

Foreign key:

employee.managerId → employee.id
On Delete: SET NULL

An employee can have a manager (another employee). Deleting a manager sets their employees’ managerId to NULL.


For queries that filter by multiple columns, create composite indexes with the most selective column first:

Good:

Index: (userId, createdOn)
Query: WHERE userId = 123 AND createdOn > '2024-01-01'

Less optimal:

Index: (createdOn, userId)
Query: WHERE userId = 123 AND createdOn > '2024-01-01'

Include all columns needed by a query in the index to avoid table lookups:

Index: (userId, createdOn, status)
Query: SELECT status FROM order WHERE userId = 123 ORDER BY createdOn

The database can satisfy this query entirely from the index.

  • Monitor index usage with database analytics
  • Remove unused indexes (they slow down writes)
  • Rebuild indexes periodically on high-traffic tables
  • Use partial indexes for large tables with filtered queries

  1. Index foreign keys – Speeds up joins (automatic with *Id naming)
  2. Index unique constraints – Enforces uniqueness and speeds up lookups
  3. Use composite indexes – For multi-column queries
  4. Consider partial indexes – For large tables with filtered queries
  5. Monitor performance – Add indexes based on actual query patterns
  1. Always use foreign keys – Enforces referential integrity
  2. Choose appropriate actions – CASCADE for dependent data, NO ACTION for independent data
  3. Use SET NULL for optional relationships – Allows parent deletion without losing children
  4. Document relationships – Add comments explaining the relationship
  5. Test cascade behavior – Ensure deletions cascade as expected