Query Exercise Answer: What Makes SELECT TOP 1 or SELECT MAX Different?
This Query Exercise was very different: I didn’t ask you to solve a particular problem. I pointed out that I’ve heard advice that SELECT MAX is faster than SELECT TOP 1, and that’s not quite true. I asked you to find factors that would cause these two queries to get different execution plans:
Transact-SQL
|
1 2 3 4 5 6 |
SELECT TOP 1 LastAccessDate FROM dbo.Users ORDER BY LastAccessDate DESC; SELECT MAX(LastAccessDate) FROM dbo.Users; |
In the exercise post, I showed that with a nonclustered rowstore index like this, whose leading column is not LastAccessDate:
Transact-SQL
|
1 2 |
CREATE INDEX Location_LastAccessDate ON dbo.Users(Location, LastAccessDate); |
That would give them different execution plans, leading to TOP 1 being faster because it went parallel, while MAX stayed single-threaded and ran longer.
I tell you what, when I was writing that blog post, the hardest thing by far was not to give away too many answers. I worded that sentence above really, really carefully because right there in that sentence alone, there’s a factor that changes the answers. We could just change something about the environment so that the MAX query goes multi-threaded too (or the TOP 1 query goes single-threaded!)
Cost Threshold for Parallelism
Continuing with the above example, let’s change Cost Threshold for Parallelism from the common 50 down to, say, 25:
Transact-SQL
|
1 2 3 4 |
EXEC sys.sp_configure N'cost threshold for parallelism', N'25' GO RECONFIGURE GO |
And then run our two queries again. Now, both of them go parallel, as shown in the actual execution plans:
Now both queries qualify for parallelism, and the MAX is faster. On the flip side, if we raise CTFP to a much higher number, like 500, the new execution plans are… wait… hang on a second here…
How does the TOP 1 query still have parallelism? Let’s hover our mouse over the SELECT and examine its Estimated Subtree Cost:
How’s that possible? If my Cost Threshold for Parallelism is 500, how can a query with a cost of 499.193 go parallel? Well, there’s a trick: we’re looking at the estimated cost of the parallel query, not the serial one. To see the cost of the serial query, add an OPTION (MAXDOP 1) hint to it:
Transact-SQL
|
1 2 3 |
SELECT TOP 1 LastAccessDate FROM dbo.Users ORDER BY LastAccessDate DESC OPTION (MAXDOP 1); |
And we can see that the serial cost is a whopping 977 query bucks:
So that’s why the query goes multi-threaded. Parallelism: it’s a hell of a drug. Alright, let’s reset the playing field before we try other factors:
Transact-SQL
|
1 2 3 4 5 6 |
EXEC sys.sp_configure N'cost threshold for parallelism', N'50' GO RECONFIGURE GO DropIndexes; GO |
Columnstore Indexes
They’re great for aggregate queries like MAX, so let’s slap one on and see how it affects performance:
Transact-SQL
|
1 2 3 4 5 6 7 8 9 10 |
CREATE NONCLUSTERED COLUMNSTORE INDEX LastAccessDate ON dbo.Users(LastAccessDate); GO SELECT TOP 1 LastAccessDate FROM dbo.Users ORDER BY LastAccessDate DESC; SELECT MAX(LastAccessDate) FROM dbo.Users; |
The actual query plans look similar in the sense that they have the same operator, but the devil’s in the details on this one:
The TOP 1 uses a sort, which sounds bad, because it sounds like it would sort all 8,917,507 rows – especially with that monster arrow coming out of the columnstore index scan operator. However, that arrow doesn’t mean jack, as we explain in the Fundamentals of Columnstore training class.
The bottom line is that the TOP 1 uses 93ms of CPU time and runs in 205ms. The MAX uses 16ms of CPU time and runs in 97ms. The MAX wins both ways here, but it’s not a dramatic win – most folks aren’t going to complain too much about the difference between these two plans.
However, there’s a catch to this comparison: my database happens to be in SQL Server 2016 or newer compatibility mode for this one. Watch what happens when we introduce yet another variable into this experiment…
Compatibility Level
I’ve still got the columnstore index in place, but let’s drop back to 2014 compatibility level:
Transact-SQL
|
1 |
ALTER DATABASE [StackOverflow] SET COMPATIBILITY_LEVEL = 120 |
And then check our new actual query plans:
Sometimes, smaller plans are better. However, not in this case: the wide TOP 1 plan finishes in just 351ms, but the seemingly-simple MAX plan takes a whopping 2.2 seconds to run!
This comparison is also a great reminder that the percentage query costs on plans are absolutely useless and meaningless! I catch people saying, “The bottom query looks better because it’s only 4% of the cost,” but that’s just garbage:
It drives me crazy that Microsoft even includes this junk in query plans in the year 2024. They’re doing a disservice with that number.
What We Learned in This Exercise
I’ve only covered a few variables in the equation that make TOP 1 and MAX perform differently. For more, check out the comments on the Query Exercise post.
Database servers have a butterfly effect: even the slightest change, seemingly unrelated to anything else, can affect query performance all over the place. I’m not saying you have to test everything before you change anything at all – the real world is just too busy and complicated to do that.
That butterfly effect should teach you 3 things:
- Just because 2 simple queries produce the same result doesn’t mean they get the same query plan.
- Never say “This T-SQL syntax is faster than that other T-SQL syntax,” because there are tons of butterfly effect variables.
- Before you give advice on how to write a query, get familiar with the target environment first.
Related

Hi! I’m Brent Ozar.
I make Microsoft SQL Server go faster. I love teaching, travel, cars, and laughing. I’m based out of Las Vegas. He/him. I teach SQL Server training classes, or if you haven’t got time for the pain, I’m available for consulting too.
Get Free SQL Stuff
"*" indicates required fields








4 Comments. Leave new
<$0.02>
I just wonder, why SQL Server does really read the whole pages / rows and either sorts or aggregates all rows when there is a Columnstore index.
Theoretical it could just read the metadata of each rowgroup (up to ~1 Mio rows each), where it finds the highest / lowest value for the column and sorts/aggregates just the values of the few rowgroups, which shold take almost no time.
Would at least work for MIN/MAX and TOP 1 (but not for TOP 2, except the min value is equal to the max value or – on an integer / date column – differs by just one)
PS: 0,02 EUR
Great question. Whenever I think, “Why didn’t the developers add more code to do X?”, I think about how my own to-do list looks.
I’m constantly struggling with a massive backlog of things I wanna do, and I have to triage that to focus on the things that will help the most people, as quickly as possible, with the least effort on my part. I’m guessing the Microsoft developers have that same challenge, as do most of us.
[…] your comments. Check out the solutions from other folks, and compare and contrast your work. Then, read my thoughts in the answer post. Have […]