Use Access SQL to do a grouped ranking
Asked Answered
M

6

11

How do I rank salespeople by # customers grouped by department (with ties included)?

For example, given this table, I want to create the Rank column on the right. How should I do this in Access?

SalesPerson Dept #Customers Rank
Bill        DeptA     20    1
Ted         DeptA     30    2
Jane        DeptA     40    3
Bill        DeptB     50    1
Mary        DeptB     60    2

I already know how to do a simple ranking with this SQL code. But I don't know how to rework this to accept grouping.

Select Count(*) from [Tbl] Where [#Customers] <  [Tblx]![#Customers] )+1

Also, there's plenty of answers for this using SQL Server's Rank() function, but I need to do this in Access. Suggestions, please?

Malvoisie answered 16/12, 2010 at 16:38 Comment(0)
T
16
SELECT *, (select count(*) from tbl as tbl2 where
tbl.customers > tbl2.customers and tbl.dept = tbl2.dept) + 1 as rank from tbl

Just add the dept field to the subquery...

Tupungato answered 16/12, 2010 at 16:55 Comment(1)
Does it order them properly, though? Interesting - didn't know Access could do this. What version, please? Also, unless you update a field in the database, you're going to be running this more often than you might wish.Reiner
D
3

Great solution with subquery! Except for huge recordsets, the subquery solution gets very slow. Its better(quicker) to use a Self JOIN, look at the folowing solution: self join

SELECT tbl1.SalesPerson , count(*) AS Rank 
FROM tbl AS tbl1 INNER JOIN tbl AS tbl2 ON tbl1.DEPT = tbl2.DEPT 
    AND tbl1.#Customers < tbl2.#Customers 
GROUP BY tbl1.SalesPerson 
Doggoned answered 26/7, 2013 at 9:55 Comment(0)
W
2

I know this is an old thread. But since I spent a great deal of time on a very similar problem and was greatly helped by the former answers given here, I would like to share what I have found to be a MUCH faster way. (Beware, it is more complicated.)

First make another table called "Individualizer". This will have one field containing a list of numbers 1 through the-highest-rank-that-you-need.

Next create a VBA module and paste this into it:

'Global Declarations Section.
Option Explicit
Global Cntr

'*************************************************************
' Function:  Qcntr()
'
' Purpose: This function will increment and return a dynamic
' counter. This function should be called from a query.
'*************************************************************

Function QCntr(x) As Long
   Cntr = Cntr + 1
   QCntr = Cntr
End Function

'**************************************************************
' Function:  SetToZero()
'
' Purpose: This function will reset the global Cntr to 0. This
' function should be called each time before running a query
' containing the Qcntr() function.
'**************************************************************

Function SetToZero()
   Cntr = 0
End Function

Save it as Module1.

Next, create Query1 like this:

SELECT Table1.Dept, Count(Table1.Salesperson) AS CountOfSalesperson
FROM Table1
GROUP BY Table1.Dept;

Create a MakeTable query called Query2 like this:

SELECT SetToZero() AS Expr1, QCntr([ID]) AS Rank, Query1.Dept, 
Query1.CountOfSalesperson, Individualizer.ID 
INTO Qtable1
FROM Query1 
INNER JOIN Individualizer 
   ON Query1.CountOfSalesperson >= Individualizer.ID;

Create another MakeTable query called Query3 like this:

SELECT SetToZero() AS Expr1, QCntr([Identifier]) AS Rank, 
[Salesperson] & [Dept] & [#Customers] AS Identifier, Table1.Salesperson, 
Table1.Dept, Table1.[#Customers] 
INTO Qtable2
FROM Table1;

If you have another field already that uniquely identifies every row you wouldn't need to create an Identifier field.

Run Query2 and Query3 to create the tables. Create a fourth query called Query4 like this:

SELECT Qtable2.Salesperson, Qtable2.Dept, Qtable2.[#Customers], Qtable1.ID AS Rank
FROM Qtable1 
INNER JOIN Qtable2 ON Qtable1.Rank = Qtable2.Rank;

Query4 returns the result you are looking for.

Practically, you would want to write a VBA function to run Query2 and Query3 and then call that function from a button placed in a convenient location.

Now I know this sounds ridiculously complicated for the example you gave. But in real life, I am sure your table is more complicated than this. Hopefully my examples can be applied to your actual situation. In my database with over 12,000 records this method is by FAR the fastest (as in: 6 seconds with 12,000 records compared to over 1 minute with 262 records ranked with the subquery method).

The real secret for me was the MakeTable query because this ranking method is useless unless you immediately output the results to a table. But, this does limit the situations that it can be applied to.

P.S. I forgot to mention that in my database I was not pulling results directly from a table. The records had already gone through a string of queries and multiple calculations before they needed to be ranked. This probably contributed greatly to the huge difference in speed between the two methods in my situation. If you are pulling records directly from a table, you might not notice nearly as big an improvement.

Wellfound answered 8/10, 2014 at 21:56 Comment(2)
Thanks for contributing. For the record, what you have called "Individualizer" is more commonly known as a Numbers table.Pretorius
Actually, the task at the time wasn't that much different from my example, only repeated several times over. Imagine looking at a year's worth of sales, then ranking each salesman by total sales grouped by month and product. The ranking method was the biggest bottleneck of the entire process but the small size of the database (pretty close to yours if I recall) kept it from being a showstopper. If I ever have to do this again in Access, I'll keep your suggestion in mind.Malvoisie
H
0

To add to this and any other related Access Ranking or Rank Tie Breaker how-tos for other versions of Access, ranking should not be performed on crosstab queries if your FROM clause happens to NOT contain a table but a query that is either a crosstab query or a query that contains within it elsewhere a crosstab query.

The code referenced above where a SELECT statement within a SELECT statment is used (sub query),

 "SELECT *, (select count(*) from tbl as tbl2 where tbl.customers > tbl2.customers and tbl.dept = tbl2.dept) + 1 as rank from tbl"

will not work and will always fail expressing a error on portion of the code where "tbl.customers > tbl2.customers" cannot be found.

In my situation on a past project, I was referencing a query instead of a table and within that query I had referenced a crosstab query thus failing and producing an error. I was able to resolve this by creating a table from the crosstab query first, and when I referenced the newly created table in the FROM clause, it started working for me.

So in final, normally you can reference a query or table in the FROM clause of the SELECT statement as what was shared previously above to do ranking, but be carefull as to if you are referencing a query instead of a table, that query must Not be a crosstab query or reference another query that is a crosstab query.

Hope this helps anyone else that may have had problems looking for a possible reason if you happen to reference the statements above and you are not referencing a table in your FROM clause within your own project. Also, performing subqueries on aliases with crosstab queries in Access probably isn't good idea or best practice either so stray away from that if/when possible.

If you found this useful, and wish that Access would allow the use of a scrolling mouse in a passthru query editor, give me a like please.

Halfmoon answered 16/12, 2010 at 16:39 Comment(0)
R
0

You need to do some math. I typically take advantage of the combination of a counter field and an "offset" field. You're aiming for a table which looks like this (#Customers isn't necessary, but will give you a visual that you're doing it properly):

SalesPerson Dept #Customers Ctr Offset
Bill        DeptA     20    1   1
Ted         DeptA     30    2   1
Jane        DeptA     40    3   1
Bill        DeptB     50    4   4
Mary        DeptB     60    5   4

So, to give rank, you'd do [Ctr]-[Offset]+1 AS Rank

  1. build a table with SalesPerson, Dept, Ctr, and Offset
  2. insert into that table, ordered by Dept and #Customers (so that they're all sorted properly)
  3. Update Offset to be the MIN(Ctr), grouping on Dept
  4. Perform your math calculation to determine Rank
  5. Clear out the table so you're ready to use it again next time.
Reiner answered 16/12, 2010 at 16:52 Comment(1)
I don't know if it would work or not, but pjabbott's solution is simpler to work with and ready to go.Malvoisie
M
0

I normally pick tips and ideas from here and sometimes end up building amazing things from it!

Today, (well let’s say for the past one week), I have been tinkering with Ranking of data in Access and to the best of my ability, I did not anticipate what I was going to do something so complex as to take me a week to figure it out! I picked titbits from two main sites:

  1. https://usefulgyaan.wordpress.com/2013/04/23/ranking-in-ms-access/ (seen that clever ‘>=’ part, and the self joins? Amazing… it helped me to build my solution from just one query, as opposed to the complex method suggested above by asonoftheMighty (not discrediting you… just didn’t want to try it for now; may be when I get to large data I might want to try that as well…)
  2. Right here, from Paul Abott above ( ‘and tbl.dept = tbl2.dept’)… I was lost after ranking because I was placing AND YearID = 1, etc, then the ranking would end up happening only for sub-sets, you guessed right, when YearID = 1! But I had a lot of different scenarios…

Well, I gave that story partly to thank the contributors mentioned, because what I did is to me one of the most complex of the ranking that I think can help you in almost any situation, and since I benefited from others, I would like to share here what I hope may benefit others as well.

Forgive me that I am not able to post my table structures here, it is a lot of related tables. I will only post the query, so if you need to you may develop your tables to end up with that kind of query. But here is my scenario:

You have students in a school. They go through class 1 to 4, can either be in stream A or B, or none when the class is too small. They each take 4 exams (this part is not important now), so you get the total score for my case. That’s it. Huh??

Ok. Lets rank them this way:

We want to know the ranking of

• all students who ever passed through this school (best ever student)

• all students in a particular academic year (student of the year)

• students of a particular class (but remember a student will have passed through all classes, so basically his/her rank in each of those classes for the different years) this is the usual ranking that appears in report cards

• students in their streams (above comment applies)

• I would also like to know the population against which we ranked this student in each category

… all in one table/query. Now you get the point?

(I normally like to do as much of my 'programming' in the database/queries to give me visuals and to reduce the amount of code I will later have to right. I actually won't use this query in my application :), but it let's me know where and how to send my parameters to the query it came from, and what results to expect in my rdlc)

Don't you worry, here it is:

SELECT Sc.StudentID, Sc.StudentName, Sc.Mark, 
(SELECT COUNT(Sch.Mark) FROM [StudentScoreRankTermQ] AS Sch WHERE (Sch.Mark >= Sc.Mark)) AS SchoolRank, 
(SELECT Count(s.StudentID) FROM StudentScoreRankTermQ AS s) As SchoolTotal, 
(SELECT COUNT(Yr.Mark) FROM [StudentScoreRankTermQ] AS Yr WHERE (Yr.Mark >= Sc.Mark) AND (Yr.YearID = Sc.YearID) ) AS YearRank, 
(SELECT COUNT(StudentID) FROM StudentScoreRankTermQ AS Yt WHERE (Yt.YearID = Sc.YearID) ) AS YearTotal, 
(SELECT COUNT(Cl.Mark) FROM [StudentScoreRankTermQ] AS Cl WHERE (Cl.Mark >= Sc.Mark) AND (Cl.YearID = Sc.YearID) AND (Cl.TermID = Sc.TermID)  AND (Cl.ClassID=Sc.ClassID)) AS ClassRank, 
(SELECT COUNT(StudentID) FROM StudentScoreRankTermQ AS C WHERE (C.YearID = Sc.YearID) AND (C.TermID = Sc.TermID) AND (C.ClassID = Sc.ClassID) ) AS ClassTotal, 
(SELECT COUNT(Str.Mark) FROM [StudentScoreRankTermQ] AS Str WHERE (Str.Mark >= Sc.Mark)  AND (Str.YearID = Sc.YearID) AND (Str.TermID = Sc.TermID)  AND (Str.ClassID=Sc.ClassID) AND (Str.StreamID = Sc.StreamID) ) AS StreamRank, 
(SELECT COUNT(StudentID) FROM StudentScoreRankTermQ AS St WHERE (St.YearID = Sc.YearID) AND (St.TermID = Sc.TermID)  AND (St.ClassID = Sc.ClassID) AND (St.StreamID = Sc.StreamID) ) AS StreamTotal, 
Sc.CalendarYear, Sc.Term, Sc.ClassNo, Sc.Stream, Sc.StreamID, Sc.YearID, Sc.TermID, Sc.ClassID
FROM StudentScoreRankTermQ AS Sc
ORDER BY Sc.Mark DESC;

You should get something like this:

+-----------+-------------+------+------------+-------------+----------+-----------+-----------+------------+------------+-------------+------+------+-------+--------+
| StudentID | StudentName | Mark | SchoolRank | SchoolTotal | YearRank | YearTotal | ClassRank | ClassTotal | StreamRank | StreamTotal | Year | Term | Class | Stream |
+-----------+-------------+------+------------+-------------+----------+-----------+-----------+------------+------------+-------------+------+------+-------+--------+
| 1         | Jane        | 200  | 1          | 20          | 2        | 12        | 1         | 9          | 1          | 5           | 2017 | I    | 2     | A      |
| 2         | Tom         | 199  | 2          | 20          | 1        | 12        | 3         | 9          | 1          | 4           | 2016 | I    | 1     | B      |
+-----------+-------------+------+------------+-------------+----------+-----------+-----------+------------+------------+-------------+------+------+-------+--------+

Use the separators | to reconstruct the result table

Just an idea about the tables, each student will be related to a class. Each class relates to years. Each stream relates to a class. Each term relates to a year. Each exam relates to a term and student and a class and a year; a student can be in class 1A in 2016 and moves on to class 2b in 2017, etc…

Let me also add that this a beta result, I have not tested it well enough and I do not yet have an opportunity to create a lot of data to see the performance. My first glance at it told me that it is good. So if you find reasons or alerts you want to point my way, please do so in comments so I may keep learning!

Morita answered 28/3, 2017 at 19:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.