Tag: windowfunctions

  • Counting Groups with Window Functions: #SQLNewBlogger

    I looked at row_number() in a previous post. Now I want to build on that and do some counting of rows with COUNT() and the OVER clause. I’ll show how this differs a bit from a normal aggregate.

    Another post for me that is simple and hopefully serves as an example for people trying to get blogging as #SQLNewBloggers. This is also part of a series on Window Functions.

    The Raw Data

    Let’s use AdventureWorks as a sample database. In the Sales.SalesOrderHeader table there are lots of customer orders, each with a Customer ID. I’ll limit my results to these customers, which has a spread of orders:

    WHERE soh.CustomerID IN (11011, 11015, 11019, 11012)

    We’ll use this WHERE clause to help us decide

    Counting Orders

    If I wanted to count the number of orders each customer hasplaced, I can use this code:

    SELECT
       soh.CustomerID
    , COUNT (*) AS OrderCount
    FROM Sales.SalesOrderHeader AS soh
    WHERE
       soh.CustomerID IN ( 11011, 11015, 11019, 11012 )
    GROUP BY soh.CustomerID;

    I see these results:

    2024-10_0175

    We can see that each of these customers has different numbers of orders.  If I want a simple summary, this is the best code. However, what if I want to add other columns?

    Supposed I want to add not only the customer ID, but also the shipdate. I’m wondering when orders were shipped. If I use the code above, I’d need to add this in the column list, and the group by. If I do this, I get this code:

    SELECT
       soh.CustomerID
    , soh.ShipDate
    , COUNT (*) AS OrderCount
    FROM Sales.SalesOrderHeader AS soh
    WHERE
       soh.CustomerID IN ( 11011, 11015, 11019, 11012 )
    GROUP BY
       soh.CustomerID
    , soh.ShipDate;

    And this result:

    2024-10_0176

    The results are cut off, but they repeat with a 1 for each row. The front end can sum these, but it’s easy to make a mistake here. Especially if there are filters.

    With an OVER() clause, my aggregate changes a bit. I would use this code, where I am paritioning, or essentially grouping, the data by the customer ID. I don’t have a group by, so I don’t need to add other fields in two places. Here is the code:

    SELECT
       soh.CustomerID
    , soh.ShipDate
    , COUNT (*) OVER (PARTITION BY soh.CustomerID) AS OrderCount
    FROM Sales.SalesOrderHeader AS soh
    WHERE
       soh.CustomerID IN ( 11011, 11015, 11019, 11012 );

    And the results.

    2024-10_0177

    I have the count repeated, but if I am showing this in the front end, I can hide that column in a table, but I can also access the total count from any row.

    Differences with Group By

    Let’s say I decide to add something else, like the account number and the SalesOrderNumber. If I do this in a GROUP BY, I need to add this to the column list and the group by, as shown here:

    SELECT
       soh.CustomerID
    , soh.AccountNumber
    , soh.SalesOrderNumber
    , soh.ShipDate
    , COUNT (*) AS OrderCount
    FROM Sales.SalesOrderHeader AS soh
    WHERE
       soh.CustomerID IN ( 11011, 11015, 11019, 11012 )
    GROUP BY
       soh.CustomerID
    , soh.ShipDate
    , soh.AccountNumber
    , soh.SalesOrderNumber;
    GO

    The change the Window function is as shown:

    SELECT
       soh.CustomerID
    , soh.AccountNumber
    , soh.SalesOrderNumber
    , soh.ShipDate
    , COUNT (*) OVER (PARTITION BY soh.CustomerID) AS OrderCount
    FROM Sales.SalesOrderHeader AS soh
    WHERE
       soh.CustomerID IN ( 11011, 11015, 11019, 11012 );

    Maintenance is easy, and what’s more, I don’t have to worry about ordering my group by in different ways. If I do care about grouping, then I can alter the partitioning, but often I just want to add other columns without making the code more complex in a second place.

    The results for the group by have the fields, but all 1s in the count again.

    2024-10_0179

    The window function has the counts for each customer.

    2024-10_0178

    Small things, but those points of maintenance can be annoying and they can cause problems. For complex data, the other fields might not group well, or the order of grouping might change what is shown.

    There are more things to be concerned about here, but one of the big things might be a running total, where I count the orders over time. I’d like results like this:

    2024-10_0180

    My window function code is:

    SELECT
       soh.CustomerID
    , soh.AccountNumber
    , soh.SalesOrderNumber
    , soh.ShipDate
    , COUNT (*) OVER (PARTITION BY soh.CustomerID ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS OrderCount
    FROM Sales.SalesOrderHeader AS soh
    WHERE
       soh.CustomerID IN ( 11011, 11015, 11019, 11012 );

    The regular code with group by is this:

    select soh1.customerid, soh1.AccountNumber, soh1.SalesOrderNumber, soh1.ShipDate
    , count(*) 'running_total'
    from sales.salesorderheader soh1
    inner join sales.salesorderheader soh2 on soh2.salesorderid <= soh1.salesorderid
                                        and soh2.customerid = soh1.customerid
    group by soh1.customerid, soh1.AccountNumber, soh1.SalesOrderNumber, soh1.ShipDate
    order by soh1.customerid, soh1.ShipDate

    Not bad, but the big difference is in resources used. The statistics IO below shows the difference. Can you guess which is which?

    2024-10_0183

    Hint, the one using less reads is the OVER() code.

    SQL New Blogger

    Setting up a good scenario is a little tricky, and that took time. I messed with a few data sets that helped explain this to me, and you, in a way that made sense. I have a few other scenarios, which I will write about because an OVER() isn’t magic and it might not be the right choice.

    Those will be other blogs.

    This is a good way to showcase that you understand part of how this works. There’s a whole series to be written on different aspects of how the OVER() clause works. Spending 15-30 minutes each time you experiment helps showcase your knowledge and learning to employers.

    That’s what employers need: experimenting and learning.

  • Adding Row Numbers to a Query: #SQLNewBlogger

    I realized that I hadn’t done much blogging on Window functions in T-SQL, and I’ve done a few presentations, so I decided to round out my blog a bit. This post will start with the ROW_NUMBER() function as a gentle intro to window functions.

    Another post for me that is simple and hopefully serves as an example for people trying to get blogging as #SQLNewBloggers. This is also part of a series on Window Functions.

    A Basic Set of Data

    I’m going to use some fun data for me. I’ve been tracking my travels, since I’m on the road a lot. I’m a data person and part of tracking is trying to ensure I’m not doing too much. Just looking at the data helps me keep perspective and sometimes cancel (or decline) a trip.

    In any case, you don’t care, but I essentially have this data in a table. As you can see, I have the date of travel, the city, area, etc. I also have a few flags as to whether I was traveling that day, if I spent a night away from home, and how far I was.

    2024-09_0199

    I have a travelID in here, which is a sequence, but what if I wanted to the trips I took in August 2024. I’d want a distance > 0 (not at home) and filters by dates. Adding that to my query, I’d run this:

    SELECT
       TravelID
    , TravelDate
    , TravelCity
    , Area
    , Province
    FROM travel
    WHERE
       TravelDate     > '2024/07/31'
       AND TravelDate < '2024/09/01'
       AND Distance > 0
    ORDER BY TravelDate;

    This gives me these results:

    2024-09_0202

    There are 9 rows in here, but they have a weird ID number, plus these are different trips. I can just add a row_number to this data, and I’d see this result. Ignore the OVER and track I used, but you can see an incrementing number added to each row. The second column in the result set matches with the number added by SSMS on the side.

    2024-09_0204

    What if I wanted to see the separate trips with some row number for the day in each city?

    That’s where a row_number() can help.

    Creating a Window

    The window comes from the OVER() clause, which is added to a number of functions, including Row_number(). The OVER() clause lets me set a window or rows on which the function works. I can set a partition and an order.

    The partition is a column where we are essentially grouping data. For me, this would be the city. When I change city, I want to reset the number. Looking at the data above, I’d expect to see 1, 2 for the first 2 days in Minneapolis, then a 1 for a day in Fort Collins, and another 1 for day in Aurora

    The ordering is what order is the data in the partition. In this case, I want to have the data in the window ordered by traveldate, so I’ll use that. I now have this code:

    SELECT
       TravelID
    , ROW_NUMBER () OVER (PARTITION BY TravelCity
                           ORDER BY TravelDate)
    , TravelDate
    , TravelCity
    , Area
    , Province
    FROM travel
    WHERE
       TravelDate     > ‘2024/07/31’
       AND TravelDate < ‘2024/09/01’
       AND Distance > 0
    ORDER BY TravelDate;

    And I get these results, where I can see that I essentially had 4 trips (all with number 1s), and these were the trips:

    • Minn – 2 days
    • Fort Collins – 1 day
    • Aurora – 1 day
    • New York City – 5 days

    2024-09_0205

    This shows how row_number() gives me a sequence based on the partition. The select null part in the earlier query just ignores the order by, which is required for row_number(). With no order, how do we know what sequence?

    Let’s change this slightly. What if I partition by Province? Then I see this:

    2024-09_0206

    We put the data in date order, and ran through each province. In this case, my two 1 day trips around Colorado are bucketed (partitioned) together and I see one less trip. If I did this by country, I’d see all of this as one sequential list, since all my trips were in one country.

    However, if I did countries for June, I’d see this, with the raw data on the left and the row_number() on the right. There’s a weird sequence in here; can you see it?

    2024-09_0208

    The weirdness is that my trips to England were broken up by a trip to Italy. So while my sequence looks good for the first part of the trip to English for 6 days, when I returned a week later, we get numbers 7, 8, 9. That’s because the data is grouped first by country, and the sequence added. The ordering of the sequence is by date, so the later days (June 13,-15) are marked with the higher sequence that continues on.

    Hopefully this gives you a basic look at row_number() and some of the possibilities. I’ll examine it further in another post, along with various other window functions.

    SQL New Blogger

    Complex coding and finding weird situations are things employers want you to be able to do. If you work on algorithms or you’re learning new language elements, blog about them. That will impress people.

    This post took me about 30 minutes, plus about 15 minutes or playing with code to set things up. You could likely do this in an hour if you’ve never blogged, though let someone proof things for you.

  • T-SQL Tuesday #168–Using Window Functions

    tsqltuesdayI am the host for T-SQL Tuesday this month, and I hope that a lot of people like the topic. This idea actually came to me earlier this year when I happened to see someone ask about a T-SQL problem and get an answer using a Window function. This person mentioned they hadn’t used the window function before, and I wondered how many people haven’t even tried using the OVER() clause with a window function.

    I also saved the idea of window functions just in case I didn’t have a host, and I realized a few months ago November was blank. So I created an invitation for technical solutions using window functions, hopefully mature solutions you’ve use many times.

    If you want to host, contact me, and send me your blog link and I’ll get you scheduled. FYI, I’m looking for people in the second half of 2024, so it’s not an immediate need.

    Cleaning Up Window Functions

    I don’t write a ton of code, and I don’t have any really cool solutions, but I did want to highlight one thing from SQL Server 2022: the WINDOW clause.

    In the past we’ve often had code like this:

    WITH    HRCTE
               AS ( SELECT   hrorder = ROW_NUMBER() OVER ( PARTITION BY p.franchName ORDER BY HR DESC )
                           , p.nameFirst
                           , p.nameLast
                           , p.franchName
                           , p.HR
                    FROM     dbo.Players p
                  )
         SELECT  hrdenserank = DENSE_RANK() OVER ( PARTITION BY HRCTE.franchName ORDER BY HR DESC )
               , hrrank = RANK() OVER ( PARTITION BY HRCTE.franchName ORDER BY HR DESC )
               , playercound = COUNT(p.nameLast) OVER ( PARTITION BY HRCTE.franchName ORDER BY HR DESC )
               , hrsum = sum(p.HR) OVER ( PARTITION BY HRCTE.franchName ORDER BY HR DESC )
         FROM    HRCTE
         WHERE   HRCTE.hrorder <= 5;

    That’s not bad, but I’ve written a bunch of repeating code in the OVER() clauses. In SQL Server 2022, I can do something more like this (just the outer query):

        SELECT  hrdenserank = DENSE_RANK() OVER frname
               , hrrank = RANK() OVER frname
               , playercound = COUNT(p.nameLast) OVER frname
               , hrsum = sum(p.HR) OVER frname
         FROM    HRCTE
         WHERE   HRCTE.hrorder <= 5
         WINDOW frname AS ( PARTITION BY HRCTE.franchName ORDER BY HR DESC );

    That, to me, is a cool maturity of the Windowing function capability in T-SQL. I can alias a window and reuse it in my code. This also makes I can make a few different windows and easily see which one is used with which aggregate.

  • T-SQL Tuesday #168 – Mature Window Functions

    tsqltuesdayIt’s time for T-SQL Tuesday and I’m hosting this month. I usually do one a year, just because I can and being responsible for a month keeps me engaged in the party.

    This month my invitation is on Window functions and is described below.

    If you’d like to host T-SQL Tuesday, let me know. I have lots of openings in 2024 and I’m looking for someone with a blog, some creativity, and an idea for a technical topic that you’d like to see other people write about.

    Mature Window Functions

    We’ve had window functions in SQL Server for a decade now, since SQL Server 2012.

    This month I’m asking you to write on how window functions have made your life easier. A few ideas for you:

    • What problems have you solved with a window function? Bonus points for lead/lag/first_value/last_value
    • Have you used the SQL Server 2022 enhancements in any queries?
    • How has performance improved for you with a window function
    • Draw a picture of a window with a spatial function – more extra points

    Give us some specifics, with real world problems. Obfuscate the data, at least if you have my name in your dev system, but help others understand how they might solve a complex aggregate using a Window function. The more specific examples, the more others might get help from one of the posts.

    The Rules

    Not many rules, but a few of them.

    • Post between 00:00:00 and 23:59:59 on 2023-11-14
    • Include the logo above in your post
    • Link that logo to this post
    • Leave me a trackback or comment on this post (double check if you have this automated)
    • Post you URL on Twitter, LinkedIn, etc. with the hashtag #tsql2sday
    • Have fun