SQL (PostgreSQL, MySQL, SQLite) Module 21 – SQL Server and Oracle side by side
Collations and case in SQL Server and Oracle
SQL Server compares text without regard to case by default; Oracle compares bytes. Control case, accents and Hindi or Marathi text in both engines.
What you will learn
- Predict whether a comparison, DISTINCT or ORDER BY ignores case and accents in each engine
- Configure one query or one column to compare text with or without regard to case
- Fix collation conflicts, including the temporary-table trap in SQL Server
- Store Hindi and other Indian-language text without data loss
Before you start
What you’ll learn · Before you start
What you will learn
- Predict whether a comparison, DISTINCT or ORDER BY ignores case and accents in each engine
- Configure one query or one column to compare text with or without regard to case
- Fix collation conflicts, including the temporary-table trap in SQL Server
- Store Hindi and other Indian-language text without data loss
Before you start
On this page
A collation is the rule a database uses to compare and sort text, and SQL Server and Oracle start from opposite rules.
SQL Server’s setup takes its default from the operating system’s locale: SQL_Latin1_General_CP1_CI_AS for English
(United States) and Latin1_General_CI_AS for English (India), both case-insensitive and accent-sensitive. So
'Pune' = 'PUNE' is true, DISTINCT folds Pune, PUNE and pune into one value, and 'café' = 'cafe' stays false.
Oracle compares text as binary by default, because the session settings NLS_COMP and NLS_SORT are BINARY: the
same comparison is false, DISTINCT keeps three cities, and every capital letter sorts before every small one.
Collations therefore decide whether a login, a duplicate check or a report gives the same answer after a move between
the engines.
Two defaults on one table
SQLite has two collations that behave like the two defaults for English letters: BINARY compares bytes as Oracle
does, and NOCASE ignores the case of the letters A to Z as SQL Server’s _CI collations do. That makes it a fair
model of both engines for the words below, which use English letters only:
-- The two defaults, modelled with SQLite's own collations for English letters only:
-- BINARY compares bytes, like Oracle's default (NLS_COMP = BINARY, NLS_SORT = BINARY) or a SQL Server _BIN2 collation;
-- NOCASE folds the 26 letters A to Z, like SQL Server's case-insensitive default (_CI). SQL Server's _CI collations
-- also fold accented capitals, which NOCASE does not, so these rows use English letters only.
CREATE TABLE cities (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
INSERT INTO cities (name) VALUES ('Pune'), ('PUNE'), ('pune'), ('Mumbai'), ('mumbai');
CREATE TABLE produce (name TEXT NOT NULL);
INSERT INTO produce (name) VALUES ('banana'), ('Zucchini'), ('apple'), ('Mango');
-- 1. Equality and a filter.
SELECT 'Pune' = 'PUNE' AS equal_binary,
'Pune' = 'PUNE' COLLATE NOCASE AS equal_case_insensitive,
(SELECT count(*) FROM cities WHERE name = 'pune') AS rows_binary,
(SELECT count(*) FROM cities WHERE name = 'pune' COLLATE NOCASE) AS rows_case_insensitive;
-- 2. DISTINCT and GROUP BY use the same rule as equality.
SELECT count(DISTINCT name) AS distinct_binary,
count(DISTINCT name COLLATE NOCASE) AS distinct_case_insensitive
FROM cities;
-- 3. Sorting: bytes put every capital letter before every small one.
SELECT group_concat(name, ', ' ORDER BY name) AS order_binary,
group_concat(name, ', ' ORDER BY name COLLATE NOCASE) AS order_case_insensitive
FROM produce; Output
┌──────────────┬────────────────────────┬─────────────┬───────────────────────┐ │ equal_binary │ equal_case_insensitive │ rows_binary │ rows_case_insensitive │ ├──────────────┼────────────────────────┼─────────────┼───────────────────────┤ │ 0 │ 1 │ 1 │ 3 │ └──────────────┴────────────────────────┴─────────────┴───────────────────────┘ ┌─────────────────┬───────────────────────────┐ │ distinct_binary │ distinct_case_insensitive │ ├─────────────────┼───────────────────────────┤ │ 5 │ 2 │ └─────────────────┴───────────────────────────┘ ┌────────────────────────────────┬────────────────────────────────┐ │ order_binary │ order_case_insensitive │ ├────────────────────────────────┼────────────────────────────────┤ │ Mango, Zucchini, apple, banana │ apple, banana, Mango, Zucchini │ └────────────────────────────────┴────────────────────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < case_rules.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
Every answer differs. The filter finds one row under the binary rule and three without regard to case.
COUNT(DISTINCT name) gives 5 or 2, so a report of “cities” changes size after a move. The sort puts Mango and
Zucchini first under the binary rule, because capitals have smaller codes than small letters. Nothing fails loudly:
a login, a search box or a duplicate check written for SQL Server keeps running in Oracle and quietly returns fewer
rows.
Accents are where the model stops. Letter case in SQL Server is folded for accented letters too, so É equals é under
_CI, while SQLite’s NOCASE folds only A to Z. And an accent-sensitive collation (_AS) keeps café and cafe apart
in SQL Server, while _AI makes them equal.
Reading a collation’s name
The name of a collation states its rules. Ignoring case in SQL Server is a matter of choosing a name with _CI:
| Criterion | SQL Server | Oracle |
|---|---|---|
| Case-sensitive or not | _CS or _CI in the collation name | BINARY compares case; a name ending in _CI ignores it |
| Accent-sensitive or not | _AS or _AI | A name ending in _AI ignores accents and case |
| Compare code points | _BIN2 (and the older _BIN) | BINARY, the default |
| UTF-8 in char and varchar | _UTF8, SQL Server 2019 and later | The database character set, AL32UTF8 |
| Rule for literals and variables | The current database collation | The session: NLS_COMP and NLS_SORT |
_BIN2 collations compare Unicode code points, which is what Oracle’s BINARY does with the bytes of its database
character set. BINARY_CI ignores case but keeps accents; BINARY_AI ignores both.
Where the rule is set
Where each engine takes its rule for comparing text
Text description of the diagram
The diagram has two columns of boxes, one for each engine. In each, a level is the default for the level below it, and the last level overrides everything above it.
SQL Server, from top to bottom: 1. The instance collation, chosen at setup. The tempdb system database uses it, so temporary tables do too. 2. The database collation, set with CREATE DATABASE … COLLATE. It is the default for the columns of that database. 3. The column collation, set in CREATE TABLE with COLLATE after the column's type. 4. An expression with COLLATE, such as name COLLATE Latin1_General_100_CS_AS, which overrides the column's collation for one comparison or sort.
Oracle, from top to bottom: 1. The session settings NLS_COMP and NLS_SORT, both BINARY by default. A column without a declared collation follows them. 2. A table or column collation declared in the DDL, which Oracle accepts only when the initialization parameter MAX_STRING_SIZE is EXTENDED. 3. An expression with the COLLATE operator, such as name COLLATE BINARY_CI, which works whatever MAX_STRING_SIZE is.
In SQL Server each level is the default for the next one down: the instance collation for databases, the database
collation for columns, and a column’s collation for every comparison that uses it. COLLATE after an expression
overrides all of them for one comparison or sort. The instance collation also belongs to tempdb, where temporary
tables live, which matters as soon as a database uses another collation.
Oracle puts the rule in the session: NLS_SORT names a sort order and NLS_COMP decides which operations follow it.
A column can declare its own collation only when the database runs with MAX_STRING_SIZE = EXTENDED; otherwise the
DDL fails with ORA-43929. The COLLATE operator on an expression works either way.
Hindi, Marathi and other Indian scripts
A collation in SQL Server also fixes the code page of char and varchar columns, and a Latin code page has no
Devanagari letters at all. Microsoft’s documentation adds that data lost in a code-page conversion is not reported,
so a Hindi name saved into such a column is damaged without any error. The run below counts characters and bytes:
-- Characters and bytes of four names: what a column sized in bytes can hold. SQLite stores text as UTF-8, so the
-- length of the text cast to a BLOB is its UTF-8 size. UTF-16, used by SQL Server's nvarchar and Oracle's
-- NVARCHAR2 (AL16UTF16), takes 2 bytes for every character up to U+FFFF and 4 bytes above it.
WITH names(label, name) AS (
VALUES ('English name', 'Asha'), ('French name', 'Zoë'), ('Hindi greeting', 'नमस्ते'), ('Marathi, the word', 'मराठी')
),
chars(label, name, pos, cp) AS (
SELECT label, name, 1, unicode(substr(name, 1, 1)) FROM names
UNION ALL
SELECT label, name, pos + 1, unicode(substr(name, pos + 1, 1)) FROM chars WHERE pos < length(name)
)
SELECT label,
count(*) AS characters,
length(CAST(name AS BLOB)) AS utf8_bytes,
sum(CASE WHEN cp > 65535 THEN 4 ELSE 2 END) AS utf16_bytes,
sum(cp > 255) AS beyond_latin1,
name
FROM chars
GROUP BY label, name
ORDER BY utf8_bytes, label;
-- The six characters of नमस्ते: all in the Devanagari block U+0900 to U+097F, so 3 bytes each in UTF-8.
WITH chars(pos, ch) AS (
SELECT 1, substr('नमस्ते', 1, 1)
UNION ALL
SELECT pos + 1, substr('नमस्ते', pos + 1, 1) FROM chars WHERE pos < length('नमस्ते')
)
SELECT pos, printf('U+%04X', unicode(ch)) AS code_point, length(CAST(ch AS BLOB)) AS utf8_bytes, ch
FROM chars; Output
┌───────────────────┬────────────┬────────────┬─────────────┬───────────────┬────────┐ │ label │ characters │ utf8_bytes │ utf16_bytes │ beyond_latin1 │ name │ ├───────────────────┼────────────┼────────────┼─────────────┼───────────────┼────────┤ │ English name │ 4 │ 4 │ 8 │ 0 │ Asha │ │ French name │ 3 │ 4 │ 6 │ 0 │ Zoë │ │ Marathi, the word │ 5 │ 15 │ 10 │ 5 │ मराठी │ │ Hindi greeting │ 6 │ 18 │ 12 │ 6 │ नमस्ते │ └───────────────────┴────────────┴────────────┴─────────────┴───────────────┴────────┘ ┌─────┬────────────┬────────────┬────┐ │ pos │ code_point │ utf8_bytes │ ch │ ├─────┼────────────┼────────────┼────┤ │ 1 │ U+0928 │ 3 │ न │ │ 2 │ U+092E │ 3 │ म │ │ 3 │ U+0938 │ 3 │ स │ │ 4 │ U+094D │ 3 │ ् │ │ 5 │ U+0924 │ 3 │ त │ │ 6 │ U+0947 │ 3 │ े │ └─────┴────────────┴────────────┴────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < hindi_bytes.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
Each Devanagari letter and sign is one code point between U+0900 and U+097F, and UTF-8 needs 3 bytes for each of them,
so नमस्ते takes 18 bytes in UTF-8 and 12 in UTF-16. Store such text in nvarchar, or in varchar under a _UTF8
collation in SQL Server 2019 and later. In Oracle use an AL32UTF8 database with VARCHAR2 sized in characters, or
NVARCHAR2.
Your turn
Exercise · Easy · SQL
List the logins that would collide without regard to case
A shop's accounts move from SQL Server, where the email column used a case-insensitive collation, to Oracle, which compares text byte by byte. In Oracle, the team will keep emails unique without regard to case with a unique index on UPPER(email). The copy of the data you have here came from an older system that never checked this, so some addresses appear more than once in different capitals, and the index would refuse to build.
The table users has two columns, id and email. Write the query that lists every address the index would reject, one row per address: the address in capitals as email_key, and the number of accounts that share it as accounts. Addresses that occur only once must not appear.
Starter code · case_collisions.sql
-- List each address that appears more than once without regard to case:
-- the address in capitals as email_key, and how many accounts share it as accounts.
SELECT email AS email_key, count(*) AS accounts
FROM users
GROUP BY email
HAVING count(*) > 1; The sample tests · tests.yaml
# Sample tests: each one builds a small users table, runs your query on it and compares the rows it returns (in any
# order).
tests:
- name: finds two spellings of one address
setup: |
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL);
INSERT INTO users VALUES
(1, 'asha@mail.in'),
(2, 'Asha@Mail.in'),
(3, 'ravi@mail.in');
expected:
columns: [email_key, accounts]
rows:
- [ASHA@MAIL.IN, 2]
- name: counts three spellings once, and leaves single addresses out
setup: |
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL);
INSERT INTO users VALUES
(1, 'meera.s@mail.in'),
(2, 'MEERA.S@MAIL.IN'),
(3, 'Meera.S@mail.IN'),
(4, 'kabir@mail.in'),
(5, 'zoya@mail.in');
expected:
columns: [email_key, accounts]
rows:
- [MEERA.S@MAIL.IN, 3]
- name: reports every colliding address
setup: |
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL);
INSERT INTO users VALUES
(1, 'imran@mail.in'),
(2, 'Imran@mail.in'),
(3, 'neha@mail.in'),
(4, 'NEHA@mail.in'),
(5, 'neha@MAIL.in'),
(6, 'tenzin@mail.in');
expected:
columns: [email_key, accounts]
rows:
- [IMRAN@MAIL.IN, 2]
- [NEHA@MAIL.IN, 3]
- name: returns nothing when every address is unique
setup: |
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL);
INSERT INTO users VALUES
(1, 'arjun@mail.in'),
(2, 'arjun.k@mail.in'),
(3, 'fatima@mail.in');
expected:
columns: [email_key, accounts]
rows: [] A hint
Group the rows by the same expression the index will use, upper(email), and count the rows in each group. A group is a problem only when it has more than one row, which is a condition on the groups, so it belongs in HAVING.
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
5 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.
Case-insensitive on purpose, per query, per session and in a unique index Pro: opens with a Pro, Premium or Ultimate pass
Most systems need both rules: case-insensitive matching for logins and searches, exact matching for codes and passwords. This part shows how to pick the rule for one comparison, one session or one column in each engine, and reproduces a case-insensitive unique email with the index that keeps its login fast.
In this part:
- One comparison at a time in SQL Server, and what LIKE does with it
- Oracle’s two session switches, and the one that is not enough
- A unique email without regard to case, reproduced
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).
Collation conflicts, the tempdb trap and sizing Unicode columns, worked through Pro: opens with a Pro, Premium or Ultimate pass
Two collations in one comparison is an error in SQL Server, and the most common way to get one is a temporary table. This part follows that failure step by step, then sizes text columns for Indian scripts with the numbers for each engine.
In this part:
- Why two collations in one comparison fail
- The tempdb trap, step by step
- Sizing text columns for Indian scripts, with the numbers
1 runnable example (SQL) · about 3 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 do CI and AS mean in the SQL Server collation SQL_Latin1_General_CP1_CI_AS, and how does Oracle compare text by default?
CI means case-insensitive and AS means accent-sensitive, so under this collation 'Pune' = 'PUNE' is true but 'café' = 'cafe' is false; CP1 names code page 1252, used for char and varchar columns. SQL Server's setup picks this collation from the operating system's locale for English (United States). Oracle starts from the opposite rule: its session settings NLS_COMP and NLS_SORT are BINARY by default, so it compares the bytes of the text. 'Pune' = 'PUNE' is then false, DISTINCT keeps both spellings, and every capital letter sorts before every small one. A query that relies on SQL Server's case-insensitive default therefore returns fewer rows in Oracle unless the comparison ignores case on purpose.
-
Mid to senior
A stored procedure runs in 40 customer databases. In one of them, created with a case-sensitive collation, it fails with a collation conflict at a join between a temporary table and a permanent table. Why only there, and how do you fix it for all 40?
-
Mid to senior
In Oracle, a login query uses WHERE UPPER(email) = UPPER(:email) on a users table of 5 million rows and takes 900 ms. Make it fast, and make sure no two accounts share an address in different capitals.
-
Mid to senior
Customer names typed in Hindi look right in the app before saving, but a SQL Server report shows them as question marks. The column is varchar(50). Find the cause and fix it without losing more data.
Key takeaways
- SQL Server’s usual default collation ignores case and respects accents; Oracle compares binary by default.
- In SQL Server,
COLLATEsets the rule per database, column or expression. - In Oracle,
NLS_SORTalone changes only sorting;NLS_COMP = LINGUISTICmakes comparisons follow it. - Store Indian scripts as Unicode, and size byte-counted columns at 3 bytes per Devanagari character in UTF-8.
References
- SQL Server documentation: Collation and Unicode support (Microsoft)
- SQL Server documentation: COLLATE (Transact-SQL) (Microsoft)
- SQL Server documentation: Collation precedence (Microsoft)
- SQL Server documentation: char and varchar (Transact-SQL) (Microsoft)
- SQL Server documentation: table (Transact-SQL), collation of table variable columns (Microsoft)
- SQL Server documentation: LIKE (Transact-SQL) (Microsoft)
- Oracle AI Database Globalization Support Guide 26: Linguistic Sorting and Matching (Oracle Corporation)
- Oracle AI Database Globalization Support Guide 26: Supporting Multilingual Databases with Unicode (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: COLLATE Operator (Oracle Corporation)
- Oracle AI Database Reference 26: NLS_COMP (Oracle Corporation)
- Oracle AI Database Reference 26: NLS_SORT (Oracle Corporation)
- Oracle AI Database Reference 26: MAX_STRING_SIZE (Oracle Corporation)
- Oracle AI Database Reference 26: NLS_LENGTH_SEMANTICS (Oracle Corporation)
- Oracle AI Database SQL Language Reference 26: Data Types (VARCHAR2 length semantics) (Oracle Corporation)
- Oracle AI Database Concepts 26: Indexes and Index-Organized Tables (function-based indexes) (Oracle Corporation)
- Oracle error help: ORA-43929 (Oracle Corporation)
- SQLite: Datatypes In SQLite (collating sequences BINARY and NOCASE) (SQLite)
- SQLite: CREATE INDEX (indexes on expressions) (SQLite)
- The Unicode Standard: Devanagari code chart (U+0900 to U+097F) (Unicode Consortium)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress