SQL (PostgreSQL, MySQL, SQLite) Module 21 – SQL Server and Oracle side by side
NULLs and empty strings: Oracle versus SQL Server
Oracle stores an empty string as NULL, SQL Server keeps it as a value. See how that changes filters, counts, UNIQUE keys and data moved between them.
What you will learn
- Predict how Oracle's rule that '' is NULL changes inserts, filters, counts and concatenation
- Compare UNIQUE constraints and indexes that contain NULLs in SQL Server, Oracle and PostgreSQL
- Use NVL, NVL2, DECODE, ISNULL, COALESCE and NULLIF without type or evaluation surprises
- Plan how empty strings and NULLs move between engines during a migration
Before you start
What you’ll learn · Before you start
What you will learn
- Predict how Oracle's rule that '' is NULL changes inserts, filters, counts and concatenation
- Compare UNIQUE constraints and indexes that contain NULLs in SQL Server, Oracle and PostgreSQL
- Use NVL, NVL2, DECODE, ISNULL, COALESCE and NULLIF without type or evaluation surprises
- Plan how empty strings and NULLs move between engines during a migration
Before you start
On this page
Oracle treats a character value of length zero as NULL. Insert '' into a VARCHAR2 column and the row holds NULL:
WHERE nick = '' never matches it, WHERE nick IS NULL finds it, and COUNT(nick) skips it. SQL Server keeps an
empty string as an ordinary value of length 0 that is not NULL, so '' = '' is true there and COUNT(nick) counts
the row. Inside one engine both rules are consistent. The trouble starts when data or code crosses between them: a
NOT NULL column full of empty strings in SQL Server cannot be loaded into Oracle, counts and filters change their
answers, and text built by concatenation comes out differently. Oracle’s own reference calls the rule current
behaviour that may change in a future release, and advises against code that treats empty strings and NULLs as the
same thing.
One INSERT, two different rows
The same INSERT of '' in SQL Server and in Oracle
Text description of the diagram
The diagram follows one statement, INSERT INTO customers (id, nick) VALUES (2, ''), into two engines.
- In SQL Server the empty string is stored as it is: a value of length 0 that is not NULL. Afterwards nick = '' is true for the row, nick IS NULL is false, and COUNT(nick) counts it.
- In Oracle a zero-length string is NULL, so the row holds NULL. Afterwards nick = '' is unknown and matches nothing, nick IS NULL is true, and COUNT(nick) skips the row.
Everything else in this lesson follows from that one difference. SQL Server stores what it was given, and its NULLs
and empty strings stay apart. Oracle has only NULLs for a missing text value: an empty string coming in from a form,
a CSV file or another database becomes NULL on the way in. A single space is not empty, so ' ' stays a real value in
both engines.
The same table under both rules
SQLite, the engine that runs this track’s examples in your browser, keeps empty strings the way SQL Server does.
NULLIF(nick, '') returns NULL where nick is '', which is exactly what Oracle would have stored, so one table can
be read both ways side by side:
-- Five customers of a shop. Ravi and Zoya left the nickname box empty; Meera's was never asked for.
-- SQLite keeps '' as a value, as SQL Server does. NULLIF(nick, '') turns each '' into NULL, which is
-- what Oracle stores for a zero-length string: a model of Oracle's documented rule, run in SQLite.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
nick TEXT
);
INSERT INTO customers (id, name, nick) VALUES
(1, 'Asha', 'Ash'),
(2, 'Ravi', ''),
(3, 'Meera', NULL),
(4, 'Kabir', 'Kabs'),
(5, 'Zoya', '');
-- The same four questions, asked of the data as SQL Server keeps it and as Oracle would keep it.
WITH kept AS (
SELECT id, nick AS sql_server, NULLIF(nick, '') AS oracle FROM customers
)
SELECT 'WHERE nick = ''''' AS question,
(SELECT count(*) FROM kept WHERE sql_server = '') AS sql_server,
(SELECT count(*) FROM kept WHERE oracle = '') AS oracle
UNION ALL
SELECT 'WHERE nick IS NULL',
(SELECT count(*) FROM kept WHERE sql_server IS NULL),
(SELECT count(*) FROM kept WHERE oracle IS NULL)
UNION ALL
SELECT 'COUNT(nick)', count(sql_server), count(oracle) FROM kept
UNION ALL
SELECT 'COUNT(DISTINCT nick)', count(DISTINCT sql_server), count(DISTINCT oracle) FROM kept;
-- Row by row: what is stored, its length, and a label joined from two columns.
-- In SQLite, || with a NULL gives NULL, like T-SQL's + with its default settings.
-- Oracle's || skips a NULL operand, modelled here with coalesce(..., '').
SELECT id,
quote(nick) AS stored,
length(nick) AS len,
quote(NULLIF(nick, '')) AS oracle_stores,
length(NULLIF(nick, '')) AS oracle_len,
name || ' / ' || nick AS label_plus,
name || ' / ' || coalesce(NULLIF(nick, ''), '') AS label_oracle
FROM customers
ORDER BY id; Output
┌──────────────────────┬────────────┬────────┐ │ question │ sql_server │ oracle │ ├──────────────────────┼────────────┼────────┤ │ WHERE nick = '' │ 2 │ 0 │ │ WHERE nick IS NULL │ 1 │ 3 │ │ COUNT(nick) │ 4 │ 2 │ │ COUNT(DISTINCT nick) │ 3 │ 2 │ └──────────────────────┴────────────┴────────┘ ┌────┬────────┬─────┬───────────────┬────────────┬──────────────┬──────────────┐ │ id │ stored │ len │ oracle_stores │ oracle_len │ label_plus │ label_oracle │ ├────┼────────┼─────┼───────────────┼────────────┼──────────────┼──────────────┤ │ 1 │ 'Ash' │ 3 │ 'Ash' │ 3 │ Asha / Ash │ Asha / Ash │ │ 2 │ '' │ 0 │ NULL │ │ Ravi / │ Ravi / │ │ 3 │ NULL │ │ NULL │ │ │ Meera / │ │ 4 │ 'Kabs' │ 4 │ 'Kabs' │ 4 │ Kabir / Kabs │ Kabir / Kabs │ │ 5 │ '' │ 0 │ NULL │ │ Zoya / │ Zoya / │ └────┴────────┴─────┴───────────────┴────────────┴──────────────┴──────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < empty_vs_null.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
Three of the four answers change. Two rows match = '' under SQL Server’s rule and none under Oracle’s, because
nothing in Oracle equals an empty string: comparing anything with NULL is unknown. IS NULL finds one row in SQL
Server and three in Oracle. COUNT(nick) falls from 4 to 2, and COUNT(DISTINCT nick) from 3 to 2. Oracle’s
LENGTH('') is NULL for the same reason, while SQL Server’s LEN('') is 0.
The labels in the second result show the other half of the story. In SQLite, || with a NULL operand gives NULL, and
so does T-SQL’s + with its standard setting CONCAT_NULL_YIELDS_NULL ON, which SQL Server 2017 and later always
apply. SQL Server 2025’s new || operator returns NULL as well. Oracle’s || skips a NULL operand instead, so Meera’s
label survives there and vanishes in SQL Server. T-SQL’s CONCAT function treats NULL arguments as empty strings, the
closest match to Oracle’s behaviour.
Sorting, CHAR columns and the rules both engines share
Sort order differs too. SQL Server treats NULL as the lowest value, so NULLs come first in ascending order. Oracle puts
NULLs last in ascending order and first in descending order, and its former empty strings sort with them. Write
NULLS FIRST or NULLS LAST in Oracle when the position matters; SQL Server has no such clause, so sort on
CASE WHEN nick IS NULL THEN 1 ELSE 0 END first.
Fixed-length columns follow the same rule. Oracle blank-pads a CHAR value to its declared length, so ' ' in a
CHAR(3) column becomes three spaces, but '' becomes NULL before any padding happens.
Two rules are the same in both engines. NOT IN against a list that contains a NULL returns no rows, and = NULL
matches nothing (SQL Server’s old ANSI_NULLS OFF mode, which changed that, is deprecated).
The functions that handle NULLs
Each engine has its own functions for replacing a NULL or comparing two values that may be NULL:
| Criterion | SQL Server | Oracle |
|---|---|---|
| Replace NULL by a value | ISNULL(x, y), or COALESCE | NVL(x, y), or COALESCE |
| One value if not NULL, another if NULL | IIF or CASE | NVL2(x, if_not_null, if_null) |
| NULL when two values are equal | NULLIF(x, y) | NULLIF(x, y) |
| Compare two values, NULLs equal | x IS NOT DISTINCT FROM y (2022 and later) | DECODE(x, y, 1, 0) = 1 |
| Join text, NULL as empty | CONCAT(a, b, ...) | a || b (NULL is skipped) |
ISNULL returns its second argument when the first is NULL. NVL does the same in Oracle. NVL2 returns its second
argument when the first is not NULL and its third when it is. COALESCE returns the first argument that is not NULL,
in both engines. NULLIF returns NULL when its two arguments are equal. IS NOT DISTINCT FROM compares two values and
treats two NULLs as equal. DECODE compares a value with a list and also treats two NULLs as equal.
What moving data between them changes
- UNIQUE keys follow a different NULL rule in each engine, so a key that is valid in SQL Server can collide in Oracle.
- Oracle’s B-tree indexes leave out rows whose key columns are all NULL.
- ISNULL and COALESCE return different types, and
COALESCEmay evaluate an argument twice in SQL Server. - Before-and-after checks need a comparison that treats two NULLs as equal.
- A dry run on a copy of the data puts numbers on every change before the real move.
Your turn
Exercise · Easy · SQL
Find the rows Oracle would refuse to load
A list of suppliers is about to be copied from SQL Server to Oracle. In SQL Server the table vendors has these columns:
id: the vendor's number;name: the vendor's name, declaredNOT NULL;code: the vendor's short code, declaredNOT NULL;city: the vendor's city, which may be NULL.
SQL Server accepts an empty string in a NOT NULL column, because an empty string is a value there. Oracle stores an empty string as NULL, so a row with an empty name or an empty code would break the NOT NULL rule in Oracle, and the load would refuse it.
Write the query that returns the id and name of every vendor that Oracle would refuse for this reason. An empty city is no problem: that column accepts NULL. The query runs here on SQLite, which keeps empty strings the way SQL Server does.
Starter code · find_rejected.sql
-- Return the id and name of every vendor that Oracle would refuse to load,
-- because a NOT NULL column (name or code) holds an empty string.
SELECT id, name
FROM vendors
WHERE code IS NULL; The sample tests · tests.yaml
# Sample tests: each one builds a small vendors table as it sits in SQL Server, runs your query on it and compares the
# rows it returns (in any order).
tests:
- name: finds a vendor whose code is an empty string
setup: |
CREATE TABLE vendors (id INTEGER PRIMARY KEY, name TEXT NOT NULL, code TEXT NOT NULL, city TEXT);
INSERT INTO vendors VALUES
(1, 'Ganesh Traders', 'GT01', 'Pune'),
(2, 'Noor Textiles', '', 'Surat'),
(3, 'Kaveri Spices', 'KS07', NULL);
expected:
columns: [id, name]
rows:
- [2, Noor Textiles]
- name: finds an empty name too, but not an empty city
setup: |
CREATE TABLE vendors (id INTEGER PRIMARY KEY, name TEXT NOT NULL, code TEXT NOT NULL, city TEXT);
INSERT INTO vendors VALUES
(4, '', 'AN12', 'Kochi'),
(5, 'Lotus Paper', 'LP03', ''),
(6, 'Indus Steel', 'IS55', 'Nagpur');
expected:
columns: [id, name]
rows:
- [4, '']
- name: returns a row once even when both columns are empty
setup: |
CREATE TABLE vendors (id INTEGER PRIMARY KEY, name TEXT NOT NULL, code TEXT NOT NULL, city TEXT);
INSERT INTO vendors VALUES
(7, '', '', 'Delhi'),
(8, 'Sahyadri Agro', '', NULL),
(9, 'Mehta & Sons', 'MS09', 'Rajkot');
expected:
columns: [id, name]
rows:
- [7, '']
- [8, Sahyadri Agro]
- name: returns nothing when every NOT NULL column has a value
setup: |
CREATE TABLE vendors (id INTEGER PRIMARY KEY, name TEXT NOT NULL, code TEXT NOT NULL, city TEXT);
INSERT INTO vendors VALUES
(10, 'Pragati Tools', 'PT10', NULL),
(11, 'Vasant Foods', 'VF11', '');
expected:
columns: [id, name]
rows: [] A hint
In this table an empty string is stored as '', so name = '' finds an empty name. A row is refused when either of the two NOT NULL columns is empty, so the condition has two parts joined by OR. Leave city out of it.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
UNIQUE keys and indexes when empty strings become NULL, with the failures reproduced Pro: opens with a Pro, Premium or Ultimate pass
A UNIQUE key and an index both have their own rules for NULLs, and the three engines disagree on them. This part reproduces a load that fails only because empty strings turned into NULLs, and works out what an Oracle index on a nullable column does and does not contain.
In this part:
- What each engine lets a UNIQUE key hold
- A composite key that loads in SQL Server and fails in Oracle, reproduced
- An Oracle index skips NULL keys, worked through with numbers
1 runnable example (SQL) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
NULL functions in depth and a migration plan, tested on a dry run Pro: opens with a Pro, Premium or Ultimate pass
The NULL functions look interchangeable, but they differ in the type they return, in when they evaluate their arguments and in how they compare two NULLs. This part shows where each one surprises people and turns everything in the lesson into a migration plan with numbers from a dry run.
In this part:
- Return types and evaluation, where ISNULL, NVL and COALESCE disagree
- Comparing two values that may be NULL, with the failure reproduced
- A migration plan, sized on a 1,000-row dry run
2 runnable examples (SQL) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What does Oracle store when you insert an empty string into a VARCHAR2 column, and why does WHERE nick = '' then find nothing?
Oracle stores NULL. Its SQL reference says that a character value with a length of zero is treated as null, so the INSERT succeeds, but the row holds NULL instead of an empty string. The condition
nick = ''compares the column with an empty string, which Oracle also reads as NULL, and a comparison with NULL is unknown, never true, so no row matches. Usenick IS NULLto find the row. SQL Server behaves differently: it keeps''as a value of length 0 that is not NULL, sonick = ''finds the row there andnick IS NULLdoes not. Oracle's documentation adds that this treatment may change in a future release and advises against treating empty strings and NULLs as the same, so writeIS NULLwhenever you mean a missing value. -
Mid to senior
A SQL Server table with UNIQUE (branch_code, counter_code) holds 2 million rows and no duplicates. Copied into Oracle with the same constraint, the load stops with ORA-00001. Why, and how do you find and fix the rows before the next attempt?
-
Mid to senior
After a reporting database moved from SQL Server to Oracle, the number of customers with a phone number fell from 84,000 to 71,000, yet the row count of every table matches. What happened, and how do you make the two reports agree?
-
Mid to staff
The same application stores customers in SQL Server for one client and in Oracle for another. Some customers have no middle name. How should the schema and the code represent that, so every query gives the same answer on both engines?
Key takeaways
- Oracle stores an empty string as NULL; SQL Server keeps it as a value of length 0.
- In Oracle, test for a missing text value with
IS NULL:= ''never matches. COUNT(column)andCOUNT(DISTINCT column)drop when empty strings become NULLs.- Oracle’s
||skips NULLs; T-SQL’s+and||return NULL, andCONCATtreats NULL as empty. NULLIF(column, '')previews in SQL Server what Oracle would store.
References
- Oracle AI Database SQL Language Reference 26: Nulls (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: Concatenation Operator (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: Data Types (CHAR and VARCHAR2) (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: constraint (unique and NOT NULL constraints) (Oracle Corporation)
- Oracle AI Database Concepts 26: Indexes and Index-Organized Tables (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: NVL (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: NVL2 (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: DECODE (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: COALESCE (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: About SQL Functions (nulls in SQL functions) (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: SELECT (ORDER BY, NULLS FIRST and NULLS LAST) (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: IN Condition (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: CASE Expressions (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: Aggregate Functions (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: LENGTH (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: Comparison Conditions (Oracle Corporation)
- Oracle error help: ORA-00001 (Oracle Corporation)
- Oracle error help: ORA-01400 (Oracle Corporation)
- SQL Server documentation: Unique constraints and check constraints (Microsoft)
- SQL Server documentation: Create filtered indexes (Microsoft)
- SQL Server documentation: ISNULL (Transact-SQL) (Microsoft)
- SQL Server documentation: COALESCE (Transact-SQL) (Microsoft)
- SQL Server documentation: IS [NOT] DISTINCT FROM (Transact-SQL) (Microsoft)
- SQL Server documentation: SET ANSI_NULLS (Transact-SQL) (Microsoft)
- SQL Server documentation: SET CONCAT_NULL_YIELDS_NULL (Transact-SQL) (Microsoft)
- SQL Server documentation: || (String concatenation) (Transact-SQL) (Microsoft)
- SQL Server documentation: CONCAT (Transact-SQL) (Microsoft)
- SQL Server documentation: ORDER BY clause (Transact-SQL) (Microsoft)
- SQL Server documentation: LEN (Transact-SQL) (Microsoft)
- SQL Server documentation: IIF (Transact-SQL) (Microsoft)
- SQL Server documentation: CASE (Transact-SQL) (Microsoft)
- SQL Server documentation: IN (Transact-SQL) (Microsoft)
- SQL Server documentation: Aggregate functions (Transact-SQL) (Microsoft)
- SQL Server documentation: DATALENGTH (Transact-SQL) (Microsoft)
- SQL Server documentation: GREATEST (Transact-SQL) (Microsoft)
- PostgreSQL documentation: Constraints (unique constraints and NULLS NOT DISTINCT) (The PostgreSQL Global Development Group)
- SQLite: SQL Language Expressions (IS NOT DISTINCT FROM) (SQLite)
- SQLite: Built-In Scalar SQL Functions (nullif, quote, coalesce) (SQLite)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress