SQL String Functions: SUBSTRING, SPLIT_PART, CONCAT, and LIKE in Interviews

Quick Overview
A Data Analyst guide to PostgreSQL string operations. Seven verified walkthroughs cover normalization before grouping, regex and delimiter parsing, CONCAT_WS, ordered STRING_AGG, escaped LIKE patterns, and ordered row expansion.
String functions turn stored text into comparison keys, parsed fields, display labels, and ordered lists. The important decision is whether a string is free text or a structured value with a documented format.
Normalize only what the analysis contract treats as equivalent. Parse defensively when delimiters can be absent, preserve NULL when information is missing, and state ordering whenever several strings become one result.
Normalize before grouping
Whitespace and letter case should not split email domains in this analysis. The CASE expression also keeps missing or delimiter-free addresses in a NULL group instead of pretending they have a valid domain.
Input: contacts
| contact_id | first_name | last_name | |
|---|---|---|---|
| 1 | Ana | Li | ANA@Example.COM |
| 2 | ben | NULL | ben@example.com |
| 3 | Cara | O'Neil | cara@sample.org |
| 4 | NULL | Diaz | invalid-address |
| 5 | Eli | NULL |
WITH normalized AS (
SELECT
contact_id,
CASE
WHEN POSITION('@' IN TRIM(email)) > 0
THEN LOWER(SPLIT_PART(TRIM(email), '@', 2))
ELSE NULL
END AS email_domain
FROM contacts
)
SELECT
email_domain,
COUNT(*) AS contact_count
FROM normalized
GROUP BY email_domain
ORDER BY email_domain NULLS LAST;
Output
| email_domain | contact_count |
|---|---|
| example.com | 2 |
| sample.org | 1 |
| NULL | 2 |
Normalization is not universal cleaning. Lowercasing may be right for domains and wrong for case-sensitive identifiers. The SQL GROUP BY guide covers the resulting aggregate grain.
Parse structured strings with explicit rules
SUBSTRING with a regular expression can extract components when a reference format is known. Rows that do not contain the requested numeric components return NULL for those fields.
Input: ticket_refs
| ref_id | reference |
|---|---|
| 1 | INC-2026-0042 |
| 2 | BUG-2025-0105 |
| 3 | BADREF |
SELECT
ref_id,
SUBSTRING(reference FROM '^[A-Z]+') AS prefix,
SUBSTRING(reference FROM '[0-9]{4}') AS year_text,
SUBSTRING(reference FROM '[0-9]+$') AS sequence_text
FROM ticket_refs
ORDER BY ref_id;
Output
| ref_id | prefix | year_text | sequence_text |
|---|---|---|---|
| 1 | INC | 2026 | 0042 |
| 2 | BUG | 2025 | 0105 |
| 3 | BADREF | NULL | NULL |
SPLIT_PART is useful when delimiters define the structure. Here it finds the host, while SUBSTRING keeps the complete path and stops before a query string. A missing path becomes /.
Input: web_requests
| request_id | url |
|---|---|
| 1 | https://docs.example.com/sql/order |
| 2 | http://shop.example.com/items/42?ref=home |
| 3 | https://example.com |
WITH without_scheme AS (
SELECT
request_id,
REGEXP_REPLACE(url, '^[^:]+://', '') AS remainder
FROM web_requests
)
SELECT
request_id,
SPLIT_PART(remainder, '/', 1) AS host,
COALESCE(
SUBSTRING(remainder FROM '/[^?]*'),
'/'
) AS path
FROM without_scheme
ORDER BY request_id;
Output
| request_id | host | path |
|---|---|---|
| 1 | docs.example.com | /sql/order |
| 2 | shop.example.com | /items/42 |
| 3 | example.com | / |
Concatenation and aggregation need NULL rules
The || operator propagates NULL, while CONCAT_WS skips NULL arguments. Converting empty trimmed values to NULL produces usable display names without extra spaces.
Input: contacts
| contact_id | first_name | last_name | |
|---|---|---|---|
| 1 | Ana | Li | ANA@Example.COM |
| 2 | ben | NULL | ben@example.com |
| 3 | Cara | O'Neil | cara@sample.org |
| 4 | NULL | Diaz | invalid-address |
| 5 | Eli | NULL |
SELECT
contact_id,
CONCAT_WS(
' ',
NULLIF(TRIM(first_name), ''),
NULLIF(TRIM(last_name), '')
) AS display_name
FROM contacts
ORDER BY contact_id;
NULLIF if a contact with no name parts should return NULL.Output
| contact_id | display_name |
|---|---|
| 1 | Ana Li |
| 2 | ben |
| 3 | Cara O'Neil |
| 4 | Diaz |
| 5 | Eli |
STRING_AGG combines several rows. Its internal ORDER BY defines list order, and DISTINCT removes tags that become equal after normalization.
Input: article_tags
| article_id | tag |
|---|---|
| 1 | SQL |
| 1 | postgresql |
| 1 | sql |
| 2 | Strings |
| 2 | analytics |
SELECT
article_id,
STRING_AGG(
DISTINCT LOWER(TRIM(tag)),
', '
ORDER BY LOWER(TRIM(tag))
) AS tag_list
FROM article_tags
GROUP BY article_id
ORDER BY article_id;
Output
| article_id | tag_list |
|---|---|
| 1 | postgresql, sql |
| 2 | analytics, strings |
The difference between distinct input values and distinct result rows is covered in SQL DISTINCT. SQL ORDER BY covers the two ordering scopes.
Escape LIKE wildcards when they are literal
In a LIKE pattern, % matches any number of characters and _ matches one character. ESCAPE '\' makes the following wildcard literal, so the query can test for actual underscore and percent characters.
Input: product_codes
| product_id | code |
|---|---|
| 1 | acct_100 |
| 2 | acctX100 |
| 3 | sale%2026 |
| 4 | saleX2026 |
| 5 | plain |
SELECT
product_id,
code,
code LIKE '%\_%' ESCAPE '\' AS has_literal_underscore,
code LIKE '%\%%' ESCAPE '\' AS has_literal_percent
FROM product_codes
ORDER BY product_id;
acctX100 would satisfy the underscore pattern because underscore means any one character.Output
| product_id | code | has_literal_underscore | has_literal_percent |
|---|---|---|---|
| 1 | acct_100 | true | false |
| 2 | acctX100 | false | false |
| 3 | sale%2026 | false | true |
| 4 | saleX2026 | false | false |
| 5 | plain | false | false |
ILIKE adds case-insensitive matching in PostgreSQL. It does not change wildcard or escaping rules.
Split delimited text into ordered rows
STRING_TO_TABLE expands one delimited value into several rows. WITH ORDINALITY records each token's original position. The blank token in response 4 is filtered, so its surviving green token keeps position 3.
Input: survey_responses
| response_id | selected_options |
|---|---|
| 1 | red;blue;green |
| 2 | blue |
| 3 | NULL |
| 4 | red;;green |
SELECT
s.response_id,
t.option,
t.position
FROM survey_responses AS s
CROSS JOIN LATERAL STRING_TO_TABLE(
s.selected_options,
';'
) WITH ORDINALITY AS t(option, position)
WHERE t.option <> ''
ORDER BY s.response_id, t.position;
Output
| response_id | option | position |
|---|---|---|
| 1 | red | 1 |
| 1 | blue | 2 |
| 1 | green | 3 |
| 2 | blue | 1 |
| 4 | red | 1 |
| 4 | green | 3 |
Repeated multi-value analysis is usually easier with one value per stored row. When a delimited field is unavoidable at an ingestion boundary, parse once, retain source position if it matters, and validate blank and NULL tokens. More exercises are in SQL practice questions.
FAQ
What does SPLIT_PART return when a field is missing?
PostgreSQL returns an empty string when the requested field number is beyond the available pieces. Check delimiter presence or convert an empty result with NULLIF when missing should be NULL.
How does CONCAT_WS handle NULL?
It skips NULL arguments and inserts the separator only between remaining values. Empty strings are not NULL, so normalize them first when they should be treated as missing.
Does STRING_AGG guarantee order?
Only when its aggregate call includes an ORDER BY. The query's final ORDER BY controls result rows, not the order of values inside each aggregate string.
What do percent and underscore mean in LIKE?
Percent matches zero or more characters. Underscore matches exactly one character. Define an escape character when either symbol must be matched literally.
Should delimited text be stored in one column?
Not when the values need frequent filtering, joining, constraints, or aggregation. A child table with one row per value is usually a clearer relational shape; delimited strings are often best treated as an ingestion or display format.
Related Articles
Coderbyte SQL Assessment Guide: Query Types, Timing, and What Employers See
Learn Coderbyte SQL assessment query types, timing, grading, employer reports, common mistakes, and a practical seven-day preparation plan for candidates.
Capital One Data Analyst Internship 2027: VJT, Power Day, and Why There May Be No CodeSignal
Capital One Data Analyst Internship 2027 guide: VJT, Power Day cases, behavioral interviews, SQL prep, timelines, and why CodeSignal may be skipped.
SQL ORDER BY: Ascending, Descending, Multi-Column Sorting, and Where NULLs Land
Use PostgreSQL ORDER BY for deterministic multi-column sorting, explicit NULL placement, top N, keyset pagination, ties, and windows.
SQL SELECT DISTINCT: What It Actually Deduplicates, and When It Hides a Bug
Understand SQL DISTINCT across full rows, NULLs, counts, groups, latest-row selection, and join fanout using verified PostgreSQL outputs.
Comments (0)