2009-09-09 8 views
2

У меня есть база данных SQL Server 2005, которая содержит таблицу Memberships.Диапазон дат Пересечение Разделение в SQL

Схема таблицы:

PersonID int, Surname nvarchar(30), FirstName nvarchar(30), Description nvarchar(100), StartDate datetime, EndDate datetime

Я сейчас работаю над функцией сетки, которая показывает ломка членство на человеке. Одним из требований является разделение рядов членства, где есть пересечение диапазонов дат. Пересечение должно быть связано с фамилией и именем FirstName, т. Е. Разбиение происходит только с записями о членстве одной и той же фамилии и FirstName.

Пример таблицы данных:

18 Smith John Poker Club 01/01/2009 NULL 
18 Smith John Library  05/01/2009 18/01/2009 
18 Smith John Gym   10/01/2009 28/01/2009 
26 Adams Jane Pilates  03/01/2009 16/02/2009

Ожидаемый результат набор:

18 Smith John Poker Club     01/01/2009 04/01/2009 
18 Smith John Poker Club/Library  05/01/2009 09/01/2009 
18 Smith John Poker Club/Library/Gym 10/01/2009 18/01/2009 
18 Smith John Poker Club/Gym   19/01/2009 28/01/2009 
18 Smith John Poker Club     29/01/2009 NULL 
26 Adams Jane Pilates      03/01/2009 16/02/2009

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

+0

Как ваш дизайн обрабатывает несколько членов с тем же именем/фамилией? Не исключено, что данные образца, которые вы предоставили, относятся к трем различным людям, которые называются Джон Смит. –

+0

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

+0

Есть личный идентификатор - я бы полностью проигнорировал бит названия до окончательного вывода. Выберите – MartW

ответ

2

Проблема, с которой вы столкнулись с этой проблемой, заключается в том, что по мере роста набора данных решения по ее решению с TSQL не будут хорошо масштабироваться. Ниже приведена серия временных таблиц, созданных на лету для решения проблемы. Он разбивает каждый элемент диапазона дат в соответствующие дни, используя таблицу чисел. В этом случае он не будет масштабироваться, в основном из-за ваших значений NULL с открытым диапазоном, которые кажутся нестабильными, поэтому вам нужно поменять местами в фиксированную дату далеко в будущее, что ограничивает диапазон преобразования до допустимого периода времени. Вероятно, вы можете увидеть лучшую производительность, построив таблицу дней или таблицу календаря с соответствующей индексацией для оптимизированного рендеринга каждого дня.

После того, как диапазоны разделены, описания объединяются с использованием XML PATH, так что каждый день в серии диапазонов содержит все описания, перечисленные для него. Нумерация строк с помощью PersonID и Date позволяет найти первую и последнюю строку каждого диапазона с помощью двух проверок NOT EXISTS, чтобы найти экземпляры, в которых предыдущая строка не существует для соответствующего набора PersonID и описания, или где следующая строка не имеет значения, t существует для сопоставления идентификатора PersonID и описания.

Этот результирующий набор затем нумеруется с помощью ROW_NUMBER, чтобы их можно было спаривать, чтобы построить окончательные результаты.

/* 
SET DATEFORMAT dmy 
USE tempdb; 
GO 
CREATE TABLE Schedule 
(PersonID int, 
Surname nvarchar(30), 
FirstName nvarchar(30), 
Description nvarchar(100), 
StartDate datetime, 
EndDate datetime) 
GO 
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Poker Club', '01/01/2009', NULL) 
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Library', '05/01/2009', '18/01/2009') 
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Gym', '10/01/2009', '28/01/2009') 
INSERT INTO Schedule VALUES (26, 'Adams', 'Jane', 'Pilates', '03/01/2009', '16/02/2009') 
GO 

*/ 

SELECT 
PersonID, 
Description, 
theDate 
INTO #SplitRanges 
FROM Schedule, (SELECT DATEADD(dd, number, '01/01/2008') AS theDate 
    FROM master..spt_values 
    WHERE type = N'P') AS DayTab 
WHERE theDate >= StartDate 
    AND theDate <= isnull(EndDate, '31/12/2012') 

SELECT 
ROW_NUMBER() OVER (ORDER BY PersonID, theDate) AS rowid, 
PersonID, 
theDate, 
STUFF((
    SELECT '/' + Description 
    FROM #SplitRanges AS s 
    WHERE s.PersonID = sr.PersonID 
    AND s.theDate = sr.theDate 
    FOR XML PATH('') 
), 1, 1,'') AS Descriptions 
INTO #MergedDescriptions 
FROM #SplitRanges AS sr 
GROUP BY PersonID, theDate 


SELECT 
ROW_NUMBER() OVER (ORDER BY PersonID, theDate) AS ID, 
* 
INTO #InterimResults 
FROM 
(
SELECT * 
FROM #MergedDescriptions AS t1 
WHERE NOT EXISTS 
    (SELECT 1 
    FROM #MergedDescriptions AS t2 
    WHERE t1.PersonID = t2.PersonID 
    AND t1.RowID - 1 = t2.RowID 
    AND t1.Descriptions = t2.Descriptions) 
UNION ALL 
SELECT * 
FROM #MergedDescriptions AS t1 
WHERE NOT EXISTS 
    (SELECT 1 
    FROM #MergedDescriptions AS t2 
    WHERE t1.PersonID = t2.PersonID 
    AND t1.RowID = t2.RowID - 1 
    AND t1.Descriptions = t2.Descriptions) 
) AS t 

SELECT DISTINCT 
PersonID, 
Surname, 
FirstName 
INTO #DistinctPerson 
FROM Schedule 

SELECT 
t1.PersonID, 
dp.Surname, 
dp.FirstName, 
t1.Descriptions, 
t1.theDate AS StartDate, 
CASE 
    WHEN t2.theDate = '31/12/2012' THEN NULL 
    ELSE t2.theDate 
END AS EndDate 
FROM #DistinctPerson AS dp 
JOIN #InterimResults AS t1 
ON t1.PersonID = dp.PersonID 
JOIN #InterimResults AS t2 
ON t2.PersonID = t1.PersonID 
    AND t1.ID + 1 = t2.ID 
    AND t1.Descriptions = t2.Descriptions 

DROP TABLE #SplitRanges 
DROP TABLE #MergedDescriptions 
DROP TABLE #DistinctPerson 
DROP TABLE #InterimResults 

/* 

DROP TABLE Schedule 

*/ 

выше решение также будет обрабатывать пробелы между дополнительными описаниями, так что если вы должны были добавить еще один Описание для PersonID 18, оставляя зазор:

INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Gym', '10/02/2009', '28/02/2009') 

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

1

Попробуйте это

SET DATEFORMAT dmy 
DECLARE @Membership TABLE( 
    PersonID int, 
    Surname  nvarchar(16), 
    FirstName nvarchar(16), 
    Description nvarchar(16), 
    StartDate datetime, 
    EndDate  datetime) 
INSERT INTO @Membership VALUES (18, 'Smith', 'John', 'Poker Club', '01/01/2009', NULL) 
INSERT INTO @Membership VALUES (18, 'Smith', 'John','Library', '05/01/2009', '18/01/2009') 
INSERT INTO @Membership VALUES (18, 'Smith', 'John','Gym', '10/01/2009', '28/01/2009') 
INSERT INTO @Membership VALUES (26, 'Adams', 'Jane','Pilates', '03/01/2009', '16/02/2009') 

--Program Starts 
declare @enddate datetime 
--Measuring extreme condition when all the enddates are null(i.e. all the memberships for all members are in progress) 
-- in such a case taking any arbitary date e.g. '31/12/2009' here else add 1 more day to the highest enddate 
select @enddate = case when max(enddate) is null then '31/12/2009' else max(enddate) + 1 end from @Membership 

--Fill the null enddates 
; with fillNullEndDates_cte as 
(
    select 
      row_number() over(partition by PersonId order by PersonId) RowNum 
      ,PersonId 
      ,Surname 
      ,FirstName 
      ,Description 
      ,StartDate 
      ,isnull(EndDate,@enddate) EndDate 
    from @Membership 
) 
--Generate a date calender 
, generateCalender_cte as 
(
    select 
     1 as CalenderRows 
     ,min(startdate) DateValue 
    from @Membership 
     union all 
     select 
      CalenderRows+1 
      ,DateValue + 1 
     from generateCalender_cte 
     where DateValue + 1 <= @enddate 
) 
--Generate Missing Dates based on Membership 
,datesBasedOnMemberships_cte as 
(
    select 
      t.RowNum 
      ,t.PersonId 
      ,t.Surname 
      ,t.FirstName 
      ,t.Description   
      , d.DateValue 
      ,d.CalenderRows 
    from generateCalender_cte d 
    join fillNullEndDates_cte t ON d.DateValue between t.startdate and t.enddate 
) 
--Generate Dscription Based On Membership Dates 
, descriptionBasedOnMembershipDates_cte as 
(
    select  
     PersonID 
     ,Surname 
     ,FirstName 
     ,stuff((
      select '/' + Description 
      from datesBasedOnMemberships_cte d1 
      where d1.PersonID = d2.PersonID 
      and d1.DateValue = d2.DateValue 
      for xml path('') 
     ), 1, 1,'') as Description 
     , DateValue 
     ,CalenderRows 
    from datesBasedOnMemberships_cte d2 
    group by PersonID, Surname,FirstName,DateValue,CalenderRows 
) 
--Grouping based on membership dates 
,groupByMembershipDates_cte as 
(
    select d.*, 
    CalenderRows - row_number() over(partition by Description order by PersonID, DateValue) AS [Group] 
    from descriptionBasedOnMembershipDates_cte d 
) 
select PersonId 
,Surname 
,FirstName 
,Description 
,convert(varchar(10), convert(datetime, min(DateValue)), 103) as StartDate 
,case when max(DateValue)= @enddate then null else convert(varchar(10), convert(datetime, max(DateValue)), 103) end as EndDate 
from groupByMembershipDates_cte 
group by [Group],PersonId,Surname,FirstName,Description 
order by PersonId,StartDate 
option(maxrecursion 0) 
0

[Только много, много лет спустя.]

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

Смотрите, если ниже помощь:

  1. Документация: https://github.com/Quebe/SQL-Algorithms/blob/master/Temporal/Date%20Segment%20Manipulation/DateSegments_AlignWithinTable.md

  2. хранимых процедур: https://github.com/Quebe/SQL-Algorithms/blob/master/Temporal/Date%20Segment%20Manipulation/DateSegments_AlignWithinTable.sql

Например, ваш вызов может выглядеть следующим образом:

EXEC dbo.DateSegments_AlignWithinTable 
@tableName = 'tableName', 
@keyFieldList = 'PersonID', 
@nonKeyFieldList = 'Description', 
@effectivveDateFieldName = 'StartDate', 
@terminationDateFieldName = 'EndDate' 

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

SELECT 
    PersonID, StartDate, EndDate, 

    SUBSTRING ((SELECT ',' + [Description] FROM AlignedDataTable AS innerTable 
     WHERE 
      innerTable.PersonID = AlignedDataTable.PersonID 
      AND (innerTable.StartDate = AlignedDataTable.StartDate) 
      AND (innerTable.EndDate = AlignedDataTable.EndDate) 
     ORDER BY id 
     FOR XML PATH ('')), 2, 999999999999999) AS IdList 

FROM AlignedDataTable 
GROUP BY PersonID, StartDate, EndDate 
ORDER BY PersonID, StartDate 
Смежные вопросы