Definition
- CONCAT / || – joins strings together; PostgreSQL and Oracle use
||, MySQL usesCONCAT(). - SUBSTRING / SUBSTR – extracts part of a string by position and length.
- LENGTH / CHAR_LENGTH – returns string length; MySQL's
LENGTHcounts bytes,CHAR_LENGTHcounts characters. - TRIM / LTRIM / RTRIM – removes whitespace or specified characters from the string's edges.
- UPPER / LOWER – converts case;
UPPER('rajesh')returns'RAJESH'. - REPLACE – substitutes all occurrences of a substring;
REPLACE(str, from, to). - LPAD / RPAD – pads a string to a fixed width, commonly used for zero-padding numbers.
When National Bank of India prints a statement, it must format Rajesh's name as "RAJESH KUMAR", mask his account to "XXXX2001", trim stray spaces from imported data, and build display strings like "SAV2001 : Rajesh". String functions are the bank's text toolbox for cleaning and formatting customer data before it reaches any report or screen.
Understanding the Concepts
What are the common SQL string functions?
SQL provides built-in functions for manipulating character data. The core functions cover concatenation, extraction, length, trimming, case conversion, replacement, position finding, padding, and reversal. The exact names and syntax vary across MySQL, PostgreSQL, and Oracle.
-- Quick overview using National Bank of India account data
SELECT
account_id,
UPPER(holder_name) AS name_upper,
TRIM(holder_name) AS name_trimmed,
LENGTH(holder_name) AS name_length,
SUBSTRING(account_id FROM 1 FOR 3) AS account_prefix,
LPAD(CAST(account_number AS VARCHAR), 10, '0') AS padded_number
FROM accounts
WHERE status = 'ACTIVE';
How does concatenation differ across databases?
PostgreSQL and Oracle use the || operator. MySQL uses the CONCAT() function because its || is logical OR by default (unless PIPES_AS_CONCAT is set).
-- PostgreSQL / Oracle: double-pipe operator
SELECT account_id || ' : ' || holder_name AS display_label
FROM accounts;
-- Result: 'SAV2001 : Rajesh Kumar'
-- MySQL: CONCAT function
SELECT CONCAT(account_id, ' : ', holder_name) AS display_label
FROM accounts;
-- Result: 'SAV2001 : Rajesh Kumar'
-- MySQL gotcha: || is logical OR, not concatenation
SELECT 'SAV' || '2001'; -- Returns 0 (logical OR of two strings)
SELECT CONCAT('SAV', '2001'); -- Returns 'SAV2001'
How does SUBSTRING / SUBSTR work?
The SQL standard form is SUBSTRING(str FROM pos FOR len). MySQL and Oracle also accept the shorter SUBSTR(str, pos, len) form.
-- SQL standard (PostgreSQL)
SELECT SUBSTRING(account_id FROM 1 FOR 3) AS prefix
FROM accounts;
-- Result: 'SAV'
-- MySQL / Oracle shorthand
SELECT SUBSTR(account_id, 1, 3) AS prefix
FROM accounts;
-- Result: 'SAV'
-- Account masking: show only the last 4 characters
SELECT 'XXXX' || RIGHT(account_id, 4) AS masked_id
FROM accounts;
-- Result: 'XXXX2001'
What is the difference between LENGTH and CHAR_LENGTH?
In PostgreSQL and Oracle, LENGTH counts characters. In MySQL, LENGTH counts bytes and CHAR_LENGTH counts characters. For multi-byte characters (UTF-8), these can differ.
-- MySQL: LENGTH vs CHAR_LENGTH for multi-byte strings
SELECT LENGTH('Rajesh') AS byte_len,
CHAR_LENGTH('Rajesh') AS char_len;
-- byte_len: 6, char_len: 6 (ASCII, same result)
-- With multi-byte character (Hindi)
SELECT LENGTH('राजेश') AS byte_len,
CHAR_LENGTH('राजेश') AS char_len;
-- byte_len: 15 (3 bytes per character in UTF-8)
-- char_len: 5 (actual character count)
-- PostgreSQL: LENGTH always counts characters
SELECT LENGTH('राजेश'); -- Returns 5
How do TRIM, LTRIM, and RTRIM work?
TRIM removes whitespace (or specified characters) from both ends. LTRIM removes from the left only, RTRIM from the right only.
-- Clean up imported account holder names
SELECT
holder_name AS raw_name,
TRIM(holder_name) AS trimmed,
LTRIM(holder_name) AS left_trimmed,
RTRIM(holder_name) AS right_trimmed
FROM imported_accounts;
-- SQL standard syntax
SELECT TRIM(BOTH ' ' FROM ' Rajesh Kumar ');
-- Result: 'Rajesh Kumar'
SELECT TRIM(LEADING '0' FROM '000042');
-- Result: '42'
How do UPPER, LOWER, and REPLACE work?
UPPER and LOWER convert case. REPLACE substitutes all occurrences of a substring.
-- Normalize holder name for comparison
SELECT UPPER(holder_name) AS normalized_name
FROM accounts
WHERE UPPER(holder_name) = 'RAJESH KUMAR';
-- Replace characters in account display
SELECT REPLACE(account_id, 'SAV', 'SAVINGS-') AS formatted_id
FROM accounts;
-- 'SAV2001' becomes 'SAVINGS-2001'
-- Remove hyphens from imported PAN numbers
SELECT REPLACE(pan_number, '-', '') AS clean_pan
FROM customers;
How do LPAD and RPAD work?
LPAD pads a string on the left to reach a specified length. RPAD pads on the right. These are commonly used for zero-padding reference numbers and formatting fixed-width reports.
-- Zero-pad account numbers to 10 digits
SELECT LPAD(CAST(account_number AS VARCHAR), 10, '0') AS padded
FROM accounts;
-- 2001 becomes '0000002001'
-- Generate a fixed-width transaction reference
SELECT 'TXN' || LPAD(CAST(txn_id AS VARCHAR), 12, '0') AS txn_ref
FROM transactions;
-- TXN000000001234
-- Right-pad branch name for a fixed-width report
SELECT RPAD(branch_name, 20, '.') AS formatted_branch
FROM branches;
-- 'DELHI..............'
How do POSITION, INSTR, LEFT, RIGHT, and REVERSE work?
POSITION finds the location of a substring. INSTR is the MySQL/Oracle equivalent. LEFT and RIGHT extract characters from the start or end. REVERSE reverses the string.
-- POSITION (SQL standard / PostgreSQL)
SELECT POSITION('@' IN email) AS at_position
FROM customers;
-- 'rajesh@nbi.com' returns 7
-- INSTR (MySQL / Oracle)
SELECT INSTR(email, '@') AS at_position
FROM customers;
-- LEFT and RIGHT
SELECT LEFT(account_id, 3) AS prefix, -- 'SAV'
RIGHT(account_id, 4) AS suffix -- '2001'
FROM accounts;
-- Oracle alternative: SUBSTR with negative offset for RIGHT
SELECT SUBSTR(account_id, -4) AS suffix FROM accounts;
-- Returns '2001'
-- REVERSE
SELECT REVERSE('SAV2001'); -- '1002VAS'
What is the performance trap with string functions on indexed columns?
Wrapping an indexed column in a function like UPPER() or TRIM() defeats a plain index because the index stores the original values, not the function output.
-- Wrong: full table scan despite index on holder_name
SELECT * FROM accounts
WHERE UPPER(holder_name) = 'RAJESH KUMAR';
-- The index on holder_name cannot be used
-- Fix 1: Functional index
CREATE INDEX idx_holder_upper ON accounts (UPPER(holder_name));
-- Now UPPER(holder_name) queries use the index
-- Fix 2: Store a normalized column
ALTER TABLE accounts ADD COLUMN holder_name_upper VARCHAR
GENERATED ALWAYS AS (UPPER(holder_name)) STORED;
CREATE INDEX idx_holder_norm ON accounts (holder_name_upper);
-- Fix 3: Case-insensitive collation (PostgreSQL)
-- CREATE COLLATION ci (provider = icu, locale = 'und-u-ks-level2');
-- ALTER TABLE accounts ALTER COLUMN holder_name TYPE VARCHAR COLLATE ci;
Cross-database syntax comparison table
| Function | PostgreSQL | MySQL | Oracle |
|---|---|---|---|
| Concatenation | \|\| | CONCAT() | \|\| |
| Substring | SUBSTRING(s FROM p FOR l) | SUBSTR(s, p, l) | SUBSTR(s, p, l) |
| Length (chars) | LENGTH(s) | CHAR_LENGTH(s) | LENGTH(s) |
| Length (bytes) | OCTET_LENGTH(s) | LENGTH(s) | LENGTHB(s) |
| Position | POSITION(x IN s) | INSTR(s, x) | INSTR(s, x) |
| Left N chars | LEFT(s, n) | LEFT(s, n) | SUBSTR(s, 1, n) |
| Right N chars | RIGHT(s, n) | RIGHT(s, n) | SUBSTR(s, -n) |
package com.bank.service;
import java.sql.*;
import java.util.logging.Logger;
public class AccountDisplayService {
private static final Logger LOG = Logger.getLogger(
AccountDisplayService.class.getName());
private final Connection connection;
public AccountDisplayService(Connection connection) {
this.connection = connection;
}
public String getMaskedDisplay(String accountId) throws SQLException {
String sql = "SELECT 'XXXX' || RIGHT(account_id, 4) || ' : ' "
+ "|| UPPER(TRIM(holder_name)) AS display "
+ "FROM accounts WHERE account_id = ?";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, accountId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return rs.getString("display");
}
}
}
return "UNKNOWN";
}
}
Output:
XXXX2001 : RAJESH KUMAR
Visual Overview
┌──────────────────────────────────────────────────┐
│ SQL String Functions: Quick Reference │
├──────────────────────────────────────────────────┤
│ │
│ Input: ' rajesh kumar ' │
│ │
│ TRIM ────► 'rajesh kumar' │
│ UPPER ────► 'RAJESH KUMAR' │
│ LOWER ────► 'rajesh kumar' │
│ │
│ Input: 'SAV2001' │
│ │
│ LEFT(s,3) ────► 'SAV' │
│ RIGHT(s,4) ────► '2001' │
│ SUBSTRING(1,3)────► 'SAV' │
│ REVERSE ────► '1002VAS' │
│ LPAD(s,10,'0')────► '000SAV2001' │
│ │
│ Concatenation: │
│ PostgreSQL: 'SAV' || '2001' ──► 'SAV2001' │
│ MySQL: CONCAT('SAV','2001') ► 'SAV2001' │
│ │
│ Length: │
│ PostgreSQL: LENGTH('राजेश') = 5 (chars) │
│ MySQL: LENGTH('राजेश') = 15 (bytes) │
│ MySQL: CHAR_LENGTH('राजेश') = 5 (chars) │
│ │
│ Performance Trap: │
│ WHERE UPPER(col) = 'X' ──► defeats plain index │
│ Fix: CREATE INDEX ON (UPPER(col)) │
└──────────────────────────────────────────────────┘
Key Takeaway
The common SQL string functions cover concatenation, substring extraction, length, trimming, case conversion, replacement, position lookup, left and right slicing, padding, and reversal. The key point is that the dialects differ: PostgreSQL and Oracle concatenate with the double-pipe operator while MySQL uses the CONCAT function and treats double-pipe as logical OR by default; Oracle and MySQL offer SUBSTR while the standard is SUBSTRING; and MySQL's LENGTH counts bytes so you use CHAR_LENGTH there for character counts.
At National Bank of India, Rajesh used these functions constantly in the statement service. He normalized messy imported names with UPPER and TRIM, built display labels by concatenating the account id, a separator, and the cleaned name, masked account numbers by prefixing "XXXX" to the rightmost four characters using RIGHT, and zero-padded sequence numbers with LPAD for fixed-width reports.
The performance trap Pankaj always calls out is that wrapping an indexed column in a function, such as filtering on UPPER of the holder name, defeats a normal index. The fix is to either create a functional index on the expression, store a normalized generated column, or use a case-insensitive collation.
The cross-database differences are real and matter in production. When writing portable SQL, stick to the SQL-standard forms where possible, or abstract the differences in your data-access layer.
Remember one sentence: string functions clean, format, and inspect text, but always confirm the dialect because the concatenation operator and the substring and length names genuinely differ across databases.
---