SQL (PostgreSQL, MySQL, SQLite) Module 21 – SQL Server and Oracle side by side
String functions: SQL Server vs Oracle
LEN and LENGTH, SUBSTRING and SUBSTR, CHARINDEX and INSTR, joins with NULL parts, STRING_AGG and LISTAGG, splitting and LIKE in SQL Server and Oracle.
What you will learn
- Measure, cut and search strings with each engine's functions, and spot the arguments that differ
- Join strings and aggregate rows into lists without NULL surprises or length errors
- Split a list into items with their positions, keeping empty items in place
- Match patterns with LIKE and with regular expressions in both engines
Before you start
What you’ll learn · Before you start
What you will learn
- Measure, cut and search strings with each engine's functions, and spot the arguments that differ
- Join strings and aggregate rows into lists without NULL surprises or length errors
- Split a list into items with their positions, keeping empty items in place
- Match patterns with LIKE and with regular expressions in both engines
Before you start
On this page
SQL Server and Oracle offer the same string operations under different names, and the small differences between
them are what breaks ported code. SQL Server measures a string with LEN, cuts it with SUBSTRING, searches it
with CHARINDEX and joins strings with + or CONCAT; Oracle uses LENGTH, SUBSTR, INSTR and ||. The two
search functions take their arguments in opposite order, position 0 means something different to each, LEN leaves
out trailing spaces, and Oracle stores an empty string as NULL, which changes both lengths and joins. Lists differ
as well: STRING_AGG and LISTAGG turn rows into one string under different size limits, and LIKE understands
character classes only in SQL Server.
Every result on this page came from SQLite 3.49.1 running inside sql.js 1.14.2, and Run repeats the query in your browser. SQLite’s string functions resemble both dialects, so each script labels its columns with the SQL Server or Oracle call they stand for, and the rules of each engine are quoted from its manual.
The functions side by side
| Criterion | SQL Server 2025 | Oracle 26ai |
|---|---|---|
| Characters | LEN(s), no trailing spaces | LENGTH(s), with them |
| Bytes | DATALENGTH(s) | LENGTHB(s) |
| Substring | SUBSTRING(s, start, n) | SUBSTR(s, start, n) |
| Find x in s | CHARINDEX(x, s, start) | INSTR(s, x, start, nth) |
| Join | s + t, or CONCAT(s, t, …) | s || t, or CONCAT(s, t) |
| Join with a separator | CONCAT_WS with the separator first | a || sep || b |
| Rows into a list | STRING_AGG(s, sep) | LISTAGG(s, sep) |
| A list into rows | STRING_SPLIT with an ordinal | REGEXP_SUBSTR with CONNECT BY |
| Classes such as [A-Z] | LIKE; REGEXP_LIKE from 2025 | REGEXP_LIKE only |
Most rows are a change of name, which is why ports start well. The trouble sits in four details: what counts toward a length, where a position starts, which argument comes first, and what a NULL part does to a joined string.
Length, bytes and the empty string
SQL Server’s manual defines LEN as the number of characters without trailing spaces and points to DATALENGTH for
the number of bytes. Oracle’s LENGTH counts every character of a VARCHAR2 value, trailing spaces included, and
LENGTHB counts bytes. The script measures four values:
-- How long is a string? SQL Server's LEN and DATALENGTH and Oracle's LENGTH, modelled with SQLite's length and
-- octet_length. SQLite counts characters (code points) and stores text as UTF-8.
-- len LEN(s): characters, without trailing spaces
-- oracle_length LENGTH(s): characters, trailing spaces included; NULL for '' because Oracle stores '' as NULL
-- utf8_bytes DATALENGTH(s) of varchar under a _UTF8 collation, or Oracle's LENGTHB(s) in an AL32UTF8 database
-- nvarchar_bytes DATALENGTH(s) of nvarchar: 2 bytes per character for every character used here
WITH samples (label, s) AS (
VALUES ('code with 3 trailing spaces', 'abc '),
('empty string', ''),
('French name', 'Zoë'),
('Tamil word', 'தமிழ்')
)
SELECT label,
length(rtrim(s, ' ')) AS len,
length(nullif(s, '')) AS oracle_length,
octet_length(s) AS utf8_bytes,
2 * length(s) AS nvarchar_bytes
FROM samples; Output
┌─────────────────────────────┬─────┬───────────────┬────────────┬────────────────┐ │ label │ len │ oracle_length │ utf8_bytes │ nvarchar_bytes │ ├─────────────────────────────┼─────┼───────────────┼────────────┼────────────────┤ │ code with 3 trailing spaces │ 3 │ 6 │ 6 │ 12 │ │ empty string │ 0 │ │ 0 │ 0 │ │ French name │ 3 │ 3 │ 4 │ 6 │ │ Tamil word │ 5 │ 5 │ 15 │ 10 │ └─────────────────────────────┴─────┴───────────────┴────────────┴────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < lengths.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
The code with three trailing spaces has a LEN of 3 and an Oracle LENGTH of 6, so a check such as LEN(code) = 3
accepts values that Oracle reports as six characters long. The empty string has a length of 0 in SQL Server and no
length at all in Oracle: the manual says that Oracle treats a character value of length zero as NULL, and the length
of NULL is NULL. Bytes depend on the encoding. The Tamil word has 5 characters, 15 bytes in UTF-8 and 10 bytes as
nvarchar, so a column sized in bytes fills up long before one sized in characters.
Cutting and searching
SUBSTRING and SUBSTR both count from 1, so four characters from position 5 are the same text in both engines.
Position 0 is where they part. SQL Server’s manual says that a start below 1 still uses up length for the positions
before the first character, while Oracle’s manual says that a position of 0 is treated as 1. A negative position
counts from the end of the string, but only in Oracle.
-- Cutting and searching one product code, modelled with SQLite's substr and instr.
SELECT substr('SKU-2041-BLUE', 5, 4) AS from_5_take_4, -- SUBSTRING(s, 5, 4) and SUBSTR(s, 5, 4) agree: '2041'
substr('SKU-2041-BLUE', 0, 4) AS sqlserver_from_0, -- SUBSTRING(s, 0, 4): positions 0 to 3, so only 3 characters
substr('SKU-2041-BLUE', 1, 4) AS oracle_from_0, -- SUBSTR(s, 0, 4): Oracle reads position 0 as 1, so 4 characters
substr('SKU-2041-BLUE', -4) AS oracle_last_4; -- SUBSTR(s, -4): a negative position counts from the end
-- CHARINDEX(find, s) names the text to find first; Oracle's INSTR(s, find) names it second.
WITH codes (s) AS (VALUES ('SKU-2041-BLUE'))
SELECT instr(s, '-') AS first_hyphen, -- CHARINDEX('-', s) or INSTR(s, '-')
instr(substr(s, instr(s, '-') + 1), '-') + instr(s, '-') AS second_hyphen, -- INSTR(s, '-', 1, 2) in Oracle
substr(s, 1, instr(s, '-') - 1) AS prefix, -- the text before the first hyphen
instr('-', s) AS arguments_swapped -- INSTR('-', s): looks for s inside '-'
FROM codes; Output
┌───────────────┬──────────────────┬───────────────┬───────────────┐ │ from_5_take_4 │ sqlserver_from_0 │ oracle_from_0 │ oracle_last_4 │ ├───────────────┼──────────────────┼───────────────┼───────────────┤ │ 2041 │ SKU │ SKU- │ BLUE │ └───────────────┴──────────────────┴───────────────┴───────────────┘ ┌──────────────┬───────────────┬────────┬───────────────────┐ │ first_hyphen │ second_hyphen │ prefix │ arguments_swapped │ ├──────────────┼───────────────┼────────┼───────────────────┤ │ 4 │ 9 │ SKU │ 0 │ └──────────────┴───────────────┴────────┴───────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < positions.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
SUBSTRING(s, 0, 4) returned three characters and SUBSTR(s, 0, 4) four, a difference that appears as a missing or
an extra character in every row. The search functions differ in argument order: CHARINDEX('-', s) names the text
to find first, INSTR(s, '-') names the string to search first, and a port that keeps the old order returns 0 for
every row without any error. Oracle’s INSTR also takes an occurrence, so INSTR(s, '-', 1, 2) finds the second
hyphen, which SQL Server reaches by nesting one CHARINDEX inside another as its start position.
Joining strings, and NULL
A missing part, joined four ways
Text description of the diagram
The diagram starts from an address whose flat number is NULL while the street and the city are known, and shows what each way of joining the parts with a comma and a space returns.
- SQL Server's + operator returns NULL as soon as one part is NULL, so the whole address disappears.
- SQL Server's CONCAT treats the NULL as an empty string, so the result starts with the separator that followed the flat number.
- SQL Server's CONCAT_WS skips the NULL part together with its separator and returns the street and the city only.
- Oracle's || operator treats the NULL as an empty string, like CONCAT, and also keeps the leading separator.
SQL Server’s + returns NULL when any part is NULL, because CONCAT_NULL_YIELDS_NULL is ON, which its manual says
is always the case from SQL Server 2017. CONCAT turns a NULL into an empty string, and CONCAT_WS skips a NULL
part together with its separator. Oracle’s manual says that its || joins a NULL like an empty string, so the result
is NULL only when every part is NULL.
-- One address from parts that may be missing (NULL), joined the way each engine joins text.
CREATE TABLE addresses (id INTEGER PRIMARY KEY, flat TEXT, street TEXT NOT NULL, city TEXT NOT NULL);
INSERT INTO addresses (id, flat, street, city) VALUES
(1, 'Flat 4B', 'Lake Road', 'Pune'),
(2, NULL, 'Hill Street', 'Kochi');
SELECT id,
-- SQL Server: flat + ', ' + street + ', ' + city is NULL as soon as one part is NULL (SQLite's || does the same)
flat || ', ' || street || ', ' || city AS plus_operator,
-- CONCAT turns NULL into an empty string, so the separator after it stays
concat(flat, ', ', street, ', ', city) AS concat_function,
-- CONCAT_WS skips the NULL part together with its separator
concat_ws(', ', flat, street, city) AS concat_ws_function,
-- Oracle: || treats NULL as an empty string, like CONCAT
coalesce(flat, '') || ', ' || street || ', ' || city AS oracle_pipes
FROM addresses
ORDER BY id; Output
┌────┬──────────────────────────┬──────────────────────────┬──────────────────────────┬──────────────────────────┐ │ id │ plus_operator │ concat_function │ concat_ws_function │ oracle_pipes │ ├────┼──────────────────────────┼──────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 1 │ Flat 4B, Lake Road, Pune │ Flat 4B, Lake Road, Pune │ Flat 4B, Lake Road, Pune │ Flat 4B, Lake Road, Pune │ │ 2 │ │ , Hill Street, Kochi │ Hill Street, Kochi │ , Hill Street, Kochi │ └────┴──────────────────────────┴──────────────────────────┴──────────────────────────┴──────────────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < concat_nulls.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
The second address lost everything with +, kept a stray leading comma with CONCAT and with Oracle’s ||, and
came out clean only with CONCAT_WS. Numbers behave differently too. In SQL Server, 'Room ' + 101 fails, because
int ranks above varchar in data type precedence and the engine tries to turn the text into a number; Oracle’s
'Room ' || 101 converts the number and returns Room 101. In SQL Server, use CONCAT, which converts its arguments
to strings, or an explicit CAST.
Your turn
Exercise · Medium · SQL
Split a comma list into items with their positions
The table payments holds id and methods, a comma-separated list of payment methods such as card,wallet,,cash. The n-th item belongs to the n-th instalment, so an empty item still takes its place: in that list, cash is item 4.
Return id, pos and method for every item of every list, empty items included as empty strings, numbered from 1 in list order, the way SQL Server's STRING_SPLIT(methods, ',', 1) numbers them. Order the rows by id and then pos. No list is NULL or empty.
Starter code · split_methods.sql
-- Return id, pos and method for every item of every list, empty items included, numbered from 1.
-- This version returns only the first item of each list.
SELECT id, 1 AS pos, substr(methods, 1, instr(methods || ',', ',') - 1) AS method
FROM payments
ORDER BY id, pos; The sample tests · tests.yaml
# Sample tests: each one builds the payments table, runs your query and compares the rows it returns, in order.
tests:
- name: a list with an empty third item
ordered: true
setup: |
CREATE TABLE payments (id INTEGER PRIMARY KEY, methods TEXT NOT NULL);
INSERT INTO payments (id, methods) VALUES (1, 'card,wallet,,cash');
expected:
columns: [id, pos, method]
rows:
- [1, 1, 'card']
- [1, 2, 'wallet']
- [1, 3, '']
- [1, 4, 'cash']
- name: a single item, and a list that ends with an empty item
ordered: true
setup: |
CREATE TABLE payments (id INTEGER PRIMARY KEY, methods TEXT NOT NULL);
INSERT INTO payments (id, methods) VALUES (5, 'cash'), (6, 'card,');
expected:
columns: [id, pos, method]
rows:
- [5, 1, 'cash']
- [6, 1, 'card']
- [6, 2, ''] A hint
Add a comma to the end of each list, then write a recursive common table expression that cuts off one item per step: the item is the text before the first comma, substr(rest, 1, instr(rest, ',') - 1), and the rest is the text after it. Stop when nothing is left.
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.
Lists into one string and back, with failures reproduced Pro: opens with a Pro, Premium or Ultimate pass
Joining rows into one list and splitting a list back into rows are where the limits and defaults of the two engines differ most. This part reproduces the length failures and the lost positions, and gives a form that works in both.
In this part:
- Ordered lists, and lists without repeats
- The length limits, with numbers
- Splitting with positions
- A JSON array keeps every place
2 runnable examples (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).
Patterns, cleaning and conversions, with numbers Pro: opens with a Pro, Premium or Ultimate pass
Imports and reports meet text that is not quite what they expect: codes in the wrong case, phone numbers full of punctuation, numbers stored as text. This part compares the pattern, cleaning and conversion functions of both engines and reproduces each difference.
In this part:
- LIKE with classes, and regular expressions
- Cleaning an import, reproduced
- Trailing spaces in comparisons, reproduced
- Trimming sets of characters
- Numbers and text
3 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 do SQL Server's LEN and Oracle's LENGTH return for a code with trailing spaces, and for an empty string?
LEN counts characters without the trailing spaces, so three letters followed by three spaces have a LEN of 3, while DATALENGTH reports the bytes of the whole value. Oracle's LENGTH counts every character of a VARCHAR2 value, trailing spaces included, so the same code has a length of 6. An empty string has a LEN of 0 in SQL Server, but Oracle stores a string of length zero as NULL, so LENGTH returns NULL. Code that checks lengths or tests for empty values needs both rules in mind when it moves between the engines.
-
Mid to senior
An Oracle job splits each comma-separated line of a supplier file with REGEXP_SUBSTR(line, '[^,]+', 1, LEVEL) and maps item n to column n. Since the supplier started leaving optional fields empty, values land in the wrong columns. Explain the bug, fix it in Oracle, and say how SQL Server would split the same line.
-
Mid to senior
A nightly export writes one line per customer with all of their order numbers, built with LISTAGG in Oracle and STRING_AGG in SQL Server. It has started failing for a few large customers, with ORA-01489 in Oracle and error 9829 in SQL Server. Explain both errors and fix the export in each engine.
-
Senior to staff
A team ported a T-SQL reporting layer to Oracle by renaming functions: LEN to LENGTH, SUBSTRING to SUBSTR, CHARINDEX to INSTR and + to ||. The unit tests pass, yet several production reports now differ. Which differences do you check first, and how do you find the affected queries?
Key takeaways
LENleaves out trailing spaces, Oracle’sLENGTHcounts them, and Oracle stores an empty string as NULL.CHARINDEX(find, s)andINSTR(s, find)take their arguments in opposite order, and position 0 differs betweenSUBSTRINGandSUBSTR.- SQL Server’s
+returns NULL for a NULL part;CONCAT,CONCAT_WSand Oracle’s||do not. STRING_AGGandLISTAGGhave byte limits, and a split must keep empty items to keep their positions.
References
- LEN (Transact-SQL) (Microsoft)
- DATALENGTH (Transact-SQL) (Microsoft)
- SUBSTRING (Transact-SQL) (Microsoft)
- CHARINDEX (Transact-SQL) (Microsoft)
- + (String Concatenation) (Transact-SQL) (Microsoft)
- SET CONCAT_NULL_YIELDS_NULL (Transact-SQL) (Microsoft)
- CONCAT (Transact-SQL) (Microsoft)
- CONCAT_WS (Transact-SQL) (Microsoft)
- Data type precedence (Transact-SQL) (Microsoft)
- STRING_AGG (Transact-SQL) (Microsoft)
- Database Engine events and errors (9000 to 9999): error 9829 (Microsoft)
- STRING_SPLIT (Transact-SQL) (Microsoft)
- LIKE (Transact-SQL) (Microsoft)
- REGEXP_LIKE (Transact-SQL) (Microsoft)
- Regular expressions functions (Transact-SQL) (Microsoft)
- LTRIM (Transact-SQL) (Microsoft)
- TRIM (Transact-SQL) (Microsoft)
- TRANSLATE (Transact-SQL) (Microsoft)
- TRY_CAST (Transact-SQL) (Microsoft)
- FORMAT (Transact-SQL) (Microsoft)
- Oracle AI Database SQL Language Reference: LENGTH (Oracle Corporation)
- Oracle AI Database SQL Language Reference: Nulls (Oracle Corporation)
- Oracle AI Database SQL Language Reference: SUBSTR (Oracle Corporation)
- Oracle AI Database SQL Language Reference: INSTR (Oracle Corporation)
- Oracle AI Database SQL Language Reference: Concatenation Operator (Oracle Corporation)
- Oracle AI Database SQL Language Reference: LISTAGG (Oracle Corporation)
- Oracle error help: ORA-01489 (Oracle Corporation)
- Oracle error help: ORA-01722 (Oracle Corporation)
- Oracle AI Database Reference: MAX_STRING_SIZE (Oracle Corporation)
- Oracle AI Database SQL Language Reference: REGEXP_SUBSTR (Oracle Corporation)
- Oracle AI Database SQL Language Reference: Pattern-matching Conditions (LIKE, REGEXP_LIKE) (Oracle Corporation)
- Oracle AI Database SQL Language Reference: REGEXP_REPLACE (Oracle Corporation)
- Oracle AI Database SQL Language Reference: LPAD (Oracle Corporation)
- Oracle AI Database SQL Language Reference: LTRIM (Oracle Corporation)
- Oracle AI Database SQL Language Reference: REPLACE (Oracle Corporation)
- Oracle AI Database SQL Language Reference: INITCAP (Oracle Corporation)
- Oracle AI Database SQL Language Reference: TO_NUMBER (Oracle Corporation)
- Oracle AI Database SQL Language Reference: TO_CHAR (number) (Oracle Corporation)
- OPENJSON (Transact-SQL) (Microsoft)
- JSON_ARRAYAGG (Transact-SQL) (Microsoft)
- = (String comparison or assignment) (Transact-SQL) (Microsoft)
- Oracle AI Database SQL Language Reference: Data Type Comparison Rules (blank-padded and nonpadded) (Oracle Corporation)
- Oracle AI Database SQL Language Reference: JSON_TABLE (Oracle Corporation)
- Oracle AI Database SQL Language Reference: JSON_ARRAYAGG (Oracle Corporation)
- SQLite: JSON Functions (json_each) (SQLite)
- SQLite: Built-In Scalar SQL Functions (SQLite)
- SQLite: Built-in Aggregate Functions (SQLite)
- SQLite: SQL Language Expressions (LIKE and GLOB) (SQLite)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress