SQL String Functions
Measure, reshape, clean, and combine text values.
Text literals use single quotes: 'active'.
Double quotes mean an identifier (a column or table name), which is a common
beginner mix-up.
Common functions
| Function | Returns |
|---|---|
length(s) | Number of characters. |
lower(s) / upper(s) | Change case. |
trim(s) | Remove leading/trailing spaces. |
substring(s, 1, 3) | Slice from position, length. |
replace(s, 'a', 'b') | Replace all occurrences. |
split_part(s, '@', 2) | Nth piece after splitting on a delimiter. |
concat(a, b) or a || b | Join strings together. |
Cleaning and combining
select first_name || ' ' || last_name as full_name,
lower(trim(email)) as email_clean,
split_part(email, '@', 2) as email_domain
from customers Case-insensitive matching
-- normalize both sides
where lower(state) = 'pa'
-- or use ILIKE (DuckDB / Postgres)
where state ilike 'pa' Text types and lengths
In DuckDB, TEXT, STRING, CHAR(n), and
VARCHAR(n) are all the same type, VARCHAR. The
length is accepted and ignored: a varchar(5) column stores
eight characters without complaint, and a char(5) column
doesn't pad 'ab' with spaces.
create or replace table text_types (
a text,
b char(5),
c varchar(5),
d string
);
insert into text_types values ('x', 'ab', 'abcdefgh', 'y');
select length(b) as b_length,
c,
typeof(c) as c_type
from text_types; length counts characters, and strlen counts the
bytes they take in UTF-8. They differ for any character outside plain ASCII.
An emoji with a skin tone is two characters to length and one
to length_grapheme, which counts what a reader sees as one
symbol:
select length('héllo') as chars,
strlen('héllo') as bytes,
length('👍🏽') as thumbs_chars,
length_grapheme('👍🏽') as thumbs_symbols,
strlen('👍🏽') as thumbs_bytes;
That returns 5, 6, 2, 1, and 8. A limit like "a code is at most 10
characters" means length; a limit on storage or a fixed-width
file means bytes.
Comparing and sorting text
= compares text exactly. 'Tools ' with a trailing
space doesn't equal 'Tools', and 'tools' doesn't
either. Clean both sides with trim and lower, or
compare with collate nocase:
select 'Tools ' = 'Tools' as trailing_space,
'tools' = 'Tools' as different_case,
trim('Tools ') = 'Tools' as trimmed,
'tools' = 'Tools' collate nocase as nocase; ORDER BY on text sorts by character code, so every uppercase
letter comes before every lowercase one, and accented letters come after
z. collate nocase.noaccent sorts the way a reader
expects:
select name
from (values ('zebra'), ('Éclair'), ('apple'), ('Banana')) as t(name)
order by name;
select name
from (values ('zebra'), ('Éclair'), ('apple'), ('Banana')) as t(name)
order by name collate nocase.noaccent; The first returns Banana, apple, zebra, Éclair. The second returns apple, Banana, Éclair, zebra.
Close matches and patterns
levenshtein(a, b) counts the single-letter edits that turn one
string into the other, and jaro_winkler_similarity(a, b) scores
them from 0 to 1. Both compare characters exactly, so lowercase both sides
first:
select first_name,
last_name,
levenshtein(lower(last_name), 'castelano') as edits
from customers
where levenshtein(lower(last_name), 'castelano') <= 2 Both Castellanos come back, 1 edit away. A close match is a candidate, not proof: Ford and Foss are only 2 edits apart. For text with a shape rather than a spelling, such as "digits, then letters", see SQL regular expressions.
How other databases differ
| Engine | Too long for VARCHAR(n) | Counting | = on text |
|---|---|---|---|
| DuckDB | Stored anyway | length characters, strlen bytes | Case and spaces count |
| PostgreSQL |
Error: value too long for type character varying(5) | length characters, octet_length bytes
|
Case counts. CHAR(n) pads, and ignores the padding
|
| MySQL | Error in strict mode: Data too long for column | LENGTH is bytes;
CHAR_LENGTH characters
| Case-insensitive by default |
| SQL Server | Error: String or binary data would be truncated | LEN characters, without trailing spaces;
DATALENGTH bytes
| Case-insensitive by default; trailing spaces ignored |
| SQLite | Stored anyway | length characters | Case and spaces count |
So the same query can match a row in SQL Server or MySQL and miss it in DuckDB. When text comes from another system, trim it and settle its case once, as it loads.
Concatenation differs by engine: || works in DuckDB, Postgres,
SQLite, and Oracle. SQL Server uses + (or CONCAT),
and only accepts || from SQL Server 2025 and in Azure SQL.
MySQL needs CONCAT unless PIPES_AS_CONCAT is
enabled. CONCAT(...) is the most portable choice.
Related
See why text, numbers, dates, and booleans behave differently.
Everyday date and text functions.
Practice safe date filters, buckets, and lead-time reports.
Use a half-open range, not BETWEEN, on timestamps.
Compare, sort, and calculate with numeric values.
Debug filters that match nothing.