获取租用有库存产品的不可用日期

Cul*_*tes 5 sql-server sql-server-2012

数据库查询,通常很简单,但有时又很困难。(大脑训练师)

所以我有产品、股票和rentStockOrders。这些产品可以租用几天。这些股票也有一个可用的日期。是否可以租用新产品(库存)取决于该产品已租用的库存。

  • 库存物品不能在可用日期之前租用。
  • rentStockOrder(在订单和库存之间链接)包含预订,因此rentStartDate 和rentEndDate。
  • 查询应检查当所有股票都已出租时哪些日期不可用。
  • 产品可以租用几天,其中不提供开始/结束日期。选择产品,然后使用日期时间选择器选择租赁的开始日期。
  • 整体最大日期提前一年(可以是输入参数),最小日期为今天(或 +2)。

这个想法是用户还没有选择开始日期,在用户能够这样做之前,我想在日期时间选择器中禁用某些日期,因为该产品的租赁期没有可用的库存,因此无法用作开始日期.

放在上下文中:选择了一种产品,用户可以选择指定他想要租用该产品的天数(1 周、2 周或 3 周)。用户选择后,他们必须选择开始日期。与其每次都显示此日期不可用的错误,我宁愿事先禁用开始日期。

由于产品可供租用的情况较多,因此我认为最好从我的数据库中选择不可用的选择日期列表,而不是完整的可用日期列表。因此无法在日期时间选择器中单击不可用的日期。

到目前为止,我发现的大多数示例都包括我没有的开始和结束日期的输入参数,我有一个产品想要租用的天数长度以及在特定时间范围内已经租用了多少股票。

股票

+---------+-----------+-------------------+
| stockId | productId | availableFromDate |
+---------+-----------+-------------------+
|       1 |         1 | 01-01-2016        |
|       2 |         1 | 01-01-2016        |
+---------+-----------+-------------------+
Run Code Online (Sandbox Code Playgroud)

租赁股票订单

+------------------+---------+----------------+----------------+
| rentStockOrderId | stockId | beginRentDate  |  endRentDate   |
+------------------+---------+----------------+----------------+
|                1 |       1 | 15-01-2016     | 14-02-2016     |
|                2 |       2 | 30-01-2016     | 20-02-2016     |
|                3 |       2 | 26-02-2016     | 07-03-2016     |
|                4 |       1 | 29-02-2016     | 14-03-2016     |
+------------------+---------+----------------+----------------+
Run Code Online (Sandbox Code Playgroud)

根据这些记录,我想生成一个不可用日期的列表。为简单起见,我省略了一些列

输入是一天和一个产品 ID。因此,如果我输入days: 14 和productId: 1 我会得到以下一些预期结果:

  • 25-01-2016(stockId 1 已预订,库存 2 即将预订,14 天不可能。
  • 30-01-2016(均已预订)
  • 13-02-2016(股票1还没回来)
  • 17-02-2016(库存2已预订,库存1将在13天后出租,不够14天)。
  • ..还有更多的股票已经租用。

我没想到的是例如 15-02-2016,因为股票 1 将在接下来的 14 天内可用。

如果太困难,那么获取可用日期可能更简单,我将在代码中进行切换。在此示例中,从数据库中提取的数据会更少,但实际上,一种产品大约有 250 项,因此获取不可用日期可能会更好。

我一直在努力让这个工作至少获得可用日期,到目前为止没有成功,没有返回任何记录:

+---------+-----------+-------------------+
| stockId | productId | availableFromDate |
+---------+-----------+-------------------+
|       1 |         1 | 01-01-2016        |
|       2 |         1 | 01-01-2016        |
+---------+-----------+-------------------+
Run Code Online (Sandbox Code Playgroud)

原始来源,老实说,我不明白最后一部分以及这个查询实际上在做什么。

Vla*_*nov 4

我会用一张Calendar桌子。这张表只是列出了几十年的日期。

CREATE TABLE [dbo].[Calendar](
    [dt] [date] NOT NULL,
 CONSTRAINT [PK_Calendar] PRIMARY KEY CLUSTERED 
(
    [dt] ASC
))
Run Code Online (Sandbox Code Playgroud)

在我的系统中,它有一些额外的列,例如[IsLastDayOfMonth], [IsLastDayOfQuarter],这在某些报告中很有用,但在您的情况下,您只需要日期列。有很多方法可以填充这样的表

例如,1900-01-01 的 100K 行(约 270 年):

INSERT INTO dbo.Calendar (dt)
SELECT TOP (100000) 
    DATEADD(day, ROW_NUMBER() OVER (ORDER BY s1.[object_id])-1, '19000101') AS dt
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

样本数据

DECLARE @Stocks TABLE (
    StockId int
    , ProductId int
    , AvailableFromDate date);

INSERT INTO @Stocks(StockId, ProductId, AvailableFromDate) VALUES
(1, 1, '2016-01-01'),
(2, 1, '2016-01-01');

DECLARE @RentStockOrders TABLE (
    RentStockOrderId int
    , StockId int
    , BeginRentDate date
    , EndRentDate date);

INSERT INTO @RentStockOrders (RentStockOrderId, StockId, BeginRentDate, EndRentDate) VALUES
(1, 1, '2016-01-15', '2016-02-14'),
(2, 2, '2016-01-30', '2016-02-20'),
(3, 2, '2016-02-26', '2016-03-07'),
(4, 1, '2016-02-29', '2016-03-14');
Run Code Online (Sandbox Code Playgroud)

参数

DECLARE @ParamProductID int = 1;
DECLARE @ParamDays int = 14;
DECLARE @ParamStartDate date = '2015-12-01';
DECLARE @ParamEndDate date = '2017-01-01';
-- these dates define some reasonable limit
Run Code Online (Sandbox Code Playgroud)

第一个变体

事实证明,窗口函数只接受文字常量作为窗口的大小,而不接受变量。唉。尽管如此,我还是会展示这个查询,因为它说明了该方法,并展示了如果 SQL Server 支持将变量作为窗口大小,那么该方法将是多么简单。它还给出了我们可以用来验证第二个变体的正确答案。

WITH
CTE_AllDays
-- list all days and stock IDs between @ParamStartDate and @ParamEndDate
-- for each day and stock indicate whether it is available based on AvailableFromDate
AS
(
    SELECT
        S.StockId
        ,dbo.Calendar.dt
        ,CASE WHEN dbo.Calendar.dt >= S.AvailableFromDate 
            THEN 1 ELSE 0 END AS AvailableStockDay
        -- 1 - available
        -- 0 - not available
    FROM
        @Stocks AS S
        INNER JOIN dbo.Calendar ON
            dbo.Calendar.dt >= @ParamStartDate
            AND dbo.Calendar.dt <= @ParamEndDate
    WHERE
        S.ProductId = @ParamProductID
)
,CTE_BookedDays
-- list all booked (unavailable) days for each stock ID
AS
(
    SELECT
        S.StockId
        ,CA.dt
        ,0 AS AvailableStockDay
        -- 0 - not available
    FROM
        @RentStockOrders AS R
        INNER JOIN @Stocks AS S ON S.StockId = R.StockId
        CROSS APPLY
        (
            SELECT dbo.Calendar.dt
            FROM dbo.Calendar
            WHERE
                dbo.Calendar.dt >= @ParamStartDate
                AND dbo.Calendar.dt <= @ParamEndDate
                AND dbo.Calendar.dt >= R.BeginRentDate
                AND dbo.Calendar.dt <= R.EndRentDate
        ) AS CA
    WHERE
        S.ProductId = @ParamProductID
)
,CTE_Daily
-- combine individual availability flags
-- first: multiply to get the final availability for a stock ID and day
-- second: group further by day and SUM flags
AS
(
    SELECT
        CTE_AllDays.dt
        ,CASE WHEN 
            SUM(
            CTE_AllDays.AvailableStockDay * ISNULL(CTE_BookedDays.AvailableStockDay, 1)
            ) = 0 
        THEN 0 ELSE 1 END AS AvailableDay
        -- day is available, if any stock is available
        -- SUM=0 - not available
        -- SUM>0 - available
    FROM
        CTE_AllDays
        LEFT JOIN CTE_BookedDays ON
            CTE_BookedDays.StockId = CTE_AllDays.StockId AND
            CTE_BookedDays.dt = CTE_AllDays.dt
    GROUP BY
        CTE_AllDays.dt
)
,CTE_Sum
-- rolling sum of flags with 14 days window
AS
(
    SELECT
        dt
        ,SUM(CTE_Daily.AvailableDay) OVER (ORDER BY CTE_Daily.dt
            ROWS BETWEEN CURRENT ROW AND 13 FOLLOWING) AS AvailableConsecutive
    -- we can't put @ParamDays here instead of constant
    FROM CTE_Daily
)
-- If a rolling sum =  14, 
-- it means that all 14 consecutive days are available

-- If a rolling sum <> 14, 
-- it means that at least one of the 14 consecutive days is not available
SELECT dt
FROM CTE_Sum
WHERE AvailableConsecutive <> 14
-- we can put @ParamDays here instead of 14, but not above
ORDER BY dt;
Run Code Online (Sandbox Code Playgroud)

结果

2015-12-01请注意,我在正式发布之前开始了日期范围。

+------------+
|     dt     |
+------------+
| 2015-12-01 |
| 2015-12-02 |
| 2015-12-03 |
| 2015-12-04 |
| 2015-12-05 |
| 2015-12-06 |
| 2015-12-07 |
| 2015-12-08 |
| 2015-12-09 |
| 2015-12-10 |
| 2015-12-11 |
| 2015-12-12 |
| 2015-12-13 |
| 2015-12-14 |
| 2015-12-15 |
| 2015-12-16 |
| 2015-12-17 |
| 2015-12-18 |
| 2015-12-19 |
| 2015-12-20 |
| 2015-12-21 |
| 2015-12-22 |
| 2015-12-23 |
| 2015-12-24 |
| 2015-12-25 |
| 2015-12-26 |
| 2015-12-27 |
| 2015-12-28 |
| 2015-12-29 |
| 2015-12-30 |
| 2015-12-31 |
| 2016-01-17 |
| 2016-01-18 |
| 2016-01-19 |
| 2016-01-20 |
| 2016-01-21 |
| 2016-01-22 |
| 2016-01-23 |
| 2016-01-24 |
| 2016-01-25 |
| 2016-01-26 |
| 2016-01-27 |
| 2016-01-28 |
| 2016-01-29 |
| 2016-01-30 |
| 2016-01-31 |
| 2016-02-01 |
| 2016-02-02 |
| 2016-02-03 |
| 2016-02-04 |
| 2016-02-05 |
| 2016-02-06 |
| 2016-02-07 |
| 2016-02-08 |
| 2016-02-09 |
| 2016-02-10 |
| 2016-02-11 |
| 2016-02-12 |
| 2016-02-13 |
| 2016-02-14 |
| 2016-02-16 |
| 2016-02-17 |
| 2016-02-18 |
| 2016-02-19 |
| 2016-02-20 |
| 2016-02-21 |
| 2016-02-22 |
| 2016-02-23 |
| 2016-02-24 |
| 2016-02-25 |
| 2016-02-26 |
| 2016-02-27 |
| 2016-02-28 |
| 2016-02-29 |
| 2016-03-01 |
| 2016-03-02 |
| 2016-03-03 |
| 2016-03-04 |
| 2016-03-05 |
| 2016-03-06 |
| 2016-03-07 |
| 2016-12-20 |
| 2016-12-21 |
| 2016-12-22 |
| 2016-12-23 |
| 2016-12-24 |
| 2016-12-25 |
| 2016-12-26 |
| 2016-12-27 |
| 2016-12-28 |
| 2016-12-29 |
| 2016-12-30 |
| 2016-12-31 |
| 2017-01-01 |
+------------+
Run Code Online (Sandbox Code Playgroud)

第二种变体

查询的第一部分直到CTE_Daily是相同的。然后,我将使用间隙和岛屿方法来查找可用日期的岛屿并计算它们的大小。

WITH
CTE_AllDays
-- list all days and stock IDs between @ParamStartDate and @ParamEndDate
-- for each day and stock indicate whether it is available based on AvailableFromDate
AS
(
    SELECT
        S.StockId
        ,dbo.Calendar.dt
        ,CASE WHEN dbo.Calendar.dt >= S.AvailableFromDate 
            THEN 1 ELSE 0 END AS AvailableStockDay
        -- 1 - available
        -- 0 - not available
    FROM
        @Stocks AS S
        INNER JOIN dbo.Calendar ON
            dbo.Calendar.dt >= @ParamStartDate
            AND dbo.Calendar.dt <= @ParamEndDate
    WHERE
        S.ProductId = @ParamProductID
)
,CTE_BookedDays
-- list all booked (unavailable) days for each stock ID
AS
(
    SELECT
        S.StockId
        ,CA.dt
        ,0 AS AvailableStockDay
        -- 0 - not available
    FROM
        @RentStockOrders AS R
        INNER JOIN @Stocks AS S ON S.StockId = R.StockId
        CROSS APPLY
        (
            SELECT dbo.Calendar.dt
            FROM dbo.Calendar
            WHERE
                dbo.Calendar.dt >= @ParamStartDate
                AND dbo.Calendar.dt <= @ParamEndDate
                AND dbo.Calendar.dt >= R.BeginRentDate
                AND dbo.Calendar.dt <= R.EndRentDate
        ) AS CA
    WHERE
        S.ProductId = @ParamProductID
)
,CTE_Daily
-- combine individual availability flags
-- first: multiply to get the final availability for a stock ID and day
-- second: group further by day and SUM flags
AS
(
    SELECT
        CTE_AllDays.dt
        ,CASE WHEN 
            SUM(
            CTE_AllDays.AvailableStockDay * ISNULL(CTE_BookedDays.AvailableStockDay, 1)
            ) = 0 
        THEN 0 ELSE 1 END AS AvailableDay
        -- day is available, if any stock is available
        -- SUM=0 - not available
        -- SUM>0 - available
    FROM
        CTE_AllDays
        LEFT JOIN CTE_BookedDays ON
            CTE_BookedDays.StockId = CTE_AllDays.StockId AND
            CTE_BookedDays.dt = CTE_AllDays.dt
    GROUP BY
        CTE_AllDays.dt
)
,CTE_RowNumbers
-- calculate two sets of row numbers to isolate consecutive rows with 0s and 1s 
-- (gaps and islands)
AS
(
    SELECT
        dt
        ,AvailableDay
        ,ROW_NUMBER() OVER (ORDER BY dt) AS rn1
        ,ROW_NUMBER() OVER (PARTITION BY AvailableDay ORDER BY dt) AS rn2
    FROM CTE_Daily
)
,CTE_Groups
-- each gaps and island will have the same GroupNumber
-- count the size of the Group
-- number the rows within each Group to find if @ParamDays rows fit into the Group
AS
(
    SELECT
        dt
        ,AvailableDay
        ,GroupNumber
        ,COUNT(*) OVER (PARTITION BY GroupNumber) AS GroupSize
        ,ROW_NUMBER() OVER (PARTITION BY GroupNumber ORDER BY dt) AS GroupRN
    FROM
        CTE_RowNumbers
        CROSS APPLY (SELECT rn1 - rn2 AS GroupNumber) AS CA
)
SELECT
    dt
    --,CASE WHEN AvailableDay = 1 AND GroupSize - GroupRN + 1 >= @ParamDays
    --THEN 1 ELSE 0 END AS AvailableConsecutive
FROM CTE_Groups
WHERE
    CASE WHEN AvailableDay = 1 AND GroupSize - GroupRN + 1 >= @ParamDays
    THEN 1 ELSE 0 END = 0
    -- AvailableConsecutive = 0 to list all unavailable days
ORDER BY dt;
Run Code Online (Sandbox Code Playgroud)

结果与第一个变体相同,但此变体使用参数@ParamDays

要了解其工作原理,请从第一个 CTE 开始运行查询,检查结果,然后添加下一个 CTE,检查结果,依此类推。