Tag: T-SQL

  • Generating a Constrained Random Date–#SQLNewBlogger

    Another post for me that is simple and hopefully serves as an example for people trying to get blogging as #SQLNewBloggers.

    There have been lots of posts on the topic of generating random values, and some great articles. One of my favorites is Jeff Moden’s Generating Test Data: Part 1 – Generating Random Integers and Floats. Part 2 deals with dates, and that’s actually what I needed, but really I needed part 1.

    In my situation, I was helping a customer generate some random data. They had filled a table, Customers, with some data.

    2018-08-24 13_05_44-Microsoft Edge
    The goal was to populate a child table with some data. The child table had a date column that was supposed to be between the Entered and Exit dates in the Customer table.

    My update would have a join, obviously, and I can reference the enter and exit date, but how to get a date between them? My first thought was that I wanted a DATEADD() function. Something like this:

    UPDATE ce
    SET ce.EventTimeStamp = DATEADD( MINUTE, SomeRandomValue, c.CustomerExitedDateTime)), c.CustomerEnteredDateTime)
    FROM   dbo.Customer AS c
    INNER JOIN dbo.CustomerEvent AS ce
    ON ce.CustomerID = c.CustomerID

    The trick is what random value to use? If you look through Jeff’s article, you will see that the trick is to use a tally table and the NEWID() function. However, this doesn’t work:

     UPDATE ce
    SET ce.EventTimeStamp = DATEADD( MINUTE, NEWID(), c.CustomerEnteredDateTime)
    FROM   dbo.Customer AS c
    INNER JOIN dbo.CustomerEvent AS ce
    ON ce.CustomerID = c.CustomerID
    ;

    What I need to do is convert the GUID to a number. In this case, I added CHECKSUM around it, again, as in Jeff’s article. Then use ABS() to enclose this to get all positive numbers.

     UPDATE ce
    SET ce.EventTimeStamp = DATEADD( MINUTE, ABS(CHECKSUM((NEWID())))), c.CustomerEnteredDateTime)
    FROM   dbo.Customer AS c
    INNER JOIN dbo.CustomerEvent AS ce
    ON ce.CustomerID = c.CustomerID
    ;

    This gives me values, but they aren’t constrained. What I need to do is limit the upper random value so that the end time doesn’t exceed the Customer.CustomerExitDateTime for that row.

    To do this, I can constraint a large set of numbers to some value with the modulo function. This will limit what values can appear. The basic script is this:

    UPDATE ce
    SET ce.EventTimeStamp = DATEADD( MINUTE, ABS(CHECKSUM((NEWID())))) % 10, c.CustomerEnteredDateTime)
    FROM   dbo.Customer AS c
    INNER JOIN dbo.CustomerEvent AS ce
    ON ce.CustomerID = c.CustomerID
    ;

    This would give me values between 1 and 0 minutes after the start time, but this doesn’t mean these values won’t be after the exit time. This is also an unrealistic window if most of the time the enter and exit times vary by hours.

    What I did instead was to use the difference between the enter and exit times, with DATEDIFF() as my modulo function. That gives me:

    WITH myTally (n)
    AS
    -- SQL Prompt formatting off
    (SELECT n = ROW_NUMBER() OVER (ORDER BY (SELECT null))
      FROM (VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)) a(n)
       CROSS JOIN (VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)) b(n)
    )
    UPDATE ce
    SET ce.EventTimeStamp = DATEADD( MINUTE, ABS(CHECKSUM((NEWID()))) % (DATEDIFF(MINUTE, c.CustomerEnteredDateTime, c.CustomerExitedDateTime)), c.CustomerEnteredDateTime)
    FROM   dbo.Customer AS c
    INNER JOIN dbo.CustomerEvent AS ce
    ON ce.CustomerID = c.CustomerID
    ;

    I run this, and I get the table updated with a random set of values.

    2018-08-24 13_19_08-Microsoft Edge

    SQLNewBlogger

    This was a problem in my daily work. It was a customer, but it could easily be an internal query problem. I spent about 10 minutes grabbing screen shots and taking apart the query I’d built.

    You can do this, too. Show us your mind working with the solutions you write in your own blog.

  • Remember the Default Window

    I ran across a question recently from a user about why they had strange results from a windowing query. This is better explained with an example, so let’s look at one.

    I have some data in a table. This is NFL data, and a sample of it looks like this:

    2018-08-22 18_57_25-SQLQuery1.sql - Plato_SQL2016.NFLAnalysis (PLATO_Steve (52))_ - Microsoft SQL Se

    What I want to do is compare the passing yards each year with the most current value for that player, showing the plus or minus. This means that for Aaron Rodgers, who threw for 1675 yards in 2017, I’d want to show this for the first few years of his career:

      PlayerName  NFLYear PassYards Most Recent Yards Difference
    ------------- ------- --------- ----------------- -----------
    Aaron Rodgers 2005 65 1675 -1610
    Aaron Rodgers 2006 46 1675 -1629
    Aaron Rodgers 2007 218 1675 -1457
    Aaron Rodgers 2008 4038 1675 2363

    This shows
    me an easy view of the years where he was better in his career than he is now. Last year was likely a down year because of injury, but we’ll see this year.

    In any case, if I run this query using LAST_VALUE() for the final year of his career, I don’t get the right results.

    2018-08-22 19_11_16-SQLQuery1.sql - Plato_SQL2016.NFLAnalysis (PLATO_Steve (52))_ - Microsoft SQL Se

    It seems as though in every row, I’m getting the current row as the last value, not the last value of the partition. My partition is by player, so I should only have a window for each player. In this case, I should have the years 2005-2017 for Aaron Rodgers. My ordering is by year, so the last value should be 1675.

    Why isn’t it?

    The reason has to do with the framing. As the window is consumed, the default values for the framing are between

    • start – unbounded preceding
    • end – current row

    That means the first row for 2005 has the range of 2005-2005. The preceding rows are this row, and the current row is this row. For 2006, we have the first row as 2005 and the current row as 2006. The last value in this case is 46.

    What we need to do is specify the entire window if we want that. In this case, we could use the current row as the start, but we certainly need the unbounded following rows.

    2018-08-22 19_18_30-SQLQuery1.sql - Plato_SQL2016.NFLAnalysis (PLATO_Steve (52))_ - Microsoft SQL Se

    This is a common mistake when writing window queries. I’d recommend you always include the partition and the framing to avoid any issues.

  • Will Terminators Be Required?

    I was looking at an article the other day and noticed that there was a CTE sample with the semicolon on the line before the code. I’ve been seeing this convention for years, starting your CTE with a semicolon because people aren’t sure this will get dropped in a batch with other code. It’s not that the CTE needs this, but the previous statement needs to be terminated. There are a few other T-SQL constructs that require any previous statements to be terminated, and as a result, we have a series of strange publishing conventions for sample code.

    I really wish that the language designers had thought this through and stopped trying to overload and reuse keywords. We could have avoided this with a simple CTE language element to indicate the structure. I know, I know, there are other considerations, but this seems annoying. I’m sure that the addition of the CTE fully expected that at some point semicolons would be required for all code.

    Brent wrote about this a few years ago. The Syntax page for T-SQL currently says this about the semicolon: “Transact-SQL statement terminator. Although the semicolon is not required for most statements in this version of SQL Server, it will be required in a future version.” There is no shortage of confusion about where terminators might be required and how to structure code, partially because SQL hasn’t ever used terminators and the evolution of the language has been a bit inconsistent with regard to structure.

    These days it seems that nothing will ever be removed. It appears that nothing else will be deprecated in this age of cloud software and feature toggles.I suspect at this point that we’ll see features wither in the codebase, not receiving future development if Microsoft doesn’t see them as valuable, living in limbo forever.

    I don’t think we’ll ever see terminators required, and as the amount of legacy code grows, it becomes less and less likely they will become mandated.

    Steve Jones

    The Voice of the DBA Podcast

    Listen to the MP3 Audio ( 3.2MB) podcast or subscribe to the feed at iTunes and Libsyn.

  • Solving Ken’s FizzBuzz 3D–#SQLNewBlogger

    Another post for me that is simple and hopefully serves as an example for people trying to get blogging as #SQLNewBloggers.

    I like the FizzBuzz test. It’s cute and fun, and I’ve had my kids solve it, just to think about what it means to structure a simple problem. It’s not a great test of whether you’re a good programmer, but if you can’t solve this, you probably aren’t.

    Ken Fisher set up a challenge to solve FizzBuzz with SQL, but in a 3D manner. He added a few challenge to this, like no modulus operator. I don’t know why you’d deliberately avoid using this, but it’s a programming exercise, so why not. I took the challenges because, well, Ken asked me do.

    SQLNewBlogger

    I’m putting this part first, because this is a good exercise, and a great post for your blog. Solve this yourself first, and write about it. Then you can read my code.

    The coding part of this probably took me 20-25 minutes to do. I worked on it in a few stages, pausing to do other work and let the solution simmer a bit. I realized a few times that I was making it harder, so the breaks were helpful.

    Writing this up was about 30 minutes, but it made me think about what I’d done.

    Building a Solution

    My first thought with this is I need a tally table. Of course, I’m building a set of coordinates from 1 to 100. I started here.

    WITH myTally (n)
    AS
    -- SQL Prompt formatting off
    (SELECT n = ROW_NUMBER() OVER (ORDER BY (SELECT null))
      FROM (VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)) a(n)
       CROSS JOIN (VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)) b(n)
    )
    , cteCoordinates (x, y, z)
    -- SQL Prompt formatting on
    AS
    (
    SELECT a.n ,
            b.n ,
            c.n
    FROM myTally AS a
         CROSS JOIN myTally AS b
         CROSS JOIN myTally AS c
    )
    SELECT x, y, z
    FROM cteCoordinates;

    This gave me my list of numbers.

    2018-07-02 14_40_48-SQLQuery4.sql - (local)_SQL2016.sandbox (vstsbuild (53))_ - Microsoft SQL Server

    My next step was to think about the FizzBuzz test. I can’t use Modulo, so how can I determine if I am evenly divisible by 3? I decided to look at the simple division operator. I got these results, but notice anything?

    2018-07-02 14_42_55-SQLQuery4.sql - (local)_SQL2016.sandbox (vstsbuild (53))_ - Microsoft SQL Server

    I get a new number every time the division changes. Immediately I think of window functions here. So I decided to do some checking here. Since I’m looking for a change, I went with a LAG function.

    If the LAG isn’t equal to the current value, I’ve had a change. Therefore, a Fizz.

    2018-07-02 14_45_44-SQLQuery4.sql - (local)_SQL2016.sandbox (vstsbuild (53))_ - Microsoft SQL Server

    Let’s clean this up to show Fizz and add the Buzz with a 5.

    2018-07-02 14_47_27-SQLQuery4.sql - (local)_SQL2016.sandbox (vstsbuild (53))_ - Microsoft SQL Server

    I’m getting there, but what about FizzBuzz? Well, the CASE will execute in order, so let’s add that one at the top.

    2018-07-02 14_49_00-SQLQuery4.sql - (local)_SQL2016.sandbox (vstsbuild (53))_ - Microsoft SQL Server

    I’ve mostly solved this, so let’s put this in my query. I’ll substitute the CASE in for each item of the cross join. I get this:

    WITH myTally (n)
    AS
    -- SQL Prompt formatting off
    (SELECT n = ROW_NUMBER() OVER (ORDER BY (SELECT null))
      FROM (VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)) a(n)
       CROSS JOIN (VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)) b(n)
    ),
    cteCoordinates (x, y, z)
    AS
    ( SELECT
    CASE
         WHEN ( a.n/3 != LAG(a.n/3, 1, 0) OVER (ORDER BY a.n)
           AND  a.n/5 != LAG(a.n/5, 1, 0) OVER (ORDER BY a.n)
             )THEN 'FizzBuzz'
         WHEN a.n/3 != LAG(a.n/3, 1, 0) OVER (ORDER BY a.n) THEN 'Fizz'
         WHEN a.n/5 != LAG(a.n/5, 1, 0) OVER (ORDER BY a.n) THEN 'Buzz'
         ELSE CAST(a.n AS VARCHAR(8)) END,
    case WHEN ( b.n/3 != LAG(b.n/3, 1, 0) OVER (ORDER BY b.n)
           AND   b.n/5 != LAG(b.n/5, 1, 0) OVER (ORDER BY b.n)
             )THEN 'FizzBuzz'
         WHEN b.n/3 != LAG(b.n/3, 1, 0) OVER (ORDER BY b.n) THEN 'Fizz'
         WHEN b.n/5 != LAG(b.n/5, 1, 0) OVER (ORDER BY b.n) THEN 'Buzz'
         ELSE CAST(b.n AS VARCHAR(8)) END,
    case WHEN ( c.n/3 != LAG(c.n/3, 1, 0) OVER (ORDER BY c.n)
           AND  c.n/5 != LAG(c.n/5, 1, 0) OVER (ORDER BY c.n)
             )THEN 'FizzBuzz'
         WHEN c.n/3 != LAG(c.n/3, 1, 0) OVER (ORDER BY c.n) THEN 'Fizz'
         WHEN c.n/5 != LAG(c.n/5, 1, 0) OVER (ORDER BY c.n) THEN 'Buzz'
         ELSE CAST(c.n AS VARCHAR(8)) END
        FROM mytally a
        CROSS JOIN mytally b
        CROSS JOIN mytally c
    )
    SELECT *
      FROM cteCoordinates
      ORDER BY cteCoordinates.x, cteCoordinates.y, cteCoordinates.z

    That doesn’t seem to work. Even forgetting the ordering, I have a mess.

    2018-07-02 14_58_53-SQLQuery4.sql - (local)_SQL2016.sandbox (vstsbuild (53))_ - Microsoft SQL Server

    At this point I took a break. Clearly I’m overlooking something in my five minutes of work.

    Debugging

    Let’s move the items around and get some ideas here. I’ll get the actual numbers and then the FizzBuzz results. When I print the coordinate values along with the decodes, I see this.

    2018-07-02 15_04_06-SQLQuery4.sql - (local)_SQL2016.sandbox (vstsbuild (53))_ - Microsoft SQL Server

    What’s my error? I’m not thinking of LAG() correctly. This is by row, and the lagging for the x and y coordinates (the first two columns), aren’t changing at the same rate.

    Aha.

    Let’s change this. I’ll setup a simpler CTE first, one that takes the values from 1-100 and returns the value as well as the FizzBuzz calculation. This gives me this code:

    WITH myTally (n)
    AS
    (
    SELECT n = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM
    -- SQL Prompt formatting off
    (    VALUES (1) ,(2) ,(3) ,(4) ,(5) ,(6) ,(7) ,(8) ,(9) ,(10)) AS a (n)
         CROSS JOIN    ( VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10) ) AS b (n)
         CROSS JOIN    ( VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10) ) AS c (n)
    -- SQL Prompt formatting on
    ) ,
          cteFizzBuzz (x, n)
    AS
    (
    SELECT CASE
                WHEN (n / 3 != LAG(n / 3, 1, 0) OVER (ORDER BY n)) AND (n / 5 != LAG(n / 5, 1, 0) OVER (ORDER BY n))
                   THEN 'FizzBuzz'
                WHEN n / 3 != LAG(n / 3, 1, 0) OVER (ORDER BY n) THEN
                    'Fizz'
                WHEN n / 5 != LAG(n / 5, 1, 0) OVER (ORDER BY n) THEN
                    'Buzz'
                ELSE
                    CAST(n AS VARCHAR(8))
            END, n
    FROM myTally
    ) ,    cteCoordinates (x, y, z)

    Now that I have this, let’s build the coordinates now. I’ll use the first set of code above as an example, and cross join the cteFizzBuzz with itself. I’ll make this the third CTE.

    ) ,    cteFinal (x, y, z, a, b, c)
    AS
    (
    SELECT a.n, b.n, c.n, a.x, b.x, c.x
    FROM           cteFizzBuzz AS a
         CROSS JOIN cteFizzBuzz AS b
         CROSS JOIN cteFizzBuzz AS c
    )

    Note that I’m returning the original values (for sorting) and the calculated FizzBuzz values, which are characters. If I didn’t care about this, I could ignore the first few columns.

    In my outer query, I display the words, but order by the numbers.

    SELECT 
        c.a, c.b, c.c
      FROM cteFinal c
      ORDER BY c.x, c.y, c.z

    This gives me the answer (scrolled down to show a few cases).

    2018-07-02 15_12_18-SQLQuery5.sql - (local)_SQL2016.sandbox (vstsbuild (54))_ - Microsoft SQL Server

    On my machine, this takes about 4s to run. Not bad, and probably not optimal. The query plan is a mess, but this is for fun in a quick run, so let’s leave this  for now.

    2018-07-02 15_14_08-SQLQuery5.sql - (local)_SQL2016.sandbox (vstsbuild (54))_ - Microsoft SQL Server

    There are likely better solutions. I’m not an optimization guy out of the box. I get things done first, then I’ll go back and evaluate some time to tune things. In this case, adding in 3 more values (to 300) takes 1:58s. Going to 500 rows caused an SSMS out of memory error.

    If I send the results to a temp table, things are better:

    • 100x100x100 (1,000,000 rows) – 00:01
    • 300x300x300 (27,000,000 rows) – 00:06
    • 500 x 500 x 500 (125,000,000 rows)  – 00:29
    • 1000x1000x1000 (1,000,000,000 rows) – 6:48

    Note, don’t send results to the client if you don’t need to.