Laravel Eloquent:15 种数据库索引策略,打造闪电般的查询速度
引言
缓慢的数据库查询是 Laravel 应用性能的隐形杀手。多数开发者往往止步于主键与外键,忽视了更丰富的索引手段。Eloquent 让数据交互更优雅,但未优化的查询会在高并发场景下拖垮再强健的系统。本文围绕 Laravel 场景,系统讲解进阶索引策略:从复合索引、部分索引,到覆盖索引与专项优化,帮助你把“迟钝”的查询调校到“闪电”级别。不论你正面向百万行数据,还是准备迎战大促流量,这些经过实战验证的技巧都能让你的 Eloquent 查询保持巅峰状态。
理解数据库索引基础
在动手实现前,先厘清索引在 Laravel 环境下的基本原理:
- B-Tree 索引:多数列的默认索引(非常适合范围查询)
- Hash 索引:等值查询性能最佳(MySQL 仅 MEMORY 引擎可用)
- Full-Text 索引:用于文本搜索
- Spatial 索引:用于地理空间数据
Laravel 中的关键索引原则:
- 索引加速 WHERE、JOIN、ORDER BY、GROUP BY
- 每个索引都会增加 INSERT/UPDATE/DELETE 的开销
- 设计不当的索引可能被优化器忽略
- 选择性很重要(高基数字段更适合建索引)
策略 1:复合索引优化(Composite Index Optimization)
复合索引是优化复杂 Eloquent 查询最有力的武器。
基础实现:
// database/migrations/2025_07_27_create_orders_table.php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->decimal('total', 10, 2);
$table->timestamp('created_at')->useCurrent();
// 针对常见查询模式的复合索引
$table->index(['user_id', 'created_at']);
});进阶实践要点:
- 按选择性由高到低排列列顺序
- WHERE 中使用的列优先放前面
- 范围查询列放在索引的最后
- 通过包含所有需要的列来争取索引覆盖(index-only scan)
真实示例:
// 查询:
Order::where('status', 'completed')
->whereBetween('created_at', [now()->subMonth(), now()])
->orderBy('total', 'desc')
->get();
// 最优复合索引:
// 注意范围列与排序列的放置顺序
$table->index(['status', 'created_at', 'total']);策略 2:部分索引(Partial/Filtered Index)
部分索引仅为数据子集建索引,从而降低索引体积、提升效率。
MySQL 实现:
-- 仅为活跃用户建立索引(假设占比 50%)
CREATE INDEX idx_users_active ON users (email) WHERE active = 1;Laravel 迁移中的写法:
// database/migrations/2025_07_27_create_partial_indexes.php
public function up()
{
DB::statement('CREATE INDEX idx_orders_high_value ON orders (total) WHERE total > 1000');
DB::statement('CREATE INDEX idx_products_featured ON products (category_id) WHERE featured = true');
}适用场景:
- 频繁针对特定子集查询
- 布尔标记分布偏斜(如 95% 为 false)
- 需要专注优化最常见的查询模式
- 完整索引过大、超出内存
策略 3:覆盖索引(Covering Index)
覆盖索引包含查询所需的全部列,使数据库无需回表读取数据页。
实现示例:
// 查询:
Order::select('id', 'total')
->where('user_id', 123)
->where('status', 'completed')
->get();
// 最优覆盖索引:
$table->index(['user_id', 'status', 'total', 'id']);实践建议:
- 包含 SELECT、WHERE、ORDER BY 中涉及的所有列
- 列顺序:等值列 → 范围列 → 仅选择的列
- 控制索引尺寸(避免不必要的列)
- 使用 EXPLAIN 验证是否出现“Using index”
验证示例:
EXPLAIN SELECT id, total
FROM orders
WHERE user_id = 123 AND status = 'completed';
-- 在 Extra 列关注 "Using index"策略 4:JSON 列索引(Indexing JSON Columns)
现代 Laravel 项目常用 JSON 列,但其索引需要特别处理。
MySQL JSON 索引:
// 通过虚拟列实现可索引
$table->unsignedInteger('shipping_country')->virtualAs('JSON_EXTRACT(shipping_info, "$.country")');
$table->index('shipping_country');
// 或直接对 JSON 路径建索引(MySQL 5.7+)
DB::statement('ALTER TABLE users ADD INDEX idx_preferences_theme ((preferences->"$.theme"))');PostgreSQL JSONB 索引:
-- 对 JSONB 列使用 GIN 索引
CREATE INDEX idx_users_preferences ON users USING GIN (preferences);Laravel 查询优化:
// 不推荐:
User::whereRaw("JSON_EXTRACT(preferences, '$.theme') = ?", ['dark'])->get();
// 推荐利用可索引语法:
User::where('preferences->theme', 'dark')->get();策略 5:分页性能索引(Indexing for Pagination)
大表分页若缺少合适索引,性能会明显下滑。
基于游标的分页(Cursor Pagination)索引:
// 为 created_at 游标分页建索引
$table->index(['created_at', 'id']);
// 使用方式:
Post::where('published', true)
->orderBy('created_at', 'desc')
->orderBy('id', 'desc')
->cursorPaginate(15);Seek-Method 优化(传统 offset 分页):
// 针对传统分页
$table->index(['category_id', 'created_at']);
// 查询:
Product::where('category_id', 5)
->orderBy('created_at', 'desc')
->offset(50000)
->limit(25)
->get();进一步建议:
- 优先采用 keyset(游标)分页代替 offset
- 为分页列做索引覆盖
- 超大规模数据可考虑物化视图
- 为不同排序方向建立独立索引
策略 6:为 Eloquent 关系建索引(Relationships)
围绕常见关系模式进行针对性索引优化。
BelongsTo 关系:
// 外键一律索引
$table->foreignId('category_id')->constrained()->index();Has-Many-Through 关系:
// 例如 Country → Users → Orders
$table->index(['country_id', 'created_at']);
$table->index(['user_id', 'country_id']); // 反向检索多态关系(Polymorphic):
$table->string('commentable_type');
$table->unsignedBigInteger('commentable_id');
$table->index(['commentable_type', 'commentable_id']);多对多中间表(Pivot):
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('user_id')->constrained()->index();
$table->foreignId('role_id')->constrained()->index();
// 复合唯一约束
$table->unique(['user_id', 'role_id']);
// 反向检索的额外索引
$table->index(['role_id', 'user_id']);
});策略 7:索引运维(Index Maintenance)
索引需要持续维护,才能保持效能。
自动分析示例:
// app/Console/Commands/AnalyzeIndexes.php
public function handle()
{
$tables = DB::select('SHOW TABLES');
foreach ($tables as $table) {
$tableName = reset((array)$table);
$this->analyzeTableIndexes($tableName);
}
}
protected function analyzeTableIndexes(string $table)
{
$indexes = DB::select("SHOW INDEX FROM {$table}");
foreach ($indexes as $index) {
// 低基数索引预警
if ($index->Seq_in_index == 1 && $index->Cardinality < 100) {
$this->warn("Low cardinality index on {$table}.{$index->Column_name}");
}
// 可扩展:重复索引检测(对比索引定义)
}
}维护建议:
- 定期执行 ANALYZE TABLE
- 使用 OPTIMIZE TABLE 重建碎片化索引
- 结合数据库工具监控索引使用率
- 清理未使用索引(它们会拖慢写入)
策略 8:全文检索索引(Full-Text Search)
配合 Laravel Scout 或原生全文检索,正确的索引至关重要。
MySQL 全文索引:
// 迁移
$table->text('content');
$table->fullText('content');
// 使用 whereFullText
Post::whereFullText('content', 'Laravel')->get();PostgreSQL 全文索引:
-- 使用 GIN 索引
CREATE INDEX idx_posts_search ON posts USING GIN (to_tsvector('english', content));
-- 在 Laravel 中配合 whereRaw
-- Post::whereRaw("to_tsvector('english', content) @@ to_tsquery('english', ?)", ['laravel'])->get();进阶技巧:
- 为词干化与非词干化文本分列
- 使用 trigram 索引做模糊匹配
- 与常规索引组合用于过滤
- 复杂检索需求可引入 Meilisearch、Algolia 等专用引擎
策略 9:时序数据索引(Time-Series Data)
面向日志、分析等时间密集型表的专项优化。
时间分区索引:
-- 日级分区
CREATE INDEX idx_analytics_date ON analytics (date);
-- 小时聚合
CREATE INDEX idx_analytics_hour ON analytics ((date_trunc('hour', created_at)));时序优化建议:
- PostgreSQL 可采用 BRIN 索引
- 实施按时间维度的分片
- 为常见时间范围查询建立覆盖索引
- 规模极大时考虑专用时序数据库
Laravel 封装:
// 面向时间范围的查询作用域
public function scopeLastWeek(Builder $query)
{
return $query->whereBetween('created_at', [now()->subWeek(), now()]);
}
// 需对 created_at 建索引策略 10:地理空间索引(Geospatial)
为位置检索优化查询性能。
MySQL 空间索引:
// 迁移
$table->point('location')->spatialIndex();
// 查询
Location::whereRaw('ST_Distance(location, POINT(?, ?)) < ?', [
$lat, $lng, 5000 // 5km
])->get();PostgreSQL(PostGIS):
-- GIST 索引
CREATE INDEX idx_locations_gix ON locations USING GIST (location);
-- 在 Laravel 中配合 whereRaw
-- Location::whereRaw('ST_DWithin(location, ST_MakePoint(?, ?)::geography, 5000)', [
-- $lng, $lat
-- ])->get();优化提示:
- 使用与数据匹配的 SRID
- 先做 bounding box 预过滤
- 简化场景可采用 geohash
- 空间索引成本高,仅在必要时启用
策略 11:枚举与布尔字段索引(Enum/Boolean)
针对低基数字段的特别考量。
布尔字段:
-- 仅在分布偏斜(如 95% 为 false)时索引
CREATE INDEX idx_users_premium ON users (is_premium) WHERE is_premium = true;枚举字段:
- 大集合场景避免 enum,优先外键表
- 小集合可考虑部分索引
- 与高基数列组合成复合索引
不建议建索引的场景:
- 极低基数字段(如性别)
- 极少出现在 WHERE 条件
- 分布高度偏斜且未采用部分索引
策略 12:软删除与索引(Soft Deletes)
搭配 Laravel 软删除时的索引策略。
基础实现:
// 在相关索引中包含 deleted_at
$table->index(['user_id', 'deleted_at']);进阶实现:
// 仅针对未删除数据的部分索引
DB::statement('CREATE INDEX idx_orders_active ON orders (user_id) WHERE deleted_at IS NULL');
// 常见查询模式的复合索引
$table->index(['status', 'deleted_at', 'created_at']);查询优化:
// 不建议:
Order::withTrashed()->where('user_id', 123)->get();
// 建议:
Order::where('user_id', 123)->get(); // 默认排除已软删
// 需要 user_id, deleted_at 上的索引配合策略 13:索引监控与分析(Monitoring & Analysis)
通过主动监控发现优化机会。
MySQL 监控示例:
-- 查找未使用索引
SELECT
TABLE_NAME,
INDEX_NAME,
STATS_TIMESTAMP
FROM
information_schema.STATISTICS
WHERE
TABLE_SCHEMA = 'your_database'
AND INDEX_NAME != 'PRIMARY'
AND NOT EXISTS (
SELECT 1
FROM information_schema.TABLE_IO_WAITS_SUMMARY_BY_INDEX_USAGE
WHERE OBJECT_SCHEMA = 'your_database'
AND OBJECT_NAME = TABLE_NAME
AND INDEX_NAME = STATISTICS.INDEX_NAME
);与 Laravel 集成:
// app/Providers/AppServiceProvider.php
public function boot()
{
if ($this->app->isProduction()) {
DB::listen(function ($query) {
if (str_contains($query->sql, 'WHERE') && $query->time > 50) {
Log::warning('Slow query detected', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time
]);
// 可扩展:对该表触发索引分析
$this->analyzeQueryTable($query->sql);
}
});
}
}关键监控指标:
- Index hit ratio(命中率,>95% 为宜)
- 慢查询频率
- 索引体积增长
- 写入性能影响
策略 14:复杂 Eloquent 查询的索引(Subquery/Union 等)
优化子查询、联合查询等高级模式。
子查询优化:
// 查询:
Product::whereHas('orders', function ($query) {
$query->where('status', 'completed');
})->get();
// 需要在 orders 表上建立索引:
// [product_id, status] 支撑子查询
// [id] 支撑连接Union 查询索引:
// 联合查询
$active = User::where('active', true);
$premium = User::where('is_premium', true);
$users = $active->union($premium)->get();
// 需分别具备:
// idx_users_active (active)
// idx_users_premium (is_premium)进阶模式:
- 复杂 SELECT 尽量使用覆盖索引
- 多列条件尽量使用复合索引
- 极复杂查询可引入物化视图
- 将庞大查询拆解为多个可索引的简单子查询
策略 15:自动索引推荐系统(Automated Index Recommendation)
基于查询模式自动提出索引建议。
实现示例:
// app/Services/IndexAdvisor.php
class IndexAdvisor
{
protected $queryLog = [];
public function registerQuery(string $sql, array $bindings, float $time)
{
$this->queryLog[] = [
'sql' => $sql,
'bindings' => $bindings,
'time' => $time,
'normalized' => $this->normalizeQuery($sql)
];
}
public function generateRecommendations(): array
{
$patterns = $this->analyzeQueryPatterns();
$recommendations = [];
foreach ($patterns as $pattern) {
if ($pattern['avg_time'] > 50 && $pattern['count'] > 100) {
$recommendations[] = $this->recommendIndexForPattern($pattern);
}
}
return $recommendations;
}
protected function normalizeQuery(string $sql): string
{
// 移除具体值,标准化空白
return preg_replace('/\'.*?\'|\d+/', '?', $sql);
}
}
// 结合 Laravel 查询监听
DB::listen(function ($query) {
app(IndexAdvisor::class)->registerQuery(
$query->sql,
$query->bindings,
$query->time
);
});部署流水线集成:
# deploy.sh
php artisan index:analyze
php artisan index:recommend > index_recommendations.txt
# 手动审核后再应用
cat index_recommendations.txt
read -p "Apply index recommendations? (y/n) " response
if [ "$response" = "y" ]; then
php artisan index:apply
fi索引策略检查清单(Checklist)
上线前请确认:
- 分析慢查询日志,锁定候选
- 使用 EXPLAIN 验证索引生效
- 在预发环境验证
- 监控写入性能影响
- 建立索引监控体系
- 安排定期索引维护
- 完整记录与文档化所有自定义索引
常见问题(FAQ)
问:每张表该有多少索引?
答:没有放之四海而皆准的数字,可参考经验:
- 小表(<10k 行):1–2 个
- 中表(10k–1M 行):3–5 个
- 大表(>1M 行):5–8 个
请聚焦最关键的查询模式,而非“能建就建”。
问:何时用复合索引,何时用单列索引?
答:
- 复合索引适用于:查询稳定地在多列同时过滤;单列选择性不高,但组合后选择性显著提升;希望利用覆盖索引
- 单列索引适用于:不同查询在不同列过滤;单列本身选择性很高;对写入性能非常敏感
问:如何判断索引是否被使用?
答:
- MySQL:EXPLAIN SELECT ...(关注 key/possible_keys)
- PostgreSQL:EXPLAIN ANALYZE SELECT ...
- Laravel:开启查询日志,观察引用的索引
- 同时结合数据库统计信息监控索引使用率
问:索引太多的性能影响?
答:每增加一个索引通常会:
- INSERT 变慢 1–5%
- UPDATE 变慢 1–10%(视涉及列而定)
- 占用额外磁盘空间(可达表大小的 20–100%)
- 还可能误导优化器作出欠佳选择
请在读写之间权衡,并以应用的实际负载为准。
结语
掌握索引优化,能把你的 Laravel 应用从“能用”推向“极致”。上述 15 种策略覆盖了从常规到进阶的真实场景,但最优解永远因数据分布与查询模式而异,没有“一招鲜”。
通往性能峰值的道路,是持续监测、迭代调优与严谨度量的过程。从最慢的查询入手,本周落地一个索引策略,衡量它的收益,再逐步扩展。
准备好给你的 Laravel 数据库提速了吗?挑一个本文策略试试,并观察效果。如果有收获,欢迎在评论区分享你的索引优化案例,订阅以获取更多 Laravel 性能优化实践!
@2025-08/