【问题标题】:SQL Date Range Query - Table ComparisonSQL 日期范围查询 - 表比较
【发布时间】:2014-10-28 23:00:21
【问题描述】:

我有两个包含以下信息的 SQL Server 表:

t_venues

venue_id 是唯一的

venue_id  |  start_date  |  end_date
       1  |  01/01/2014  |  02/01/2014
       2  |  05/01/2014  |  05/01/2014
       3  |  09/01/2014  |  15/01/2014
       4  |  20/01/2014  |  30/01/2014

t_venueuser

venue_id 不是唯一的

venue_id  |  start_date  |  end_date
       1  |  02/01/2014  |  02/01/2014
       2  |  05/01/2014  |  05/01/2014
       3  |  09/01/2014  |  10/01/2014
       4  |  23/01/2014  |  25/01/2014

我需要从这两个表中找到每个范围内没有选择的日期,所以输出如下所示:

venue_id  |  start_date  |  end_date
       1  |  01/01/2014  |  01/01/2014
       3  |  11/01/2014  |  15/01/2014
       4  |  20/01/2014  |  22/01/2014
       4  |  26/01/2014  |  30/01/2014

我可以比较这两个表并使用“except”获取来自t_venues 的日期范围以出现在我的查询中,但我无法让查询生成未选择的日期。任何帮助将不胜感激。

【问题讨论】:

  • 结果中的场地 3 行不应该是 11/01/2014 到 15/01/2014 吗?
  • 是的,谢谢 - 好地方!
  • 如果有帮助,不要忘记将答案标记为已接受。 stackoverflow.com/help/someone-answers

标签: sql sql-server tsql date gaps-and-islands


【解决方案1】:

日历表!

日历表的另一个完美候选者。如果你懒得找,here's one I made earlier

设置数据

DECLARE @t_venues table (
   venue_id   int
 , start_date date
 , end_date   date
);

INSERT INTO @t_venues (venue_id, start_date, end_date)
  VALUES (1, '2014-01-01', '2014-01-02')
       , (2, '2014-01-05', '2014-01-05')
       , (3, '2014-01-09', '2014-01-15')
       , (4, '2014-01-20', '2014-01-30')
;

DECLARE @t_venueuser table (
   venue_id   int
 , start_date date
 , end_date   date
);

INSERT INTO @t_venueuser (venue_id, start_date, end_date)
  VALUES (1, '2014-01-02', '2014-01-02')
       , (2, '2014-01-05', '2014-01-05')
       , (3, '2014-01-09', '2014-01-10')
       , (4, '2014-01-23', '2014-01-25')
;

查询

SELECT t_venues.venue_id
     , calendar.the_date
     , CASE WHEN t_venueuser.venue_id IS NULL THEN 1 ELSE 0 END As is_available
FROM   dbo.calendar /* see: http://gvee.co.uk/files/sql/dbo.numbers%20&%20dbo.calendar.sql for an example */
 INNER
  JOIN @t_venues As t_venues
    ON t_venues.start_date <= calendar.the_date
   AND t_venues.end_date   >= calendar.the_date
 LEFT
  JOIN @t_venueuser As t_venueuser
    ON t_venueuser.venue_id = t_venues.venue_id
   AND t_venueuser.start_date <= calendar.the_date
   AND t_venueuser.end_date   >= calendar.the_date
ORDER
    BY t_venues.venue_id
     , calendar.the_date
;

结果

venue_id    the_date                is_available
----------- ----------------------- ------------
1           2014-01-01 00:00:00.000 1
1           2014-01-02 00:00:00.000 0
2           2014-01-05 00:00:00.000 0
3           2014-01-09 00:00:00.000 0
3           2014-01-10 00:00:00.000 0
3           2014-01-11 00:00:00.000 1
3           2014-01-12 00:00:00.000 1
3           2014-01-13 00:00:00.000 1
3           2014-01-14 00:00:00.000 1
3           2014-01-15 00:00:00.000 1
4           2014-01-20 00:00:00.000 1
4           2014-01-21 00:00:00.000 1
4           2014-01-22 00:00:00.000 1
4           2014-01-23 00:00:00.000 0
4           2014-01-24 00:00:00.000 0
4           2014-01-25 00:00:00.000 0
4           2014-01-26 00:00:00.000 1
4           2014-01-27 00:00:00.000 1
4           2014-01-28 00:00:00.000 1
4           2014-01-29 00:00:00.000 1
4           2014-01-30 00:00:00.000 1

(21 row(s) affected)

解释

我们的日历表包含每个日期的条目。

我们加入我们的t_venues(顺便说一句,如果您有选择,请去掉t_ 前缀!)每天在我们的start_dateend_date 之间返回。仅此连接的 venue_id=4 的示例输出:

venue_id    the_date
----------- -----------------------
4           2014-01-20 00:00:00.000
4           2014-01-21 00:00:00.000
4           2014-01-22 00:00:00.000
4           2014-01-23 00:00:00.000
4           2014-01-24 00:00:00.000
4           2014-01-25 00:00:00.000
4           2014-01-26 00:00:00.000
4           2014-01-27 00:00:00.000
4           2014-01-28 00:00:00.000
4           2014-01-29 00:00:00.000
4           2014-01-30 00:00:00.000

(11 row(s) affected)

现在我们每天有一行,我们 [外部] 加入我们的 t_venueuser 表。我们加入的方式与以前几乎相同,但有一个额外的变化:我们也需要基于venue_id 加入!

venue_id=4 运行此命令会得到以下结果:

venue_id    the_date                t_venueuser_venue_id
----------- ----------------------- --------------------
4           2014-01-20 00:00:00.000 NULL
4           2014-01-21 00:00:00.000 NULL
4           2014-01-22 00:00:00.000 NULL
4           2014-01-23 00:00:00.000 4
4           2014-01-24 00:00:00.000 4
4           2014-01-25 00:00:00.000 4
4           2014-01-26 00:00:00.000 NULL
4           2014-01-27 00:00:00.000 NULL
4           2014-01-28 00:00:00.000 NULL
4           2014-01-29 00:00:00.000 NULL
4           2014-01-30 00:00:00.000 NULL

(11 row(s) affected)

看看我们如何为没有t_venueuser 记录的行设置NULL 值。天才,不是吗? ;-)

所以在我的第一个查询中,我给了您一个快速的 CASE 语句,显示可用性(1=可用,0=不可用)。这仅用于说明,但可能对您有用。

然后,您可以将查询包装起来,然后在此计算列上应用一个额外的过滤器,或者只需在 WHERE t_venueuser.venue_id IS NULL 中添加一个 where 子句,这将起到同样的作用。

【讨论】:

  • 感谢您抽出宝贵时间提供帮助!我已经尝试过两种解决方案,它们都很棒 - 所以谢谢!我学到了很多东西!
  • @samhankin 很高兴我能提供帮助。如果您对代码有任何疑问,请提出,我会更新我的答案以填补任何空白:)
【解决方案2】:

这是一个完整的 hack,但它给出了您需要的结果,我只在您提供的数据上对其进行了测试,因此很可能存在较大集合的问题。

一般来说,您在这里要解决的是间隙和孤岛问题的变体,这是(简要地)缺少某些项目的序列。缺失的项目称为间隙,现有的项目称为孤岛。如果您想大致了解此问题,请查看以下几篇文章:

代码:

;with dates as
(
    SELECT  vdates.venue_id,    
            vdates.vdate
    FROM  ( SELECT DATEADD(d,sv.number,v.start_date) vdate
                 , v.venue_id
            FROM t_venues v
            INNER JOIN master..spt_values sv 
                ON sv.type='P'
               AND sv.number BETWEEN 0 AND datediff(d, v.start_date, v.end_date)) vdates
    LEFT JOIN t_venueuser vu
        ON vdates.vdate >= vu.start_date
       AND vdates.vdate <= vu.end_date
       AND vdates.venue_id = vu.venue_id
    WHERE ISNULL(vu.venue_id,-1) = -1
)
SELECT venue_id, ISNULL([1],[2]) StartDate, [2] EndDate
FROM   (SELECT venue_id, rDate, ROW_NUMBER() OVER (PARTITION BY venue_id, DateType ORDER BY rDate) AS rType, DateType as dType
        FROM(   SELECT d1.venue_id
                      ,d1.vdate AS rDate
                      ,'1' AS DateType
                FROM dates AS d1    
                LEFT JOIN dates AS d0
                    ON DATEADD(d,-1,d1.vdate) = d0.vdate
                LEFT JOIN dates AS d2       
                    ON DATEADD(d,1,d1.vdate) = d2.vdate
                WHERE CASE ISNULL(d2.vdate, '01 Jan 1753') WHEN '01 Jan 1753' THEN '2' ELSE '1' END = 1
                AND ISNULL(d0.vdate, '01 Jan 1753') = '01 Jan 1753'
                UNION 
                SELECT d1.venue_id
                      ,ISNULL(d2.vdate,d1.vdate)
                      ,'2'
                FROM dates AS d1    
                LEFT JOIN dates AS d2       
                    ON DATEADD(d,1,d1.vdate) = d2.vdate
                WHERE CASE ISNULL(d2.vdate, '01 Jan 1753') WHEN '01 Jan 1753' THEN '2' ELSE '1' END = 2
            ) res
        ) src
PIVOT   (MIN (rDate)
        FOR dType IN
        ( [1], [2] )
        ) AS pvt

结果:

venue_id    StartDate   EndDate
1           2014-01-01  2014-01-01
3           2014-01-11  2014-01-15
4           2014-01-20  2014-01-22
4           2014-01-26  2014-01-30

【讨论】:

  • 这真是太棒了。谢谢!我已经在一个更大的数据集上运行它,我能找到的唯一问题是事件已经过去并且在事件开始时有可用日期,它只显示 1 天?例如:场地为 01/09/2014 至 10/09/2014,场地用户预订时间为 05/09/2014 至 10/09/2014,输出将显示 04/09/2014 至 04/09/2014 ?我现在正在玩它,看看我是否可以修复它!
  • @samhankin 很高兴我们能提供帮助,如果还有什么可以帮助的,请告诉我们。
猜你喜欢
  • 2013-09-16
  • 1970-01-01
  • 2010-09-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多