分享一个Mongo查询索引优化方案前言正文总结

前言

今天介绍一下我在工作中MongoDB数据表索引设计的思路,以供大家参考,前面有文章介绍过MongoDB查询语法及分表存储方案,不了解的朋友可以点击下面链接学习一下。

结合单表SQL快速学习Mongo操作语法

MongoDB分表存储查询方案

正文

我们就直接进入正题,结合实际业务介绍表设计及索引设计

业务背景

我们是一个监测系统,设备每分钟上传数据经过解析后存储到MongoDB中,数据中都会有设备的编号及时间信息(时间戳)及一些监测数据(气象相关)。

  • 精确查询(查询指定设备指定时间点的数据)
  • 范围查询(查询指定设备指定时间范围的数据)
  • 数据结构
# 原始单条数据内容
{
   "deviceCode": "xxx",
   "ts": 1632450060000,
   "temp": 27.1,
   "humidity": 0.77,
   "windDirection": 180,
   "windSpeed": 10
}
复制代码
字段 描述 单位
deviceCode 设备编码
ts 时间 ms
temp 温度
humidity 湿度 %
windDirection 风向 °
windSpeed 风速 m/s

优化前

根据查询需求创建索引,查询速度有保证,但是索引量带来的内存开销较大,且增加速度快

// 1.创建一个集合
db.createCollection("wather_data")
// 2.创建查询索引(因为有大量数据后,建索引耗时非常长)
db.weather_data.createIndex({"deviceCode":1,"ts":1})
// 3.初始化100万条假数据这里我用代码生成

import cn.hutool.core.util.RandomUtil;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

@RestController
public class Simulator {

    @Autowired
    private MongoTemplate mongoTemplate;

    @GetMapping("simulator")
    public void execute() {
        // 100个点位
        List<String> deviceCodeList = new ArrayList<>(1000);
        for (int i = 1; i <= 100; i++) {
            deviceCodeList.add("code_" + i);
        }
        Calendar calendar = Calendar.getInstance();
        // 每个点位一万条数据
        for (int i = 0; i < 10000; i++) {
            List<DBObject> dataList = new ArrayList<>(1000);
            for (String deviceCode : deviceCodeList) {
                DBObject data = new BasicDBObject();
                // 设备编码
                data.put("deviceCode", deviceCode);
                // 时间
                data.put("ts", calendar.getTimeInMillis());
                // 随机模拟数据
                data.put("temp", RandomUtil.randomDouble(20, 40, 2, RoundingMode.DOWN));
                data.put("humidity", RandomUtil.randomDouble(10, 80, 2, RoundingMode.DOWN));
                data.put("windDirection", RandomUtil.randomDouble(0, 360, 2, RoundingMode.DOWN));
                data.put("windSpeed", RandomUtil.randomDouble(0, 30, 2, RoundingMode.DOWN));
                dataList.add(data);
            }
            // 批量存储一分钟100个点位的数据
            mongoTemplate.getCollection("weather_data").insert(dataList);
            // 减一分钟
            calendar.add(Calendar.MINUTE, -1);
        }

    }
}

复制代码

查询速度

精确查询(查询指定点位指定分钟的数据)

db.getCollection('weather_data').find({'deviceCode':"code_88", "ts": 1632464271085})
有索引耗时:1~2ms
无索引耗时:200~300ms
复制代码

范围查询(查询指定点位一个小时的数据)

db.getCollection('weather_data').find({'deviceCode':"code_88", "ts": {'$gte':1632460545000,'$lte':1632464271085}});
有索引耗时:1~3ms
无索引耗时:300~1000ms
复制代码

磁盘占用及索引内存占用(单位:M)

db.getCollection('weather_data').stats(1024*1024);
索引的内存开销
deviceCode_1_ts_1	13M
集合占用磁盘大小
storageSize             57M                    
复制代码

100w条数据,按照deviceCode及ts建组合查询索引,单个集合查询索引占用内存达到了13M,如果数据量达到千万级别,集合的索引内存占用会达到百兆级别。对于监测系统,数据是随着时间不断增多的,就算是分表处理,整体索引内存占用量也是相同的,下面介绍一下索引优化方案。

优化方案

优化目的

保障查询性能同时降低索引内存消耗

数据变化

在入库前对ts时间戳进行格式化,增加一个hour字段

{
   "deviceCode": "xxx",
   "ts": 1632450060000,
   "hour": "2021-09-24 10",
   "temp": 27.1,
   "humidity": 0.77,
   "windDirection": 180,
   "windSpeed": 10
}
复制代码

索引

在mongo集合中,按照deviceCode及hours创建索引

db.weather_data_01.createIndex({"deviceCode":1,"hour":1})
复制代码

磁盘占用及索引内存占用(单位:M)

db.getCollection('weather_data_01').stats(1024*1024);
索引的内存开销
deviceCode_1_ts_1	5M
集合占用磁盘大小
storageSize             45M   
复制代码

可以看到我们同样100万条数据,使用新的索引设计,索引内存占用及数据的磁盘占用都减少了很多,内存占用不到之前的40%,下面看看同样的查询业务怎么做

查询实现

精确查询

db.getCollection('weather_data_01').find({"deviceCode":"code_88","hour":"2021-09-24 16","ts":1632470878256})
有索引耗时:1~2ms
无索引耗时:200~300ms
复制代码

范围查询(查询指定点位一个小时的数据)

db.getCollection('weather_data_01').find({"deviceCode":"code_88","hour":"2021-09-24 15"})
耗时:1~2ms
复制代码

这时候由于我们数据增加了一个hour字段,且按照deviceCode及hour建索引,我们查询指定小时的所有数据就可以不用ts进行范围查询了

范围查询(跨越了两个小时)

查询deviceCode为code_88在2021-09-24 15:10到2021-09-24 16:10的数据
db.getCollection('weather_data_01').find({"deviceCode":"code_88","hour":{"$gte":"2021-09-24 15","$lte":"2021-09-24 16"}, "ts":{"$gte":1632467400000,"$lte":1632471000000}});
耗时: 1~3ms
复制代码

优化后,不仅仅内存占用有明显的降低,在查询业务上我们也更加灵活,如果我们涉及到查询一天的分钟数据的业务比较多,也可以在入库时候将ts格式化成day,存储一个日期字段并索引等。我们在索引设计时候也可以无中生有,为索引生成字段,比如我们数据涉及手机号及相关精确查询,可以针对手机号前三位生成一个字段并创建索引等等。

总结

mongo索引会消耗内存,我们在创建索引时候要考虑索引的内存开销,避免全表索引,如果需要有全表索引,在入库时候设置_id为自己生成的值(UUID或雪花算法生成id),mongo建表会默认为_id字段创建索引,在主键查询业务中使用_id进行查询即可,内容如果有误区请指正。