String Functions (CONCAT, SUBSTRING, TRIM)

Reviewed & published by Brayan K

Real-world text is messy: names split across columns, emails that need a username extracted, codes padded with zeros, fields with stray whitespace. SQL's string functions let you clean, reshape, and combine text right inside your queries — no application code required.

Part of the free SQL course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

You'll learn to join text with CONCAT, slice it with SUBSTRING, change case with UPPER/LOWER, scrub whitespace with TRIM, swap text with REPLACE, and locate substrings with POSITION.

What You'll Learn

Our Sample Table: users

Split names, mixed-case emails, and a couple of product SKUs we'll slice apart.

1. CONCAT — Joining Text

CONCAT stitches values together into one string. The most common use is building a full name from separate first/last columns. The ANSI-standard alternative is the || operator.

CONCAT is threading beads onto a string: each argument is a bead, and you can slip a separator bead (like a space) between them.

-- engine: postgres — this lesson uses the standard string functions
-- (CONCAT, POSITION … IN, TRIM('0' FROM …)), which SQLite does not have.
-- The three tables these examples query:
CREATE TABLE users (
    id         INTEGER PRIMARY KEY,
    first_name TEXT,
    last_name  TEXT,
    username   TEXT,
    email      TEXT,
    phone      TEXT
);
INSERT INTO users VALUES
    (1, 'Amélie', 'Dubois', 'amelie', 'Amelie@Shop.COM', '555-12-99'),
    (2, 'Lucas',  'Martin', 'lucas',  'LUCAS@mail.io',   '555-40-01');

CREATE TABLE products (
    id           INTEGER PRIMARY KEY,
    sku          TEXT,
    product_name TEXT,
    description  TEXT
);
INSERT INTO products VALUES
    (1, 'ABC-1001', 'Wireless Mouse', 'A quiet two-button mouse'),
    (2, 'XYZ-2042', 'Desk Lamp',      'Warm light, three levels');

CREATE TABLE imports (
    raw_code  TEXT,
    phone     TEXT,
    padded_id TEXT
);
INSERT INTO imports VALUES
    ('  A1 ', '555-12-99', '00042');

-- CONCAT joins values into one string
SELECT
    CONCAT(first_name, ' ', last_name) AS full_name
FROM users;

-- ✅ Expected result:
-- full_name
-- Amélie Dubois
-- Lucas Martin

2. LENGTH, UPPER & LOWER

LENGTH returns the number of characters, while UPPER and LOWER change case. Case functions are essential for case-insensitive comparisons: normalize both sides with LOWER() before matching.

-- Uses the tables from the first example.
-- LENGTH counts characters; UPPER / LOWER change case
SELECT
    email,
    LENGTH(email)  AS len,
    UPPER(first_name) AS shout,
    LOWER(email)   AS normalized
FROM users;

-- ✅ Expected result:
-- email | len | shout | normalized
-- Amelie@Shop.COM | 15 | AMÉLIE | amelie@shop.com
-- LUCAS@mail.io | 13 | LUCAS | lucas@mail.io

3. SUBSTRING — Slicing Text

SUBSTRING(text, start, length) extracts part of a string. The crucial gotcha: positions are 1-based, so the first character is position 1, not 0. Omit the length to take everything from start to the end.

-- Uses the tables from the first example.
-- SUBSTRING(text, start, length) — positions start at 1, not 0!
SELECT
    sku,
    SUBSTRING(sku, 1, 3) AS prefix,   -- first 3 chars
    SUBSTRING(sku, 5)    AS rest      -- from position 5 to the end
FROM products;

-- ✅ Expected result:
-- sku | prefix | rest
-- ABC-1001 | ABC | 1001
-- XYZ-2042 | XYZ | 2042

Your Turn: build a label

Fill in the function that joins multiple strings into one.

-- Uses the tables from the first example.
-- 🎯 YOUR TURN — fill in the function, then "Try it Yourself"
-- Goal: build a "label" column = first_name, a space, then last_name.

SELECT
    ___(first_name, ' ', last_name) AS label   -- 👉 the string-joining function
FROM users;

-- ✅ Expected: "Amélie Dubois", "Lucas Martin", ...

4. TRIM, REPLACE & POSITION

TRIM strips surrounding whitespace (or a character you name), REPLACE swaps every occurrence of one substring for another, and POSITION (a.k.a. INSTR in some databases) tells you where a substring starts. Together they handle most data-cleaning chores.

-- Uses the tables from the first example.
-- TRIM removes surrounding spaces; REPLACE swaps text
SELECT
    TRIM(raw_code)                          AS clean_code,
    REPLACE(phone, '-', '')                 AS digits_only,
    TRIM('0' FROM padded_id)                AS unpadded
FROM imports;

-- raw_code='  A1 '   -> 'A1'
-- phone='555-12-99'  -> '5551299'
-- padded_id='00042'  -> '42'

-- ✅ Expected result:
-- clean_code | digits_only | unpadded
-- A1 | 5551299 | 42
-- Uses the tables from the first example.
-- POSITION / INSTR finds where a substring appears (1-based, 0 if absent)
SELECT
    email,
    SUBSTRING(email, 1, POSITION('@' IN email) - 1) AS local_part
FROM users;

-- ✅ Expected result:
-- email | local_part
-- Amelie@Shop.COM | Amelie
-- LUCAS@mail.io | LUCAS

🧩 Reorder Challenge

Unscramble these lines into a query that produces an uppercase 3-letter prefix from each sku and sorts by it.

-- SCRAMBLED — arrange into a query that builds a uppercase
-- 3-letter prefix from each product's sku (e.g. 'abc-1001' -> 'ABC').

FROM products
SELECT sku,
UPPER(SUBSTRING(sku, 1, 3)) AS prefix
ORDER BY prefix

Why: the functions nest inside-out — SUBSTRING(sku, 1, 3) grabs the first three characters (1-based!), then UPPER(...) capitalizes the result. That expression defines the prefix alias in SELECT, and because ORDER BY runs after SELECT, it can sort by that alias directly.

🧠 Quick Recall

Predict each result, then reveal.

'data' — starting at position 1 (the 'd'), taking 4 characters. Remember SQL is 1-based.

2 — TRIM strips the surrounding spaces, leaving 'hi', which has 2 characters.

'abc' — every dash is replaced with an empty string, removing all three separators.

Common Errors (and the fix)

Mini-Challenge: Email → Username

Combine SUBSTRING, POSITION, LOWER and TRIM to turn an email into a clean username. Expected shape is in the comments.

-- 🎯 MINI-CHALLENGE — uses only this lesson's ideas
-- Build a clean username from each email:
--   1. Take everything BEFORE the @  (SUBSTRING + POSITION)
--   2. Force it to lowercase          (LOWER)
--   3. Remove any stray spaces        (TRIM)
-- Show email and the resulting "username".
--
-- ✅ Expected: e.g. 'amelie@shop.com' -> 'amelie'

-- your query here

🎉 Lesson Complete

Practice quiz

What does CONCAT do?

  • Counts characters
  • Slices a substring
  • Joins values into one string
  • Changes case

Answer: Joins values into one string. CONCAT stitches values together into one string; the ANSI-standard equivalent is the || operator.

Which is the ANSI-standard operator for joining strings?

  • ||
  • +
  • &
  • .

Answer: ||. The || operator is the ANSI standard (Postgres, SQLite, Oracle); CONCAT is the most portable function.

In SQL, what position is the first character of a string for SUBSTRING?

  • 0
  • -1
  • It depends on the row
  • 1

Answer: 1. SQL string positions are 1-based, so SUBSTRING('hello', 1, 1) returns 'h', not 'e'.

What does SUBSTRING('database', 1, 4) return?

  • 'atab'
  • 'data'
  • 'abas'
  • 'datab'

Answer: 'data'. Starting at position 1 (the 'd') and taking 4 characters gives 'data'.

What is LENGTH(TRIM(' hi '))?

  • 2
  • 6
  • 4
  • 0

Answer: 2. TRIM strips the surrounding spaces leaving 'hi', which has 2 characters.

What does REPLACE('a-b-c', '-', '') produce?

  • 'a-b-c'
  • 'a b c'
  • 'abc'
  • '-abc'

Answer: 'abc'. REPLACE swaps every occurrence of the dash with an empty string, removing all three separators.

What do UPPER and LOWER do, and why are they used for matching?

  • Sort rows; for ordering
  • Change case; normalize both sides for case-insensitive comparison
  • Count characters; for length checks
  • Trim spaces; for cleaning

Answer: Change case; normalize both sides for case-insensitive comparison. UPPER/LOWER change case; normalizing both sides with LOWER() enables case-insensitive matching (but can skip a plain index).

What does POSITION return when the substring is absent?

  • NULL
  • -1
  • An error
  • 0

Answer: 0. POSITION (a.k.a. INSTR) is 1-based and returns 0 when the substring does not appear.

In MySQL, what can happen to CONCAT if one argument is NULL?

  • It ignores NULLs always
  • The whole result can become NULL
  • It throws an error
  • It returns 0

Answer: The whole result can become NULL. In MySQL CONCAT returns NULL if any argument is NULL; wrap nullable values in COALESCE or use CONCAT_WS.

Why can SUBSTRING(s, 0, 3) behave unexpectedly?

  • 0 is a reserved word
  • SUBSTRING needs four arguments
  • Positions start at 1, not 0, so 0 is out of the normal range
  • It only works on numbers

Answer: Positions start at 1, not 0, so 0 is out of the normal range. SQL string indexing is 1-based; use SUBSTRING(s, 1, 3) to take the first three characters.

Continue this course

Frequently asked questions

How do I join strings — CONCAT or || ?

Both work. CONCAT(a, b, c) is a function available in MySQL, SQL Server and Postgres. The || operator is the ANSI standard (Postgres, SQLite, Oracle). MySQL uses || for logical OR unless PIPES_AS_CONCAT is set, so CONCAT is the most portable choice.

Is SUBSTRING 1-indexed or 0-indexed?

SQL string positions start at 1, not 0. SUBSTRING('hello', 1, 3) returns 'hel'. This trips up programmers used to 0-based languages, so always remember: the first character is position 1.

What's the difference between TRIM, LTRIM and RTRIM?

TRIM removes characters (spaces by default) from both ends, LTRIM from the left only, and RTRIM from the right only. Many databases also let you specify which character to trim, e.g. TRIM('0' FROM '00042') = '42'.

Does CONCAT handle NULL gracefully?

It depends. In MySQL, CONCAT returns NULL if any argument is NULL, while CONCAT_WS skips NULLs. In Postgres, CONCAT treats NULL as an empty string but the || operator returns NULL. Wrap nullable values in COALESCE to be safe and predictable.

How do I do case-insensitive matching with these functions?

Normalize both sides with LOWER() or UPPER(): WHERE LOWER(email) = LOWER(:input). Be aware this can prevent the use of a plain index on the column, so for hot paths consider a functional index or a case-insensitive collation.

Related lessons