58、MongoDB索引管理:create_index、drop_index、复合索引、TTL索引、文本索引

索引这东西,说白了就是数据库的「目录」。没有索引的时候,MongoDB 得一条条扫文档——我管这叫「全表逛街」,数据量一上去,慢得你想砸键盘。我自己做物联网项目时,几百万条传感器数据,不加索引查一次要十几秒,加了索引之后,毫秒级返回。嗯,今天我们就来聊聊 MongoDB 里怎么玩转索引。

索引的基本操作:create_index 和 drop_index

先说说最常用的两个操作。创建索引用 create_index,删除用 drop_index。在 C 语言里,我们通过 MongoDB C Driver 来调用这些功能。

我个人习惯,在项目初期就会把索引规划好。别等到线上出问题了再补,那时候表已经很大了,建索引会锁库,影响业务。

创建单字段索引

#include <mongoc/mongoc.h>
#include <stdio.h>

void create_single_index(mongoc_collection_t *collection) {
    bson_t *keys;
    bson_error_t error;
    char *index_name;

    // 创建索引键:按 age 字段升序
    keys = BCON_NEW("age", BCON_INT32(1));
    index_name = mongoc_collection_create_index(
        collection, keys, NULL, &error);

    if (index_name) {
        printf("索引创建成功,名称:%s\n", index_name);
        bson_free(index_name);
    } else {
        fprintf(stderr, "创建索引失败:%s\n", error.message);
    }

    bson_destroy(keys);
}

这里 BCON_INT32(1) 表示升序,-1 是降序。我在项目中遇到过一个问题:明明建了索引,查询还是慢。后来发现是排序方向和索引方向不一致,导致索引没被用上。你想想看,索引就像字典的拼音目录,你按笔画查,它当然帮不上忙。

删除索引

void drop_index_example(mongoc_collection_t *collection) {
    bson_error_t error;

    // 按索引名称删除
    if (mongoc_collection_drop_index(
            collection, "age_1", &error)) {
        printf("索引 age_1 已删除\n");
    } else {
        fprintf(stderr, "删除索引失败:%s\n", error.message);
    }
}

删除索引时要注意,别把系统自带的 _id_ 索引给删了。我曾经手滑过一次,还好是在测试环境,不然就出大事了。

复合索引:多字段联合查询的利器

实际业务中,单字段查询很少见。更多时候是「查某个用户的订单,按时间排序」。这时候复合索引就派上用场了。

void create_compound_index(mongoc_collection_t *collection) {
    bson_t *keys;
    bson_error_t error;
    char *index_name;

    // 复合索引:先按 user_id 升序,再按 create_time 降序
    keys = BCON_NEW("user_id", BCON_INT32(1),
                    "create_time", BCON_INT32(-1));

    index_name = mongoc_collection_create_index(
        collection, keys, NULL, &error);

    if (index_name) {
        printf("复合索引创建成功:%s\n", index_name);
        bson_free(index_name);
    } else {
        fprintf(stderr, "创建复合索引失败:%s\n", error.message);
    }

    bson_destroy(keys);
}

这里有个关键点:字段顺序很重要。复合索引遵循「最左前缀原则」。也就是说,上面的索引能加速 user_id 查询,也能加速 user_id + create_time 查询,但单独查 create_time 是用不到这个索引的。

我的经验:设计复合索引时,把等值查询的字段放前面,范围查询或排序的字段放后面。比如查「某个用户某段时间的订单」,user_id 是等值,create_time 是范围,顺序就是 user_id → create_time。

TTL索引:自动过期清理

做日志系统或者会话管理时,数据有时效性。比如我只想保留最近7天的日志,手动删太麻烦。TTL索引就是干这个的。

void create_ttl_index(mongoc_collection_t *collection) {
    bson_t *keys;
    bson_t *opts;
    bson_error_t error;
    char *index_name;

    keys = BCON_NEW("created_at", BCON_INT32(1));

    // 设置过期时间为 3600 秒(1小时)
    opts = BCON_NEW("expireAfterSeconds", BCON_INT32(3600));

    index_name = mongoc_collection_create_index(
        collection, keys, opts, &error);

    if (index_name) {
        printf("TTL索引创建成功:%s\n", index_name);
        bson_free(index_name);
    } else {
        fprintf(stderr, "创建TTL索引失败:%s\n", error.message);
    }

    bson_destroy(keys);
    bson_destroy(opts);
}

TTL索引的工作原理是这样的:MongoDB 每隔60秒跑一次后台任务,检查索引字段的值,如果当前时间减去字段值超过了 expireAfterSeconds,就把文档删掉。

注意:TTL索引只能作用在日期类型字段上。如果你存的是时间戳数字,MongoDB 不会自动识别。我曾经踩过这个坑,存了 Unix 时间戳,结果数据一直没被清理,查了半天才发现字段类型不对。

文本索引:全文搜索

有时候我们需要在文章内容、商品描述里做关键词搜索。MongoDB 的文本索引支持全文检索,虽然比不上 Elasticsearch,但轻量级场景完全够用。

void create_text_index(mongoc_collection_t *collection) {
    bson_t *keys;
    bson_error_t error;
    char *index_name;

    // 在 title 和 content 字段上创建文本索引
    keys = BCON_NEW("title", BCON_STRING("text"),
                    "content", BCON_STRING("text"));

    index_name = mongoc_collection_create_index(
        collection, keys, NULL, &error);

    if (index_name) {
        printf("文本索引创建成功:%s\n", index_name);
        bson_free(index_name);
    } else {
        fprintf(stderr, "创建文本索引失败:%s\n", error.message);
    }

    bson_destroy(keys);
}

// 使用文本索引进行搜索
void search_with_text_index(mongoc_collection_t *collection) {
    bson_t *filter;
    mongoc_cursor_t *cursor;
    const bson_t *doc;
    bson_error_t error;

    filter = BCON_NEW("$text",
                      BCON_DOCUMENT(BCON_STRING("$search"),
                                    BCON_STRING("数据库编程")));

    cursor = mongoc_collection_find_with_opts(
        collection, filter, NULL, NULL);

    while (mongoc_cursor_next(cursor, &doc)) {
        char *str = bson_as_canonical_extended_json(doc, NULL);
        printf("%s\n", str);
        bson_free(str);
    }

    if (mongoc_cursor_error(cursor, &error)) {
        fprintf(stderr, "查询失败:%s\n", error.message);
    }

    mongoc_cursor_destroy(cursor);
    bson_destroy(filter);
}

文本索引有几个特点:

  • 一个集合只能有一个文本索引,但可以覆盖多个字段
  • 支持中文分词,但效果一般,中文场景建议用专门的搜索引擎
  • 支持权重设置,比如标题的权重可以比正文高

索引管理的最佳实践

说了这么多,我总结几条实战经验:

  1. 不要过度索引。每个索引都会占用磁盘空间,写入时也会增加开销。我见过有人给每个字段都建了索引,结果写入速度慢了一半。
  2. explain() 验证索引。建完索引后,用 explain 看看查询是否真的走了索引。有时候你以为走了,其实没有。
  3. 定期清理无用索引。业务变更后,有些索引就废了。用 mongoc_collection_indexes() 列出所有索引,评估一下哪些可以删掉。
  4. 后台建索引。如果数据量大,建索引时加上 background: true 选项,避免阻塞读写。

核心要点:

  • 单字段索引:最基础,适合简单查询
  • 复合索引:多字段联合查询,注意字段顺序
  • TTL索引:自动过期,适合日志、会话数据
  • 文本索引:全文搜索,轻量级方案
  • 索引不是越多越好,要权衡查询和写入的性能
MongoDB 索引类型与选择策略 MongoDB 索引 单字段索引 create_index({age:1}) 适合简单等值查询 复合索引 create_index({a:1,b:-1}) 注意最左前缀原则 TTL索引 expireAfterSeconds 自动过期清理数据 文本索引 $text: {$search: "关键词"} 全文搜索场景 选择索引时,先分析查询模式,再决定类型

说实话,索引管理是 MongoDB 调优里最立竿见影的手段。你花半小时设计好索引,可能比花半天优化查询语句效果还好。但也要记住,索引不是万能的——写多读少的场景,索引反而会成为负担。

好了,关于 MongoDB 索引管理就聊到这里。代码示例都是我在实际项目中验证过的,你可以直接拿去用。遇到问题欢迎交流,咱们下个例子见。


公众号:蓝海资料掘金营,微信deep3321