How to Query JSON Data Quickly in SQL Server, Part 1: Pre-2025
Before SQL Server 2025, if you want to store JSON data in Microsoft SQL Server or Azure SQL DB, and you want fast queries, the easiest way is to:
- Store the data in an NVARCHAR(MAX) column (because the native JSON datatype didn’t arrive until SQL Server 2025)
- Add a computed column for the specific JSON keys we’ll want to query quickly
- Index those keys
- Query it using the JSON_VALUE function
To demo what I mean, I’m going to take the Users table from the Stack Overflow database, and I’m going to pretend that we’re only storing the Id column, and the rest of the columns are JSON, stored in a UserAttributes column. Let’s create a new Users_JSON table to simulate it:
Transact-SQL
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
DROP TABLE IF EXISTS dbo.Users_JSON; GO CREATE TABLE dbo.Users_JSON ( Id int NOT NULL PRIMARY KEY CLUSTERED, UserAttributes json ); GO /* Takes 2-3 minutes with the 2018-06 training database on 4 cores, 32GB RAM: */ INSERT INTO dbo.Users_JSON (Id, UserAttributes) SELECT Id, JSON_OBJECT( 'DisplayName': DisplayName, 'Reputation': Reputation, 'Age': Age, 'CreationDate': CreationDate, 'LastAccessDate': LastAccessDate, 'Location': Location, 'UpVotes': UpVotes, 'DownVotes': DownVotes, 'Views': Views, 'WebsiteUrl': WebsiteUrl, 'AboutMe': AboutMe, 'EmailHash': EmailHash, 'AccountId': AccountId ) FROM dbo.Users; GO |
Now we’ve got a table with just Ids and JSON data:

If we want to find the users with DisplayName = ‘Brent Ozar’, and we query with the JSON_VALUES function, SQL Server has to scan through that entire table, cracking open every JSON value, and finding it. That’s a lot of reading, and a lot of CPU work:
Transact-SQL
|
1 2 3 4 5 |
SET STATISTICS TIME, IO ON; SELECT * FROM dbo.Users_JSON WHERE JSON_VALUE(UserAttributes, '$.DisplayName') = N'Brent Ozar'; |
The query is slow and CPU-intensive, going parallel across multiple cores, maxing our server out for about 4 seconds:
This stuff works in the sense that it compiles and runs, but as you hit real-world data sizes (there are less than 10 million rows in that table), that doesn’t scale. We’re gonna need to run more than one query every 4 seconds.
Add a computed column and index it.
We do have to decide what columns we wanna index it, but take a deep breath and relax – I’m not saying you need to do any application code work at all. We’re just going to make some of the columns really fast. Take DisplayName:
Transact-SQL
|
1 2 3 4 |
ALTER TABLE dbo.Users_JSON ADD vDisplayName AS JSON_VALUE(UserAttributes, '$.DisplayName'); CREATE INDEX vDisplayName ON dbo.Users_JSON (vDisplayName); |
Now, run the exact same JSON_VALUE query without changing our app or our code:
Transact-SQL
|
1 2 3 |
SELECT * FROM dbo.Users_JSON WHERE JSON_VALUE(UserAttributes, '$.DisplayName') = N'Brent Ozar'; |
The query runs instantly, reading hardly any data and doing no CPU work:
SQL Server automatically recognizes what we’re trying to do in the query, realizes it’s got an indexed computed column ready to go, and uses that to deliver our query results. It’s like magic, and it’s worked this way since 2016 with JSON_VALUE. Works with other datatypes too, like numbers and dates.
It even works with LIKE queries:
Transact-SQL
|
1 2 3 |
SELECT * FROM dbo.Users_JSON WHERE JSON_VALUE(UserAttributes, '$.DisplayName') LIKE N'Brent%'; |
Producing a nice index seek, and even pretty good row estimations. Here, SQL Server estimates 3699 rows will be produced (and 1523 are):
If we do a non-sargable filter, like a leading % sign in our LIKE:
Transact-SQL
|
1 2 3 |
SELECT * FROM dbo.Users_JSON WHERE JSON_VALUE(UserAttributes, '$.DisplayName') LIKE N'%Jorriss%'; |
SQL Server still uses the index, just scanning it instead of seeking it, which is a good thing:
Scan SOUNDS bad, but it’s still less CPU work and logical reads than our un-indexed JSON scan. Back at the beginning of the post, we were looking at half a million reads and 14 seconds of CPU work to scan all the JSON, but with the index, even non-sargable stuff isn’t terrible:
Once you’ve got the computed column and index in place, you could change your queries to be normal T-SQL, like this:
Transact-SQL
|
1 2 3 |
SELECT * FROM dbo.Users_JSON WHERE vDisplayName = N'Brent Ozar'; |
They’ll be super-fast (just like the JSON_VALUE queries) – but I don’t recommend doing this. If you change your queries to point to the computed column, then I always have to have the computed column in place in the database. Leaving your queries as JSON_VALUE queries means we keep the flexibility of changing the database, plus we get fast index seeks. There’s no benefit to changing our queries to point to the new computed column – only heartache when our schema needs eventually change.
This indexed-computed-column technique has worked really well.
It’s a nice compromise that works well when the developers want the flexibility of changing which attributes they store, adding more attributes over time – but some attributes need really fast searches. I’ve been teaching it for years in this module of my Mastering Index Tuning class, where I also talk about the drawbacks and other solutions. (You can join in on these classes for a special price during my Black Friday sale this month!)
Developers are happy because they can change their UsersAttributes schema whenever they want without talking to the DBA. They can even decide to remove columns that used to be part of our core fast-query design – as long as they tell the DBA, and the DBA drops the index and computed column. The developers don’t have to change their queries – they just keep using JSON_VALUE. The results are fast when we agree that it’s one of the core columns, and still doable when they’re outside the core set of columns, just slower.
One drawback is that the NVARCHAR(MAX) datatype isn’t really JSON: SQL Server doesn’t validate the data for you. Another is that if you want multiple filters, you have to write them out individually. Another is that the more complex your JSON becomes, the more you have to pay attention to stuff like lax mode and strict mode. I’mma be honest, dear reader: like the lion, I don’t concern myself with such things, and I just point the developers to the documentation. I say look, if you want JSON queries to be millisecond-fast in SQL Server and Azure SQL DB, you gotta tell me the specific columns you’re going to query, in advance, and I’m gonna tell you how to write the query.
But the biggest drawback – by far – is that we have to define the core set of columns we wanna query quickly.
Developers want real flexibility: the ability to change their JSON schema at any time, and query any values out of it, quickly, without friction from the database side. That’s what Microsoft tried to deliver in SQL Server 2025, and I’ll cover that in the next post in the series. Careful what you ask for, though….
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






12 Comments. Leave new
Fantastic Brent, thank you! I regularly query and sometimes (shamefully) store JSON in SQL Server at least temporarily.
This is so great for ETL since I can store the raw JSON values in a staging table and then convert them via T-SQL and even filter them like you showed. Normally I’d use SSIS but this is great!
Love it, thanks!
A little confused here – before DROP TABLE IF EXISTS… don’t you need to declare something like:
CREATE TYPE dbo.json FROM nvarchar(max);
Trying to demo on my SQL 2022 (of course StackOverflow DB in 2022 compat level), as I expected I got error “Cannot find data type json.” – so I thought it was a typo.
[…] we did in yesterday’s post about how to get fast JSON queries pre-2025, we’ll create a Users_JSON table, but this time we’ll use the new native JSON datatype, […]
Brent this is yesterday 🙂
(Part 1)
Brent, great tip! It’s a solution to some of our performance issues. Thanks.
Why would you use json_value() in the where clause in stead of the computed column ?
Generally when we talk about index tuning, we’re talking about doing it so we don’t have to change the code.
And generally when the developers use JSON, they’re doing it for flexibility. This approach lets them use JSON storage and JSON coding – without having to take a dependency on a newly created computed column.
If you change the code to refer to the computed column by name, it takes longer, and you lose the flexibility of changing your definitions without getting the DBA involved.
If you want to validate the json, it is easy, put a check constraint with ISJSON function. Problem easily solved.
So that removes the work from the SQL Server, so SQL Server’s CPU cycles are no longer spent validating the JSON?
You always use some resource with a constraint, the “json” data type itself would be a constraint. If there was the json data type, then SQL server would spend some resource. Not sure I understand your point here. Any constraint requires some resource consumption.
Not on the SQL Server. You can do that validation on the app side instead, where CPU is effectively free, unlike SQL Server at $2,000-$7,000 USD per CPU core. Not the best use of money there.
Good morning.
Am I missing something or is this example against a JSON data type and the title is pre-2025 options for querying JSON data. I have a use case to learn this and was wondering if I am misunderstanding the topic.