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 查询最有力的武器。

基础实现:

php
// 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)

真实示例:

php
// 查询:
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 实现:

sql
-- 仅为活跃用户建立索引(假设占比 50%)
CREATE INDEX idx_users_active ON users (email) WHERE active = 1;

Laravel 迁移中的写法:

php
// 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)

覆盖索引包含查询所需的全部列,使数据库无需回表读取数据页。

实现示例:

php
// 查询:
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”

验证示例:

sql
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 索引:

php
// 通过虚拟列实现可索引
$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 索引:

sql
-- 对 JSONB 列使用 GIN 索引
CREATE INDEX idx_users_preferences ON users USING GIN (preferences);

Laravel 查询优化:

php
// 不推荐:
User::whereRaw("JSON_EXTRACT(preferences, '$.theme') = ?", ['dark'])->get();

// 推荐利用可索引语法:
User::where('preferences->theme', 'dark')->get();

策略 5:分页性能索引(Indexing for Pagination)

大表分页若缺少合适索引,性能会明显下滑。

基于游标的分页(Cursor Pagination)索引:

php
// 为 created_at 游标分页建索引
$table->index(['created_at', 'id']);

// 使用方式:
Post::where('published', true)
    ->orderBy('created_at', 'desc')
    ->orderBy('id', 'desc')
    ->cursorPaginate(15);

Seek-Method 优化(传统 offset 分页):

php
// 针对传统分页
$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 关系:

php
// 外键一律索引
$table->foreignId('category_id')->constrained()->index();

Has-Many-Through 关系:

php
// 例如 Country → Users → Orders
$table->index(['country_id', 'created_at']);
$table->index(['user_id', 'country_id']); // 反向检索

多态关系(Polymorphic):

php
$table->string('commentable_type');
$table->unsignedBigInteger('commentable_id');
$table->index(['commentable_type', 'commentable_id']);

多对多中间表(Pivot):

php
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)

索引需要持续维护,才能保持效能。

自动分析示例:

php
// 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 重建碎片化索引
  • 结合数据库工具监控索引使用率
  • 清理未使用索引(它们会拖慢写入)

配合 Laravel Scout 或原生全文检索,正确的索引至关重要。

MySQL 全文索引:

php
// 迁移
$table->text('content');
$table->fullText('content');

// 使用 whereFullText
Post::whereFullText('content', 'Laravel')->get();

PostgreSQL 全文索引:

sql
-- 使用 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)

面向日志、分析等时间密集型表的专项优化。

时间分区索引:

sql
-- 日级分区
CREATE INDEX idx_analytics_date ON analytics (date);

-- 小时聚合
CREATE INDEX idx_analytics_hour ON analytics ((date_trunc('hour', created_at)));

时序优化建议:

  • PostgreSQL 可采用 BRIN 索引
  • 实施按时间维度的分片
  • 为常见时间范围查询建立覆盖索引
  • 规模极大时考虑专用时序数据库

Laravel 封装:

php
// 面向时间范围的查询作用域
public function scopeLastWeek(Builder $query)
{
    return $query->whereBetween('created_at', [now()->subWeek(), now()]);
}
// 需对 created_at 建索引

策略 10:地理空间索引(Geospatial)

为位置检索优化查询性能。

MySQL 空间索引:

php
// 迁移
$table->point('location')->spatialIndex();

// 查询
Location::whereRaw('ST_Distance(location, POINT(?, ?)) < ?', [
    $lat, $lng, 5000 // 5km
])->get();

PostgreSQL(PostGIS):

sql
-- 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)

针对低基数字段的特别考量。

布尔字段:

sql
-- 仅在分布偏斜(如 95% 为 false)时索引
CREATE INDEX idx_users_premium ON users (is_premium) WHERE is_premium = true;

枚举字段:

  • 大集合场景避免 enum,优先外键表
  • 小集合可考虑部分索引
  • 与高基数列组合成复合索引

不建议建索引的场景:

  • 极低基数字段(如性别)
  • 极少出现在 WHERE 条件
  • 分布高度偏斜且未采用部分索引

策略 12:软删除与索引(Soft Deletes)

搭配 Laravel 软删除时的索引策略。

基础实现:

php
// 在相关索引中包含 deleted_at
$table->index(['user_id', 'deleted_at']);

进阶实现:

php
// 仅针对未删除数据的部分索引
DB::statement('CREATE INDEX idx_orders_active ON orders (user_id) WHERE deleted_at IS NULL');

// 常见查询模式的复合索引
$table->index(['status', 'deleted_at', 'created_at']);

查询优化:

php
// 不建议:
Order::withTrashed()->where('user_id', 123)->get();

// 建议:
Order::where('user_id', 123)->get(); // 默认排除已软删
// 需要 user_id, deleted_at 上的索引配合

策略 13:索引监控与分析(Monitoring & Analysis)

通过主动监控发现优化机会。

MySQL 监控示例:

sql
-- 查找未使用索引
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 集成:

php
// 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 等)

优化子查询、联合查询等高级模式。

子查询优化:

php
// 查询:
Product::whereHas('orders', function ($query) {
    $query->where('status', 'completed');
})->get();

// 需要在 orders 表上建立索引:
// [product_id, status] 支撑子查询
// [id] 支撑连接

Union 查询索引:

php
// 联合查询
$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)

基于查询模式自动提出索引建议。

实现示例:

php
// 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
    );
});

部署流水线集成:

bash
# 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/

JaguarJack
后端开发工程师,前端入门选手,略知相关服务器知识,偏爱❤️ Laravel & Vue
本作品采用《CC 协议》,转载必须注明作者和本文链接