MongoDB 索引与性能
概述
MongoDB 索引是提升查询性能的关键机制。没有索引的查询会进行全表扫描(Collection Scan),在数据量大时性能极差。合理设计索引是 MongoDB 性能优化的核心。
ESR 规则
MongoDB 官方推荐索引设计遵循 ESR 原则:
- Equality — 等值匹配字段放在最前面
- Sort — 排序字段放在等值字段后面
- Range — 范围查询字段放在最后
基础概念
索引类型
| 索引类型 | 说明 | 适用场景 |
|---|---|---|
| 单字段索引 | 单个字段上的索引 | 常用查询条件 |
| 复合索引 | 多个字段的组合索引 | 多条件查询 |
| 多键索引 | 数组字段上的索引 | $in、$all 查询 |
| 文本索引 | 文本搜索索引 | $text 全文搜索 |
| 地理空间索引 | 2d/2dsphere 索引 | 位置查询 |
| 哈希索引 | 哈希值索引 | 哈希分片键 |
| TTL 索引 | 带过期时间的索引 | 自动删除过期数据 |
| 唯一索引 | 值必须唯一的索引 | email、username |
语法与代码
创建索引
php
<?php
declare(strict_types=1);
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->selectCollection('app_db', 'users');
// 单字段索引
$collection->createIndex(['email' => 1]); // 1=升序, -1=降序
$collection->createIndex(['name' => -1]);
// 复合索引 — ESR 规则
$collection->createIndex(['status' => 1, 'createdAt' => -1]);
// status 等值匹配在前, createdAt 排序在后
// 唯一索引
$collection->createIndex(['email' => 1], ['unique' => true]);
$collection->createIndex(['username' => 1], ['unique' => true, 'sparse' => true]);
// TTL 索引 — 文档在指定秒数后自动删除
$collection->createIndex(['createdAt' => 1], [
'expireAfterSeconds' => 3600 * 24 * 30, // 30天后删除
]);
// 部分索引 — 只索引满足条件的文档
$collection->createIndex(['status' => 1], [
'partialFilterExpression' => ['status' => ['$eq' => 'active']],
]);
// 多键索引 — 自动创建(数组字段)
// tags 数组字段自动创建多键索引
$collection->createIndex(['tags' => 1]);
// 文本索引
$collection->createIndex(['title' => 'text', 'content' => 'text'], [
'weights' => ['title' => 10, 'content' => 5],
'name' => 'text_search_index',
]);索引管理
php
<?php
declare(strict_types=1);
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->selectCollection('app_db', 'users');
// 列出所有索引
$indexes = $collection->listIndexes();
foreach ($indexes as $index) {
echo "索引名: {$index['name']}\n";
echo "键: " . json_encode($index['key']) . "\n";
echo "唯一: " . ($index['unique'] ? '是' : '否') . "\n";
echo "\n";
}
// 删除索引
$collection->dropIndex('email_1'); // 按名称删除
$collection->dropIndex(['email' => 1]); // 按键规范删除
$collection->dropIndexes(); // 删除所有索引(除 _id)
// 查看索引大小
$collection->aggregate([
['$indexStats' => []],
]);实战示例
使用 explain 分析查询
php
<?php
declare(strict_types=1);
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->selectCollection('app_db', 'orders');
// explain — 分析查询执行计划
$query = ['status' => 'completed', 'amount' => ['$gte' => 100]];
$sort = ['createdAt' => -1];
$explain = $collection->find($query, ['sort' => $sort])->explain();
// 解读 explain 结果
$stage = $explain->queryPlanner->winningPlan;
if (isset($stage->stage) && $stage->stage === 'COLLSCAN') {
echo "全表扫描 — 需要添加索引!\n";
} elseif (isset($stage->stage) && $stage->stage === 'IXSCAN') {
$indexName = $stage->indexName ?? 'unknown';
echo "使用了索引: {$indexName}\n";
}
// 检查是否使用了覆盖查询(Covered Query)
// 覆盖查询: 索引包含了查询需要的所有字段,无需回表
$explain = $collection->find(
['status' => 'active'],
['projection' => ['status' => 1, 'createdAt' => 1, '_id' => 0]]
)->explain();
// executionStats 包含详细执行统计
$explainWithStats = $collection->find($query)
->explain(MongoDB\Operation\Explain::VERBOSITY_ALL);
$stats = $explainWithStats->executionStats;
echo "执行时间: {$stats->executionTimeMillis}ms\n";
echo "扫描文档: {$stats->totalDocsExamined}\n";
echo "返回文档: {$stats->nReturned}\n";慢查询分析
php
<?php
declare(strict_types=1);
use MongoDB\Client;
class SlowQueryAnalyzer
{
private MongoDB\Driver\Database $database;
public function __construct(Client $client)
{
$this->database = $client->selectDatabase('admin');
}
/**
* 获取慢查询日志
*/
public function getSlowQueries(int $thresholdMs = 100): array
{
$result = $this->database->command([
'getLog' => 'slow',
]);
$slowQueries = [];
foreach ($result->toArray()[0]['log'] ?? [] as $entry) {
if (($entry['millis'] ?? 0) > $thresholdMs) {
$slowQueries[] = [
'operation' => $entry['op'] ?? 'unknown',
'namespace' => $entry['ns'] ?? '',
'millis' => $entry['millis'],
'query' => json_encode($entry['query'] ?? []),
'timestamp' => $entry['ts'] ?? '',
];
}
}
return $slowQueries;
}
/**
* 分析集合的索引使用情况
*/
public function analyzeCollection(string $db, string $collection): array
{
$result = $this->database->command([
'aggregate' => $collection,
'pipeline' => [['$indexStats' => []]],
'cursor' => new stdClass(),
], ['database' => $db]);
return $result->toArray();
}
}索引策略管理类
php
<?php
declare(strict_types=1);
use MongoDB\Client;
class IndexManager
{
private MongoDB\Driver\Collection $collection;
private array $requiredIndexes = [];
public function __construct(
Client $client,
string $dbName,
string $collectionName,
array $requiredIndexes = []
) {
$this->collection = $client->selectCollection($dbName, $collectionName);
$this->requiredIndexes = $requiredIndexes;
}
/**
* 同步索引 — 创建缺少的索引
*/
public function syncIndexes(): array
{
$existingIndexes = $this->getExistingIndexMap();
$created = [];
foreach ($this->requiredIndexes as $indexDef) {
$keyJson = json_encode($indexDef['key']);
$indexName = $indexDef['name'] ?? $this->generateIndexName($indexDef['key']);
if (!isset($existingIndexes[$keyJson])) {
$options = $indexDef['options'] ?? [];
if (!isset($options['name'])) {
$options['name'] = $indexName;
}
$this->collection->createIndex($indexDef['key'], $options);
$created[] = $indexName;
echo "创建索引: {$indexName}\n";
}
}
return $created;
}
/**
* 创建索引(后台创建,不阻塞)
*/
public function createInBackground(array $keys, array $options = []): string
{
$options['background'] = true;
$result = $this->collection->createIndex($keys, $options);
return $result;
}
private function getExistingIndexMap(): array
{
$map = [];
foreach ($this->collection->listIndexes() as $index) {
$map[json_encode((array) $index['key'])] = $index['name'];
}
return $map;
}
private function generateIndexName(array $keys): string
{
$parts = [];
foreach ($keys as $field => $direction) {
$parts[] = $field . '_' . $direction;
}
return implode('_', $parts);
}
}
// 使用示例
$indexManager = new IndexManager($client, 'app_db', 'users', [
['key' => ['email' => 1], 'options' => ['unique' => true, 'name' => 'idx_email_unique']],
['key' => ['status' => 1, 'createdAt' => -1], 'options' => ['name' => 'idx_status_created']],
['key' => ['name' => 1], 'options' => ['name' => 'idx_name']],
['key' => ['createdAt' => 1], 'options' => ['expireAfterSeconds' => 2592000, 'name' => 'idx_created_ttl']],
]);
$indexManager->syncIndexes();注意事项
索引的代价
php
<?php
// 索引带来查询性能提升,但也有代价:
// 1. 写入开销 — 每次插入/更新/删除都需要更新索引
// 索引越多,写入越慢
// 建议: 单个集合索引不超过 5-6 个
// 2. 内存占用 — 索引存储在内存中
// 大索引会占用大量 RAM
// 使用 $indexStats 检查索引大小和使用频率
// 3. 磁盘空间 — 索引文件占据额外磁盘空间
// $collStats 可以查看索引大小
// 4. 创建耗时 — 大集合创建索引需要时间
// 生产环境使用 background: true 后台创建生产环境创建索引
在生产环境创建索引时,使用 background: true 选项避免阻塞其他操作。对于大集合,考虑在维护窗口期间创建。
最佳实践
1. 复合索引设计
php
<?php
// ESR 规则示例
// 查询: db.users.find({status: 'active', age: {$gt: 18}}).sort({createdAt: -1})
// 最佳索引设计:
$collection->createIndex([
'status' => 1, // E: 等值条件在前
'createdAt' => -1, // S: 排序在中
'age' => 1, // R: 范围条件在后
]);
// 反面教材:
// 错误 — 范围字段在排序字段之前
$collection->createIndex(['status' => 1, 'age' => 1, 'createdAt' => -1]);
// 这会导致排序无法使用索引2. 查询优化清单
php
<?php
// 1. 所有查询条件字段是否有索引?
// 2. 是否存在全表扫描?使用 explain 检查
// 3. 排序字段是否在索引中?
// 4. 是否可以使用覆盖查询?
// 5. $or 查询每个分支是否都有索引?
// 6. 投影是否排除了不需要的字段?
// 7. 是否正确使用了 limit/skip 分页?
// 8. 大数据集是否使用了 allowDiskUse?3. 分页优化
php
<?php
// 方式1: skip + limit(小数据量)
$cursor = $collection->find([], ['skip' => 100, 'limit' => 20, 'sort' => ['_id' => 1]]);
// 方式2: 基于游标的分页(大数据量,推荐)
$lastId = null;
$pageSize = 20;
if ($lastId) {
$cursor = $collection->find(
['_id' => ['$gt' => new ObjectId($lastId)]],
['limit' => $pageSize, 'sort' => ['_id' => 1]]
);
} else {
$cursor = $collection->find(
[],
['limit' => $pageSize, 'sort' => ['_id' => 1]]
);
}进阶用法
调试与测试技巧
php
<?php
declare(strict_types=1);
// 单元测试辅助函数
function createTestResource(): mixed
{
return match (true) {
default => new stdClass(),
};
}
// 调试输出函数
function debugOutput(mixed , string = ''): void
{
= ? ": " : '';
.= print_r(, true);
fwrite(STDERR, . "\n");
}
// 性能基准测试
function benchmark(callable , int = 1000): float
{
= hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$fn();
}
return (hrtime(true) - $start) / 1e9;
}日志记录实践
php
<?php
declare(strict_types=1);
/**
* 简易日志记录器
*/
class SimpleLogger
{
private string $logFile;
private string $level = 'INFO';
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
private function log(string $level, string $message, array $context): void
{
$timestamp = date('Y-m-d H:i:s');
$contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}配置与环境检测
php
<?php
declare(strict_types=1);
// 环境检测工具
class EnvironmentChecker
{
public static function checkRequirements(array $requirements): array
{
$results = [];
foreach ($requirements as $name => $check) {
$results[$name] = is_callable($check) ? $check() : false;
}
return $results;
}
public static function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'sapi' => PHP_SAPI,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'loaded_extensions' => get_loaded_extensions(),
];
}
}常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 网络问题/配置错误 | 检查配置,增加超时时间 |
| 权限不足 | 文件/目录权限 | 使用 chmod/chown 修正 |
| 性能下降 | 索引缺失/数据量大 | 添加索引,优化查询 |
| 数据不一致 | 并发冲突/事务残留 | 使用锁机制和事务 |
| 内存溢出 | 大数据集/未释放资源 | 增大内存限制,分批处理 |
故障排除步骤
- 检查错误日志和异常信息
- 确认配置和环境是否正确
- 使用调试工具逐步排查
- 参考官方文档查找已知问题
版本兼容性说明
| 功能 | 最低版本 | 说明 |
|---|---|---|
| 基础功能 | PHP 8.1 | 本文档基准版本 |
| 只读属性 | PHP 8.1 | public readonly 修饰符 |
| 枚举类型 | PHP 8.1 | enum 类型和 match 表达式 |
| Fiber | PHP 8.1 | 协程/轻量级并发 |
| 命名参数 | PHP 8.0 | foo(arg_name: value) |
| 联合类型 | PHP 8.0 | `int |
| Null 安全运算符 | PHP 8.0 | $obj?->method() |
| 析构器 promotion | PHP 8.0 | __construct(public $x) |
php
<?php
declare(strict_types=1);
// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
if (version_compare(PHP_VERSION, $minVersion, '<')) {
throw new RuntimeException(
sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
);
}
}
ensureVersion('8.1.0');