IntermediateReferenceDatabases

SQL Server

SQL Server edition limits that decide architecture - memory and core caps, Always On versus FCI, RCSI and tempdb, Query Store, core-based licensing.

Applies to
SQL Server2017 (14.x) - 2025 (17.x)2025 (17.x) is current
matureUpdated Reviewed

The feature matrix is not what constrains a SQL Server design. The scale limits are. Standard edition will not use more than a fixed amount of memory for its buffer pool and will not schedule work across more than a fixed number of cores, no matter what hardware you put underneath it, and the gap between Standard and Enterprise is a licensing decision measured in cores, not a configuration change. Get those numbers wrong at the design stage and every later tuning effort is spent working around a ceiling that was chosen for you.

Edition scale limits

These are per instance of the Database Engine, and they are hard: exceeding the hardware simply wastes it. The figures changed in 2025, which matters if you are sizing a migration.

Limit Enterprise Standard (2025) Standard (2022 and earlier) Express (2025) Express (2022 and earlier)
Max compute capacity, Database Engine OS maximum Lesser of 4 sockets or 32 cores Lesser of 4 sockets or 24 cores Lesser of 1 socket or 4 cores Lesser of 1 socket or 4 cores
Max buffer pool memory OS maximum 256 GB 128 GB 1,410 MB 1,410 MB
Max columnstore segment cache Unlimited 32 GB 32 GB 352 MB 352 MB
Max memory-optimised data per database Unlimited 32 GB 32 GB 352 MB 352 MB
Max relational database size 524 PB 524 PB 524 PB 50 GB 10 GB
Instances per server 50 50 50 50 50

The buffer pool cap is not the instance’s total memory. Columnstore segment cache and memory-optimised (In-Memory OLTP) data are capped separately on Standard, at 32 GB each, outside the buffer pool figure. A Standard instance therefore has a working set larger than 256 GB, but you cannot spend the columnstore allowance on row-store pages, or the reverse.

Compute is capped by sockets and cores, whichever binds first. Standard on a two-socket server with 24 physical cores per socket uses 32 of the 48 cores; the other 16 are bought and idle. In a virtualised environment the limit applies to logical processors instead of cores, because the processor architecture is not visible to the guest. Deploying multiple instances on one server is the documented way to consume capacity beyond a single instance’s limit - hence the 50-instance figure.

Express is a distribution format, not a small database. The size cap rose from 10 GB to 50 GB in SQL Server 2025 (17.x), which matters for shipped appliances, but 1,410 MB of buffer pool and four cores mean anything with a working set worth caching reads from disk continuously. Express is also excluded from most of the high-availability list below.

Enterprise’s only compute limit is a legacy one. Under Server + Client Access License (CAL) licensing (not available for new agreements) Enterprise is capped at 20 cores per instance; under core-based licensing there is no limit. If you inherit an Enterprise instance that refuses to use more than 20 cores, this is why.

One ceiling is independent of edition: SQL Server does not support more than 64 logical cores per NUMA node. From SQL Server 2022 (16.x) CU11 the Database Engine refuses to start if it detects a configuration that can exceed this, and from CU15 Setup warns about it. The fix is BIOS-level: sub-NUMA clustering on Intel, Nodes per Socket on AMD, or disabling SMT on Azure Mv3-series VMs.

Always On: availability groups versus failover cluster instances

These two things share a brand and protect against different failures. Deploying the wrong one is a common and expensive mistake, because you only find out during the incident it does not cover.

Failover cluster instance (FCI) Availability group (AG)
Unit of redundancy The whole server instance A named set of user databases
Storage Shared - one copy of the data, on cluster disks, CSV, Storage Spaces Direct or SMB shares Not shared - every replica holds its own full copy
Protects against Node hardware, OS, service failure Node failure and loss of the storage copy
Does not protect against Loss or corruption of the shared storage Nothing at the instance level: logins, jobs, and linked servers are not in the AG
Readable secondaries No - the passive node runs nothing Yes, optionally, plus backups on secondaries
Node/replica ceiling 16 nodes on Enterprise, 2 on Standard Up to 8 secondary replicas on Enterprise
Standard edition Supported, two nodes Basic availability groups only

An FCI is one instance installed across cluster nodes and presented on the network as a single virtual network name. Exactly one node owns the resource group (network name, IP address, shared disks, the Database Engine and Agent services) at a time. On failover, dirty pages are flushed if the failure allows it, services stop on the old node, ownership transfers, and clients reconnect to the same name with no reconfiguration. Because there is one copy of the data, an FCI does nothing for storage failure, and failover time is bounded by the number of dirty pages in the buffer cache. Indirect checkpoints throttle that, at the cost of more I/O during normal operation.

An availability group replicates at the database level. The primary sends transaction log records to every secondary, each of which hardens the log and applies it independently. Failover happens at the level of the replica, not the database: a suspect database does not trigger it. Secondaries are not backups, and Microsoft is explicit about that. You still need a backup regime, although log backups and copy-only full backups can be offloaded to a secondary. On Windows an HA availability group requires a Windows Server Failover Cluster, with each replica on a different node; on Linux the cluster manager is Pacemaker. Quorum is computed across all WSFC nodes, whether or not they host replicas, and there is no witness role as there was in database mirroring. A read-scale availability group has no cluster manager and consequently no automatic failover.

The Enterprise edition matrix says up to eight secondary replicas including five synchronous ones; the availability groups overview describes the same topology as a primary plus four synchronous-commit secondaries. Either way, plan for a synchronous set of five and asynchronous replicas beyond that.

The Standard edition restriction

Standard does not get availability groups. It gets basic availability groups, and the restriction is severe: two replicas, and one database. Not one availability group with several databases. One database. Contained availability groups, distributed availability groups, and automatic read/write connection rerouting are all Enterprise-only. So is online index create and rebuild, resumable online index rebuilds, and online page and file restore, all of which are what make maintenance on a 24/7 system possible in the first place.

The practical effect: on Standard, an application with six databases that must fail over together has no supported availability-group answer. The options are an FCI (two nodes, shared storage, so no protection against storage loss), log shipping, or Enterprise.

Combining the two is legitimate: an FCI can host a replica of an availability group for remote disaster recovery. One caveat applies. Automatic failover from an FCI to other nodes within an availability group is not supported.

Isolation levels and RCSI

The default isolation level is READ COMMITTED, and its behaviour depends entirely on one database option most instances never change.

Level Reader behaviour Cost
READ UNCOMMITTED No shared locks; dirty reads possible Unreliable results
READ COMMITTED, READ_COMMITTED_SNAPSHOT OFF (SQL Server default) Shared locks block reads of uncommitted rows and block writers Reader/writer blocking
READ COMMITTED, READ_COMMITTED_SNAPSHOT ON Row versioning gives a statement-level consistent snapshot; only Sch-S locks Version store, 14 bytes per row
REPEATABLE READ Shared locks held to end of transaction Heavy blocking
SNAPSHOT (ALLOW_SNAPSHOT_ISOLATION ON) Transaction-level consistency from row versions Update conflicts (error 3960)
SERIALIZABLE Range locks Most blocking

With READ_COMMITTED_SNAPSHOT off, a SELECT takes shared locks: it is blocked by uncommitted writers and it blocks writers in turn. On a contended OLTP database that single fact produces most of the blocking chains you will ever investigate. Turning the option on switches the same READ COMMITTED sessions to row versioning. Each statement sees a transactionally consistent view of the data as it stood when the statement began, taking only schema stability locks. No application code changes. It is the default on Azure SQL Database, and it is also the setting Microsoft names as the one that gives the most benefit from optimized locking.

ALTER DATABASE [Orders] SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;

Two things to know before running it. First, when you set the option only the connection executing ALTER DATABASE may be in the database; there must be no other open connection until it completes, though the database does not have to be in single-user mode. WITH ROLLBACK IMMEDIATE is how you get there on a live system, and it kills in-flight transactions.

Second, the semantics change. Readers no longer wait for writers, so a query that used to block until a writer committed now returns the previous committed value instead. Code that relied on blocking as an accidental serialisation mechanism will behave differently. The READCOMMITTEDLOCK table hint restores shared locking for individual statements where you actually need it.

tempdb and the version store

Row versions live in a version store. If accelerated database recovery (ADR) is enabled the version store is in the user database; otherwise it is in tempdb. Every data modification in the database is versioned once either READ_COMMITTED_SNAPSHOT or ALLOW_SNAPSHOT_ISOLATION is on, including modifications made by transactions that never read a version.

Versions are retained as long as any active transaction might need them. A single long-running transaction therefore pins the whole version store, which is the mechanism behind almost every “tempdb filled up after we enabled RCSI” incident. Row versions created inside a trigger are held to the end of the transaction even though nothing needs them after the trigger finishes.

The failure modes are specific and each has its own message:

Condition Behaviour
tempdb under pressure, shrink in progress Longest transactions that have not yet generated versions are marked victims; message 3967 per victim
Victim transaction tries to read a version Message 3966, transaction rolled back
tempdb full, version store in tempdb Writes continue but stop generating versions, message 3959; reads that need a missing version fail
Version store full, ADR enabled Reads succeed; UPDATE and DELETE fail; INSERT still succeeds if space allows
UPDLOCK under SNAPSHOT and the latest version is not visible Msg 3960, snapshot isolation transaction aborted due to update conflict

There is a cost in the data rows too. Up to 14 bytes per row are appended for row versioning information: the transaction sequence number and a pointer to the versioned row, added the first time a row is modified or inserted while versioning is on. On a table with 100-byte average rows, Microsoft’s own example, that is up to 14 percent growth, plus page splits as the extra bytes fail to fit on existing pages. Lower the fill factor before you enable it on a large, densely packed table.

Presize tempdb on its own storage and monitor sys.dm_db_file_space_usage and sys.dm_tran_active_snapshot_database_transactions. RCSI converts a blocking problem into a space problem, and the space problem is caused by long transactions.

Query Store

Query Store records a history of queries, plans and runtime statistics per database and retains them in time windows, so a regression can be tied to the plan change that caused it. The plan cache holds only the latest plan for a query and evicts under memory pressure; Query Store does not.

It is enabled by default in READ_WRITE mode for new databases from SQL Server 2022 (16.x). It is not enabled by default on 2016, 2017 or 2019, and a database restored or upgraded in place keeps its previous setting, so a 2022 or 2025 instance full of migrated databases may have it off everywhere. It cannot be enabled on master or tempdb.

What it captures: plans for DML, meaning SELECT, INSERT, UPDATE, DELETE, MERGE and BULK INSERT. By design it does not capture DDL such as CREATE INDEX, though it does capture the internal DML those statements generate. Natively compiled stored procedures are excluded unless you enable them with sys.sp_xtp_control_query_exec_stats. From SQL Server 2017 (14.x) it also tracks per-query wait statistics, which is the only convenient way to attribute instance-level waits to a specific query.

Defaults worth knowing before you rely on it:

Option Default Notes
MAX_STORAGE_SIZE_MB 1000 (2019+); 100 on 2016-2017 Not strictly enforced; checked only at flush time
DATA_FLUSH_INTERVAL_SECONDS 900 (15 min) Also the window of data lost on an unclean shutdown
INTERVAL_LENGTH_MINUTES 60 Aggregation window for runtime statistics
CLEANUP_POLICY (STALE_QUERY_THRESHOLD_DAYS) 30 Retention
SIZE_BASED_CLEANUP_MODE AUTO Starts at 90% of max, stops near 80%, deletes cheapest and oldest first
QUERY_CAPTURE_MODE AUTO (2019+); ALL on 2016-2017 AUTO filters out trivial ad hoc queries
MAX_PLANS_PER_QUERY 200
WAIT_STATS_CAPTURE_MODE ON (2017+)

The size limit being checked only at flush time is the operational trap: Query Store can breach MAX_STORAGE_SIZE_MB between checks and then flip itself to READ_ONLY, silently stopping collection at exactly the moment the workload got interesting. Size-based cleanup then frees space and it switches back. If you find a database in READ_ONLY mode, raise the size or clear the store, then fix the cause. An unparameterised ad hoc workload is the usual one. Microsoft’s guidance is blunt: if you think you need more than 10 GB, fix the workload’s plan reuse instead, with forced parameterisation or QUERY_CAPTURE_MODE = CUSTOM.

A configuration that survives a real workload:

ALTER DATABASE [Orders] SET QUERY_STORE = ON
    (
      OPERATION_MODE = READ_WRITE,
      CLEANUP_POLICY = ( STALE_QUERY_THRESHOLD_DAYS = 90 ),
      DATA_FLUSH_INTERVAL_SECONDS = 900,
      MAX_STORAGE_SIZE_MB = 1024,
      INTERVAL_LENGTH_MINUTES = 60,
      SIZE_BASED_CLEANUP_MODE = AUTO,
      MAX_PLANS_PER_QUERY = 200,
      WAIT_STATS_CAPTURE_MODE = ON,
      QUERY_CAPTURE_MODE = CUSTOM,
      QUERY_CAPTURE_POLICY = (
        STALE_CAPTURE_POLICY_THRESHOLD = 24 HOURS,
        EXECUTION_COUNT = 30,
        TOTAL_COMPILE_CPU_TIME_MS = 1000,
        TOTAL_EXECUTION_CPU_TIME_MS = 100
      )
    );

Those CUSTOM thresholds are the documented AUTO values from SQL Server 2019 (15.x) onwards, set explicitly so that changing one is a deliberate act.

Plan forcing is the reason to have Query Store on before you need it. Because multiple plans per query are retained, the Query Processor can be directed to use a specific one, using a mechanism similar to a query hint but requiring no application change. When a plan regresses after a statistics refresh, a new index, or a parameter-sensitive recompile, the “Regressed Queries” and “Queries With Forced Plans” views in SSMS identify it and force the earlier plan in seconds. Treat a forced plan as an incident mitigation with an expiry date, not a fix: it pins the optimiser to a decision made against data that will keep changing.

Two global trace flags trade data for availability: 7745 stops Query Store flushing to disk before shutdown, losing up to DATA_FLUSH_INTERVAL_SECONDS of data, and 7752 loads it asynchronously at startup so the database comes online before the store has recovered.

Licensing, and why “add cores” is the wrong reflex

Under the per-core model, the number of licences equals the number of physical cores on the server, subject to a minimum of four licences per physical processor, or, when licensing a virtual machine, the number of virtual cores, subject to a minimum of four per VM. Licences are sold in two-core packs, so an order is always the licence count divided by two.

Three design implications:

  • Vertical scaling has a linear licence cost with no volume relief. Doubling cores doubles the licence count in the same edition. The cheapest performance work is nearly always the work that reduces CPU demand: indexing, plan stability, RCSI to remove blocking. More cores come after that.
  • The four-licence-per-processor minimum penalises wide, low-core-count hardware. Four sockets with two cores each still costs 16 licences. Fewer sockets with more cores per socket is the licence-efficient shape, up to the edition’s core cap.
  • The Standard-to-Enterprise decision is usually driven by an HA requirement, not throughput. Full availability groups, online index rebuilds and resource governor are Enterprise features. If you need them, you pay the Enterprise per-core rate on every core in the server.

Developer and Evaluation editions include all Enterprise functionality but are licensed for development and test only; Evaluation expires after 180 days. SQL Server 2025 adds a Standard Developer edition, which is the right choice for a test environment that must reproduce Standard’s ceilings. Verify entitlements against the licensing guidance and Product Terms, not this page.

Diagnostics

SQL Server exposes diagnostics through Dynamic Management Views (DMVs), the error log, and Extended Events.

Blocking and deadlocks. Identify blocked sessions:

SELECT
    r.session_id,
    r.blocking_session_id,
    r.wait_type,
    r.wait_time,
    t.text AS query_text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id <> 0;

For deadlocks, enable the system_health Extended Events session (on by default in 2012+) and query the deadlock graph:

SELECT XEvent.query('.') AS deadlock_graph
FROM (
    SELECT CAST(target_data AS XML) AS target_data
    FROM sys.dm_xe_session_targets st
    JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
    WHERE s.name = 'system_health' AND st.target_name = 'ring_buffer'
) AS data
CROSS APPLY target_data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(XEvent);

High CPU from query compilation. Check plan cache pressure:

SELECT
    objtype,
    COUNT(*) AS plan_count,
    SUM(size_in_bytes) / 1024 / 1024 AS size_mb
FROM sys.dm_exec_cached_plans
GROUP BY objtype
ORDER BY size_mb DESC;

Thousands of Adhoc plans indicate missing parameterisation. Enable forced parameterization at the database level or fix the application to use parameterised queries.

TempDB contention. Visible as PAGELATCH_* waits on pages in database 2:

SELECT wait_type, wait_time_ms, waiting_tasks_count
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE 'PAGELATCH%'
ORDER BY wait_time_ms DESC;

Fix: add more TempDB data files (one per logical CPU core, up to 8), and enable trace flag 1118 (uniform extents) on versions before 2016.

Availability Group failover not completing. Check the AG dashboard in SSMS or:

SELECT
    ag.name,
    ars.role_desc,
    ars.synchronization_health_desc,
    ar.availability_mode_desc
FROM sys.dm_hadr_availability_replica_states ars
JOIN sys.availability_replicas ar ON ar.replica_id = ars.replica_id
JOIN sys.availability_groups ag ON ag.group_id = ar.group_id;

If synchronization_health_desc is NOT_HEALTHY, the secondary is behind. Check the error log on the secondary for redo thread errors, and verify network connectivity between replicas.

Log file growing unbounded. The transaction log does not truncate if a long-running transaction or replication holds it open:

SELECT name, log_reuse_wait_desc FROM sys.databases WHERE name = '<db>';

Common values: ACTIVE_TRANSACTION (find and kill or commit it), REPLICATION (the log reader job is behind or a subscriber is disconnected), AVAILABILITY_REPLICA (secondary is behind on redo).

Where SQL Server is the wrong choice

When the licence cost buys nothing you use. A workload that fits in Standard’s ceilings, needs no availability group, and uses no Enterprise feature is paying for an engine whose distinguishing capabilities are switched off. PostgreSQL covers that ground with no per-core cost, and its concurrency model is the RCSI behaviour by default rather than an option you must enable.

When you need more than one writable node. Neither an FCI nor an availability group gives you multi-primary writes. Secondaries are readable at best. If the requirement is write scale-out, the answer is sharding at the application layer or a different class of system entirely.

When you need automatic failover without a cluster. AGs on Windows require a WSFC; on Linux they require Pacemaker. A read-scale availability group has no cluster manager and therefore no automatic failover at all. There is no self-contained failover mechanism inside the database.

Embedded, below Express’s ceilings. Express with LocalDB is a reasonable developer convenience, but a 1,410 MB buffer pool and four cores make it a poor production choice for anything with a real working set. SQLite or PostgreSQL are usually better answers for embedded and small-server cases.

SQL Server release support

4 supported
ReleaseLatestReleasedEnd of lifeStatus
17.017.0.5005.3 CU9Nov 2025Jan 2036Supported
16.016.0.4295.3 CU27Nov 2022Jan 2033Supported
13.0-sp3-acp13.0.7095.1 Azure Connect pack+GDRMay 2022Jul 2026End of life
13.0-sp313.0.6500.1 GDRSept 2021Jul 2026End of life
15.015.0.4490.9 CU32+GDRNov 2019Jan 2030Supported
12.0-sp312.0.6449.1 CU4+GDROct 2018Jul 2024End of life
14.014.0.3550.4 CU31+GDRSept 2017Oct 2027Supported

Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.

Where SQL Server sits

How this component relates to the rest of the stack. Items without a link are not covered here yet.

Deployed with

Commonly run alongside

  • Windows Failover Clustering

Alternatives

Solves a similar problem differently