Cleaning and splitting text
Change case, trim spaces, join columns together and pull pieces out of text.
In this lesson 8 sections
Real data is messy: names typed in capitals, stray spaces, two facts crammed into one column. These functions clean text up and pull it apart.
Changing case
SELECT UPPER('Acme Manufacturing'), -- 'ACME MANUFACTURING'
LOWER('Acme Manufacturing'); -- 'acme manufacturing'
The main use is comparing text when you can’t trust the capitals. 'Acme' = 'ACME' is FALSE on most databases (lesson 9), so compare the lowered versions:
WHERE LOWER(name) = 'acme manufacturing'
Trimming spaces
A value typed as ' Acme ' won’t match 'Acme', and it’s hard to spot by eye because spaces are invisible.
SELECT TRIM(' Acme '), -- 'Acme'
LTRIM(' Acme '), -- 'Acme ' (left side only)
RTRIM(' Acme '), -- ' Acme' (right side only)
LENGTH(' Acme '); -- 8: spaces count
LENGTH is a quick way to catch them: if a four-letter name has a length of 8, something’s hiding.
Joining text together
|| glues text together. Add your own spaces and punctuation as text in quotes:
SELECT name || ' (' || industry || ')' AS label
FROM accounts; -- 'Northwind Traders (Retail)'
The NULL trap: if any piece is NULL, the whole result is NULL.
SELECT 'Deal from ' || NULL; -- NULL, not 'Deal from '
Wrap columns that might be empty in COALESCE (lesson 19): 'Source: ' || COALESCE(lead_source, 'Unknown'). Or use concat(), which skips NULLs:
SELECT concat('a', NULL, 'b'), -- 'ab'
concat_ws(' / ', 'a', NULL, 'c'); -- 'a / c': the first argument goes between each piece
Pulling out part of the text
Two functions do almost all the work:
| Function | Returns | Example | Result |
|---|---|---|---|
substr(text, start, length) |
Part of the text, starting at a position | substr('Northwind', 1, 5) |
'North' |
instr(text, find) |
The position where find first appears, or 0 if it doesn’t |
instr('Northwind Traders', ' ') |
10 |
Positions start at 1, not 0. Leave out the length and substr returns everything to the end.
Combine them to split on a character. Every deal name in the sample data is an account word, a space, then the product:
SELECT name,
substr(name, 1, instr(name, ' ') - 1) AS before_space, -- 'Northwind'
substr(name, instr(name, ' ') + 1) AS after_space -- 'Network Refresh'
FROM opportunities;
The - 1 stops before the space, and the + 1 starts just after it. Read it from the inside out: find the space, then cut there.
The same shape pulls the domain out of an email address, using '@' instead of a space. Matching contacts to accounts by email domain is a common CRM cleanup job.
Replacing text
SELECT REPLACE('555-867-5309', '-', ''); -- '5558675309'
REPLACE swaps every match, not just the first. Replace with '' to delete a character.
Common mistakes
| Mistake | Fix |
|---|---|
Gluing a column that can be NULL with || |
COALESCE the column, or use concat() |
| Splitting when the character isn’t there | instr returns 0, so substr(name, 0 + 1) returns the whole text. Check with WHERE instr(name, ' ') > 0. |
| Comparing text without trimming | TRIM both sides, especially for data typed by people |
| Fixing messy text in every query | Clean it once when the data is loaded, then query the clean version |
Dialect notes
| Task | Sandbox (SQLite) | SQL Server | PostgreSQL | MySQL |
|---|---|---|---|---|
| Join text | a || b |
a + b or CONCAT(a, b) |
a || b |
CONCAT(a, b) |
| Length | LENGTH(x) |
LEN(x) (ignores trailing spaces) |
LENGTH(x) |
CHAR_LENGTH(x) |
| Part of the text | substr(x, 1, 5) |
SUBSTRING(x, 1, 5) |
SUBSTRING(x, 1, 5) |
SUBSTRING(x, 1, 5) |
| Find a position | instr(x, ' ') |
CHARINDEX(' ', x) |
POSITION(' ' IN x) |
INSTR(x, ' ') |
| Split on a character | substr + instr |
substr + CHARINDEX |
split_part(x, ' ', 2) |
SUBSTRING_INDEX(x, ' ', 1) |
Two traps when moving between databases. In MySQL, || means OR, not join, so 'a' || 'b' returns 0. And SQL Server’s LEN ignores trailing spaces, so LEN('Acme ') is 4.
Snowflake notes
| Task | Snowflake |
|---|---|
| Join text | a || b, CONCAT(a, b), or CONCAT_WS(' ', a, b) |
| Split on a character | SPLIT_PART(x, ' ', 2): the second piece |
| Find a position | POSITION(' ' IN x) or CHARINDEX(' ', x) |
| Case-insensitive match | ILIKE (lesson 4) |
| Patterns | REGEXP_SUBSTR(x, pattern), REGEXP_REPLACE |
SPLIT_PART replaces most substr + instr combinations, and it returns an empty string, not an error, when the piece doesn’t exist. Text comparisons are case-sensitive, so LOWER or ILIKE are everyday tools there.
More in the field guide: String functions and Text: VARCHAR.
Try it
The product is everything after the first space in a deal’s name: Northwind Network Refresh is a Network Refresh deal. How many deals are there of each product? Return the product and the count, most deals first.
One row will look odd. That’s a test deal someone forgot to delete, and spotting rows like that is part of the job.
Write it in the sandbox below, then press Check answer.
SQL sandbox
Run queries against the sample CRM tables (accounts, opportunities,users, contacts and opportunity_history), right in your browser. Nothing you run leaves your computer.