考虑一个数据集,项可以在时间上配对。
例如,使用徽章登录和退出某个区域,可能会记录以下数据:
┏━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━┓
┃ Time ┃ BadgeId ┃ Direction ┃
┣══════════╪═════════╪═══════════┫
┃ 1001930 ┃ A ┃ IN ┃
┣━━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━━━┫
┃ 1004901 ┃ B ┃ IN ┃
┣━━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━━━┫
┃ 1005192 ┃ A ┃ OUT ┃
┣━━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━━━┫
┃ 1012933 ┃ A ┃ IN ┃
┣━━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━━━┫
┃ 1014495 ┃ B ┃ OUT ┃
┣━━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━━━┫
┃ 1017891 ┃ A ┃ OUT ┃
┗━━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━━━┛
然后临时配对以获得类似的内容:
┏━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ BadgeId ┃ TimeIn ┃ TimeOut ┃
┣═════════╪══════════╪══════════┫
┃ A ┃ 1001930 ┃ 1005192 ┃
┣━━━━━━━━━╋━━━━━━━━━━╋━━━━━━━━━━┫
┃ A ┃ 1012933 ┃ 1017891 ┃
┣━━━━━━━━━╋━━━━━━━━━━╋━━━━━━━━━━┫
┃ B ┃ 1004901 ┃ 1014495 ┃
┗━━━━━━━━━┻━━━━━━━━━━┻━━━━━━━━━━┛
给一个包含数亿个这样的记录的数据集,做这种时间配对的最有效方法是什么?我对使用LINQ(或其他基于集合的查询语言)的最佳理论方法和最实用的方法感兴趣。
答案 0 :(得分:1)
也许这不是处理数百万条记录的最佳理论方法。但是,这是可行的,可以用作进一步改进的起点。
class Program
{
static void Main(string[] args)
{
var StartingRecords = new List<Record>()
{
new Record(1001930, "A", "IN"),
new Record(1004901, "B", "IN"),
new Record(1005192, "A", "OUT"),
new Record(1012933, "A", "IN"),
new Record(1014495, "B", "OUT"),
new Record(1017891, "A", "OUT"),
};
var records = StartingRecords.OrderBy(x => x.BadgeId).ThenBy(x => x.Time).ToList();
var pairs = records.Skip(1).Zip(records, (second, first) => Tuple.Create(first, second)).
Where(x => x.Item1.BadgeId == x.Item2.BadgeId &&
x.Item1.Direction == "IN" && x.Item2.Direction == "OUT").
Select(x => new Pair(x.Item1.BadgeId, x.Item1.Time, x.Item2.Time)).ToList();
foreach (var pair in pairs)
Console.WriteLine(pair.BadgeId + "\t" + pair.TimeIn + "\t" + pair.TimeOut);
Console.Read();
}
}
class Record
{
public long Time { get; set; }
public string BadgeId { get; set; }
public string Direction { get; set; }
public Record(long time, string badgeId, string direction)
{
Time = time;
BadgeId = badgeId;
Direction = direction;
}
}
class Pair
{
public string BadgeId { get; set; }
public long TimeIn { get; set; }
public long TimeOut { get; set; }
public Pair(string badgeId, long timeIn, long timeOut)
{
BadgeId = badgeId;
TimeIn = timeIn;
TimeOut = timeOut;
}
}
输出:
A 1001930 1005192
A 1012933 1017891
B 1004901 1014495
答案 1 :(得分:1)
我不确定这样做的效率或性能如何,但是我认为LINQ可以将其转换为SQL,因此,如果您使用的是数据库,则可能会将更多的计算推向服务器。
首先,将记录按徽章分组:
var p1 = from p in punches
group p by p.Badge into pg
select new {
Badge = pg.Key,
Punches = pg.OrderBy(p => p.Time)
};
然后,对于每个徽章的记录组,遍历所有“ IN”记录,并将其与“ OUT”记录匹配(如果存在):
var p2 = p1.SelectMany(pg => pg.Punches.Where(p => p.Dir == "IN")
.Select(p => new {
pg.Badge,
TimeIn = p.Time,
TimeOut = pg.Punches.Where(po => po.Dir == "OUT" && po.Time > p.Time)
.FirstOrDefault().Time
}));
最后,订购结果:
var ans = p2.OrderBy(bio => bio.Badge).ThenBy(bio => bio.TimeIn);
由于LINQ to SQL自动传播空值,因此我认为这将为“ IN”处理丢失的“ OUT”打孔,但不会处理孤立的“ OUT”打孔。
另一种可能性是使用带有两个参数的Select
将打孔记录成对分组,但这仅适用于LINQ to Objects,因此,除非您在处理之前过滤数据,否则数百万条记录将全部拉入内存。
出于完整性考虑,请尝试以下操作:
var p2 = p1.AsEnumerable()
.SelectMany(pg => pg.Punches.Select((p, i) => (p, i))
.GroupBy(pi => pi.i / 2, pi => pi.p)
.Select(pp => new {
pg.Badge,
TimeIn = pp.Where(p => p.Dir == "IN").FirstOrDefault()?.Time,
TimeOut = pp.Where(p => p.Dir == "OUT").FirstOrDefault()?.Time
}));
如果您的打孔顺序不正确(例如,您缺少首字母“ IN”。