# Indexing SwiftData models

## Contents

- [Declaring indexes with #Index](#declaring-indexes-with-index)
- [Deciding what to index](#deciding-what-to-index)
- [Indexes are schema changes](#indexes-are-schema-changes)
- [Checking that an index is used](#checking-that-an-index-is-used)

## Declaring indexes with #Index

`#Index` is available from iOS 18. Put it inside the `@Model` class body. Each
array you pass describes one index, so several arrays produce several indexes.

```swift
import SwiftData
import Foundation

@Model
final class Recording {
    #Index<Recording>([\.venue], [\.recordedAt], [\.isStarred, \.recordedAt])

    @Attribute(.unique) var recordingID: UUID
    var venue: String
    var recordedAt: Date
    var isStarred: Bool

    init(venue: String, recordedAt: Date, isStarred: Bool = false) {
        self.recordingID = UUID()
        self.venue = venue
        self.recordedAt = recordedAt
        self.isStarred = isStarred
    }
}
```

In that declaration:

- `[\.venue]` and `[\.recordedAt]` are two separate single-column indexes.
- `[\.isStarred, \.recordedAt]` is one compound index. It helps a query that
  filters on the first key and orders by the second, such as "starred
  recordings, newest first". Order the key paths the way the query uses them:
  filter column first, sort column second.

## Deciding what to index

An index speeds reads and costs something on every write and in store size.
Add one when the numbers say so.

Worth indexing:

- A property that appears in `#Predicate` filters over a large collection
  (roughly a thousand rows or more).
- A property used as a `SortDescriptor` key over a large collection.

Usually not worth it:

- Collections that stay small (a few hundred rows or fewer).
- Properties that are rarely part of a query.
- Properties that change very often; each change updates the index.
- Very low cardinality columns, such as a `Bool` on a small table.
- Queries already served by an existing uniqueness constraint or index, unless
  profiling shows otherwise.

Index the few hot paths, not every property.

## Indexes are schema changes

Adding or dropping an index changes the store schema. Test the change on a
store that looks like real user data before shipping. If the same release also
changes the model in ways lightweight migration cannot handle, keep a
`VersionedSchema` and `SchemaMigrationPlan` ready. Never assume an index change
needs no migration testing.

## Checking that an index is used

Profile with the Core Data template in Instruments; SwiftData's default store
is SQLite, and the template shows its fetches. A "Full Table Scan" warning on a
fetch that runs often means that fetch has no usable index. Re-run after adding
the index to confirm the warning is gone and the fetch time dropped.

Apple documentation: https://developer.apple.com/documentation/swiftdata/index(_:)
