Compatibility Level 170: What SQL Server 2025 Turns On

The instance was upgraded, yet a new function still fails in one database. Compatibility level 170 controls several SQL Server 2025 behaviors independently of the engine version.

A hot air balloon lying flat on a field at dawn as a person opens the burner and a red flame starts

Read the Engine and Database Separately

Installing SQL Server 2025 changes the engine running your databases. Each database also has its own compatibility setting. Restored databases retain a supported older level instead of automatically adopting every new query behavior. That separation gives you room to test application queries after upgrading the server.

I check both values before troubleshooting a missing feature. An engine version from SERVERPROPERTY answers what software is running. The database row answers which compatibility behavior applies here. A server upgrade announcement can be perfectly accurate while telling you only half the story.

SELECT SERVERPROPERTY('ProductVersion') AS EngineVersion,
       SERVERPROPERTY('ProductMajorVersion') AS EngineMajorVersion,
       DB_NAME() AS CurrentDatabase;
SELECT name, compatibility_level
FROM sys.databases
WHERE database_id = DB_ID();

For SQL Server 2025, the major engine version is 17. Older engine versions cannot accept level 170. Also check the query window's database selection. Testing in master while diagnosing an application database introduces a particularly avoidable distraction.

Know What the Higher Level Enables

Compatibility level 170 enables new optimizer behavior and selected language features. REGEXP_LIKE and REGEXP_SPLIT_TO_TABLE require it on SQL Server 2025. Optional parameter plan optimization also needs this level and its database scoped configuration enabled.

Do not assume every feature introduced with the engine has the same requirement. Some other regular expression scalar functions work without this particular compatibility gate. Backup compression options belong to backup commands and engine support. Check the requirement for the feature you actually intend to use.

The setting also enables the cardinality estimation changes associated with the new level. Estimates influence access paths, joins, and memory grants. A different plan is expected in some cases. Better overall workload behavior is the objective, rather than keeping every plan drawing identical.

Establish Query Store Before Changing Anything

Query Store gives you historical plans and execution statistics within the database. Enable it before the compatibility change and collect a representative workload. A baseline from a quiet lunch break says little about a busy month-end process. Include scheduled reports, background jobs, and important parameter variations.

These examples use an existing test database named CompatLab. Start with a restored application copy before scheduling a production change. Review storage limits and retention settings rather than relying on a default allocation. Query Store needs enough space to retain both sides of your comparison.

ALTER DATABASE [CompatLab] SET QUERY_STORE = ON;
ALTER DATABASE [CompatLab]
SET QUERY_STORE (OPERATION_MODE = READ_WRITE);
GO
USE [CompatLab];
GO
SELECT actual_state_desc, desired_state_desc,
       current_storage_size_mb, max_storage_size_mb,
       query_capture_mode_desc
FROM sys.database_query_store_options;

An enabled setting alone does not prove useful capture. Check that actual_state_desc is READ_WRITE and that your important statements appear. Read-only state, restrictive capture, or exhausted storage leaves holes in the evidence. Resolve those gaps before you change query behavior.

Move to Compatibility Level 170 in a Controlled Window

Record the original database setting outside the query session. Also record the change time and relevant scoped configurations. Keep those facts with your test results. A rollback value remembered from another database is a poor substitute for the value you actually read.

Change only the database selected for this test. The command affects its query compilation behavior and clears its plan cache. Expect fresh compilation afterward. It does not install a new engine or alter every database on the instance. Avoid combining this change with unrelated schema or index deployments.

ALTER DATABASE [CompatLab]
SET COMPATIBILITY_LEVEL = 170;
GO
USE [CompatLab];
GO
SELECT name, compatibility_level
FROM sys.databases
WHERE database_id = DB_ID();
SELECT name, value
FROM sys.database_scoped_configurations
WHERE name IN
    (N'OPTIONAL_PARAMETER_OPTIMIZATION',
     N'PARAMETER_SENSITIVE_PLAN_OPTIMIZATION');
From engine upgrade to a level change: a diagram about the compatibility level 170

Confirm Compatibility Level 170 With a Small Test

Use a harmless statement to check the language gate. The following input values are chosen examples, rather than real application data. The predicate accepts an uppercase prefix followed by four digits. Inspect the returned codes and verify they match that stated rule.

SELECT CodeValue
FROM (VALUES (N'AB1234'), (N'ab1234'), (N'AB12')) AS v(CodeValue)
WHERE REGEXP_LIKE(CodeValue, N'^[A-Z]{2}[0-9]{4}$', 'c');
SELECT value
FROM REGEXP_SPLIT_TO_TABLE(N'red,green,blue', N',');

The split output does not establish a contractual sort order by itself. Add an appropriate ordering method when downstream logic requires one. For application validation, also decide how NULL, empty strings, and unexpected length should behave. A syntax test only confirms that the feature is available.

Compare Comparable Query Store Windows

I compare the important queries individually before accepting an upgrade. Aggregate totals hide changes in execution count and parameter mix. A query run once after the change does not provide the same evidence as a heavily exercised baseline. Inspect both duration and workload volume.

Run this query in CompatLab after capturing the two periods. Replace the time values with your recorded UTC test windows. Query Store statistics summarize intervals, so use interval boundaries for a cleaner comparison. Partial overlaps include the whole matching interval in this example.

DECLARE @ChangeUTC datetimeoffset = '2026-01-15T12:00:00+00:00';
DECLARE @BeforeUTC datetimeoffset = DATEADD(HOUR, -2, @ChangeUTC);
DECLARE @AfterUTC datetimeoffset = DATEADD(HOUR, 2, @ChangeUTC);
SELECT q.query_id, p.plan_id,
       CASE WHEN i.end_time <= @ChangeUTC
            THEN 'Before' ELSE 'After' END AS TestPeriod,
       SUM(s.count_executions) AS ExecutionCount,
       SUM(s.avg_duration * s.count_executions) /
           NULLIF(SUM(s.count_executions), 0) / 1000.0 AS AverageDurationMs,
       SUM(s.avg_cpu_time * s.count_executions) /
           NULLIF(SUM(s.count_executions), 0) / 1000.0 AS AverageCpuMs
FROM sys.query_store_query AS q
JOIN sys.query_store_plan AS p ON p.query_id = q.query_id
JOIN sys.query_store_runtime_stats AS s ON s.plan_id = p.plan_id
JOIN sys.query_store_runtime_stats_interval AS i
  ON i.runtime_stats_interval_id = s.runtime_stats_interval_id
WHERE s.execution_type = 0
  AND i.end_time > @BeforeUTC
  AND i.start_time < @AfterUTC
  AND (i.end_time <= @ChangeUTC OR i.start_time >= @ChangeUTC)
GROUP BY q.query_id, p.plan_id,
         CASE WHEN i.end_time <= @ChangeUTC
              THEN 'Before' ELSE 'After' END
ORDER BY q.query_id, TestPeriod, p.plan_id;

Investigate Regressions Before Blaming the Number

The query excludes intervals crossing the change time. That prevents mixing the two settings inside one summary. Weighting averages by execution count handles multiple statistics rows correctly. The underlying duration and CPU columns use microseconds, so the expression converts their averages to milliseconds.

When a query regresses, open both plans in SSMS. Compare estimates, actual execution evidence, grants, spills, and parameter values. Check statistics freshness and workload changes too. Multiple plans can reflect useful parameter variants. Do not label every extra plan a failure.

Query Store plan forcing provides a targeted response when a suitable prior plan remains usable. Test it and verify forcing succeeds. Keep a removal plan for that temporary intervention. A single problematic statement should prompt focused analysis before you abandon improvements across the workload.

Keep a Tested Route Back From Compatibility Level 170

What will happen to new application code if you lower the setting? REGEXP_LIKE calls introduced during the deployment stop working below their required level. Coordinate code rollback with database rollback. Changing a number cannot undo a dependency already added to an application.

If your recorded original level was 160, this command restores that value. Use the actual recorded value for another database. Run the representative workload again and review new compilations. Lowering the setting leaves the SQL Server 2025 engine in place. It does not restore an older engine's security fixes or storage format.

ALTER DATABASE [CompatLab]
SET COMPATIBILITY_LEVEL = 160;

Approve compatibility level 170 after feature tests and workload comparisons support it. Keep the baseline, change time, and exception decisions available. That record makes later plan changes much easier to explain than a vague recollection that the upgrade seemed fine.

Related reading on this blog: Impact of Changing Database Compatibility Level on Cache and SQL SERVER 2022: Oldest Compatibility Level Supported.

Around the level change: a checklist on the compatibility level 170

A compatibility level is not the engine version, it is a database behavior setting.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.

Compatibility Level, Query Store, SQL Server, SQL Upgrade
Previous Post
SQL SERVER – Create a Very First Report with the Report Wizard
Next Post
SQL SERVER – Data Sources and Data Sets in Reporting Services SSRS

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.