Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot
Free core, Pro depth: 12 of 30 minutes free
Free: The functions side by side · Length, bytes and the empty string · Cutting and searching · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 18 minutes.

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

String functions in SQL Server 2025 and Oracle 26ai
Criterion SQL Server 2025Oracle 26ai
Characters LEN(s), no trailing spacesLENGTH(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 firsta || sep || b
Rows into a list STRING_AGG(s, sep)LISTAGG(s, sep)
A list into rows STRING_SPLIT with an ordinalREGEXP_SUBSTR with CONNECT BY
Classes such as [A-Z] LIKE; REGEXP_LIKE from 2025REGEXP_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:

Characters and bytes of four strings SQL · lengths.sql
-- 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

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.

Substrings and searches in one product code SQL · positions.sql
-- 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

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

An address with a NULL flat joined four ways: + gives NULL, CONCAT and Oracle || keep a leading comma, CONCAT_WS skips it.flat is NULL,street 'Hill Street', city 'Kochi'Joined with ', ' between the partsSQL Server flat + ', ' + street …:NULL, the whole address is lostSQL Server CONCAT(flat, ', ', street, …):', Hill Street, Kochi'SQL Server CONCAT_WS(', ', flat, street, city):'Hill Street, Kochi'Oracle flat || ', ' || street …:', Hill Street, Kochi'

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.

  1. SQL Server's + operator returns NULL as soon as one part is NULL, so the whole address disappears.
  2. SQL Server's CONCAT treats the NULL as an empty string, so the result starts with the separator that followed the flat number.
  3. SQL Server's CONCAT_WS skips the NULL part together with its separator and returns the street and the city only.
  4. 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.

An address with a missing part, joined four ways SQL · concat_nulls.sql
-- 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

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.

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.

  1. Question 1 of 5 What does this statement return in SQL Server?

    Read the code, then choose one answer.

    SELECT LEN('abc   '), DATALENGTH('abc   ');
    Show the answer to question 1

    Answer: 3 and 6

    LEN counts characters without the trailing spaces, while DATALENGTH counts the bytes of the whole value, spaces included. Oracle's LENGTH counts the trailing spaces of a VARCHAR2 value, so it returns 6.

  2. Question 2 of 5 What does this query return in Oracle?

    Read the code, then choose one answer.

    SELECT LENGTH('') FROM dual;
    Show the answer to question 2

    Answer: NULL

    Oracle stores a zero-length string as NULL, and LENGTH of NULL is NULL. SQL Server keeps the empty string as a value, so LEN('') returns 0 there.

  3. Question 3 of 5 A port turned CHARINDEX('-', code) into this Oracle expression. What does it return for the code SKU-2041?

    Read the code, then choose one answer.

    SELECT INSTR('-', 'SKU-2041') FROM dual;
    Show the answer to question 3

    Answer: 0, because it looks for SKU-2041 inside the hyphen

    CHARINDEX names the text to find first, and INSTR names it second, so the arguments must swap places: INSTR(code, '-'). With the order kept, INSTR searches the one-character string for the code and finds nothing.

  4. Question 4 of 5 In SQL Server, flat is NULL and street is 'Hill Street'. What does flat + ', ' + street return?

    Choose one answer.

    Show the answer to question 4

    Answer: NULL

    CONCAT_NULL_YIELDS_NULL is ON, as it always is from SQL Server 2017, so joining a NULL with + gives NULL. CONCAT returns ', Hill Street', CONCAT_WS skips the NULL with its separator, and Oracle's || treats the NULL as an empty string.

  5. Question 5 of 5 Does the condition code LIKE '[A-Z]%' find the codes that start with a capital letter in Oracle?

    Choose one answer.

    Show the answer to question 5

    Answer: No, because Oracle's LIKE knows only % and _, so it looks for the characters [A-Z] themselves

    Character classes in brackets are a SQL Server extension of LIKE. In Oracle, write REGEXP_LIKE(code, '^[A-Z]').

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.

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.

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

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • LEN leaves out trailing spaces, Oracle’s LENGTH counts them, and Oracle stores an empty string as NULL.
  • CHARINDEX(find, s) and INSTR(s, find) take their arguments in opposite order, and position 0 differs between SUBSTRING and SUBSTR.
  • SQL Server’s + returns NULL for a NULL part; CONCAT, CONCAT_WS and Oracle’s || do not.
  • STRING_AGG and LISTAGG have byte limits, and a split must keep empty items to keep their positions.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.