Календарь
Календарь¶
Календарь это набор специального инструментария, который позволяет легко решать задачи по управлению и получению информации связанной с рабочим и нерабочим временем. Календарь оперирует датами исключительно в формате UTC. Так же стоит помнить, что календарь рассчитывается с расчётом на московское время. Календарь представляет из себя табличку, в которой записана заранее рассчитанная информация о том, является ли момент времени рабочим или нет.
Квант времени¶
Для отражения момента времени используется специальное понятие кванта времени. Существует два типа квантов времени - рабочий и не рабочий.
-
Рабочий квант - Длится 15 минут. На такие кванты разбит весь рабочий день.
-
Нерабочий квант - длится всё время с момента конца предыдущего рабочего кванта и до момента начала следующего рабочего кванта.
Таблица с квантами времени имеет следующую структуру:
-
QuantNumber - номер кванта.
-
StartTimeUTC – дата/время начала кванта (включительно).
-
EndTimeUTC – дата/время окончания кванта (не включительно).
-
Type - Тип кванта (0 - рабочее время, 1 - выходной).
Рассчитанные кванты придерживаются нескольких основных правил:
-
Номера квантов идут по порядку и неразрывны.
-
Кванты нерабочего времени имеют те же номера, что и идущий перед ним квант.
-
Рабочее время разбито на кванты по 15 минут, нерабочее время заносится единым квантом.
-
Между квантами не должно быть промежутка по времени. Конец одного кванта равен началу следующего за ним кванта.
Для примера рассмотрим отрывок из рассчитанного календаря:
QuantNumber |
StartTimeUTC | EndTimeUTC | Type |
---|---|---|---|
0 | 2014-01-01 00:00:00 | 2014-01-01 05:00:00 | 1 |
1 | 2014-01-01 05:00:00 | 2014-01-01 05:15:00 | 0 |
2 | 2014-01-01 05:15:00 | 2014-01-01 05:30:00 | 0 |
3 | 2014-01-01 05:30:00 | 2014-01-01 05:45:00 | 0 |
… | … | … | … |
14 | 2014-01-01 08:15:00 | 2014-01-01 08:30:00 | 0 |
15 | 2014-01-01 08:30:00 | 2014-01-01 08:45:00 | 0 |
16 | 2014-01-01 08:45:00 | 2014-01-01 09:00:00 | 0 |
16 | 2014-01-01 09:00:00 | 2014-01-01 11:00:00 | 1 |
17 | 2014-01-01 11:00:00 | 2014-01-01 11:15:00 | 0 |
18 | 2014-01-01 11:15:00 | 2014-01-01 11:30:00 | 0 |
19 | 2014-01-01 11:30:00 | 2014-01-01 11:45:00 | 0 |
… | … | … | … |
22 | 2014-01-01 12:15:00 | 2014-01-01 12:30:00 | 0 |
… | … | … | … |
30 | 2014-01-01 14:15:00 | 2014-01-01 14:30:00 | 0 |
31 | 2014-01-01 14:30:00 | 2014-01-01 14:45:00 | 0 |
32 | 2014-01-01 14:45:00 | 2014-01-01 15:00:00 | 0 |
32 | 2014-01-01 15:00:00 | 2014-01-06 05:00:00 | 1 |
33 | 2014-01-06 05:00:00 | 2014-01-06 05:15:00 | 0 |
34 | 2014-01-06 05:15:00 | 2014-01-06 05:30:00 | 0 |
35 | 2014-01-06 05:30:00 | 2014-01-06 05:45:00 | 0 |
… | … | … | … |
48 | 2014-01-06 08:45:00 | 2014-01-06 09:00:00 | 0 |
48 | 2014-01-06 09:00:00 | 2014-01-06 11:00:00 | 1 |
49 | 2014-01-06 11:00:00 | 2014-01-06 11:15:00 | 0 |
… | … | … | … |
64 | 2014-01-06 14:45:00 | 2014-01-06 15:00:00 | 0 |
64 | 2014-01-06 15:00:00 | 2014-01-07 05:00:00 | 1 |
65 | 2014-01-07 05:00:00 | 2014-01-07 05:15:00 | 0 |
Важно помнить, что календарь оперирует временем в UTC. Видно, что квант с номером 0, - квант нерабочего времени, он заканчивается 2014-01-01 05:00. Следом за ним идут кванты рабочего времени по 15 минут, до момента 2014-01-01 09:00, когда начнётся обед (квант нерабочего времени), который длится до 2014-01-01 11:00. Далее до 2014-01-01 15:00 идут снова кванты рабочего времени по 15 минут, пока не появится квант нерабочего времени, отражающий ночь, и длящийся до 2014-01-06 05:00.
Карточка настроек календаря¶
Для настройки календаря в системе есть специальная карточка настроек.
Она содержит в себе:
-
Таблица исключений.
-
Период действия календаря.
-
Интервалы рабочего дня и обеденного перерыва.
-
Кнопки, вызывающие пересчёт календаря и проверку целостности рассчитанного календаря.
Подробнее о настройке календаря см. Руководство администратора.
Работа с календарём¶
Для работы с календарём существует специальное API - IBusinessCalendarService и набор хранимых функций и процедур. Для использования API - достаточно отрезолвить его из Unity. Так как в большинстве случаев API календаря используется внутри расширений - достаточно запросить его в конструкторе расширения.
class SomeExtension : CardGetExtension
{
private readonly IBusinessCalendarService calendarService;
public CalendarCardButtonsExtension(
IBusinessCalendarService calendarService)
{
this.calendarService = calendarService;
}
// ...
}
Далее можно использовать любой из предоставленных в API методов. Так же есть специальный набор хранимых процедур, которые можно использовать внутри sql-запросов.
Проверить - является ли рабочим указанное в UTC дата/время¶
При помощи функции IsWorkTime - можно проверить, является ли указанный момент времени рабочим.
/// <summary>
/// Проверить - является ли рабочим указанное дата\время в UTC.
/// </summary>
/// <param name="dateTimeUTC">Дата\время в UTC.</param>
/// <returns> <see cref="BusinessCalendarTimeType"/>
/// Work - рабочее время.
/// Holiday - нерабочее время.</returns>
BusinessCalendarTimeType IsWorkTime(DateTime dateTimeUTC);
Функция принимает параметр dateTimeUTC DateTime и на выходе возвращает значение перечисления BusinessCalendarTimeType.
public enum BusinessCalendarTimeType
{
Work, // Рабочее время
Holiday // Нерабочее время
}
Так же можно воспользоваться хранимой функцией CalendarIsWorkTime.
FUNCTION CalendarIsWorkTime(@DateTimeUtc datetime) RETURNS bit
Функция принимает параметры аналогично методу из API:
-
@DateTimeUtc - дата\время в UTC, для которой производится расчёт.
-
Возвращает - 0 - рабочее время, 1 - нерабочее время.
Описание логики работы
Допустим, необходимо проверить является ли выбранный момент времени рабочим временем. Для этого можно вызвать метод API IsWorkTime или хранимую функцию CalendarIsWorkTime. Полученный результат сообщает о типе кванта, к которому относится запрашиваемый момент времени.
Для примера возьмём - 2014-01-01 08:49:00 (момент времени указывается в UTC).
Для того, чтобы понять является ли момент времени 2014-01-01 08:49:00 рабочим, нужно запросить из таблицы календаря квант, в который этот момент времени попадает. По типу полученного кванта можно будет понять, является ли момент времени рабочим или нет.
Если мы обратимся к таблице (см. выше), то мы увидим, что момент времени 2014-01-01 08:49:00 соответствует кванту с номером 16, который начинается 2014-01-01 08:45:00 и длится до 2014-01-01 09:00:00, а его тип - 0. Значит квант является квантом рабочего времени и момент времени 2014-01-01 08:49:00 является рабочим временем.
Расчёт рабочего времени между датами.¶
При помощи функции GetDateDiff - можно получить количество рабочих квантов между двумя моментами времени.
/// Расчёт рабочего времени между датами.
/// </summary>
/// <param name="dateTimeStartUTC">Первая дата в UTC.</param>
/// <param name="dateTimeEndUTC">Вторая дата в UTC (должна быть больше первой).</param>
/// <returns>Возвращает рабочее время между датами в квантах.</returns>
long GetDateDiff(DateTime dateTimeStartUTC, DateTime dateTimeEndUTC);
Функция принимает два параметра dateTimeStartUTC и dateTimeEndUTC (начало и конец соответственно) типа DateTime и на выходе возвращает количество рабочих квантов между этими двумя моментами времени.
Так же можно воспользоваться хранимой функцией CalendarGetDateDiff.
FUNCTION CalendarGetDateDiff(@FirstDateUtc datetime, @SecondDateUtc datetime) RETURNS int
Функция принимает данные аналогично методу из API:
-
@FirstDateUtc - первая дата в UTC.
-
@SecondDateUtc - вторая дата в UTC.
-
Возвращает количество рабочих квантов между датами (1 квант = 15 минут).
Описание логики работы
Допустим нам необходимо получить оставшееся на заданиях время в рабочих часах. Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию. Не забываем, что полученный результат будет в рабочих квантах (интервалах по 15 минут).
Для примера возьмём моменты времени - 2014-01-01 05:29:00 и 2014-01-01 11:20:00 (моменты времени указываются в UTC). Для того, чтобы найти количество рабочих квантов между двумя моментами времени (2014-01-01 05:29:00 и 2014-01-01 11:20:00) необходимо найти два кванта, которые относятся к первому и второму моменту времени соответственно.
Если мы обратимся к таблице (см. выше), то увидим, что это кванты с номерами 2 и 18 соответственно. А затем вычесть из номера кванта второго момента времени - номер кванта первого момента времени, получится 18-2=16. Полученная разница будет являться количеством рабочих квантов между двумя моментами времени. Так как 1 рабочий квант это 15 минут, то между моментами времени 2014-01-01 05:29:00 и 2014-01-01 11:20:00 будет 4 рабочих часа.
Отсчёт в квантах рабочего времени от указанной даты¶
При помощи функции AddWorkingQuantsToDate - можно получить ближайшее рабочее время, которое наступит, если добавить к моменту времени некоторое количество рабочих квантов.
/// <summary>
/// Отсчёт рабочего времени от указанной даты.
/// </summary>
/// <param name="dateTimeUTC">Дата\время в UTC, к которому производится добавление.</param>
/// <param name="quants">рабочее время в квантах.</param>
/// <returns>Возвращает дату\время в UTC. Дата округляется до 15 минут в большую сторону.</returns>
DateTime AddWorkingQuantsToDate(DateTime dateTimeUTC, long quants);
Функция принимает параметр dateTimeUTC типа DateTime - момент времени, от которого необходимо начинать отсчёт и параметр quants типа long - количество рабочих квантов, которые необходимо добавить. На выходе возвращает ближайшее рабочее время, которое наступит, если добавить к моменту времени dateTimeUTC рабочие кванты в количестве quants.
Так же можно воспользоваться хранимой функцией CalendarAddWorkQuants.
FUNCTION CalendarAddWorkQuants(@DateUtc datetime, @Quants int) RETURNS datetime
Функция принимает данные аналогично методу из API:
-
@DateUtc - дата\время в UTC, к которому производится добавление.
-
@Quants - рабочее время в квантах (1 квант = 15 минут).
-
Возвращает дата\время в UTC. Дата округляется до 15 минут в большую сторону.
Описание логики работы
Допустим, нам необходимо узнать к какому рабочему дню будет завершено задание, если начать его выполнение сейчас и на его выполнение нужно какое-то количество рабочих часов.
Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию. Не забываем, что при обращении к календарю мы оперируем рабочими квантами (интервалами по 15 минут).
Для примера возьмём моменты времени - 2014-01-01 05:25:00 (момент времени указывается в UTC) и 5 рабочих часов (20 рабочих квантов). Для того, чтобы найти момент времени в ближайшем рабочем дне, когда пройдут 5 рабочих часов относительно момента времени 2014-01-01 05:25:00 нужно для начала найти квант, к которому относится выбранный момент времени.
Если мы обратимся к таблице (см. выше), то увидим, что это квант с номером 2. Далее прибавим 20 квантов (5 часов) к 2-ум и получим 22. Найдём квант с номером 22 и типом 0. Начало этого кванта будет искомым моментом времени 2014-01-01 12:30:00.
Отсчёт в рабочих днях от указанной даты¶
При помощи функции AddWorkingDaysToDate - можно получить ближайшее рабочее время, которое наступит, если добавить к моменту времени некоторое количество расчётных рабочих дней.
/// <summary>
/// Получение даты рабочего времени смещением в рабочих днях относительно заданной даты.
/// </summary>
/// <param name="dateTimeUTC">Дата в UTC.</param>
/// <param name="daysOffset">Смещение в рабочих днях.</param>
/// <returns>Возвращает дату рабочего времени.</returns>
DateTime AddWorkingDaysToDate(DateTime dateTimeUTC, double daysOffset);
Функция принимает параметр dateTimeUTC типа DateTime - момент времени, от которого необходимо начинать отсчёт и параметр daysOffset типа double - количество рабочих дней, которые необходимо добавить. На выходе возвращает ближайшее рабочее время, которое наступит, если добавить к моменту времени dateTimeUTC рабочие дни в количестве daysOffset.
Так же можно воспользоваться хранимой функцией CalendarAddWorkingDaysToDate.
FUNCTION CalendarAddWorkingDaysToDate(@DateTimeUtc datetime, @Offset float) RETURNS datetime
Функция принимает данные аналогично методу из API:
-
@DateTimeUtc - дата\время в UTC, к которому производится добавление.
-
@Offset - рабочее время в днях.
-
Возвращает дата\время в UTC. Дата округляется до 15 минут в большую сторону.
Описание логики работы
Допустим, нам необходимо узнать к какой дате будет завешено задание, если начать его выполнение сейчас и на его выполнение нужно какое-то количество рабочих дней.
Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию. Не забываем, что при обращении к календарю мы оперируем рабочими квантами (интервалами по 15 минут).
Для примера возьмём моменты времени - 2014-01-01 05:25:00 (момент времени указывается в UTC) и 1,5 рабочих дней. Для того, чтобы найти момент времени в ближайшем рабочем дне, когда пройдут 1,5 рабочих дня относительно момента времени 2014-01-01 05:25:00 нужно для начала найти количество квантов в 1,5 рабочих днях. (32 * 1,5 = 48, исходя из 8-ми часового рабочего дня). Следовательно, должно пройти 48 рабочих кванта, с момента времени 2014-01-01 05:25:00
Если мы обратимся к таблице (см. выше), то увидим, что это квант с номером 2. Далее прибавим 48 квантов (1,5 дня) к 2-ум и получим 50. Найдём квант с номером 50 и типом 0. Начало этого кванта будет искомым моментом времени 2014-01-06 11:15:00.
Отсчёт в фактических рабочих днях от указанной даты¶
При помощи функции AddWorkingDaysToDateExact - можно получить ближайшее рабочее время, которое наступит, если добавить к моменту времени некоторое количество фактических рабочих дней.
/// <summary>
/// Добавление нужного количества рабочих дней к дате.
/// </summary>
/// <param name="dateTimeUTC">Дата в UTC.</param>
/// <param name="interval">Количество рабочих дней.</param>
/// <returns>Возвращает дату рабочего времени.</returns>
DateTime CalendarAddWorkingDaysToDateExact(DateTime dateTimeUTC, int interval);
Функция принимает параметр dateTimeUTC типа DateTime - момент времени, от которого необходимо начинать отсчёт и параметр daysOffset типа int - количество рабочих дней, которые необходимо добавить. На выходе возвращает ближайшее рабочее время, которое наступит, если добавить к моменту времени dateTimeUTC рабочие дни в количестве interval.
Так же можно воспользоваться хранимой функцией CalendarAddWorkingDaysToDateExact.
FUNCTION CalendarAddWorkingDaysToDateExact(@dateTimeUTC datetime, @Interval int) RETURNS datetime
Функция принимает данные аналогично методу из API:
-
@dateTimeUTC - дата\время в UTC, к которому производится добавление.
-
@Interval - рабочее время в днях.
-
Возвращает дата\время в UTC. Дата округляется до 15 минут в большую сторону.
Описание логики работы
Берем подмножество квантов календаря за период, в котором гарантированно окажется нужно количество рабочих дней. Эмпирически это (нужное кол-во рабочих дней)*3+14. 14 - это самый длинные в мире каникулы (новогодние). Такая фильтрация нужна, чтобы не обрабатывать весь календарь, что ОЧЕНЬ медленно и ОЧЕНЬ тяжело для сервера. Выбираем все рабочие кванты за этот период. В итоге мы получаем, что на каждый рабочий день есть хотя бы один квант. Конвертируем дату\время начала кванта в дату и делаем дистинкт - т.е. у нас остается всего по одной строке на каждый рабочий день. В итоге мы получили список рабочих дней в этом периоде. Сортируем, нумеруем и выбираем нужный по номеру = (нужное кол-во рабочих дней).
Получение начала первого рабочего кванта рабочего дня, полученного смещением относительно заданной даты¶
При помощи функции GetFirstQuantStart - можно получить начало первого рабочего кванта рабочего дня, полученного смещением относительно заданной даты.
/// <summary>
/// Получение начала первого рабочего кванта рабочего дня,
/// полученного смещением относительно заданной даты.
/// </summary>
/// <param name="dateTimeUTC">Дата в UTC.</param>
/// <param name="daysOffset">Смещение в рабочих днях.</param>
/// <returns>Возвращает дату\время в UTC начала первого рабочего кванта рабочего дня.</returns>
DateTime GetFirstQuantStart(DateTime dateTimeUTC, int daysOffset);
Функция принимает параметр dateTimeUTC типа DateTime - момент времени, от которого необходимо выполнять смещение и параметр daysOffset типа int количество дней, на которые необходимо произвести смещение. На выходе возвращает момент времени, в который начинается ближайший рабочий день. Параметр daysOffset может иметь отрицательное значение, а если он равен нулю, то будет получено начало ближайшего рабочего дня.
Пример использования:
var dayStart = calendarService.GetFirstQuantStart(DateTime.UtcNow, -5);
Так же можно воспользоваться хранимой функцией CalendarGetFirstQuantStart.
FUNCTION CalendarGetFirstQuantStart(@DateTimeUtc datetime, @Offset int) RETURNS datetime
Функция принимает данные аналогично методу из API.
-
@DateTimeUtc - дата\время в UTC, для которой производится расчёт.
-
@Offset - смещение в днях.
-
Возвращает дата/время начала рабочего дня.
Описание логики работы
Допустим нам необходимо узнать, какой рабочий день будет через некоторое количество рабочих дней после момента времени или за несколько дней до и во сколько этот рабочий день начнётся. Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию.
Для примера возьмём моменты времени - 2014-01-01 05:25:00 (момент времени указывается в UTC) и смещение в 1 рабочий день. Для того, чтобы найти начало ближайшего дня смещением относительно заданного момента времени на один день - нужно найти первый рабочий квант, захватывающий момент времени 2014-01-01 05:25:00 + 1 день.
Если мы обратимся к таблице (см. выше), то увидим, что это квант с номером 33. Начало этого кванта будет искомым моментом времени 2014-01-06 05:00:00.
Получение конца последнего рабочего кванта рабочего дня, полученного смещением относительно заданной даты¶
При помощи функции GetLastQuantEnd - можно получить конец первого рабочего кванта рабочего дня, полученного смещением относительно заданной даты.
/// <summary>
/// Получение конца последнего рабочего кванта рабочего дня,
/// полученного смещением относительно заданной даты.
/// </summary>
/// <param name="dateTimeUTC">Дата в UTC.</param>
/// <param name="daysOffset">Смещение в рабочих днях.</param>
/// <returns>Возвращает дату\время в UTC конца последнего рабочего кванта рабочего дня.</returns>
DateTime GetLastQuantEnd(DateTime dateTimeUTC, int daysOffset);
Функция принимает параметр dateTimeUTC типа DateTime - момент времени, от которого необходимо выполнять смещение и параметр daysOffset типа int количество дней, на которые необходимо произвести смещение. На выходе возвращает момент времени, в который заканчивается ближайший рабочий день. Параметр daysOffset может иметь отрицательное значение, а если он равен нулю, то будет получен конец ближайшего рабочего дня.
Пример использования:
var dayEnd = calendarService.GetLastQuantEnd(DateTime.UtcNow, 0);
Так же можно воспользоваться хранимой функцией CalendarGetLastQuantStart.
FUNCTION CalendarGetLastQuantStart(@DateTimeUtc datetime, @Offset int) RETURNS datetime
Функция принимает данные аналогично методу из API.
-
@DateTimeUtc - дата\время в UTC, для которой производится расчёт.
-
@Offset - смещение в днях.
-
Возвращает - дата/время начала рабочего дня.
Описание логики работы
Допустим нам необходимо узнать, какой рабочий день будет через некоторое количество рабочих дней после момента времени или за несколько дней до и во сколько этот рабочий день закончится. Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию.
Для примера возьмём моменты времени - 2014-01-01 05:25:00 (момент времени указывается в UTC) и смещение в 1 рабочий день. Для того, чтобы найти конец ближайшего дня смещением относительно заданного момента времени на один день - нужно найти первый рабочий квант, захватывающий момент времени 2014-01-01 05:25:00 + 1 день. Начало этого кванта будет 2014-01-06 05:00:00. Затем найти начало следующего дня при помощи полученных данных. Это 2014-01-07 00:00:00. Далее найдём все рабочие кванты начало которых больше 2014-01-06 05:00:00, но меньше 2014-01-07 00:00:00 и, отсортировав по убыванию возьмём первый из списка квант. Это будет квант с номером 64 и типом 0. Его время окончания и будет искомым значением 2014-01-06 15:00:00.
Служебные методы¶
Служебные методы реализуют специфический служебный функционал, такой как перестраивание календаря и проверка на наличие ошибок. Вызывать эти методы внутри пользовательских расширений запрещается.
Перестраивание календаря¶
При помощи функции RebuildCalendar - можно вызвать пересчёт календаря аналогично тому, как календарь перестраивается из карточки.
/// <summary>
/// Вызывает перестройку календаря на основе внесённых исключений.
/// </summary>
/// <param name="operationGuid">Guid операции.</param>
/// <param name="dateTimeStartUTC">Первая дата в UTC.</param>
/// <param name="dateTimeEndUTC">Вторая дата в UTC (должна быть больше первой).</param>
/// <param name="workTimeStartUTC">Начало рабочего дня.</param>
/// <param name="workTimeEndUTC">Конец рабочего дня.</param>
/// <param name="lunchTimeStartUTC">Начало обеда.</param>
/// <param name="lunchTimeEndUTC">Конец обеда.</param>
void RebuildCalendar(
Guid operationGuid,
DateTime dateTimeStartUTC,
DateTime dateTimeEndUTC,
DateTime workTimeStartUTC,
DateTime workTimeEndUTC,
DateTime lunchTimeStartUTC,
DateTime lunchTimeEndUTC);
Функция реализована с использованием API операций и принимает параметр operationGuid типа Guid - ID операции, а так же параметры начала/конца рабочего дня, начала/конца обеденного перерыва и период действия календаря.
Хранимая процедура CalendarPrepareQuants:
CREATE PROCEDURE CalendarPrepareQuants
(
@PeriodStartTimeUTC datetime, -- дата\время, начиная с которой должны быть перерасчитаны кванты
@PeriodEndTimeUTC datetime, -- дата\время, до которой должные быть перерасчитаны кванты.
@WorkDayStartTimeUTC datetime, -- время начала рабочего дня.
@WorkDayEndTimeUTC datetime, -- время конца рабочего дня.
@LaunchStartTimeUTC datetime, -- время начала обеда.
@LaunchEndTimeUTC datetime-- время конца обеда.
)
Проверка календаря на наличие ошибок¶
При помощи функции ValidateCalendar - можно проверить календарь на наличие ошибок.
/// <summary>
/// Проверяет календарь на отсутствие пропусков между квантами.
/// </summary>
/// <param name="message">Сообщение с кратким отчётом.</param>
/// <returns>Признак наличия ошибок.</returns>
bool ValidateCalendar(out string message);
Функция out принимает параметр message типа string, в который записывается краткий отчёт. На выходе идёт признак наличия ошибок.
Пример использования
string message = "";
var isErrors = calendarService.ValidateCalendar(out message);
Проверка календаря на наличие ошибок реализована исключительно как функция API и не имеет своей хранимой процедуры или функции.
Применение календаря на больших выборках/отчётах¶
Для больших выборок или отчётов использовать хранимые функции не целесообразно. Для таких случаев целесообразнее общаться с таблицей календаря напрямую.
Следующий пример иллюстрирует, как можно получить количество рабочих часов выделенных на задания, зная дату\время создания задания и дату\время момента, когда задание должно быть выполнено. Мы просто считаем разницу между номерами квантов.
select ((cqu2.QuantNumber - cqu1.QuantNumber)/4.0) as WorkHoursLeft, *
from Tasks as tsk with(nolock)
outer apply
(
select top 1 q.QuantNumber
from CalendarQuants q with(nolock)
where q.StartTimeUTC <= tsk.Created
order by q.StartTimeUTC desc
) as cqu1
outer apply
(
select top 1 q.QuantNumber
from CalendarQuants q with(nolock)
where q.StartTimeUTC <= tsk.Planned
order by q.StartTimeUTC desc
) as cqu2
Warning
Пожалуйста, не используйте вариант запроса, представленный ниже. Несмотря на свою внешнюю простоту, он выполняется в тясячи раз медленнее!
Неправильный запрос
select ((cqu2.QuantNumber - cqu1.QuantNumber)/4.0) as WorkHoursLeft, *
from Tasks as tsk with(nolock)
left join CalendarQuants as cqu1 with(nolock) on cqu1.StartTimeUTC <= tsk.Created and cqu1.EndTimeUTC > tsk.Created
left join CalendarQuants as cqu2 with(nolock) on cqu2.StartTimeUTC <= tsk.Planned and cqu2.EndTimeUTC > tsk.Planned
Причина лучшей производительности первого варианта
И в первом и во втором случае используется поиск по кластерному индексу:
--Первый вариант
Clustered Index Seek(
OBJECT:([tessa].[dbo].[CalendarQuants].[idx_CalendarQuants_StartTimeUTCEndTimeUTC] AS [q]),
SEEK:([q].[StartTimeUTC] <= [tessa].[dbo].[Tasks].[Planned] as [tsk].[Planned]) ORDERED FORWARD)
--Второй вариант
Clustered Index Seek(
OBJECT:([tessa].[dbo].[CalendarQuants].[idx_CalendarQuants_StartTimeUTCEndTimeUTC] AS [cqu2]),
SEEK:([cqu2].[StartTimeUTC] <= [tessa].[dbo].[Tasks].[Planned] as [tsk].[Planned]),
WHERE:([tessa].[dbo].[CalendarQuants].[EndTimeUTC] as [cqu2].[EndTimeUTC]>[tessa].[dbo].[Tasks].[Planned] as [tsk].[Planned]) ORDERED FORWARD)
Но в первом варианте на каждое задание происходит ровно одно чтение значения (благодаря топу и индексу построенному по возрастанию), а во втором варианте, для каждого из найденных значений (дата планируемого завершения больше даты начала кванта) проверяется, что дата завершения меньше даты конца кванта - кол-во чтений зависит от размера таблицы календаря, и близости самого старого найденного кванта к концу таблицы.