前言
今天介绍一下我在工作中MongoDB数据表索引设计的思路,以供大家参考,前面有文章介绍过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进行查询即可,内容如果有误区请指正。
近期评论