SQLServerCentral Article

Prevent overlapping of time events with an indexed view

,

Disclaimer

This article does not pretend to present this solution as superior to others, it's intent is to show you how to solve the overlapping problem with an indexed view.

Problem

There is a schedule table that holds the start and the end of planned activities. We want to prevent users from scheduling activities that uses time already reserved.

Let’s create a tally table and the Schedule table with some data.

  CREATE DATABASE OverlappingEvents 
  GO
  USE OverlappingEvents 
  GO
  CREATE TABLE Numbers
      (
        Number INT NOT NULL
                   PRIMARY KEY
                   CHECK ( Number >= 1 )
      )
  GO
  INSERT INTO Numbers
          SELECT TOP ( 60 * 24 * 2 ) ROW_NUMBER() OVER ( ORDER BY c.object_id )
              FROM sys.columns c
                  CROSS JOIN sys.columns cc
  GO
  CREATE TABLE Schedule
      (
        PersonId INT NOT NULL
      , Activity VARCHAR(150) NOT NULL
      , StartDate DATETIME NOT NULL
      , EndDate DATETIME NOT NULL
      , CHECK ( EndDate > StartDate )
      )
  GO 
  INSERT INTO Schedule
      VALUES ( 1, 'Piano Concert', '2013-11-20 18:00', '2013-11-20 18:20' )
  INSERT INTO Schedule
      VALUES ( 1, 'Weeding Anniversary Dinner', '2013-12-09 19:00',
               '2013-12-09 23:30' )
  INSERT INTO Schedule
      VALUES ( 1, 'Work Meeting', '2013-12-09 18:00', '2013-12-09 19:15' )
  -- Oooops! it overlaps with the weeding anniversary
  INSERT INTO Schedule
      VALUES ( 2, 'Football Night', '2013-11-20 17:00', '2013-11-20 20:30' )
  INSERT INTO Schedule
      VALUES ( 2, 'Reading Club', '2013-12-09 19:00', '2013-12-09 20:30' )

If we review the activities we have scheduled, we will realize that this table is not a vey good friend, it did not prevent me from  appointing a work meeting that will make me arrive late to my anniversary (a wife is never happy with this).

Solution

How can we know if two activities overlap? Both use the same time period. In this case, the anniversary and the work meeting need the minutes 1 to 15 of the hour 19. Let’s create a view that will define which minutes are occupied by the activities.

  CREATE VIEW ScheduleTimeUsage
  WITH SCHEMABINDING
  AS
      SELECT s.PersonId
            , s.Activity
            , MinuteNeeded = DATEADD(MINUTE, n.Number - 1, s.StartDate)
          FROM dbo.Schedule s
              INNER JOIN dbo.Numbers n
              ON n.Number BETWEEN 1
                          AND     DATEDIFF(MINUTE, s.StartDate, s.EndDate)&bsp;
  GO
  SELECT *
      FROM ScheduleTimeUsage
      ORDER BY PersonId
        , MinuteNeeded
  GO

Please note that each activity has one row for each minute period it occupies. This was achieved by joining the Numbers table where the Number is less than the duration in minutes of the activity and adding this number to start date of the activity.

By focusing on the anniversary and meeting events, we will notice something interesting. The same time periods are present in both activities.  In other words, I’m using the same time for two different things.

  SELECT *
      FROM ScheduleTimeUsage
      WHERE MinuteNeeded BETWEEN '2013-12-09 19:10'
                         AND     '2013-12-09 19:15'
          AND PersonId = 1
      ORDER BY PersonId
        , MinuteNeeded

How can we prevent these duplicates from happening? Yes, you are right! A unique index on this view can do the trick. Let’s try it.

CREATE UNIQUE CLUSTERED INDEX 
IX_PreventActivityOverlapping 
ON ScheduleTimeUsage(PersonId,MinuteNeeded)
GO

Oops ! we can’t because of the meeting overlapping.  

We will remove it and try again.

  DELETE Schedule
      WHERE PersonId = 1
          AND Activity LIKE '%meeting%'

  CREATE UNIQUE CLUSTERED INDEX 
  IX_PreventActivityOverlapping 
  ON ScheduleTimeUsage(PersonId,MinuteNeeded)

We are good now. How does this works? It just enforces that the pair (PersonId,MinuteNeeded) does not exists more than once in the schedule table.

 Let’s see if this works as we expect. Try to insert other event that overlaps with the anniversary and other that does not.

  INSERT INTO Schedule
      VALUES ( 1, 'Mexico Soccer game', '2013-12-09 18:00', '2013-12-09 19:40' ) 
  -- No way Jose, this is trouble.
  INSERT INTO Schedule
      VALUES ( 1, 'Mexico Soccer game', '2013-12-09 18:00', '2013-12-09 18:40' ) 
  -- Not anymore =)

The first schedule was not accepted, but the second is totally ok.s example

The cinema shows example

Let’s create a movies table that contains the name and duration of each movie and a movie shows table in which we will allocate a movie in a room at the time it will start.

Our goal is to avoid shows overlapping based on the duration and start of a movie in a room. In other words, the next movie can't start until the current one is finished.

  CREATE TABLE Movies
      (
        MovieCode VARCHAR(10) NOT NULL
                              PRIMARY KEY
      , Name VARCHAR(50) NOT NULL
      , DurationInMinutes INT NOT NULL
                              CHECK ( DurationInMinutes > 0 )
      )
  GO
  INSERT INTO Movies
          SELECT 'G2013-1'
                , 'Gravity'
                , 90
          UNION ALL
          SELECT 'T2013-2'
                , 'Thor Dark World'
                , 110
          UNION ALL
          SELECT 'R2013-3'
                , 'Rush'
                , 128
          UNION ALL
          SELECT 'P2013-4'
                , 'Pulling Strings'
                , 90
  GO
  CREATE TABLE MovieShows
      (
        Room INT NOT NULL
                 CHECK ( Room > 0 )
      , MovieCode VARCHAR(10) NOT NULL
                              FOREIGN KEY REFERENCES Movies ( MovieCode )
      , StartHour INT NOT NULL
                      CHECK ( StartHour BETWEEN 0 AND 23 )
      , StartMinute INT NOT NULL
                        CHECK ( StartMinute BETWEEN 0 AND 59 )
                        DEFAULT 0
      )
  GO
  INSERT INTO MovieShows
          SELECT 1
                , 'G2013-1'
                , 18
                , 0
          UNION ALL
          SELECT 1
                , 'G2013-1'
                , 19
                , 40
          UNION ALL
          SELECT 2
                , 'T2013-2'
                , 17
                , 0
          UNION ALL
          SELECT 2
                , 'T2013-2'
                , 19
                , 0
          UNION ALL
          SELECT 3
                , 'R2013-3'
                , 16
                , 0
          UNION ALL
          SELECT 4
                , 'P2013-4'
                , 23
                , 50
  GO

See the contents of the tables and the overview of when a movie will start and end. 

  SELECT *
      FROM Movies
  SELECT *
      FROM MovieShows 
  SELECT s.Room
        , m.Name
        , m.DurationInMinutes
        , MovieStartMinuteOfDay = ( s.StartHour * 60 ) + s.StartMinute
        , MovieEndMinuteOfDay = ( ( s.StartHour * 60 ) + s.StartMinute
                                  + m.DurationInMinutes )
      FROM MovieShows s
          INNER JOIN Movies m
          ON m.MovieCode = s.MovieCode 
  GO

Applying the same principle of the personal schedule example, we will create a view that defines the time usage of the rooms.

  CREATE VIEW RoomsUsage
  WITH SCHEMABINDING
  AS
      SELECT s.Room
            , m.MovieCode
            , MinuteOfDayNeeded = ( ( s.StartHour * 60 ) + s.StartMinute
                                    + n.Number - 1 ) % ( 60 * 24 ) 
  -- A movie might last over midnight
          FROM dbo.MovieShows s
              INNER JOIN dbo.Movies m
              ON m.MovieCode = s.MovieCode
              INNER JOIN dbo.Numbers n
              ON n.Number BETWEEN 1 AND m.DurationInMinutes
  GO
  SELECT *
      FROM RoomsUsage
  GO

Create the unique index to prevent that a room allocates more than movie at the same time.

CREATE UNIQUE CLUSTERED INDEX 
IX_PreventOverlapping
ON RoomsUsage (Room,MinuteOfDayNeeded)
GO

If this works, it should not let us overlap movies in a room.

UPDATE Movies
    SET DurationInMinutes = 300
    WHERE MovieCode = 'G2013-1'
-- Not good, will cause overlaps
UPDATE Movies
    SET DurationInMinutes = 95
    WHERE MovieCode = 'G2013-1'
 -- Ok
INSERT INTO MovieShows
        SELECT 3
              , 'R2013-3'
              , 16
              , 50
-- Not good, will cause overlaps
INSERT INTO MovieShows
        SELECT 3
              , 'R2013-3'
              , 18
              , 30
 -- Ok
UPDATE MovieShows
    SET StartHour = 18
    WHERE Room = 2
        AND MovieCode = 'T2013-2'
        AND StartHour = 17 
--Not good, overlap
UPDATE MovieShows
    SET StartHour = 17
      , StartMinute = 10
    WHERE Room = 2
        AND MovieCode = 'T2013-2'
        AND StartHour = 17 
-- Great! It only allows good data.

What is interesting about this example is that this view enforces the business rule in both tables. We can’t increase the duration of a movie nor change its start time if this will cause that the movie needs time that is already reserved. 

Considerations

The numbers table size must be proportional to the duration of the events. The materialized view size is proportional to the quantity and duration of events. You can always set more precise time period by changing the time usage view to use other date parts.

This is a good tool to consider if using triggers is not an option for you.

Resources

Rate

4.07 (15)

You rated this post out of 5. Change rating

Share

Share

Rate

4.07 (15)

You rated this post out of 5. Change rating