2009-04-23 14 views
8

Je suis en cours d'exécution dans un blocage sur un problème plus important.Vérification du chevauchement de plage de temps, le problème watchman [SQL]

Dans le cadre d'une grande requête, j'ai besoin de résoudre un problème "veilleur de nuit". J'ai une table avec des changements de calendrier en tant que tel:

ID | Start   | End 
1 | 2009-1-1 06:00 | 2009-1-1 14:00 
2 | 2009-1-1 10:00 | 2009-1-1 18:00 
3 | 2009-2-1 20:00 | 2009-2-2 04:00 
4 | 2009-2-2 06:00 | 2009-2-2 14:00 

Dans le cadre d'une requête, je dois déterminer s'il y a au moins 1 gardien dans une chambre en tout temps pour un intervalle de temps donné. Donc, si j'ai spécifié la plage 2009-1-1 06:00 à 2009-1-1 12:00, le résultat est vrai, car les décalages 1 et 2 fusionnent pour couvrir cette période. En fait, n'importe quel nombre de décalages pourrait être enchaîné pour maintenir la veille. Cependant si j'ai vérifié 2009-2-1 22:00 à 2009-1-2 10:00, le résultat est faux car il y a une coupure entre 4 et 6h le matin suivant.

Je voudrais implémenter ce soit dans LINQ, soit comme une fonction définie par l'utilisateur dans SQL Server (2005), car dans les deux cas, cela fait juste partie de la logique d'une requête plus grande qui doit être exécutée sur identifier les éléments qui ont besoin d'attention. Le jeu de données réel implique une centaine d'enregistrements de quart qui se croisent à n'importe quelle période donnée, mais qui ne couvrent pas toujours toute la gamme.

Le plus proche que j'ai trouvé est How to group ranged values using SQL Server pour les plages de numéros, mais cela dépend de chaque plage se terminant juste avant le début de la plage suivante. Si je pouvais construire la même vue unifiée des montres, en ne prenant en considération que les montres qui se chevauchent, il serait alors trivial de vérifier si une heure spécifique était couverte. Une vue unifiée ressemblerait à ceci:

Start   | End 
2009-1-1 06:00 | 2009-1-1 18:00 
2009-2-1 20:00 | 2009-2-2 04:00 
2009-2-2 06:00 | 2009-2-2 14:00 

Note: Toute cette affaire serait relativement facile à mettre en œuvre simplement en tirant toutes les données et en cours d'exécution une boucle manuelle là-dessus, mais qui est le système actuel, et son plutôt lent en raison du nombre de changements et du nombre de plages de temps à vérifier.

+0

Ne pas indiquer de plage de dates dans "Si j'ai spécifié la plage 2009-1-1 12:00 à 2009-1-1 06:00" à "2009-1-1 06:00 à 2009-1-1 12:00 "? – Sung

+2

@David Vous pouvez télécharger cet ebook: "Développement d'applications de base de données orientées temporelles en SQL" (http://www.cs.arizona.edu/people/rts/tdbbook.pdf). Il a beaucoup de bonnes informations sur les requêtes SQL complexes sur les tables avec des plages de dates. –

+0

+1 - Je travaille sur un problème similaire, en identifiant les disjoints ET les chevauchements – spencer7593

Répondre

2

Voici un moyen d'aplatir la plage de dates comme celui-ci

Start   | End 
2009-1-1 06:00 | 2009-1-1 18:00 
2009-2-1 20:00 | 2009-2-2 04:00 
2009-2-2 06:00 | 2009-2-2 14:00 

Vous devez comparer précédentetprochains dates dans chaque ligne et voir si

  • ligne actuelle de Début La date se situe entre la plage de dates de la rangée précédente.
  • Ligne actuelle Fin date se situe entre la plage de dates de la ligne suivante.

alt text

En utilisant le code ci-dessus, la mise en œuvre UDF est aussi simple comme suit.

create function fnThereIsWatchmenBetween(@from datetime, @to datetime) 
returns bit 
as 
begin 
    declare @_Result bit 

    declare @FlattenedDateRange table (
     Start datetime, 
     [End] datetime 
    ) 

    insert @FlattenedDateRange(Start, [End]) 
    select distinct 
      Start = 
       case 
        when Pv.Start is null then Curr.Start 
        when Curr.Start between Pv.Start and Pv.[End] then Pv.Start 
        else Curr.Start 
       end, 
      [End] = 
       case 
        when Curr.[End] between Nx.Start and Nx.[End] then Nx.[End] 
        else Curr.[End] 
       end 
    from shift Curr 
      left join shift Pv on Pv.ID = Curr.ID - 1 --; prev 
      left join shift Nx on Nx.ID = Curr.ID + 1 --; next 

    if exists( select 1 
       from FlattenedDateRange R 
       where @from between R.Start and R.[End] 
         and @to between R.Start and R.[End]) begin 
     set @_Result = 1 --; There is/are watchman/men during specified date range 
    end 
    else begin 
     set @_Result = 0 --; There is NO watchman 
    end 

    return @_Result 
end 
+0

et quand, par exemple, nous avons des décalages: 12-2 et 4-6 et nous vérifions 1-6. cela devrait échouer, puisque 2-4 est vide. – nlucaroni

+0

Le code a été mis à jour. – Sung

+0

Très bien.Ironiquement, parce que cela faisait partie d'un problème plus important et plus complexe, j'ai trouvé une façon plus rapide de tout faire en créant un type de table de recherche alimenté par des triggers, en déplaçant la plus grande partie du coût du CPU. est beaucoup moins fréquent alors quand ils ont besoin d'être lus et analysés). Cependant, je dois traiter beaucoup de ces types de chevauchement ici, donc cela va être très utile très bientôt. – David

1

Un intervalle non surveillé commence évidemment à la fin d'une période surveillée ou au début de toute la période que vous vérifiez. Vous avez donc besoin d'une requête qui sélectionne tous les éléments de cet ensemble qui n'ont pas de décalage de chevauchement. La requête devrait ressembler à:

select 1 
from shifts s1 where not exists 
    (select 1 from shifts s2 
    where s2.start<=s1.end and s2.end > s1.end 
    ) 
    and s1.end>=start_of_range and s1.end< end_of_range 
union 
select 1 
where not exists 
    (select 1 from shifts s2 
     where s2.start<=start_of_range and s2.end > start_of_range 
    ) 

Si ce n'est pas vide, alors vous avez un intervalle non gardé. Je suppose qu'il fonctionnera en temps quadratique, donc il pourrait être plus lent que "trier, chercher et boucler".

+0

J'ai des difficultés à faire ce travail. Je suppose que vous avez l'intention de filtrer quelque part pour l'heure de fin, mais en ajoutant cela, ou en vous assurant seulement que seuls les changements valides sont passés (en faisant des quarts de vue), les résultats semblent sans rapport avec la gamme fournie. les résultats sont donnés indépendamment du fait que la plage soit à l'intérieur ou à l'extérieur des quarts de travail. – David

+0

À droite, la sous-requête supérieure a totalement ignoré la plage - Ajout de la vérification –

0

Une façon est de créer une table temporaire avec une ligne pour chaque valeur de temps oblige à vérifier (qui est fonction de la résolution de vos déplacements).

S'il s'agissait de minutes, il aurait 60 * 24 = 1440 lignes pour un jour; environ 10K lignes pour une semaine.

Ensuite, le SQL est relativement simple:

SELECT COUNT (1)
DE #minutes m
LEFT JOIN quarts s SUR m.checktime ENTRE s.start_time ET s.end_time
HAVING COUNT (1) = 0

Cela a l'avantage de pouvoir également montrer combien de postes couvrent la même période.

La durée d'exécution devrait être négligeable compte tenu des échelles que vous avez décrites.

0

Je regardais les plages de dates et j'ai pensé que je reviendrais sur cette question. Je peux tomber à plat sur mon visage, mais il semble que ces deux conditions seraient assez

(1) Shift is not at beginning of range and has no left neighbour 

OR 

(2) Shift is not at end of range and has no right neighbour. 

Appréciez cela peut ne pas être le plus efficace.

CREATE TABLE times 
(
TimeID int, 
StartTime Time, 
EndTime Time 
) 

INSERT INTO times 
VALUES 
(1,'10:00:00','11:00:00'), 
(2,'11:00:00','12:00:00'), 
(3,'13:00:00','14:00:00'), 
(4,'14:30:00','15:00:00'), 
(5,'15:00:00','16:00:00'), 
(6,'16:00:00','17:00:00') 

declare @start_of_range time ='09:30:00' 
declare @end_of_range time = '17:30:00' 



select timeID,StartTime,EndTime 
from times s1 where 
-- No left neighbour and not at beginning of range 
    not exists 
    (select 1 from times s2 
    where s2.startTime < s1.startTime and s2.endTime >= s1.startTime 
    ) 
    and s1.StartTime>@start_of_range 
    or 
-- No right neighbour and not at end of range 
    not exists 
    (select 1 from times s2 
    where s2.startTime <= s1.endTime and s2.endTime > s1.endTime 
    ) 
    and s1.EndTime<@end_of_range 

Jeu de résultats

timeID StartTime EndTime 
1 10:00:00.0000000 11:00:00.0000000 
2 11:00:00.0000000 12:00:00.0000000 
3 13:00:00.0000000 14:00:00.0000000 
4 14:30:00.0000000 15:00:00.0000000 
6 16:00:00.0000000 17:00:00.0000000 

En fait, il est seulement nécessaire de vérifier soit les bons voisins ou les voisins de gauche, aussi longtemps que vous assurez-vous que le début et la fin de la plage est cochée, vous pouvez donc introduire le début de la plage comme un intervalle factice et il suffit de cocher les bons voisins comme suit: -

select * from 
(
select timeID,StartTime,EndTime 
from times union select 0,@start_of_range,@start_of_range) s1 
where 
    not exists 
    (select 1 from times s2 
    where s2.startTime<=s1.endTime and s2.endTime > s1.endTime 
    ) 
    and s1.EndTime<@end_of_range 

jeu de résultats

timeID StartTime EndTime 
0 09:30:00.0000000 09:30:00.0000000 
2 11:00:00.0000000 12:00:00.0000000 
3 13:00:00.0000000 14:00:00.0000000 
6 16:00:00.0000000 17:00:00.0000000 
Questions connexes