2009-09-09 12 views
2

J'ai une base de données SQL Server 2005 qui contient une table appelée Memberships.Fractionnement d'intersection de plages de dates dans SQL

Le schéma de la table est:

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

Je travaille actuellement sur une fonctionnalité de grille qui montre une cassure des adhésions par personne. L'une des exigences est de diviser les lignes d'appartenance lorsqu'il existe une intersection de plages de dates. L'intersection doit être liée par le nom de famille et le prénom, c.-à-d. Que les divisions se produisent uniquement avec les enregistrements d'appartenance du même nom et prénom.

données de table Exemple:

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

Résultat attendu jeu:

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

Est-ce que quelqu'un a une idée comment je pourrais écrire une procédure stockée qui renvoie un jeu de résultats qui a la cassure décrit ci-dessus.

+0

Comment votre conception gère-t-elle plusieurs membres avec le même prénom/nom? Ce n'est pas au-delà des limites de la possibilité que les exemples de données que vous avez fournis se rapportent à trois personnes différentes appelées John Smith. –

+0

C'est un point valide, dont j'ai édité ma question pour refléter cette possibilité. Je suis en effet en train de stocker un identifiant pour chaque personne, mais au moment où j'ai écrit la question je ne pensais pas à des noms en double. Bravo pour les commentaires. – user168369

+0

Il y a un PersonID - j'ignorerais complètement le nom jusqu'à la sortie finale. Sélectionnez – MartW

Répondre

2

Le problème que vous allez rencontrer avec ce problème est qu'à mesure que l'ensemble de données se développe, les solutions pour le résoudre avec TSQL ne seront pas bien adaptées. Le tableau ci-dessous utilise une série de tables temporaires construites à la volée pour résoudre le problème. Il divise chaque entrée de plage de dates dans ses jours respectifs à l'aide d'une table de nombres. C'est là qu'il ne sera pas mis à l'échelle, principalement en raison de vos valeurs NULL à intervalle ouvert qui semblent être inifinity, de sorte que vous devez permuter une date fixe loin dans le futur qui limite la plage de conversion à une durée possible. Vous pourriez probablement voir de meilleures performances en construisant une table de jours ou une table de calendrier avec une indexation appropriée pour un rendu optimisé de chaque jour. Une fois les plages divisées, les descriptions sont fusionnées à l'aide de XML PATH, de sorte que chaque jour de la série comporte toutes les descriptions répertoriées. Numérotation des lignes par PersonID et Date permet de trouver la première et dernière rangée de chaque plage en utilisant deux vérifications NOT EXISTS pour trouver des instances où une ligne précédente n'existe pas pour un ensemble PersonID et Description correspondant, ou où la ligne suivante doesn ' t existe pour un ID de personne et un ensemble de descriptions correspondants.

Cet ensemble de résultats est ensuite renuméroté à l'aide de ROW_NUMBER afin qu'ils puissent être associés pour générer les résultats finaux.

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

*/ 

La solution ci-dessus traitera également des écarts entre les descriptions supplémentaires, donc si vous deviez ajouter une autre description pour PersonID 18 en laissant un espace:

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

Il va combler le vide de manière appropriée. Comme indiqué dans les commentaires, vous ne devriez pas avoir d'informations de nom dans ce tableau, il devrait être normalisé à une table de personnes qui peut être joint à dans le résultat final. J'ai simulé cette autre table en utilisant un SELECT DISTINCT pour créer une table temporaire pour créer ce JOIN.

1

Essayez cette

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

[Seulement de nombreuses années plus tard.]

J'ai créé une procédure stockée qui alignera et briser les segments par une cloison dans une seule table, et vous pouvez utiliser ces sauts alignés pour faire pivoter la description dans une colonne irrégulière en utilisant une sous-requête et XML PATH.

Voir si le dessous de l'aide:

  1. Documentation: https://github.com/Quebe/SQL-Algorithms/blob/master/Temporal/Date%20Segment%20Manipulation/DateSegments_AlignWithinTable.md

  2. procédure stockée: https://github.com/Quebe/SQL-Algorithms/blob/master/Temporal/Date%20Segment%20Manipulation/DateSegments_AlignWithinTable.sql

Par exemple, votre appel pourrait ressembler à:

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

Vous voudrez capturer le résultat (qui est une table) dans une autre table ou une table temporaire (en supposant qu'il est appelé "AlignedDataTable" dans l'exemple ci-dessous). Ensuite, vous pouvez pivoter en utilisant une sous-requête.

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