Проверка периодов дат в диапазоне нескольких принятых периодов (например, лицензионные периоды и периоды использования)

#sql #sql-server #vb.net #tsql

#sql #sql-сервер #vb.net #tsql

Вопрос:

Создание запроса check / DQ, который проверяет, было ли лицензировано использование чего-либо в течение этого времени использования.

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

 Licensed:       |------A-----------------||-C-|                     |-----E------|    |---F--|  |--G--|
                             |----------B----------||-D-|                                  |---H---|
Use:        xxxx1-||--2--| |-3-| |-4-| |--5--|    |-6-||-7xxx x8x xx9-|        |--x10x--|   |-11--|
 

Тестирование — это период использования, который полностью существует в лицензионных диапазонах.

  • U1 — сбой, поскольку это только часть LA
  • U2 — передача с использованием LA
  • U3 — передача с использованием LA
  • U4 — передача с использованием LB ИЛИ LA
  • U5 — передача с использованием LB ИЛИ LA amp; LC
  • U6 — передача с использованием LB amp; LD
  • U7 — сбой части перекрытия LD
  • U8 — сбой без перекрытий
  • U9 — сбой части перекрытия файла
  • U10 — сбой перекрывает оба файла и LF, но не полностью
  • U11 — передача с использованием LH

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

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

Одним из предположений было бы сначала упростить периоды L до суммированных диапазонов L (с использованием CTE), а затем выполнить not in range в вариантах использования? ie:

 Licensed:       |------A---------------------------|             |-----B------|   |-------C------|
Use:        xxx-1-||--2--| |-3-| |-4-| |--5--|   |-6xxx xx7x  xx8-|        |--x9x--|   |-10--|
 

Хотя это будет более упрощено при предварительной фильтрации лицензий, которые охватывают вариант использования. ie:

U2 будет рассматривать только A, поскольку диапазон дат L охватывает только LA.

 Licensed:       |------A-----------------|
Use:                 |--2--|
 

Надеюсь, это интересный пример, который будет выполняться как проверка на лету в приложении в реальной жизни — поэтому, хотя я мог бы использовать код для этого, я надеюсь, что он будет быстрее и понятнее / читабелен в SQL, и я мог бы превратить его в отчет DQ.

Как бы вы это сделали? пример.

Использование SQL Server 2016 и / или VB.Net .

Пример данных:

 DROP TABLE IF EXISTS #License
DROP TABLE IF EXISTS #Use

--License table
CREATE TABLE #License 
(
     ID char(1) NOT NULL, 
     FromDate date NOT NULL, 
     ToDate date NOT NULL
)

INSERT INTO #License SELECT 'A', '2020-01-01','2020-03-01'
INSERT INTO #License SELECT 'B', '2020-02-01','2020-03-10'
INSERT INTO #License SELECT 'C', '2020-03-02','2020-03-05'
INSERT INTO #License SELECT 'D', '2020-03-11','2020-03-20'
INSERT INTO #License SELECT 'E', '2020-06-01','2020-07-01'
INSERT INTO #License SELECT 'F', '2020-08-01','2020-08-15'
INSERT INTO #License SELECT 'G', '2020-09-01','2020-09-15'
INSERT INTO #License SELECT 'H', '2020-08-10','2020-09-10'
--SELECT *, DATEDIFF(dd,FromDate,ToDate) AS Days FROM #License

--Use Cases
CREATE TABLE #Use 
(
     ID int NOT NULL, 
     FromDate date NOT NULL, 
     ToDate date NOT NULL, 
     ExpectedResult bit NOT NULL
)

INSERT INTO #Use SELECT 1, '2019-12-01','2020-01-15', 0
INSERT INTO #Use SELECT 2, '2020-01-16','2020-01-20', 1
INSERT INTO #Use SELECT 3, '2020-01-20','2020-02-05', 1
INSERT INTO #Use SELECT 4, '2020-02-10','2020-02-20', 1
INSERT INTO #Use SELECT 5, '2020-03-01','2020-03-04', 1
INSERT INTO #Use SELECT 6, '2020-03-08','2020-03-15', 1
INSERT INTO #Use SELECT 7, '2020-03-18','2020-03-25', 0
INSERT INTO #Use SELECT 8, '2020-04-01','2020-04-30', 0
INSERT INTO #Use SELECT 9, '2020-05-20','2020-06-10', 0
INSERT INTO #Use SELECT 10, '2020-06-10','2020-08-10', 0
INSERT INTO #Use SELECT 11, '2020-08-12','2020-09-08', 1
--SELECT *, DATEDIFF(dd,FromDate,ToDate) AS Days FROM #Use

-- Fails
--SELECT U.ID, ExpectedResult
--    , CASE WHEN L.ID IS NOT NULL THEN 1 ELSE 0 END AS Result
--    , CASE WHEN ExpectedResult = (CASE WHEN L.ID IS NOT NULL THEN 1 ELSE 0 END) THEN 1 ELSE 0 END AS Success
--    ,*
--FROM #Use U
--LEFT JOIN #License L ON 
--    U.FromDate BETWEEN L.FromDate AND L.ToDate 
--    AND U.ToDate BETWEEN L.FromDate AND L.ToDate
----WHERE U.ID = 1
;
-- Using Gordons answer.
WITH lt AS (
    SELECT dateadd(day, 1, l.ToDate) AS test_date --, ID
    FROM #License l
)
SELECT Results.*, U.ExpectedResult, CASE WHEN U.ExpectedResult = Results.Result THEN 1 ELSE 0 END AS ExpectedIsResult
FROM (
    SELECT u.ID --, *
    , COUNT(*) AS DateTests -- the amount of dates tested against licenses (USE and LICENSE To 1 within USE)
    , COUNT(l.ID) AS TestsMatchedLicense -- the amount that matched a LICENSE
    , (CASE WHEN COUNT(*) = COUNT(l.ID) THEN 1 ELSE 0 END) AS Result -- any date that didn't match that was required Fails the result.

    FROM #Use u CROSS APPLY
         (SELECT v.test_date
          FROM (values (u.FromDate), (u.ToDate) ) AS v(test_date)
          UNION ALL
          SELECT lt.test_date --Check the the U goes past a license
          FROM lt
          WHERE lt.test_date >= u.FromDate AND
                lt.test_date <= u.ToDate
         ) AS slt 
         --Match USE range dates to a LICENSE   AND   LICENSE TO 1 within USE range matches a LICENSE - as the LICENSE range may not cover the whole USE period but another license may.
         LEFT JOIN #License l ON slt.test_date BETWEEN l.FromDate AND l.ToDate 
    GROUP BY u.ID
) AS Results INNER JOIN #Use U ON Results.ID = U.ID
 

Использование простой попытки, описанной выше, имело большой успех, за исключением U6 — однако не все случаи соответствовали всем соответствующим лицензиям, т.Е. U5

Комментарии:

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

2. Извините, я создам пример данных sql и попытаюсь — может потребоваться некоторое время для попытки.

Ответ №1:

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

 with lt as (
      select l.*, dateadd(day, 1, l.todate) as test_date
      from license l
     )
select u.id,
       (case when count(*) = count(l.id) then 'Pass'
             else 'Fail'
        end)
from uset u cross apply
     (select v.test_date
      from (values (u.fromdate), (u.todate)
           ) v(test_date)
      union all
      select lt.test_date
      from lt
      where lt.fromdate >= u.fromdate and
            lt.todate <= u.todate
     ) lt left join
     license l
     on lt.test_date between l.fromdate and l.todate
group by u.id;
 

Вот скрипка db<> .

Комментарии:

1. Спасибо Гордон за этот ответ, теперь попробовав его на примере и обновив ваш ответ парой пропущенных изменений (которые я обновил в сценарии в вопросе), я немного затрудняюсь понять, как это работает — поскольку это работает в соответствии с ожидаемым. Не могли бы вы подробнее рассказать о том, как, возможно, работают все сегменты запроса. В противном случае золотая звезда, если вы сделали это из воздуха.

2. Хорошо, я думаю, я понимаю — вы сопоставляете все даты диапазона ИСПОЛЬЗОВАНИЯ с диапазоном ЛИЦЕНЗИЙ или, если ЛИЦЕНЗИЯ не охватывает весь диапазон ИСПОЛЬЗОВАНИЯ, найдите один рядом с концом ЛИЦЕНЗИИ. Затем сопоставьте все даты, чтобы проверить, действительно ли они соответствуют ЛИЦЕНЗИИ … конечно, я бы сам этого не понял. Спасибо.

3. @CooPzZ. .. Вы отредактировали вопрос с использованием выборочных данных, поэтому я отредактировал ответ с помощью скрипки SQL и ваших имен столбцов.

4. пример не подходит для U10. Я изменил 2-й выбор объединения, чтобы вместо этого использовать test_dates, и он работал, как ожидалось. Я бы также воздержался от использования ‘lt’ в качестве имени CTE и названия соединенных дат — это то, что некоторое время смущало меня при компиляции запроса, в моей голове, поскольку это были разные данные.