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.