Самый быстрый способ разделить перекрывающиеся диапазоны дат

у меня есть данные диапазона дат в таблице SQL DB, которая имеет эти три (только соответствующие) столбца:

  • ID (int identity)
  • RangeFrom (только дата)
  • RangeTo (только дата)

для любого заданного диапазона дат может быть произвольное количество записей, которые могут перекрываться (полностью или частично).

условия

  1. каждая запись с более высоким ID (новая запись) приоритет над более старыми записями, которые могут перекрываться (полностью или частично)
  2. диапазоны не менее 1 дня (RangeFrom и RangeTo отличаются на один день)

Итак, для заданного диапазона дат (не длиннее ie. 5 лет) я должен

  1. получить весь диапазон записей, которые попадают в этот диапазон (полностью или частично)
  2. разделить эти перекрытия на неперекрывающиеся диапазоны
  3. верните эти новые неперекрывающиеся диапазоны

мой взгляд на это

поскольку существует много сложных данных, связанных с этими диапазонами (много соединений и т. д.), И поскольку процессор + мощность памяти намного эффективнее, чем SQL DB engine, я решил скорее загрузить перекрывающиеся данные из БД в мой уровень данных и сделать диапазон прерывания/расщепления в памяти. Это дает мне гораздо большую гибкость, а также скорость с точки зрения разработки и выполнения.

если вы думаете, что это должно быть лучше обрабатываются в Б.: дай мне знать.

вопрос

Я хотел бы написать самый быстрый и, если это вообще возможно, также ресурс не голодный алгоритм преобразования. Поскольку я получаю много этих записей, и они связаны с различными пользователями, я должен запустить этот алгоритм для каждого пользователя и его набора данных перекрывающихся диапазонов.

каким будет наиболее эффективный (быстрый и не ресурсоемкий) способ разделения этих перекрывающихся диапазонов?

пример данные

у меня есть записи ID=1 to ID=5 что визуально перекрываются таким образом (даты на самом деле не имеют значения, я могу лучше показать эти перекрытия таким образом):

       6666666666666
                44444444444444444444444444         5555555555
          2222222222222            333333333333333333333            7777777
11111111111111111111111111111111111111111111111111111111111111111111

результат должен выглядеть так:

111111166666666666664444444444444444444444333333333555555555511111117777777

результат на самом деле выглядит так, как будто мы смотрим на эти перекрытия сверху, а затем получаем идентификаторы, которые мы видим сверху вниз.

результат фактически преобразуется в новые записи диапазона, поэтому старые идентификаторы становятся неуместный. Но их RangeFrom и RangeTo значения (вместе со всеми связанными данными) будут использоваться:

111111122222222222223333333333333333333333444444444555555555566666667777777

это конечно просто пример перекрывающихся диапазонов. Это может быть что угодно от 0 записей X для любого заданного диапазона дат. И, как мы видим, диапазон ID=2 был полностью перезаписан 4 и 6, поэтому он стал полностью устаревшим.

4 ответов


как насчет массива целых чисел

я придумал свою собственную идею:

  1. для данного диапазона дат я бы создал в памяти массив целых чисел с таким количеством элементов, сколько дней в диапазоне.

  2. заполнить массив с null значения. Все они.

  3. заказать записи по ID в обратном порядке

  4. сгладить перекрывающиеся диапазоны путем итерации над заказанными записями и сделайте следующее по каждому пункту:

    1. сделать пункт
    2. рассчитать начальное и конечное смещение для массива (разница дней)
    3. установите все значения массива между этими двумя смещениями в item ID, но только если значение null
    4. переходите к шагу 4.1
  5. вы в конечном итоге с массивом сплющенных диапазонов и заполнены идентификаторами записей

  6. создать новый набор записывает и создает каждую новую запись при изменении ID в массиве. Каждая запись должна использовать данные, связанные с идентификатором записи в массиве

  7. повторите все это для следующего человека и его набора перекрывающихся диапазонов (не забудьте повторно использовать тот же массив). = вернитесь к Шагу 2.

и это в основном.

10 лет, данный диапазон дат требует массива ок. 3650 nullable целых чисел, которые я думаю довольно небольшой объем памяти (каждое целое число занимает 4 байта, но я не знаю, сколько места занимает целое число с нулевым значением, которое имеет int и bool но предположим, что 8 байт, которые составляют 3650*8 = 28.52 k) и могут быть легко и довольно быстро манипулировать в памяти. Поскольку я не сохраняю диапазоны дат, разделение или что-то подобное, это едва ли просто операции назначения с if, который проверяет, установлено ли значение.

в 10 дата год-это редкий exaggeratet крайность. 75% диапазонов дат будет в течение 3 месяцев или квартала года (90 дней * 8 байт = 720 байт), а 99% - в течение всего года (365*8 = 2920 байт = 2,85 к)

Я нахожу этот алгоритм более подходит для выравнивания перекрывающихся диапазонов.

до половины памяти я мог бы использовать int вместо int? и установлено -1 вместо null.

преждевременная возможность разрыва цикла итерации

я мог бы как ну, держите количество дней, которые не установлены, и когда он достигнет 0, я могу легко разорвать цикл, потому что все остальные диапазоны полностью перекрываются, поэтому они не будут устанавливать больше значений в массиве. Так что это даже немного ускорит процесс, когда у меня будет много записей диапазона (что будет довольно редко).


свободный библиотека периодов времени для .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 - >диапазон] в [дата - >список идентификаторов].

(сортировка по дате, каждая дата-независимо от того, начало или конец, является началом диапазона времени, достигающего до следующей даты.) Чтобы ваш стол выглядел так:

        |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
 ...
  • выберите самый большой элемент в каждом списке
  • обединить следующие записи с одинаковым наибольшим значением.

поскольку у вас будут дубликаты идентификаторов в вашей конечной базе данных ("1" в вашем примере разделяется на два сегмента), сохранение базы данных в формате date->ID в отличие от ID->range кажется предпочтительным в конце.

теперь для очевидных оптимизаций-конечно, не держите список идентификаторов с каждой записью даты. Просто заполните таблицу date - >ID с нулевыми идентификаторами и, заполняя ее конечными записями, замените самую большую запись значения нашел:

  • создать таблицу всех записей даты, [дата - > ID]
  • для каждой записи в исходной таблице:
    • Выберите даты в диапазоне от-до,
    • если какой-либо из них имеет значение ID null или ниже, чем текущий проверенный идентификатор записи, заполните текущий идентификатор.
  • затем concatenate-если следующая запись имеет тот же идентификатор, что и предыдущая, удалите next.
  • в конце концов, вы можете денормализовать немного, заменить извлечения двух последовательные записи для диапазона с [date - > ID, length] или [date - > ID,end_date]

добавление новой записи похоже на одну итерацию операции создания. Удаление записи, с другой стороны, кажется довольно сложным.


эффективно вы хотите сложить данные и выбрать максимум из стека. Я должен был реализовать что-то подобное этому раньше, и подход, который мы использовали, который дал нам немного больше гибкости, чем вам требуется, поэтому, возможно, не подходит, чтобы сделать это:

имейте объект для управления записями и добавьте каждую запись к этому объекту. при добавлении записи создайте новый диапазон дат и свяжите значение записи с диапазоном. Затем проверьте, если диапазон перекрывается с любым другим существующим диапазоном. Если он перекрывается, создайте новый диапазон для каждого перекрытия и свяжите все значения на обоих/всех (в зависимости от того, делаете ли вы это при добавлении каждого диапазона или за один проход) перекрывающихся диапазонов с новым диапазоном. Это можно сделать либо при добавлении данных, либо за один проход после добавления всех данных.

В конце у вас есть объект, который содержит уникальные диапазоны, каждый из которых имеет набор значений, связанных с ним, похож на картинке выше.

       |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|

затем вы можете предоставить классу функцию выравнивания (возможно, используя шаблон стратегии), которая преобразует уникальные диапазоны с коллекциями значений в уникальные диапазоны с одним значением, это, очевидно, объединит диапазоны, которые заканчиваются тем же значением.

вам нужен класс, который выбирает максимальное значение из каждого уникального диапазона, но вы также можете выбрать минимальное значение, суммировать значения, усредните их, посчитайте и т. д. Каждый из этих вариантов может быть реализован путем прохождения различной реализации стратегии.

Как я уже сказал, этот подход может быть менее эффективным, чем подход, который выбирает только максимальное значение, так как в этом случае вам не нужно будет хранить все значения в стеке, но реализация была довольно прямой, как я помню.