You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在MongoDB文档数组中避免重复数据插入?

问题描述

我在MongoDB的文档数组中存储了一批数据,文档结构如下:

{ 
  "Ticker":"TSLA34", 
  "History":[ 
    { "Price":26.36, "UpdatedAt":"10/22/2015 10:12:00 AM" }, 
    { "Price":26.37, "UpdatedAt":"10/22/2015 10:13:00 AM" } 
  ] 
}

需求是:按分钟向History数组中插入时间相关的价格数据,若文档不存在则创建并插入数据;若文档已存在,需检查History数组中是否已有该分钟的记录,仅当不存在时才插入。

我使用.NET 6中的MongoDB.Driver编写了如下代码尝试实现该逻辑:

// var item = new Chart("TSLA34", new HistoryEntry(26.36, DateTime.UtcNow))
var filterByTicker = Builders<Chart>.Filter.Eq(g => g.Ticker, item.Ticker);
var filterByDate = Builders<Chart>.Filter.ElemMatch(g => g.History, Builders<HistoryEntry>.Filter.Eq(x => x.UpdatedAt, item.History.First().UpdatedAt));
var filter = filterByTicker & Builders<Chart>.Filter.Not(filterByDate);
var update = Builders<Chart>.Update.Push(asset => asset.History, item.History.First());
var updateOneModel = new UpdateOneModel<Chart>(filter, update) {IsUpsert = true};

我可以先通过Find查询并用Linq检查后再执行更新,但想了解是否有更高效的MongoDB原生实现方式,请问有什么解决方案?

解决方案

你的现有思路方向正确,但可以优化为更贴合MongoDB原生特性的原子操作,同时解决时间精度匹配的核心问题:

1. 核心优化:分钟级时间匹配

需求是按分钟去重,不能直接用完整的UpdatedAt(包含秒、毫秒)匹配,必须先将时间截断到分钟级别。

客户端截断时间的实现

先在客户端把目标时间处理为分钟级精度,再构建过滤器:

var targetEntry = item.History.First();
// 将时间截断到分钟(移除秒、毫秒)
var truncatedTime = new DateTime(
    targetEntry.UpdatedAt.Year,
    targetEntry.UpdatedAt.Month,
    targetEntry.UpdatedAt.Day,
    targetEntry.UpdatedAt.Hour,
    targetEntry.UpdatedAt.Minute,
    0,
    DateTimeKind.Utc
);

// 构建过滤器:匹配Ticker,且History中无对应分钟的记录
var filter = Builders<Chart>.Filter.And(
    Builders<Chart>.Filter.Eq(g => g.Ticker, item.Ticker),
    Builders<Chart>.Filter.Not(
        Builders<Chart>.Filter.ElemMatch(g => g.History, 
            Builders<HistoryEntry>.Filter.Eq(x => x.UpdatedAt, truncatedTime))
    )
);

// 构建更新操作:向History数组插入截断时间后的记录
var update = Builders<Chart>.Update.Push(g => g.History, 
    new HistoryEntry(targetEntry.Price, truncatedTime));

// 执行原子更新/插入
var result = await collection.UpdateOneAsync(
    filter,
    update,
    new UpdateOptions { IsUpsert = true }
);

服务器端截断时间的实现(MongoDB 5.0+)

如果想完全在MongoDB端处理时间截断,避免客户端与服务器的时间差异,可使用$dateTrunc聚合表达式:

var targetEntry = item.History.First();

var filter = Builders<Chart>.Filter.And(
    Builders<Chart>.Filter.Eq(g => g.Ticker, item.Ticker),
    Builders<Chart>.Filter.Not(Builders<Chart>.Filter.Expr(
        new BsonDocument("$expr", new BsonDocument(
            "$in", new BsonArray {
                new BsonDocument("$dateTrunc", new BsonDocument {
                    { "date", "$History.UpdatedAt" },
                    { "unit", "minute" }
                }),
                new BsonArray {
                    new BsonDocument("$dateTrunc", new BsonDocument {
                        { "date", targetEntry.UpdatedAt },
                        { "unit", "minute" }
                    })
                }
            }
        ))
    ))
);

var update = Builders<Chart>.Update.Push(g => g.History, targetEntry);
var result = await collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true });

2. 关键优势

  • 原子性:UpdateOne配合IsUpsert=true是原子操作,避免并发场景下先查再更导致的重复插入问题。
  • 性能:直接使用MongoDB原生过滤和更新,比客户端先查询再处理的方式减少一次网络往返。
  • 准确性:通过时间截断确保同一分钟的记录不会被重复插入,解决原代码中可能因秒/毫秒差异导致的误判。

3. 索引优化建议

为提升查询效率,建议创建以下索引:

  • 单键索引:db.charts.createIndex({ Ticker: 1 })
  • 数组索引:db.charts.createIndex({ "History.UpdatedAt": 1 })

内容的提问来源于stack exchange,提问作者Gabriel Guedes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 19:01:01