Back to Article List

ISNULL, COALESCE, NULLIF and CASE in SQL Server

ISNULL, COALESCE, NULLIF and CASE in SQL Server

Updated and tested 11 October 2026

NULL marks the absence of a value. Every comparison you write against it comes back unknown; true and false are both off the table. That is why WHERE region = NULL matches nothing at all on a table where hundreds of customers have no region recorded, and why a filter that gets refactored into that shape can strip every non-EU row out of a regional report while the query still reports success and the job still goes green. SQL Server is doing exactly what the standard tells it to do. It will not complain once.

NULL handling produces that class of bug more than anything else in T-SQL, and the reason it survives so long in a code base is that nothing crashes; the number is simply wrong, and wrong numbers get believed. What follows is the set of functions you use to deal with it, worked against the same four tables as the rest of this series: dbo.customers with a nullable region and credit_limit, plus dbo.orders, dbo.order_items and dbo.employees.

Three-valued logic and IS NULL

SQL does not have true and false. It has true, false and unknown, and NULL is the thing that drags an expression into that third state, because NULL means "no value here" and a comparison against no value cannot produce a verdict. A WHERE clause only keeps the rows where the predicate came out true, and unknown is not true. Simpler version, and the one I would give someone at a whiteboard: if SQL Server cannot tell, the row does not come back.

SELECT COUNT(*) FROM dbo.customers WHERE region = NULL;     -- always 0
SELECT COUNT(*) FROM dbo.customers WHERE region <> NULL;    -- also always 0
SELECT COUNT(*) FROM dbo.customers WHERE region IS NULL;    -- the real answer

Both of the first two return zero on any modern instance, and they return zero even on the rows where region really is NULL, which is the part that makes the first two look like they might be doing something. That is SET ANSI_NULLS ON behaviour, and it stopped being optional a while ago. Microsoft's wording: "SET ANSI_NULLS OFF and the ANSI_NULLS OFF database option are deprecated. Starting with SQL Server 2017 (14.x), ANSI_NULLS is always set to ON." If you turn up a page saying the default is OFF, that is a legacy line (the same page says the ODBC driver and OLE DB provider set it ON when they connect) and it does not describe what your server does today.

Small aside while we are here, because the vocabulary trips people up in code review. The third truth value has a name of its own, unknown, and it is not the same thing as NULL even though everyone says both words as if they were interchangeable; NULL is a marker sitting in a column and unknown is what a comparison against that marker evaluates to. The standards documents are careful about the distinction and nobody in a stand-up ever is. Back to the query.

The same logic bites through NOT IN, and this one is nastier because it looks like it is working:

SELECT c.customer_id, c.company_name
FROM dbo.customers AS c
WHERE c.customer_id NOT IN (SELECT o.customer_id FROM dbo.orders AS o);

One NULL customer_id anywhere in dbo.orders and that query returns nothing at all, forever, because the predicate expands into a chain of <> comparisons and one of them is against NULL, so the whole conjunction lands on unknown for every row you test. Microsoft documents it on the IN reference page. Write NOT EXISTS instead, which handles NULLs the way you assumed NOT IN did, and since how the WHERE clause evaluates predicates is where half the NULL surprises in any code base end up living, that is the next thing to read after this section.

ISNULL versus COALESCE

They look interchangeable, they are not, and every difference between them is written down on the COALESCE reference page for anyone who reads that far down. MSSQL COALESCE is the portable one of the pair, which is usually where the argument starts and almost never where it should end.

The five documented differences

  • Standards. ISNULL is T-SQL only. COALESCE is ANSI SQL and works in Postgres, MySQL, Oracle and everything else.
  • Argument count. ISNULL takes exactly two. COALESCE takes as many as you like and returns the first non-NULL one.
  • Return type. ISNULL returns the data type of the first argument. COALESCE returns the highest-precedence type among all arguments, which is a different rule with different consequences.
  • Evaluation count. ISNULL evaluates its input once. COALESCE inputs "can be evaluated multiple times" because the engine rewrites the whole thing as a CASE expression.
  • Nullability. The result of ISNULL is treated as NOT NULL. The result of COALESCE may still be nullable.

That last one has a consequence you meet the first time you build a computed column:

-- works
CREATE TABLE dbo.t1 (col1 int NULL, col2 AS ISNULL(col1, 0) PRIMARY KEY);

-- fails: the computed column is nullable, so it cannot be a primary key
CREATE TABLE dbo.t2 (col1 int NULL, col2 AS COALESCE(col1, 0) PRIMARY KEY);

Same intent, same arithmetic, and one of them builds while the other does not. It is also the trick for forcing NOT NULL metadata onto a column coming out of a recursive common table expression, where every output column is nullable regardless of what the base table declares, so if you have ever tried to put a primary key on a SELECT ... INTO from a recursive CTE and been refused, this is why.

ISNULL silently truncates the replacement value

Here is the one I would put on a poster. Microsoft's wording is blunt: "replacement_value can be truncated if replacement_value is longer than check_expression." Because ISNULL returns the type of the first argument, the replacement gets cut down to fit it, and no warning is raised anywhere in the process.

CREATE TABLE dbo.sku_notes (sku varchar(5) NULL);
INSERT INTO dbo.sku_notes (sku) VALUES (NULL);

SELECT ISNULL(sku, 'unassigned')   AS with_isnull,
       COALESCE(sku, 'unassigned') AS with_coalesce
FROM dbo.sku_notes;
with_isnull   with_coalesce
-----------   -------------
unass         unassigned

'unassigned' is ten characters and sku is varchar(5), so ISNULL hands back unass, while COALESCE applies type precedence across both arguments and returns the string intact. Checking that a replacement fits is its own small trap, since LEN lies to you about trailing spaces and DATALENGTH does not, and on a padded char column those two disagree by exactly the amount you care about. I watched this land in a customer-facing export where a status column was char(3). Every missing value arrived as unk while the code plainly said unknown. Nobody queried it for months because it looked deliberate.

When COALESCE evaluates its arguments twice

The rewrite-as-CASE behaviour stays invisible right up until one of the arguments has a cost or a side effect. This is the shape that bites:

SELECT c.customer_id,
       COALESCE((SELECT MAX(o.total_amount)
                 FROM dbo.orders AS o
                 WHERE o.customer_id = c.customer_id), 0) AS biggest_order
FROM dbo.customers AS c;

That subquery can run twice per customer row, once to test the value for NULL and once to return it, so on ten thousand customers you get twenty thousand executions of an aggregate you meant to run ten thousand times. The same thing happens with NEWID(), RAND() and CRYPT_GEN_RANDOM, except there it is worse, because the two evaluations return different values and you can end up with a non-NULL test followed by a completely different non-NULL result.

A few ways out of it, and I reach for the last one:

  • Swap in ISNULL, which evaluates once by definition. Fine for two arguments, useless past that.
  • Compute the expensive expression into a variable or a temp column first, then coalesce the cheap thing.
  • Lift the aggregate into a CTE that computes it once for the whole set, then coalesce its column in the outer query.
  • Move the subquery into an OUTER APPLY and coalesce the resulting column, which evaluates it once and reads better than any of the others. That rewrite gets a step-by-step treatment in CROSS APPLY and OUTER APPLY.

None of this matters for a plain literal default, so COALESCE(region, 'unknown') is fine and always will be. Literals, use whichever you like. Anything with a function call or a subquery inside it, keep it out of COALESCE. I have not benchmarked what a twelve-argument COALESCE does to the size of a plan, and I am not going to invent a number for it. My own lists stop at four or five arguments; past that I would move the fallbacks into a lookup table.

NULLIF and the divide by zero guard

NULLIF(a, b) returns NULL when the two arguments are equal and returns a otherwise, which is exactly what the longhand CASE does:

NULLIF(a, b)  =  CASE WHEN a = b THEN NULL ELSE a END

Written like that it sounds like a solution looking for a problem, and it stays sounding like one until the morning you meet Msg 8134, Level 16, State 1: Divide by zero error encountered. in a report that had run without complaint for a year. That is the real use for it:

SELECT o.order_id,
       o.total_amount,
       SUM(i.quantity)                              AS units,
       o.total_amount / NULLIF(SUM(i.quantity), 0)  AS price_per_unit
FROM dbo.orders AS o
JOIN dbo.order_items AS i ON i.order_id = o.order_id
GROUP BY o.order_id, o.total_amount;

Zero units turns the divisor into NULL. The division returns NULL and the statement completes. Wrap the whole expression in ISNULL(..., 0) if the report wants a zero in that cell, though I would argue a blank is the more honest output for "we cannot compute this". Backfilling those NULLs permanently in the table is a separate decision, and UPDATE statements in SQL Server covers doing it without a non-deterministic FROM clause, which is the way that job usually goes wrong.

NULLIF returns the data type of its first expression.

And keep nondeterministic functions out of it, for the same reason you keep them out of COALESCE: the arguments can be evaluated more than once, because it is the same CASE machinery underneath.

Your idea deserves better hosting

24/7 support 30-day money-back guaranteeRefund covers the VPS plan on your first order. Cancel anytime
Fatura Kesim Döngüsü

VPS.S1

$5.99 Save  17 %
$4.99 Aylık
  • 2 vCPU AMD EPYC
  • 2 GB RAMBELLEK
  • 30 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil

VPS.S3

$14.99 Save  33 %
$9.99 Aylık
  • 4 vCPU AMD EPYC
  • 6 GB RAMBELLEK
  • 70 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil

VPS.S4

$19.99 Save  20 %
$15.99 Aylık
  • 6 vCPU AMD EPYC
  • 8 GB RAMBELLEK
  • 80 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil

EPYC VPS.P1

$8.99 Save  22 %
$6.99 Aylık
  • 2 vCPU AMD EPYC
  • 4 GB RAMBELLEK
  • 40 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

EPYC VPS.P2

$16.99 Save  24 %
$12.99 Aylık
  • 2 vCPU AMD EPYC
  • 8 GB RAMBELLEK
  • 80 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

EPYC VPS.P4

$29.99 Save  23 %
$22.99 Aylık
  • 4 vCPU AMD EPYC
  • 16 GB RAMBELLEK
  • 160 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

EPYC VPS.P5

$39.99 Save  25 %
$29.99 Aylık
  • 8 vCPU AMD EPYC
  • 16 GB RAMBELLEK
  • 180 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

EPYC VPS.P6

$59.99 Save  25 %
$44.99 Aylık
  • 8 vCPU AMD EPYC
  • 32 GB RAMBELLEK
  • 200 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

EPYC VPS.P7

$69.99 Save  29 %
$49.99 Aylık
  • 16 vCPU AMD EPYC
  • 32 GB RAMBELLEK
  • 240 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

Genoa VPS.G2

$24.99 Save  20 %
$19.99 Aylık
  • 2 vCPUAMD EPYC Genoa 4. nesil 9xx4, 3,25 GHz veya benzeri, Zen 4 mimarisinde. AMD EPYC G4
  • 4 GB DDR5BELLEK
  • 50 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

Genoa VPS.G4

$44.99 Save  22 %
$34.99 Aylık
  • 4 vCPUKurumsal sunucu donanımında, ayrılmış vCPU çekirdeklerine sahip AMD EPYC işlemci. AMD EPYC G4
  • 8 GB DDR5BELLEK
  • 100 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

Genoa VPS.G6

$89.99 Save  22 %
$69.99 Aylık
  • 8 vCPUKurumsal sunucu donanımında, ayrılmış vCPU çekirdeklerine sahip AMD EPYC işlemci. AMD EPYC G4
  • 16 GB DDR5BELLEK
  • 200 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

Genoa VPS.G7

$159.99 Save  22 %
$124.99 Aylık
  • 8 vCPUKurumsal sunucu donanımında, ayrılmış vCPU çekirdeklerine sahip AMD EPYC işlemci. AMD EPYC G4
  • 32 GB DDR5BELLEK
  • 250 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil. dahil
  • Ücretsiz otomatik yedeklemeGünlük, haftalık veya aylık çalışacak şekilde ayarlayabileceğiniz bir yedekleme alanı içerir.

AMD Ryzen VPS.R1

$16.99 Save  18 %
$13.99 Aylık
  • 1 özel CPU AMD Ryzen 9 7950X, 4,5 GHz veya benzeri, Zen 4 mimarisinde. vCPU
  • 4 GB DDR5BELLEK
  • 50 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6 dahil IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil.
  • Otomatik yedekleme dahil

AMD Ryzen VPS.R2

$29.99 Save  17 %
$24.99 Aylık
  • 2 özel CPU AMD Ryzen 9 7950X, 4,5 GHz veya benzeri, Zen 4 mimarisinde. vCPU
  • 8 GB DDR5BELLEK
  • 100 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6 dahil IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil.
  • Otomatik yedekleme dahil

AMD Ryzen VPS.R4

$109.99 Save  18 %
$89.99 Aylık
  • 8 özel CPU AMD Ryzen 9 7950X, 4,5 GHz veya benzeri, Zen 4 mimarisinde. vCPU
  • 32 GB DDR5BELLEK
  • 400 GB NVMeDEPOLAMA
  • Sınırsız bant genişliği
  • IPv4 & IPv6 dahil IPv6 desteği şu anda Fransa, Finlandiya veya Hollanda'da mevcut değil.
  • Otomatik yedekleme dahil

*VAT excluded.

INCLUDED WITH EVERY PLAN

✓ No setup fees ✓1 Gbps network
✓ Full root access on KVM ✓ Free server monitoring ✓ Firewall management ✓24/7 support

CASE expressions in SQL Server

CASE comes in two forms. The simple form compares one expression against a list of values. The searched form evaluates an independent boolean condition in each branch.

-- simple CASE
SELECT order_id,
       CASE status
            WHEN 'pending'   THEN 'Awaiting payment'
            WHEN 'shipped'   THEN 'On its way'
            WHEN 'cancelled' THEN 'Cancelled'
            ELSE 'Unrecognised status'
       END AS status_label
FROM dbo.orders;

-- searched CASE
SELECT order_id,
       CASE WHEN total_amount >= 10000 THEN 'enterprise'
            WHEN total_amount >= 1000  THEN 'mid'
            WHEN total_amount IS NULL   THEN 'unpriced'
            ELSE 'small'
       END AS band
FROM dbo.orders;

Branches are evaluated in order and the first match wins, so a broad condition placed first swallows everything below it. Leave the ELSE out and unmatched rows get NULL. Nothing is raised. I write an explicit ELSE in every CASE, including the ones where it says ELSE NULL, because it documents the intent and it costs one line.

Simple CASE cannot match NULL

The simple form only does equality, and equality against NULL is unknown, so this branch is dead code:

-- never returns 'unknown', not once, no matter what the data says
SELECT CASE region WHEN NULL THEN 'unknown' ELSE 'known' END
FROM dbo.customers;

-- correct
SELECT CASE WHEN region IS NULL THEN 'unknown' ELSE 'known' END
FROM dbo.customers;

It compiles and it runs. It returns 'known' for every row, including the NULL ones. Any time NULL is one of the cases you care about, you are writing the searched form.

The CASE return type precedence error

A CASE expression returns exactly one type, and that type is the highest-precedence type among all the result branches, so mixing an int into a set of strings means the strings lose:

SELECT order_id,
       CASE WHEN total_amount >= 1000 THEN 'large'
            WHEN total_amount >= 100  THEN 'medium'
            ELSE 0
       END AS size_band
FROM dbo.orders;
Msg 245, Level 16, State 1
Conversion failed when converting the varchar value 'large' to data type int.

The ELSE 0 is the culprit, since int outranks varchar in the precedence order, so SQL Server decides the expression returns int and then sets about converting every string branch. What makes it so annoying is the timing: the query passes review, runs happily on test data where no row hits the string branches, and errors in production on the first large order that arrives. Quote the zero as ELSE '0', or cast the branches deliberately.

The full precedence list lives in SQL Server data types, CAST and CONVERT, along with the other conversions it forces on you. It is the same list the engine is consulting here. Microsoft's CAST and CONVERT reference carries the conversion matrix.

Aggregates in WHEN arguments are evaluated first

The CASE reference documents this one and almost nobody has read it: "Aggregate expressions that appear in WHEN arguments to a CASE expression are evaluated first, then provided to the CASE expression."

Which means a CASE guard built out of aggregates does not protect the division the way the code implies it does. Writing CASE WHEN SUM(i.quantity) = 0 THEN 0 ELSE SUM(o.total_amount) / SUM(i.quantity) END looks like short-circuit protection and is not reliably that, because the aggregate expressions get computed before the CASE ever sees them. Use NULLIF on the divisor. It is shorter, it is unambiguous, and it does not depend on evaluation order at all.

CASE in ORDER BY, GROUP BY and conditional aggregation

CASE is an expression, so it goes anywhere an expression goes. Custom sort order:

SELECT order_id, status, order_date
FROM dbo.orders
ORDER BY CASE status
              WHEN 'pending'   THEN 1
              WHEN 'shipped'   THEN 2
              WHEN 'cancelled' THEN 3
              ELSE 4
         END,
         order_date DESC;

That sorts by business priority. Alphabetical order is what you give up. It also costs you an index-ordered scan, since the sort key is an expression the index knows nothing about, so keep the pattern for result sets that are already small by the time they reach the sort.

The pattern worth stealing outright is conditional aggregation, which gives you several differently-filtered totals from one pass over the data with no self joins anywhere:

SELECT c.country,
       COUNT(*)                                                          AS total_orders,
       SUM(CASE WHEN o.status = 'shipped'   THEN o.total_amount ELSE 0 END) AS shipped_value,
       SUM(CASE WHEN o.status = 'cancelled' THEN o.total_amount ELSE 0 END) AS cancelled_value,
       COUNT(CASE WHEN o.status = 'pending' THEN 1 END)                  AS pending_count
FROM dbo.customers AS c
JOIN dbo.orders AS o ON o.customer_id = c.customer_id
GROUP BY c.country
ORDER BY shipped_value DESC;

Look at the difference between the SUM lines and the COUNT line, because it is deliberate and it is easy to undo by accident. SUM wants ELSE 0 so the arithmetic works on every row. COUNT deliberately has no ELSE, because unmatched rows then come back as NULL and COUNT skips NULLs, which is precisely the counting behaviour you were after. Get those two backwards and you produce a count equal to the row count in every group, which is a very believable wrong answer and one that survives review.

IIF as shorthand for CASE

IIF(condition, true_value, false_value) is shorthand for a searched CASE with one WHEN and an ELSE. The engine translates it into exactly that, so everything above about return types applies to it unchanged.

SELECT order_id,
       IIF(total_amount > 500, 'priority', 'standard') AS handling
FROM dbo.orders;

It is tidy for a two-way split and it runs out of road fast, because there is no way to add a third outcome without nesting IIF inside IIF, and nested IIFs stop being readable at the second level. Anything with more than two outcomes, or any branch that needs IS NULL, goes back to CASE. I use IIF in select lists and avoid it in anything a colleague will maintain, which is a slightly hypocritical position I am comfortable with.

NULLs in aggregates and COUNT(*) versus COUNT(column)

Every aggregate except COUNT(*) ignores NULLs, and that is a feature until it is not.

SELECT COUNT(*)                 AS all_rows,
       COUNT(region)            AS rows_with_a_region,
       COUNT(DISTINCT region)   AS distinct_regions,
       AVG(credit_limit)        AS avg_limit,
       AVG(ISNULL(credit_limit, 0)) AS avg_limit_treating_null_as_zero
FROM dbo.customers;

COUNT(*) counts rows. COUNT(region) counts the rows where region is not NULL. On a table of 500 customers where 120 have no region, those return 500 and 380, and both are correct answers to different questions.

The two AVG columns are where money goes missing, because AVG(credit_limit) divides by the number of customers who have a limit while AVG(ISNULL(credit_limit, 0)) divides by all of them, and on the same data those two can differ by a third. Neither one is wrong. Somebody has to decide what a missing credit limit means before anyone writes the query, and that decision belongs to whoever owns the report and not to whoever is typing. SQL Server will even tell you it dropped rows when ANSI_WARNINGS is on: Warning: Null value is eliminated by an aggregate or other SET operation. Most tools swallow that message before you see it.

Choosing between all of these takes about five seconds once two rules are in your head. Anything that has to be NOT NULL, or anything where the replacement is a plain literal and the checked column is wide enough to hold it, use ISNULL. Everything else, including any list longer than two and any column you will port to another engine one day, use COALESCE. Then go and grep the code base for = NULL, because there is one in there.

Questions?

Does COALESCE treat an empty string as NULL?

No, an empty string is a value, so COALESCE(region, 'unknown') passes '' straight through untouched. Wrap the column in NULLIF to turn blanks into NULL first: COALESCE(NULLIF(region, ''), 'unknown') handles the NULL rows and the empty ones in one expression. A value made only of spaces counts as empty here too, because SQL Server pads strings to the same length before an equality comparison, so ' ' equals ''.

Jörg L. Günther
About the author

Jörg L. Günther

Jörg has spent much of his career in enterprise software, working first with database deployments and customer environments, then in technical training and documentation. Since then he has written for companies working in virtualisation, database tooling and cloud infrastructure. The subjects he takes on often have a large operational component: database migrations, high-availability setups, storage configuration and procedures carried out on live systems. Jörg lives near Frankfurt. He plays piano and cycles for transport as much as for recreation. He's also been doing woodworking for several years - he says most of the furniture in his study is his own work.

Deploy your next project in seconds

Bring your winning ideas online faster, with modern hardware and unmetered bandwidth. Join a European cloud trusted by thousands of developers and businesses worldwide.