7th Aug 2017 Updated: 16th Aug 2018 8 minutes read SQL Window Functions: Partitions and Ranking Functions Aldo Zelen Advanced SQL Window functions Table of Contents What Is PARTITION BY? Ranking Functions Rank SQL ROW NUMBER NTILE You’ve started your mastery of SQL window functions by learning RANK, NTILE, and other basic functions. In this article, we will explain how to use SQL partitions with ranking functions. Mastering SQL window functions (AKA analytical functions) is a bumpy road, but it helps to break the journey into logical stages that build on each other. In the previous Common SQL Functions article, you learned about the various rank functions, which are the most basic form of window functions. In this article, we will build on that knowledge, using the concept of partitions. This will expand the scope and complexity of the statements from the previous article. We will start by explaining the PARTITION BY clause, and then we will show how to use it with ranking functions. What Is PARTITION BY? Partitioning is optional part of commands containing SQL window functions. Most analytical functions, including RANGE, can be written with a PARTITION BY clause: <AnalyticFunction> ([<column-list>]) over ([<partition by>]) What does a PARTITION do? It defines a partition on which the window function does some work (e.g. executes). It is important to note the similarities between GROUP BY and PARTITION BY parts of the SQL statements. But be careful not to confuse the two. GROUP BY defines aggregation groups on the level of the SQL statement and PARTITION BY defines virtual PARTITIONS that are needed for analytical function output. This may seem confusing at first but after the examples I am sure that things will clear up. Note: SQL window functions do not change the number of rows you get with your select statement. This is one obvious difference between them and group by statement. Ranking Functions As stated in the previous article ranking analytical functions “rank” data based on the “ordering” you specify in the function specification. Now we expand this statement, so ranking analytical functions rank data based on the ordering you specify in the function specification inside the specified rank partition. To demonstrate various rank analytical functions with partition we will revisit our simple sales table. In this table we are tracking sales. The columns are: id: primary key of the sales salesman_id: id of the sales man how made the salesman sales_item: id of the sold item sales_num: number of items sold sales_price: price of the individual item datetime: date of the sale Rank SQL We can now redefine RANK as a SQL window function that returns a rank or a number based on the ordering of the rows by some condition inside a predefined partition. Let’s say we want to rank sales items for every salesman from most sold to least sold. Take into account the wording ‘for … salesman’. “For something” usually defines the partition keys that we need in our SQL statement. In all our simple examples we will look at sales form March (the third month). SELECT id, salesman_id, sales_item, sales_num, sales_price, rank() over (partition by salesman_id order by sales_num desc) as RANK FROM sales WHERE EXTRACT(MONTH FROM datetime) = 3 ORDER BY salesman_id,sales_num desc; The result of using such window functions in SQL is a table displayed in the following way: ID SALESMAN_ID SALES_ITEM SALES_NUM SALES_PRICE RANK 8 10 10 50 55.1 1 7 10 200 20 1.1 2 1 10 100 4 50.5 3 9 30 25 330 12.1 1 2 30 200 7 50 2 3 40 200 40 10.5 1 4 50 200 15 110 1 5 60 200 8 30.5 1 6 70 200 22 10.5 1 Thanks to including “rank() over (partition by...)” in our code, we find out that the salesmen with the id of 10 and 30 have more than one sale in the table currently. We have ordered their sales with the decreasing function so that the larger sales have a smaller rank. If we were to change the order to asc or just leave it empty, ORDER BY is mandatory, or larger sales would have a larger rank. SELECT id, salesman_id, sales_item, sales_num, sales_price, rank() over (partition by salesman_id order by sales_num ) as RANK FROM sales WHERE EXTRACT(MONTH FROM datetime) = 3 ORDER BY salesman_id,sales_num ; ID SALESMAN_ID SALES_ITEM SALES_NUM SALES_PRICE RANK 1 10 100 4 50.5 1 7 10 200 20 1.1 2 8 10 10 50 55.1 3 2 30 200 7 50 1 9 30 25 330 12.1 2 3 40 200 40 10.5 1 4 50 200 15 110 1 5 60 200 8 30.5 1 6 70 200 22 10.5 1 Let’s take a look at a more complex example of using “rank() over (partition by…)”. Let’s say that we need to see all the sales statistics of the best salesman of the month for the last year. So how do we define the best salesman? The best salesman is the salesman who had the “biggest” sales (sales amount times sales price) by dollar amount in the month. So if we look again at our code using one of the SQL window functions: SELECT * FROM (SELECT id, salesman_id, sales_item, sales_num, sales_price, rank() over (partition by extract(month from datetime) order by sales_num*sales_price ) as salesman_rank_list, extract(month from datetime) sales_month, sales_num*sales_price sales FROM sales ORDER BY sales_month,salesman_rank_list) ranked WHERE salesman_rank_list = 1; And filter in the outer WHERE clause the first ranked salesman we get our result: ID SALESMAN _ID SALES _ITEM SALES _NUM SALES _PRICE SALESMAN_ RANK_LIST SALES_ MONTH SALES 15 70 80 2 60.5 1 1 121 24 30 25 50 26.1 1 2 1305 7 10 200 20 1.1 1 3 22 17 10 40 40 5.1 1 4 204 18 30 25 30 2.1 1 5 63 13 50 600 215 10 1 6 2150 12 40 2 40 17.5 1 7 700 26 10 50 54 50.5 1 8 2727 14 60 50 8 302.5 1 9 2420 19 70 80 2 60.5 1 10 121 21 10 40 40 5.1 1 11 204 22 30 25 30 2.1 1 12 63 As you saw in this example rank() over (partition by..). can be used not only for reporting but for constructing more powerful (and complex) SQL statements. ROW NUMBER We have covered the specifics of ROW NUMBER analytical function in the previous article, now let’s look at how it handles the partition clause. Although it is not deterministic this function has it’s uses, one of the best known uses is the “elimination of duplicate rows”. So let’s say that, by some mistake, we insert the same double sales for the salesman 40: ID SALESMAN _ID SALES _ITEM SALES _NUM SALES _PRICE DATETIME R_FLAG 3 40 200 40 10.5 02-MAR-17 1 27 40 200 40 10.5 02-MAR-17 2 The ROW_NUM still flags every for, column R_FLAG, with a different number. We can use this information to delete duplicate rows. The query for deleting duplicate rows is : DELETE FROM sales WHERE rowid IN (SELECT rid FROM (SELECT rowid rid, id, salesman_id, sales_item, sales_num, sales_price, DATETIME, row_number() over (partition BY salesman_id,sales_item,sales_num,sales_price order by id) r_flag FROM sales WHERE EXTRACT(MONTH FROM DATETIME) = 3 AND SALESMAN_ID = 40 ) WHERE r_flag != 1 ); Note: In this query we are using the Oracle ROWID pseudocolumn. For each row in the database, the ROWID pseudocolumn returns the address of the row. Other databases have similar pseudocolumns (PostgreSQL CTID, MSSQL RID, etc.) NTILE Hungry for more SQL window functions? Let’s look how the final ranking analytical function handles partitions. Quick recap: NTILE function divides rows of data into equal groups and returns the rank of those groups. A simple and common use case would be if we specified the business value of particular sales volume. So we could divide the sales volume, defined with sales_num, into four groups by every product: High Medium-High Medium-Low Low We will take our example with the sales and expand it using NTILE function with the function argument of 4. With this we are dividing sales of each partition (one salesman is one partition) into 4 equal parts. Equal in number of rows. SELECT id, salesman_id, sales_item, sales_num, sales_price, case when NTILE = 1 THEN 'High' when NTILE = 2 THEN 'Middle-High' when NTILE = 3 THEN 'Middle-Low' when NTILE = 4 THEN 'Low' end sales_group, ntile FROM (SELECT id, salesman_id, sales_item, sales_num, sales_price, NTILE(4) over (partition by sales_item order by sales_num desc) as ntile FROM sales WHERE EXTRACT(MONTH FROM datetime) = 3) ORDER BY sales_item,sales_num desc; The result we generate is: ID SALESMAN _ID SALES _ITEM SALES _NUM SALES _PRICE SALES _GROUP NTILE 8 10 10 50 55.1 High 1 9 30 25 330 12.1 High 1 1 10 100 4 50.5 High 1 3 40 200 40 10.5 High 1 6 70 200 22 10.5 High 1 10 10 200 20 2.1 Middle-High 2 7 10 200 20 1.1 Middle-High 2 4 50 200 15 110 Middle-Low 3 5 60 200 8 30.5 Middle-Low 3 2 30 200 7 50 Low 4 As we see from the result set NTILE divided the partition” of sales_item ID 200 into four parts, with the last group being the smallest. This article is expanding your knowledge of SQL window functions to a new level of complexity as now you know how to use “rank() over (partition by…)” For practice, you could solve the complex examples in this article using only aggregation and self-joins – give it a try! Regarding SQL window functions this is not the end, there are more analytical functions and even SQL patterns to come in upcoming articles. In the meanwhile be sure to practice your new knowledge of partition on LearnSQL.com. Tags: Advanced SQL Window functions