.Net Core 大数据集下快速校验ID是否存在的高效优化方案
性能优化方案
你当前实现耗时久的核心原因有两个:
- 把350万条存量ID全量加载到应用内存,数据库读取、网络传输的开销占了总耗时的大头
- 用
List<int>存储存量ID做存在性判断,Contains方法时间复杂度是O(n),待比对ID量越大,内存计算的耗时越高
你之前试过的逐行Any写法性能更差,是典型的N+1查询问题:每判断一个ID就向数据库发一次请求,大量重复IO开销直接把性能拖垮。
下面是按改造成本从低到高、收益从大到小排序的可落地优化方案:
1. 零结构调整的快速优化
把存存量ID的集合从List<int>换成HashSet<int>,HashSet的存在性判断时间复杂度是O(1),仅这一步就能让内存比对环节的速度提升几十到上百倍:
var ingestIds = ingestList.Select(i => i.Id).ToList(); // 查存量ID直接转HashSet,不要存List var existingIdSet = _context.CurrentRecords.Select(i => i.Id).ToHashSet(); var newIds = ingestIds.Where(id => !existingIdSet.Contains(id)).ToList();
注意:这个方案只是优化了内存比对速度,还是要全量拉350万ID,性能瓶颈没有完全解决,仅适合临时过渡使用。
2. 常规场景最优方案(90%的场景用这个就够)
不要全量拉存量ID! 反过来让数据库只返回本次待导入列表里已经存在的ID,利用数据库主键索引的优势做过滤,既不需要扫全表,也不需要传输几百万条无用数据。
如果单次待导入的ID量在10万以内,直接用EF Core的Contains查询即可:
var ingestIds = ingestList.Select(i => i.Id).ToList(); // 只查本次导入列表里命中的存量ID,走主键索引速度极快 var existIds = _context.CurrentRecords .Where(record => ingestIds.Contains(record.Id)) .Select(record => record.Id) .ToHashSet(); // 算差集得到需要新增的ID var newIds = ingestIds.Where(id => !existIds.Contains(id)).ToList();
如果你用的是旧版本EF Core,存在单次SQL参数数量上限(一般是2100个),就把待导入ID按每批1000个分片查询,合并结果即可:
var ingestIds = ingestList.Select(i => i.Id).ToList(); var existIds = new HashSet<int>(); // 分片批量查询 foreach (var idBatch in ingestIds.Chunk(1000)) { var batchExist = _context.CurrentRecords .Where(record => idBatch.Contains(record.Id)) .Select(record => record.Id) .ToList(); foreach (var id in batchExist) existIds.Add(id); } var newIds = ingestIds.Where(id => !existIds.Contains(id)).ToList();
正常情况下,这个方案能把原来9-10分钟的耗时压缩到秒级。
3. 超大量导入场景优化
如果单次待导入的ID量超过10万,用临时表方案效率最高:
- 首先在数据库建一张独立的临时表,仅设一个ID字段
- 把本次待导入的所有ID批量写入临时表
- 写一句左连接SQL,直接查出临时表里存在、业务主表里不存在的ID,就是需要新增的ID集合
这种方案完全没有SQL参数数量限制,集合运算全在数据库侧完成,哪怕单次导入上百万ID,耗时也能控制在十几秒以内。
前置注意项
- 先确认主表的
Id字段是主键,或者已经建立了索引,这是所有查询性能的基础,没有索引的话任何写法都快不起来 - 彻底放弃逐行
Any的写法,N+1查询的IO开销在批量场景下没有任何性能优势 - 如果用EF Core 7及以上版本,可以直接结合批量插入API,连ID拉回应用内存的步骤都能省略,进一步减少数据传输开销
内容的提问来源于stack exchange,提问作者user7704925
相关产品推荐
相关产品推荐

