Wednesday, July 22, 2026

SSRS Is Dead. Here's The Move to PBIRS.

A few months back I wrote SSIS Is Not Dead. Yet., opening with the line 'SSRS is gone'. Today it's time to give SSRS the send-off it deserves. SQL Server 2025 shipped without SSRS and Microsoft has named Power BI Report Server (PBIRS) as the on-premises replacement. No new SSRS version is coming. Per Microsoft's Reporting Services Consolidation FAQ, SSRS 2022 is the end of the line.

No panic. We've got time. SSRS 2022 is supported until January 11, 2033. That makes this a planning problem, not a fire drill. But understand that 'supported' in this context means security patches, and nothing more. All future investment is in PBIRS. So the first question is not how to move. It's whether you need to move at all.

Do You Even Need to Move Yet?

Honest answer: Not today. Your reporting catalog can sit on a newer database engine while SSRS 2022 keeps serving reports. Use this to place yourself:

Your Situation Your Move
Paginated-only, SSRS 2022 Stay put. Supported to 1/11/2033.
Upgrading engine to SQL 2025 Plan the PBIRS migration as its own track.
Want .pbix reports on-prem Migrate. Only PBIRS hosts both RDL and PBIX.
SharePoint-integrated mode Move to Native mode first. No direct path.
SSRS on Express edition Express loses reporting rights in 2025. Time to license up.

Important to know that 'no new SSRS' is not the same as 'SSRS stops working'. If you run pixel-perfect paginated reports and nothing else, you have seven years of runway. Use that runway wisely.

Licensing: The Part That Changed in Your Favor

For SQL Server 2022 and earlier versions, PBIRS on-prem required Enterprise edition plus Software Assurance, or a standalone Power BI Premium purchase. That wall disappeared with SQL Server 2025. Per the consolidation FAQ:

Your License PBIRS Access
SQL 2025, any paid edition Same 2025 key installs PBIRS. Done.
SQL 2022 and earlier Enterprise + SA only; MSFT provides the PBIRS key.
Just kicking tires Free Developer or Evaluation edition.

If you are Standard edition without SA, this is the first time PBIRS has been realistically on your menu. That alone changes the map for a lot of shops.

The Migration Itself

There is no in-place upgrade. You do not 'upgrade' SSRS to PBIRS. You install PBIRS and walk your report catalog over to it. Microsoft's official migration guide covers the full procedure, and SQLServerCentral published a great end-to-end walkthrough going from a SQL 2016 SSRS source to a SQL 2025 PBIRS destination.

Check your PBIRS build before you start. A SSRS 2022 catalog requires the May 2025 release of PBIRS, or newer. Anything older will not accept it, and you will find that out at Step 5, not Step 1.

The demo below uses RPTSQL01 for the old SSRS box, and RPTSQL02 for the new PBIRS box.

Step 1: Back up the encryption key. FIRST.

The key protects your stored credentials and connection strings. Lose it, and every data source on the new server greets you with a credential prompt. Use Report Server Configuration Manager (Encryption Keys > Backup), or the command line:

-- Run on RPTSQL01, adjust paths for your environment
rskeymgmt -e -f "D:\Backup\rs_key.snk" -p "YourStrongPassword"

Step 2: Back up the ReportServer databases and config files.

-- Run on RPTSQL01
BACKUP DATABASE [ReportServer]
  TO DISK = 'D:\Backup\ReportServer.bak'
  WITH INIT, COMPRESSION;

BACKUP DATABASE [ReportServerTempDB]
  TO DISK = 'D:\Backup\ReportServerTempDB.bak'
  WITH INIT, COMPRESSION;

Get copies of rsreportserver.config, rssrvpolicy.config, and web.config from the SSRS install path. You will want them for reference of any custom settings.

Step 3: Install PBIRS.

Just like with SSRS, you can install PBIRS on its own dedicated host with the ReportServer databases on a separate SQL Server instance, OR they can be combined on a single host. Straightforward install. See Install Power BI Report Server. One difference from SSRS is that the installer does not let you name the PBIRS instance. It is always PBIRS. Any scripts, monitoring, firewall rules, or documentation that reference the old ReportServer instance should be reviewed before cutover. Also good to know that migrating SSRS-to-PBIRS on the same hardware is supported.

Step 4: Restore the catalog. Same database name.

-- Run on the database instance for RPTSQL02
RESTORE DATABASE [ReportServer]
  FROM DISK = 'D:\Backup\ReportServer.bak'
  WITH MOVE 'ReportServer'     TO 'E:\Data\ReportServer.mdf',
       MOVE 'ReportServer_log' TO 'F:\Logs\ReportServer.ldf',
       RECOVERY;

The name must stay ReportServer. Do not get clever here.

Whatever your catalog database was named on the source -- ReportServer for most of us -- it needs to use that exact name on the destination. This is not the moment to standardize naming conventions. Rename it during the restore and PBIRS will not recognize its own catalog.

Step 5: Connect and restore the key.

Open your new Report Server Configuration Manager, connect the PBIRS instance to the restored catalog, then restore the encryption key from Step 1. Your reports, folders, security assignments, and subscriptions come along with the catalog.

Step 6: Validate, then clean up the old SSRS instance.

I recommend running the old and new in parallel until the new server has survived at least one complete business cycle, or until you've validated every subscription frequency used in your environment. Then you can decom the old SSRS box, RPTSQL01.

The Gotchas Nobody Mentions Until Go-Live

Subscriptions are SQL Agent jobs

The ReportServer database restore brings subscription definitions, not the Agent jobs that fire them. You need to verify that the corresponding SQL Agent jobs have been recreated and are executing successfully. There is a known pattern on the Fabric community forums: migration completes, all jobs show green, and no report emails ever arrive. Check ReportServerService_* logs under the PBIRS LogFiles directory, and always test one net-new subscription against your migrated ones.

The Migration Wizard has blind spots

The SSRS Reports Migration Wizard is a handy alternative to the catalog-restore method, and it does support PBIRS as both source and target. But you must know what it leaves behind. It moves your RDLs, shared datasets, data sources, and standard subscriptions. It does not move data-driven subscriptions, execution log history, or .pbix files, and it only speaks Native mode. If any of those matter to you, the catalog restore remains your best option.

SQL 2025 encryption defaults can silently break data sources

SQL Server 2025 enforces TDS 8.0 and stricter encryption out of the box. Report data sources and linked servers riding on older drivers can fail after the engine upgrade, and it will not show up in your database-level testing. Validate every shared data source against the new engine, not just the reports.

Restore custom assemblies/extensions

Custom assemblies and extensions are not stored in the ReportServer catalog. If you've customized SSRS beyond stock installations, inventory those components before shutting the old server down.

SharePoint-integrated mode is a dead end.

MSFT deprecated it, and no migration path targets it. If you are one of the few still running it, your first project is SharePoint-to-Native, and PBIRS is your second.

The Bottom Line

If your engine upgrade to SQL 2025 includes reporting, you've got two projects ahead of you, not one. The engine track and the reporting track have separate acceptance criteria — subscription schedules, encryption keys, stored credentials, custom extensions and URL reservations. Teams that treat them as one project may very well find their reporting tier is broken post-go-live, in ways their testing never could have surfaced.

SSRS has served us since 2004 and has earned a graceful exit. Let's give it one. Back up that encryption key, run parallel until a full subscription cycle passes clean, and do not let January 2033 arrive with this still on the to-do list. You've got plenty of time. For now.

More to Read

MSFT — Reporting Services Consolidation FAQ
MSFT — Migrate a report server installation
SQLServerCentral — Migrate SSRS Reports to PBIRS in SQL Server 2025
sqlfingers — SSIS Is Not Dead. Yet.

Tuesday, July 21, 2026

How Hard Is It to Trigger SQL Server 2025's DOP Feedback?

When Microsoft introduced Degree of Parallelism (DOP) Feedback for SQL Server, I immediately wanted to see it in action. The idea is compelling: if a repeating query uses more parallel workers than it really needs, SQL Server learns from previous executions and quietly chooses a better DOP the next time.

At least, that's how I understood it.

I decided to test it out in the lab.

The Lab

I started with SQL Server 2025 RTM and enabled the documented prerequisites for DOP Feedback.

  • Query Store in READ_WRITE mode
  • DOP Feedback enabled
  • Compatibility Level 160

From there, I configured the environment to encourage parallel plans.

  • MAXDOP set to 14
  • Cost Threshold for Parallelism reduced to 50
  • Batch Mode on Rowstore disabled during part of the testing to force row-mode execution

I built a repeatable workload, verified an actual DOP of 14, confirmed row-mode execution, and watched Query Store capture every execution.

Everything appeared to satisfy the documented requirements, so I expected SQL Server to at least consider the workload.

Then I Started Measuring

The execution plan was fully parallel.

The workload executed repeatedly.

Query Store captured every execution.

The query also generated measurable parallelism waits:

Wait Type Increase During Test
CXPACKET +27,678 ms
CXCONSUMER +23,424 ms
CXSYNC_PORT +1,130 ms
CXSYNC_CONSUMER +62 ms

This wasn't a tiny query. It was clearly exercising the parallel execution engine.

So I expected the workload to become a candidate for DOP Feedback.

It Never Happened.

sys.query_store_plan_feedback remained empty again and again.

I decided to create an Extended Events session to capture the DOP Feedback lifecycle, but the event I expected to see most, dop_feedback_eligible_query, never fired.

Then I thought compatibility level might be the missing piece, so I repeated the entire test under Compatibility Level 170.

No change.

No persisted feedback. No eligibility event. No evidence that SQL Server ever considered my workload a candidate for DOP Feedback. Ever.

At this point, the absence of evidence had become evidence itself.

What changed?

My understanding of the feature. I began this test assuming that a sufficiently expensive parallel query with measurable CX waits would become eligible for DOP Feedback. But nothing in my testing supported that assumption.

Instead, the evidence suggests something more interesting.

It tells me that DOP Feedback isn't just looking for every expensive parallel query. There are clearly additional eligibility criteria that determine whether SQL Server even considers a query for review. Those criteria aren't fully documented, and my workload never satisfied them.

Microsoft documents the feature and its prerequisites. Those internal decision points, however, remain intentionally undocumented.

My workload simply never crossed that line.

At this point I stopped trying to invent new workloads. If SQL Server wasn't even raising the eligibility event, making the query bigger or more expensive wasn't going to teach me anything. The more interesting question now is why the engine ignored the workload in the first place.

Bottom Line

I set out to demonstrate SQL Server automatically tuning a query. Instead, I found something I wasn't expecting.

After verifying Query Store, DOP Feedback, Compatibility Levels 160 and 170, row-mode execution, repeated executions, measurable CX waits, and a fully parallel execution plan -- SQL Server never considered the workload eligible for DOP Feedback.

That doesn't mean DOP Feedback doesn't work. It means SQL Server is far more selective than I expected.

One of the easiest mistakes to make with adaptive features is assuming they'll engage simply because all the prerequisites are enabled. My tests have reminded me that's only the beginning. The harder part is understanding what makes a workload eligible in the first place.

My next step is to find a workload that does become eligible and compare it against this one. When I find it, I'll write the sequel.

More to Read

Microsoft Learn: Degree of parallelism (DOP) feedback
MSFT Tech Community: Smarter Parallelism -- DOP feedback in SQL Server 2025
Microsoft Learn: sys.query_store_plan_feedback
Erik Darling: What’s The Point Of DOP Feedback In SQL Server 2022?

Sunday, July 19, 2026

SSMS 22.7 Schema Compare — Before You Click Apply

For twenty years, comparing two database schemas meant leaving SSMS. You opened Visual Studio and SSDT, or you paid for a third-party tool, or you did what most of us actually did -- scripted both sides out and eyeballed the diff. Schema compare has been one of the most requested features in SSMS for as long as I can remember.

SSMS 22.7 finally includes this feature. Graphical schema compare, native, no add-ins, in preview. You pick a source and a target, it shows you every difference, and it will write the script to make the target match -- or even apply the changes directly, if you let it.

That last part is where I want to spend some time because the compare is the easy half. The Apply button is the piece that can hurt you.

What It Is and What You Need

Schema compare works against any two of a live database, a SQL database project, or a .dacpac file, in either direction. A .dacpac is a data-tier application package -- a single file containing a database's full schema definition, no data, the same format SSDT has used for deployments for years. A SQL database project is the source-control side of the same thing: a folder of CREATE scripts, one per object, that builds into a .dacpac. If neither of those is part of your world, no worries! The database-to-database comparison is the bread and butter here, and it's likely all most of us will use.

Required version is SSMS 22.7.0 or later. The feature ships with the base install, so comparing live databases or .dacpac files needs nothing extra. Only SQL database projects require the Database DevOps workload, which is one of SSMS 22's optional components. To install it, launch the Visual Studio Installer, choose Modify on your SSMS 22 installation, check the Database DevOps workload, and let it update. It is still a preview feature, so expect some rough edges and remember that it's likely to change before GA.

There are three ways to open it, and where you start determines what gets filled in for you:

Object Explorer

Right-click a database, Tasks > Schema Compare (Preview). That database becomes your source.

Solution Explorer

Right-click a SQL database project, Schema Compare (Preview). This is the one that requires the Database DevOps workload.

Tools menu

Tools > Schema Compare. Opens empty, and you pick both source and target yourself.

However you get there, the workflow is the same. Run the comparison and the differences come back in a grid, grouped by action -- adds, changes, and deletes. Selecting an object shows its side-by-side difference in the lower pane, and from there you decide what happens next. You can check or uncheck individual changes, then either Generate Script, which opens the change script in a new query window for your review, or Apply, which runs the sync against the target right then and there. And, if it's a comparison you'll run regularly, like a weekly sync between production and dev, you can save the whole configuration, source, target, options and all, as an .scmp file. Then you reopen and reuse it next week instead of having to set everything up all over again.

If you've spent any time with schema compare in Visual Studio database projects, all of this will feel familiar. It's the same workflow, finally living where DBAs actually work.

The Sandbox

To test it, I need two databases that disagree. The setup below builds a 'dev' and a 'prod' copy of the same schema, then plants five specific differences between them. Each one is a kind of drift I've met in the wild, and each one should land in the compare results differently.

This is schema only, no data load. Schema compare doesn't read your rows, so this post doesn't need any.

USE master;
CREATE DATABASE CompareDev;
GO
CREATE DATABASE CompareProd;
GO

-- the matching objects in both databases
USE CompareDev;
GO
CREATE TABLE dbo.tblCustomers
(
    CustomerID INT IDENTITY(1,1) PRIMARY KEY,
    CustomerName VARCHAR(100) NOT NULL,
    Region VARCHAR(20) NOT NULL
);
CREATE TABLE dbo.tblOrders
(
    OrderID INT IDENTITY(1,1) PRIMARY KEY,
    CustomerID INT NOT NULL,
    OrderDate DATETIME NOT NULL,
    OrderTotal DECIMAL(10,2) NOT NULL
);
GO
CREATE PROCEDURE dbo.usp_GetOrdersByRegion
    @Region VARCHAR(20)
AS
BEGIN
    SET NOCOUNT ON;
    SELECT o.OrderID, o.OrderDate, o.OrderTotal
    FROM dbo.tblOrders o JOIN dbo.tblCustomers c 
      ON c.CustomerID = o.CustomerID
    WHERE c.Region = @Region;
END;
GO

USE CompareProd;
GO
CREATE TABLE dbo.tblCustomers
(
    CustomerID INT IDENTITY(1,1) PRIMARY KEY,
    CustomerName VARCHAR(100) NOT NULL,
    Region VARCHAR(20) NOT NULL
);
CREATE TABLE dbo.tblOrders
(
    OrderID INT IDENTITY(1,1) PRIMARY KEY,
    CustomerID INT NOT NULL,
    OrderDate DATETIME NOT NULL,
    OrderTotal DECIMAL(10,2) NOT NULL
);
GO
CREATE PROCEDURE dbo.usp_GetOrdersByRegion
    @Region VARCHAR(20)
AS
BEGIN
    SET NOCOUNT ON;
    SELECT o.OrderID, o.OrderDate, o.OrderTotal
    FROM dbo.tblOrders o JOIN dbo.tblCustomers c 
      ON c.CustomerID = o.CustomerID
    WHERE c.Region = @Region;
END;
GO


-- now the drift. five intentional differences
-- 1. new table in dev only (an add)
USE CompareDev;
GO
CREATE TABLE dbo.tblLoyaltyTier
(
    TierID INT IDENTITY(1,1) PRIMARY KEY,
    TierName VARCHAR(30) NOT NULL
);
GO

-- 2. new column in dev only (a change)
ALTER TABLE dbo.tblCustomers ADD Email VARCHAR(255) NULL;
GO

-- 3. proc modified in dev only (a change)
ALTER PROCEDURE dbo.usp_GetOrdersByRegion
    @Region VARCHAR(20)
AS
BEGIN
    SET NOCOUNT ON;
    SELECT o.OrderID, o.OrderDate, o.OrderTotal, c.CustomerName
    FROM dbo.tblOrders o JOIN dbo.tblCustomers c 
      ON c.CustomerID = o.CustomerID
    WHERE c.Region = @Region
    ORDER BY o.OrderDate DESC;
END;
GO

-- 4. old table still in prod only (a delete, if you let it)
USE CompareProd;
GO
CREATE TABLE dbo.tblLegacyImport
(
    ImportID INT IDENTITY(1,1) PRIMARY KEY,
    RawData VARCHAR(500) NULL
);
GO

-- 5. the emergency index. added in prod during a fire,
--    never backported to dev. we all have one of these.
CREATE NONCLUSTERED INDEX idx_tblOrders_OrderDate
    ON dbo.tblOrders (OrderDate)
    INCLUDE (OrderTotal);
GO

Five differences, and they are not all the same kind of dangerous:

# Drift Compare Should Say Risk on Sync
1 tblLoyaltyTier in dev only Add None. New table.
2 Email column in dev only Change Low. Nullable add.
3 Proc changed in dev Change Low. ALTER PROC.
4 tblLegacyImport in prod only Delete High. DROP TABLE.
5 Index in prod only Delete High. Drops a prod index.

Numbers 4 and 5 are the real test here. Syncing dev to prod means making prod look exactly like dev -- and anything that exists only in production becomes targeted for removal. In this sandbox that's a leftover table and an index. Number 5 is one we'll all recognize. The index that went into production during an emergency, but never made it back to dev or into the next formal deployment. It's an old story, and a schema compare tool could help you avoid that the next time around.

Running the Compare

Getting started is quick. In Object Explorer, right-click CompareDev and select Tasks > Schema Compare (Preview). The Schema Compare window opens with CompareDev already set as the source, which leaves the target. Click the ellipsis next to the Target box, choose a database as the target type, connect to your instance, and pick CompareProd. With both sides set, click Compare and the tool goes to work.

Notice the direction I chose. Dev as source and prod as target is the standard deployment direction, but also a dangerous one. Everything in dev that prod doesn't have becomes an add, and everything in prod that dev doesn't have becomes a delete.

The Script It Writes

The next part is what matters most to me. I clicked Generate Script with everything included and nothing excluded. You know, the scenario that happens when a hurried DBA runs the compare.

The generated script runs about a hundred lines, and most of it is standard deployment scaffolding full of SET options with PRINT statements between steps. Two very interesting findings: First, it's a SQLCMD script, full of :setvar and :on error exit directives, so it will not run in a normal query window. You will need to enable SQLCMD Mode (Query menu > SQLCMD Mode) or it errors out immediately -- the script even checks for this itself. And secondly, there is no transaction wrapping. The :on error exit directive stops the script when a step fails, but anything already applied stays applied. Keep that in mind before running it against anything that matters.

Now the part I built this sandbox to see. Here's what it generated for the prod-only table:

Credit where due. Before dropping tblLegacyImport, the script checks whether the table contains data, and if it does, the deployment halts rather than destroy rows. My sandbox table is empty, so the drop proceeds, but the point is that a populated table stops the whole script. That's a real guardrail.

And here is what it generated for the prod-only index:

No check, no warning, no halt. A PRINT statement and a DROP. The table gets a data-loss guard because dropping data is understood to be dangerous, but the emergency index -- the one that went into production purposely -- is removed without a second thought. The script announces it politely on its way out the door. If you weren't reading closely, number 5 just happened to you.

Apply vs Generate Script

There are two buttons at the top of the Schema Compare window that I need to emphasize. Generate Script opens the changes in a query window and hands you the wheel. So you get to read it and run it, on your terms. Apply skips all of that and runs the sync against the target directly. I wondered, do you get any chance to verify? And how much time between clicking Apply and the changes landing?

Exactly the confirmation I hoped to see. After clicking Yes, the synchronization begins immediately and finishes with one of these:

I even ran it once more to be sure the system detects the changes, and it found 0 differences.

My Verdict

I have to say that I like it. Even though my test was minimal, it did exactly what I asked it to, and even had guardrails for me, in case I wasn't fully paying attention to my ask. Schema Compare is a feature I will use. Generate Script will probably remain my default button. Apply has earned my respect, but not my trust. Yet.

One last note: Microsoft is collecting feedback on this feature right now, before it goes GA. This means use it. Beat it up real good and if the options don't protect the things you think need protected, this is the time to say so. Complain early, complain often. Help them make this into something we can comfortably rely upon.

Cleanup, because we always clean up:

USE master;
DROP DATABASE CompareDev;
DROP DATABASE CompareProd;

More to Read

Microsoft Learn: Schema Compare (Preview) in SQL Server Management Studio
Microsoft: Announcing the Release of SSMS 22.7.0 -- and many previews
sqlfingers: SSMS 22: What's Different, What's Worth It, and What May Bite You

Friday, July 17, 2026

The SQL Server DBA's Guide to AI Tools in 2026

Two years ago, 'AI for SQL Server' mostly meant pasting your T-SQL into a web browser, wrestling with prompts until it stopped hallucinating and screaming at ChatGPT. Today, the market has matured and split into three distinct battlefields: writing T-SQL faster, agentic DBA operations (plan tuning, audits, diagnostics), and building text-to-SQL solutions. The right tool depends on what you're trying to accomplish, because AI for SQL Server has become a collection of specialized tools rather than one-size-fits-all assistants.

Before comparing tools, it's worth acknowledging that much of the published 'AI tool comparison' content is written by vendors ranking themselves first, and several widely quoted benchmarks weren't even run against SQL Server. Rather than marketing claims, this post leans primarily on practitioner experience from SQL Server DBAs, in addition to the vendor documentation.

The Good

GitHub Copilot completions in SSMS -- the easy win

Copilot is now a standard SSMS 22 workload: sign in with a GitHub account and you get inline T-SQL completions (my fave), chat, and database-aware answers grounded in your active connection. The free tier (about 50 requests per month) costs nothing to try, and Pro is $10/month. As of SSMS 22.3, you can even store coding-standard instructions as extended properties at the database or object level, and Copilot uses them when generating code, which is a genuinely useful way to make its output match your shop's standards.

Here's the important part: Copilot in SSMS is really two features wearing one name, and they've earned very different reputations. The inline completions are excellent. Brent Ozar recommends enabling them immediately and letting Copilot handle repetitive joins, syntax, and boilerplate. The chat and Agent features are a very different story. See 'The Ugly' below.

Before you standardize on it, keep two things in mind. First, the free tier disappears quickly once you use Copilot for more than code completion. Second, your prompts and database metadata are sent to GitHub-hosted models, which may be a show-stopper if your environment prohibits sending schema or query text outside your organization.

dbForge AI Assistant -- best add-in for pure query development

Devart's AI Assistant plugs into dbForge Studio for SQL Server or into standard SSMS via dbForge SQL Complete. It's great at converting natural language to valid T-SQL, explaining legacy code, troubleshooting syntax errors, and offers optimization suggestions -- all without leaving SSMS or dbForge Studio.

It has genuine schema awareness, deep IDE integration, and a mature surrounding ecosystem of formatting, refactoring, and schema-comparison tools.

One Catch: It is tied to a commercial tool suite. The true cost is the broader dbForge licensing model, meaning you cannot buy the AI engine as a standalone, cheap utility.

SsmsAgentic -- agentic DBA work inside SSMS

A third-party VSIX that adds a Claude-powered agent pane to SSMS 22. Unlike completion tools, it executes the diagnostic work itself. It performs investigative DBA work by reading metadata, DMVs, execution plans and permissions, and then proposes actions for approval. It reuses your existing SSMS connection (including Entra ID) and your existing Claude plan, so there's no second AI bill. One-time license ($49–$199) after a 15-day trial.

It's the only SSMS-native option today doing actual investigative DBA work rather than suggesting code for you to run, and per-statement approval is the right security posture for agentic database access.

One Catch: Because it is third-party and not MSFT-backed, it requires the Claude CLI installed locally, SSMS v22+, and it is Windows only. Full disclosure: product reviews are still very thin out there, so these details come from the vendor's own site.

Claude Code + sqlcmd (or an MCP) -- the practitioner's agentic path

This one earned its spot from practitioner writing, not vendor marketing. Brent Ozar documented his own workflow using Claude Code against SQL Server and Azure SQL DB: at its simplest, Claude Code calls sqlcmd to run queries and read results, and as you get more advanced, you can wire up an MCP for richer database access. His advice for getting started safely: practice on an open-source repo you already use, like the FRK, Ola Hallengren's maintenance scripts, or DBAtools -- not your production estate.

You pick the model, you see the whole prompt, and nothing sits between you and the LLM rewriting your question. It's the same 'agentic' capability the shrink-wrapped tools sell, minus the black box.

One Catch: command-line workflow, more assembly required, and the same governance question as every agentic tool -- what you let it touch is on you.

Microsoft SQL MCP Server -- for building AI applications

MSFT's open-source MCP server (shipped via Data API Builder 1.7+) exposes a SQL database to any MCP-aware client -- Claude Desktop, VS Code, Cursor, or your own agent -- through a fixed set of typed CRUD tools with schema-level RBAC. This is the right shape when you're building an app or agent (ie., a support copilot that looks up orders) rather than assisting a human at a keyboard. A recent SQLServerCentral write-up found the setup much easier than expected, quirks and all.

It's free, open-source, runs on-prem, and it's the only path on this list that works from Mac and/or Linux.

One Catch: it's plumbing, not a product. No UI, no NL2SQL, DML only -- no DDL, no DMV access, no plan analysis. Setup means JSON config and usually hosting a container. And read 'The Ugly' before you hand it your data.

The Bad

To be clear, 'bad' here doesn't mean bad software. It means tools people are reaching for that are a poor fit for SQL Server work specifically.

Raw ChatGPT / generic chat with no database context

Still the most common choice, and still the most error-prone for production work. With no awareness of your schema, generic LLMs invent plausible table and column names, and they chronically cross dialects -- emitting LIMIT instead of TOP, or PostgreSQL :: casting instead of CAST(), even when told to target SQL Server. Fine for learning concepts but risky for anything you'll execute against a live database without careful review. Redgate's Simple Talk lands in the same place: you can trust AI with database issues, but verify every response it gives you. It's worth noting that the practitioner objection isn't to the LLMs themselves -- Brent Ozar's own preferred workflow sends well-built prompts to the LLM of your choice. The failure is with the missing context, not the model.

Amazon Q

Genuinely useful if your data lives in Redshift or Athena -- but close to useless for native SQL Server. No support for databases outside AWS, setup requires IAM and CLI configuration, and even within AWS its quality varies by service. If your estate is SQL Server on-prem or on Azure, skip it entirely.

Zero-setup 'paste your schema' web portals

Lightweight browser apps where you paste DDL and ask questions. The convenience is real, but dialect support skews heavily toward PostgreSQL and MySQL, accuracy drops sharply on complex queries, and manually pasting hundreds of lines of SQL Server schema into an unsecured browser text field gets old fast -- and should make your security team twitch. With no execution capability, you're copy-pasting in both directions, and the AI is blind to the things that actually matter: bottlenecks, waits, and lock escalation.

Generic text-to-SQL tools used for DBA work

Standalone tools like AI2SQL are legitimately good at their actual job -- letting analysts and PMs generate reporting queries from plain English against a connected schema. But they're query generators, not DBA tools. No SSMS integration, no DMV access, no execution-plan work, no migration or security-audit capability. Using them for SQL Server administration is a category error. Their published accuracy benchmarks were run against PostgreSQL, so the numbers don't transfer to T-SQL, which brings us to the ugly part.

The Ugly

The ugly here isn't referencing actual tools. It's the pieces inside the tools that are problematic and you need to know about.

SSMS Copilot rewrites your prompts -- and you can't see how

Brent Ozar's testing found that SSMS Copilot's advice quality lags well behind asking the same LLMs directly, and the culprit is the prompt Copilot wraps around your question. By 2026 he'd concluded that between SSMS interfering with his prompts and Copilot's shift toward usage-based pricing, the chat simply isn't worth using directly -- his workaround is building the prompt himself in T-SQL (the @AI parameter in sp_BlitzCache and sp_BlitzIndex) and sending it to whichever LLM he chooses. The kicker, courtesy of his readers: the chat logs -- including the hidden system prompt MSFT prepends to your questions -- sit in plain text under %localappdata%\SSMSCopilot. The tool most shops will standardize on is the one where you have the least visibility into what's actually being asked.

The benchmark shell game

Most 'AI for SQL' comparison content is written by vendors ranking themselves first, and the accuracy numbers that get repeated across these roundups trace back to benchmarks run against PostgreSQL schemas. PostgreSQL accuracy tells you nothing about whether a tool writes correct T-SQL, handles DMVs, or can read an execution plan. If a vendor quotes a percentage, you need to ask what engine it was measured on.

Your data goes along for the ride

Every one of these tools -- Copilot, agentic extensions, MCP pipelines -- ships your prompts, metadata, and query results to somebody's hosted model. The SQLServerCentral author who wired Claude Desktop to his ERP database said it very clearly: whoever uses the MCP has access to that data, and the data is sent to the provider for processing. He also had to disable Row-Level Security to get his demo working -- which is exactly the kind of shortcut that migrates quietly from demo to production. The permission model is on you. Dedicated SQL logins with minimal rights, no DELETE or DROP grants for agents, and a hard look at what your compliance posture allows to leave the building.

How to Choose

The pragmatic reality for most database teams is that you won't rely on a single tool. The common setup is a fast completion tool for raw typing speed, paired with one agentic path for the investigative work that used to consume your entire afternoon.

Your situation Use this
Write T-SQL all day in SSMS GitHub Copilot completions
Want AI to do the investigation
(slow-query triage, index review, audits)
SsmsAgentic, or Claude Code + sqlcmd/MCP
Want full control of the prompt Claude Code + sqlcmd, or @AI in sp_Blitz tools
dbForge shop dbForge AI Assistant
Building an app or agent SQL MCP Server
On Mac/Linux SQL MCP Server + Claude Desktop or VS Code
Non-technical, just need a query Schema-connected text-to-SQL tool

One Last Thought

After comparing today's AI tools for SQL Server, one thing became clear: there really isn't a single 'best' choice. Nearly every tool on this list can write T-SQL. The real differences are how well they understand SQL Server, how transparent they are about what they're doing, and how much control they leave in your hands.

If I were building a toolkit today, I'd install GitHub Copilot for everyday coding, use Claude Code for investigative work, choose dbForge AI if I already lived in the dbForge ecosystem, and reach for SQL MCP only when building AI applications. Together, those tools cover nearly everything a SQL Server DBA is likely to need today.

The best AI tool for SQL Server isn't the one that writes the most code. It's the one that helps you make better decisions while keeping you firmly in control.

More to Read

Brent Ozar - SSMS Copilot is Messing With Your AI Prompts
Brent Ozar - Using Claude Code with SQL Server and Azure SQL DB
SQLServerCentral - From SQL Server On-Premises to Claude Desktop: A Full MCP Pipeline
SsmsAgentic - AI for SQL Server
Devart - dbForge AI Assistant for SQL Server
AI2SQL - Best AI SQL Tools 2026
Demis Hassabis - A Framework for Frontier AI and the Dawning of a New Age

Thursday, July 16, 2026

Don't Ask What the Feature Does. Ask What It Changes.

Sometimes it feels like we are drowning in announcements.

New AI feature. New compression algorithm. New optimizer enhancement. New DMV. New security bulletin. New Copilot capability.

Every release cycle feels the same.

"SQL Server now supports..."

"Microsoft announced..."

Great. But why should I care?

That isn't cynicism. It's the question every experienced DBA learns to ask, and one too many technical articles never answer. They stop after explaining what changed, but the smart DBA wants to know what it changes for them.

This post is about that gap -- and the mental framework senior DBAs use to close it.

The vendor's framing vs. yours

When MSFT (or any vendor) announces a feature, the announcement almost always makes one of four claims:

  • It makes something faster.
    A new compression codec, a smarter optimizer, a parallelism improvement.
  • It makes something safer.
    A hardened protocol, a new encryption option, a permission model change.
  • It makes something cheaper.
    Less storage, fewer cores, lower licensing tiers doing more work.
  • It makes something easier.
    A wizard, a new DMV, a Copilot integration, one less thing to script by hand.

Those four buckets are the vendor's framing, and the claims are usually accurate. But they're just answering the vendor's question, not the DBA's.

The vendor tells you what the feature does. The experienced DBA reads the same announcement and starts asking:

  • Why is it needed?
    What problem was bad enough that MSFT built this? If I don't have that problem, I may not need the fix.
  • What does it impact?
    Backups? Security? Licensing? The optimizer? Something I monitor, or something I've never had to think about?
  • Where is it relevant?
    Which of my servers, which of my databases, which of my customers? All of them, one of them, none of them?
  • What does it break?
    What did I configure, script, or assume years ago that this quietly steps on?

None of those questions are about the feature. They're about your environment. That's why two DBAs can read the same announcement and one shrugs while the other clears their calendar.

If every answer comes back empty, you file it and move on. If one comes back loaded, you've got some work to do.

What that looks like in practice

Abstract frameworks are cheap. Let's run some real announcements through the questions.

ZSTD backup compression

The announcement says: a new compression algorithm, better ratios, less CPU. The vendor framing: faster and cheaper.

What it means to you: your backup window has changed. What took ninety minutes now finishes in forty. Your maintenance window, log shipping, restore expectations, and everything scheduled around them were built around those ninety minutes. The feature matters because all of that may need to change with it.

Copilot in the database tooling

The announcement says: AI-assisted queries, natural language over your schema, productivity gains. The vendor framing: easier.

What it means to you: the AI inherits your permissions. There is no separate 'Copilot user' with a narrow grant. When it runs a query, it runs as you -- sysadmin, db_owner, whatever you're holding. Every conversation you've ever had about least privilege just gained a new participant, and it types very fast. 😆

RC4 Kerberos deprecation

The announcement says: a legacy encryption type is being disabled by default. The vendor framing: safer.

What it means to you: weak service accounts just became an immediate liability. That fifteen-year-old service account with the short password and the RC4-only configuration was always a problem in theory. Now it's a problem with a deadline. 'We'll get to it' just became 'better find it and fix it now'.

AI features inside the engine

The announcement says: AI built into the engine -- native vector search and T-SQL that calls external AI models to generate embeddings. The vendor framing here isn't faster/safer/cheaper/easier. It's a fifth claim: 'this makes something new possible'. Those deserve the most scrutiny of all.

What it means to you: your SQL Server may now initiate outbound Internet traffic. For twenty years, 'the SQL box is talking to the Internet' was a five-alarm indicator of risk. That is no longer the case. But your firewall and monitoring rules still assume it is. This isn't a feature to fear. It's one to understand. It's new outbound traffic that you have to manage -- deliberately, and on your terms.

Missing index recommendations

The announcement says: the engine surfaces indexes it thinks you should build. The vendor framing: easier.

What it changes for you: Almost nothing -- and that's the lesson. SQL Server is identifying opportunities, not prescribing changes. The missing index DMV output is a symptom log, not a change order. Hastily creating every recommended index is most likely going to hurt you a lot more than help. The right first response isn't action -- it's evaluation. Test what looks promising, skip what doesn't, and treat the rest as free intelligence about your workload.

The only filter that matters

Microsoft ships hundreds of pages of release notes every year. New engine features. New tooling. New DMVs. New security guidance. New Azure capabilities.

Nobody reads all of it.

Nobody needs to.

Experienced DBAs don't stay current by trying to memorize every announcement. They stay current by recognizing which announcements change their environment.

That's why those four questions matter.

  • Why is it needed?
  • What does it impact?
  • Where is it relevant?
  • What does it break?

If every answer comes back empty, file it away and move on.

If one comes back loaded, give it the attention it deserves and learn that feature thoroughly.

The goal isn't to know every new feature. It's to recognize when a new feature changes the way you operate.

The features will keep coming.

The questions stay the same.

More to Read

sqlfingers: ZSTD LEVEL Isn't Exactly What You Think
sqlfingers: Copilot in SSMS Runs As You
sqlfingers: What the LLM Doesn't Know About Your Database
Brent Ozar: How to Go Live on SQL Server 2022 (or 2025) — Great example of evaluating operational impact instead of just enabling every new feature.

Wednesday, July 15, 2026

SQL Server 2025 Can Call the Internet. So Can an Attacker.

Two months ago I wrote about Copilot in SSMS running as you until you fence it in. The lesson was that the security boundary is SQL Server's permission system, not the AI's intentions. Last month Brent dropped sp_BlitzUpdate, which uses SQL Server 2025's new ability to call external REST endpoints to pull code from GitHub and install it on startup. He noted you probably should not be grabbing code from the Internet and running it on your SQL Servers.

On June 10, 2026, Justin Kalnasy of SpecterOps published the other half of that sentence. If SQL Server 2025 can call the Internet, so can an attacker who owns a login on it. The full post is 'Oops, I Weaponized the Database: Abusing AI Features in SQL Server 2025'. Very good read. Here's what you need to know as a SQL Server professional.

The three features and what they actually allow

SQL Server 2025 shipped three AI-focused capabilities intended to support RAG pipeline workflows. All three are legitimate, documented, and working exactly as designed. That last part is relevant, because Microsoft has already been asked.

Feature Intended use What an attacker does with it
sp_invoke_external_rest_endpoint Call an external REST API from T-SQL POST up to 100MB of database contents per call to any HTTPS endpoint with a trusted cert
CREATE EXTERNAL MODEL Register an embedding model location Specify a UNC path to coerce NTLM authentication over SMB and capture the service account hash
AI_GENERATE_EMBEDDINGS Send text to a model and receive a vector array Disguise C2 command-and-control traffic as embedding telemetry, indistinguishable from legitimate model calls on the wire

None of these techniques give an attacker access to SQL Server. They become relevant after an attacker has already obtained a login with sufficient privileges. The concern is what those privileges now allow them to do.

The 100 MB payload limit is not much of a limitation. Kalnasy demonstrates that an attacker can split data across asynchronous requests and reconstruct it remotely, allowing very large databases to be exfiltrated in pieces.

The part that actually changes your threat model

SpecterOps built an 81-line T-SQL query that operates as a full command-and-control implant. It uses AI_GENERATE_EMBEDDINGS to check in with a C2 server, receive instructions encoded as synthetic vector arrays, XOR-decrypt them, execute commands, encode the output the same way, and send it back. Kalnasy includes a comparison of legitimate embedding traffic against C2 traffic in the original post. That image is the whole argument.

In Kalnasy's words, "Now ask yourself if an untrained analyst is really going to be able to spot the difference? Probably not".

This is the part that matters. For decades, egress web traffic from a database host was a reliable red flag. Traditionally, unexpected outbound Internet connections from a database server have always been a decent indicator that something should be investigated. SQL Server 2025 makes that signal far less reliable. As soon as an organization enables these AI features, that signal becomes much harder to trust, and any security tooling must now inspect query content and traffic payloads to verify risk. That is a much harder problem.

Kalnasy reported the NTLM coercion primitive specifically to Microsoft on April 20, 2026. On May 12, 2026, Microsoft determined the behavior did not represent a security boundary violation due to the nature of file path resolution, and it did not meet the bar for security servicing. Microsoft did not classify the reported behavior as requiring a security update. These features work as intended. The controls are yours to implement.

What the exfil actually looks like — safely

I am not reprinting a working exfil script. The SpecterOps repo is linked at the bottom if you want to lab it. What is useful here is seeing the shape so you could recognize it in an audit log.

These are descriptions of the three exfil patterns from the research so you know what to watch for:

Bulk table dump. Enable the stored procedure with sp_configure 'external rest endpoint enabled', 1, query a table with FOR JSON AUTO, store the result in a variable, and pass it as the @payload to sp_invoke_external_rest_endpoint. The entire table leaves in a single HTTPS POST. Credentials table, user table, whatever is accessible -- 100MB at a time.

File contents. Couple the same stored procedure with OPENROWSET(BULK ..., SINGLE_CLOB). Read any file the SQL Server service account can reach, including Windows hosts files, configuration files, anything on accessible shares, and POST it out the same way. The outbound connection originates from the SQL Server process, not a C2 agent.

Persistent trigger. In the SpecterOps PoC, Kalnasy demonstrates an AFTER INSERT trigger on a credentials table that calls sp_invoke_external_rest_endpoint on every write, POSTing new rows to a remote server in real time. As a proof of concept it is deliberately constructed, but the technique is sound, and a trigger is harder to evict than an agent because it lives in the database schema, not in memory. I'm including a query for you here to run now. You need to know if you have any triggers calling an external URL.

/* Detection: find triggers that call the REST endpoint.*/
SELECT
    OBJECT_SCHEMA_NAME(t.object_id) AS SchemaName,
    OBJECT_NAME(t.object_id) AS TriggerName,
    OBJECT_NAME(t.parent_id) AS ParentTable,
    m.[Definition]
FROM sys.triggers t JOIN sys.sql_modules m
  ON m.object_id = t.object_id
WHERE m.definition LIKE '%sp_invoke_external_rest_endpoint%'
   OR m.definition LIKE '%AI_GENERATE_EMBEDDINGS%';

On a clean instance it should return nothing. If it returns rows, you should look into it.

What to alert on

SpecterOps provides specific detection guidance using Splunk SPL queries against SQL Audit or Extended Events output. The full queries are in the original post and worth bookmarking. These are the four signals to capture:

Signal Why
external rest endpoint enabled = 1 The master switch. This appears in the SQL Server ERRORLOG natively, but you want it in your SIEM too.
CREATE / ALTER / DROP EXTERNAL MODEL A new model registration, possibly pointing at a UNC path or hostile URL. Should not happen without a change ticket.
CREATE ASSEMBLY / AS EXTERNAL NAME The advanced C2 variant loads a CLR implant in-memory from hex bytes. If you have CLR disabled and this fires, stop everything.
Trigger DDL on sensitive tables The persistent exfil vector. Any new trigger on a credentials, user, or financial table warrants immediate review.

The two controls that actually stop it

Alerting only tells you it happened. These two things can be used to prevent it.

Pull sysadmin off your application service accounts. Kalnasy is direct: "Too often we find web applications with database connection strings containing an account with sysadmin roles. If your app is only doing standard database queries and writes you do NOT need sysadmin privileges". Every technique in the research assumes sysadmin or near-equivalent. Remove it and you have removed the ability to flip sp_configure, create external models, and deploy CLR assemblies. This is the same principle as the CopilotExec least-privilege pattern, applied one layer out.

Block internet-bound egress from your database hosts at the firewall. This is the single highest-leverage control and the one that ends the conversation regardless of what a login can do inside the engine. If the SQL Server host cannot reach arbitrary internet endpoints, a 100MB JSON POST has nowhere to go. If you are using AI features, host your models internally and allow only those specific destinations. If you are not using AI features, there is no legitimate reason for your database host to initiate HTTPS connections to the open Internet at all. That rule was always true. SQL Server 2025 just made enforcing it urgent.

-- There is no T-SQL for this. The fix is at the network edge:
--   DENY  outbound 443 from SQL Server host subnets to 0.0.0.0/0
--   ALLOW outbound 443 from SQL Server host subnets to your internal model hosts only
--   ALERT on any denied egress from a database host -- that alert IS the canary
--
-- If your SQL Server is already making internet HTTPS calls and you
-- didn't know about it, you have a problem that needs review.

Why this is different from the usual CVE post

There is no KB number here. There is no patch to wait for. There is no 'apply update and move on.' Microsoft reviewed the NTLM coercion primitive specifically, determined it works as designed, and closed the report. The rest of the techniques are in the same category: documented features, used as intended, just by the wrong person.

That shifts the responsibility entirely to you: your service account permissions, your egress rules, your audit configuration, and your analysts' ability to distinguish legitimate embedding traffic from traffic designed to look like it. Kalnasy puts it plainly: "Behavior takes time to baseline into modern security solutions, and the burden is being put solely on the administrator to add supplementary controls. Simply telling users to implement strong access controls and to monitor without clear guidance isn't good enough."

AI isn't the vulnerability. Your security boundaries are. SQL Server 2025 simply tests them in ways previous versions never could.

More to Read

SpecterOps: Oops, I Weaponized the Database — Justin Kalnasy, June 10, 2026
GitHub: mssql2025-poc (SpecterOps PoC code)
Microsoft Learn: sp_invoke_external_rest_endpoint
Microsoft Learn: CREATE EXTERNAL MODEL
sqlfingers: Copilot in SSMS Runs As You Until You Fence It In

Tuesday, July 14, 2026

SQL Server 2016 is Out. Here's the Fine Print.

It's July 14, 2026. SQL Server 2016 just reached end of extended support. Ten years of service, and as of today: no more security patches, no more bug fixes, no more calling Microsoft when it's on fire. We've been telling you this day was coming since February. It's not coming anymore. It's here.

And here's the irony. Nothing will break today. Your 2016 instances will hum along like nothing happened. No error, no warning, no countdown clock in SSMS. The engine doesn't know it has been abandoned. That silence is exactly what will get people -- the risk doesn't announce itself, it just accumulates. Unless you're paying for ESUs, every CVE from here on out ignores your 2016 boxes completely.

Two Things Happened Today, Not One

Today is also Patch Tuesday. And this particular Patch Tuesday flips the Kerberos RC4 hardening (CVE-2026-20833) into its final enforcement phase. We covered exactly what that does to SQL Server logins last month: legacy service accounts still leaning on RC4 stop getting Kerberos tickets, and what you see is the world's least helpful error:

Cannot generate SSPI context.

Think about it. Which estates are most likely to have decade-old service accounts with legacy encryption types? Exactly. The estates still running SQL Server 2016. If connections start failing this week, don't immediately assume it's the EOL. That EOL breaks nothing today. Check the domain controllers' patch status first, then work through the RC4 remediation steps in my June 18 post.

Six Things 2016 Users May Not Know

1. Azure is no longer a free pass.

This is the big one. For SQL Server 2014, the play was simple: lift the VM into Azure and the Extended Security Updates came free. Everyone learned that trick. Everyone is now assuming it still works. It doesn't. Under Microsoft's pricing consistency model that took effect April 1, 2026, SQL Server 2016 ESUs are chargeable everywhere -- on-prem, other clouds, Azure VMs, even Azure Stack. Same list price regardless of where it runs or how you buy. If your Azure migration budget assumed free ESUs -- the way it worked for 2014 -- that line item just became a real cost. Redo the math before you sign anything.

2. Procrastinating won't save you a dime.

Thinking you'll skate unpatched until a scary CVE drops, then buy ESUs? Microsoft thought of that. If you subscribe late, your first bill includes a one-time bill-back charge all the way to the start of the ESU term. Sign up in December, pay from July. The meter started at midnight whether you're enrolled or not.

3. Your support plan just went quiet.

Per Microsoft's own ESU FAQ, you cannot log a support ticket for SQL Server 2016 anymore -- even if you have a paid support plan. No ESU subscription, no ticket. Plenty of shops budget for a support contract as their safety net and have no idea the net was removed this morning.

4. Express, Web, and Developer can't buy their way out.

ESUs are available for Enterprise and Standard editions only. If you have 2016 Express instances squirreled away under desks and inside vendor appliances -- and you do -- there is no ESU option for them at any price. Upgrade or retire. End of story.

5. Windows Server 2016 did NOT die today.

This one has been widely misreported. The two products do not share a deathbed. SQL Server 2016 ended today, July 14, 2026, but Windows Server 2016 runs until January 12, 2027. If a box runs both, that's one application stack with two lifecycle deadlines, six months apart. The database is the urgent conversation, the OS is the January conversation. Plan them as related projects, not one generic '2016 upgrade'.

6. ESU patches are not Patch Tuesday.

Even if you pay, don't expect a monthly cadence. ESUs cover critical security updates only, released when a qualifying vulnerability requires one. No bug fixes, no non-critical patches, no features, no design changes. It's a security drip-line, not a servicing plan. Oh -- and even Volume Licensing purchases require Azure Arc registration to activate. There is no ESU path that avoids Arc entirely.

Find Your Exposure -- Right Now

Run this on anything you suspect. It works on SQL Server 2008 and later, so it's safe on the old stuff, ie., the boxes we're actually worried about. If ProductVersion starts with 13., congratulations, you own an unsupported database server as of this morning:

/* what am I actually running? (safe on SQL 2008+) */
SELECT SERVERPROPERTY('ProductVersion') AS ProductVersion,   -- 13.x = SQL Server 2016
       SERVERPROPERTY('ProductLevel') AS ProductLevel,       -- RTM / SPn
       SERVERPROPERTY('Edition') AS Edition,
       @@VERSION AS FullVersionString;                       -- last line reveals the host OS

Check FullVersionString, too. If the tail end says 'Windows Server 2016', that box has BOTH deadlines from item #5 -- database now, OS in January.

Then check whether each 2016 instance is even allowed to buy the lifeboat:

/* ESU eligibility check -- Enterprise and Standard only */
SELECT @@SERVERNAME AS ServerName,
       SERVERPROPERTY('Edition') AS Edition,
       CASE
           WHEN CAST(SERVERPROPERTY('Edition') AS VARCHAR(64)) LIKE 'Enterprise%'
             OR CAST(SERVERPROPERTY('Edition') AS VARCHAR(64)) LIKE 'Standard%'
           THEN 'ESU eligible'
           ELSE 'NOT ESU eligible -- upgrade or retire' END AS ESU_Status;

Sweep your whole estate, not just the servers you remember. Registered Servers or a CMS query against every instance takes minutes. The 2016 box that hurts you won't be the one on your inventory sheet -- it'll be the 'temporary' one from 2018 that a vendor installed and nobody documented.

The Dates That Matter Now

Date What Happens
July 14, 2026 SQL Server 2016 extended support ends. Today.
July 15, 2026 ESU Year 1 billing begins (midnight UTC), enrolled or not.
January 12, 2027 Windows Server 2016 extended support ends. Separate deadline.
July 17, 2029 SQL Server 2016 ESU availability ends. The real cliff.

So What Do You Do This Week?

Inventory first -- every instance, every edition, every host OS -- using the scripts above. Then triage each instance into one of three buckets:

Bucket What Goes In It
Upgrade now Customer-facing or compliance-scoped. PCI-DSS, HIPAA, SOC 2 auditors flag unsupported software.
Bridge with ESUs Vendor-locked or politically complicated. Can't move yet, can't leave exposed.
Retire That report server nobody has touched since 2021. You can now justify the decom.

If you're upgrading, target SQL Server 2022 or 2025 -- don't burn a migration on 2017, which itself dies in October 2027. And read what breaks on the Monday after a 'successful' upgrade before you schedule the weekend. The engine is the easy part. The drivers, certificates, and linked servers standing around it are not.

SQL Server 2016 was a genuinely great release -- Query Store, Always Encrypted, temporal tables, the version that made 'just put it in Standard Edition' a real option. It earned the decade. But the calendar has no snooze button, and staying on 2016 now means accepting risk that compounds daily -- unpatched, and by choice. If you need help planning your exit, let me know. This is what we do. Better to plan it now than triage it later. 😉

More to Read

Microsoft Learn: SQL Server Extended Security Updates FAQ
MSFT Tech Community: SQL Server 2016 Extended Security Updates
Microsoft Licensing: ESU Pricing Consistency Update
sqlfingers: Cannot Generate SSPI Context: The July RC4 Change That Breaks SQL Logins
sqlfingers: Your SQL Server 2016 Upgrade Will Succeed. Then Monday Happens.
sqlfingers: SQL Server 2016: 111 Days. The Last Patch Just Dropped.

Wednesday, July 8, 2026

The Apostrophe That Wasn't There — Until SQL Server Created It

A few weeks back, Redgate's Simple Talk pulled apart a SQL injection hole in one of Microsoft's own shipped system procedures, sys.sp_dbmmonitorupdate. That is the part that should stop you. Microsoft wrote it. This procedure is not careless. It is sanitized. But it doubled up its single quotes with REPLACE, exactly the way the textbook says to, and became a target anyway.

Last time I wrote about injection, the lesson was 'stop concatenating user input.' This is the sequel for anyone still concatenating and trying to make it safe with REPLACE. The problem is that the value can contain a Unicode character that looks exactly like an apostrophe, but isn't one. REPLACE looks only for the real apostrophe and lets the lookalike pass. Then the value lands in a char buffer, and SQL Server quietly converts the lookalike into a genuine apostrophe — without being asked.

Input:   Oʼreilly     (that mark is a lookalike, not a real apostrophe)

Step 1   REPLACE only doubles REAL apostrophes. It does not recognize
         the lookalike, so it changes nothing and passes it through.

Step 2   The value is copied into a char (non-Unicode) buffer. That
         copy silently converts the lookalike into a REAL apostrophe.

Result   A real, unescaped apostrophe is now sitting in the string you
         already 'sanitized'. The quote breaks out. Injection is live.

This has been labeled 'Unicode homoglyph' since Redgate's Simple Talk showed a real system-procedure case. In SQL Server, this means a character that visually resembles another character -- like lowercase 'L' and the number 1, or as in the example above, a Unicode apostrophe and a standard single quote. These homoglyphs become dangerous when an implicit Unicode-to-non-Unicode conversion performs a best-fit character mapping. In this case, SQL Server silently translated U+02BC into U+0027 after the quote-doubling had already occurred, effectively creating a new SQL delimiter that the sanitization step never saw.

Why doubling quotes stops working

First, one piece of vocabulary, because the rest of this leans on it. Every character has an ID number, its Unicode code point, written as 'U+' plus a hex value. The apostrophe on your keyboard is U+0027. It has a near-twin, the modifier letter apostrophe (U+02BC), a completely different character that happens to look identical in most fonts.

Now the failure. REPLACE matches on that ID number. You told it to find the keyboard apostrophe (U+0027) and double it, so when you hand it the twin (U+02BC) there is no match and it passes straight through. The damage lands one line later, when that value is assigned to a non-Unicode variable: SQL Server's best-fit mapping rewrites the twin into the real keyboard apostrophe. A genuine quote is now sitting in a string you already declared clean.

You can see it here. This part is deterministic, not a measurement:

DECLARE @n nvarchar(10) = N'ʼ';   -- U+02BC, MODIFIER LETTER APOSTROPHE
DECLARE @v varchar(10)  = N'ʼ';   -- implicit cast to non-Unicode

SELECT UNICODE(@n) AS AsNVarchar,   -- 700
       UNICODE(@v) AS AsVarchar;    -- 39  (that is U+0027, a real quote)

700 goes in, 39 comes out — and notice there is no CAST or CONVERT anywhere in that code. Just assigning the Unicode value (the N'...') to a varchar variable is enough. SQL Server converts it for you, silently, using best-fit character mapping. The implicit conversion is the whole bug.

demo time

IF OBJECT_ID('dbo.usp_FindCustomer','P') IS NOT NULL 
DROP PROCEDURE dbo.usp_FindCustomer;
GO
IF OBJECT_ID('dbo.Customer','U') IS NOT NULL 
DROP TABLE dbo.Customer;
GO

CREATE TABLE dbo.Customer
(
    CustomerId INT IDENTITY(1,1) PRIMARY KEY,
    LastName VARCHAR(128) NOT NULL
);
GO

-- Three rows, including a REAL apostrophe surname. Row count hard-capped by the VALUES list.
INSERT dbo.Customer (LastName) 
VALUES ('Smith'), ('O''Brien'), ('Nguyen');
GO

The 'safe' procedure

It sanitizes. It doubles the quotes like the textbook says. Its only sin is landing the result in a char(256) buffer before building the command.

CREATE PROCEDURE dbo.usp_FindCustomer
    @Name NVARCHAR(128)
AS
BEGIN
    SET NOCOUNT ON;

    -- The step everyone trusts: double up single quotes.
    SET @Name = REPLACE(@Name, '''', '''''');

    -- The mistake: a NON-Unicode buffer. This is where U+02BC becomes U+0027.
    DECLARE @command CHAR(256);
    SET @command = N'SELECT TOP (100) CustomerId, LastName '
                 + N'FROM dbo.Customer WHERE LastName = ''' + @Name + N'''';

    PRINT @command;      -- inspect the built string instead of blindly running it
    -- EXEC (@command);  -- left commented ON PURPOSE
END;
GO

Case 1 — a normal apostrophe (this one is fine)

EXEC dbo.usp_FindCustomer @Name = N'O''Brien';

The REPLACE in the proc turned that one apostrophe into two, so the command it builds ends with the double ticks in the LastName. That is not a typo. In T-SQL, using '' inside a string is the escape for a single apostrophe. When SQL Server runs that, it reads the value back as O'Brien. Copy that SELECT and run it yourself. You'll see it is working correctly -- and that is what lulls you into trusting REPLACE.

Case 2 — the lookalike (this one is not)

-- The character before 'reilly' is U+02BC, NOT the U+0027 on your keyboard.
EXEC dbo.usp_FindCustomer @Name = N'Oʼreilly';

This time REPLACE finds no U+0027 to double, so it does nothing. The char(256) assignment converts U+02BC into a real apostrophe, and the output now carries a quote that cannot be used. Try to run that SELECT and you'll see the engine doesn't care for that unmatched quote:

You're only seeing this error because this payload was clumsy enough to break the syntax. Make the injected tail valid T-SQL and nothing breaks. SQL Server just runs it, no error, no warning, with whatever rights the caller holds. The unclosed quote syntax error is the best outcome on the table. A real attacker's version will run clean and provide no evidence of who is in your system doing what.

The fix: keep it Unicode, and stop concatenating

Two things, belt and suspenders. First, never let sanitized values fall into a non-Unicode buffer. The implicit conversion is what creates the vulnerability. Microsoft's fix for the reported system-procedure case was simple: they widened the command buffer to nvarchar(4000) so the U+02BC never gets a chance to collapse into a quote. Second, and better, do not concatenate the value into the SQL string at all. Parameterize it, so the value is data and never code.

CREATE OR ALTER PROCEDURE dbo.usp_FindCustomer_Fixed
    @Name NVARCHAR(128)
AS
BEGIN
    SET NOCOUNT ON;

    -- Stays Unicode end to end: no best-fit collapse, no minted apostrophe.
    DECLARE @command NVARCHAR(4000);
    SET @command = N'SELECT TOP (100) CustomerId, LastName '
                 + N'FROM dbo.Customer WHERE LastName = @Name';

    -- @Name crosses as a PARAMETER, never concatenated into the text.
    EXEC sys.sp_executesql
         @command,
         N'@Name NVARCHAR(128)',
         @Name = @Name;
END;
GO

Now run both inputs with the fixed proc and there is no drama. The @Name value is not parsed as SQL. It is bound as a parameter, compared as data, and they both run properly. O'Brien returns and the lookalike matches no one and returns nothing. No conversion or broken quote. The empty result is the whole win. No REPLACE required, and that is the point. Hand-rolled escaping is a maintenance trap; parameters are the actual answer.

Cleanup

IF OBJECT_ID('dbo.usp_FindCustomer','P') IS NOT NULL DROP PROCEDURE dbo.usp_FindCustomer;
IF OBJECT_ID('dbo.usp_FindCustomer_Fixed','P') IS NOT NULL DROP PROCEDURE dbo.usp_FindCustomer_Fixed;
IF OBJECT_ID('dbo.Customer','U') IS NOT NULL DROP TABLE dbo.Customer;

Version notes

Nothing here needs a modern build. sp_executesql has shipped since forever, and the conversion behavior applies to any supported SQL Server version. One honest caveat -- best-fit mapping is collation dependent, so which Unicode lookalikes collapse into which ASCII characters can vary by the collation in play. The defense, however, does not vary. Keep the buffer Unicode and pass values as parameters, and the mapping never gets a turn.

More to Read

Simple Talk: Exposing a SQL injection vulnerability you have never heard of
Remus Rusanu: SQL Injection — casting can introduce additional single quotes
sqlfingers inc: AI Didn't Hack Mexico. SQL Injection Did