SQL Server - 查询最近的日期范围

时间:2012-04-21 13:03:00

标签: sql-server tsql

如果我有这样的表结构:

ProductCode  Date
Foo          4/1/2012
Foo          4/2/2012
Foo          4/3/2012
Foo          4/6/2012
Foo          4/7/2012
Foo          4/8/2012
Foo          4/9/2012
Foo          4/10/2012
Foo          4/15/2012
Foo          4/16/2012
Foo          4/17/2012

有没有办法查询给定ProductCodeDate的日期范围(假设范围必须是连续的)?换句话说,对于此表,Foo存在于3个日期范围:4/1-4/3; 4/6-4/10;和4/15-4/17并且我正在寻找给定日期的日期范围。

请注意,Foo没有日期4/44/54/114/124/13和{{1} }。

示例:
4/14将返回ProductCode=Foo, Date=4/2,因为条目是连续的 4/1-4/3不会返回任何内容 ProductCode=Foo, Date=4/4将返回ProductCode=Foo, Date=4/7,因为条目是连续的 4/6-4/10不会返回任何内容 等

6 个答案:

答案 0 :(得分:1)

当前一天没有行时,新范围开始。如果您运行的是SQL Server 2012,则可以使用lag窗口函数来检查行是否引入了新范围。一旦知道哪些行引入了新范围,就可以计算头行数,为每个范围分配唯一编号。

使用范围编号,您可以使用minmax查找开始日期和结束日期。在那之后,这只是一个选择行的问题:

; with  IsHead as
        (
        select  ProductCode
        ,       Date
        ,       case when lag(Date) over (partition by ProductCode 
                  order by Date) = dateadd(day, -1, Date) then 0 
                  else 1 end as IsHead
        from  YourTable
        )
,       RangeNumber as
        (
        select  ProductCode
        ,       Date
        ,       sum(IsHead) over (partition by ProductCode order by Date) 
                  as RangeNr
        from    IsHead
        )
,       Ranges as
        (
        select  *
        ,       min(Date) over (partition by RangeNr) as RangeStart
        ,       max(Date) over (partition by RangeNr) as RangeEnd
        from    RangeNumber
        )
select  *
from    Ranges
where   ProductCode = 'Bar'
        and Date = '4/2/2012'

Example at SQL Fiddle.

答案 1 :(得分:1)

如果SQL Server 2005支持LAG,则可以使用LAG。不幸的是,LAG window function仅适用于SQL Server 2012,PostgreSQL 8.4 and above; - )

适用于SQL Server 2005我认为,SQLFiddle没有SQL 2005支持,只尝试过SQLFiddle的SQL Server 2008,而不是2012:

with DetectLeaders as
(
    select cr.ProductCode, CurRowDate = cr.Date, PrevRowDate = pr.Date
    from tbl cr
    left join tbl pr 
    on pr.ProductCode = cr.ProductCode AND cr.Date = DATEADD(DAY,1,pr.Date)
),
MembersLeaders as
(
    select *, 
        MemberLeader = 
            (select top 1 CurRowDate 
            from DetectLeaders nearest
            where nearest.PrevRowDate is null 
                and nearest.ProductCode = DetectLeaders.ProductCode
                and DetectLeaders.CurRowDate >= nearest.CurRowDate 
            order by nearest.CurRowDate desc)   
    from DetectLeaders
)
select BeginDate = MIN(CurRowDate), EndDate = MAX(CurRowDate) 
from MembersLeaders
where MemberLeader = 
  (select MemberLeader 
   from MembersLeaders
   where ProductCode = 'Foo' and CurRowDate = '4/7/2012')

实时测试:http://sqlfiddle.com/#!3/3fd1f/1


基本上这就是它的工作原理:

PRODUCTCODE     CURROWDATE  PREVROWDATE MEMBERLEADER
Foo             2012-04-01              2012-04-01
Foo             2012-04-02  2012-04-01  2012-04-01
Foo             2012-04-03  2012-04-02  2012-04-01
Foo             2012-04-06              2012-04-06
Foo             2012-04-07  2012-04-06  2012-04-06
Foo             2012-04-08  2012-04-07  2012-04-06
Foo             2012-04-09  2012-04-08  2012-04-06
Foo             2012-04-10  2012-04-09  2012-04-06
Foo             2012-04-15              2012-04-15
Foo             2012-04-16  2012-04-15  2012-04-15
Foo             2012-04-17  2012-04-16  2012-04-15
Bar             2012-05-01              2012-05-01
Bar             2012-05-02  2012-05-01  2012-05-01
Bar             2012-05-03  2012-05-02  2012-05-01
Bar             2012-05-06              2012-05-06
Bar             2012-05-07  2012-05-06  2012-05-06
Bar             2012-05-08  2012-05-07  2012-05-06
Bar             2012-05-09  2012-05-08  2012-05-06
Bar             2012-05-10  2012-05-09  2012-05-06
Bar             2012-05-15              2012-05-15
Bar             2012-05-16  2012-05-15  2012-05-15
Bar             2012-05-17  2012-05-16  2012-05-15

http://sqlfiddle.com/#!3/35818/11

答案 2 :(得分:0)

可以使用递归CTE。

declare @target_date datetime = convert(datetime, '04/07/2012', 101);

with source_table as (
  select ProductCode, convert(datetime, Date, 101) as Date
  from (
    values
    ('Foo', '4/1/2012')
   ,('Foo', '4/2/2012')
   ,('Foo', '4/3/2012')
   ,('Foo', '4/6/2012')
   ,('Foo', '4/7/2012')
   ,('Foo', '4/8/2012')
   ,('Foo', '4/9/2012')
   ,('Foo', '4/10/2012')
   ,('Foo', '4/15/2012')
   ,('Foo', '4/16/2012')
   ,('Foo', '4/17/2012')
  ) foo(ProductCode, Date)
),
recursive_date_lower as (
  select Date from source_table where Date = @target_date

  union all

  select dateadd(d, -1, r.Date) from recursive_date_lower r where exists (select 0 from source_table s where s.Date = dateadd(d, -1, r.Date))
),
recursive_date_upper as (
  select Date from source_table where Date = @target_date

  union all

  select dateadd(d, 1, r.Date) from recursive_date_upper r where exists (select 0 from source_table s where s.Date = dateadd(d, 1, r.Date))
)
select
   (select min(Date) from recursive_date_lower) as start,
   (select max(Date) from recursive_date_upper) as finish

答案 3 :(得分:0)

注意:我添加了第二个解决方案(非递归),其逻辑读取次数较少(性能更佳)。

1)您可以使用recursive CTE(演示here):

DECLARE @Test TABLE
(
    ID          INT IDENTITY NOT NULL UNIQUE, --ID is for insert order
    ProductCode VARCHAR(10) NOT NULL,
    [Date]      SMALLDATETIME NOT NULL,
    PRIMARY KEY(ProductCode, [Date])
);

INSERT  @Test (ProductCode , [Date])
          SELECT 'Foo' , '20120401'
UNION ALL SELECT 'Foo' , '20120402'
UNION ALL SELECT 'Foo' , '20120403'

UNION ALL SELECT 'Foo' , '20120404'
--UNION ALL SELECT 'Foo' , '20120405'

UNION ALL SELECT 'Foo' , '20120406'
UNION ALL SELECT 'Foo' , '20120407'
UNION ALL SELECT 'Foo' , '20120408'
UNION ALL SELECT 'Foo' , '20120409'
UNION ALL SELECT 'Foo' , '20120410'
UNION ALL SELECT 'Foo' , '20120415'
UNION ALL SELECT 'Foo' , '20120416'
UNION ALL SELECT 'Foo' , '20120417';

DECLARE @MyProductCode VARCHAR(10),
        @MyDate SMALLDATETIME;


SELECT  @MyProductCode = 'Foo',
        @MyDate = '20120402';

WITH    CteRecursive
AS
(
        --Starting row 
        SELECT  t.ID,
                t.ProductCode,
                t.[Date],
                1 AS RowType
        FROM    @Test t
        WHERE   t.ProductCode = @MyProductCode
        AND     t.[Date] = @MyDate
        UNION ALL
        --Add the next days DATEADD(DAY, +1, ..)
        SELECT  crt.ID,
                crt.ProductCode,
                crt.[Date],
                2 AS RowType
        FROM    CteRecursive prev
        INNER JOIN @Test crt ON DATEADD(DAY, 1, prev.[Date]) = crt.[Date] AND prev.RowType IN (1,2)
        UNION ALL
        --Add the previous days DATEADD(DAY, -1, ..)
        SELECT  crt.ID,
                crt.ProductCode,
                crt.[Date],
                0 AS RowType
        FROM    CteRecursive prev
        INNER JOIN @Test crt ON DATEADD(DAY, -1, prev.[Date]) = crt.[Date] AND prev.RowType IN (0,1)
)
SELECT  *
FROM    CteRecursive r
ORDER BY r.[Date]
/*--Or
SELECT  MIN(r.[Date]) AS BeginDate, MAX(r.[Date]) AS EndDate
FROM    CteRecursive r
*/

结果:

ID          ProductCode Date                    RowType
----------- ----------- ----------------------- -------
1           Foo         2012-04-01 00:00:00     0
2           Foo         2012-04-02 00:00:00     1
3           Foo         2012-04-03 00:00:00     2
4           Foo         2012-04-04 00:00:00     2

2)非递归解决方案:

DECLARE @Test TABLE
(
    ProductCode VARCHAR(10) NOT NULL,
    [Date]      SMALLDATETIME NOT NULL,
    PRIMARY KEY(ProductCode, [Date])
);

INSERT  @Test (ProductCode , [Date])
          SELECT 'Foo' , '20120401'
UNION ALL SELECT 'Foo' , '20120402'
UNION ALL SELECT 'Foo' , '20120403'

UNION ALL SELECT 'Foo' , '20120404'
--UNION ALL SELECT 'Foo' , '20120405'

UNION ALL SELECT 'Foo' , '20120406'
UNION ALL SELECT 'Foo' , '20120407'
UNION ALL SELECT 'Foo' , '20120408'
UNION ALL SELECT 'Foo' , '20120409'
UNION ALL SELECT 'Foo' , '20120410'
UNION ALL SELECT 'Foo' , '20120415'
UNION ALL SELECT 'Foo' , '20120416'
UNION ALL SELECT 'Foo' , '20120417';

DECLARE @MyProductCode VARCHAR(10),
        @MyDate SMALLDATETIME;


SELECT  @MyProductCode = 'Foo',
        @MyDate = '20120402';

DECLARE @StartDate SMALLDATETIME, 
        @EndDate SMALLDATETIME;

SELECT  @EndDate = MAX(b.[Date])
FROM    
(
        SELECT  a.[Date],
                ROW_NUMBER() OVER(ORDER BY a.Date ASC)-1 AS RowNum
        FROM    @Test a
        WHERE   a.ProductCode = @MyProductCode
        AND     a.[Date] >= @MyDate
) b
WHERE   b.[Date] = DATEADD(DAY, b.RowNum, @MyDate);

SELECT  @StartDate = MIN(b.[Date])
FROM    
(
        SELECT  a.[Date],
                ROW_NUMBER() OVER(ORDER BY a.Date DESC)-1 AS RowNum
        FROM    @Test a
        WHERE   a.ProductCode = @MyProductCode
        AND     a.[Date] <= @MyDate
) b
WHERE   b.[Date] = DATEADD(DAY, -b.RowNum, @MyDate);

SELECT  @StartDate [@StartDate], @EndDate [@EndDate];
SELECT  LEFT(CONVERT(VARCHAR(10), @StartDate, 101),5) [@StartDate], LEFT(CONVERT(VARCHAR(10), @EndDate, 101),5) [@EndDate];

结果:

@StartDate              @EndDate
----------------------- -----------------------
2012-04-01 00:00:00     2012-04-04 00:00:00

@StartDate @EndDate
---------- --------
04/01      04/04

答案 4 :(得分:0)

您可以尝试这样的事情(假设SQL Server 2005 +):

WITH partitioned AS (
  SELECT
    ProductCode,
    Date,
    GroupID = DATEDIFF(DAY, 0, Date)
            - ROW_NUMBER() OVER (PARTITION BY ProductCode ORDER BY Date)
  FROM atable
),
ranges AS (
  SELECT
    ProductCode,
    Date,
    MinDate = MIN(Date) OVER (PARTITION BY ProductCode, GroupID),
    MaxDate = MAX(Date) OVER (PARTITION BY ProductCode, GroupID)
  FROM partitioned
)
SELECT
  MinDate,
  MaxDate
FROM ranges
WHERE ProductCode = @ProductCode
  AND Date = @Date

答案 5 :(得分:0)

您还可以使用CROSS APPLY查找最近的日期:

with DetectLeaders as
(
    select cr.ProductCode, CurRowDate = cr.Date, PrevRowDate = pr.Date
    from tbl cr
    left join tbl pr 
    on pr.ProductCode = cr.ProductCode AND cr.Date = DATEADD(DAY,1,pr.Date)
),
MembersLeaders as
(
    select *       
    from DetectLeaders
    cross apply(
        select top 1 MemberLeader = CurRowDate 
        from DetectLeaders nearest
        where nearest.PrevRowDate is null 
            and nearest.ProductCode = DetectLeaders.ProductCode
            and DetectLeaders.CurRowDate >= nearest.CurRowDate 
        order by nearest.CurRowDate desc
    ) as xxx
)
select BeginDate = MIN(CurRowDate), EndDate = MAX(CurRowDate) 
from MembersLeaders
where MemberLeader = 
  (select MemberLeader 
   from MembersLeaders
   where ProductCode = 'Foo' and CurRowDate = '4/7/2012')

实时测试:http://sqlfiddle.com/#!3/3fd1f/2


基本上这就是它的工作原理:

PRODUCTCODE     CURROWDATE  PREVROWDATE MEMBERLEADER
Foo             2012-04-01              2012-04-01
Foo             2012-04-02  2012-04-01  2012-04-01
Foo             2012-04-03  2012-04-02  2012-04-01
Foo             2012-04-06              2012-04-06
Foo             2012-04-07  2012-04-06  2012-04-06
Foo             2012-04-08  2012-04-07  2012-04-06
Foo             2012-04-09  2012-04-08  2012-04-06
Foo             2012-04-10  2012-04-09  2012-04-06
Foo             2012-04-15              2012-04-15
Foo             2012-04-16  2012-04-15  2012-04-15
Foo             2012-04-17  2012-04-16  2012-04-15
Bar             2012-05-01              2012-05-01
Bar             2012-05-02  2012-05-01  2012-05-01
Bar             2012-05-03  2012-05-02  2012-05-01
Bar             2012-05-06              2012-05-06
Bar             2012-05-07  2012-05-06  2012-05-06
Bar             2012-05-08  2012-05-07  2012-05-06
Bar             2012-05-09  2012-05-08  2012-05-06
Bar             2012-05-10  2012-05-09  2012-05-06
Bar             2012-05-15              2012-05-15
Bar             2012-05-16  2012-05-15  2012-05-15
Bar             2012-05-17  2012-05-16  2012-05-15

http://www.sqlfiddle.com/#!3/3fd1f/3

CROSS APPLY/OUTER APPLY与JOIN相比,也可以很好地扩展:http://www.ienablemuch.com/2012/04/outer-apply-walkthrough.html