我在SQL DB表中有日期范围数据,这些数据包含以下三个(仅相关)列:
ID
(int identity)RangeFrom
(仅限日期)RangeTo
(仅限日期)对于任何给定的日期范围,可能存在可能重叠(完全或部分)的任意数量的记录。
ID
(较新记录)的每条记录优先于可能重叠(完全或部分)的旧记录RangeFrom
和RangeTo
相差一天)由于存在大量与这些范围相关的复杂数据(大量连接等),并且由于处理器+内存功率比SQL数据库引擎效率高得多,因此我决定将重叠数据从DB加载到我的数据层并执行内存中的范围斩波/分裂。这使我在开发和执行方面具有更大的灵活性和速度。
如果你认为这应该在DB中更好地处理,请告诉我。
我想写最快的,如果可能的话也是资源非饥饿的转换算法。由于我获得了大量这些记录并且它们与各种用户相关,因此我必须为每个用户及其重叠范围数据集运行此算法。
分割这些重叠范围的最有效(快速且非资源饥饿)方式是什么?
我有ID=1
到ID=5
的记录以这种方式在视觉上重叠(日期实际上是无关紧要的,我可以用这种方式更好地显示这些重叠):
6666666666666
44444444444444444444444444 5555555555
2222222222222 333333333333333333333 7777777
11111111111111111111111111111111111111111111111111111111111111111111
结果应如下所示:
111111166666666666664444444444444444444444333333333555555555511111117777777
结果实际上看起来好像我们正在从顶部查看这些重叠,然后获取我们从这个自上而下的视图中看到的ID。
结果实际上会转换为新的范围记录,因此旧ID变得无关紧要。但是将使用他们的RangeFrom
和RangeTo
值(以及所有相关数据):
111111122222222222223333333333333333333333444444444555555555566666667777777
这当然只是重叠范围的一个例子。对于任何给定的日期范围,它可以是从0记录到X的任何内容。而且我们可以看到范围ID = 2被4和6完全覆盖,所以它变得完全过时了。
答案 0 :(得分:4)
我想出了自己的想法:
对于给定的日期范围,我将创建一个整数的内存数组,其中的项目数与该范围内的天数一样多。
使用null
值填充数组。所有这些。
按ID以相反顺序订购记录
通过迭代有序记录来展平重叠范围,并对每个项目执行以下操作:
null
您最终获得了一系列展平范围并填充了记录ID
创建新的记录集,并在数组中的ID更改时创建每个新记录。每条记录应使用与数组
为下一个人及其重叠范围集重复整个事情(不要忘记重复使用相同的数组)。 =回到第2步。
基本上就是这样。
给定日期范围的10年需要大约一个数组。 3650个可以为空的整数,我认为这是一个相当小的内存占用(每个整数占用4个字节,但我不知道有多少空间占用一个可以为int
和bool
的可空整数,但我们假设8总计在3650 * 8 = 28.52k的字节,并且可以在内存中轻松快速地操作。由于我没有保存日期范围,因此拆分或类似的任何操作几乎不仅仅是赋值操作,如果检查是否已经设置了值。
10年的日期范围是一种罕见的夸张极端。 75%的日期范围将在3个月或一年中的四分之一(90天* 8字节= 720字节),99%将在一年的范围内(365 * 8 = 2920字节= 2,85k)< / p>
我觉得这个算法不适合于展平重叠的日期范围。
对于内存占用量的一半,我可以使用int
代替int?
并设置为-1
而不是null
。
我还可以保留未设置的天数,当它达到0时,我可以很容易地打破循环,因为所有剩余的范围都是完全重叠的,因此它们不会在数组中设置更多的值。因此,当我有很多范围记录时,这甚至可以加快速度(这将是相当罕见的)。
答案 1 :(得分:2)
免费Time Period Library for .NET包含工具 TimePeriodIntersector ,它与各种重叠时间范围相交。
该算法使用时间轴并枚举一个时间范围内的所有时刻(计算每时刻的起点/终点):
// ----------------------------------------------------------------------
public void TimePeriodIntersectorSample()
{
TimePeriodCollection periods = new TimePeriodCollection();
periods.Add( new TimeRange( new DateTime( 2011, 3, 01 ), new DateTime( 2011, 3, 10 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 05 ), new DateTime( 2011, 3, 15 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 12 ), new DateTime( 2011, 3, 18 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 20 ), new DateTime( 2011, 3, 24 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 22 ), new DateTime( 2011, 3, 28 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 24 ), new DateTime( 2011, 3, 26 ) ) );
TimePeriodIntersector<TimeRange> periodIntersector =
new TimePeriodIntersector<TimeRange>();
// calculate intersection periods; do not combine the resulting time periods
ITimePeriodCollection intersectedPeriods = periodIntersector.IntersectPeriods( periods, false );
foreach ( ITimePeriod intersectedPeriod in intersectedPeriods )
{
Console.WriteLine( "Intersected Period: " + intersectedPeriod );
}
// > Intersected Period: 05.03.2011 - 10.03.2011 | 5.00:00
// > Intersected Period: 12.03.2011 - 15.03.2011 | 3.00:00
// > Intersected Period: 22.03.2011 - 24.03.2011 | 2.00:00
// > Intersected Period: 24.03.2011 - 26.03.2011 | 2.00:00
} // TimePeriodIntersectorSample
ID映射应该是一项简单的任务。
答案 2 :(得分:1)
我不太确定会有多么有用,但我接近这个的方式...... (首先未经优化以便于理解......)
(按日期排序,每个日期 - 无论是开始还是结束,都是到达下一个日期的时间范围的开始。) 这样你的桌子就像:
|666|666666|6666|
| | |4444|444|444444444444|4444444| |55555|55555|
| |222222|2222|222| |3333333|333333333|33333| | |7777777
1111111|111|111111|1111|111|111111111111|1111111|111111111|11111|11111|1111111|
1234567|890|123456|7890|123|4
1 -> 1
8 -> 1,6
11 -> 6,2,1
17 -> 6,4,2,1
21 -> 4,2,1
24 -> 4,1
...
由于您的最终数据库中将有重复的ID(在您的示例中,“1”将分为两个段),因此最终将数据库保持为date-&gt; ID格式而不是ID-&gt;范围
现在进行明显的优化 - 当然不要在每个日期记录中保留ID列表。只需使用空ID填写date-&gt; ID表,并在填写最终记录时,替换目前为止找到的最大值记录:
添加新记录就像创建操作的一次迭代。另一方面,删除记录似乎非常棘手。
答案 3 :(得分:0)
实际上,您希望堆叠数据,并从堆栈中选择最大值。我不得不在之前和我们使用的方法中实现类似的东西,这给了我们比你需要的更多的灵活性,所以可能不适合这样做:
拥有一个用于管理记录的对象,并将每条记录添加到此对象。添加记录时,创建新的日期范围并将记录的值与范围相关联。然后检查范围是否与任何其他现有范围重叠。如果它确实重叠,则为每个重叠创建一个新范围并关联两个/所有的所有值(取决于在添加每个范围时是否这样做,或在单个过程中)与新范围重叠的范围。这可以在添加数据时完成,也可以在添加所有数据后一次性完成。
最后你有一个包含唯一范围的对象,每个范围都有一组与之关联的值,有点像你上面的图片。
|666|666666|6666| | | |4444|444|444444444444|4444444| |55555|55555| | |222222|2222|222| |3333333|333333333|33333| | |7777777 1111111|111|111111|1111|111|111111111111|1111111|111111111|11111|11111|1111111|
然后,您可以提供一个具有展平函数的类(可能使用策略模式),它将具有值集合的唯一范围转换为具有单个值的唯一范围,这显然会连接最终具有相同值的范围
您需要一个从每个唯一范围中选择最大值的类,但您可能还需要选择最小值,对值求和,平均值,计算它们等等。这些选项中的每一个都可以通过传递来完成战略的不同实施。
正如我所说的那样,这种方法可能效率低于仅选择最大值的方法,因为在这种情况下你不需要保留堆栈中的所有值,但是我记得实现是相当直接的