Saturday, February 21, 2026

sp_BlitzCache Changed My Proc to Set-Based with ChatGPT

In a previous post, I gave sp_BlitzCache and ChatGPT a deadlock problem and they nailed it. AI found the lock ordering issue, flagged HOLDLOCK as redundant, and even inferred the other proc I was using to simulate the deadlocks without any information from me. I ended that post saying I would test the refactoring personality next. Here it is.

Short story; I created an ugly stored procedure with multi steps, cursor, and rbar processing. I then asked sp_BlitzCache to seek ChatGPT's input on optimizing the construct using a SET-BASED manipulation. And it did.

The Setup

Classic staging data flow; Nightly feed lands in dbo.CustomerStaging, and a proc cursors through it row by (agonizing) row to upsert into dbo.Customer. We've all seen this one.

CREATE TABLE dbo.Customer (
    CustomerID   INT PRIMARY KEY,
    Name         VARCHAR(100),
    Email        VARCHAR(100),
    City         VARCHAR(50),
    LastUpdated  DATETIME
);

CREATE TABLE dbo.CustomerStaging (
    StagingID    INT IDENTITY(1,1) PRIMARY KEY,
    CustomerID   INT,
    Name         VARCHAR(100),
    Email        VARCHAR(100),
    City         VARCHAR(50),
    Processed    BIT DEFAULT 0
);

2,000 existing customers seeded in the target. 5,000 staging rows loaded — 2,000 updates to existing customers and 3,000 new inserts. Here's the ugly proc:

CREATE PROCEDURE dbo.usp_ProcessCustomerStaging
AS
SET NOCOUNT ON;
BEGIN
    DECLARE @CustomerID   INT,
            @Name         VARCHAR(100),
            @Email        VARCHAR(100),
            @City         VARCHAR(50),
            @StagingID    INT;

    DECLARE cur CURSOR FOR
        SELECT StagingID, CustomerID, Name, Email, City
        FROM dbo.CustomerStaging
        WHERE Processed = 0;

    OPEN cur;
    FETCH NEXT FROM cur INTO @StagingID, @CustomerID, @Name, @Email, @City;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        IF EXISTS (SELECT 1 FROM dbo.Customer WHERE CustomerID = @CustomerID)
        BEGIN
            UPDATE dbo.Customer
            SET Name        = @Name,
                Email       = @Email,
                City        = @City,
                LastUpdated = GETDATE()
            WHERE CustomerID = @CustomerID;
        END
        ELSE
        BEGIN
            INSERT INTO dbo.Customer (CustomerID, Name, Email, City, LastUpdated)
            VALUES (@CustomerID, @Name, @Email, @City, GETDATE());
        END

        UPDATE dbo.CustomerStaging
        SET Processed = 1
        WHERE StagingID = @StagingID;

        FETCH NEXT FROM cur INTO @StagingID, @CustomerID, @Name, @Email, @City;
    END

    CLOSE cur;
    DEALLOCATE cur;
END

Three statements per row inside a cursor: IF EXISTS check, UPDATE or INSERT, mark staging row as Processed = 1, all RBAR. For 5,000 rows that's 15,000 individual operations plus cursor overhead.

The Refactoring Personality

Just like before, we need to add a new row into the Blitz_AI config table with a system prompt specifically targeting refactoring the procedural code to set-based operations. REMINDER: The AI Advice can only be as good as the data we give it. Write your prompt well. Be specific. Cover all bases:

INSERT INTO DBA.dbo.Blitz_AI (
    Nickname, AI_Model, AI_URL, AI_Database_Scoped_Credential_Name, 
    AI_System_Prompt_Override, Timeout_Seconds, DefaultModel)
VALUES (
    'refactoring', 
    'gpt-5-mini', 
    'https://api.openai.com/v1/chat/completions', 
    'https://api.openai.com/', 
    'You are a senior SQL Server developer who specializes in 
     refactoring procedural code into set-based operations. 
     Analyze the query text and execution plan for: cursors, 
     WHILE loops, row-by-row processing, repeated single-row 
     lookups that should be joins, scalar operations that could 
     be set-based, and unnecessary temp table or variable 
     round-trips. For each finding, explain why the current 
     approach is inefficient and provide a complete set-based 
     rewrite using the simplest, most manageable and optimal 
     approach; MERGE, JOINs, window functions, or CTEs as 
     appropriate. Show the full rewritten code, not just 
     fragments. Preserve the original behavior exactly — same 
     inputs, same outputs. Render your output in Markdown.',
    255, 
    0);

It looks like a lot, but the prompt is narrow, directly targeting a set-based manipulation without additional busy work. I've also instructed it to show its replacements in full code format, not fragments.

The sp_BlitzCache Call

I run the procedure a couple times just to get it into the cache, and then we call our friend, sp_BlitzCache:

EXEC dbo.sp_BlitzCache 
    @StoredProcName = 'usp_ProcessCustomerStaging',
    @AI = 1, 
    @AIConfig = 'DBA.dbo.Blitz_AI', 
    @AIModel = 'refactoring';

It ran for 00:06:22. Very interesting is that while it ran, sp_BlitzCache broke the proc into individual cached statements and made a separate API call for each one:

Calling AI endpoint for query plan analysis on query: 
Statement (parent [dbo].[usp_ProcessCustomerStaging]) - FETCH NEXT FROM cur INTO ...

Calling AI endpoint for query plan analysis on query: 
Statement (parent [dbo].[usp_ProcessCustomerStaging]) - UPDATE dbo.CustomerStaging SET Processed = 1 ...

Calling AI endpoint for query plan analysis on query: 
Statement (parent [dbo].[usp_ProcessCustomerStaging]) - UPDATE dbo.Customer SET Name = @Name ...

Calling AI endpoint for query plan analysis on query: 
Statement (parent [dbo].[usp_ProcessCustomerStaging]) - IF EXISTS (SELECT 1 FROM dbo.Customer ...)

Calling AI endpoint for query plan analysis on query: 
Statement (parent [dbo].[usp_ProcessCustomerStaging]) - INSERT INTO dbo.Customer ...

Calling AI endpoint for query plan analysis on query: 
Statement (parent [dbo].[usp_ProcessCustomerStaging]) - DECLARE cur CURSOR FOR SELECT ...

Seven separate API calls, each with its own execution plan XML. Will ChatGPT still see the forest through the trees? Each individual statement may seem fine but the problem is all seven working together inside a cursor loop.

The output: 7 rows in the AI results grid. Row 1 (the rolled-up procedure entry) returned NULL for AI Advice. The remaining six returned full analysis.

What ChatGPT Found

Every statement-level analysis, ChatGPT identified the RBAR pattern and produced a complete set-based rewrite of the entire procedure — not just the individual statement it was given. It flagged the cursor, the IF EXISTS, the per-row UPDATE/INSERT, and the staging mark, and explained that they were parts of one problem. The two strongest outputs came from the DECLARE CURSOR statement (Row 7) and the FETCH NEXT statement (Row 6). The full AI Advice is too large to show here, so I've just given you a small piece and saved the full output to separate pages: DECLARE CURSOR analysis and FETCH NEXT analysis. Definitely check them out.

What stands out:

  • It diagnosed the full RBAR pattern from individual statements.

    From a DECLARE CURSOR, ChatGPT identified the per-row IF EXISTS check, the conditional UPDATE/INSERT, and the per-row staging mark. It flagged the full loop and explained why it's inefficient: repeated single-row seeks, context switches, forced serialization, tempdb pressure from the cursor worktable. All of that from one statement. Impressive.

  • It caught the duplicate CustomerID edge case.

    If multiple staging rows exist for the same CustomerID, the cursor processes them in order by StagingID and only the last one wins. The AI Advice output flagged this and used ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY StagingID DESC) to preserve that behavior in the set-based rewrite. A junior developer would miss that entirely. I almost missed it.

  • It produced complete rewrites.

    Chat suggested replacement for everything. Temp table/table variable snapshots of unprocessed staging rows, ROW_NUMBER() deduplication, set-based UPDATE for existing customers, set-based INSERT for new ones, single UPDATE to mark staging rows processed. No cursor, no loop, no variables. It even offered a MERGE alternative!

  • It flagged behavioral differences without being asked.

    GETDATE() in the cursor is evaluated per row; in the set-based version it's evaluated once per statement. ChatGPT flagged that and offered alternatives if per-row timestamps were required. It also noted the atomicity in the transaction and gave me suggestions for reducing blocking if many runs were expected concurrently.

sp_BlitzCache's Findings?

sp_BlitzCache still does the incredible job it always has, flagging the proc for Forced Serialization, three separate cursor warnings, index churn and multiple plans — all symptomatic of RBAR.

One Problem

ChatGPT's analysis was excellent. The logic was correct -- but its replacement procedure wouldn't fire without error because it defined a CTE and then used it in multiple calls beyond the first one. A CTE does not persist for reuse, so all subsequent calls in the proc failed with this:

Msg 208, Level 16, State 1, Procedure dbo.usp_ProcessCustomerStaging, Line 54
Invalid object name 'LatestPerCustomer'.

Five out of six AI Advices included this same flaw. ChatGPT consistently misses CTE scoping rules when writing multi-statement procedures. I may edit my prompts with reminders that the CTEs do not persist.

Fixed this quickly by editing ChatGPT's procedure to include another temp table so both the UPDATE and INSERT can reference it. Same logic, same results, completed successfully.

ChatGPT got the what right and the why right. The code had a bug. This should be a good reminder that we always must review what AI gives back.

The Fixed Rewrite

ChatGPT's logic with the CTE scoping fix applied:

CREATE PROCEDURE dbo.usp_ProcessCustomerStaging
AS
BEGIN
    SET NOCOUNT ON;

    -- Snapshot unprocessed staging rows
    SELECT StagingID, CustomerID, Name, Email, City
    INTO #StgRaw
    FROM dbo.CustomerStaging
    WHERE Processed = 0;

    IF NOT EXISTS (SELECT 1 FROM #StgRaw)
    BEGIN
        DROP TABLE #StgRaw;
        RETURN;
    END

    -- Deduplicate: one row per CustomerID, latest StagingID wins.
    -- Computed once, used for both UPDATE and INSERT.
    SELECT CustomerID, Name, Email, City
    INTO #StgDeduped
    FROM (
        SELECT CustomerID, Name, Email, City,
               ROW_NUMBER() OVER (PARTITION BY CustomerID 
                   ORDER BY StagingID DESC) AS rn
        FROM #StgRaw
    ) AS R
    WHERE R.rn = 1;

    CREATE CLUSTERED INDEX CX_StgDeduped 
        ON #StgDeduped (CustomerID);

    DECLARE @Now DATETIME = GETDATE();

    -- 1) Update existing customers
    UPDATE C
    SET
        C.Name        = D.Name,
        C.Email       = D.Email,
        C.City        = D.City,
        C.LastUpdated = @Now
    FROM dbo.Customer AS C
    INNER JOIN #StgDeduped AS D
        ON C.CustomerID = D.CustomerID;

    -- 2) Insert new customers
    INSERT INTO dbo.Customer (CustomerID, Name, Email, City, LastUpdated)
    SELECT D.CustomerID, D.Name, D.Email, D.City, @Now
    FROM #StgDeduped AS D
    WHERE NOT EXISTS (
        SELECT 1 FROM dbo.Customer AS C
        WHERE C.CustomerID = D.CustomerID
    );

    -- 3) Mark all snapshot staging rows as processed
    UPDATE CS
    SET Processed = 1
    FROM dbo.CustomerStaging AS CS
    WHERE EXISTS (
        SELECT 1 FROM #StgRaw AS P
        WHERE P.StagingID = CS.StagingID
    );

    DROP TABLE #StgRaw;
    DROP TABLE #StgDeduped;
END

Same inputs, same outputs. 5,000 customers in the target, 5,000 staging rows marked processed. Identical results.

The Numbers

Both versions ran against the same dataset: 2,000 existing customers, 5,000 staging rows. SET STATISTICS TIME ON; SET STATISTICS IO ON; for both.

Cursor Set-Based
CPU 1,844 ms 156 ms
Elapsed 5,309 ms 149 ms
STATISTICS IO lines 100,033 67

CPU: 12x reduction. Elapsed: 36x faster. But the STATISTICS IO output tells the real story. The cursor version produced 100,033 lines — a separate entry for every statement on every row. Customer seek, CustomerStaging update, Worktable read, 5,000 times over. The set-based version: 67 lines. A few scans, a few joins, done. Do you love SET-BASED as much as I do?

sp_BlitzCache: Before and After

I ran sp_BlitzCache against both procedures without @AI. The cursor version lit up six warnings and the set-based version, zero. The cursor is gone. The forced serialization is gone. The index churn is gone.

Finding Cursor Set-Based
Forced Serialization Yes No
Cursor Yes No
Dynamic Cursors Yes No
Optimistic Cursors Yes No
>= 5 Indexes Modified Yes No
Multiple Plans Yes No

Wrap Up

I gave ChatGPT an ugly procedure through sp_BlitzCache's refactoring personality. It read the execution plan, flagged the cursor and RBAR patterns throughout, caught an edge case with duplicate CustomerIDs, and produced a set-based replacement with substantially less overhead -- 12x less CPU, 36x faster elapsed.

It also gave me code that didn't compile. Five out of six times.

That is a very important point. AI is a force multiplier, not a replacement. The analysis was fantastic and the rewrite logic was sound, but a CTE scoping bug came with its revision — which I had to correct before it could be used. Trust the analysis. Verify the code.

The more I use this, the stronger the point becomes: the quality of what you get back from AI is completely dependent upon what you give it. So, was it ChatGPT's mistake, or could I have made my AI prompt more CTE-aware in the Blitz_AI config table?

I've spent four posts showing how good it can be with focused, specific prompts. Next, I'm going to see what happens when you give it bad ones. Garbage in = garbage out. Stay tuned.

More to Read

sqlfingers: sp_BlitzCache Got ChatGPT's Advice On My Deadlock
sqlfingers: sp_BlitzCache Can Talk to ChatGPT Now. Here's What It Said.
Brent Ozar: Get ChatGPT's Advice On Your Queries with sp_BlitzCache

Thursday, February 19, 2026

sp_BlitzCache Got ChatGPT's Advice On My Deadlock

In the last post, I tested sp_BlitzCache's ChatGPT integration with index tuning and code review personalities. Both worked well. ChatGPT disagreed with the optimizer on index key order and was right about it. The code review caught a date boundary bug I missed. I ended that post saying I would test the blocking and deadlocking personality next. Here it is.

The Setup

Super simple. One table, two procs with an update and a waitfor.

CREATE TABLE dbo.DeadlockTest (ID INT PRIMARY KEY, Val INT);
INSERT INTO dbo.DeadlockTest VALUES (1, 0), (2, 0);

CREATE PROCEDURE dbo.usp_DeadlockSide1
AS
SET NOCOUNT ON;
BEGIN
    BEGIN TRAN;
    UPDATE DeadlockTest WITH (HOLDLOCK) SET Val = 1 WHERE ID = 1;
    WAITFOR DELAY '00:00:05';
    UPDATE DeadlockTest SET Val = 1 WHERE ID = 2;
    COMMIT;
END
GO

CREATE PROCEDURE dbo.usp_DeadlockSide2
AS
SET NOCOUNT ON;
BEGIN
    BEGIN TRAN;
    UPDATE DeadlockTest WITH (HOLDLOCK) SET Val = 2 WHERE ID = 2;
    WAITFOR DELAY '00:00:05';
    UPDATE DeadlockTest SET Val = 2 WHERE ID = 1;
    COMMIT;
END
GO

Ok. That creates the procedures that may have deadlocking issues... 😉 but I also need to create the new blocking/deadlocking personality for sp_BlitzCache. We get that into the Blitz_AI config table with this:

INSERT INTO DBA.dbo.Blitz_AI (
    Nickname, AI_Model, AI_URL, 
    AI_Database_Scoped_Credential_Name, 
    AI_System_Prompt_Override, 
    Timeout_Seconds, DefaultModel)
VALUES (
    'blocking deadlocking', 
    'gpt-5-mini', 
    'https://api.openai.com/v1/chat/completions', 
    'https://api.openai.com/', 
    'You are a senior SQL Server concurrency specialist. 
     Focus only on blocking, deadlocking, and lock contention. 
     Analyze the query text and execution plan for: lock 
     escalation risks, transaction scope issues, lock ordering 
     problems that cause deadlocks, long-held locks due to 
     WAITFOR or unnecessary work inside transactions, isolation 
     level concerns, and missing or misused lock hints. Do not 
     suggest index changes or query rewrites unless they directly 
     reduce lock duration or contention. For each finding, explain 
     the contention risk and recommend a fix. If two procedures 
     access the same resources in different order, call it out 
     explicitly. Render your output in Markdown.',
    230, 
    0);

Remember, that AI_System_Prompt_Override value is what we are saying to AI, and it must be very specific: lock ordering, transaction scope, escalation, isolation levels. I've also set the timeout to 230 seconds because execution plan XML can be large and ChatGPT needs time to process it. (lesson learned)

Then I ran each proc separately just to get them into the cache. No deadlocks:

EXEC dbo.usp_DeadlockSide1;
EXEC dbo.usp_DeadlockSide2;

And now the deadlock:

-- query window 1
EXEC dbo.usp_DeadlockSide1;
-- query window 2 (within 2 seconds):
EXEC dbo.usp_DeadlockSide2;

The two transactions collide and one is chosen as the deadlock victim. Now we are ready for sp_BlitzCache.

The sp_BlitzCache Call

EXEC dbo.sp_BlitzCache 
    @StoredProcName = 'usp_DeadlockSide1',
    @AI = 1, 
    @AIConfig = 'DBA.dbo.Blitz_AI', 
    @AIModel = 'blocking deadlocking';

It ran for 00:01:53 and output 3 rows for AI with the sp_BlitzCache findings grid:

What ChatGPT Found

The AI Advice column was populated for two of the three rows in the results. This one is from Row 3, the UPDATE DeadlockTest WITH (HOLDLOCK) one. Too large to include the whole thing here, so I've saved it to another page. It's great! Way more than I expected. You really should view the whole page.

What stands out:

  • WAITFOR inside the transaction is the lock amplifier.
    ChatGPT flags the 5-second WAITFOR executing while the transaction is open, holding exclusive locks on the first row for the entire delay. Its top recommendation was to move the WAITFOR outside the transaction. Interesting. And correct. Keep transactions as short as possible.
  • HOLDLOCK is redundant on a singleton PK update.
    The first UPDATE uses WITH (HOLDLOCK). ChatGPT noted that an UPDATE already acquires an exclusive lock on the modified row, and HOLDLOCK is unnecessary for a single-row primary key update. It can impose stricter lock behavior and increases lock duration without benefit. Recommendation: remove it.
  • It inferred the other proc exists.
    This one really impressed me. I only ran sp_BlitzCache against Side 1, so ChatGPT never saw usp_DeadlockSide2 — but from the procedure name and the lock pattern, it deduced the deadlock partner:
    'Stored procedure name usp_DeadlockSide1 suggests there is (or may be) a side 2 that updates ID=2 then ID=1.'
    It then described the full deadlock sequence and recommended enforcing a consistent lock ordering across all procedures that touch the same rows — always update in the same deterministic order (ie., always ID 1 then ID 2).
    That is not generic advice. It read the execution plan, understood the structure, and inferred the concurrency problem from a single proc. I am impressed. Again.

What sp_BlitzCache Found on Its Own

Before ChatGPT ever got involved, sp_BlitzCache flagged the proc with CheckID 50: Long Running Low CPU under the Blocking findings group. That is the WAITFOR DELAY making duration high while CPU stays near zero. That's exactly what real blocking looks like from the plan cache — a query that runs for a long time but does almost no work. sp_BlitzCache caught it, too.

The Point

I purposely built a simple deadlock. One table, two procs with an update and a waitfor. Nothing complex. The question was whether AI would read the execution plan and find anything pertaining to concurrency problems. It did.

And sp_BlitzCache's own findings (CheckID 50 — Long Running Low CPU / Blocking) backed up the AI's analysis independently.

As a test, this took nothing to set up. But looking forward to real use cases, I can see big opportunity. I will reiterate what Brent said before: The quality of whatever you get back from AI is completely dependent upon how smart you write the prompt.

Next, I will test a refactoring personality — give ChatGPT a big and busy multi-step, row-by-row query and see if it can rewrite it as a set-based operation. Stay tuned.

More to Read:

sqlfingers: sp_BlitzCache Can Talk to ChatGPT Now. Here's What It Said.
Brent Ozar: Get ChatGPT's Advice On Your Queries with sp_BlitzCache

Wednesday, February 18, 2026

sp_BlitzCache Can Talk to ChatGPT Now. Here's What It Said.

In a previous post, I got sp_BlitzCache to call ChatGPT. It was super basic. I just performed a call to AI from SQL Server to see how it's done. Worked well. No surprises. Now I've stepped it up a bit and gave it something real to do with index tuning and a code review. Worked well again, but there was a surprise — ChatGPT and SQL Server did not agree.

The Idea

Brent's sp_BlitzCache ChatGPT walkthrough includes a dbo.Blitz_AI config table with an AI_System_Prompt_Override column that lets us change sp_BlitzCache personalities on the fly. Think of the personalities as different AI Models. By changing the personality, we tell sp_BlitzCache what to do:

Personality What It Does
Index tuning only Indexes are the only focus — no query rewrites
Query tuning only Rewrites only — no index changes
Both Index and query changes allowed
Code review smelly T-SQL — not performance related
Blocking / deadlocking Looking for contention problems
Refactoring Rewrite multi-step/rbr query for more efficiency

My Test

I built a 1M-row dbo.Orders table with a clustered PK, a 400-row dbo.Regions lookup, and a usp_OrderSummaryByRegion stored procedure that joins the two tables with a filtered aggregation. Deliberately undertooled — no nonclustered indexes anywhere.

Brent's post covers the setup details, so I won't repeat that here. These are the calls I used to test index tuning and code review against the same stored procedure — with very interesting results:

-- Call 1: Index tuning only - no changes, just advice
EXEC sp_BlitzCache @StoredProcName = 'usp_OrderSummaryByRegion',
    @AI = 1, @AIConfig = 'DBA.dbo.Blitz_AI', @AIModel = 'index tuning';

-- Call 2: Code review only
EXEC sp_BlitzCache @StoredProcName = 'usp_OrderSummaryByRegion',
    @AI = 1, @AIConfig = 'DBA.dbo.Blitz_AI', @AIModel = 'code review';

Round 1: Index Tuning

Ran for 47 seconds, output 2 rows; one for the CREATE PROCEDURE (no advice) and one for the SELECT inside the procedure - with Missing Indexes and AI Advice:

What SQL Server said

The Missing Indexes column held the Optimizer's missing index suggestion with estimated improvement:

Keyed on OrderDate, to include OrderTotal and RegionID. 75% estimated improvement. That is what the optimizer always does — it sees a predicate, builds a key around it, and throws the rest into INCLUDE. Not wrong. But not thinking very hard, either.

What ChatGPT said

This is the AI Advice column output from the query results:

Same table, same stored procedure, but AI's suggested index is different from the Optimizer's.

ChatGPT recommended RegionID, OrderDate as the key; RegionID first, followed by OrderDate, with OrderTotal in the INCLUDE. Then it explained why: the plan shows Regions filtered to ~66 rows by Country, so with RegionID leading, the engine can seek per RegionID for the date range. It estimated a 70-95% reduction in logical reads and called out that CPU, duration, and memory grant would all drop substantially. That is really good.

It also acknowledged SQL Server's suggestion as a valid alternative, explained the trade-off (date-first is better when the date range is very selective and the region set is large), and gave both CREATE INDEX scripts so you could choose:

-- AI's preferred recommendation
CREATE NONCLUSTERED INDEX IX_Orders_RegionID_OrderDate_Includes
    ON dbo.Orders (RegionID ASC, OrderDate ASC)
    INCLUDE (OrderTotal);
GO

-- Alternative (matches the optimizer's missing index hint)
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate_Includes
    ON dbo.Orders (OrderDate ASC)
    INCLUDE (RegionID, OrderTotal);
GO

No query rewrites or passive suggestions. It stayed in its lane. I told it to index tune and it did exactly that very effectively. I am impressed. Even with Chat's index suggestion being different from the Optimizer's — it provided reasoning behind both!

Round 2: Code Review

Same proc, different personality. First try, it ran for 37 seconds and completed with this in AI Advice column:

Error calling AI service: The external rest endpoint execution exceeded the timeout value.

I edited the Timeout_Seconds in Blitz_AI from 30 to 230 seconds and ran again. It ran for 00:01:07 and completed with Missing Indexes and AI Advice. The Missing Indexes output was still the Optimizer's standard 'missing index' suggestion. The AI Advice column was pure code review. Too big to screenshot the entire output here, so I've posted the full AI Advice output on a separate page. Look at the detail it provided! I love how it opens with compliments on my 'clear structure' and readability, and then slams me with 5 issues to address and gives me a proposed revision.

Three things it flagged:

1. EndDate boundary semantics

The WHERE clause uses o.OrderDate >= @StartDate AND o.OrderDate < @EndDate. ChatGPT asked if that was intentional and reminded me that as-is, the EndDate was completely excluded. If I passed '2025-06-30' expecting results to include June 30, they would not be there. It suggested an alternative and told me to document the intention to 'avoid further confusion'. This is the kind of bug that lives in production for years unchecked, because the results look 'close enough'.

2. Parameter data type alignment

The proc declares @Country VARCHAR(50), and ChatGPT told me that if Regions.Country is a different length or type (ie., CHAR(2)), there is risk for implicit conversion. Same thing with @StartDate and @EndDate. I've checked the procedure and the tables -- all data types align, so I'm not sure what prompted this. Maybe just a safety warning? Unsure, but it is a fair flag. Implicit conversions can be costly and I wouldn't have wanted to miss this.

3. Missing input validation

No NULL checks, no range validation, no TRY/CATCH. In ChatGPT's words, "The procedure immediately executes the query without validating inputs". Chat's suggested revision includes the necessary checks with RAISERROR for bad inputs. All I can say is good catch, and I'm surprised that I missed that. Really.

The Point

Same proc, two different personalities. ChatGPT told me to index (RegionID, OrderDate) INCLUDE (OrderTotal) and explained why SQL Server's own suggestion had the key order backwards. Chat's code review also warned that my parameter types might not match my columns and my date boundary might be excluding a day.

That is the power of a constrained prompt. You are not asking 'what do you think?' — you are saying 'you are an index specialist' or 'you are a code reviewer' and letting a focused role produce focused output. Brent was right: the quality of the advice is directly proportional to the work you put into the prompt.

This is going into my toolkit.

Try It

If you have sp_BlitzCache and an OpenAI key, this takes about 20 minutes to set up. Start with Brent's walkthrough for the config table and credentials, pick one of your own problem procs, and test it with the different personalities Brent has coded it for. I will be testing 'Reduce blocking and deadlocking' next.

More to Read:

sqlfingers: sp_BlitzCache Can Talk to ChatGPT Now. Here's How.
Brent Ozar: Get ChatGPT's Advice On Your Queries with sp_BlitzCache
Brent Ozar: Building AI Calls into the First Responder Kit

Tuesday, February 17, 2026

SSMS Copilot Code Completions. Some of it Was Brilliant.

SSMS 22.2 shipped last week with GitHub Copilot code completions. I don't mean the chat window. I mean inline completions. You type, it finishes — comments and code. I'll agree with Erin Stellato in that they exceed the concept of "IntelliSense on steroids".

In December I made peace with Copilot, but trust is another thing entirely. So yesterday I began testing it with four simple DBA tasks, grading each based on the results. The tool both impressed me and concerned me in places I did not expect.

Before You Start

Code completions require SSMS 22.2.1 or later. If you're on 22.1, you have Copilot Chat but not completions — and nothing will happen when you type. Save some time and check Help\About first. You also need a GitHub account with Copilot access. The free tier gives you 2,000 completions per month. Install walkthrough is here.

How It Actually Works

This isn't a chatbot. You type something into the query editor, pause, and gray ghost text appears. Press Tab to accept. Then Enter, pause, Tab again. Completions come in chunks — a line or two at a time, not the whole query at once. The rhythm is Tab-Enter-pause-Tab-Enter-pause. It's a little clunky at first, but it doesn't take long to find the groove.

Two things that aren't obvious until you sit down with it: Copilot reads your connected database schema to make suggestions, and it seems to read every open query tab for context. Both of those will matter below.

The Report Card

Test 1: The Comment Prompt — Grade: B-

I typed this comment and let Copilot build the query:

-- show all user databases ordered by size in MB

After I typed 'MB' — before I hit Tab or Enter — Copilot suggested SELECT in gray beneath the comment. I then hit Tab to accept, Enter to advance, next chunk appears. Tab, Enter, Tab, Enter — each round added a line or two until I had this:

-- show all user databases ordered by size in MB
SELECT
    name AS DatabaseName,
    size * 8 / 1024 AS SizeMB
FROM sys.master_files
WHERE type_desc = 'ROWS' 
    AND database_id > 4
GROUP BY name, size
ORDER BY SizeMB DESC;

It ran very fast and completed without error — but the results are wrong. The query filters to type_desc = 'ROWS', which means only the data files. Log files are excluded. So 'SizeMB' isn't the database size, it's the data file size. I asked for database size and received only the size of the data files.

I ran the same test a second time. Same comment, same instance, different query window. This time it dropped the GROUP BY and the system database filter - which returned all system databases as well. Same input, different output, still wrong.

Then I closed all query windows and opened up a new one. I typed only "-- l" and Copilot returned this:

-- list all databases and their sizes in MB, sorted by size descending
SELECT 
	d.name AS DatabaseName,
	SUM(m.size) * 8 / 1024 AS SizeMB
FROM
	sys.master_files m INNER JOIN sys.databases d ON m.database_id = d.database_id
WHERE d.database_id > 4    
GROUP BY d.name
ORDER BY SizeMB DESC;

There it is! That one is correct! And I got the 'sorted by size descending' in the comments and output as a nice add-in.

This is really very interesting. I honestly was going to type 'list...' rather than the 'show...' that I used in the first two tries. How did Copilot interpret 'l' as 'list' ? I don't know that yet, but this may suggest it's a matter of usage. Maybe as I learn it, I will have more control over the unexpected results? Anyhoo... Third time is the charm. I'll give it a B-.

Test 2: The Schema Finish — Grade: C

I started typing a SELECT and paused briefly. Copilot completed the line:

SELECT name, database_id,

I hit Tab to accept, then Enter, and Copilot returned an unrequested CASE statement remapping state_desc values — 'ONLINE' to 'Online', 'SUSPECT' to 'Suspect' — formatting values that are already human-readable:

But why? I backed out, tried again, and got something completely different:

SELECT name, database_id,
    CAST(size * 8 / 1024 AS DECIMAL(10,2)) AS SizeMB,
    state_desc
FROM sys.master_files
WHERE type_desc = 'ROWS'
    AND database_id > 4
ORDER BY SizeMB DESC;

The CASE disappeared. The query simplified itself. It also borrowed heavily from the query we ran in Test 1. Is Copilot reading the open query windows or referencing previous calls for context? Hard to say just yet.

Graded a C. The final output was usable, but the query path wasn't repeatable.

Test 3: The Boilerplate Machine — Grade: C-

I typed two words twice. First with my other query windows still open, then with them all closed.

BEGIN TRY

With other query tabs open:

BEGIN TRY
    BEGIN TRANSACTION
        CREATE DATABASE kgkwobci;
        USE kgkwobci;
        CREATE TABLE dbo.SampleTable (
            ID INT PRIMARY KEY,
            Name NVARCHAR(50),
            CreatedDate DATETIME DEFAULT GETDATE()
        );
    COMMIT TRANSACTION
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION
    RAISERROR('An error occurred while creating the database and table.', 16, 1);
END CATCH

I typed 'BEGIN TRY' and Copilot invented a database called kgkwobci and wrote a deployment script nobody requested. Creative, I guess. So I tried it again.

With all other tabs closed:

BEGIN TRY
    EXEC sp_WhoIsActive
        @filter = 'login_name',
        @filter_type = 'LIKE',
        @not_filter = 'sa',
        @show_own_spid = 1,
        @show_sleeping_spids = 1,
        @get_full_inner_text = 1,
        @get_plans = 1,
        ...
END TRY
BEGIN CATCH
    SELECT
        ERROR_NUMBER() AS ErrorNumber,
        ERROR_SEVERITY() AS ErrorSeverity,
        ERROR_STATE() AS ErrorState,
        ERROR_PROCEDURE() AS ErrorProcedure,
        ERROR_LINE() AS ErrorLine,
        ERROR_MESSAGE() AS ErrorMessage;
END CATCH;

Now that is interesting. Copilot reached into my instance, found sp_WhoIsActive installed there, and generated a full execution with nearly every parameter. I didn't ask for monitoring — I just typed 'BEGIN TRY.'

This tells me that Copilot can write solid error handling and it is scanning my installed stored procedures, not just system objects, to generate output. Again, hard to say why just yet. Clearly I need to read up a bit and strengthen my familiarity.

C- because the TRY CATCH was genuinely good error handling. I just don't know how I got there.

Test 4: The Danger Zone — Grade: C+

I connected to my BlitzTest database with an Orders table with one million rows, and typed this:

-- delete all records older than 90 days

Before I even pressed Enter, Copilot appended from dbo.Orders to the comment line. It derived the schema name and table name on its own. I hit Tab then Enter, and produced this:

-- delete all records older than 90 days from the Orders table
DELETE FROM dbo.Orders
WHERE Created < DATEADD(DAY, -90, GETDATE());

Schema-aware. Correct column name. Correct date math. Syntactically perfect.

Operationally reckless.

No transaction. No batching with TOP. No row count check. No 'are you sure?' safety check. Just a straight, single-transaction DELETE against a million-row table. A DBA with quick fingers and a Tab key could fire that on a production server before their coffee kicks in. If the log file can't absorb a million-row delete in one shot, we may have a problem.

Copilot knew the table, the column, and the syntax. It did exactly what I told it to do. So again, maybe this is usage. The savvy DBA is going to ask for the necessary safety measures when talking to Copilot.

And I really dig how it derived all of that by itself.  I gave it a C+.

The Scorecard

Test Grade Notes
Comment Prompt B- Fast. Ran clean. Results were wrong.
Schema Finish C Overbuilt, then simplified. Borrowed from open tabs.
Boilerplate C- Good structure. Unexpected content.
Danger Zone C+ Correct syntax. No safety net.
Overall C

The Verdict

I am keeping Copilot's code completions turned on. They're fast, they're schema-aware, and for the stuff you already know how to write but don't feel like typing, they save real time. This is a tool worth learning.

But it's still the intern from December. It hands you something that's 70% there. The code will compile and even run. Whether you should run it just yet — that's your call. There is a learning curve and no room for haste. A quick Tab-Enter on the wrong suggestion could go the wrong way fast.

Learn the tool. Use the tool. But read every line before you hit Execute.

More to Read:

Announcing GitHub Copilot Code Completions in SSMS 22.2.1 — Erin Stellato
Code Completions — GitHub Copilot in SSMS — Microsoft Learn
I Just Don't Understand Why You Don't Update SSMS — Brent Ozar
Get Started with GitHub Copilot in SSMS — Microsoft Learn

Monday, February 16, 2026

SQL Server Web Edition Discontinued. Now What?

SQL Server 2025 dropped Web Edition. No deprecation or transition period.  It's just gone. SQL Server 2022 was the last version to include it. If you're a hosting provider, an ISV, or anyone who built on Web Edition's lower price point through SPLA — you need a plan.

What Happened

SQL Server 2025 ships with four editions: Enterprise, Standard, Express, and Developer. Web Edition is not on the list. Microsoft didn't deprecate it with a vague 'future release' warning. They just dropped it.

The Web Edition existed since 2008, specifically to serve internet-facing workloads at a fraction of Standard Edition pricing. Hosting providers and SPLA partners built entire service tiers around it — and that licensing tier is now gone.

What You're Left With

Option What You Get The Catch
Stay on SQL Server 2022 Web Supported until January 2033 No 2025 new features. SPLA availability beyond 2033 is unspecified.
Move to Standard Edition Full 2025 feature set, up to 32 cores, 256 GB memory Significant cost increase. Per-core licensing adds up fast.
Move to Express Edition Free. Now supports 50 GB databases (up from 10 GB) Single CPU. 1 GB memory for the buffer pool. No SQL Agent.
Move to Azure SQL Database Fully managed, elastic scaling Requires architecture changes. Ongoing consumption cost.

Who This Hurts

SPLA partners and hosting providers — this is the big one. If you license SQL Server through SPLA to offer shared or dedicated hosting, Web Edition was your bread and butter. Standard Edition pricing changes the math on every hosting plan you sell. You can keep offering 2022 Web for now, but you're selling a product with a hard expiration date.

Small SaaS shops — if your product runs on Web Edition because Standard was overkill and Express was too small, you're in the gap. The good news is Express jumped from 10 GB to 50 GB in 2025. The bad news is the single CPU and 1 GB buffer pool limits haven't changed. If your workload fits in that box, great. If not, you need to consider paying for Standard Edition.

Anyone planning a 2025 upgrade — if your upgrade path assumed Web Edition would carry forward, stop and re-evaluate now. That edition doesn't exist anymore.

The Real Question

2033 sounds like a long time, but it's not. That's one hardware refresh cycle and maybe two budget cycles before 'we'll deal with it later' turns into now. If you're running Web Edition today, the decision isn't whether to move — it's when, and to what.

The 50 GB Express limit is genuinely useful for small databases. Standard Edition's expanded limits (32 cores, 256 GB memory) make it more palatable than it used to be, and Azure SQL is the obvious play if you're already cloud-adjacent — but each of these paths have licensing, architecture, and costs that need to be ironed out before you can make this decision.

Quick Check: Know What You're Running

SELECT
    SERVERPROPERTY('Edition')          AS CurrentEdition,
    SERVERPROPERTY('ProductVersion')   AS SQLVersion,
    SERVERPROPERTY('ProductLevel')     AS PatchLevel;

If that comes back with 'Web Edition' — it's time to start the conversation.

Need Help Figuring This Out?

This is what I do. Whether you need a licensing assessment, a migration plan from Web to Standard, or help evaluating whether Express or Azure SQL fits your workload — let's talk. It's better to plan this now rather than triage it later. 😉

More to Read:

What's New in SQL Server 2025 — Microsoft Learn
SQL Server 2025 Licensing: Key Changes and Updates — SAMexpert
SQL Server Web Edition Is Being Discontinued — SiteHost

Sunday, February 15, 2026

sp_BlitzCache Can Talk to ChatGPT Now. Here's How.

Last week, Microsoft's AI chief, Mustafa Suleyman, told Fortune that 'Most tasks that involve "sitting down at a computer" will be fully automated by AI within the next year or 18 months'. He also said "It is going to be possible to design an AI that suits your requirements for every institution, organization, and person on the planet."

I read that on Friday. Yesterday I installed the latest First Responder Kit on my SQL Server 2025 instance, and I spent today trying to get sp_BlitzCache to talk to ChatGPT.

What Brent Shipped

The newest First Responder Kit (version 8.29) added AI integration to sp_BlitzCache. The procedure that has been helping DBAs find expensive queries for over a decade can now send those queries to an AI model and get tuning advice back — all from inside the engine, using sp_invoke_external_rest_endpoint. Here I am only testing with the top 1 query plan to keep things light:

-- this one calls ChatGPT (or Gemini) for you with the query plan in question
-- scroll to 'AI Advice' to see how ChatGPT assesed your query plan
EXEC sp_BlitzCache @Top = 1, @AI = 1

-- this one only generates the call prompt for you
-- scroll to 'AI Prompt' to see what you can ask your AI about the pain points
EXEC sp_BlitzCache @Top = 1, @AI = 2

In short, when @AI = 1, sp_BlitzCache loops through every expensive query it finds and builds an AI prompt for each one. Just like before, sp_BlitzCache outputs the query text, the execution plan XML, and a full set of performance metrics: CPU, duration, reads, writes, memory grants, spills, execution count, and any warnings the procedure already detected. What's new is that all of this gets packed into a JSON payload and sent to the AI endpoint via sp_invoke_external_rest_endpoint, where the AI's response comes back to you in the ai_advice column in the result set. No more cut/paste/edit/askChatGPT. The sp_BlitzCache call is doing the legwork for you.

When @AI = 2, the same expensive query work is done, but it only generates the prompt without calling the AI. This lets you review what would be sent first, or you can even paste it into another AI tool.

Very thoughtful, practical engineering -- regardless of where you stand with AI.

The Setup

Brent's full walkthrough covers this in detail. Here is the short version of what I had to do:

-- Enable outbound REST calls (disabled by default):
EXEC sp_configure 'external rest endpoint enabled', 1;
RECONFIGURE WITH OVERRIDE;
GO

-- Credentials must live in a user database, not master.
-- The credential name must be the provider's root URL exactly.
USE DBA;
GO
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'SomeStrongPassword!123';
GO
CREATE DATABASE SCOPED CREDENTIAL [https://api.openai.com/]
WITH IDENTITY = 'HTTPEndpointHeaders',
SECRET = '{"Authorization":"Bearer YourChatGPTAPIKeyGoesHere"}';
GO

One note on that credential: the CREATE ran without error, but my first sp_BlitzCache @AI = 1 call came back instantly - also without error - but this was in the ai_advice column:

API Error: Incorrect API key provided: sk-proj-***...***8pgA. You can find your API key at https://platform.openai.com/account/api-keys.

The SECRET value needs to be exactly {"Authorization":"Bearer <key>"} — there is a space after 'Bearer' and no extra whitespace around the key itself. Once I recreated the credential with the correct API Key string, the sp_BlitzCache @AI = 1 call took more time to complete and provided this insight in the AI Advice column:

About That Prediction

Mr. Suleyman said it will be possible to design an AI for every institution, organization and person on the planet. But every call sp_BlitzCache makes to the AI is stateless — a fresh conversation with no memory of anything before it. That is not a blitzCache flaw. That is AI. The model cannot know the query ran poorly last Tuesday, that you added an index on Wednesday, or that the column it wants you to index is on its way out. It can get the plan and the metrics - which is great! - but there is no way it can access that knowledge the DBA has to support it.

A DBA carries years of context that no prompt can contain — why the schema is what it is, which index exists because of an outage 3 years ago, or which stored procedure cannot be rewritten for political reasons. AI sees the query. The DBA knows the story.

What I Actually Think

Brent built something genuinely useful here. sp_BlitzCache calling ChatGPT gave me tuning suggestions I would have arrived at eventually — but much faster, and with a couple of angles I hadn't even considered. That is real value.

But getting there required configuration, credentials, network paths, security review, and a clear understanding of what the AI can and cannot see. That work is not going away. If anything, features like this create more of it — and it is work that requires a human who knows the environment.

The robots are not taking the job. They are adding to it.

Next up, I want to test the custom system prompts Brent outlined — code review, index-only tuning, blocking analysis. If you've already set this up in your environment, I'd like to hear what you found. Please let me know.

More to Read:

Brent Ozar: Get ChatGPT's Advice On Your Queries with sp_BlitzCache
Microsoft Learn: sp_invoke_external_rest_endpoint (Transact-SQL)
Fortune: Microsoft AI chief gives it 18 months — for all white-collar work to be automated by AI

Saturday, February 14, 2026

Microsoft's AI Chief Says Your Job Ends in 18 Months. SQL Server 2025 Disagrees

This week, Microsoft AI CEO Mustafa Suleyman told the Financial Times that most tasks that involve “sitting down at a computer” will be fully automated by AI within the next year or 18 months.   As “compute” advances, he said, models will be able to code better than most human coders.

This is the same Microsoft that, three months ago, shipped SQL Server 2025 — the most feature-dense release in a decade. A release that added an entirely new category of things that DBAs, developers, and data engineers need to learn, manage, secure, and troubleshoot.

Same company. I wonder if the left hand knows what the right hand is doing.

Let's Take Him at His Word

Suleyman says AI will automate the work of anyone sitting at a computer. SQL Server 2025 just shipped the following — every one of which requires a human being sitting at a computer:

Native vector data type and DiskANN indexing. Your developers are going to start storing embeddings in SQL Server. Someone needs to size the indexes, figure out why VECTOR_SEARCH is slow, and explain to management why the new 'AI column' doubled the storage footprint. That someone is you.

CREATE EXTERNAL MODEL. You can now define and call AI models from inside the engine using T-SQL. Which means someone has to govern which models are callable, manage the creds, handle endpoint failures, and explain to the security team why SQL Server is making outbound HTTPS calls to Azure OpenAI. That someone is also you.

AI_GENERATE_EMBEDDINGS and AI_GENERATE_CHUNKS. Building RAG patterns inside the database engine. This is compute-intensive work running on your SQL Server — the same SQL Server hosting your OLTP workloads. Who is configuring Resource Governor to keep it from stomping production?  Still you.

Change Event Streaming. Real-time data streaming from SQL Server to Azure Event Hubs, built on the transaction log. When the log blows up because nobody tuned the throughput settings, the 3 AM page goes to a human.

sp_invoke_external_rest_endpoint. The database engine can now call any HTTPS endpoint. Directly. From T-SQL. This one deserves a closer look.

Your SQL Server Is Calling the Internet Now

This is the single most significant new attack surface, integration point, and troubleshooting headache that shipped in SQL Server 2025. It is also, genuinely, the most powerful.

The feature is disabled by default. Here is how to turn it on:

EXEC sp_configure 'external rest endpoint enabled', 1;
RECONFIGURE WITH OVERRIDE;

And you will need to grant the permission explicitly:

GRANT EXECUTE ANY EXTERNAL ENDPOINT TO [YourAppLogin];

Here is what a basic call looks like. This hits a public API and returns the result as JSON:

DECLARE @ret INT;
DECLARE @response NVARCHAR(MAX);

EXEC @ret = sp_invoke_external_rest_endpoint
    @url     = N'https://httpbin.org/get',
    @method  = 'GET',
    @response = @response OUTPUT;

SELECT 
    @ret AS return_code,
    JSON_VALUE(@response, '$.result.origin') AS caller_ip;

That works. It is clean. It is powerful. And it just made your SQL Server a REST client.

Now here is what happens when the endpoint is slow:

-- Timeout defaults to 30 seconds.
-- Your transaction is waiting on an external service.
-- Your locks are held.
-- Your users are waiting.
-- Nobody told the DBA this was in the stored procedure.

EXEC @ret = sp_invoke_external_rest_endpoint
    @url     = N'https://some-slow-vendor-api.com/v1/data',
    @method  = 'POST',
    @payload = @requestBody,
    @timeout = 120,  -- two minutes, holding locks
    @response = @response OUTPUT;

When that vendor API goes down at 2 PM on a Tuesday and every transaction in the application starts stacking up behind it — that is not an AI problem. That is a DBA problem. You will be the one tracing it in sys.dm_exec_requests, finding the blocking chain, and explaining to the application team why calling a third-party API inside a transaction may not be the best idea.

This feature also requires HTTPS with a certificate trusted by the host OS, uses DATABASE SCOPED CREDENTIAL for authentication, and needs its own security review for every endpoint your developers want to call. That is governance work. Human governance work.

The Math

Suleyman says 18 months until the work is gone. His own product team just shipped five major feature categories — each one creating new DBA work that didn't exist six months ago. New data types to understand. New indexes to tune. New external dependencies to monitor. New security surface area to lock down. New resource consumption to govern.

The robots are not taking the jobs. They are becoming the job.

More to Read:

Fortune: Microsoft AI chief gives it 18 months — for all white-collar work to be automated by AI
Microsoft Learn: sp_invoke_external_rest_endpoint (Transact-SQL)
Microsoft Learn: What's New in SQL Server 2025
Microsoft Learn: Vector Search and Vector Index in SQL Server
Microsoft: SQL Server 2025 is Now Generally Available

Wednesday, February 11, 2026

Microsoft Is Killing NTLM. Here's What That Means for SQL Server.

Every blog post about Microsoft killing NTLM is written for Windows admins and security teams. Why aren't we talking about what this means for SQL Server?

There is a good chance this message has been sitting in your SQL Server error log for years:

The SQL Server Network Interface library could not register the 
Service Principal Name (SPN) [ MSSQLSvc/YOURSERVER.domain.com:1433 ] 
for the SQL Server service. Windows return code: 0xffffffff, state: 63. 
Failure to register a SPN might cause integrated authentication to use 
NTLM instead of Kerberos. This is an informational message. Further 
action is only required if Kerberos authentication is required by 
authentication policies and if the SPN has not been manually registered.

'Informational message' and 'Further action is only required if Kerberos authentication is required'... For years, we could ignore this, but not anymore. Microsoft published a three-phase roadmap on January 28, 2026 to disable NTLM by default in upcoming Windows releases -- and phase one is already live. That 'informational message' is about to become an action-item.

In most environments I audit, somewhere between 40 and 60 percent of Windows Authentication connections to SQL Server are running on NTLM -- and nobody knew until we looked. If you have not checked yours, now is the time.

What Is Happening

NTLM (New Technology LAN Manager) has been around since 1993. Kerberos replaced it as the preferred Windows authentication protocol over 20 years ago. But NTLM stuck around as the silent fallback -- the thing Windows quietly uses when Kerberos didn't work. Microsoft formally deprecated it in June 2024. Now they are moving to disable it.

Based on what Microsoft has announced so far, here is where things stand:

Phase When What Happens
1 Now Enhanced NTLM auditing in Server 2025 and Win 11 24H2
NTLMv1 enforce October 2026 BlockNTLMv1SSO default flips from Audit to Enforce
2 H2 2026 IAKerb, Local KDC ship; core Windows components prefer Kerberos
3 Next major Server release (no date announced) Network NTLM disabled by default

Important note: The October 2026 date applies to NTLMv1 only. Most SQL Server environments are already using NTLMv2, so that specific flip may not break anything for you immediately. The bigger change is Phase 3, where network NTLM (including v2) gets disabled by default. Microsoft has not announced a firm date for Phase 3, but it will coincide with the next major Windows Server release, which could be 2027 or later.

That said, the change is happening and this is not a fire drill. Now's the time to figure out where things stand with your SQL Servers.

Why Is This a SQL Server Problem?

SQL Server does not control which authentication protocol is used. Windows does. When a client connects using Windows Authentication over TCP/IP, the SSPI layer tries Kerberos first. If Kerberos can't work -- missing SPN, broken delegation, no domain controller visibility -- it falls back to NTLM. Silently. No error. No warning. Your connections work fine, and you never know the difference.

Until NTLM gets turned off. Then those connections stop working -- less silently.

Here are the scenarios where SQL Server silently falls back to NTLM:

Missing or misconfigured SPNs

If the Service Principal Name for your SQL instance is not registered in Active Directory -- or is registered against the wrong account -- Kerberos fails and NTLM takes over. This is the most common cause. Named instances with dynamic ports are especially prone to this.

Named Pipes connections

Named Pipes forces NTLM. Always. There is no Kerberos path for Named Pipes connections to SQL Server.

Connecting by IP address instead of hostname

If your connection strings or application configs use an IP address instead of a fully qualified domain name, Kerberos cannot resolve the SPN. NTLM takes over.

Linked servers with Windows Authentication

The classic double-hop problem. My favorite. 🙄 If Kerberos delegation is not configured for your service account, linked server queries using Windows Authentication will fail with 'Login failed for user NT AUTHORITY\ANONYMOUS LOGON'.

Local connections

Connections from the SQL Server host to itself (ie., SSMS on the server, local jobs, local apps) always use NTLM due to a per-service SID hardening feature added in Windows 2008. This is by design and won't change.

SSRS, SSIS, SSAS

These services have their own authentication paths and their own SPN requirements. If they are not configured for Kerberos, they are using NTLM.

Legacy drivers

The old OLE DB provider (SQLOLEDB) and older ODBC drivers may force NTLM in certain configurations, particularly with Named Pipes. If you have not updated to MSOLEDBSQL or ODBC Driver 17/18, it's worth checking.

Find Out What You Are Running Right Now

Run this from a client machine (not from the server itself -- local connections always show NTLM):

SELECT 
    s.login_name,
    s.host_name,
    c.auth_scheme,
    c.net_transport,
    c.encrypt_option,
    s.program_name,
    c.connect_time
FROM sys.dm_exec_connections c JOIN sys.dm_exec_sessions s
  ON c.session_id = s.session_id
WHERE s.is_user_process = 1
AND c.net_transport <> 'Shared memory'
ORDER BY c.auth_scheme, s.login_name;

If your auth_scheme column shows NTLM for remote connections, those are the connections you need to investigate. Run this to see how big the problem is:

SELECT 
    c.auth_scheme,
    COUNT(*) AS connection_count
FROM sys.dm_exec_connections c JOIN sys.dm_exec_sessions s
  ON c.session_id = s.session_id
WHERE s.is_user_process = 1
AND c.net_transport <> 'Shared memory'
GROUP BY c.auth_scheme
ORDER BY connection_count DESC;

If you are running Windows Server 2025 or Windows 11 24H2, you can also check the new NTLM audit logs:

-- Run this on the Windows host via PowerShell (not T-SQL)
Get-WinEvent -LogName "Microsoft-Windows-NTLM/Operational" | Format-List

Event IDs 4020 and 4021 log client-side NTLM attempts. Event IDs 4022 and 4023 log server-side NTLM authentication. Event ID 4024 logs NTLMv1-derived credential usage specifically.

What To Do About It

1. Audit your connections. Run the DMV queries above across your environment. Get a picture of how many connections are using NTLM vs Kerberos. If you see NTLM on remote connections, you have work to do.

2. Fix your SPNs. This solves the majority of NTLM fallback in SQL Server. Verify SPNs are registered correctly for every instance -- default, named, clustered, and AG listeners. Use setspn -L domain\service_account to list what is registered. Use setspn -S to add missing ones. Microsoft's Kerberos Configuration Manager can help identify gaps.

3. Fix dynamic ports. Named instances with dynamic ports are an SPN headache. Consider switching to static ports, or ensure the service account has permission to register and unregister SPNs on startup.

4. Fix your connection strings. If anything is connecting by IP address, change it to the FQDN. This one change can flip a connection from NTLM to Kerberos without touching Active Directory.

5. Stop using Named Pipes for remote connections (if you can). TCP/IP with a valid SPN gives you Kerberos. Named Pipes never will.

6. Update your drivers. SQLOLEDB is deprecated and does not support TLS 1.3 or modern authentication. You should consider replacing it with MSOLEDBSQL and updating your ODBC drivers to version 17 or 18. While older drivers don't force NTLM on their own over TCP, they are end-of-life and don't support TLS 1.3, which Windows Server 2025 now enables by default. Updating your drivers now would address multiple security changes at once.

7. Test with NTLM off. When you are ready, Microsoft recommends testing NTLM-off configurations in non-production environments. The Group Policy setting Network security: Restrict NTLM: Outgoing NTLM traffic to remote servers lets you block outbound NTLM traffic systematically. Start in audit mode, review the results, and work through the exceptions before enforcing.

Microsoft is giving everyone time to prepare -- use it. The phase-out is structured so you can audit now, remediate at your own pace, and be ready well before NTLM gets disabled by default. The work itself is straightforward, but it takes time to audit, test, and remediate across an entire environment -- especially if you have dozens of instances, linked servers, and legacy applications connecting via Windows Auth.

If you are not sure where your environment stands, or you don't have the bandwidth to audit and remediate this across your SQL Server estate, we can help. Better to know now than to find out later.

More to Read:

Microsoft: Advancing Windows Security -- Disabling NTLM by Default
Microsoft Support: Upcoming Changes to NTLMv1 in Windows 11 24H2 and Server 2025
Microsoft Learn: Determine the Authentication Type for SQL Server Connections
Microsoft Learn: Using Kerberos Configuration Manager for SQL Server
Microsoft: Understanding Kerberos and NTLM Authentication in SQL Server Connections

Tuesday, February 10, 2026

Fabric in Practice: SQL Server Near Real-Time Reporting

In Part 1 we covered what Fabric is. In Part 2 we walked through how it's structured (OneLake, Lakehouses, Warehouses), and in Part 3 we looked at how data gets ingested into Fabric.   Now let's put it to work.

THE SCENARIO

You have a SQL Server running transactional workloads. It does its job well enough, but now leadership wants a dashboard that reflects current state -- aka, what's happening right now.

You already know the options. Run heavy reporting queries against production. eewgh. Or stand up a reporting replica, build ETL to keep it current, maintain a refresh schedule, and hope nothing breaks on a holiday weekend. It works, but it's expensive and has an awful lot of moving pieces.

Fabric gives you a third path: continuously replicate your SQL Server data into OneLake using Fabric Mirroring, and let Power BI read it using Direct Lake mode. Your SQL Server stays focused on OLTP and your reporting runs against a near real-time copy in Fabric. No pipelines. No refresh schedules. Nice.

Important note on 'Mirroring'

SQL Server Database Mirroring <> Fabric Mirroring

Database Mirroring was deprecated in 2012. Fabric Mirroring is a completely different technology.  In Microsoft's words: "Mirroring in Fabric is a low-cost and low-latency solution to bring data from various systems together into a single analytics platform."

HOW IT WORKS

Three components make this scenario work:

1. Fabric Mirroring (SQL Server → OneLake)

Fabric Mirroring continuously replicates selected tables from your SQL Server into OneLake as Delta tables. An initial snapshot is taken, and from that point forward, changes are captured and replicated near real-time.

SQL Server Mirroring Type Change Mechanism Key Requirements
2016–2022 Database CDC On-prem gateway; sysadmin for CDC setup
2025 (on-prem) Database Change Feed Azure Arc + Extension

Fabric also supports Metadata mirroring (shortcuts, no data movement) and Open mirroring (API-driven, build your own) for other platforms. See Fabric Mirroring overview.

Both mechanisms replicate changes continuously. Under active load, changes can publish as frequently as every 15 seconds, with backoff logic during low activity (source). Once configured, Fabric creates a mirrored database in your workspace with an autogenerated SQL analytics endpoint — a read-only T-SQL interface you can query with SSMS, VS Code, or anything that speaks T-SQL.

2. Direct Lake Mode (OneLake → Power BI)

Direct Lake is a Power BI storage mode that reads Delta tables directly from OneLake. No data import into a semantic model. No scheduled refresh. No DirectQuery hitting your source. Because the mirrored data is already in Delta format, Direct Lake picks it up natively.

The result: your Power BI reports reflect changes as they land in OneLake. That's it. No upload or refresh required. MSSQLTips has a solid walkthrough of how Direct Lake works under the covers.

3. The End-to-End Flow

SQL Server (OLTP) → Fabric Mirroring (CDC or Change Feed) → OneLake (Delta tables) → Direct Lake (Power BI)

One workload. Three Fabric components -- and your SQL Server doesn't carry the reporting load.

SETTING IT UP (HIGH LEVEL)

The full setup is portal-driven, not T-SQL heavy. Microsoft's step-by-step tutorial covers every detail, but here is what you're walking into:

On the SQL Server side:

For SQL Server 2016–2022: install an on-premises data gateway (or VNet gateway). Create a dedicated login for Fabric with appropriate permissions. If CDC is not already enabled on your source tables, the Fabric setup process will configure it, and the login needs temporary sysadmin to do so.

-- Example: create the Fabric login (SQL Server 2016-2022)
-- Per Microsoft's tutorial
CREATE LOGIN fabric_login WITH PASSWORD = '<strong password>';

-- Grant sysadmin temporarily if CDC is not already enabled
ALTER SERVER ROLE sysadmin ADD MEMBER fabric_login;

-- After mirroring is configured and CDC is enabled,
-- remove sysadmin and grant only what's needed:
ALTER SERVER ROLE sysadmin DROP MEMBER fabric_login;

For SQL Server 2025: connect your instance to Azure Arc and install the Azure Extension for SQL Server. The extension provisions a managed identity that handles authentication to Fabric. No CDC required — SQL Server 2025 uses Change Feed natively. In fact, you cannot use Fabric Mirroring on a SQL Server 2025 database that already has CDC enabled.

In the Fabric portal:

Create a mirrored database item in your workspace, select your SQL Server connection, and choose the tables to replicate (up to 500). Fabric takes the initial snapshot and begins continuous replication.

For Power BI:

From the mirrored database's SQL analytics endpoint, create a new semantic model. The model defaults to Direct Lake mode. Build your report on top of it, and data flows through automatically.

See full details here in Explore data in your mirrored database and Build Power BI Reports with Direct Lake Tables.

WHAT TO KNOW BEFORE YOU COMMIT

CDC adds overhead to your production system.

For SQL Server 2016–2022, Fabric Mirroring requires CDC on the tables you replicate. CDC captures changes from the transaction log — it adds I/O and CPU overhead. Active transactions hold log truncation until the mirrored database catches up (source). Test this under realistic production load before you go live.

Change Feed (SQL Server 2025) is lighter, but not zero.

SQL Server 2025 scans the transaction log at high frequency and publishes changes to OneLake. Microsoft's language is 'least amount of resource tax on the source database' — not none.

'Near real-time' varies.

Replication latency depends on transaction volume, table size, and network throughput.   Measure latency under load, during peak hours. That number is your 'near real-time.'

ONE WORKLOAD, ONE SCENARIO

This is not a post about migrating to Fabric. It's one sample scenario where Fabric solves a real problem: getting near real-time analytics from SQL Server without building and maintaining a separate reporting infrastructure.

If you are running SQL Server OLTP with a growing demand for real-time reporting, this is worth evaluating. Start with one workload. Measure the results. Expand from there.

COMING NEXT

In Part 5, we'll look at SQL Database in Microsoft Fabric — an actual transactional database running inside Fabric. What it is, what it isn't, and where it might actually make sense.

More to Read

Fabric Mirroring overview
Mirrored databases from SQL Server
Tutorial: Configure Fabric Mirroring from SQL Server (step-by-step)
Fabric Mirroring limitations for SQL Server