1
votes

Requête pour les enregistrements qui étaient actifs pendant une plage de dates spécifiée

J'ai une table qui enregistre chaque fois qu'un certain champ change pour un élément, avec la date du changement. Je dois interroger les données pour trouver tous les éléments pour lesquels ce champ avait une valeur spécifique à tout moment pendant une plage de dates demandée.

En d'autres termes, si l'élément avait cette valeur au début, à la fin ou à tout moment pendant les données plage, il doit être inclus.

Exemple de données:

-- Sample data
create table #Temp (
    ItemID    char,
    Valid     bit,
    StartDate date
);

insert into #Temp (ItemID, Valid, StartDate)
values ('A', 1, '2015-01-01'),
       ('B', 0, '2015-01-01'),
       ('B', 1, '2017-03-01'),
       ('C', 1, '2015-01-01'),
       ('C', 0, '2017-04-01'),
       ('D', 0, '2015-01-01'),
       ('D', 1, '2017-05-01'),
       ('D', 0, '2017-06-01'),
       ('E', 1, '2015-01-01'),
       ('E', 0, '2017-05-01'),
       ('E', 1, '2017-06-01'),
       ('F', 1, '2015-01-01'),
       ('F', 0, '2018-02-01'),
       ('G', 1, '2017-12-31'),
       ('V', 0, '2015-01-01'),
       ('V', 1, '2018-02-01'),
       ('W', 1, '2015-01-01'),
       ('W', 0, '2016-01-01'),
       ('X', 0, '2015-01-01'),
       ('Y', 1, '2018-01-01'),
       ('Z', 1, '2015-01-01'),
       ('Z', 0, '2017-01-01');

Donc, si j'ai besoin de tous les éléments qui étaient valides en 2017, la requête inclurait:

  • A (valable depuis 2015)
  • B (devenu valide en 2017)
  • C (était valide jusqu'à mi-2017)
  • D (était valide pendant un mois en 2017)
  • E (était valide au début et à la fin de 2017)
  • F (était valable tout au long de 2017)
  • G (devenu valide en 2017)

La requête n'inclurait pas V, W, X, Y ou Z - aucun d'entre eux n'était valide en 2017. (Faites particulièrement attention à G & Z, qui sont des cas extrêmes!) p>

Item  Valid  Date Changed
----  -----  ------------
A     Yes    2015-01-01

B     No     2015-01-01
B     Yes    2017-03-01

C     Yes    2015-01-01
C     No     2017-04-01

D     No     2015-01-01
D     Yes    2017-05-01
D     No     2017-06-01

E     Yes    2015-01-01
E     No     2017-05-01
E     Yes    2017-06-01

F     Yes    2015-01-01
F     No     2018-02-01

G     Yes    2017-12-31

V     No     2015-01-01
V     Yes    2018-02-01

W     Yes    2015-01-01
W     No     2016-01-01

X     No     2015-01-01

Y     Yes    2018-01-01

Z     Yes    2015-01-01
Z     No     2017-01-01

Pour info, voici quelques autres questions SO que j'ai trouvées et qui posent des questions similaires, mais pas exactement les mêmes:


0 commentaires

4 Réponses :


0
votes

Voici la solution que j'ai trouvée:

ItemID
------
A
B
C
D
E
F
G

Résultat:

-- Date range includes all of 2017
declare
    @beginSearchDate date = '2017-01-01',
    @endSearchDate date = '2017-12-31';

with
    -- CTE: Existing data combined with current value as of today
    a as (
        select ItemID, Valid, StartDate
        from #Temp
        union
        select t1.ItemID, t1.Valid, convert(date, getdate())
        from (
                    select ItemID, max(StartDate) as LatestStartDate
                    from #Temp
                    group by ItemID
                ) as t2
                inner join #Temp as t1
                        on t1.ItemID = t2.ItemID
                            and t1.StartDate = t2.LatestStartDate
    ),
    -- CTE: Current and previous values included in each record
    b as (
        select a1.*,
                lag(a1.Valid) over ( partition by a1.ItemID order by a1.StartDate )
                    as PrevValid,
                lag(a1.StartDate) over ( partition by a1.ItemID order by a1.StartDate )
                    as PrevStartDate
        from a as a1
                inner join a as a2
                        on a1.ItemID = a2.ItemID
                            and a1.StartDate = a2.StartDate
    ),
    -- CTE: Values as a series of date ranges
    c as (
        select distinct ItemID,
                        StartDate     as UntilDate,
                        PrevValid     as Valid,
                        PrevStartDate as FromDate
        from b
        where PrevValid is not null
    )

-- Find all records where date range overlaps
select distinct ItemID
from c
where Valid = 1
    and FromDate <= @endSearchDate
    and UntilDate > @beginSearchDate
order by ItemID;


0 commentaires

0
votes

Voici mon coup d'œil. Je construis la première table avec les éléments qui avaient un drapeau valide = 1 qui tombait n'importe où en dessous de la date de fin. Cela prendrait en compte l'élément A ou un élément similaire.

Je l'ai ensuite comparé jusqu'à la dernière date non valide pour chaque élément s'il en avait un, puis je l'ai filtré par date.

declare
    @beginSearchDate date = '2017-01-01',
    @endSearchDate date = '2017-12-31';


;WITH CTE as (  
 select itemid, VALID, MAX(StartDate) stDate from #temp
       where valid <> 0 and StartDate <= @endSearchDate
           group by itemID, VALID

      )
    SELECT t1.ItemID, VALID , stDate 
        from CTE t1
        outer apply (
             SELECT ItemID, MAX(StartDate) inValDate from #Temp 
               where Valid = 0 
               and StartDate <= @endSearchDate 
               and ItemID = t1.ItemID GROUP BY ItemID) t2
    WHERE t2.inValDate IS NULL 
         or (t1.stDate > t2.inValDate OR t1.stDate > @beginSearchDate OR t2.inValDate > @beginSearchDate)


0 commentaires

1
votes

Voici une solution

DECLARE @SD DATE = '2017-01-01',
        @ED DATE = '2017-12-31';

WITH BSD AS
(
SELECT *,
       LAST_VALUE(Valid) OVER(PARTITION BY ItemID ORDER BY StartDate) LV,
       COUNT(1) OVER(PARTITION BY ItemID ORDER BY StartDate DESC) CNT
FROM #Temp
WHERE StartDate <= @SD
)
SELECT ItemID
FROM BSD
WHERE LV = 1 AND CNT = 1
UNION 
SELECT ItemID
FROM #Temp
WHERE Valid = 1
      AND
      StartDate <= @ED
      AND
      StartDate >= @SD;

Démo en direct


0 commentaires

3
votes

Tout d'abord, vous pouvez transformer la liste d'origine des horodatages:

DECLARE
  @StartDate date = '2017-01-01',
  @EndDate   date = '2018-01-01',
  @ValidValue bit = 1
;

WITH
  ranges AS
  (
    SELECT
      ItemID,
      Valid,
      StartDate,
      EndDate = LEAD(StartDate, 1, CAST(CURRENT_TIMESTAMP AS date))
                OVER (PARTITION BY ItemID ORDER BY StartDate ASC)
    FROM
      #Temp
  )
SELECT DISTINCT
  ItemID
FROM
  ranges
WHERE
  Valid = @ValidValue
  AND StartDate < @EndDate
  AND EndDate > @StartDate
;

en une liste de plages, où la date de fin est soit la prochaine entrée de l'élément StartDate , soit , si la ligne actuelle est la dernière entrée, date du jour:

StartDate < @EndDate AND EndDate > @StartDate

Vous pouvez utiliser le LEAD fonction analytique pour y parvenir:

EndDate = LEAD(StartDate, 1, CAST(CURRENT_TIMESTAMP AS date))
          OVER (PARTITION BY ItemID ORDER BY StartDate ASC)


1 commentaires

Utiliser LEAD au lieu de LAG avec aujourd'hui comme date par défaut est un moyen beaucoup plus propre d'obtenir des plages de dates utilisables que la méthode que j'ai proposée. (Je n'ai pas beaucoup d'expérience avec ceux-ci, mais j'avais utilisé LAG dans un autre script récemment, alors je l'ai pris comme point de départ.) Quoi qu'il en soit, votre version accomplit en une seule étape ce que j'ai fait en trois. Très beau!