QueryTuning.org
Database Query Performance Reference
SQL Server Query Performance detailed analysis

The SET Options Your Application Sets Without Telling You — and How They Silently Break Query Performance

JK
James Kowalski
Senior DBA · SQL Server · 12 years production
April 7, 2026
13 min read
🗂 SQL Server 2016+

The query runs in 40ms from SSMS. The same query, from the application, runs in 22 seconds. You enable SQL Server Profiler and watch both executions hit identical code paths. Same query. Same parameters. Different plans. The execution plan for the application shows an index scan. The SSMS plan shows an index seek. The difference is not the query. It is the session SET options.

What SET Options Are

Every SQL Server session carries a collection of boolean flags — SET options — that control how the session evaluates data and generates plans. ARITHABORT, ANSI_NULLS, ANSI_PADDING, QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL, NUMERIC_ROUNDABORT, ANSI_WARNINGS. These flags affect whether NULL comparisons work correctly, whether arithmetic overflow raises an error, and — critically — how SQL Server caches and reuses execution plans.

SQL Server stores the SET option bitmask as part of the plan cache key. A plan compiled with ARITHABORT ON is a different cache entry from the same plan compiled with ARITHABORT OFF. Two sessions with different SET options will never share a cached plan, even if they run identical queries against identical data.

Why This Silently Destroys Performance

SSMS defaults: ARITHABORT ON. Most .NET, JDBC, and ODBC drivers default: ARITHABORT OFF. SQL Server's parameter sniffing builds the plan on first execution, using the calling session's SET options. The SSMS session builds a plan optimised for a seek. The application session, with different SET options, builds its own plan — and if the parameters are different (or if statistics are misleading), it can build a scan plan that runs 500x slower.

Diagnosing a SET Options Problem

The key signal: SSMS is fast, app is slow, same query

If your query runs fast from SSMS and slow from the application, and no other explanation holds, check SET options before anything else. This is the most common hidden cause of dev/prod performance divergence.

Step 1 — Capture what SET options each session is using
SQL Server
SELECT
  s.session_id,
  s.login_name,
  s.host_name,
  s.program_name,
  -- Decode the set_options bitmask into human-readable flags:
  CASE WHEN (s.set_options & 64) <> 0  THEN 'ON' ELSE 'OFF' END  AS arithabort,
  CASE WHEN (s.set_options & 16) <> 0  THEN 'ON' ELSE 'OFF' END  AS ansi_nulls,
  CASE WHEN (s.set_options & 8)  <> 0  THEN 'ON' ELSE 'OFF' END  AS ansi_padding,
  CASE WHEN (s.set_options & 256) <> 0 THEN 'ON' ELSE 'OFF' END  AS quoted_identifier
FROM   sys.dm_exec_sessions s
WHERE  s.is_user_process = 1
ORDER BY s.program_name, s.login_name;
Step 2 — Find how many separate cached plans exist for the same procedure
SQL Server
SELECT
  cp.usecounts,
  cp.size_in_bytes / 1024                                     AS plan_kb,
  cp.objtype,
  qs.total_elapsed_time / qs.execution_count / 1000            AS avg_ms,
  qs.total_logical_reads / qs.execution_count                   AS avg_reads
FROM       sys.dm_exec_cached_plans   cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
JOIN       sys.dm_exec_query_stats    qs ON qs.plan_handle = cp.plan_handle
WHERE      st.objectid = OBJECT_ID('dbo.YourProcedureName')
ORDER BY   qs.total_elapsed_time / qs.execution_count DESC;

-- More than 1 row = multiple plans for the same proc.
-- Compare avg_reads: the bad plan will have 100x-10000x more reads.
Step 3 — Identify which plan is being used by which session type
SQL Server
SELECT
  r.session_id,
  s.program_name,
  r.status,
  r.total_elapsed_time / 1000 AS elapsed_ms,
  r.logical_reads,
  CASE WHEN (s.set_options & 64) <> 0 THEN 'ON' ELSE 'OFF' END AS arithabort,
  st.text  AS query_text
FROM       sys.dm_exec_requests   r
JOIN       sys.dm_exec_sessions   s  ON s.session_id = r.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle)  st
CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) qp
WHERE      r.status = 'running';

-- Pattern to look for:
-- SSMS sessions: ARITHABORT = ON, elapsed = 40ms
-- App sessions:  ARITHABORT = OFF, elapsed = 22,000ms

Three Fixes, One Right One

Once confirmed, there are three ways to resolve this. Only one is correct long-term.

Fix A — Force the application to use ARITHABORT ON (connection string)
SQL Server
-- For ADO.NET connections, add this to the connection string handler:
-- Or run at connection open time:
SET ARITHABORT ON;

-- EF Core / ADO.NET: add an interceptor or run in an IDbCommandInterceptor
-- This aligns application SET options with SSMS defaults
-- and forces both to share the same plan cache entries.
-- Downside: if the sniffed plan is still bad, this does not help.
Fix B — OPTIMIZE FOR UNKNOWN to remove sniffing entirely
SQL Server
ALTER PROCEDURE dbo.YourProcedureName
  @Param1 INT
AS
SELECT ...
WHERE  SomeColumn = @Param1
OPTION (OPTIMIZE FOR (@Param1 UNKNOWN));

-- Forces the optimizer to use average statistics instead of sniffed values.
-- Best for procedures called with wildly varying parameter values.
-- Produces a generic plan — not optimal for any one value, acceptable for all.
Fix C — RECOMPILE on each call (correct when data is highly variable)
SQL Server
ALTER PROCEDURE dbo.YourProcedureName
  @Param1 INT
WITH RECOMPILE  -- compile a fresh plan on every call
AS
SELECT ...

-- Only use this when:
-- (a) The data distribution varies enormously by parameter value
-- (b) The compile cost (~1-5ms) is acceptable
-- (c) The procedure is not called thousands of times per second

The Indexed View Problem

There is a second place where SET options cause silent failures: indexed views. SQL Server will not use an indexed view in a query plan unless the session has the required SET options active: ARITHABORT ON, ANSI_NULLS ON, ANSI_PADDING ON, ANSI_WARNINGS ON, CONCAT_NULL_YIELDS_NULL ON, QUOTED_IDENTIFIER ON, NUMERIC_ROUNDABORT OFF.

An application with ARITHABORT OFF will never benefit from indexed views, even if those views were created precisely to speed up those queries. The view exists. The index exists. The optimizer cannot use it because the SET options do not qualify.

Verify a session can use indexed views
SQL Server
-- Required SET options for indexed view eligibility:
-- Run this block and confirm each is ON:
SELECT
  'ARITHABORT'           AS option_name, CASE WHEN (@@options & 64)  <> 0 THEN 'ON' ELSE 'OFF' END AS status UNION ALL
SELECT  'ANSI_NULLS',            CASE WHEN (@@options & 16)  <> 0 THEN 'ON' ELSE 'OFF' END        UNION ALL
SELECT  'QUOTED_IDENTIFIER',     CASE WHEN (@@options & 256) <> 0 THEN 'ON' ELSE 'OFF' END       UNION ALL
SELECT  'ANSI_WARNINGS',         CASE WHEN (@@options & 8)   <> 0 THEN 'ON' ELSE 'OFF' END       UNION ALL
SELECT  'NUMERIC_ROUNDABORT',    CASE WHEN (@@options & 8192) = 0 THEN 'OFF (required)' ELSE 'ON (blocks indexed view)' END;

How to Prevent This Long-Term

Every application connection should explicitly set required SET options immediately after opening. Do not rely on driver defaults. The server default for ARITHABORT changed between SQL Server versions and driver versions have differed historically. The only safe approach is explicit:

Set options at application connection open time
SQL Server
-- Run this immediately after every connection open:
-- (Add to your DbContext OnConfiguring, connection factory, or middleware)
SET ARITHABORT ON;
SET ANSI_NULLS ON;
SET ANSI_PADDING ON;
SET ANSI_WARNINGS ON;
SET QUOTED_IDENTIFIER ON;
SET CONCAT_NULL_YIELDS_NULL ON;
SET NUMERIC_ROUNDABORT OFF;

These seven lines eliminate the class of SET option divergence that causes plan cache splits. They also enable indexed view usage, ensure correct NULL semantics, and prevent a category of arithmetic errors that would otherwise silently return wrong results rather than raising an error.

SET option mismatches are diagnosed once and fixed once. The fix takes minutes. The investigation — if you do not know what to look for — can take days.

JK
James Kowalski
Senior DBA · SQL Server · QueryTuning
James has spent 12 years managing SQL Server environments for financial services and e-commerce companies. He specialises in plan cache analysis, query store investigations, and the kind of subtle configuration problems that take three engineers two days to find.