SQLab Hub
Lesson 24 · String Functions
Lesson 24 · Functions

SQL String Functions: CONCAT, SUBSTRING, TRIM and More

String functions work on text: they join values together, change their case, measure them, strip spaces and pull out or replace parts of them. They are the main tool for cleaning text data in SQL, for example turning "priya nair", "PRIYA NAIR" and " Priya Nair " into one comparable value.

" CONCAT · UPPER · LOWER · TRIM ✂ SUBSTRING & REPLACE ▶ Runnable Playground examples 🧪 6 practice exercises 📝 25-question quiz ⏱ ~35 min

What Are SQL String Functions?

A string is a sequence of characters: letters, digits, spaces and symbols. A string function is a built-in function that takes text and returns a changed version of it, or a fact about it such as its length. String functions run once per row, so the result has as many rows as the input.

The classic reason to use them is messy data. In the five sample customers below, the same person was typed three different ways. To a person they are obviously the same customer; to an exact text comparison they are three different values. TRIM removes the stray spaces and LOWER makes the case consistent:

SQLite · runs in the Playground
WITH customers(id, name, city, email, phone) AS (
  VALUES (1, 'priya nair',    'Delhi',  ' Priya.Nair@EXAMPLE.com ', '98234-11567'),
         (2, 'PRIYA NAIR',    'DELHI',  'priya.nair@example.com',   '98234 11567'),
         (3, '  Priya Nair ', 'delhi',  'Priya.Nair@Example.com',   ' 98234-11567 '),
         (4, 'Neha Kapoor',   'Mumbai', 'neha.kapoor@example.org',  '99123-44521'),
         (5, 'vikram singh',  'Pune',   'Vikram.Singh@example.net', '97845-22110')
)
SELECT TRIM(LOWER(name)) AS clean_name,
       COUNT(*) AS rows_in_table
FROM customers
GROUP BY TRIM(LOWER(name))
ORDER BY clean_name;

▶ Run this query in the SQL Playground

Customer names after TRIM and LOWER
clean_namerows_in_table
neha kapoor1
priya nair3
vikram singh1
✓ 3 rows · real output from the Playground database

Five rows become 3 distinct customers, and the 3 spellings of Priya Nair now group together. The sample customers are written into the query with WITH ... VALUES, so you can run it in the Playground and change the rows yourself.

Business problemString function
Combine first and last name into one fieldCONCAT() or ||
Make inconsistent casing consistentUPPER() / LOWER()
Remove accidental leading and trailing spacesTRIM(), LTRIM(), RTRIM()
Take part of a code, such as a prefix or the digitsSUBSTRING() / SUBSTR(), LEFT(), RIGHT()
Strip formatting characters from a phone numberREPLACE()
Find where a character sits, such as the @ in an emailINSTR(), POSITION(), CHARINDEX()
Check how many characters a value hasLENGTH() / LEN()

The Core String Functions at a Glance

The names below are the common ones. The Playground runs SQLite, which spells a few of them differently; the last column shows what to use there.

FunctionWhat it doesExampleIn SQLite
CONCAT()Joins values into one stringCONCAT(first_name, ' ', last_name)CONCAT() or ||
UPPER()Converts letters to uppercaseUPPER(city)Same
LOWER()Converts letters to lowercaseLOWER(email)Same
LENGTH()Counts charactersLENGTH(name)Same
TRIM()Removes leading and trailing spacesTRIM(name)Same, plus LTRIM / RTRIM
SUBSTRING()Takes part of a string by position and lengthSUBSTRING(sku, 5, 6)SUBSTR() or SUBSTRING()
REPLACE()Replaces every occurrence of a valueREPLACE(phone, '-', '')Same
LEFT()First n charactersLEFT(sku, 3)SUBSTR(sku, 1, 3)
RIGHT()Last n charactersRIGHT(phone, 4)SUBSTR(phone, -4)
Names differ between databases. SQL Server uses LEN() instead of LENGTH() and CHARINDEX() instead of POSITION(). The database comparison further down lists the differences.

SQL CONCAT(): Join Text Together

CONCAT() joins two or more values end to end. It never adds a separator for you: a space, comma or dash has to be passed as its own argument. The most common use is building a full name:

SQLite · runs in the Playground
SELECT CONCAT(first_name, ' ', last_name) AS full_name,
       title
FROM employees
ORDER BY id
LIMIT 5;

▶ Run this query in the SQL Playground

Employee full names built with CONCAT
full_nametitle
William LewisSenior CS Manager
John MillerParalegal
Daniel RiveraVP Engineering
Melissa SmithVP Engineering
Jason MitchellProduct Designer
✓ 5 rows · real output from the Playground database

Each row combines three pieces: the first name, a literal space and the last name. Always give the result an alias such as full_name; otherwise the column is named after the whole expression.

CONCAT with more than two columns

CONCAT takes any number of arguments, so labels for reports and dropdowns can be built in one expression:

SQLite · runs in the Playground
SELECT CONCAT(name, ' (', category, ') - $', price) AS product_label
FROM products
ORDER BY id
LIMIT 5;

▶ Run this query in the SQL Playground

Product labels with name, category and price
product_label
Play Kitchen Deluxe (Toys) - $1786.38
Micro-Cut Paper Shredder 8-Sheet (Office Supplies) - $907.93
4K Dual Dash Camera (Automotive) - $568.89
Ergonomic Desk Mat XL (Office Supplies) - $1618.64
Premium Beard Oil 60ml (Health & Beauty) - $1428.31
✓ 5 rows · real output from the Playground database

The || operator

Standard SQL joins strings with ||, which works in SQLite, PostgreSQL and Oracle. SQL Server uses + instead, and MySQL treats || as logical OR unless a setting is changed. The two forms give the same result until a value is NULL:

SQLite · runs in the Playground
SELECT CONCAT('A', NULL, 'B') AS concat_result,
       'A' || NULL || 'B'      AS pipe_result;

▶ Run this query in the SQL Playground

CONCAT and || with a NULL argument
concat_resultpipe_result
ABNULL
✓ 1 row · real output from the Playground database

In SQLite, PostgreSQL and SQL Server, CONCAT() skips NULL arguments and returns AB, while || (and SQL Server's +) returns NULL as soon as one part is NULL. MySQL is the exception: its CONCAT() returns NULL if any argument is NULL. Wrapping a nullable column in COALESCE, for example COALESCE(middle_name, ''), gives the same answer in every database.

SQL UPPER() and LOWER(): Change the Case

UPPER() converts every letter to uppercase and LOWER() to lowercase. Digits, spaces and symbols pass through unchanged, so they are safe to apply to codes and email addresses.

SQLite · runs in the Playground
SELECT first_name,
       UPPER(last_name) AS last_name_upper,
       LOWER(department) AS department_lower
FROM employees
ORDER BY id
LIMIT 5;

▶ Run this query in the SQL Playground

UPPER and LOWER on employee columns
first_namelast_name_upperdepartment_lower
WilliamLEWIScustomer success
JohnMILLERlegal
DanielRIVERAengineering
MelissaSMITHengineering
JasonMITCHELLdesign
✓ 5 rows · real output from the Playground database

Case-insensitive comparisons

Whether = cares about case depends on the database. In SQLite and PostgreSQL it does; MySQL and SQL Server usually do not, because their default collations are case-insensitive. The Playground stores cities as "New York", so compare an exact match with a lowercased one:

SQLite · runs in the Playground
SELECT SUM(city = 'new york')        AS exact_match,
       SUM(LOWER(city) = 'new york') AS lower_match
FROM users;

▶ Run this query in the SQL Playground

Users matched with and without LOWER
exact_matchlower_match
07
✓ 1 row · real output from the Playground database

The exact comparison finds 0 users and the lowercased one finds 7. Lowering both sides is the portable way to match text whatever its case. (In SQLite, SUM() of a comparison counts the rows where it is true.)

Which one to use

UPPER()LOWER()
EffectAll letters to uppercaseAll letters to lowercase
Typical useCodes, status values, report headingsEmail addresses, usernames, matching keys
ExampleUPPER('delhi') = DELHILOWER('DELHI') = delhi
Digits and symbolsUnchangedUnchanged

Neither case is more correct. What matters is using the same convention everywhere a value is compared or grouped, so two rows that mean the same thing always look the same to the database.

SQL LENGTH(): Count Characters

LENGTH() returns the number of characters in a string, spaces included. SQL Server calls it LEN(), and LEN() ignores trailing spaces. Here are the longest product names in the Playground:

SQLite · runs in the Playground
SELECT DISTINCT name,
       LENGTH(name) AS name_length
FROM products
ORDER BY name_length DESC, name
LIMIT 5;

▶ Run this query in the SQL Playground

Longest product names
namename_length
Micro-Cut Paper Shredder 8-Sheet32
Bamboo Charcoal Air Freshener29
System Design Interview Vol.229
Leather Steering Wheel Cover28
Belgian Dark Chocolate 24pk27
✓ 5 rows · real output from the Playground database

LENGTH is also a quick data-quality check. Every SKU in the Playground should have the same format, so grouping by its length should give one group:

SQLite · runs in the Playground
SELECT LENGTH(sku) AS sku_length,
       COUNT(*) AS products
FROM products
GROUP BY LENGTH(sku);

▶ Run this query in the SQL Playground

Products grouped by SKU length
sku_lengthproducts
10120
✓ 1 row · real output from the Playground database

All 120 SKUs are 10 characters long. A second row here would point straight at malformed codes.

Spaces count

Untrimmed spaces change the length, which is a common reason why a value "looks right" but does not match. Compare the raw and trimmed lengths of the sample customers:

SQLite · runs in the Playground
WITH customers(id, name, city, email, phone) AS (
  VALUES (1, 'priya nair',    'Delhi',  ' Priya.Nair@EXAMPLE.com ', '98234-11567'),
         (2, 'PRIYA NAIR',    'DELHI',  'priya.nair@example.com',   '98234 11567'),
         (3, '  Priya Nair ', 'delhi',  'Priya.Nair@Example.com',   ' 98234-11567 '),
         (4, 'Neha Kapoor',   'Mumbai', 'neha.kapoor@example.org',  '99123-44521'),
         (5, 'vikram singh',  'Pune',   'Vikram.Singh@example.net', '97845-22110')
)
SELECT id,
       '[' || name || ']' AS name_with_edges,
       LENGTH(name)       AS raw_length,
       LENGTH(TRIM(name)) AS trimmed_length
FROM customers;

▶ Run this query in the SQL Playground

Raw and trimmed name lengths
idname_with_edgesraw_lengthtrimmed_length
1[priya nair]1010
2[PRIYA NAIR]1010
3[ Priya Nair ]1310
4[Neha Kapoor]1111
5[vikram singh]1212
✓ 5 rows · real output from the Playground database

The square brackets show where each value really starts and ends. Row 3 is 13 characters long but only 10 after trimming.

Characters or bytes? In MySQL, LENGTH() counts bytes, so "café" in UTF-8 has length 5; use CHAR_LENGTH() for characters. SQLite and PostgreSQL count characters with LENGTH().

SQL TRIM(), LTRIM() and RTRIM(): Remove Spaces

TRIM() removes spaces from both ends of a string and leaves the spaces in the middle alone. LTRIM() removes them only from the left (the start) and RTRIM() only from the right (the end):

SQLite · runs in the Playground
SELECT '[' || TRIM('  Priya Nair  ')  || ']' AS trim_result,
       '[' || LTRIM('  Priya Nair  ') || ']' AS ltrim_result,
       '[' || RTRIM('  Priya Nair  ') || ']' AS rtrim_result;

▶ Run this query in the SQL Playground

TRIM, LTRIM and RTRIM side by side
trim_resultltrim_resultrtrim_result
[Priya Nair][Priya Nair ][ Priya Nair]
✓ 1 row · real output from the Playground database

TRIM can remove other characters too. In SQLite the second argument lists the characters to strip, for example TRIM('--SQL--', '-') returns SQL. The standard form, used by PostgreSQL and MySQL, is TRIM(BOTH '-' FROM '--SQL--').

Problem caused by stray spacesWhy TRIM fixes it
Values pasted from spreadsheets or web formsThey often carry invisible leading or trailing spaces
Joins on a text column miss matches"Delhi " and "Delhi" are different values until trimmed
GROUP BY shows the same value twiceEach spacing variant forms its own group
Older SQL Server. TRIM() arrived in SQL Server 2017. On older versions write LTRIM(RTRIM(column)).

SQL SUBSTRING(): Take Part of a String

SUBSTRING(string, start, length) returns length characters starting at position start. Positions start at 1, not 0. If you leave out the length, you get everything to the end of the string. SQLite accepts both SUBSTR and SUBSTRING.

Every Playground SKU looks like SKU-000001: a three-letter prefix, a dash and six digits. SUBSTR splits it into its parts:

SQLite · runs in the Playground
SELECT sku,
       SUBSTR(sku, 1, 3) AS prefix,
       SUBSTR(sku, 5)    AS digits,
       CAST(SUBSTR(sku, 5) AS INTEGER) AS sku_number
FROM products
ORDER BY id
LIMIT 5;

▶ Run this query in the SQL Playground

A SKU split into prefix and number
skuprefixdigitssku_number
SKU-000001SKU0000011
SKU-000002SKU0000022
SKU-000003SKU0000033
SKU-000004SKU0000044
SKU-000005SKU0000055
✓ 5 rows · real output from the Playground database

SUBSTR(sku, 5) starts at the fifth character, right after the dash, and runs to the end. Casting the digits to an integer drops the leading zeros.

Start position from a search

When the piece you want does not sit at a fixed position, find it first. INSTR(email, '@') returns the position of the @, so everything before it is the username and everything after it is the domain:

SQLite · runs in the Playground
SELECT email,
       SUBSTR(email, 1, INSTR(email, '@') - 1) AS username,
       SUBSTR(email, INSTR(email, '@') + 1)    AS domain
FROM users
ORDER BY id
LIMIT 5;

▶ Run this query in the SQL Playground

Email addresses split at the @
emailusernamedomain
jennifer.torres57@icloud.comjennifer.torres57icloud.com
donna.adams52@gmail.comdonna.adams52gmail.com
kenneth.young94@proton.mekenneth.young94proton.me
michael.carter46@yahoo.commichael.carter46yahoo.com
robert.campbell92@icloud.comrobert.campbell92icloud.com
✓ 5 rows · real output from the Playground database

Asking for more characters than are left is not an error: SUBSTR('SQL', 2, 100) simply returns QL.

SQL LEFT() and RIGHT(): First and Last Characters

LEFT(string, n) returns the first n characters and RIGHT(string, n) the last n. MySQL, PostgreSQL and SQL Server have both. SQLite has neither, but a negative start position in SUBSTR counts from the end, so SUBSTR(x, 1, n) is LEFT and SUBSTR(x, -n) is RIGHT.

A typical use is masking: show only the last four digits of a phone number. Initials are the first character of each name:

SQLite · runs in the Playground
SELECT first_name, last_name,
       SUBSTR(first_name, 1, 1) || SUBSTR(last_name, 1, 1) AS initials,
       phone,
       '***-***-' || SUBSTR(phone, -4) AS masked_phone
FROM users
ORDER BY id
LIMIT 5;

▶ Run this query in the SQL Playground

Initials and masked phone numbers
first_namelast_nameinitialsphonemasked_phone
JenniferTorresJT+1-263-376-2371***-***-2371
DonnaAdamsDA+1-753-780-6670***-***-6670
KennethYoungKY+1-440-743-9503***-***-9503
MichaelCarterMC+1-967-275-1964***-***-1964
RobertCampbellRC+1-549-212-8814***-***-8814
✓ 5 rows · real output from the Playground database
SQL · MySQL / PostgreSQL / SQL Server
-- MySQL, PostgreSQL, SQL Server
SELECT LEFT(first_name, 1) AS first_initial,
       RIGHT(phone, 4)     AS last_four
FROM users;
When to prefer SUBSTRING. LEFT and RIGHT assume the piece you need has a fixed width. If the codes vary in length, find the delimiter with INSTR (or POSITION / CHARINDEX) and use SUBSTRING from there, as in the email example above.

SQL REPLACE(): Swap Text for Other Text

REPLACE(string, search, replacement) replaces every occurrence of search. Passing an empty string as the replacement deletes it. If the search value is not found, the string comes back unchanged. Stripping the dashes and the plus sign leaves only the digits of each phone number:

SQLite · runs in the Playground
SELECT phone,
       REPLACE(REPLACE(phone, '-', ''), '+', '') AS digits_only
FROM users
ORDER BY id
LIMIT 5;

▶ Run this query in the SQL Playground

Phone numbers with formatting removed
phonedigits_only
+1-263-376-237112633762371
+1-753-780-667017537806670
+1-440-743-950314407439503
+1-967-275-196419672751964
+1-549-212-881415492128814
✓ 5 rows · real output from the Playground database

REPLACE handles one search value per call, so replacing several things means nesting it. Here two nested calls plus LOWER turn the product categories into URL slugs: the inner REPLACE spells out "&", the outer one turns spaces into dashes.

SQLite · runs in the Playground
SELECT DISTINCT category,
       LOWER(REPLACE(REPLACE(category, ' & ', ' and '), ' ', '-')) AS url_slug
FROM products
ORDER BY category;

▶ Run this query in the SQL Playground

Product categories turned into URL slugs
categoryurl_slug
Automotiveautomotive
Booksbooks
Clothingclothing
Electronicselectronics
Food & Beveragefood-and-beverage
Health & Beautyhealth-and-beauty
Home & Gardenhome-and-garden
Office Suppliesoffice-supplies
Sports & Outdoorssports-and-outdoors
Toystoys
✓ 10 rows · real output from the Playground database
Patterns need REGEXP_REPLACE. REPLACE only matches exact text. To remove "anything that is not a digit", MySQL 8, PostgreSQL and Oracle offer REGEXP_REPLACE(). SQLite has no built-in regular expression replace.

Find a Character: INSTR, POSITION and CHARINDEX

Every database can tell you where a substring first appears, but each names the function differently. All of them return 0 when the text is not found, which makes them handy for validation:

DatabaseFunctionExample
SQLite, MySQL, OracleINSTR(string, search)INSTR(email, '@')
PostgreSQL, MySQLPOSITION(search IN string)POSITION('@' IN email)
PostgreSQLSTRPOS(string, search)STRPOS(email, '@')
SQL ServerCHARINDEX(search, string)CHARINDEX('@', email)
SQLite · runs in the Playground
SELECT INSTR(email, '@') AS at_position,
       COUNT(*) AS users
FROM users
GROUP BY INSTR(email, '@')
ORDER BY at_position
LIMIT 6;

▶ Run this query in the SQL Playground

Position of the @ in user emails
at_positionusers
121
139
1424
1524
1632
1726
✓ 6 rows · real output from the Playground database

No group has position 0, so every email in the Playground contains an @. A filter such as WHERE INSTR(email, '@') = 0 is the quick way to find addresses that do not.

REVERSE()

REVERSE('SQL') returns LQS. MySQL, PostgreSQL and SQL Server have it; SQLite does not. It is occasionally used to find the last occurrence of a character, by searching the reversed string.

SQL · MySQL / PostgreSQL / SQL Server
-- MySQL, PostgreSQL, SQL Server
SELECT name, REVERSE(name) AS reversed_name
FROM products;

String Functions in WHERE, CASE and GROUP BY

String functions can be used anywhere an expression is allowed. Grouping by an extracted piece of text is one of the most useful patterns. This counts users per email provider:

SQLite · runs in the Playground
SELECT SUBSTR(email, INSTR(email, '@') + 1) AS email_domain,
       COUNT(*) AS users
FROM users
GROUP BY email_domain
ORDER BY users DESC, email_domain;

▶ Run this query in the SQL Playground

Users per email domain
email_domainusers
hotmail.com31
yahoo.com29
gmail.com28
proton.me23
icloud.com22
outlook.com17
✓ 6 rows · real output from the Playground database

Inside CASE WHEN, a string function can sort rows into groups. Here product names are labelled by length:

SQLite · runs in the Playground
SELECT CASE WHEN LENGTH(name) <= 15 THEN 'Short'
            WHEN LENGTH(name) <= 25 THEN 'Medium'
            ELSE 'Long'
       END AS name_length_group,
       COUNT(*) AS products
FROM products
GROUP BY name_length_group
ORDER BY products DESC;

▶ Run this query in the SQL Playground

Products grouped by name length
name_length_groupproducts
Medium88
Long25
Short7
✓ 3 rows · real output from the Playground database

And in WHERE, a function filters on part of a value. This finds employees whose title contains "Manager", using LIKE with wildcards:

SQLite · runs in the Playground
SELECT title,
       COUNT(*) AS employees
FROM employees
WHERE title LIKE '%Manager%'
GROUP BY title
ORDER BY employees DESC, title;

▶ Run this query in the SQL Playground

Employees with Manager in their title
titleemployees
Senior CS Manager7
CS Manager3
Engineering Manager3
Finance Manager3
HR Manager3
Sales Manager3
Marketing Manager2
Operations Manager1
✓ 8 rows · real output from the Playground database

A Complete Data Cleaning Query

Real cleaning chains several functions. This query takes the five sample customers and, in one pass, trims and capitalises the name, lowercases the city and email, and reduces the phone number to digits:

SQLite · runs in the Playground
WITH customers(id, name, city, email, phone) AS (
  VALUES (1, 'priya nair',    'Delhi',  ' Priya.Nair@EXAMPLE.com ', '98234-11567'),
         (2, 'PRIYA NAIR',    'DELHI',  'priya.nair@example.com',   '98234 11567'),
         (3, '  Priya Nair ', 'delhi',  'Priya.Nair@Example.com',   ' 98234-11567 '),
         (4, 'Neha Kapoor',   'Mumbai', 'neha.kapoor@example.org',  '99123-44521'),
         (5, 'vikram singh',  'Pune',   'Vikram.Singh@example.net', '97845-22110')
)
SELECT id,
       UPPER(SUBSTR(TRIM(name), 1, 1)) || LOWER(SUBSTR(TRIM(name), 2)) AS clean_name,
       LOWER(TRIM(city))  AS clean_city,
       LOWER(TRIM(email)) AS clean_email,
       REPLACE(REPLACE(TRIM(phone), '-', ''), ' ', '') AS clean_phone
FROM customers;

▶ Run this query in the SQL Playground

Sample customers after cleaning
idclean_nameclean_cityclean_emailclean_phone
1Priya nairdelhipriya.nair@example.com9823411567
2Priya nairdelhipriya.nair@example.com9823411567
3Priya nairdelhipriya.nair@example.com9823411567
4Neha kapoormumbaineha.kapoor@example.org9912344521
5Vikram singhpunevikram.singh@example.net9784522110
✓ 5 rows · real output from the Playground database

Rows 1 to 3 now have identical values in every column, so they can be found with a GROUP BY and merged. Functions apply from the inside out: TRIM(name) runs first, then SUBSTR, then UPPER or LOWER.

Only the first letter is capitalised. SQLite and MySQL have no function for "title case", so "vikram singh" becomes "Vikram singh". PostgreSQL and Oracle have INITCAP(), which capitalises every word.

Find the duplicates

SQLite · runs in the Playground
WITH customers(id, name, city, email, phone) AS (
  VALUES (1, 'priya nair',    'Delhi',  ' Priya.Nair@EXAMPLE.com ', '98234-11567'),
         (2, 'PRIYA NAIR',    'DELHI',  'priya.nair@example.com',   '98234 11567'),
         (3, '  Priya Nair ', 'delhi',  'Priya.Nair@Example.com',   ' 98234-11567 '),
         (4, 'Neha Kapoor',   'Mumbai', 'neha.kapoor@example.org',  '99123-44521'),
         (5, 'vikram singh',  'Pune',   'Vikram.Singh@example.net', '97845-22110')
)
SELECT LOWER(TRIM(email)) AS clean_email,
       COUNT(*) AS rows_found
FROM customers
GROUP BY LOWER(TRIM(email))
HAVING COUNT(*) > 1;

▶ Run this query in the SQL Playground

Emails that appear more than once after cleaning
clean_emailrows_found
priya.nair@example.com3
✓ 1 row · real output from the Playground database

Without TRIM and LOWER, no two of those emails would be equal and the query would find no duplicates at all.

String Functions in MySQL, PostgreSQL, SQL Server and SQLite

OperationMySQLPostgreSQLSQL ServerSQLite
LengthLENGTH() (bytes), CHAR_LENGTH()LENGTH()LEN()LENGTH()
ConcatenateCONCAT()CONCAT(), ||CONCAT(), +CONCAT() (3.44+), ||
CONCAT with a NULLReturns NULLSkips the NULLSkips the NULLSkips the NULL
SubstringSUBSTRING(), SUBSTR()SUBSTRING(), SUBSTR()SUBSTRING()SUBSTR(), SUBSTRING()
First / last n charactersLEFT(), RIGHT()LEFT(), RIGHT()LEFT(), RIGHT()SUBSTR(x, 1, n), SUBSTR(x, -n)
Find positionINSTR(), POSITION()POSITION(), STRPOS()CHARINDEX()INSTR()
TrimTRIM(), LTRIM(), RTRIM()TRIM(), LTRIM(), RTRIM()TRIM() (2017+), LTRIM(), RTRIM()TRIM(), LTRIM(), RTRIM()
ReverseREVERSE()REVERSE()REVERSE()Not built in

The ideas transfer between databases; only some names change. Check the function name for your target database before you move a query from one to another.

String Functions and Performance

Wrapping a column in a function inside WHERE, as in WHERE UPPER(city) = 'DELHI', usually stops the database from using an ordinary index on city. The index stores the original values, not the uppercased ones, so every row has to be read and converted. On a small table that does not matter; on millions of rows it can.

FilterCan use a normal index?Why
WHERE city = 'Delhi'YesPlain comparison on the indexed column
WHERE UPPER(city) = 'DELHI'No, unless there is an index on UPPER(city)The function changes the value before comparing
WHERE city LIKE 'Del%'OftenA fixed prefix can be looked up in the index
WHERE city LIKE '%elhi'NoA leading wildcard means any starting value

There are two common fixes. One is an expression index on the function itself: PostgreSQL, Oracle and SQLite support CREATE INDEX ... ON users (LOWER(email)), MySQL from 8.0.13, and SQL Server through an indexed computed column. The other is to clean values once, when they are written, so queries can compare them directly. Use EXPLAIN to check whether a query uses an index before changing anything.

Common String Function Mistakes

Six errors that give wrong results, often without an error message.

1. Counting positions from 0

❌ Starts one character too early
SQL
SELECT SUBSTR(sku, 0, 3) AS prefix
FROM products;
✅ Positions start at 1
SQL
SELECT SUBSTR(sku, 1, 3) AS prefix
FROM products;

String positions in SQL start at 1. In SQLite, SUBSTR(sku, 0, 3) returns only "SK"; other databases behave differently, but none treat 0 as the first character.

2. Forgetting the separator in CONCAT

❌ Gives "WilliamLewis"
SQL
SELECT CONCAT(first_name, last_name) AS full_name
FROM employees;
✅ Adds the space
SQL
SELECT CONCAT(first_name, ' ', last_name) AS full_name
FROM employees;

CONCAT never inserts spaces or punctuation. Every character you want in the output has to be one of its arguments.

3. Letting one NULL blank out the whole string

❌ NULL if any part is NULL
SQL
SELECT first_name || ' ' || middle_name || ' ' || last_name
FROM people;
✅ Safe in every database
SQL
SELECT first_name || ' ' || COALESCE(middle_name || ' ', '') || last_name
FROM people;

With || (and with CONCAT in MySQL) a single NULL makes the whole result NULL. COALESCE replaces the missing part with an empty string.

4. Comparing text without handling case

❌ Finds 0 users in the Playground
SQL
SELECT *
FROM users
WHERE city = 'new york';
✅ Finds every New York user
SQL
SELECT *
FROM users
WHERE LOWER(city) = 'new york';

In SQLite and PostgreSQL, = is case-sensitive. Lowercasing the column makes the comparison work whatever case was stored.

5. Trusting LENGTH on untrimmed values

❌ Misses values with stray spaces
SQL
SELECT *
FROM customers
WHERE LENGTH(name) = 10;
✅ Measures the real content
SQL
SELECT *
FROM customers
WHERE LENGTH(TRIM(name)) = 10;

Leading and trailing spaces count as characters. Trim before you measure, as the "Spaces count" example above shows.

6. Assuming every database has the same function names

❌ LEN() fails outside SQL Server
SQL
SELECT LEN(name)
FROM products;
✅ LENGTH() in MySQL, PostgreSQL, SQLite
SQL
SELECT LENGTH(name)
FROM products;

LENGTH vs LEN, POSITION vs CHARINDEX and CONCAT vs + vs || all vary. Check the database comparison table before moving a query.

String Functions Cheat Sheet

TaskSQLite (Playground)Result for the input shown
Join with a spaceCONCAT('Data', ' ', 'Analyst')Data Analyst
UppercaseUPPER('delhi')DELHI
LowercaseLOWER('Priya.Nair@Example.com')priya.nair@example.com
LengthLENGTH('SQLab')5
Remove outer spacesTRIM(' Priya Nair ')Priya Nair
Part of a stringSUBSTR('SKU-000042', 5)000042
First n charactersSUBSTR('SKU-000042', 1, 3)SKU
Last n charactersSUBSTR('+1-263-376-2371', -4)2371
Replace textREPLACE('98234-11567', '-', '')9823411567
Find a characterINSTR('ana@example.com', '@')4

Practice String Functions Queries

6 exercises on the real Playground tables.

Write each query yourself first. Every solution has been run against the Playground database.

  1. 1Easy Show each employee's full name as "last_name, first_name", sorted alphabetically. Show the first 10. Table: employees

    Show solution
    SQLite · runs in the Playground
    SELECT last_name || ', ' || first_name AS employee
    FROM employees
    ORDER BY last_name, first_name
    LIMIT 10;

    ▶ Run this query in the SQL Playground

    || joins the pieces; the comma and space are a literal string between them.

  2. 2Easy List every department name in uppercase with the number of characters in it, longest first. Table: departments

    Show solution
    SQLite · runs in the Playground
    SELECT UPPER(name) AS department,
           LENGTH(name) AS characters
    FROM departments
    ORDER BY characters DESC, department;

    ▶ Run this query in the SQL Playground

    UPPER changes the display only; LENGTH counts the characters of the original name, including the space in "Customer Success".

  3. 3Medium Count the users per email domain, but only for domains that end in ".com". Table: users

    Show solution
    SQLite · runs in the Playground
    SELECT SUBSTR(email, INSTR(email, '@') + 1) AS email_domain,
           COUNT(*) AS users
    FROM users
    WHERE email LIKE '%.com'
    GROUP BY email_domain
    ORDER BY users DESC;

    ▶ Run this query in the SQL Playground

    INSTR finds the @, SUBSTR takes everything after it, and LIKE with a leading wildcard keeps addresses ending in .com.

  4. 4Medium Show the SKU number (without the "SKU-" prefix and leading zeros) and name of the five most expensive products. Table: products

    Show solution
    SQLite · runs in the Playground
    SELECT CAST(SUBSTR(sku, 5) AS INTEGER) AS sku_number,
           name,
           price
    FROM products
    ORDER BY price DESC
    LIMIT 5;

    ▶ Run this query in the SQL Playground

    SUBSTR(sku, 5) skips the four characters "SKU-"; CAST to INTEGER removes the leading zeros.

  5. 5Medium Build a login name for each employee: the first letter of the first name plus the last name, all lowercase. Show the first 8 by id. Table: employees

    Show solution
    SQLite · runs in the Playground
    SELECT first_name, last_name,
           LOWER(SUBSTR(first_name, 1, 1) || last_name) AS login
    FROM employees
    ORDER BY id
    LIMIT 8;

    ▶ Run this query in the SQL Playground

    SUBSTR(first_name, 1, 1) is the first initial; LOWER is applied to the joined result so both parts are lowercase.

  6. 6Hard Find the review comments that mention "quality" in any case, with how many reviews use each one. Table: reviews

    Show solution
    SQLite · runs in the Playground
    SELECT comment,
           COUNT(*) AS reviews
    FROM reviews
    WHERE LOWER(comment) LIKE '%quality%'
    GROUP BY comment
    ORDER BY reviews DESC, comment;

    ▶ Run this query in the SQL Playground

    Lowercasing the comment before the LIKE comparison makes the search case-insensitive in every database.

📝 Quiz — String Functions

Test your understanding · 0/25 answered

Question 1 of 25

What are SQL string functions mainly used for?

Question 2 of 25

What does CONCAT(first_name, ' ', last_name) do?

Question 3 of 25

What does UPPER('priya nair') return?

Question 4 of 25

What does LOWER('PRIYA.NAIR@EXAMPLE.COM') return?

Question 5 of 25

What does LENGTH('SQLab') return?

Question 6 of 25

What is the SQL Server equivalent of LENGTH()?

Question 7 of 25

What does TRIM(' Priya Nair ') return?

Question 8 of 25

What is the difference between TRIM and RTRIM?

Question 9 of 25

What does SUBSTRING('SKU-000042', 1, 3) return?

Question 10 of 25

In SUBSTRING(name, 1, 5), what does the 1 mean?

Question 11 of 25

What does REPLACE(phone, '-', '') do?

Question 12 of 25

What does LEFT('SKU-000042', 3) return in MySQL or SQL Server?

Question 13 of 25

What does RIGHT('SKU-000042', 2) return?

Question 14 of 25

Which function finds the position of a substring in SQL Server?

Question 15 of 25

What does REVERSE('SQL') return?

Question 16 of 25

Why write WHERE LOWER(city) = 'new york' instead of WHERE city = 'new york'?

Question 17 of 25

What is a downside of WHERE UPPER(city) = 'DELHI' on a very large table?

Question 18 of 25

Which combination is common for standardising a messy name column?

Question 19 of 25

What does SELECT COUNT(*) FROM customers WHERE LENGTH(name) > 10 answer?

Question 20 of 25

What does CASE WHEN SUBSTR(sku, 1, 3) = 'SKU' THEN 'Standard' ELSE 'Other' END do?

Question 21 of 25

What does CONCAT(first_name, middle_name) return when middle_name is NULL?

Question 22 of 25

What is a common off-by-one mistake with SUBSTRING?

Question 23 of 25

What can EXPLAIN tell you about WHERE UPPER(city) = 'DELHI'?

Question 24 of 25

Which operator does SQL Server use to join strings, besides CONCAT()?

Question 25 of 25

Why are string functions important for data analysts?

String Functions Interview Questions

Q1. What is the difference between TRIM, LTRIM and RTRIM?

TRIM removes leading and trailing spaces, LTRIM only leading ones and RTRIM only trailing ones. None of them touch spaces between words.

Q2. How would you extract the domain from an email address?

Find the @ and take everything after it: SUBSTR(email, INSTR(email, '@') + 1) in SQLite or MySQL, SUBSTRING(email FROM POSITION('@' IN email) + 1) in PostgreSQL, or SUBSTRING(email, CHARINDEX('@', email) + 1, LEN(email)) in SQL Server.

Q3. How do you find values that have leading or trailing spaces?

Compare the value with its trimmed version: WHERE name <> TRIM(name). Any row where they differ has extra spaces at one end.

Q4. How do you find duplicate customers whose names differ only in case and spacing?

GROUP BY TRIM(LOWER(name)) HAVING COUNT(*) > 1. Every group returned is a name that appears more than once once formatting is ignored.

Q5. How does CONCAT handle NULL in different databases?

MySQL's CONCAT returns NULL if any argument is NULL. PostgreSQL, SQL Server and SQLite skip NULL arguments. The || operator and SQL Server's + return NULL if either side is NULL. Use COALESCE to make the behaviour the same everywhere.

Q6. Why can a function in WHERE make a query slow, and how do you fix it?

A normal index stores the original column values, so a filter on UPPER(city) cannot use it and every row is read. Fix it with an expression index on UPPER(city) (or a computed column in SQL Server), or store the value already cleaned.

Q7. What is the difference between LENGTH and CHAR_LENGTH in MySQL?

LENGTH counts bytes and CHAR_LENGTH counts characters. They differ for text with non-ASCII characters, such as "café" in UTF-8, which has 4 characters but 5 bytes.

Q8. When would you use SUBSTRING instead of LEFT or RIGHT?

When the piece you need is not at a fixed position. LEFT and RIGHT assume a fixed width; SUBSTRING combined with INSTR, POSITION or CHARINDEX can start at a delimiter that moves from row to row.

Q9. Why might two values still not match after TRIM and LOWER?

They may contain characters that TRIM does not remove, such as non-breaking spaces, tabs or zero-width characters, or the same letter in a different Unicode form. A targeted REPLACE or a regular expression is needed for those.

Frequently Asked Questions

What are string functions in SQL?

String functions are built-in functions that work on text. They join values (CONCAT), change case (UPPER, LOWER), measure length (LENGTH), remove spaces (TRIM), take part of a string (SUBSTRING, LEFT, RIGHT) and replace text (REPLACE). They are the main tool for cleaning text data.

What is CONCAT in SQL?

CONCAT() joins two or more values into one string, for example CONCAT(first_name, ' ', last_name) builds a full name. It does not add separators, so the space has to be passed as an argument.

What is the difference between UPPER and LOWER?

UPPER() converts every letter to uppercase and LOWER() converts every letter to lowercase. Both are used to make text consistent and to compare values regardless of case.

What does TRIM do?

TRIM() removes spaces from the start and end of a string without touching spaces in the middle. LTRIM removes only leading spaces and RTRIM only trailing ones.

How does SUBSTRING work?

SUBSTRING(string, start, length) returns length characters starting at position start, counting from 1. For example SUBSTRING('SKU-000042', 5, 6) returns 000042. Without the length it returns the rest of the string.

What is the difference between LENGTH and LEN?

They both count characters. LENGTH() is used in MySQL, PostgreSQL, Oracle and SQLite; LEN() is the SQL Server name. LEN() ignores trailing spaces, and MySQL's LENGTH() counts bytes rather than characters.

How are string functions used in data analysis?

Mostly for cleaning: standardising names and cities, normalising email addresses, stripping formatting from phone numbers and splitting codes into their parts, so that values can be grouped, matched and reported correctly.

Are string functions asked about in SQL interviews?

Yes. Typical questions ask you to build a full name, extract an email domain, find duplicates that differ only in case or spacing, or explain how CONCAT treats NULL.

Can string functions be used in WHERE and CASE WHEN?

Yes. They can be used anywhere an expression is allowed: in WHERE for filters such as LOWER(city) = 'new york', in CASE WHEN to label rows, and in GROUP BY to group by part of a value.

Do string functions differ between MySQL, SQL Server and PostgreSQL?

Some names do. SQL Server uses LEN, CHARINDEX and the + operator; PostgreSQL and Oracle use the || operator; MySQL and SQLite have INSTR. SQLite has no LEFT, RIGHT or REVERSE, so use SUBSTR instead.

What is the most common use of string functions?

Data cleaning: trimming stray spaces, fixing capitalisation, normalising emails and phone numbers, and extracting structured pieces from codes such as product SKUs.