
Every AL query object you’ve ever written has been reading data a specific way, and most developers have never had to think about it because there was never a choice to make. Queries in Business Central default to reading uncommitted data a dirty read, in database terms and until version 29, that was simply how a query behaved. There was no property to change it.
Records got this control years ago. Queries never did, until now.
What “Dirty Read” Actually Means, and Why It Was the Default
Business Central 2023 release wave 1 introduced the ReadIsolation method for the Record data type, letting developers control locking behavior when reading records directly. Queries were left out of that change entirely, and the reason is really about what a query is for. Queries are built for reporting and aggregation across potentially large datasets, and locking every row a query touches would make exactly the workloads queries are designed for perform badly.
So, queries defaulted to ReadUncommitted: read whatever data exists right now, regardless of whether another transaction has committed it yet. No locks are placed, which means nothing a query reads gets in the way of any other transaction modifying, deleting, or rolling back that same data while the query is running. It’s the fastest possible read, and it’s also, by strict definition, reading data that might not actually be real by the time you look at it — a value from a transaction that gets rolled back a moment later.
For most reporting scenarios, this tradeoff is genuinely fine. A dashboard showing approximately current figures rarely need the guarantee that every number reflects only fully committed transactions. But “genuinely fine for most cases” is different from “the only option,” and until v29, most cases were the only option AL queries had.
What Version 29 Actually Adds
Queries now support a ReadState property with four values, each mapping directly to a familiar SQL Server isolation level for anyone who’s worked with locking hints directly:
query 50100 MyQuery{ ReadState = ReadCommitted; ...}
ReadCommitted reads only data that has actually been committed to the database, though rows aren’t guaranteed to stay consistent for the duration of the transaction — the direct AL equivalent of SQL’s Read Committed. ReadExclusive also reads only committed data, but places update locks on everything it reads, preventing other transactions from modifying or deleting that data while the query runs — equivalent to SQL’s UpdLocks. ReadShared reads only committed data and places share locks that block modification or deletion until the current transaction commits, while still allowing other transactions to read the same data — the AL equivalent of Repeatable Read. ReadUncommitted remains the default, unchanged: no locks, potentially dirty data, maximum performance.
The same update extends comparable control to isolated storage reads, giving row locking for safer read-modify-write patterns there as well.
Why This Actually Matters for Real AL Development
The value here isn’t that ReadUncommitted was wrong as a default — for the majority of reporting queries, it remains the right choice, and switching everything to a stricter isolation level indiscriminately would just reintroduce the locking contention queries were designed to avoid. The value is that a specific, narrow category of queries that genuinely needed a stronger guarantee had no way to get one before now.
Consider a query feeding a read-modify-write pattern checking a value, then acting on the result in a way that depends on that value staying accurate for the duration of an operation. Under ReadUncommitted, that check could be reading a value that gets rolled back a moment later, and the subsequent action would be built on data that was never real. That’s exactly the class of bug that’s brutal to reproduce and diagnose, because it depends on precise transaction timing that won’t show up reliably in testing. ReadCommitted or ReadExclusive, applied deliberately to that specific query, closes the gap.
This is worth testing directly against a real concurrent workload rather than assuming from the property description alone. The performance cost of ReadExclusive or ReadShared under genuine contention many users hitting the same underlying data simultaneously is the kind of thing that behaves very differently in a load test than in a quiet development sandbox.
Finally
Most AL developers have never had a reason to think about what isolation level their queries run at, because there was only ever one option. That invisibility was fine right up until a query needed a guarantee that ReadUncommitted structurally couldn’t provide.
Version 29 doesn’t change the right default for most queries. It closes a real gap for the minority that need something stronger and the developers who benefit most from this are the ones who go back and identify which of their existing queries actually belong in that minority, rather than leaving every query on the old default simply because it’s always been the only choice available.


