引言
缓慢的数据库查询是 Laravel 应用性能的隐形杀手。多数开发者往往止步于主键与外键,忽视了更丰富的索引手段。Eloquent 让数据交互更优雅,但未优化的查询会在高并发场景下拖垮再强健的系统。本文围绕 Laravel 场景,系统讲解进阶索引策略:从复合索引、部分索引,到覆盖索引与专项优化,帮助你把“迟钝”的查询调校到“闪电”级别。不论你正面向百万行数据,还是准备迎战大促流量,这些经过实战验证的技巧都能让你的 Eloquent 查询保持巅峰状态。
理解数据库索引基础
在动手实现前,先厘清索引在 Laravel 环境下的基本原理:
Laravel 中的关键索引原则:
复合索引是优化复杂 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']);
});
进阶实践要点:
真实示例:
// 查询:
Order::where('status', 'completed')
->whereBetween('created_at', [now()->subMonth(), now()])
->orderBy('total', 'desc')
->get();
// 最优复合索引:
// 注意范围列与排序列的放置顺序
$table->index(['status', 'created_at', 'total']);
部分索引仅为数据子集建索引,从而降低索引体积、提升效率。
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');
}
适用场景:
覆盖索引包含查询所需的全部列,使数据库无需回表读取数据页。
实现示例:
// 查询:
Order::select('id', 'total')
->where('user_id', 123)
->where('status', 'completed')
->get();
// 最优覆盖索引:
$table->index(['user_id', 'status', 'total', 'id']);
实践建议:
验证示例:
EXPLAIN SELECT id, total
FROM orders
WHERE user_id = 123 AND status = 'completed';
-- 在 Extra 列关注 "Using index"
现代 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();
大表分页若缺少合适索引,性能会明显下滑。
基于游标的分页(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();
进一步建议:
围绕常见关系模式进行针对性索引优化。
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']);
});
索引需要持续维护,才能保持效能。
自动分析示例:
// 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}");
}
// 可扩展:重复索引检测(对比索引定义)
}
}
维护建议:
配合 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();
进阶技巧:
面向日志、分析等时间密集型表的专项优化。
时间分区索引:
-- 日级分区
CREATE INDEX idx_analytics_date ON analytics (date);
-- 小时聚合
CREATE INDEX idx_analytics_hour ON analytics ((date_trunc('hour', created_at)));
时序优化建议:
Laravel 封装:
// 面向时间范围的查询作用域
public function scopeLastWeek(Builder $query)
{
return $query->whereBetween('created_at', [now()->subWeek(), now()]);
}
// 需对 created_at 建索引
为位置检索优化查询性能。
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();
优化提示:
针对低基数字段的特别考量。
布尔字段:
-- 仅在分布偏斜(如 95% 为 false)时索引
CREATE INDEX idx_users_premium ON users (is_premium) WHERE is_premium = true;
枚举字段:
不建议建索引的场景:
搭配 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 上的索引配合
通过主动监控发现优化机会。
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);
}
});
}
}
关键监控指标:
优化子查询、联合查询等高级模式。
子查询优化:
// 查询:
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)
进阶模式:
基于查询模式自动提出索引建议。
实现示例:
// 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
上线前请确认:
问:每张表该有多少索引?
答:没有放之四海而皆准的数字,可参考经验:
请聚焦最关键的查询模式,而非“能建就建”。
问:何时用复合索引,何时用单列索引?
答:
问:如何判断索引是否被使用?
答:
问:索引太多的性能影响?
答:每增加一个索引通常会:
请在读写之间权衡,并以应用的实际负载为准。
掌握索引优化,能把你的 Laravel 应用从“能用”推向“极致”。上述 15 种策略覆盖了从常规到进阶的真实场景,但最优解永远因数据分布与查询模式而异,没有“一招鲜”。
通往性能峰值的道路,是持续监测、迭代调优与严谨度量的过程。从最慢的查询入手,本周落地一个索引策略,衡量它的收益,再逐步扩展。
准备好给你的 Laravel 数据库提速了吗?挑一个本文策略试试,并观察效果。如果有收获,欢迎在评论区分享你的索引优化案例,订阅以获取更多 Laravel 性能优化实践!
@2025-08/