Database performance issues are often caused by missing or poorly configured indexes. Prisma provides a declarative way to define indexes directly in your schema. This guide covers everything you need to know about configuring indexes in Prisma, from basic single-column indexes to advanced PostgreSQL index types. For teams building modern web applications, proper database indexing is essential for achieving the performance levels users expect from professional web development services.
Understanding Database Indexes
Database indexes are data structures that allow the database to find records quickly without scanning every row in a table. Think of an index like a book's index--it helps you find information without reading every page. Without indexes, the database must perform full table scans to find data, which becomes increasingly slow as your data grows.
Indexes work by creating a sorted copy of the indexed columns along with pointers to the actual data rows. This sorted structure enables efficient binary searches, reducing query time from O(n) for full scans to O(log n) for indexed lookups.
How Indexes Affect Query Performance
It's important to understand the trade-off between read and write performance. While indexes improve read speed, they slow down write operations (INSERT, UPDATE, DELETE) because the indexes must be updated whenever the underlying data changes. Each index adds overhead to write operations, so it's crucial to balance the number of indexes against your application's read and write patterns.
When to Add Indexes
Primary keys and unique constraints get indexes automatically. Consider adding indexes to columns that appear in:
- WHERE clause conditions
- JOIN conditions
- ORDER BY clauses
- GROUP BY operations
- Foreign key relationships
Use your database's EXPLAIN ANALYZE feature to identify slow queries that could benefit from indexes. Focus on columns that appear frequently in your application's query patterns.
Basic Index Configuration with @@index
The @@index attribute is Prisma's primary mechanism for defining database indexes. You add it at the model level to specify which columns should be indexed. Prisma handles translating this declarative definition into the appropriate SQL CREATE INDEX statements. Our backend development services team regularly implements these patterns to optimize database performance for enterprise applications.
1model User {2 id Int @id @default(autoincrement())3 email String @unique4 name String5 role String6 7 @@index([email])8 @@index([role])9}In this example, we create indexes on the email and role columns. The email column already has a unique constraint (which implicitly creates an index), but adding an explicit @@index can be useful for queries that don't require uniqueness enforcement.
Composite Indexes
Composite indexes (also called multi-column indexes) are essential when queries filter on multiple columns together. They allow the database to efficiently retrieve rows that match conditions on multiple columns without having to perform expensive joins between separate indexes.
Important: The order of columns in a composite index matters significantly. The leftmost prefix principle means that an index on [columnA, columnB] can only be used for queries that include columnA, or queries that include both columnA and columnB. It cannot be used for queries that only filter on columnB.
1model Order {2 id Int @id @default(autoincrement())3 userId Int4 status String5 createdAt DateTime @default(now())6 7 @@index([userId, status])8}Advanced Index Configuration
Prisma supports several advanced index configuration options that let you fine-tune indexes for specific database engines and use cases. These options are available through attribute arguments on @@index, @@unique, @id, and @@id.
Configuring Index Length (MySQL)
The length argument is specific to MySQL and allows you to define indexes on a prefix of String and Bytes columns. MySQL has limits on the maximum size of index entries (767 bytes for InnoDB in older versions, 3072 bytes in newer versions with innodb_large_prefix). The length argument helps you work within these constraints.
This is particularly useful for long VARCHAR columns where you only need to index a prefix for efficient lookups, such as the first 50 characters of a URL slug or title.
1model Post {2 id Int @id3 title String @db.VarChar(300)4 slug String @db.VarChar(500)5 content String @db.Text6 7 @@index([title(length: 100)])8 @@index([slug(length: 150)])9}Configuring Sort Order
The sort argument allows you to specify whether index entries should be stored in ascending (ASC) or descending (DESC) order. This can improve performance for queries that sort data in a specific direction.
- MySQL/MariaDB: Supports sort order on unique constraints and indexes
- PostgreSQL: Supports sort order on indexes only (not unique constraints)
- SQL Server: Supports sort order on all constraints and indexes
1model Product {2 id Int @id3 name String4 price Decimal5 createdAt DateTime @default(now())6 7 @@index([price(sort: Desc)])8 @@index([createdAt(sort: Desc)])9}Index Type (PostgreSQL)
PostgreSQL supports different index access methods beyond the default B-Tree. The type argument allows you to specify alternative index types optimized for specific use cases.
B-Tree Indexes (Default)
B-Tree (Balanced Tree) is the default index type in PostgreSQL and most databases. B-Tree indexes are ideal for equality and range queries. They work efficiently with comparison operators (=, <, >, <=, >=) and are the recommended choice for most use cases.
B-Tree indexes maintain data in sorted order and allow efficient searches, range scans, and ORDER BY operations. They handle NULL values correctly and are the most versatile index type.
1model User {2 id Int @id3 email String @unique4 name String5 6 // B-Tree is the default, type: BTree is optional7 @@index([email])8}Hash Indexes
Hash indexes are optimized exclusively for equality comparisons (= and <>). They cannot be used for range queries (<, >, <=, >=) or for sorting operations. In exchange for this limitation, Hash indexes can be faster than B-Tree for exact match lookups.
Important considerations:
- Hash indexes are PostgreSQL-specific
- They don't support multi-column indexes
- Hash indexes currently don't get WAL (Write-Ahead Logging) in PostgreSQL, which affects durability in case of crashes
- Consider whether the performance gain justifies the limitations
1model Session {2 id String @id3 token String @unique4 userId Int5 expiresAt DateTime6 7 @@index([token], type: Hash)8 @@index([userId])9}GIN (Generalized Inverted Index)
GIN indexes are designed for composite values like arrays, JSONB data, and full-text search. They're essential for implementing search functionality that needs to find values within arrays or check for containment in JSON objects.
GIN indexes handle these query operators efficiently:
@>(contains)<@(contained by)?(key exists)?|(any key exists)?&(all keys exist)
Common use cases include storing tags or categories as arrays, JSON metadata columns, and implementing full-text search.
1model Document {2 id Int @id3 title String4 tags String[]5 metadata Json6 content String7 8 @@index([tags], type: Gin)9 @@index([metadata], type: Gin)10 @@index([title, content])11}BRIN (Block Range Index)
BRIN indexes are designed for very large tables with naturally correlated data, such as time-series data. They're dramatically smaller than B-Tree indexes (often less than 1% of the table size) while still providing meaningful query optimization.
BRIN works by dividing the table into blocks (ranges of physical pages) and storing summary information for each range. This makes BRIN ideal for columns where the physical order correlates with the indexed values, such as:
- Sequential timestamps (events logged in order)
- Incrementing IDs
- Sequentially assigned identifiers
BRIN provides a trade-off: smaller index size and faster writes, but less precise indexing than B-Tree.
1model EventLog {2 id BigInt @id3 timestamp DateTime @default(now())4 level String5 message String6 7 @@index([timestamp], type: Brin)8 @@index([level])9}Custom Index Names with Map
The map argument lets you specify a custom name for the index in the underlying database. By default, Prisma generates index names based on table and column names (e.g., User_email_idx). Custom names can be useful for:
- Consistency with existing naming conventions
- Readability in database documentation
- Referencing indexes in monitoring tools or database migrations
- Working with databases that have naming restrictions
1model Customer {2 id Int @id3 email String @unique4 companyId Int5 6 @@index([companyId], map: "idx_customer_company")7}Clustered Indexes (SQL Server)
SQL Server's clustered argument controls whether an index is clustered or non-clustered. A clustered index determines the physical order of data in a table--rows are stored on disk in the order of the clustered index. Each table can have only one clustered index (which is typically the primary key).
Non-clustered indexes are separate structures that contain the indexed values and pointers to the actual data rows. When you query using a non-clustered index, the database first finds the index entry, then uses the pointer to retrieve the actual row.
Key considerations:
- The clustered index should be on the most frequently accessed column(s)
- Avoid changing the clustered index frequently (it rewrites the entire table)
- Primary keys become clustered by default in SQL Server
1model Invoice {2 id Int @id3 invoiceNo String @unique4 createdAt DateTime @default(now())5 total Decimal6 7 @@index([invoiceNo], clustered: true)8}Full-Text Search Indexes
Prisma supports full-text search indexes through the @@fulltext attribute, available with the fullTextIndex preview feature. This is essential for implementing search functionality that needs to find words within text content rather than exact matches.
1model Article {2 id Int @id3 title String4 content String5 authorId Int6 published Boolean @default(false)7 8 @@fulltext([title, content])9}1const results = await prisma.article.findMany({2 where: {3 published: true,4 OR: [5 { title: { search: 'prisma | postgres' } },6 { content: { search: 'prisma | postgres' } }7 ]8 },9 select: { id: true, title: true }10});Best Practices for Prisma Indexes
Index Design Principles
-
Index based on query patterns: Use your application's actual query patterns to guide index decisions, not guesswork.
-
Order columns by selectivity: In composite indexes, put the most selective column (the one that filters out the most rows) first.
-
Consider leftmost prefix: Design composite indexes so they can be used by the widest range of queries.
-
Monitor and iterate: Use EXPLAIN ANALYZE to verify indexes are being used. Remove unused indexes to reduce write overhead.
-
Balance read and write performance: Too many indexes slow down writes. Find the right balance for your workload.
-
Profile before deploying: Test index changes with realistic data volumes and query patterns.
Common Anti-Patterns
What to avoid:
-
Indexing every column: Each index adds overhead to writes. Only index columns that appear in queries.
-
Redundant indexes: Indexes like
[columnA]and[columnA, columnB]are redundant for queries on justcolumnA. -
Ignoring column order: Wrong column order in composite indexes can make them unusable for many queries.
-
Low-cardinality columns: Indexing columns with few unique values (like boolean flags) rarely helps unless combined with other columns.
-
Forgetting to update indexes: Query patterns change over time. Review and update indexes periodically.
Monitoring Index Effectiveness
Use your database's query analysis tools to verify indexes are working:
- PostgreSQL:
EXPLAIN ANALYZEshows query plans and actual execution times - MySQL:
EXPLAINdisplays how queries use indexes - SQL Server: Include execution plans in SSMS to analyze index usage
Look for:
- Sequential table scans (indicates missing index)
- Index scans that return many rows (consider a more selective index)
- Unused indexes (consider removing to reduce write overhead)
Migrating Indexes with Prisma Migrate
Prisma Migrate handles index creation and modification automatically when you update your schema. The migration system generates SQL to create or modify indexes, ensuring database consistency.
Creating a new index:
- Add @@index to your model
- Run
prisma migrate dev - Prisma generates a migration file with CREATE INDEX statements
- Apply the migration to create the index
Modifying indexes: Some index modifications require dropping and recreating the index. Be aware that:
- Some index operations acquire locks that can block queries
- Consider using CONCURRENTLY option in PostgreSQL for online index creation (requires Prisma version with support)
- Plan index changes during maintenance windows for critical tables
1// Before migration2model Product {3 id Int @id4 name String5 price Decimal6}7 8// After adding indexes9model Product {10 id Int @id11 name String12 price Decimal13 sku String14 15 @@index([name])16 @@index([price])17 @@index([sku], type: Hash)18}Common Use Cases
User Authentication Indexes
1model User {2 id Int @id @default(autoincrement())3 email String @unique4 password String5 status String @default("active")6 lastLogin DateTime?7 createdAt DateTime @default(now())8 9 // For filtering active users10 @@index([status])11 // For "users who haven't logged in since X" queries12 @@index([lastLogin])13}E-Commerce Product Indexes
1model Product {2 id Int @id3 name String4 category String5 subcategory String?6 price Decimal7 inStock Boolean @default(true)8 rating Float?9 updatedAt DateTime @updatedAt10 11 // For category filtering12 @@index([category])13 // For price range queries14 @@index([price])15 // For "products in category within price range"16 @@index([category, price])17 // For "in-stock products in category"18 @@index([inStock, category])19 // For sorting by rating20 @@index([rating(sort: Desc)])21}Time-Series Data Indexes
1model EventLog {2 id BigInt @id @default(autoincrement())3 type String4 userId Int?5 sessionId String?6 timestamp DateTime @default(now())7 metadata Json?8 9 // BRIN for time-range queries on sequential data10 @@index([timestamp(type: Brin)])11 // For filtering by event type12 @@index([type])13 // For user activity history14 @@index([userId, timestamp])15 // For session-based queries16 @@index([sessionId, timestamp])17}Summary
Prisma provides comprehensive index configuration options through declarative schema attributes. The key takeaways are:
-
Start with the basics: Use @@index for single-column and composite indexes on frequently queried columns.
-
Know your database: Different databases support different index types and options. PostgreSQL offers the most variety (B-Tree, Hash, GIN, BRIN, etc.).
-
Use the right tool for the job: Match index types to your query patterns--equality searches can use Hash indexes, array/JSON queries benefit from GIN, and time-series data works well with BRIN.
-
Design composite indexes thoughtfully: Column order matters. Put the most selective column first and consider which queries will use the index.
-
Monitor and iterate: Use EXPLAIN ANALYZE to verify indexes are being used. Remove unused indexes to maintain write performance.
-
Consider trade-offs: Indexes improve read performance but add overhead to writes. Find the right balance for your specific workload.
By understanding and applying these index configuration techniques, you can significantly improve your application's database query performance. For teams working on complex web development projects that require scalable backend architecture, proper indexing is a foundational skill that directly impacts user experience and operational efficiency.
Prisma ORM Documentation
Official documentation for Prisma ORM, including schema reference and API documentation.
Learn moreDatabase Performance Guide
Learn strategies for optimizing database performance in modern web applications.
Learn moreAPI Development Services
Build scalable backend APIs with our expert development team.
Learn more