简体   繁体   中英

SQL Server - Mock Records from Stored Procedure

Apologies if this is a duplicate - I have searched for an answer, but what I found isn't exactly what I'm looking for.

I have a very simple stored procedure that returns date based stock from an inventory table. It takes start date and end date params with a stockist id .

The query is like the following...

select  
    S.Date, 
    S.Amount 
from Stock S
where 
    S.StockistID = @pi_stockistId and
    S.Date >= @pi_startDate and
    S.Date <= @pi_endDate

What I need to ensure is that there is always a row returned for the date range requested - even if there is no record in the Database for that date and stockist. Mocked records will have an Amount of Zero.

The data is being fed through to an external system, so I don't need to insert the records unless said system increases the value from zero (I have no need for storing thousands of null records in the table).

If a subsequent call from the external system increases the amount, at this point I will insert the record to the table.

I know I could do this by creating a temp table variable and inserting non-existing records into the result set - I just wondered if I can do anything in the query to prevent having to use table variables.

Again, apologies if this has already been asked.

I'm using SQL Server 2014.

Thanks

Example

Here's a sample of data from the Stock Table

------------------------------------
| Date       | StockistId | Amount |
------------------------------------
| 12/04/2017 | 1          | 10     |
| 14/04/2017 | 1          | 20     |
------------------------------------

If I run my query for the dates 12/04/2017 - 14/04/2017 for Stockist 1, I get the following...

-----------------------
| Date       | Amount |
-----------------------
| 12/04/2017 | 10     |
| 14/04/2017 | 20     |
-----------------------

As there is no record for 13/04/2017

What I would like to return is...

-----------------------
| Date       | Amount |
-----------------------
| 12/04/2017 | 10     |
| 13/04/2017 | 0      |
| 14/04/2017 | 20     |
-----------------------

Where my query has 'mocked' up a record for 13/04/2017

If you have a calendar table this becomes a very easy query.

SELECT  c.Date,
        Amount = ISNULL(s.Amount, 0)
FROM    dbo.Calendar AS c
        LEFT JOIN Stock AS s
            ON s.Date = c.Date
            AND s.StockistID = @pi_stockistId
WHERE   c.Date >= @pi_startDate
AND     c.Date <= @pi_endDate;

If you don't have a calendar table I would recommend that you create one, they are very useful. If you can't create one it is fairly easy to generate one on the fly:

First just generate a series of numbers using cross joins and ROW_NUMBER()

WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2)
SELECT  ROW_NUMBER() OVER(ORDER BY N) - 1
FROM    N3;

This will generate numbers from 0 - 9999, which should cover any date range you need.

Then you can use this number along with DATEADD to get the days in your range:

DECLARE @pi_startDate DATE = '20170101',
        @pi_endDate DATE = '20170301';

WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (N) AS (SELECT ROW_NUMBER() OVER(ORDER BY N) - 1 FROM N3)

SELECT  TOP (DATEDIFF(DAY, @pi_startDate, @pi_endDate) + 1)
        Date = DATEADD(DAY, N, @pi_startDate)
FROM    Numbers;

Which gives you your dates.

Then for the final step you just need to left join to your stocks table

DECLARE @pi_startDate DATE = '20170101',
        @pi_endDate DATE = '20170301';

WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Calendar (Date) AS 
(   SELECT TOP (DATEDIFF(DAY, @pi_startDate, @pi_endDate) + 1)
            DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY N) - 1, @pi_startDate)
    FROM    N2
)
SELECT  c.Date,
        Amount = ISNULL(s.Amount, 0)
FROM    Calendar AS c
        LEFT JOIN Stock AS s
            ON s.Date = c.Date
            AND s.StockistID = @pi_stockistId;

More about calendar tables, and generating series without loops can be found in a 3 part article:

While accepted answer is perfectly fine, I'd just like to provide another way to create Dates on fly - using recursive CTE. Maybe debatable, but I'd say it's simpler.

DECLARE @startDate DATE = '20170401';
DECLARE @endDate DATE = '20170410';

WITH CTE_Dates AS 
(
    SELECT @StartDate AS Dt
    UNION ALL
    SELECT DATEADD(DAY,1,Dt)
    FROM CTE_Dates 
    WHERE Dt < @endDate
)
SELECT * 
FROM CTE_Dates
--LEFT JOIN to your data here
OPTION (MAXRECURSION 0); -- needed if range is more than 100 days

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM