| |

SQL 50 🛢️ String Manipulation Functions

A string in SQL is a sequence of characters. In practice, it is rarely clean. It has leading and trailing whitespace, inconsistent casing, substrings that need extraction, parts that need concatenation, and lengths that need measuring. SQL provides a set of string functions for these operations, and although the exact names vary slightly between databases, the core functions are standardized and portable. This chapter covers the functions that appear most often in real queries: concatenation, length, case conversion, trimming, extraction, searching, and replacement.

Key point: String functions in SQL are mostly standard, but the names differ between databases. LENGTH in PostgreSQL is LEN in SQL Server. SUBSTRING is standard, but SQLite uses SUBSTR. CONCAT is standard, but the || operator is more portable for two arguments. Knowing the function and its variants across databases is part of writing portable SQL.


Why string functions matter

The data-cleaning problem. Data entered by humans is inconsistent. Names have extra spaces. Emails have mixed case. Phone numbers have different formats. Product codes have prefixes that need to be stripped. String functions are the first line of defense against dirty data. They normalize, trim, and standardize values before they are stored or compared.

The display problem. Reports need formatted output. A full name is the concatenation of first and last. A formatted address combines several fields with separators. A truncated description shows the first 100 characters. String functions produce the display values that users see.

The matching problem. Searching for a substring, finding the position of a character, or checking whether a string starts with a prefix are all string operations. LIKE and regular expressions handle pattern matching, but POSITION, INSTR, and SUBSTRING handle precise extraction.

The parsing problem. A single column may contain structured data that should be split. A CSV line has fields separated by commas. A log entry has a timestamp followed by a message. A URL has a domain and a path. String functions split these values into their components.

The database-difference problem. SQL string functions are not identical across databases. PostgreSQL, MySQL, and SQLite have their own names for common operations. Oracle uses SUBSTR and INSTR where PostgreSQL uses SUBSTRING and POSITION. SQL Server uses LEN where the standard uses LENGTH. The core functions are similar, but the details differ. This chapter uses the standard names and notes the common variants.


a. Concatenation, length, and case

Concatenation combines two or more strings. The standard operator is ||, supported by PostgreSQL, Oracle, SQLite, and MySQL (with PIPES_AS_CONCAT). SQL Server uses +. The CONCAT function is standard and accepts any number of arguments.

SELECT 'Hello' || ' ' || 'World';          -- 'Hello World'
SELECT CONCAT('Hello', ' ', 'World');      -- 'Hello World'

The || operator is more portable for two arguments. CONCAT is more portable for many arguments and handles NULLs differently: CONCAT('a', NULL) returns 'a' in MySQL and PostgreSQL, while 'a' || NULL returns NULL.

Length returns the number of characters in a string. The standard function is LENGTH. SQL Server uses LEN, which excludes trailing spaces. MySQL and PostgreSQL use LENGTH, which counts bytes for CHAR columns in some configurations; CHAR_LENGTH is the character-counting function.

SELECT LENGTH('Hello');        -- 5
SELECT CHAR_LENGTH('Hello');   -- 5
SELECT LEN('Hello');           -- 5 (SQL Server)

For multi-byte characters, LENGTH may return the byte count rather than the character count. CHAR_LENGTH or CHARACTER_LENGTH returns the character count. In PostgreSQL, LENGTH returns the character count for TEXT and VARCHAR.

Case conversion changes a string to uppercase or lowercase. The standard functions are UPPER and LOWER.

SELECT UPPER('Hello');    -- 'HELLO'
SELECT LOWER('Hello');    -- 'hello'

These functions are useful for case-insensitive comparisons. WHERE LOWER(email) = LOWER(input) matches regardless of the case of either value. A functional index on LOWER(email) makes the comparison efficient.

MySQL also provides INITCAP, which capitalizes the first letter of each word. PostgreSQL provides INITCAP as well.

SELECT INITCAP('hello world');   -- 'Hello World'

b. Trimming, extraction, and searching

Trimming removes characters from the beginning or end of a string. The standard functions are TRIM, LTRIM, and RTRIM.

SELECT TRIM('  Hello  ');     -- 'Hello'
SELECT LTRIM('  Hello  ');    -- 'Hello  '
SELECT RTRIM('  Hello  ');    -- '  Hello'

TRIM removes whitespace by default. It can also remove a specific character with the LEADING, TRAILING, or BOTH keywords:

SELECT TRIM(LEADING '0' FROM '000123');    -- '123'
SELECT TRIM(TRAILING '.' FROM 'Hello...'); -- 'Hello'

Extraction returns a substring. The standard function is SUBSTRING, which takes a string, a start position, and optionally a length. The start position is 1-based, not 0-based.

SELECT SUBSTRING('Hello World', 1, 5);      -- 'Hello'
SELECT SUBSTRING('Hello World', 7);         -- 'World'
SELECT SUBSTRING('Hello World' FROM 7);     -- 'World' (standard syntax)

SQLite uses SUBSTR with the same semantics. SQL Server uses SUBSTRING with the same semantics. Oracle uses SUBSTR. PostgreSQL supports both SUBSTRING and SUBSTR.

The start position can be negative in some databases, counting from the end. In PostgreSQL, SUBSTRING('Hello' FROM -3) returns 'llo'. In MySQL, SUBSTRING('Hello', -3) returns 'llo'. This is not standard, and the behavior varies.

Searching finds the position of a substring. The standard function is POSITION, which returns the 1-based position of the first occurrence, or 0 if not found.

SELECT POSITION('World' IN 'Hello World');    -- 7
SELECT POSITION('xyz' IN 'Hello World');      -- 0

SQL Server uses CHARINDEX. MySQL uses INSTR and LOCATE. Oracle uses INSTR. PostgreSQL uses both POSITION and STRPOS.

SELECT CHARINDEX('World', 'Hello World');     -- 7 (SQL Server)
SELECT INSTR('Hello World', 'World');         -- 7 (MySQL)
SELECT STRPOS('Hello World', 'World');        -- 7 (PostgreSQL)

Replacement substitutes one substring for another. The standard function is REPLACE.

SELECT REPLACE('Hello World', 'World', 'SQL');   -- 'Hello SQL'
SELECT REPLACE('a-b-c', '-', '_');               -- 'a_b_c'

REPLACE replaces all occurrences. To replace only the first occurrence, most databases require a different approach, such as combining POSITION and SUBSTRING.

Padding adds characters to the left or right of a string to reach a specified length. The standard functions are LPAD and RPAD.

SELECT LPAD('5', 3, '0');      -- '005'
SELECT RPAD('5', 3, '*');      -- '5**'

These are useful for fixed-width output and for formatting identifiers.


c. Combining functions for real tasks

String functions are most useful when combined. A few patterns appear repeatedly.

Formatting a full name:

SELECT TRIM(CONCAT(first_name, ' ', last_name)) AS full_name
FROM persons;

The CONCAT joins the parts with a space. The TRIM removes the leading or trailing space when one of the parts is empty or NULL.

Extracting a domain from an email:

SELECT SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain
FROM users;

The POSITION finds the @, and the SUBSTRING extracts everything after it. This assumes the email is valid and contains exactly one @.

Normalizing an email for comparison:

SELECT LOWER(TRIM(email)) AS normalized_email
FROM users;

The TRIM removes whitespace, and the LOWER converts to lowercase. The result can be compared or stored in a unique index.

Extracting initials:

SELECT
    UPPER(SUBSTRING(first_name, 1, 1)) ||
    UPPER(SUBSTRING(last_name, 1, 1)) AS initials
FROM persons;

Each SUBSTRING extracts the first character, UPPER capitalizes it, and || concatenates them.

Truncating a description:

SELECT
    CASE
        WHEN LENGTH(description) > 100
        THEN SUBSTRING(description, 1, 97) || '...'
        ELSE description
    END AS short_description
FROM products;

The CASE checks the length, and the SUBSTRING takes the first 97 characters and appends an ellipsis.

Splitting a CSV line:

SELECT
    SUBSTRING(line, 1, POSITION(',' IN line) - 1) AS first_field,
    SUBSTRING(line, POSITION(',' IN line) + 1) AS rest
FROM csv_data;

This splits at the first comma. Splitting at the second comma requires finding the position of the first comma, then searching for the next comma in the remaining substring. This gets tedious quickly, and databases with dedicated split functions—SPLIT_PART in PostgreSQL, STRING_SPLIT in SQL Server—are easier to use.

Removing a prefix:

SELECT SUBSTRING(code FROM LENGTH('PREFIX-') + 1) AS code_without_prefix
FROM products
WHERE code LIKE 'PREFIX-%';

The SUBSTRING starts after the prefix. The WHERE clause ensures the substring is applied only to rows where the prefix is present.


Complete Example Session

-- ============================================
-- PART 1: CONCATENATION
-- ============================================
SELECT 'Hello' || ' ' || 'World';              -- 'Hello World'
SELECT CONCAT('Hello', ' ', 'World');          -- 'Hello World'
SELECT CONCAT('a', NULL);                      -- 'a' in PostgreSQL
SELECT 'a' || NULL;                            -- NULL
-- ============================================
-- PART 2: LENGTH
-- ============================================
SELECT LENGTH('Hello');                        -- 5
SELECT CHAR_LENGTH('Hello');                   -- 5
SELECT LENGTH('');                             -- 0
SELECT LENGTH(NULL);                           -- NULL
-- ============================================
-- PART 3: CASE CONVERSION
-- ============================================
SELECT UPPER('Hello');                         -- 'HELLO'
SELECT LOWER('Hello');                         -- 'hello'
SELECT INITCAP('hello world');                 -- 'Hello World'
-- ============================================
-- PART 4: TRIMMING
-- ============================================
SELECT TRIM('  Hello  ');                      -- 'Hello'
SELECT LTRIM('  Hello  ');                     -- 'Hello  '
SELECT RTRIM('  Hello  ');                     -- '  Hello'
SELECT TRIM(LEADING '0' FROM '000123');        -- '123'
SELECT TRIM(TRAILING '.' FROM 'Hello...');     -- 'Hello'
-- ============================================
-- PART 5: SUBSTRING
-- ============================================
SELECT SUBSTRING('Hello World', 1, 5);         -- 'Hello'
SELECT SUBSTRING('Hello World', 7);            -- 'World'
SELECT SUBSTRING('Hello World' FROM 7);        -- 'World'
SELECT SUBSTR('Hello World', 1, 5);            -- 'Hello' (SQLite)
-- ============================================
-- PART 6: POSITION
-- ============================================
SELECT POSITION('World' IN 'Hello World');     -- 7
SELECT POSITION('xyz' IN 'Hello World');       -- 0
SELECT STRPOS('Hello World', 'World');         -- 7 (PostgreSQL)
SELECT INSTR('Hello World', 'World');          -- 7 (MySQL)
-- ============================================
-- PART 7: REPLACE
-- ============================================
SELECT REPLACE('Hello World', 'World', 'SQL'); -- 'Hello SQL'
SELECT REPLACE('a-b-c', '-', '_');             -- 'a_b_c'
SELECT REPLACE('aaa', 'a', 'b');               -- 'bbb'
-- ============================================
-- PART 8: PADDING
-- ============================================
SELECT LPAD('5', 3, '0');                      -- '005'
SELECT RPAD('5', 3, '*');                      -- '5**'
SELECT LPAD('hello', 10, ' ');                 -- '     hello'
-- ============================================
-- PART 9: FULL NAME FORMATTING
-- ============================================
SELECT TRIM(CONCAT(first_name, ' ', last_name)) AS full_name
FROM persons;
-- Handles NULL first or last name gracefully
-- ============================================
-- PART 10: EMAIL DOMAIN EXTRACTION
-- ============================================
SELECT
    email,
    SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain
FROM users;
-- 'alice@example.com' → 'example.com'

The ten parts covered concatenation, length, case conversion, trimming, SUBSTRING, POSITION, REPLACE, padding, full name formatting, and email domain extraction.


Quick Reference

Core String Functions

FunctionPurposeStandard Name
ConcatenationJoin stringsCONCAT, ||
LengthCharacter countLENGTH, CHAR_LENGTH
UppercaseConvert to upperUPPER
LowercaseConvert to lowerLOWER
TrimRemove whitespaceTRIM, LTRIM, RTRIM
SubstringExtract partSUBSTRING
PositionFind substringPOSITION
ReplaceSubstituteREPLACE
Pad leftAdd leading charsLPAD
Pad rightAdd trailing charsRPAD

Database Variants

OperationPostgreSQLMySQLSQL ServerSQLite
LengthLENGTHLENGTHLENLENGTH
SubstringSUBSTRINGSUBSTRINGSUBSTRINGSUBSTR
PositionPOSITION, STRPOSINSTR, LOCATECHARINDEXINSTR
Concatenate||, CONCATCONCAT, ||+, CONCAT||

Common Patterns

PatternPurpose
TRIM(CONCAT(a, ' ', b))Format full name
LOWER(TRIM(email))Normalize email
SUBSTRING(email FROM POSITION('@' IN email) + 1)Extract domain
UPPER(SUBSTRING(a, 1, 1)) || UPPER(SUBSTRING(b, 1, 1))Initials
LPAD(id, 5, '0')Fixed-width ID
REPLACE(code, '-', '')Remove separators

Trimming Options

FunctionRemoves
TRIM(s)Leading and trailing whitespace
LTRIM(s)Leading whitespace
RTRIM(s)Trailing whitespace
TRIM(LEADING c FROM s)Leading occurrence of c
TRIM(TRAILING c FROM s)Trailing occurrence of c
TRIM(BOTH c FROM s)Both ends

Best Practices

✅ Do This:

-- Use TRIM to clean input
SELECT TRIM(email) FROM users;                               -- ✅

-- Use LOWER for case-insensitive comparison
WHERE LOWER(email) = LOWER(:input)                           -- ✅

-- Use CONCAT for multiple arguments
SELECT CONCAT(first, ' ', last) FROM persons;                -- ✅

-- Use SUBSTRING with FROM for standard syntax
SELECT SUBSTRING(email FROM POSITION('@' IN email) + 1);     -- ✅

-- Use LPAD for fixed-width output
SELECT LPAD(id, 5, '0');                                     -- ✅

-- Use CASE before SUBSTRING to handle short strings
CASE WHEN LENGTH(s) > 10 THEN SUBSTRING(s, 1, 10) END        -- ✅

❌ Don’t Do This:

-- Don't assume || handles NULL like CONCAT
SELECT 'a' || NULL;  -- NULL                                -- ⚠️

-- Don't use LENGTH when CHAR_LENGTH is needed
SELECT LENGTH('café');  -- byte count in some DBs           -- ⚠️

-- Don't forget that SUBSTRING is 1-based
SELECT SUBSTRING('Hello', 0, 1);  -- '' in some DBs         -- ⚠️

-- Don't replace only some occurrences with REPLACE
SELECT REPLACE('a-b-c', '-', '_');  -- replaces all          -- ✅
-- To replace one, use POSITION + SUBSTRING

-- Don't apply SUBSTRING without checking length
SELECT SUBSTRING(s, 1, 100);  -- safe, returns whole string -- ✅
-- SUBSTRING with start beyond length returns empty string

-- Don't mix function names across databases
SELECT LEN(s);  -- SQL Server only                          -- ❌

Common Pitfalls

PitfallWhy It HappensFix
|| returns NULLNULL operandUse CONCAT
LENGTH returns bytesMulti-byte charactersUse CHAR_LENGTH
SUBSTRING returns emptyStart position beyond lengthCheck length first
POSITION returns 0Substring not foundCheck before using in SUBSTRING
Wrong function nameDatabase differenceUse the correct variant
REPLACE too broadReplaces all occurrencesUse POSITION + SUBSTRING
Trailing spaces ignoredLEN in SQL ServerUse DATALENGTH for bytes

Real-World Examples

1. Full Name

SELECT TRIM(CONCAT(first_name, ' ', last_name)) AS full_name
FROM persons;

2. Normalize Email

SELECT LOWER(TRIM(email)) AS email FROM users;

3. Extract Domain

SELECT SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain
FROM users;

4. Initials

SELECT
    UPPER(SUBSTRING(first_name, 1, 1)) ||
    UPPER(SUBSTRING(last_name, 1, 1)) AS initials
FROM persons;

5. Truncate Description

SELECT
    CASE WHEN LENGTH(description) > 100
         THEN SUBSTRING(description, 1, 97) || '...'
         ELSE description
    END AS short_description
FROM products;

6. Fixed-Width ID

SELECT LPAD(customer_id::text, 6, '0') AS padded_id
FROM customers;

7. Remove Prefix

SELECT SUBSTRING(code FROM LENGTH('SKU-') + 1) AS sku
FROM products
WHERE code LIKE 'SKU-%';

8. Replace Separators

SELECT REPLACE(phone, '-', '') AS digits_only FROM contacts;

9. Check Prefix

SELECT * FROM products
WHERE LEFT(code, 4) = 'SKU-';

10. Capitalize First Letter

SELECT UPPER(SUBSTRING(name, 1, 1)) || LOWER(SUBSTRING(name, 2)) AS proper_name
FROM users;

Visual

String Function Flow

┌─────────────────────────────────────────────────────────────┐
│  TYPICAL DATA CLEANING PIPELINE                             │
│                                                             │
│  Raw input: '  ALICE@EXAMPLE.COM  '                        │
│    │                                                        │
│    ▼                                                        │
│  TRIM → 'ALICE@EXAMPLE.COM'                                 │
│    │                                                        │
│    ▼                                                        │
│  LOWER → 'alice@example.com'                                │
│    │                                                        │
│    ▼                                                        │
│  SUBSTRING + POSITION → 'example.com'                       │
│                                                             │
│  Each function transforms the value.                        │
│  They compose left to right.                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

SUBSTRING and POSITION Together

┌─────────────────────────────────────────────────────────────┐
│  EXTRACT DOMAIN FROM EMAIL                                  │
│                                                             │
│  email = 'alice@example.com'                                │
│                                                             │
│  POSITION('@' IN email) → 6                                 │
│                                                             │
│  SUBSTRING(email FROM 6 + 1) → SUBSTRING(email FROM 7)      │
│                                                             │
│  'alice@example.com'                                        │
│   1234567890123456                                          │
│       ↑                                                     │
│       position 6 = '@'                                      │
│       start at 7 = 'e'                                      │
│                                                             │
│  Result: 'example.com'                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Trimming Variants

┌─────────────────────────────────────────────────────────────┐
│  INPUT: '  Hello  '                                         │
│                                                             │
│  TRIM       → 'Hello'                                       │
│  LTRIM      → 'Hello  '                                     │
│  RTRIM      → '  Hello'                                     │
│                                                             │
│  INPUT: '000123'                                            │
│                                                             │
│  TRIM(LEADING '0' FROM '000123') → '123'                    │
│                                                             │
│  INPUT: 'Hello...'                                          │
│                                                             │
│  TRIM(TRAILING '.' FROM 'Hello...') → 'Hello'               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Database Function Names

┌─────────────────────────────────────────────────────────────┐
│  LENGTH                                                     │
│  PostgreSQL: LENGTH, CHAR_LENGTH                            │
│  MySQL:      LENGTH, CHAR_LENGTH                            │
│  SQL Server: LEN                                            │
│  SQLite:     LENGTH                                         │
│                                                             │
│  SUBSTRING                                                  │
│  PostgreSQL: SUBSTRING, SUBSTR                              │
│  MySQL:      SUBSTRING, SUBSTR                              │
│  SQL Server: SUBSTRING                                      │
│  SQLite:     SUBSTR                                         │
│                                                             │
│  POSITION                                                   │
│  PostgreSQL: POSITION, STRPOS                               │
│  MySQL:      INSTR, LOCATE                                  │
│  SQL Server: CHARINDEX                                      │
│  SQLite:     INSTR                                          │
│                                                             │
│  CONCATENATION                                              │
│  PostgreSQL: ||, CONCAT                                     │
│  MySQL:      CONCAT, ||                                     │
│  SQL Server: +, CONCAT                                      │
│  SQLite:     ||                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

FunctionPurposeNotes
CONCATJoin stringsHandles NULL as empty
||Join stringsNULL propagates
LENGTHCharacter countByte count in some DBs
CHAR_LENGTHCharacter countAlways characters
UPPERUppercaseStandard
LOWERLowercaseStandard
INITCAPTitle casePostgreSQL, MySQL
TRIMRemove whitespaceBoth ends
LTRIMRemove leadingLeft side
RTRIMRemove trailingRight side
SUBSTRINGExtract part1-based index
POSITIONFind substringReturns 0 if not found
REPLACESubstituteAll occurrences
LPADPad leftFixed width
RPADPad rightFixed width

Key takeaways:

  • String functions are mostly standard but the names vary. LENGTH is LEN in SQL Server. SUBSTRING is SUBSTR in SQLite and Oracle. POSITION is CHARINDEX in SQL Server and INSTR in MySQL. Knowing the variants is necessary for portable SQL.
  • Concatenation has two forms. The || operator is standard and portable for two arguments. CONCAT is a function that accepts multiple arguments and handles NULLs differently: CONCAT('a', NULL) returns 'a' in most databases, while 'a' || NULL returns NULL.
  • LENGTH may return bytes, not characters. For multi-byte character sets, LENGTH can return the byte count. CHAR_LENGTH or CHARACTER_LENGTH always returns the character count.
  • TRIM has several forms. TRIM removes whitespace from both ends. LTRIM and RTRIM remove from one end. TRIM(LEADING c FROM s) and TRIM(TRAILING c FROM s) remove specific characters.
  • SUBSTRING is 1-based. The first character is at position 1. A start position of 0 is invalid in some databases and produces an empty string in others. A start position beyond the string length produces an empty string.
  • POSITION returns 0 when the substring is not found. This is different from returning NULL. The return value can be used directly in SUBSTRING, but if the substring is not found, the result is unpredictable. Check the position first when the substring may be absent.
  • REPLACE replaces all occurrences. To replace only the first occurrence, combine POSITION and SUBSTRING. This is more verbose but necessary when the replacement should be limited.
  • String functions compose. TRIM(CONCAT(first, ' ', last)) formats a name. SUBSTRING(email FROM POSITION('@' IN email) + 1) extracts a domain. LOWER(TRIM(email)) normalizes an email. The functions are building blocks, and the patterns are built from combinations.

Remember: Strings are the most common data type in real databases, and they are rarely clean. String functions are the tools for normalizing, extracting, formatting, and validating them. The core functions—concatenation, length, case conversion, trimming, substring, position, replace, and padding—cover most of what is needed. The names differ between databases, but the semantics are similar. Learn the standard names, know the common variants, and combine the functions to handle the real tasks: formatting names, extracting domains, normalizing emails, truncating descriptions, and cleaning input. String manipulation is not glamorous, but it is unavoidable, and the functions that do it are the ones used most often.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!