Help creating a dynamic table of numbers

  • Hello all,

    I'm trying to dynamically create a table of days spanned for a aging report. I would like the end user to be able to choose the days span like 10, 15, 30 days and also how many buckets like show me 3 buckets at 10 day intervals. That result would be 0-10, 11-20, 21-30. I am trying some code with a WHILE loop but I'm really horrible with them. Any help would be appreciated.

    Current code i'm tryingDECLARE @Interval INT = 5

    ,@Span INT = 30

    ,@MinNum INT

    ,@MaxNum INt

    DECLARE @Table TABLE (MinNum INT, MaxNum INT )

    WHILE (SELECT COUNT(*) FROM @Table) < @Interval

    BEGIN

    SET @MinNum = 0 + @Span + 1

    SET @MaxNum = @Span + @Span

    INSERT INTO @Table (MinNum, MaxNum)

    SELECT @MinNum, @MaxNum

    END

    SELECT * FROM @Table

    ***SQL born on date Spring 2013:-)

  • I'm closer I just need to figure out how to add the 1

    DECLARE @Interval INT = 5

    ,@Span INT = 30

    ,@MinNum INT = 0

    ,@MaxNum INt

    DECLARE @Table TABLE (MinNum INT, MaxNum INT )

    SET @MaxNum = @Span

    WHILE (SELECT COUNT(*) FROM @Table) < @Interval

    BEGIN

    INSERT INTO @Table (MinNum, MaxNum)

    SELECT @MinNum, @MaxNum

    SET @MinNum = @MinNum + @Span

    SET @MaxNum = @MaxNum + @Span;

    END

    SELECT * FROM @Table

    ***SQL born on date Spring 2013:-)

  • Will this approach work for you? I don't have a while loop because, from what you described, I don't think it's necessary.

    I you have a starting date, from here you can DATEADD to include date columns if you need them.

    DECLARE @DaysPerInterval Integer = 10,

    @NumberOfIntervals Integer = 3;

    WITH ctePartitions AS (

    SELECT t.N, Window = NTILE(@NumberOfIntervals) OVER(ORDER BY t.N)

    FROM dbo.TallyN(@NumberOfIntervals * @DaysPerInterval) t

    )

    SELECT Window, RangeStart = MIN(N), RangeEnd = MAX(N)

    FROM ctePartitions

    GROUP BY Window

    ORDER BY Window;

    I used the dbo.TallyN function, which is Itzik Ben Gan's zero-read tally ITVF. Here's the function:

    ALTER FUNCTION dbo.TallyN(@N Bigint) RETURNS TABLE WITH SCHEMABINDING

    AS

    RETURN WITH level0 AS (

    SELECT 0 AS g UNION ALL SELECT 0), --2

    level1 AS (SELECT 0 AS g FROM level0 AS a CROSS JOIN level0 AS b), --2^2 = 4

    level2 AS (SELECT 0 AS g FROM level1 AS a CROSS JOIN level1 AS b), --4^2 = 16

    level3 AS (SELECT 0 AS g FROM level2 AS a CROSS JOIN level2 AS b), --16^2 = 256

    level4 AS (SELECT 0 AS g FROM level3 AS a CROSS JOIN level3 AS b), --256^2 = 65536

    level5 AS (SELECT 0 AS g FROM level4 AS a CROSS JOIN level4 AS b), --65536^2 = 4294967296

    Tally AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n FROM level5)

    SELECT TOP (@N) N

    FROM Tally

    ORDER BY N;

  • This is my take on the same issue:

    DECLARE@Interval int = 10,

    @Span int = 60,

    @MinNum int = 0,

    @MaxNum int

    DECLARE@Table TABLE (MinN INT, MaxN INT)

    SET @MaxNum = @MinNum + @Span

    INSERT INTO @Table (MinN, MaxN)

    select

    CASE

    WHEN n = 0 THEN 0

    ELSE n * @Interval + 1

    END,

    n * @Interval + @Interval

    from tally

    where n BETWEEN (@MinNum/@Interval) AND ((@MinNum + @Span)/@Interval) - 1

    SELECT * FROM @Table

    However it is not as elegant as Ed's and probably will not scale well, perhaps someone can improve or tell me where my thinking is wrong; I have kept the OP's option for minimum start point.

    It is based around the excellent article by Jeff Moden on the tally table, I used the following to generate it;

    SELECT TOP 10000 IDENTITY(int, 0, 1) N

    INTO Tally

    FROM master.dbo.syscolumns sc1,

    master.dbo.syscolumns

    GO

    ALTER TABLE Tally

    ADD CONSTRAINT Tally_PK PRIMARY KEY CLUSTERED (N)

    GO

    .

    ...

  • SELECT

    MinNum = @MinNum + (n * @Span),

    MaxNum = @MinNum + ((n+1) * @Span)

    FROM (VALUES (0),(1),(2),(3),(4),(5),(6)) d (n)

    WHERE n < @Interval

    β€œWrite the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • WOW, That's why I love coming here so many great solutions for the same issue. I'm testing each one out but I think I may be replacing my loop with Chris's solutions as it is so short and sweet!

    So Awesome thank you guys!!:-D

    ***SQL born on date Spring 2013:-)

  • Quick thoughts Thomas, do not use a while loop as there is no need for it and neither a table variable which will only introduce extra pressure on the tempdb.

    😎

    Here is my version which is unsurprisingly very similar to Chris's πŸ˜€

    USE TEEST;

    GO

    DECLARE @Interval INT = 5 ;

    DECLARE @Span INT = 10 ;

    DECLARE @MinNum INT = 0 ;

    DECLARE @MaxNum INT = @Span;

    ;WITH T(N) AS (SELECT N FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) AS x(N))

    , NUMS(N) AS (SELECT TOP ( @Interval) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS N FROM T T1,T T2,T T3,T T4,T T5)

    SELECT

    @MinNum + ( NM.N * @Span) AS MinNum

    ,@MinNum + ((NM.N + 1) * @Span) - 1 AS MaxNum

    FROM NUMS NM;

  • Thanks Eric, the reason I am using a table variable is so I can join the results. Ultimately I'm trying to end up with a adhoc table that I can join on date to find the appropriate aging bucket for money.

    Here is my current code any suggestions on making it simpler or faster is greatly appreciated!!!:-D

    ALTER PROCEDURE Cus_Aging_Buckets

    @AsOfDate date= NULL, @Interval int= 10, @DaySpan int= 30

    AS

    SET @AsOfDate = GETDATE();

    /***********************************************************************

    @DaysSpan is the width of the aging bucket in days

    @Interval is the number of overall buckets

    @AsOfDate is the date used to define aging up to that date

    ************************************************************************/

    /************************************************************************

    Generate Calendar table. Use ROW_NUMBER() to find the date differences going in

    reverse from @AsOfDate

    **************************************************************************/

    DECLARE @Cal TABLE

    (

    DaysDiff int, EventDate date

    );

    WITH E1(N) AS (SELECT 1 UNION ALL SELECT 1), --2 rows

    E2(N) AS (SELECT 1 FROM E1 a, E1 b), --4 rows

    E4(N) AS (SELECT 1 FROM E2 a, E2 b), --16 rows

    E8(N) AS (SELECT 1 FROM E4 a, E4 b), --256 rows

    E16(N) AS (SELECT 1 FROM E8 a, E8 b), --65536 rows

    cteTally(N) AS (

    SELECT TOP (10000) ROW_NUMBER() OVER(ORDER BY(SELECT NULL))

    FROM E16

    )

    INSERT INTO @Cal

    SELECT SeqNo = t.N, [Date] = dt.DT

    FROM cteTally AS t

    CROSS APPLY

    ( --=== Create the date

    SELECT DT = DATEADD(dd, ( t.N - 1 ) * -1, @AsOfDate)) AS dt;

    /************************************************************************

    Generate table with bucket values chosen by end user.

    **************************************************************************/

    DECLARE @Table TABLE

    (

    MinNum int, MaxNum int

    );

    DECLARE @MinNum int= 0, @MaxNum int;

    WITH T(N) AS (SELECT N FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) AS x(N))

    , NUMS(N) AS (SELECT TOP ( @Interval) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS N FROM T T1,T T2,T T3,T T4,T T5)

    INSERT INTO @Table (MinNum, MaxNum)

    SELECT

    @MinNum + ( NM.N * @DaySpan) AS MinNum

    ,@MinNum + ((NM.N + 1) * @DaySpan) AS MaxNum

    FROM NUMS NM;

    UPDATE @Table

    SET MinNum = MinNum + 1

    FROM @Table

    WHERE MinNum > 0;

    /************************************************************************

    Combine Calendar and Table to give date and what dynamic bucket it belongs

    to.

    **************************************************************************/

    DECLARE @AgingBucket TABLE (DaySeq INT, BucketDate DATE, BucketName VARCHAR (100))

    INSERT INTO @AgingBucket (DaySeq, BucketDate, BucketName)

    SELECT c.DaysDiff, c.EventDate,

    CASE

    WHEN c.DaysDiff BETWEEN MinNum AND MaxNum THEN CAST(MinNum AS varchar(10))+' AND '+CAST(MaxNum AS varchar(10))+' Bucket'

    WHEN c.DaysDiff > MAX(t.MaxNum) OVER() THEN CAST(MAX(t.MaxNum) OVER() AS varchar(10))+' + bucket'

    END

    FROM @Cal AS c

    LEFT JOIN

    @Table AS t

    ON c.DaysDiff BETWEEN t.MinNum AND t.MaxNum;

    SELECT * FROM @AgingBucket

    ***SQL born on date Spring 2013:-)

  • Here's my take on the problem. First, you need a Tally function. Here's my permutation of Itzik Ben-Gan's work. The "zero" thing needs a little work but usually good enough until I get to it.

    CREATE FUNCTION [dbo].[fnTally]

    /**********************************************************************************************************************

    Purpose:

    Return a column of BIGINTs from @ZeroOrOne up to and including @MaxN with a max value of 1 Trillion.

    As a performance note, it takes about 00:02:10 (hh:mm:ss) to generate 1 Billion numbers to a throw-away variable.

    Usage:

    --===== Syntax example (Returns BIGINT)

    SELECT t.N

    FROM dbo.fnTally(@ZeroOrOne,@MaxN) t

    ;

    Notes:

    1. Based on Itzik Ben-Gan's cascading CTE (cCTE) method for creating a "readless" Tally Table source of BIGINTs.

    Refer to the following URLs for how it works and introduction for how it replaces certain loops.

    http://www.sqlservercentral.com/articles/T-SQL/62867/

    http://sqlmag.com/sql-server/virtual-auxiliary-table-numbers

    2. To start a sequence at 0, @ZeroOrOne must be 0 or NULL. Any other value that's convertable to the BIT data-type

    will cause the sequence to start at 1.

    3. If @ZeroOrOne = 1 and @MaxN = 0, no rows will be returned.

    5. If @MaxN is negative or NULL, a "TOP" error will be returned.

    6. @MaxN must be a positive number from >= the value of @ZeroOrOne up to and including 1 Billion. If a larger

    number is used, the function will silently truncate after 1 Billion. If you actually need a sequence with

    that many values, you should consider using a different tool. ;-)

    7. There will be a substantial reduction in performance if "N" is sorted in descending order. If a descending

    sort is required, use code similar to the following. Performance will decrease by about 27% but it's still

    very fast especially compared with just doing a simple descending sort on "N", which is about 20 times slower.

    If @ZeroOrOne is a 0, in this case, remove the "+1" from the code.

    DECLARE @MaxN BIGINT;

    SELECT @MaxN = 1000;

    SELECT DescendingN = @MaxN-N+1

    FROM dbo.fnTally(1,@MaxN);

    8. There is no performance penalty for sorting "N" in ascending order because the output is explicity sorted by

    ROW_NUMBER() OVER (ORDER BY (SELECT NULL))

    Revision History:

    Rev 00 - Unknown - Jeff Moden

    - Initial creation with error handling for @MaxN.

    Rev 01 - 09 Feb 2013 - Jeff Moden

    - Modified to start at 0 or 1.

    Rev 02 - 16 May 2013 - Jeff Moden

    - Removed error handling for @MaxN because of exceptional cases.

    Rev 03 - 22 Apr 2015 - Jeff Moden

    - Modify to handle 1 Trillion rows for experimental purposes.

    **********************************************************************************************************************/

    (@ZeroOrOne BIT, @MaxN BIGINT)

    RETURNS TABLE WITH SCHEMABINDING AS

    RETURN WITH

    E1(N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1) --10E1 or 10 rows

    , E4(N) AS (SELECT 1 FROM E1 a, E1 b, E1 c, E1 d) --10E4 or 10 Thousand rows

    ,E12(N) AS (SELECT 1 FROM E4 a, E4 b, E4 c) --10E12 or 1 Trillion rows

    SELECT N = 0 WHERE ISNULL(@ZeroOrOne,0)= 0 --Conditionally start at 0.

    UNION ALL

    SELECT TOP(@MaxN) N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E12 -- Values from 1 to @MaxN

    ;

    Instead of writing a stored procedure for this, I built an iTVF so that you could actually do a join to it (sometimes not a great idea) but it can also be used as a part of a SELECT/INTO if you want to build a quick Temp Table and index it.

    Comments and usage are in the code. Wrote it to meet the output of your last bit of code above.

    CREATE FUNCTION dbo.DateBuckets

    /***************************************************************************************************

    PURPOSE:

    Returns a 10,000 row date table with a sequence number, date, and bucket-range that identifies the

    range of days that have passed for the given bucket.

    ----------------------------------------------------------------------------------------------------

    INPUTS:

    @EndDate - (DATE) The final date of the 10,000 date return.

    @NumberOfBuckets - (INT) The number of range buckets. Does not include the final overage bucket.

    @DaysPerBucket - (INT) Pretty much what is says. The number of days per range bucket.

    ----------------------------------------------------------------------------------------------------

    RETURNS:

    DaySeq - (BIGINT) The row with a BucketDate equal to @EndDate will have the value of "1".

    BucketDate - (DATE) A date starting at @EndDate and sequentially decreasing by one day

    for a total of 10,000 days.

    BucketName - (VARCHAR(32)) Format for Range Buckets: SSS and EEE Bucket

    where SSS is the starting number of days and EEE is the ending number

    of days.

    Format for Overage Buckets: MAX + Bucket

    where MAX is the product of @NumberOfBuckets * @DaysPerBucket

    ----------------------------------------------------------------------------------------------------

    USAGE:

    --===== Basic Syntax

    SELECT DaySeq

    ,BucketDate

    ,BucketName

    FROM dbo.DateBuckets(@EndDate,@NumberOfBuckets,@DaysPerBucket)

    ORDER BY DaySeq

    ;

    --===== Using today as an end date, return 10 buckets of 30 days each plus the

    -- "overage" bucket for the remaining part of 10,000 days.

    SELECT DaySeq

    ,BucketDate

    ,BucketName

    FROM dbo.DateBuckets(GETDATE(),10,30)

    ORDER BY DaySeq

    ;

    ----------------------------------------------------------------------------------------------------

    Revision History:

    Rev 00 - 04 Jul 2016 - Jeff Moden

    - Initial creation (only tested to a minor degree)

    - Ref: http://www.sqlservercentral.com/Forums/Topic1799270-391-1.aspx

    ***************************************************************************************************/

    (

    @EndDate DATE

    ,@NumberOfBuckets INT --Change these to BIGINT if you want to get rid of a ton

    ,@DaysPerBucket INT --of implicit conversions.

    )

    RETURNS TABLE WITH SCHEMABINDING AS

    RETURN WITH

    cteDates AS

    ( --=== Generate the dates and other base data

    SELECT DaySeq = t.N+1

    ,BucketDate = DATEADD(dd,-t.n,@EndDate)

    ,BucketBase = t.N / @DaysPerBucket * @DaysPerBucket --Starts at 0, INT math to bucketize.

    FROM dbo.fnTally(0,9999) t

    )

    ,cteBucketEndDays AS

    ( --=== Calculates the bucket range values

    SELECT DaySeq

    ,BucketDate --== Add 1 to base for start day unless it's a 0.

    ,BucketStartDay = CASE WHEN BucketBase > 0 THEN BucketBase+1 ELSE 0 END

    ,BucketEndDay = BucketBase + @DaysPerBucket

    FROM cteDates

    ) --=== Format the final output

    SELECT DaySeq

    ,BucketDate

    ,BucketName = CASE -- Calculate start as range or overage value

    WHEN BucketStartDay < @NumberOfBuckets * @DaysPerBucket

    THEN CAST(BucketStartDay AS VARCHAR(10))

    ELSE CAST(@NumberOfBuckets * @DaysPerBucket AS VARCHAR(10))

    END

    + ' '

    + CASE -- Calulate end as a range or "+" for overage value

    WHEN BucketStartDay < @NumberOfBuckets * @DaysPerBucket

    THEN 'and ' + CAST(BucketEndDay AS VARCHAR(10))

    ELSE '+'

    END

    + ' Bucket'

    FROM cteBucketEndDays

    ;

    GO

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Jeff,

    As usually most excellent. I have no idea why doing a ITVF slipped by my feeble mind. Very, very elegant solution, works perfectly and is much cleaner and now I can use the Tally function for other things without having to re-code the wheel.

    Your a inspiration sir ! Thanks again:-D

    ***SQL born on date Spring 2013:-)

  • thomashohner (7/5/2016)


    ... re-code the wheel.

    Haha! +1

    "I cant stress enough the importance of switching from a sequential files mindset to set-based thinking. After you make the switch, you can spend your time tuning and optimizing your queries instead of maintaining lengthy, poor-performing code."

    -- Itzik Ben-Gan 2001

  • thomashohner (7/5/2016)


    Jeff,

    As usually most excellent. I have no idea why doing a ITVF slipped by my feeble mind. Very, very elegant solution, works perfectly and is much cleaner and now I can use the Tally function for other things without having to re-code the wheel.

    Your a inspiration sir ! Thanks again:-D

    Thank you for the very kind feedback, Thomas.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

Viewing 12 posts - 1 through 11 (of 11 total)

You must be logged in to reply to this topic. Login to reply