Tuesday, June 16, 2026

Fast Is Not Ready: What the AI Data Shows

There is a slide making the rounds in my world right now. You have probably seen a version of it. It promises a live AI agent in production, a validated ROI model, and a scale roadmap. Six weeks. Fixed price. The design is clean, the language is confident, and the offer is hard to argue with.

I want to clarify something before I go further. I am not anti-AI. I use it every working day, often inside the very SQL Server work that pays my bills. Where it lands on a real problem with real data, it earns its place at the table. This is not a 'STOP AI!' post.

My concern is more focused, and I think it is harder to dismiss. It is the speed. We are wiring these systems into production faster than we are building the boundaries that production demands. And the reason we are moving this fast is that someone has made moving fast look easy, packaged it up, and put a price on it. That package is worth a hard look before we talk about what it leaves behind.

What the packaging promises

The pitch is consistent across vendors -- A high-value use case, fully deployed. A cost model your CFO can sign. A backlog of the next ten use cases, ready to go. All of it on a timeline measured in weeks rather than quarters, and all of it for a number you agree to up front.

I understand why leaders say yes. It is a clean answer to a messy mandate. When the directive from the top is 'we must do AI now,' a fixed-price sprint to production looks like progress you can put on a slide. The trouble is that the slide and the outcome don't always follow the same path.

The door we are leaving open

Every agent stood up in a hurry is a new door into the environment, and too many of them are being hung without locks -- and the people who make a living finding unlocked doors have definitely noticed. Consider what has already happened, in production, to organizations far better resourced than most.

In June 2025, researchers disclosed a flaw in Microsoft 365 Copilot they named EchoLeak (CVE-2025-32711, rated 9.3 critical). It was a zero-click attack. An outsider sent a single crafted email, and when Copilot did the helpful thing and read it, hidden instructions inside told the assistant to go find sensitive material across OneDrive, SharePoint, Teams and Outlook and quietly ship it out. The victim clicked nothing. There was no malware to scan for, because the payload was plain English. Microsoft patched it and reported no exploitation in the wild, but the lesson should still stand: the assistant became an insider threat, and it never knew it.

Or consider the coding agent that, in 2025, deleted a production database despite instructions not to modify production systems, then produced inaccurate information about the recovery state. You can read about it here. No attacker required. The agent simply had more authority than judgment.

These are not edge cases. The OWASP project that tracks AI security risks used to catalog threats as things that might happen. Its current edition catalogs them as CVEs, vendor advisories and breach reports -- in other words, things that already happened. That is the part the vendor's six-week timeline never mentions. An agent that can read your data and act on your systems is a privileged user, and we are handing out that privilege faster than we are regulating who, what or how, it is managed.

What the data says

MIT's NANDA initiative studied 300 enterprise AI deployments. Roughly 95 percent of generative AI pilots produced no measurable impact on the bottom line. Only about 5 percent reached real, scaled value. I will be fair about that headline. It has been widely cited, though not without criticism of the methodology. But even the people pushing back tend to agree on the underlying point, which is that most pilots stall long before they reach production. You can read the coverage here.

Gartner is more direct, and more relevant, because the slide I described is selling agentic AI specifically. Gartner predicts that more than 40 percent of agentic AI projects will be canceled by the end of 2027. The reasons it gives are "escalating costs, unclear business value or inadequate risk controls." The full prediction is here.

Gartner even has a name for part of the problem, 'agent washing'. The practice of rebranding ordinary chatbots and automation as autonomous agents. By its count, of the thousands of vendors claiming agentic AI, Gartner estimates only about 130 of them are real. Most of what is being sold as an agent is an old tool wearing a new label.

The failures are not about the model

This is the part that should matter to anyone who runs systems for a living. Read the reasons again. Escalating costs. Unclear business value. Inadequate risk controls. Not one of them says the AI is too dumb. The pilots are not failing on intelligence. They are failing on the pieces that were missed: data that was never cleaned, integration that was never scoped, governance that was never written, controls that were never tested.

Those are boundaries, and a six-week, fixed-price sprint to production is, almost by definition, a sprint past those boundaries. You cannot clean the data, prove the ROI, establish governance, test the controls, and build the system all in the same six-week sprint. Something gets deferred. In most organizations, it is the part that was supposed to keep everyone out of trouble. In a regulated environment, the things that get cut are usually the same things that surface in an audit eighteen months later, long after the launch party.

What the 5 percent did differently

Here is the encouraging half of the story. The organizations that got real returns were not the fastest. They were the narrowest. They picked one painful, well-understood problem instead of trying to transform everything at once, and they fixed their data before they pointed a model at it. They also tended to partner rather than build everything in-house, and they often started in the boring back office rather than the flashy customer-facing demo -- where the value was quiet but real.

None of that fits on a six-week timeline, and none of it markets well. Patience is the least sellable quality in anybody's board room. It is also, apparently, the one that correlates with success.

Why I am writing this down

I am not asking anyone to slow the technology. At this point, I doubt anyone could if they tried. I am asking that we let the boundaries keep pace with it. Governance, data quality, risk controls, and a human who can still read the audit trail are not friction. On the evidence above, they are the entire difference between the 5 percent and the 95 percent.

The technology itself is going to be fine. It always is. My worry is for the organizations that bet the timeline before they build the guardrails, because that bill always comes due, and it does not come due on the quarter the slide was presented. Fast is exciting. Fast is sellable. But fast is not the same as ready. The organizations that succeed will not be the ones that moved first. They will be the ones that understood a very simple truth: Powerful systems require equally powerful controls.

xp_cmdshell: The Door Is Closed by Default. Keep It Shut.

This year's SQL Server CVEs have a common shape. CVE-2026-21262 (March Patch Tuesday, CVSS 8.8) lets an authenticated, low-privileged login climb to sysadmin over the network. That sounds bad on its own, but sysadmin is not the finish line for an attacker. It's just the on-ramp. The exit ramp is xp_cmdshell, and that is the piece that turns 'they got into the database' into 'what they ran under your service account'.

If you run managed services, you are not patching one instance, you are patching a fleet, and somewhere in that fleet is an instance where this gate is open and nobody noticed. Let's look at the gate and how to verify it is shut across the servers you manage.

The gate is closed by default

Since SQL Server 2005, xp_cmdshell ships disabled. Call it on a clean instance and you get the classic refusal:

EXEC xp_cmdshell 'whoami';
Msg 15281, Level 16, State 1, Procedure sys.xp_cmdshell, Line 1 [Batch Start Line 0]
SQL Server blocked access to procedure 'sys.xp_cmdshell' of component 'xp_cmdshell' because this 
component is turned off as part of the security configuration for this server. A system administrator 
can enable the use of 'xp_cmdshell' by using sp_configure. For more information about enabling 
'xp_cmdshell', search for 'xp_cmdshell' in SQL Server Books Online.

Good. The door is shut as it should be. The problem is how easy the door is to open when you're sysadmin, and how many times it's probably already been opened by some long-departed vendor install script.

Check the current state

Before anything else, find out where you actually stand. sys.configurations tells you both the saved value and the running value:

SELECT name,
       value,
       value_in_use,
       description
FROM   sys.configurations
WHERE  name = 'xp_cmdshell';

Of course, this is my server, so it's 1. That means xp_cmdshell is enabled. 0 would mean disabled. On your servers, if this comes back 1 and you did not put it there, that is an immediate concern.

Why this is a risk

Here is a demo so you can see exactly why this matters. Since my server already shows 1, I'll walk through how it gets opened in the first place. Enabling it takes four lines:

EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;

And now the door is open.

EXEC xp_cmdshell 'whoami';

Look at that closely. The command did not run as the login that called it. It ran as the SQL Server service account. Whatever that account can touch on the host and the network, the attacker can now touch, from a T-SQL prompt, no shell access required. Swap whoami for anything the service account can reach and you understand the rest of the story. The exposure. The risk.

Who runs as what

The execution context depends on who is calling, which is worth keeping straight:

Caller Command runs as
sysadmin member SQL Server service account
non-sysadmin (proxy configured) xp_cmdshell proxy credential
non-sysadmin (no proxy) Cannot execute

This is why CVE-2026-21262 is more than 'just' a privilege escalation. The climb to sysadmin hands an attacker the first row of that table, and the first row is the host.

Catch it being flipped on

A point-in-time check is fine, but you want to know when the value changed, not just that it is wrong today. Every sp_configure change gets written to the error log. Pull it straight out:

EXEC xp_readerrorlog 0, 1, N'xp_cmdshell';

We're looking for any lines recorded in the log including 'xp_cmdshell'. Mine was already at 1, so I flipped it to 0 to gen the image for this post. The point is that you can see the timestamp when the change was made. For something more durable than the rolling error log, a Server Audit capturing server configuration changes will keep the record where log cycling cannot quietly erase it.

Sweep the whole inventory at once

One instance at a time does not scale when you manage dozens. dbatools turns the same check into a single pass across every registered server:

Get-DbaSpConfigure -SqlInstance $instances |
    Where-Object Name -eq 'XpCmdShellEnabled' |
    Select-Object SqlInstance, ConfiguredValue, RunningValue

Any server where the running value is 1 goes to the top of your review list.

Shut the door and keep it shut

If nothing legitimately needs xp_cmdshell, the cleanup is the same shape as the setup, in reverse:

EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE;
EXEC sp_configure 'show advanced options', 0;
RECONFIGURE;

Beyond that toggle, the boring controls are the ones that hold. Keep the SQL Server service account on a least-privilege footing so that even if the door is opened, the service account is not a domain admin waiting to be borrowed. Do not expose the instance to the internet, because the scanners already know your port is there. And patch the escalation paths, because the whole reason xp_cmdshell is a back half of an attack is that something else got the attacker to sysadmin first.

The door is closed by default purposely. Your job is to keep it closed, or IF it needs to be opened, be sure to close it properly afterward.

Applies to SQL Server 2005 through 2025. xp_cmdshell has been disabled by default the entire time, which means anywhere you find it on was a deliberate act, by someone.

More to Read

xp_cmdshell Server Configuration Option (Microsoft Learn)
xp_cmdshell (Transact-SQL) (Microsoft Learn)
Create a Server Audit and Server Audit Specification (Microsoft Learn)

Monday, June 15, 2026

Kali365: The FBI Just Warned You About Your Azure SQL Login Screen

The FBI put out a PSA this month about a phishing kit called Kali365. And, if you've ever connected to Azure SQL with Entra authentication, you have already stared at the exact screen this whole attack depends on.

To sign in, use a web browser to open the page https://microsoft.com/devicelogin
and enter the code A1B2C3D4E to authenticate.

Look familiar? That is the Microsoft OAuth device code flow. It is legitimate, it is everywhere, and Kali365 weaponizes it without ever touching your password.

What Kali365 Actually Does

It is a subscription service. First spotted in April 2026, sold over Telegram for roughly $250 a month or $2,000 a year, marketed at people who could not write a phishing kit themselves. For that money they get AI-generated lures, automated campaign templates, live targeting dashboards, and the piece that matters -- the OAuth token capture.

Instead of stealing a password, the attacker tricks the victim into completing a sign-in the attacker started. Microsoft then issues access tokens to the attacker's session, already satisfying MFA requirements. The result is access to Outlook, Teams, OneDrive, and other Microsoft 365 resources without ever knowing the victim's password.

Why This Should Make a DBA Twitch

Because you do this on purpose, all the time. Connect to an Entra-backed Azure SQL database with device code auth and SQL Server tells you to go enter a code at a Microsoft page:

sqlcmd -S myserver.database.windows.net -d mydb -G ^
  --authentication-method=ActiveDirectoryDeviceCode

You get a code, you open microsoft.com/devicelogin to type it in, and you are connected. SSMS does the same thing under 'Azure Active Directory - Device Code Flow'. The flag name varies a bit by sqlcmd version, but the muscle memory is identical, and that muscle memory is exactly what the attack is counting on.

Step You, connecting to Azure SQL You, getting phished
1 You run sqlcmd / SSMS Attacker starts a sign-in for your tenant
2 Microsoft returns a device code Attacker emails you that code
3 You open microsoft.com/devicelogin You open microsoft.com/devicelogin
4 You enter the code, you get in You enter the code, they get in

Same page. Same code box. Same real Microsoft domain. But two things change: who started the sign-in and who gets in.

There Is Nothing to Misspell

This is what makes it nasty. Every 'spot the phish' habit we have trained on is about catching a fake. The wrong domain, the off-color logo, the cert warning. Here there is no fake. The page is genuinely microsoft.com. The token grant is genuinely Microsoft issuing a token. The only lie in the entire transaction is the unspoken assumption that you are the one who kicked it off. You are not. The attacker is, and you are politely finishing their login for them.

Hunt It in Your Own Tenant

Want to know if this already happened in your tenant? The Microsoft Graph PowerShell module will tell you. You need the module installed and access to Entra sign-in logs. Available retention periods and reporting capabilities depend on your licensing tier.

# One-time: install the module
Install-Module Microsoft.Graph -Scope CurrentUser

# Connect with just the scope you need
Connect-MgGraph -Scopes 'AuditLog.Read.All'

Now pull the last 7 days of sign-ins and keep only the ones that used the device code flow. I filter client-side on AuthenticationProtocol because the server-side $filter does not reliably support that property across tenants:

$start = (Get-Date).AddDays(-7).ToString('yyyy-MM-ddTHH:mm:ssZ')

Get-MgAuditLogSignIn -Filter "createdDateTime ge $start" -All |
    Where-Object { $_.AuthenticationProtocol -eq 'deviceCode' } |
    Select-Object CreatedDateTime, UserPrincipalName, AppDisplayName, IpAddress,
        @{ N = 'City';    E = { $_.Location.City } },
        @{ N = 'Country'; E = { $_.Location.CountryOrRegion } } |
    Sort-Object CreatedDateTime -Descending |
    Format-Table -AutoSize

A clean tenant gives you either nothing or a short list you recognize, with your own admin box connecting to Azure SQL (sample output, values are illustrative):

CreatedDateTime       UserPrincipalName     AppDisplayName              IpAddress      City     Country
--------------------  --------------------  --------------------------  -------------  -------  -------
2026-06-14 09:12:03   dba@contoso.com       Microsoft Command Line ...  203.0.113.10   Cozumel  MX

What you are hunting for is the row nobody can explain. A sign-in no one on your team started, from somewhere you do not operate, often against an app you never deploy:

CreatedDateTime       UserPrincipalName     AppDisplayName              IpAddress      City     Country
--------------------  --------------------  --------------------------  -------------  -------  -------
2026-06-13 02:47:55   finance@contoso.com   Microsoft Authentication..  185.220.101.4  Unknown  RO

Device code flow is something developers and admins use to log in from places a browser cannot easily go. So when a finance mailbox completes one at 3 AM from a country you don't operate in, that is the tell. Nobody on your team started that login. Someone else did, and your user finished it for them.

What To Actually Do

Never finish a sign-in you did not start.

A device code is only safe when you generated it yourself. A code that arrives by email or Teams that you did not ask for is bait, every single time. There is no legitimate reason for someone to send you one in this fashion.

Admins: block the flow if you do not need it.

Microsoft Entra Conditional Access can block the device code flow tenant-wide. Most organizations use it in a handful of narrow spots, so a default block with a tight exception list could shut a lot of this down. If your team only hits device code auth for the odd Azure SQL connection, you might consider whether you even need it enabled broadly.

If you got caught, report it.

The FBI is collecting these at the Internet Crime Complaint Center. Revoke sessions, rotate where you can, and then file.

The convenience that lets you authenticate to a database from a headless box with no browser is the same convenience that lets a $250 kit drain a mailbox. The flow is not the villain. Typing a code you did not ask for is.

More to Read

FBI IC3 Public Service Announcement on Kali365
Bitdefender: Kali365 phishing kit breaks Microsoft 365 accounts, no password required
Arctic Wolf: Token Bingo, don't let your code be the winner
Internet Crime Complaint Center (file a complaint)

Saturday, June 13, 2026

Cursed SQL, Part Two: Six Ways NULL Lies to Your Face

A week ago I wrote about six queries that run fine until they don't. Cursed SQL. That one struck a nerve, so here's the sequel. Same idea, but this time NULL is the one villain behind every example, and the three-valued logic that rides along with it.

These queries are worse in a quieter way. They don't run slow. They never throw an error. They just give you the wrong answer every time, and SQL Server never says a word. That is the curse.

First, the One Rule

SQL Server does not deal in just true and false. It uses a three-valued logic system, or '3VL', in which conditions must evaluate to TRUE, FALSE, or UNKNOWN. That third one comes to the table whenever NULL is involved in a comparison because NULL is not a value. NULL is not zero and it does not equal anything -- not even another NULL. Any direct comparison to NULL always evaluates to UNKNOWN. Never TRUE or FALSE. Just UNKNOWN.

That single rule is behind all six curses below. Keep it in your pocket and you'll start to see them coming.

The Setup

An Employee table with a few NULLs planted exactly where real data goes missing. A CEO with no manager, an employee not yet assigned a department, and an employee missing their salary. Simple omissions that happen all the time.

CREATE TABLE dbo.Employee
(
    EmployeeID  INT IDENTITY(1,1) PRIMARY KEY,
    FullName    NVARCHAR(100) NOT NULL,
    ManagerID   INT           NULL,   -- the CEO reports to no one
    Department  NVARCHAR(50)  NULL,   -- some not yet assigned
    Salary      DECIMAL(10,2) NULL    -- some withheld
);

INSERT dbo.Employee (FullName, ManagerID, Department, Salary)
VALUES (N'Dana Reyes', NULL, N'Executive', 250000),  -- CEO, no manager
       (N'Sam Carter', 1,    N'Sales',     90000),
       (N'Priya Nair', 1,    N'Sales',     NULL),     -- salary withheld
       (N'Tom Becker', 1,    NULL,         75000),    -- department not assigned
       (N'Lena Frost', 2,    N'Sales',     82000);

See our data with the NULLs:

Curse 1: NOT IN Hits a NULL and Returns Nothing

You're reporting for HR and you want to know which Departments have nobody assigned yet. So you pass the department values into your query and ask the system which ones don't exist in the Employee table.

-- Which departments have nobody assigned? (ie., the empty ones)
SELECT DeptName
FROM (VALUES (N'Executive'), (N'Sales'), (N'Engineering')) AS d (DeptName)
WHERE DeptName NOT IN (SELECT Department FROM dbo.Employee);

Dana is in Executive, three people are in Sales, and nobody has been assigned to the Engineering department yet. So that's the one you expect in the return, but you get this instead:

Nothing. Why isn't Engineering returned?

Here's the problem, step by step. First, we must recognize that the subquery actually returns every Department value in the table:

One of those values is NULL because Tom has not been assigned a department yet, and that NULL is the entire problem. Here's how NOT IN works: it takes a row's DeptName and compares it to every value in that list with 'not equal' (<>), joining the results with AND. So this:

DeptName NOT IN ('Executive', 'Sales', NULL)

Actually translates to this:

DeptName <> 'Executive'  AND  DeptName <> 'Sales'  AND  DeptName <> NULL

A result to that query only survives if its DeptName is not equal to all of them.

Now walk the Engineering row through it -- the one you actually expected back. For that row, DeptName is 'Engineering'. It is not equal to Executive or Sales, so both comparisons pass... but then it hits the NULL:

'Engineering' <> 'Executive'  ->  TRUE
'Engineering' <> 'Sales'      ->  TRUE
'Engineering' <> NULL         ->  UNKNOWN   (any compare to NULL is UNKNOWN)

TRUE AND TRUE AND UNKNOWN      ->  UNKNOWN

That's what gets you. Engineering genuinely has no employees, so without that NULL in the list, it would have been returned. But remember, TRUE + TRUE + UNKNOWN = UNKNOWN. Not TRUE. A row will only be returned when its WHOLE chain evaluates to TRUE, and that UNKNOWN is why your result came back empty.

Microsoft says so right in the docs: "Any null values returned by subquery or expression that are compared to test_expression using IN or NOT IN return UNKNOWN."

The fix is to stop using equality for this. NOT EXISTS tests for the existence of a matching row, not equality against a list, so a stray NULL doesn't poison it.

-- Fix 1: NOT EXISTS is immune to the NULL trap
SELECT d.DeptName
FROM (VALUES (N'Executive'), (N'Sales'), (N'Engineering')) AS d (DeptName)
WHERE NOT EXISTS (SELECT 1
                  FROM dbo.Employee e
                  WHERE e.Department = d.DeptName);

-- Fix 2: keep NOT IN, but strip the NULLs out of the subquery
SELECT DeptName
FROM (VALUES (N'Executive'), (N'Sales'), (N'Engineering')) AS d (DeptName)
WHERE DeptName NOT IN (SELECT Department
                       FROM dbo.Employee
                       WHERE Department IS NOT NULL);

Both return Engineering. Make NOT EXISTS your habit. It sidesteps this entirely, and often gets you better performance as a bonus.

Curse 2: The Inequality That Silently Drops the Row You Wanted

You want everyone who is not in Sales. Reasonable.

SELECT FullName, Department
FROM dbo.Employee
WHERE Department <> N'Sales';

You expected Dana in Executive and Tom, whose department is not assigned yet, but you get only Dana. Tom vanishes, because NULL <> 'Sales' is UNKNOWN, not true, and his row never qualifies. The employee with no department -- arguably the exact person you were hunting for -- silently falls out of the 'not in Sales' report. The NULL rows fall through the cracks and your data set is incomplete.

Use this method to account for any NULL values, and Tom comes back:

SELECT FullName, Department
FROM dbo.Employee
WHERE Department <> N'Sales' OR Department IS NULL;

Curse 3: The Average That Quietly Skips a Person

This one hands your boss a wrong number that looks completely right. You want the team's average salary -- one figure for a comp review.

SELECT AVG(Salary)   AS AvgSalary,
       COUNT(*)      AS HeadCount,
       COUNT(Salary) AS PaidCount
FROM dbo.Employee;

Five people on the team, but watch the counts. COUNT(*) returns 5 -- every row. COUNT(Salary) returns 4 -- only the rows with a salary. Priya's is NULL, and AVG skipped her, because aggregates ignore NULL instead of treating it as zero. So your 'average' added up four salaries and divided by four, not five. The figure looks fine, but it quietly left a person out.

SQL Server does warn you, exactly once, in a message most tools tuck out of sight:

Warning: Null value is eliminated by an aggregate or other SET operation.

That warning is the entire story. A row was removed from your math and the engine mentioned it on the way out. The point is not that one number is right and the other wrong. The point is that the default decided for you. If a withheld salary should count as zero, say so:

SELECT AVG(ISNULL(Salary, 0)) AS AvgSalary_NullAsZero
FROM dbo.Employee;

Same data, divided by five this time, on purpose. Different answer, and now it's mathematically correct.

Curse 4: One NULL Poisons the Whole String

You want a tidy label for each employee.

SELECT FullName + ' - ' + Department AS Label
FROM dbo.Employee;

Four rows look fine, but what happened to Tom? He came back as NULL. Curious why it didn't come back as 'Tom Becker - ' ? The entire string collapsed to NULL. This happens with the + operator. If any piece is NULL, the whole result becomes NULL. That is CONCAT_NULL_YIELDS_NULL doing its job, and since SQL Server 2017 it is always ON -- you can't turn it off anymore. So a single missing department wipes out the entire label.

The fix is CONCAT, which treats NULL as an empty string instead of a contaminant. Or guard each piece yourself if you'd rather show a placeholder.

-- CONCAT() ignores NULLs, treating them as empty strings
SELECT CONCAT(FullName, ' - ', Department) AS Label
FROM dbo.Employee;

Or you can use a default to show a placeholder:

SELECT FullName + ' - ' + ISNULL(Department, N'(unassigned)') AS Label
FROM dbo.Employee;

CONCAT gives you 'Tom Becker - ' with an empty tail; the ISNULL version gives you 'Tom Becker - (unassigned)'. CONCAT arrived in SQL Server 2012, so it's most likely a non-issue on anything you're running today.

Curse 5: = NULL Is Not IS NULL

You want to list all employees with no department assigned.

SELECT FullName
FROM dbo.Employee
WHERE Department = NULL;

Zero rows. But what about Tom? Remember, Department = NULL means UNKNOWN for every row, including the ones that actually are NULL, because nothing equals NULL. Not even NULL equals NULL. The right tool is the predicate built for the job:

SELECT FullName
FROM dbo.Employee
WHERE Department IS NULL;

Now Tom comes back. Years ago, with ANSI_NULLS OFF, = NULL would behave like IS NULL, but that setting is deprecated and ANSI_NULLS has been forced ON since SQL Server 2017. Don't lean on it. Use IS NULL and trust your results.

Curse 6: The CHECK Constraint That Waves NULL Right Through

This is the sneaky one, and most people don't know it. You add a guard rail to keep salaries positive.

ALTER TABLE dbo.Employee
ADD CONSTRAINT chk_salary_positive CHECK (Salary > 0);

It creates without question or error -- even though Priya already sits in the table with a NULLable salary. That should be your first clue. Now insert a brand new row with no salary at all:

INSERT dbo.Employee (FullName, Salary) VALUES (N'Ghost Worker', NULL);

It goes right in. The constraint you wrote to guarantee a positive salary just accepted a missing one. Here is why: a CHECK constraint rejects a row only when the condition evaluates to false. NULL > 0 is UNKNOWN, not false, so the row is allowed. CHECK treats true and UNKNOWN as the same verdict -- both pass. Your 'salary must be positive' rule quietly permits 'salary is missing entirely'.

The fix is to say what you actually mean. If a salary is required, the column belongs NOT NULL (clean the data first, then alter it). If NULL is genuinely allowed but any real value must be positive, spell that out in the constraint itself:

-- clear the ghost worker record
DELETE dbo.Employee WHERE FullName = N'Ghost Worker';

-- drop constraint 
IF OBJECT_ID('chk_salary_positive') IS NOT NULL
ALTER TABLE dbo.Employee DROP CONSTRAINT chk_salary_positive;

-- clean up the data with null salary
DELETE dbo.Employee WHERE Salary IS NULL;        -- this clears Priya too

-- make column not null
ALTER TABLE dbo.Employee ALTER COLUMN Salary DECIMAL(10,2) NOT NULL;

-- above prevents NULL, this enforces that all values are positive
ALTER TABLE dbo.Employee
ADD CONSTRAINT chk_salary_positive CHECK (salary > 0);

-- Now try that insert again
INSERT dbo.Employee (FullName, Salary) VALUES (N'Ghost Worker', 00000.00)

The lesson holds for every CHECK you write. The constraint does not enforce what you assume. It enforces what evaluates to false. The corrected constraint now blocks zero and negatives, and NOT NULL is what handles the missing values.

In Summary

Six shapes, one root cause. NULL means 'I don't know', 3VL logic is how the engine handles it, and UNKNOWN is the value that falls through the cracks more often than not. None of these throw errors or run slow. They just produce the wrong answers with every call.

The fixes aren't exotic. Pretty simple, actually. Reach for NOT EXISTS instead of NOT IN. Account for NULL on both sides of an inequality. Decide what your aggregates should do with missing values instead of letting AVG decide for you. Use CONCAT over +. Write IS NULL, never = NULL, and remember that a CHECK constraint only ever blocks false. Get those right and the curse lifts. Now you've got six more shapes you can spot on sight.

More to Read

sqlfingers inc: Cursed SQL -- Six Queries That Run Fine Until They Don't
Microsoft Learn: IN (Transact-SQL)
Microsoft Learn: SET ANSI_NULLS (Transact-SQL)
Microsoft Learn: SET CONCAT_NULL_YIELDS_NULL (Transact-SQL)
Microsoft Learn: CONCAT (Transact-SQL)

Friday, June 12, 2026

Miasma. The Worm That Fires When You Open the Folder.

Last Friday, June 5th, GitHub yanked 73 Microsoft repositories offline in about 105 seconds. Not fringe stuff, either. They were repos across the Azure, Azure-Samples, Microsoft and MicrosoftDocs organizations, including Azure/durabletask and Azure/functions-action. The culprit is a self-replicating supply chain worm the researchers are calling Miasma, and this is the part that gets my attention: The payload fires when you open the repository in an AI coding tool or IDE. You don't have to run anything. You don't have to install anything. You just open the folder.

Maybe you're thinking 'I'm a DBA, not a developer, this isn't my problem', but stay with me. I'll explain why your scripts folder is exactly the kind of target this thing was built for.

What happened

An attacker used a previously compromised contributor account to push a malicious commit to the Azure/durabletask repository. The same account had already been used in May to publish three poisoned versions of the durabletask Python SDK to PyPI, so this was strike two from the same stolen identity. The commit didn't poison a package this time. Instead, it planted configuration files inside the repo itself -- the kind of files that AI coding assistants and modern IDEs execute automatically when a project is opened.

Open an infected repo in Claude Code, Gemini CLI, Cursor or VS Code, and a credential-harvesting payload runs immediately. It grabs tokens for GitHub, cloud platforms and developer tooling, then uses those stolen credentials to commit itself into every other repository the victim can write to. That's the worm part -- it spreads on its own, no human required.

GitHub's automated abuse detection caught it and disabled all 73 repositories in two sweeps over roughly 105 seconds. Microsoft has since reviewed and restored them. But the takedown itself caused collateral damage -- Azure/functions-action went dark, and every CI/CD workflow referencing it broke on the spot.

The three waves

Miasma has been active since June 1st, and it's evolving fast. It's a variant of the 'Mini Shai-Hulud' worm that was publicly released in mid-May, and it has already used three distinct attack vectors:

Wave Date Vector
1 June 1 Malicious npm packages with preinstall hooks
2 June 3 Malicious binding.gyp files that execute during npm install ('Phantom Gyp')
3 June 3-5 Malicious commits to GitHub repos; payload fires when repo is opened in an AI tool or IDE

Read that closely. Waves 1 and 2 still required an install step, but wave 3 only requires curiosity. Can you even measure the extent of that exposure?

Why DBAs should care

Think about what's sitting in your scripts folder right now. If you're like most working DBAs, you've got clones of community tooling -- dbatools, the First Responder Kit, Ola Hallengren's maintenance solution, plus a pile of your own repos. And increasingly, you're opening those folders in VS Code, or pointing an AI assistant like Claude Code or Copilot at them to refactor a script or troubleshoot a job.

Now think about what's next to those scripts. Connection strings. Saved credentials for dbatools sessions. SQL logins in config files. Azure service principal secrets. A harvested credential from a developer workstation is bad; a harvested credential from a DBA workstation is the keys to the data layer. A worm won't know the difference between a developer's GitHub token and your sysadmin-equivalent service account -- and it doesn't care. It takes everything it can reach.

I should say, this isn't a 'dbatools is compromised' post. None of the SQL Server community projects were among the 73 disabled repos. But the attack pattern is now public, and the worm's ancestor was published openly for anyone to adapt. For years the trust model has been simple -- clone it from GitHub, it's from a reputable org, it's fine. That model just took a direct hit. If Microsoft's own Azure org can host a poisoned commit for two weeks, any repo can.

The mechanics, briefly

Modern IDEs and AI coding tools support automation hooks, driven by configuration files inside the project. They run when a project opens -- VS Code tasks with folder-open triggers, Claude Code hooks, and similar mechanisms in other tools. These features exist for legitimate reasons (auto-build, environment setup), but they mean a repository is no longer just passive text. It can carry instructions that your tooling obeys the moment you open it.

In this campaign, the malicious commit added exactly those kinds of configuration files. Researchers also observed the payload establishing persistence through VS Code tasks and Claude Code hooks, and exfiltrating stolen data through channels like GitHub 'dead drop' repos.

What to do about it

1. Turn off automatic task execution in VS Code

This single setting closes the folder-open execution vector in VS Code. It's prompt-based by default in recent versions, but verify it -- and set it explicitly via settings.json:

"task.allowAutomaticTasks": "off"

2. Inspect before you open

Before opening any freshly cloned repo in an IDE or AI tool, look at it with something dumb first -- plain file explorer, notepad, a directory listing. The files that matter are the automation hooks. A quick PowerShell sweep of your scripts directory:

Get-ChildItem -Path 'C:\Scripts' -Recurse -Force |
    Where-Object { $_.Name -in ('tasks.json','settings.json','binding.gyp') `
        -or $_.FullName -like '*\.claude\*' `
        -or $_.FullName -like '*\.vscode\*' } |
    Select-Object FullName, LastWriteTime |
    Sort-Object LastWriteTime -Descending

You're not looking for these files to never exist. The .vscode folders are everywhere and almost always benign. You're looking for the files you didn't put there, or those with a LastWriteTime that doesn't match when you last touched the project.

3. Get the secrets out of the scripts folder

Maybe this incident is the push you needed. No connection strings, passwords or service principal secrets in repos or alongside them. Use Windows Credential Manager, Azure Key Vault, dbatools' Export-DbaCredential patterns -- or one of the gazillion different password managers out there to keep your passwords secure. Or at least out of the path that a folder-scanning payload could walk.

4. If you opened a Microsoft repo recently, assume exposure

The window of concern runs roughly May 20 through June 5. If you -- or any scheduled job or build server you own -- pulled or even just opened anything from the affected orgs in that window, you should consider rotating the credentials that machine could reach. GitHub tokens, cloud secrets, and yes, any SQL logins whose passwords were stored locally. Check your cloud environments for service principals you didn't create and outbound traffic you can't explain. The fact that this check has become a standard step in Cloud/AI security really should not be missed.

5. Pin your community tooling

Remember that scripts folder full of dbatools, the First Responder Kit and Ola's maintenance solution? This is where the trust-model problem becomes a daily-habit problem. Clone a specific release tag, not main. Review the diff when you update. The convenience of 'git pull and go' is exactly the behavior this worm is engineered to exploit.

The bigger picture

I wrote before about least privilege for AI agents -- the idea that an AI assistant should only be able to touch what it genuinely needs. Miasma is the other side of that same coin. The AI tooling isn't the villain here. The worm exploited automation features, and AI assistants happen to be the newest and most eagerly adopted automation there is. New actors, but the lesson is the same. Every tool that can act on your behalf is a tool an attacker can act through. Scope it down, watch what it opens, and trust none of them by default -- not even the ones from a trusted org.

GitHub caught this one in 105 seconds. The next one might not trip the alarms as fast. Spend the ten minutes on the settings and the secrets cleanup now, while it's a news story and not an incident report with your name on it.

More to Read

StepSecurity: Miasma Worm Hits Microsoft Again
The Register: GitHub nukes 70+ Microsoft repos amid suspected worm attack
The Hacker News: Miasma Worm Hits 73 Microsoft GitHub Repositories
Redmondmag: Supply Chain Attack Hits Microsoft GitHub Repos, AI Coding Tools

Wednesday, June 10, 2026

Fabric Is the Destination. Who's Ready?

A reader says to me yesterday very directly during a linkedin exchange: "All of the data leadership is focused on Fabric." Nine words. He is right and I will not argue the point. The evidence is everywhere. But 'where leadership is focused' and 'what works for the small shops' are two different things, and only one of them gets answered in the keynote. Fabric is the destination. The question nobody on stage is asking is who is actually ready to go there -- and what the costs may be for those who are not.

The Tell Isn't a Keynote. It's a Feature.

You do not have to read Microsoft's strategy in a press release. You can read it in what they built into the flagship database release. SQL Server 2025's headline analytics capability is not a faster engine or a smarter optimizer. It is Mirroring to Microsoft Fabric, and it went generally available alongside the release.

Here is what it does. You point Fabric at your SQL Server, pick the tables you want, and it continuously replicates them into OneLake -- Fabric's storage layer -- with no ETL, no pipeline, no orchestration. On SQL Server 2025, the engine itself scans the transaction log at high frequency and publishes the changes; Fabric merges them in near-real-time, as fast as every 15 seconds. On 2016 through 2022, it rides on Change Data Capture instead. Either way the data lands in OneLake as read-only Parquet, ready for analytics, BI -- and AI.

Read that again, because the direction is the whole story. The marquee analytics feature of the newest SQL Server is a one-click, zero-ETL pipe whose entire job is to copy your data out of SQL Server and into Fabric. When the headline of your flagship database is 'we made it trivial to send your data somewhere else', that somewhere else is where the value is being built.

One SQL, One Direction

The positioning also backs the plumbing. March 2026 brought the first-ever SQLCon -- run not on its own, but inside FabCon, framed as unifying databases and Fabric on a single platform. The database and the platform now share one stage and one story. And the momentum behind that story is not subtle. By Microsoft's own numbers, Fabric has passed a 2 billion dollar annual revenue run rate, serves more than 31,000 customers, and is growing about 60% year over year -- the fastest-growing analytics product they currently have.

Set that next to the other half of the picture. Fabric is now the named successor to Azure Synapse. Synapse is still supported, but Microsoft has been clear that all new analytics innovation is going into Fabric, and specific Synapse pieces are already being retired in its favor. Line the signals up, and they all point the same way.

The signal What it tells you
Mirroring SQL Server to Fabric
went GA, on-prem included
Microsoft built the on-ramp from
your estate into theirs, zero-ETL.
SQL Server 2025 added a native
log-scan publisher for Fabric
The flagship engine release's analytics
headline is, in effect, 'feed Fabric.'
The first SQLCon ran
inside FabCon
The database and the platform now
share one stage and one roadmap.
Fabric named Synapse's successor;
new analytics work goes there
The investment is pointed at the
SaaS platform, not the box engine.

The data flows one way. Operational SQL Server feeds analytical Fabric. Your engine is being recast as the trusted source of record -- the place where your data is born and kept honest -- but it is also no longer the place where the analysis happens.

What This Is Not

Now the part the panic merchants will skip. Fabric is not replacing your transactional SQL Server, and nobody is migrating your order-entry system to a lakehouse. Mirroring is read-only and analytical by design. The copy to OneLake is for reporting and AI, not for inserts or writing. The OLTP engine that runs your business is going nowhere.

So this is not a funeral, and it is not a reason to let a vendor sell you a frightened migration. The engine is healthy. What has changed is not whether SQL Server survives. It is where the new value gets built on top of it.

Who's Ready? The Part Nobody's Costing

Here is where the keynote goes quiet. Fabric is the destination, fine. But the people who own a SQL Server, a rack of SSIS packages, and a budget they have to defend are not asking 'is Fabric the future.' They are asking 'does this work for my shop', and 'can I afford it'. Those answers are a lot less tidy than the slide deck.

Start with the money, because the shape of it is the opposite of what you own today. A SQL Server license is a capital cost on hardware you control. You buy it, you run it, it is yours. Fabric is a meter. You rent capacity by the hour, and the meter runs every hour the capacity is on. Here is the pay-as-you-go list, US region, per month.

Fabric capacity Pay-as-you-go
(list, per month)
F2 (entry) ~$263
F8 ~$1,051
F32 ~$4,205
F64 ~$8,410

F2 at about $263 a month looks approachable, and that is the number that gets quoted. Two things it does not tell you. First, the way you make pay-as-you-go cheap is to pause the capacity nights and weekends. But, the near-real-time mirroring needs the capacity running to stay near-real-time, so the live feed and the pause-to-save trick will counter each other. Second, and this is the one that makes people blink, below F64 every single report viewer still needs a Power BI Pro license, roughly $10 per user per month. The capacity that lets viewers in for free is F64, at about $8,410 a month -- roughly thirty times the F2. So the small shop that picks F2 to be frugal pays the entry rate plus a per-seat tax on everyone who opens a report, and the next rung up that removes the tax is a canyon away. Reserved pricing trims the capacity bill by 30 to 40 percent, but it commits you to paying around the clock, idle hours included. You have traded a server you owned for a subscription you cannot turn off.

None of that makes Fabric a bad platform. It makes it a different cost model, billed monthly, forever, and pointed at organizations big enough to keep a capacity busy. For the small and mid-size shop, 'free of a license' is not the same as 'cheap,' and nobody selling the destination is doing this arithmetic out loud.

And SSIS.

This is the part that costs me something to write. SSIS has been a workhorse in my career and a lot of yours, and it still ships with SQL Server 2025 and is fully supported. But Fabric has no SSIS runtime. The data-movement story over there is Data Factory pipelines and Dataflows Gen2 -- which means an SSIS-heavy shop does not migrate to Fabric, it rewrites. Every package is a re-implementation in a different tool, and the investment, the tooling, and the momentum are all on the new side. You can keep running SSIS for years. You just cannot bring it with you. Pretending otherwise does not change the rewrite waiting at the other end, and that rewrite is exactly the kind of cost that turns 'we are moving to Fabric' into a multi-year line item nobody scoped.

What It Means for the On-Prem DBA

If the gravity has moved, the practical question is what that does to your job. A few things, and none of them are panic.

The investment is moving up the stack.

This is the same pattern I wrote about in the Postgres piece. Microsoft feeds the layer where its customers are spending, and right now that layer is the SaaS analytics platform, not the box engine. The engine gets hardening and careful maintenance. Fabric gets the new features and the 60% growth. Plan your attention accordingly.

Mirroring makes your data Fabric's concern, which makes the plumbing yours.

A mirror is not free of consequences for the DBA. It means a dedicated Fabric login reading your transaction log, an on-premises data gateway sitting inside your network, and a real decision about which tables leave the building. That is a new security surface, and it lands on your desk. Scope the login down, watch the gateway, and treat 'what gets mirrored' as a data-governance question, not a checkbox. Least privilege, again, same as it ever was.

The skills hedge is cheap.

You do not have to become a Fabric engineer. But knowing how your data gets mirrored, and who can reach the copy in OneLake, is now part of the job description whether you asked for it or not. Learning the mirroring path on a test database is a few hours. Call it insurance.

The Bottom Line

So yes -- follow the headcount and you find the roadmap, and it all says Fabric. I am not arguing with that. My reader was right. The data leadership is focused on Fabric and the feature list proves they mean it. But focus does not also mean fit. The destination is set for the large, capacity-filling, analytics-hungry shop. For the small one, the SSIS-heavy one, the one still scrambling to clear the 2016 finish line, the honest answer to 'who is ready' is 'not yet', and the road may be longer and more expensive than anyone on stage is admitting. That is not a reason to refuse the trip. It is a reason to cost it out properly, before someone who has never met your SSIS estate puts a date on the calendar for you.

More to Read

Microsoft Fabric Blog: Mirroring for SQL Server in Microsoft Fabric (Generally Available)
Microsoft Learn: Mirroring in Microsoft Fabric (overview)
Microsoft Azure: Microsoft Fabric pricing (capacity F-SKUs)
Microsoft Fabric Blog: From Azure Synapse and Azure Data Factory to Microsoft Fabric
sqlfingers inc: Why Are We Still Paying for SQL Server?

Tuesday, June 9, 2026

Patch Tuesday June 2026: Skipped SQL Server and Landed on Your Firmware

It is Patch Tuesday again. Microsoft shipped around 200 fixes today and three publicly disclosed zero-days, and not one of them is in SQL Server. Just like May, the database engine sits this one out entirely with zero SQL Server CVEs. But that does not mean close up shop and go home early. We'll run the June numbers first, then get to the date that actually matters this month -- and it's not June 9th. It is late June, when a set of Secure Boot certificates that have been sitting in your firmware since 2011 start to expire.

The June Numbers

Counts vary a little by who is tallying... BleepingComputer puts it at 200 and some trackers land near 198, but the shape is clear either way. Roughly 200 fixes, 33 rated critical, and 28 of those critical bugs are remote code execution. Elevation of privilege dominated the overall list. Three zero-days, all publicly disclosed ahead of the patch, none flagged as exploited in the wild at release.

Zero-day Component Type
CVE-2026-45586 Windows Collaborative
Translation Framework (CTFMON)
EoP to SYSTEM
CVE-2026-49160 HTTP.sys ('HTTP/2 Bomb') Denial of service
CVE-2026-50507 Windows BitLocker Security feature bypass

That elevation-of-privilege tilt across the full list is worth a beat. An EoP bug is rarely how an attacker gets in. It is how they take over once they are already in. The move that turns one compromised account or service into SYSTEM, with full control of the machine. The CTFMON zero-day above is exactly that pattern: not a front door, but a fast way to own the machine once someone is already inside. On a SQL Server host, 'already inside' plus SYSTEM is the whole ballgame, with full control of the instance and every database on it.

Interesting side note: Windows Secure Boot also took eight security-feature-bypass fixes this month. Attackers keep poking at the boot path -- which is exactly where this month's real story is headed.

What SQL Shops Should Actually Patch

Zero SQL Server CVEs does not mean zero work. Your SQL Server boxes are Windows boxes, and a few items in this release land squarely on the hosts behind your SQL Servers.

Hyper-V guest escape, if you virtualize.

Three critical Hyper-V RCE bugs (CVE-2026-47652, CVE-2026-45641, CVE-2026-45607) can let code escape a guest VM onto the host. If your SQL instances run on Hyper-V, the host patch is the one to move on first.

Cryptographic Services and RDP.

A critical elevation-of-privilege bug in Microsoft Cryptographic Services (CVE-2026-44810) hits a foundational subsystem, and the Remote Desktop client picked up a cluster of RCE fixes. Both are normal-priority for a managed estate, but they are on your servers whether or not SQL is named.

Nothing here says break your change window, but nothing here is optional either. Run them through your usual patch process.

The Date That Actually Matters: Secure Boot

Here are the dates to put on your calendar. Secure Boot verifies your bootloader and early-boot components before Windows starts, and that trust chain leans on Microsoft certificates issued back in 2011. Those certificates were minted with a fifteen-year life, and the clock runs out this year, in stages.

Certificate Role Expires
Microsoft Corporation
KEK CA 2011
Key Exchange Key -- authorizes
updates to the DB/DBX databases
June 24, 2026
Microsoft UEFI
CA 2011
Signs third-party bootloaders
and option ROMs
June 27, 2026
Microsoft Windows
Production PCA 2011
Signs the Windows
boot manager
October 19, 2026

So June is when the floor starts shifting, and October is the date to circle in red, because the Windows boot manager signing certificate is the consequential one. The replacements already exist in the 2023 certificate family, and Microsoft has been pushing them out through Windows Update for a while now. PCs shipped since early 2024 already have them.

No, Your Server Will Not Stop Booting

You will see vendor posts this month with words like 'absolute deadline' and 'no recovery' and 'devices may fail to boot'. Ignore the drama. Microsoft's own guidance is plain about this. The machine does not suddenly refuse to boot when a 2011 certificate expires. Red Hat says the same for Linux, that systems with the 2011 certificate already enrolled keep booting fine past the expiry dates.

The real consequence is quieter, but still matters. After expiration, a device that never received the 2023 certificates can no longer take new Secure Boot database updates, which means it stops getting future boot-layer security fixes. It keeps running. It just stops getting protected against the next BlackLotus-style bootkit. That is the risk you are managing here, not a Monday morning where half the estate is dark.

How to Check Where You Stand

Most machines get the 2023 certificates automatically through Windows Updates, and Microsoft is managing that rollout for a large share of devices. The ones that might get you are older servers whose firmware needs an OEM update first, and anything your team is patching by hand. So the first question for any box is whether the update mechanism is even running -- because if the Secure-Boot-Update task is disabled or missing, the certificates never arrive. Microsoft's own troubleshooting guide has the check:

# Confirms the Secure-Boot-Update task exists and is enabled -- this is the
# mechanism that applies the 2023 certificates. From Microsoft's Secure Boot
# troubleshooting guide. Run in an elevated PowerShell session.
schtasks.exe /Query /TN "\Microsoft\Windows\PI\Secure-Boot-Update" /FO LIST /V

# Status meanings:
#   Ready              task exists and is enabled
#   Disabled           task exists but must be enabled
#   Error / Not Found  task is missing and must be recreated

Two gotchas before you push anything broadly. First, BitLocker. Applying the Secure Boot updates can throw a device into BitLocker recovery -- usually a one-time prompt on the first boot while the firmware catches up, but a repeating one on machines set to PXE-boot first. Either way, have your recovery keys in hand before you start, not after. Second, old firmware. Some hardware will not take the update without an OEM firmware refresh, and a few boxes may never get there at all. Stand up a firmware-update ring for those now, while the pressure is low. You do not want to be chasing OEM BIOS updates in October, when the boot manager certificate is the one on the clock.

The Bottom Line

Patch Tuesday skipped SQL Server entirely this month, again. Patch your Windows hosts on your normal cadence, give the Hyper-V host fixes a nudge to the front if you virtualize your instances, and otherwise breathe easy on the database tier this month.

But still do the Secure Boot inventory now. Confirm the Secure-Boot-Update task is running across the estate, flag the machines where it's Disabled or missing, and sort the OEM-firmware stragglers while you have time. The June dates are the warning shot. The October boot-manager date is the one that will actually hurt if you're unprepared. This is a calendar problem, not a fire, and calendar problems are the cheap ones to solve early.

More to Read

Microsoft Support: Windows Secure Boot certificate expiration and CA updates
Microsoft Tech Community: Act now -- Secure Boot certificates expire in June 2026
BleepingComputer: Microsoft June 2026 Patch Tuesday fixes 3 zero-days, 200 flaws
sqlfingers inc: Patch Tuesday May 2026 -- SQL Got Off Easy. Your Domain Didn't.

Why Are We Still Paying for SQL Server?

A client asked me last week, plainly: 'Why are we still paying for SQL Server when Postgres is free?' It is a fair question, and it is being asked more and more every day. This post is the honest answer from a SQL Server professional who has also spent real time with PostgreSQL and MySQL. Just my thoughts on where SQL Server and Postgres stand in June 2026, what each one really wins for us, and the bill nobody's talking about when they say 'free'.

Where They Actually Stand

Popularity is not really a benchmark, but it is a signal, and the trend line is worth reading. The independent DB-Engines ranking scores engines on search interest, job listings, technical mentions, and social signals. Here is the top of the table for June 2026, with the change on the month.

Rank Engine Score
(Jun 2026)
Change
(month)
Change
(year)
1 Oracle 1140.04 -3.24 -90.35
2 MySQL 856.29 -0.21 -97.29
3 Microsoft SQL Server 698.04 -2.95 -78.71
4 PostgreSQL 688.23 +5.55 +7.58
5 MongoDB 387.97 +3.33 -14.87

SQL Server still holds third, but barely. Less than ten points now separate it from PostgreSQL, on a scale where it led by a comfortable margin a year ago. The direction of travel is the real story, though, like what's in that last column. Over the past year SQL Server shed almost 79 points while PostgreSQL gained about 8. It is the only engine in the top four that rose at all. Oracle and MySQL fell harder than SQL Server did. The whole top of the table is compressing, and Postgres is the one climbing into it.

But popularity rankings lean toward enterprise signals like job postings, and that tells a different story than what developers reach for on their own. In the 2024 Stack Overflow developer survey, PostgreSQL was the most-used database at about 49% of respondents, against around 25% for SQL Server. Read together, the two measures explain each other: SQL Server still wins on enterprise-weighted popularity, while Postgres owns developer mindshare and new projects. I'm pretty sure that split is exactly why my client is asking.

What Postgres Genuinely Does Better

If you want to be trusted on this, you have to concede the real ground first. Postgres earns its reputation in a few places.

The license line is zero.

Not discounted, but zero. There is no per-core tax, no edition ceiling pushing you to a more expensive SKU, and no Software Assurance clock. We will put real numbers on what that saves in a minute.

Extensibility is a first-class idea.

PostGIS for spatial, pgvector for embeddings, foreign data wrappers, and a deep catalog of extensions you can bolt on without waiting for the vendor to ship a feature. When a new workload shows up, Postgres usually already has an extension for it.

It runs anywhere.

Every major cloud has a managed Postgres, and the engine itself is portable across all of them. No platform gravity and no licensing-mobility headaches when you move it.

What SQL Server Still Wins

And then the other side of the ledger, which is still just as real.

The tooling and the operational story.

SSMS, Query Store, the Intelligent Query Processing family, a mature Always On availability-group stack, and first-party enterprise support. If you live in the Microsoft estate, that integration is not just a nice-to-have. It is the reason the lights stay on in many shops.

It is still shipping real innovation.

This is not a legacy engine coasting. SQL Server 2025 put a native JSON type, fuzzy string matching, and vector support in the box. I wrote up the native JSON data type recently, and there is more in that release than most shops have looked at yet.

The Microsoft-stack gravity is a feature, not just lock-in.

Entra ID, Power BI, SSIS, Fabric, the whole pipeline assumes SQL Server is in the middle. Ripping it out means rebuilding far more than just your database.

The Migration Bill Nobody Quotes

Here is where the 'just move to Postgres' conversation usually goes quiet. Moving the table data is the easy part, and it is almost never where the cost lives. The bill is in the procedural code.

Every stored procedure, trigger, and function written in T-SQL has to be converted to PL/pgSQL. Your SSIS packages do not come along. Your SQL Agent jobs do not come along. Your application's data-access layer has to be retested against different behavior in everything from identity columns to isolation levels to date handling. This is very important and can become very complicated. There are many tools to help with the mechanics. Like Babelfish, which eases database migrations by enabling PostgreSQL to understand and execute Microsoft SQL Server commands. There's also pgloader, the AWS Schema Conversion Tool, and AWS DMS -- but these automate the translation, not the validation. Budget most of a migration for converting procedural code and testing it, not for moving rows. This is the line item that turns a 'free' database into a six-figure project.

The Cost Math, Honestly

So put the license numbers on the table. These are SQL Server 2025 list prices, before any volume or Software Assurance discounting, and 2025 holds the same rate as 2022.

Scenario SQL Server
Enterprise
SQL Server
Standard
PostgreSQL
Per 2-core pack $15,123 $3,945 $0
Minimum (4 cores) $30,246 $7,890 $0
Typical 16-core box $120,984 $31,560 $0

That Enterprise column is the number your client saw on a renewal quote, and is why the question keeps getting asked. But we have to be honest about the other side, too. 'Free' is the license, not the total. Postgres still costs you in operational expertise, in support contracts (if you want one), and in the migration project like I mentioned above. The license line really is zero, but the total cost of ownership most definitely is not. Anyone telling you that it is, is selling you something else entirely.

The Honest Verdict

So how did I actually answer my client's question? In short, the license is almost never the real reason you're 'still paying.' The real cost of leaving is your T-SQL, your SSIS, and your Agent jobs, and that bill dwarfs the renewal. So before you look at Postgres at all, check whether you're paying Enterprise prices for a workload that would run perfectly fine on Standard Edition. That is the question worth answering first. Then it depends on three things: the workload, the team, and how deep you already sit in the Microsoft stack.

For a brand-new, cost-sensitive, developer-led project with nothing built in T-SQL yet, Postgres is a viable default now, not a compromise. The license savings are real and the engine is genuinely quite capable. For a Microsoft-ecosystem enterprise with years of T-SQL, SSIS, and Always On built in, SQL Server still earns its keep, and the migration math usually says stay put, at least for the core. Where it gets interesting is the shops paying Enterprise prices for Standard-shaped workloads. That is where a hard look pays off, and it is not always a Postgres answer. Sometimes it is just right-sizing to SQL Server Standard, which just became a lot more capable in the 2025 release.

The thing I will not tell a client anymore is that the choice is obvious. The era where nobody got fired for buying SQL Server is over. A ten-point gap on DB-Engines, with Postgres the only one of the top four still climbing, is a strong signal to run the numbers for your workload instead of just renewing on autopilot. Ask the question my client asked. Just make sure you cost out the whole answer, not the zero-price sticker.

More to Read

Microsoft: SQL Server 2025 Pricing (official PDF)
Microsoft Learn: What's new in SQL Server 2025
Microsoft Learn: Editions and supported features of SQL Server 2025
DB-Engines: Complete database popularity ranking
sqlfingers inc: Your JSON Column Was Never a JSON Column