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.
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:
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
| clean_name | rows_in_table |
|---|---|
| neha kapoor | 1 |
| priya nair | 3 |
| vikram singh | 1 |
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 problem | String function |
|---|---|
| Combine first and last name into one field | CONCAT() or || |
| Make inconsistent casing consistent | UPPER() / LOWER() |
| Remove accidental leading and trailing spaces | TRIM(), LTRIM(), RTRIM() |
| Take part of a code, such as a prefix or the digits | SUBSTRING() / SUBSTR(), LEFT(), RIGHT() |
| Strip formatting characters from a phone number | REPLACE() |
| Find where a character sits, such as the @ in an email | INSTR(), POSITION(), CHARINDEX() |
| Check how many characters a value has | LENGTH() / 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.
| Function | What it does | Example | In SQLite |
|---|---|---|---|
| CONCAT() | Joins values into one string | CONCAT(first_name, ' ', last_name) | CONCAT() or || |
| UPPER() | Converts letters to uppercase | UPPER(city) | Same |
| LOWER() | Converts letters to lowercase | LOWER(email) | Same |
| LENGTH() | Counts characters | LENGTH(name) | Same |
| TRIM() | Removes leading and trailing spaces | TRIM(name) | Same, plus LTRIM / RTRIM |
| SUBSTRING() | Takes part of a string by position and length | SUBSTRING(sku, 5, 6) | SUBSTR() or SUBSTRING() |
| REPLACE() | Replaces every occurrence of a value | REPLACE(phone, '-', '') | Same |
| LEFT() | First n characters | LEFT(sku, 3) | SUBSTR(sku, 1, 3) |
| RIGHT() | Last n characters | RIGHT(phone, 4) | SUBSTR(phone, -4) |
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:
SELECT CONCAT(first_name, ' ', last_name) AS full_name,
title
FROM employees
ORDER BY id
LIMIT 5;
▶ Run this query in the SQL Playground
| full_name | title |
|---|---|
| William Lewis | Senior CS Manager |
| John Miller | Paralegal |
| Daniel Rivera | VP Engineering |
| Melissa Smith | VP Engineering |
| Jason Mitchell | Product Designer |
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:
SELECT CONCAT(name, ' (', category, ') - $', price) AS product_label
FROM products
ORDER BY id
LIMIT 5;
▶ Run this query in the SQL Playground
| 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 |
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:
SELECT CONCAT('A', NULL, 'B') AS concat_result,
'A' || NULL || 'B' AS pipe_result;
▶ Run this query in the SQL Playground
| concat_result | pipe_result |
|---|---|
| AB | NULL |
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.
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
| first_name | last_name_upper | department_lower |
|---|---|---|
| William | LEWIS | customer success |
| John | MILLER | legal |
| Daniel | RIVERA | engineering |
| Melissa | SMITH | engineering |
| Jason | MITCHELL | design |
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:
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
| exact_match | lower_match |
|---|---|
| 0 | 7 |
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() | |
|---|---|---|
| Effect | All letters to uppercase | All letters to lowercase |
| Typical use | Codes, status values, report headings | Email addresses, usernames, matching keys |
| Example | UPPER('delhi') = DELHI | LOWER('DELHI') = delhi |
| Digits and symbols | Unchanged | Unchanged |
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:
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
| name | name_length |
|---|---|
| Micro-Cut Paper Shredder 8-Sheet | 32 |
| Bamboo Charcoal Air Freshener | 29 |
| System Design Interview Vol.2 | 29 |
| Leather Steering Wheel Cover | 28 |
| Belgian Dark Chocolate 24pk | 27 |
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:
SELECT LENGTH(sku) AS sku_length,
COUNT(*) AS products
FROM products
GROUP BY LENGTH(sku);
▶ Run this query in the SQL Playground
| sku_length | products |
|---|---|
| 10 | 120 |
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:
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
| id | name_with_edges | raw_length | trimmed_length |
|---|---|---|---|
| 1 | [priya nair] | 10 | 10 |
| 2 | [PRIYA NAIR] | 10 | 10 |
| 3 | [ Priya Nair ] | 13 | 10 |
| 4 | [Neha Kapoor] | 11 | 11 |
| 5 | [vikram singh] | 12 | 12 |
The square brackets show where each value really starts and ends. Row 3 is 13 characters long but only 10 after trimming.
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):
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_result | ltrim_result | rtrim_result |
|---|---|---|
| [Priya Nair] | [Priya Nair ] | [ Priya Nair] |
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 spaces | Why TRIM fixes it |
|---|---|
| Values pasted from spreadsheets or web forms | They 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 twice | Each spacing variant forms its own group |
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:
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
| sku | prefix | digits | sku_number |
|---|---|---|---|
| SKU-000001 | SKU | 000001 | 1 |
| SKU-000002 | SKU | 000002 | 2 |
| SKU-000003 | SKU | 000003 | 3 |
| SKU-000004 | SKU | 000004 | 4 |
| SKU-000005 | SKU | 000005 | 5 |
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:
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
| username | domain | |
|---|---|---|
| jennifer.torres57@icloud.com | jennifer.torres57 | icloud.com |
| donna.adams52@gmail.com | donna.adams52 | gmail.com |
| kenneth.young94@proton.me | kenneth.young94 | proton.me |
| michael.carter46@yahoo.com | michael.carter46 | yahoo.com |
| robert.campbell92@icloud.com | robert.campbell92 | icloud.com |
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:
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
| first_name | last_name | initials | phone | masked_phone |
|---|---|---|---|---|
| Jennifer | Torres | JT | +1-263-376-2371 | ***-***-2371 |
| Donna | Adams | DA | +1-753-780-6670 | ***-***-6670 |
| Kenneth | Young | KY | +1-440-743-9503 | ***-***-9503 |
| Michael | Carter | MC | +1-967-275-1964 | ***-***-1964 |
| Robert | Campbell | RC | +1-549-212-8814 | ***-***-8814 |
-- MySQL, PostgreSQL, SQL Server
SELECT LEFT(first_name, 1) AS first_initial,
RIGHT(phone, 4) AS last_four
FROM users;
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:
SELECT phone,
REPLACE(REPLACE(phone, '-', ''), '+', '') AS digits_only
FROM users
ORDER BY id
LIMIT 5;
▶ Run this query in the SQL Playground
| phone | digits_only |
|---|---|
| +1-263-376-2371 | 12633762371 |
| +1-753-780-6670 | 17537806670 |
| +1-440-743-9503 | 14407439503 |
| +1-967-275-1964 | 19672751964 |
| +1-549-212-8814 | 15492128814 |
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.
SELECT DISTINCT category,
LOWER(REPLACE(REPLACE(category, ' & ', ' and '), ' ', '-')) AS url_slug
FROM products
ORDER BY category;
▶ Run this query in the SQL Playground
| category | url_slug |
|---|---|
| Automotive | automotive |
| Books | books |
| Clothing | clothing |
| Electronics | electronics |
| Food & Beverage | food-and-beverage |
| Health & Beauty | health-and-beauty |
| Home & Garden | home-and-garden |
| Office Supplies | office-supplies |
| Sports & Outdoors | sports-and-outdoors |
| Toys | toys |
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:
| Database | Function | Example |
|---|---|---|
| SQLite, MySQL, Oracle | INSTR(string, search) | INSTR(email, '@') |
| PostgreSQL, MySQL | POSITION(search IN string) | POSITION('@' IN email) |
| PostgreSQL | STRPOS(string, search) | STRPOS(email, '@') |
| SQL Server | CHARINDEX(search, string) | CHARINDEX('@', email) |
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
| at_position | users |
|---|---|
| 12 | 1 |
| 13 | 9 |
| 14 | 24 |
| 15 | 24 |
| 16 | 32 |
| 17 | 26 |
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.
-- 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:
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
| email_domain | users |
|---|---|
| hotmail.com | 31 |
| yahoo.com | 29 |
| gmail.com | 28 |
| proton.me | 23 |
| icloud.com | 22 |
| outlook.com | 17 |
Inside CASE WHEN, a string function can sort rows into groups. Here product names are labelled by length:
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
| name_length_group | products |
|---|---|
| Medium | 88 |
| Long | 25 |
| Short | 7 |
And in WHERE, a function filters on part of a value. This finds employees whose title contains "Manager", using LIKE with wildcards:
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
| title | employees |
|---|---|
| Senior CS Manager | 7 |
| CS Manager | 3 |
| Engineering Manager | 3 |
| Finance Manager | 3 |
| HR Manager | 3 |
| Sales Manager | 3 |
| Marketing Manager | 2 |
| Operations Manager | 1 |
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:
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
| id | clean_name | clean_city | clean_email | clean_phone |
|---|---|---|---|---|
| 1 | Priya nair | delhi | priya.nair@example.com | 9823411567 |
| 2 | Priya nair | delhi | priya.nair@example.com | 9823411567 |
| 3 | Priya nair | delhi | priya.nair@example.com | 9823411567 |
| 4 | Neha kapoor | mumbai | neha.kapoor@example.org | 9912344521 |
| 5 | Vikram singh | pune | vikram.singh@example.net | 9784522110 |
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.
INITCAP(), which capitalises every word.Find the duplicates
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
| clean_email | rows_found |
|---|---|
| priya.nair@example.com | 3 |
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
| Operation | MySQL | PostgreSQL | SQL Server | SQLite |
|---|---|---|---|---|
| Length | LENGTH() (bytes), CHAR_LENGTH() | LENGTH() | LEN() | LENGTH() |
| Concatenate | CONCAT() | CONCAT(), || | CONCAT(), + | CONCAT() (3.44+), || |
| CONCAT with a NULL | Returns NULL | Skips the NULL | Skips the NULL | Skips the NULL |
| Substring | SUBSTRING(), SUBSTR() | SUBSTRING(), SUBSTR() | SUBSTRING() | SUBSTR(), SUBSTRING() |
| First / last n characters | LEFT(), RIGHT() | LEFT(), RIGHT() | LEFT(), RIGHT() | SUBSTR(x, 1, n), SUBSTR(x, -n) |
| Find position | INSTR(), POSITION() | POSITION(), STRPOS() | CHARINDEX() | INSTR() |
| Trim | TRIM(), LTRIM(), RTRIM() | TRIM(), LTRIM(), RTRIM() | TRIM() (2017+), LTRIM(), RTRIM() | TRIM(), LTRIM(), RTRIM() |
| Reverse | REVERSE() | 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.
| Filter | Can use a normal index? | Why |
|---|---|---|
WHERE city = 'Delhi' | Yes | Plain 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%' | Often | A fixed prefix can be looked up in the index |
WHERE city LIKE '%elhi' | No | A 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
SELECT SUBSTR(sku, 0, 3) AS prefix
FROM products;
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
SELECT CONCAT(first_name, last_name) AS full_name
FROM employees;
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
SELECT first_name || ' ' || middle_name || ' ' || last_name
FROM people;
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
SELECT *
FROM users
WHERE city = 'new york';
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
SELECT *
FROM customers
WHERE LENGTH(name) = 10;
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
SELECT LEN(name)
FROM products;
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
| Task | SQLite (Playground) | Result for the input shown |
|---|---|---|
| Join with a space | CONCAT('Data', ' ', 'Analyst') | Data Analyst |
| Uppercase | UPPER('delhi') | DELHI |
| Lowercase | LOWER('Priya.Nair@Example.com') | priya.nair@example.com |
| Length | LENGTH('SQLab') | 5 |
| Remove outer spaces | TRIM(' Priya Nair ') | Priya Nair |
| Part of a string | SUBSTR('SKU-000042', 5) | 000042 |
| First n characters | SUBSTR('SKU-000042', 1, 3) | SKU |
| Last n characters | SUBSTR('+1-263-376-2371', -4) | 2371 |
| Replace text | REPLACE('98234-11567', '-', '') | 9823411567 |
| Find a character | INSTR('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.
-
1Easy Show each employee's full name as "last_name, first_name", sorted alphabetically. Show the first 10. Table:
employeesShow solution
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.
-
2Easy List every department name in uppercase with the number of characters in it, longest first. Table:
departmentsShow solution
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".
-
3Medium Count the users per email domain, but only for domains that end in ".com". Table:
usersShow solution
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.
-
4Medium Show the SKU number (without the "SKU-" prefix and leading zeros) and name of the five most expensive products. Table:
productsShow solution
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.
-
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:
employeesShow solution
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.
-
6Hard Find the review comments that mention "quality" in any case, with how many reviews use each one. Table:
reviewsShow solution
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
What are SQL string functions mainly used for?
String functions work on text: combining, reshaping, cleaning and extracting parts of it.
What does CONCAT(first_name, ' ', last_name) do?
CONCAT joins its arguments end to end. The space is a literal argument of its own.
What does UPPER('priya nair') return?
UPPER converts every letter to uppercase.
What does LOWER('PRIYA.NAIR@EXAMPLE.COM') return?
LOWER converts every letter to lowercase, which is how emails are usually standardised before comparing them.
What does LENGTH('SQLab') return?
S-Q-L-a-b is five characters.
What is the SQL Server equivalent of LENGTH()?
SQL Server uses LEN(). MySQL, PostgreSQL, Oracle and SQLite use LENGTH().
What does TRIM(' Priya Nair ') return?
TRIM removes leading and trailing spaces. The space between the two words stays.
What is the difference between TRIM and RTRIM?
RTRIM trims the right side only and LTRIM the left side only; TRIM does both.
What does SUBSTRING('SKU-000042', 1, 3) return?
It takes 3 characters starting at position 1.
In SUBSTRING(name, 1, 5), what does the 1 mean?
The second argument is the start position, counted from 1. The third is how many characters to take.
What does REPLACE(phone, '-', '') do?
REPLACE swaps every occurrence of the search value for the replacement, here an empty string.
What does LEFT('SKU-000042', 3) return in MySQL or SQL Server?
LEFT returns the first n characters. In SQLite write SUBSTR(x, 1, 3).
What does RIGHT('SKU-000042', 2) return?
RIGHT returns the last n characters. In SQLite write SUBSTR(x, -2).
Which function finds the position of a substring in SQL Server?
SQL Server uses CHARINDEX(search, string). PostgreSQL uses POSITION or STRPOS; MySQL, Oracle and SQLite have INSTR.
What does REVERSE('SQL') return?
REVERSE flips the order of the characters. SQLite does not have it; MySQL, PostgreSQL and SQL Server do.
Why write WHERE LOWER(city) = 'new york' instead of WHERE city = 'new york'?
Lowercasing the column makes the comparison case-insensitive. In the Playground, city = 'new york' finds no users at all.
What is a downside of WHERE UPPER(city) = 'DELHI' on a very large table?
A function on the filtered column hides it from a plain index unless there is an expression index on UPPER(city).
Which combination is common for standardising a messy name column?
TRIM(LOWER(name)) removes stray spaces and makes the case consistent in one expression.
What does SELECT COUNT(*) FROM customers WHERE LENGTH(name) > 10 answer?
WHERE keeps the rows with a long name and COUNT(*) counts them.
What does CASE WHEN SUBSTR(sku, 1, 3) = 'SKU' THEN 'Standard' ELSE 'Other' END do?
CASE WHEN with a string function classifies rows by a pattern in their text.
What does CONCAT(first_name, middle_name) return when middle_name is NULL?
MySQL's CONCAT returns NULL if any argument is NULL. PostgreSQL, SQL Server and SQLite skip NULL arguments. The || operator returns NULL everywhere.
What is a common off-by-one mistake with SUBSTRING?
The first character is at position 1 in SQL.
What can EXPLAIN tell you about WHERE UPPER(city) = 'DELHI'?
EXPLAIN shows the query plan, including whether an index is used or every row is scanned.
Which operator does SQL Server use to join strings, besides CONCAT()?
SQL Server uses +. PostgreSQL, Oracle and SQLite use ||.
Why are string functions important for data analysts?
Text entered by people or exported from other systems is rarely consistent, and string functions are how it is standardised in SQL.
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.