SQL - UPPER() and LOWER()

UPPER() converts all characters in a string to uppercase, while LOWER() converts them to lowercase. Both functions accept a single string argument and return the transformed result.

Key Insights

  • UPPER() and LOWER() functions convert string case in SQL queries, essential for case-insensitive comparisons and data standardization across all major database systems
  • These functions handle NULL values gracefully, returning NULL rather than throwing errors, but performance degrades on large datasets without proper indexing strategies
  • Case conversion in WHERE clauses prevents index usage; use computed columns, function-based indexes, or case-insensitive collations for production queries

Function Syntax and Basic Usage

UPPER() converts all characters in a string to uppercase, while LOWER() converts them to lowercase. Both functions accept a single string argument and return the transformed result.

-- Basic syntax
SELECT UPPER('hello world');  -- Returns: HELLO WORLD
SELECT LOWER('HELLO WORLD');  -- Returns: hello world

-- With table data
SELECT 
    email,
    LOWER(email) AS normalized_email,
    UPPER(last_name) AS last_name_upper
FROM users;

These functions work identically across PostgreSQL, MySQL, SQL Server, and Oracle. They operate on VARCHAR, CHAR, TEXT, and similar string types.

-- Multiple column transformations
SELECT 
    user_id,
    UPPER(first_name) || ' ' || UPPER(last_name) AS full_name_caps,
    LOWER(email) AS email_lower
FROM customers
WHERE status = 'active';

Case-Insensitive Comparisons

The primary use case for UPPER() and LOWER() is performing case-insensitive searches and comparisons when your database uses case-sensitive collation.

-- Case-insensitive search
SELECT * FROM products
WHERE LOWER(product_name) = LOWER('Widget Pro');

-- Multiple conditions
SELECT customer_id, email, status
FROM customers
WHERE LOWER(email) LIKE LOWER('%@gmail.com%')
  AND UPPER(country_code) = 'US';

For user authentication scenarios where email comparison must be case-insensitive:

-- Login query
SELECT user_id, password_hash, role
FROM users
WHERE LOWER(email) = LOWER(@input_email)
  AND is_active = true;

Be aware that using functions in WHERE clauses prevents index usage. This query scans the entire table:

-- Index on email won't be used
SELECT * FROM users
WHERE LOWER(email) = 'john@example.com';

Data Cleaning and Normalization

Use UPPER() and LOWER() in UPDATE statements to standardize existing data or in INSERT/UPDATE triggers to enforce consistency.

-- Normalize email addresses to lowercase
UPDATE users
SET email = LOWER(email)
WHERE email != LOWER(email);

-- Standardize country codes to uppercase
UPDATE addresses
SET country_code = UPPER(country_code)
WHERE LENGTH(country_code) = 2
  AND country_code != UPPER(country_code);

For ongoing data normalization, implement triggers:

-- PostgreSQL trigger example
CREATE OR REPLACE FUNCTION normalize_user_data()
RETURNS TRIGGER AS $$
BEGIN
    NEW.email := LOWER(NEW.email);
    NEW.username := LOWER(NEW.username);
    NEW.country_code := UPPER(NEW.country_code);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER user_normalization
    BEFORE INSERT OR UPDATE ON users
    FOR EACH ROW
    EXECUTE FUNCTION normalize_user_data();

NULL Handling and Edge Cases

Both functions return NULL when passed NULL input, following SQL’s NULL propagation rules.

SELECT 
    UPPER(NULL) AS upper_null,      -- Returns: NULL
    LOWER(NULL) AS lower_null,      -- Returns: NULL
    COALESCE(UPPER(middle_name), 'N/A') AS middle_name_upper
FROM employees;

Empty strings remain empty strings:

SELECT 
    UPPER('') AS empty_upper,       -- Returns: ''
    LOWER('') AS empty_lower,       -- Returns: ''
    LENGTH(UPPER('')) AS len;       -- Returns: 0

Special characters and numbers pass through unchanged:

SELECT 
    UPPER('abc123!@#') AS result1,  -- Returns: ABC123!@#
    LOWER('XYZ789$%^') AS result2;  -- Returns: xyz789$%^

Performance Optimization Strategies

Function-based indexes allow efficient case-insensitive searches without table scans.

-- PostgreSQL: Create index on lowercase email
CREATE INDEX idx_users_email_lower 
ON users (LOWER(email));

-- Now this query uses the index
SELECT * FROM users
WHERE LOWER(email) = 'john@example.com';

For SQL Server, use computed columns with indexes:

-- Add computed column
ALTER TABLE users
ADD email_lower AS LOWER(email) PERSISTED;

-- Create index on computed column
CREATE INDEX idx_users_email_lower 
ON users (email_lower);

-- Query using computed column
SELECT * FROM users
WHERE email_lower = 'john@example.com';

Alternatively, use case-insensitive collations to avoid functions entirely:

-- PostgreSQL: Use citext extension
CREATE EXTENSION IF NOT EXISTS citext;

ALTER TABLE users 
ALTER COLUMN email TYPE citext;

-- Now direct comparison works case-insensitively
SELECT * FROM users
WHERE email = 'John@Example.com';  -- Finds john@example.com

For MySQL, specify case-insensitive collation:

-- Create table with case-insensitive collation
CREATE TABLE users (
    user_id INT PRIMARY KEY,
    email VARCHAR(255) COLLATE utf8mb4_0900_ai_ci
);

-- Comparison is automatically case-insensitive
SELECT * FROM users
WHERE email = 'JOHN@EXAMPLE.COM';

Combining with Other String Functions

UPPER() and LOWER() integrate seamlessly with other string manipulation functions.

-- Clean and normalize data
SELECT 
    user_id,
    LOWER(TRIM(email)) AS clean_email,
    UPPER(SUBSTRING(country_code, 1, 2)) AS country
FROM user_registrations;

-- Extract and standardize domain
SELECT 
    email,
    LOWER(SUBSTRING(email, POSITION('@' IN email) + 1)) AS domain
FROM users
GROUP BY domain
HAVING COUNT(*) > 100;

Pattern matching with case conversion:

-- Find emails from specific domains (case-insensitive)
SELECT customer_id, email
FROM customers
WHERE LOWER(email) SIMILAR TO '%(gmail|yahoo|hotmail).com'
ORDER BY email;

String aggregation with case standardization:

-- PostgreSQL: Aggregate tags in uppercase
SELECT 
    article_id,
    STRING_AGG(UPPER(tag_name), ', ' ORDER BY tag_name) AS tags
FROM article_tags
GROUP BY article_id;

Database-Specific Considerations

While UPPER() and LOWER() work consistently across databases, locale-specific behavior varies.

-- Turkish 'i' problem
-- In Turkish locale, UPPER('i') = 'İ' (dotted)
-- In English locale, UPPER('i') = 'I' (dotless)

-- PostgreSQL: Specify collation
SELECT UPPER('istanbul' COLLATE "tr_TR");  -- Returns: İSTANBUL
SELECT UPPER('istanbul' COLLATE "en_US");  -- Returns: ISTANBUL

Oracle uses NLS settings:

-- Oracle: Check current NLS settings
SELECT * FROM NLS_SESSION_PARAMETERS
WHERE PARAMETER = 'NLS_SORT';

-- Force specific linguistic sort
SELECT * FROM users
WHERE LOWER(name) = LOWER('Müller')
ORDER BY NLSSORT(name, 'NLS_SORT=GERMAN');

SQL Server handles collations at column level:

-- SQL Server: Case-insensitive comparison
SELECT * FROM users
WHERE email COLLATE Latin1_General_CI_AS = 'John@Example.com';

Practical Production Patterns

Implement case-insensitive unique constraints:

-- PostgreSQL: Unique constraint on lowercase email
CREATE UNIQUE INDEX idx_users_email_unique 
ON users (LOWER(email));

-- Prevent duplicate emails regardless of case
INSERT INTO users (email) VALUES ('John@Example.com');
-- This will fail:
-- INSERT INTO users (email) VALUES ('john@example.com');

Search optimization with materialized views:

-- Create materialized view for frequent searches
CREATE MATERIALIZED VIEW user_search AS
SELECT 
    user_id,
    LOWER(first_name) AS first_name_lower,
    LOWER(last_name) AS last_name_lower,
    LOWER(email) AS email_lower
FROM users;

CREATE INDEX idx_user_search_email ON user_search (email_lower);

-- Fast case-insensitive search
SELECT * FROM user_search
WHERE email_lower = 'john@example.com';

These functions are fundamental SQL tools, but production systems require careful consideration of indexing, collation, and locale-specific behavior to maintain performance at scale.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.