Indexing Strategies
As your ledger_entries table grows, database indexing becomes essential for maintaining fast queries.
This guide explains which indexes to use, when to use them, and how to avoid common mistakes.
Why Indexing Matters
Ledgerly workloads typically involve:
- activity timelines
- dashboards
- exports
- date-range queries
- filtering by actor or target
Without proper indexes, large tables can slow down dramatically.
Indexes ensure:
- fast lookups
- predictable performance
- efficient pagination
- scalable exports
Recommended Core Indexes
These indexes are recommended for nearly all production deployments.
created_at
Most queries:
- latest entries
- date-range exports
- dashboards
Migration example:
$table->index('created_at');
This is the most important index.
action
Useful for:
- filtering by event type
- reporting
- analytics
Migration example:
$table->index('action');
severity
Useful for:
- alerts
- dashboards
- filtering
Migration example:
$table->index('severity');
Polymorphic Relationship Indexes
Ledgerly uses polymorphic relations for actors and targets.
Composite indexes are required for good performance.
Actor Index
Used for:
- user activity timelines
- filtering by actor
Migration example:
$table->index(['actor_type', 'actor_id']);
Target Index
Used for:
- model timelines
- audit views
Migration example:
$table->index(['target_type', 'target_id']);
Timeline Optimization
For high-performance timelines, add:
$table->index(['target_type', 'target_id', 'created_at']);
This improves:
- timeline queries
- chronological ordering
- pagination
Correlation ID Indexing
Correlation IDs are often stored in metadata.
Indexing strategy depends on the database.
MySQL
Use a generated column:
$table->string('correlation_id')
->virtualAs("json_unquote(json_extract(metadata, '$.correlation_id'))")
->nullable();
$table->index('correlation_id');
This allows fast queries using:
LedgerEntry::withinCorrelation($id);
PostgreSQL
Use an expression index:
CREATE INDEX ledger_entries_correlation_idx
ON ledger_entries ((metadata->>'correlation_id'));
This provides efficient lookups without schema changes.
Optional Indexes
These are useful in specific workloads.
Actor Timeline Optimization
$table->index(['actor_type', 'actor_id', 'created_at']);
Useful when:
- user activity views are common
- auditing user behavior
Action + Date Index
$table->index(['action', 'created_at']);
Useful for:
- analytics
- reporting
- filtering by action over time
Indexes to Avoid
Avoid indexing:
- diff column
- full metadata JSON
- low-value fields
These indexes:
- consume large amounts of storage
- slow down inserts
- rarely improve performance
Large Dataset Considerations
As tables grow into millions of rows:
- Always filter queries by date range
- Use cursor or chunk exports
- Avoid full-table scans
Good:
LedgerEntry::whereBetween('created_at', [...])->get();
Avoid:
LedgerEntry::all();
Monitoring Index Usage
In production, periodically check:
- slow query logs
- query execution plans
- index size
If an index is not used, consider removing it.
Migration Strategy
Indexes should be added via migrations, not manually.
Example migration:
Schema::table('ledger_entries', function (Blueprint $table) {
$table->index('created_at');
$table->index('action');
$table->index('severity');
$table->index(['actor_type', 'actor_id']);
$table->index(['target_type', 'target_id']);
});
Summary
For most applications, the following indexes are sufficient:
- created_at
- action
- severity
- actor_type + actor_id
- target_type + target_id
Add additional indexes only when necessary.
Proper indexing ensures Ledgerly remains fast and reliable even with millions of entries.