Cleaning and splitting text

Change case, trim spaces, join columns together and pull pieces out of text.

Intermediate10 min readLesson 22 of 38
In this lesson 8 sections
  1. Changing case
  2. Trimming spaces
  3. Joining text together
  4. Pulling out part of the text
  5. Replacing text
  6. Common mistakes
  7. Dialect notes
  8. Snowflake notes
Jump to practice

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.

Back to the top of the lesson