Readineer
PATTERN 12SQL Patterns / Working with Real-World Data

SQL string functions: cleaning, combining, and matching text correctly

Learn how to clean and transform text, combine values, extract parts of strings, remove unwanted spaces, replace text, and match patterns without quietly missing or misclassifying rows.

9 min read12 sections2 predictions
01Foundation

Text is rarely as clean as it looks

Real-world databases contain a large amount of text. Customer names, email addresses, product names, cities, status codes, reference numbers, and descriptions are all commonly stored as character data. Consider the following customers table:

Customers
customer_idcustomer_nameemailcityreferral_code
1Asha RaoASHA@EXAMPLE.COMBengaluruREF-IND-101
2Ben Thomasben@example.comLondonREF-UK-205
3Carla Shahcarla@example.comDubaiREF-UAE-310
4Dev Mehtadev@example.comBengaluruREF-IND-415
5Esha Kapooresha@example.comSingaporeREF-SG-520

Text values may need to be converted to a consistent case, combined into a new value, split into smaller parts, cleaned of unwanted spaces, modified, or searched using patterns. SQL provides string functions and operators for these tasks.

Exact function names and syntax vary somewhat between database systems. The examples below use PostgreSQL-style SQL where syntax differs.

02Normalizing

Changing text case with UPPER and LOWER

Text may arrive in different forms. Our email column contains values such as ASHA@EXAMPLE.COM, ben@example.com, and carla@example.com. The values represent email addresses, but their capitalization is inconsistent. We can convert text to lowercase:

Query
SELECT
    customer_id,
    email,
    LOWER(email) AS normalized_email
FROM customers
ORDER BY customer_id;
Result
customer_idemailnormalized_email
1ASHA@EXAMPLE.COMasha@example.com
2ben@example.comben@example.com
3carla@example.comcarla@example.com
4dev@example.comdev@example.com
5esha@example.comesha@example.com

LOWER changes alphabetic characters to lowercase. Similarly, UPPER(city) converts text to uppercase, so Bengaluru, London, and Dubai become BENGALURU, LONDON, and DUBAI. These functions are useful when text needs a consistent presentation or normalization step.

03Cleaning

Removing unwanted spaces with TRIM

Text imported from files, APIs, forms, and older systems often contains extra spaces. Visually, ' esha@example.com ' may look almost identical to 'esha@example.com', but they are different strings. Use TRIM to remove spaces from the beginning and end:

Query
SELECT
    customer_id,
    email,
    TRIM(email) AS cleaned_email
FROM customers
ORDER BY customer_id;
Before:  ' esha@example.com '
After:   'esha@example.com'

Combining cleaning steps

Often text needs more than one transformation. Suppose we want every email without leading or trailing spaces and in lowercase. We can combine functions:

Query
SELECT
    customer_id,
    email,
    LOWER(TRIM(email)) AS normalized_email
FROM customers
ORDER BY customer_id;

For Asha, ASHA@EXAMPLE.COM becomes asha@example.com. For Esha, ' esha@example.com ' becomes esha@example.com. SQL evaluates the inner function first, TRIM(email), and then applies LOWER(...) to that result.

LTRIM and RTRIM

Sometimes you want to remove spaces from only one side. Common functions include LTRIM(text) and RTRIM(text):

LTRIM    removes leading spaces
RTRIM    removes trailing spaces
TRIM     removes both

For most data-cleaning situations involving accidental surrounding whitespace, TRIM is the natural starting point.

04The featured failure

The equality check that misses a value that looks identical

Suppose the requirement is find the customer with email esha@example.com. Someone writes WHERE email = 'esha@example.com'. The query may return no Esha row. Why? Her stored value is ' esha@example.com ', while the filter contains 'esha@example.com'. These strings are not identical.

PAUSE & PREDICTWhich filter correctly handles both the spaces and inconsistent capitalization?
Query
-- Option A
WHERE email = 'esha@example.com'

-- Option B
WHERE LOWER(email) = 'esha@example.com'

-- Option C
WHERE LOWER(TRIM(email)) = 'esha@example.com'

-- Option D
WHERE email LIKE '%'
Your prediction

Cleaning at query time is not always the final solution

Expressions such as LOWER(TRIM(email)) are useful when working with imperfect data. But if every query must repeatedly clean the same column, the underlying data model or ingestion process may need attention. Email normalization, for example, may be better enforced when the value enters the system. Query-time cleaning is useful, but it should not automatically become a substitute for reliable data quality.

05Combining values

Combining text with concatenation

Sometimes we need to create a new text value from several columns. Suppose customer names were stored separately:

Customers
customer_idfirst_namelast_name
1AshaRao
2BenThomas
3CarlaShah

We want Asha Rao, Ben Thomas, and Carla Shah. In PostgreSQL, the || operator can concatenate strings:

Query
SELECT
    customer_id,
    first_name || ' ' || last_name AS full_name
FROM customers;
Result
customer_idfull_name
1Asha Rao
2Ben Thomas
3Carla Shah

The expression has three pieces: first_name, a space, and last_name. So Asha, ' ', and Rao becomes Asha Rao. Many database systems also provide a CONCAT function, although exact behaviour and syntax can differ.

Concatenation can create business-friendly values

Suppose we want a label containing both the customer and city:

Query
SELECT
    customer_name || ' - ' || city AS customer_location
FROM customers;
Result
customer_location
Asha Rao - Bengaluru
Ben Thomas - London
Carla Shah - Dubai

The combined value does not need to exist as a stored column. It is created for the query result.

Gotcha: NULL can affect concatenation

Suppose a customer’s last name is NULL. With concatenation operators in many systems, an expression involving NULL can itself become NULL. For example, first_name || ' ' || last_name may produce NULL when last_name is NULL. If the business requirement says a missing last name should simply be omitted, handle that explicitly:

Query
first_name || COALESCE(' ' || last_name, '')
Key idea
The important principle is the same one covered in the NULL article: decide what a missing text value should mean before replacing it.
06Extracting and replacing

Extracting part of a string with SUBSTRING

Sometimes one text value contains several useful pieces of information. Look at the referral codes REF-IND-101, REF-UK-205, REF-UAE-310. Suppose the first three characters identify the code type, REF. We can extract part of the value using SUBSTRING:

Query
SELECT
    referral_code,
    SUBSTRING(referral_code FROM 1 FOR 3) AS code_type
FROM customers;
Result
referral_codecode_type
REF-IND-101REF
REF-UK-205REF
REF-UAE-310REF
REF-IND-415REF
REF-SG-520REF

The expression SUBSTRING(referral_code FROM 1 FOR 3) means start at character 1 and return 3 characters.

Extracting a known segment

Suppose our codes always follow REF-XXX-999 and we want part of the country segment. For REF-IND-101 we might extract characters beginning after REF-. But notice a problem: IND, UK, UAE, and SG do not all have the same length. Fixed-position substring logic works best when the text format has fixed positions. If the structure varies, delimiter-aware functions or pattern-based extraction may be more appropriate depending on the database.

Use fixed-position substring logic when the position and length are truly part of the data format. Do not assume variable text has fixed positions merely because a few examples happen to fit.

Measuring text with LENGTH

A basic string function that is useful for validation is LENGTH:

Query
SELECT
    customer_name,
    LENGTH(customer_name) AS name_length
FROM customers;

This can help answer questions such as which codes have unexpected lengths, which values are empty, which descriptions exceed an expected size, or whether a source system sent malformed identifiers. For example, WHERE LENGTH(TRIM(referral_code)) <> 11 could be useful if the business specification requires every code to contain exactly eleven characters. The rule must come from the data format, not from the function itself.

Replacing text with REPLACE

REPLACE substitutes one piece of text with another. Suppose a report needs REF/IND/101 instead of REF-IND-101:

Query
SELECT
    referral_code,
    REPLACE(referral_code, '-', '/') AS formatted_code
FROM customers;

REPLACE is useful for transformations such as changing delimiters, removing known characters, standardizing simple formats, and replacing known tokens. Replacing text with an empty string effectively removes it: REPLACE(referral_code, '-', '') turns REF-IND-101 into REFIND101. This can be useful when punctuation is only presentation formatting, but if punctuation carries meaning, removing it changes the data rather than merely cleaning it.

Gotcha — REPLACE is literal
REPLACE is literal, not intelligent. REPLACE(product_name, 'Pro', '') does not understand what a suffix is; it simply replaces matching text wherever it is found. String replacement should be based on a clear format or business rule. For more complex pattern-based transformations, databases often provide regular expression functions, but those are beyond the core scope of this article.
07Pattern matching

Pattern matching with LIKE

Sometimes we do not know the complete value we want to match. Suppose we want every customer whose name begins with A. Equality is too strict: WHERE customer_name = 'A' asks for exactly A. Instead, use LIKE with a wildcard:

Query
SELECT
    customer_id,
    customer_name
FROM customers
WHERE customer_name LIKE 'A%';

The % wildcard means zero or more characters. So A% matches values such as Asha Rao, Amit, A, and Ananya Singh.

The two common LIKE wildcards

% matches zero or more characters, so LIKE 'A%' means begins with A, LIKE '%pur' means ends with pur, and LIKE '%example%' means contains example anywhere. _ matches exactly one character, so LIKE 'REF-_ND-%' requires one character in the position represented by _.

%    any number of characters
_    exactly one character

Matching text anywhere

Suppose we need customers whose email contains example.com:

Query
SELECT
    customer_name,
    email
FROM customers
WHERE email LIKE '%example.com%';

The wildcard before the text allows characters before example.com, and the wildcard after allows characters after it.

Case sensitivity depends on the database

Do not assume WHERE customer_name LIKE 'a%' will always match Asha Rao. Case-sensitive behaviour can depend on the database and collation. In PostgreSQL, ILIKE provides case-insensitive pattern matching (WHERE customer_name ILIKE 'a%'). A more portable conceptual approach is to normalize the text: WHERE LOWER(customer_name) LIKE 'a%'. The exact implementation should follow the database you are using.

Gotcha: wildcards are data when used with equality

WHERE customer_name = 'A%' does not mean name starts with A. With equality, % is just a percent character. Pattern behaviour comes from LIKE, so WHERE customer_name LIKE 'A%' is the intended pattern match.

Gotcha: % can match more than you intended

Suppose you want referral codes for India, REF-IND-101 and REF-IND-415. You might write WHERE referral_code LIKE '%IND%'. This works for the current sample, but it means match IND anywhere in the value. If some future code contains REF-OTHER-IND-999, it would also match. If the code format is structured, the pattern should reflect that structure as precisely as possible:

Query
WHERE referral_code LIKE 'REF-IND-%'

This communicates REF-, then IND, then another -, then the remaining code. A narrower pattern is easier to reason about.

08Putting it together

Combining string functions

Real text-cleaning problems often involve several operations together. Suppose we want to normalize email addresses by removing leading and trailing spaces and converting the value to lowercase, then keep only normalized addresses from example.com:

Query
SELECT
    customer_id,
    customer_name,
    LOWER(TRIM(email)) AS normalized_email
FROM customers
WHERE LOWER(TRIM(email)) LIKE '%@example.com';

The same expression appears in both the output and the filter. This is valid, but repeated transformations can make larger queries harder to read. Later, intermediate results such as CTEs can help name and reuse cleaned values:

Query
WITH normalized_customers AS (
    SELECT
        customer_id,
        customer_name,
        LOWER(TRIM(email)) AS email
    FROM customers
)
SELECT
    customer_id,
    customer_name,
    email
FROM normalized_customers
WHERE email LIKE '%@example.com';

This separates normalizing the text from using the normalized text.

Text cleaning can change matching behaviour

Consider two values, 'Ben Thomas' and ' ben thomas '. A direct equality comparison treats them as different. After LOWER(TRIM(...)), both may become ben thomas. This can be exactly what the business requires, but it can also accidentally merge values that the business considers different.

Key idea
Normalization should follow an explicit rule. “Email comparisons should ignore capitalization and surrounding spaces” is a clear rule. “Make all text look similar before comparing it” is not.
09Recognition cues

How to spot text-processing problems

Look for text operations when:

Case varies between otherwise equivalent values.
Imported values contain unexpected spaces.
Two columns need to be presented as one value.
Part of a structured identifier needs to be extracted.
A delimiter needs to be replaced.
You need values beginning or ending with particular text.
You need to search for a word or token inside a string.
Exact equality misses values that visually appear identical.

Watch for common mistakes such as:

  • Using LOWER but forgetting surrounding whitespace.
  • Assuming empty strings and NULL are the same.
  • Using fixed substring positions on variable-length data.
  • Using REPLACE without considering other occurrences of the text.
  • Using = with % and expecting wildcard matching.
  • Using a pattern that is broader than the business rule.
  • Assuming LIKE has the same case behaviour across every database.
10A practical mental model

Ask what transformation the requirement actually needs

1 · Is the problem capitalization?

Use LOWER(...) or UPPER(...).

2 · Is the problem surrounding whitespace?

Use TRIM(...).

3 · Do several values need to become one?

Use concatenation: first_name || ' ' || last_name.

4 · Do I need only part of a value?

Use SUBSTRING(...) when the position is well defined.

5 · Do I need to replace known text?

Use REPLACE(...).

6 · Do I know only part of the value?

Use pattern matching: LIKE.

Key idea
Then ask: is this transformation correcting presentation, normalizing equivalent values, or changing the meaning of the data? That distinction matters.
11Check your understanding

One more prediction

Consider these email values. Requirement: return customers whose normalized email belongs to example.com. Capitalization and surrounding spaces should not affect matching. Which condition best expresses the requirement?

emails
customeremail
AshaASHA@EXAMPLE.COM
Benben@example.com
Esha' esha@example.com '
ONE MORE PREDICTIONWhich condition best expresses the requirement?
Query
-- Option A
WHERE email LIKE '%@example.com'

-- Option B
WHERE LOWER(email) LIKE '%@example.com'

-- Option C
WHERE LOWER(TRIM(email)) LIKE '%@example.com'

-- Option D
WHERE email = '%@example.com'
Your prediction
12Summary

Summary

SQL provides string operations for cleaning, transforming, combining, and searching text. Use LOWER(email) or UPPER(text) to normalize capitalization, and TRIM(email) to remove unwanted surrounding whitespace. Functions can be combined, as in LOWER(TRIM(email)). Concatenation creates new text values (first_name || ' ' || last_name), SUBSTRING(referral_code FROM 1 FOR 3) extracts part of a value, REPLACE(referral_code, '-', '/') changes known text, and LIKE 'A%' handles pattern matching.

Key idea
The most important text-processing rule: decide what differences should be meaningful before normalizing them away. ASHA@EXAMPLE.COM and ' asha@example.com ' may represent the same email address according to your business rules, but SQL does not automatically know that. You must express the normalization, LOWER(TRIM(email)). Similarly, patterns should describe the requirement as narrowly as possible: LIKE '%IND%' means something much broader than LIKE 'REF-IND-%'. Reliable text processing comes from understanding the format of the data and being explicit about which textual differences should matter.

Next, we will learn how SQL handles different data types and how to convert values safely using explicit casts and type conversion.

Go deeper

Ask Colearn about this pattern

Not sure why an equality check misses a row you can see, or when a pattern is too broad? Ask about the concept, or paste a simplified version of your query.

Ask Colearn

3 of 3 free questions left

Instant answers, grounded in the same verified material the diagnostic grades against.

Why did my concatenation produce NULL?

In many systems an expression involving NULL becomes NULL, so first_name || ' ' || last_name returns NULL when last_name is NULL.

If a missing last name should simply be omitted, handle it explicitly, for example first_name || COALESCE(' ' || last_name, '').

from the unit →
Is an empty string the same as NULL?

No. An empty string is a known value of length zero; NULL means the value is unknown or absent. Comparisons and functions can treat them differently.

Decide what a missing text value should mean before replacing it, and use COALESCE or NULLIF to make that decision explicit.

from the unit →
Why does WHERE name = 'A%' not match names starting with A?

With equality, % is just a percent character, so = 'A%' asks for the exact three-character value A followed by a percent sign.

Pattern behaviour comes from LIKE. Use WHERE name LIKE 'A%' for the begins-with-A match you intended.

from the unit →
Why does LIKE '%IND%' match more than I want?

%IND% means match IND anywhere in the value, so a future code such as REF-OTHER-IND-999 would also match.

If the format is structured, make the pattern reflect it, for example LIKE 'REF-IND-%'. A narrower pattern is easier to reason about.

from the unit →

That’s the free taste

Two ways to go deeper.

Don’t paste credentials, personal data, or confidential production records.

Related patterns

P13Pattern · 9 minData types & CASTOnce the text is clean, convert it to the right type so comparisons behave numerically.P11Pattern · 7 minDates & timestampsThe other real-world data type: filtering and working with time correctly.P02Pattern · 8 minNULL & UNKNOWNWhy a NULL last name can make concatenation return NULL.
Up next · Pattern 13
SQL data types and CAST: converting values safely

Next, we look at how SQL handles different data types and how to convert values safely using explicit casts and type conversion.

Continue to P13 →Browse all patterns