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

Use PostgreSQL string functions for normalization, SUBSTRING and SPLIT_PART parsing, NULL-safe labels, ordered lists, LIKE, and row splitting.

Author: PracHub

Published: 8/14/2026

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

By PracHub
August 14, 2026
17 min read
0
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.

Data AnalystFree

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_idfirst_namelast_nameemail
1AnaLiANA@Example.COM
2benNULLben@example.com
3CaraO'Neilcara@sample.org
4NULLDiazinvalid-address
5EliNULL
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;
Row flow from raw contact emails to normalized domain groups Five contact rows trim and lowercase valid email domains, two rows receive null domains, and grouping produces three domain rows. 5 contact rowscase and missing data3 domain valuesexample, sample, NULL3 grouped rowscounts 2, 1, and 2
The NULL group combines one malformed address and one missing address; keep them separate if that distinction matters.

Output

email_domaincontact_count
example.com2
sample.org1
NULL2

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_idreference
1INC-2026-0042
2BUG-2025-0105
3BADREF
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;
Row flow through regular-expression substring parsing Three ticket-reference rows are tested for prefix, four-digit year, and trailing sequence patterns, producing three parsed output rows with nulls for the malformed reference. 3 reference rows2 structured, 1 malformedMatch 3 componentsno match becomes NULL3 parsed rowsBADREF has 2 NULLs
The parser exposes malformed rows instead of silently manufacturing a year or sequence.

Output

ref_idprefixyear_textsequence_text
1INC20260042
2BUG20250105
3BADREFNULLNULL

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

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;
Row flow from URLs to host and path fields Three URL rows remove their schemes, split out three host values, extract paths without query strings, and produce three parsed rows. 3 URL rows2 schemes, 1 query stringRemove, split, extracthost and path rules3 parsed rowsdefault root path
This is a deliberately narrow parser for the shown format, not a replacement for a general URL parser.

Output

request_idhostpath
1docs.example.com/sql/order
2shop.example.com/items/42
3example.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_idfirst_namelast_nameemail
1AnaLiANA@Example.COM
2benNULLben@example.com
3CaraO'Neilcara@sample.org
4NULLDiazinvalid-address
5EliNULL
SELECT
  contact_id,
  CONCAT_WS(
    ' ',
    NULLIF(TRIM(first_name), ''),
    NULLIF(TRIM(last_name), '')
  ) AS display_name
FROM contacts
ORDER BY contact_id;
Row flow from nullable name parts to display names Five contact rows trim first and last names, convert empty strings to null, and concatenate the remaining parts into five display-name rows. 5 contact rowsNULL and empty partsTrim and skip NULLsone space separator5 name rowsno doubled spaces
An empty result is still an empty string; add a final NULLIF if a contact with no name parts should return NULL.

Output

contact_iddisplay_name
1Ana Li
2ben
3Cara O'Neil
4Diaz
5Eli

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_idtag
1SQL
1postgresql
1sql
2Strings
2analytics
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;
Row flow from tag rows to ordered distinct lists Five tag rows normalize to four distinct article-tag values, sort within each article, and aggregate into two output rows. 5 tag rowsSQL repeats by case4 normalized tagsdistinct and ordered2 article rowsstable tag lists
The aggregate's order controls text inside each list; the final order controls the two result rows.

Output

article_idtag_list
1postgresql, sql
2analytics, 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_idcode
1acct_100
2acctX100
3sale%2026
4saleX2026
5plain
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;
Row flow through escaped LIKE patterns Five product-code rows are tested for literal underscore and percent characters, producing five boolean result rows with one match for each literal. 5 code rows2 contain wildcard symbolsEscape _ and %match literal characters5 boolean rows1 true per test
Without escaping, acctX100 would satisfy the underscore pattern because underscore means any one character.

Output

product_idcodehas_literal_underscorehas_literal_percent
1acct_100truefalse
2acctX100falsefalse
3sale%2026falsetrue
4saleX2026falsefalse
5plainfalsefalse

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_idselected_options
1red;blue;green
2blue
3NULL
4red;;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;
Row flow from delimited responses to ordered option rows Four response rows expand into seven tokens, the one blank token is removed, the null response emits no token, and six ordered option rows remain. 4 response rows1 NULL, 1 blank token7 expanded tokensfilter 1 blank6 option rowsoriginal positions kept
A lateral table function can emit zero, one, or many right-side rows for each response.

Output

response_idoptionposition
1red1
1blue2
1green3
2blue1
4red1
4green3

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.


Comments (0)