使用LINQ有效地配对与时间相关的记录

时间:2018-07-03 12:50:52

标签: c# database linq temporal pairing

考虑一个数据集,项可以在时间上配对。

例如,使用徽章登录和退出某个区域,可能会记录以下数据:

┏━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━┓
┃ 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(或其他基于集合的查询语言)的最佳理论方法和最实用的方法感兴趣。

2 个答案:

答案 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”。