PostgreSQL Expressions
Overview
Expressions are combinations of values, operators, and functions that PostgreSQL evaluates to compute a result. Expressions can be used in SELECT, WHERE, HAVING, and other clauses.
Expression Types
1. Constant Expressions
sql
-- Numeric constants
SELECT 42;
SELECT 3.14159;
SELECT 1.5e10;
-- String constants
SELECT 'Hello, World!';
SELECT 'It''s a nice day'; -- Escape single quote
-- Boolean constants
SELECT TRUE;
SELECT FALSE;
SELECT NULL;
-- Date/Time constants
SELECT DATE '2024-01-15';
SELECT TIME '14:30:00';
SELECT TIMESTAMP '2024-01-15 14:30:00';
SELECT INTERVAL '1 day';2. Column Reference Expressions
sql
-- Simple column reference
SELECT name FROM users;
SELECT price FROM products;
-- Column reference with table name
SELECT users.name FROM users;
SELECT products.price FROM products;
-- Column reference with table alias
SELECT u.name, o.order_number
FROM users u
JOIN orders o ON u.id = o.user_id;3. Arithmetic Expressions
sql
-- Basic arithmetic
SELECT 10 + 5 AS addition;
SELECT 10 - 5 AS subtraction;
SELECT 10 * 5 AS multiplication;
SELECT 10 / 5 AS division;
SELECT 10 % 3 AS modulo;
-- Complex arithmetic
SELECT price * quantity AS subtotal FROM order_items;
SELECT (price * quantity) * (1 + tax_rate) AS total FROM orders;
SELECT ROUND(price * 0.9, 2) AS discounted_price FROM products;
-- Use parentheses to control precedence
SELECT (10 + 5) * 2 AS result; -- 30
SELECT 10 + (5 * 2) AS result; -- 204. String Expressions
sql
-- String concatenation
SELECT 'Hello' || ' ' || 'World' AS greeting;
SELECT first_name || ' ' || last_name AS full_name FROM users;
-- Using CONCAT function
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
SELECT CONCAT('Order #', order_id, ': ', status) AS order_info FROM orders;
-- String functions
SELECT UPPER(name) AS uppercase_name FROM users;
SELECT LOWER(email) AS lowercase_email FROM users;
SELECT LENGTH(name) AS name_length FROM users;
SELECT SUBSTRING(name, 1, 5) AS short_name FROM users;
SELECT TRIM(' Hello ') AS trimmed;
SELECT REPLACE(description, 'old', 'new') AS replaced FROM products;5. Comparison Expressions
sql
-- Simple comparisons
SELECT * FROM products WHERE price > 100;
SELECT * FROM users WHERE age >= 18;
SELECT * FROM orders WHERE status = 'completed';
-- Comparison results as columns
SELECT
name,
price,
price > 100 AS is_expensive,
stock = 0 AS out_of_stock
FROM products;
-- Complex comparisons
SELECT * FROM products
WHERE price BETWEEN 50 AND 200
AND category IN ('Electronics', 'Computers');6. Logical Expressions
sql
-- AND expression
SELECT * FROM users
WHERE status = 'active' AND country = 'USA';
-- OR expression
SELECT * FROM products
WHERE category = 'Electronics' OR category = 'Books';
-- NOT expression
SELECT * FROM users WHERE NOT status = 'banned';
-- Combined logical expressions
SELECT * FROM products
WHERE (category = 'Electronics' OR category = 'Computers')
AND price < 500
AND NOT discontinued = true;Conditional Expressions
CASE Expressions
Simple CASE Expression
sql
-- Basic syntax
SELECT
name,
status,
CASE status
WHEN 'active' THEN 'Active User'
WHEN 'pending' THEN 'Pending Approval'
WHEN 'suspended' THEN 'Suspended'
ELSE 'Unknown'
END AS status_description
FROM users;
-- Practical application
SELECT
product_name,
category,
CASE category
WHEN 'Electronics' THEN 'Tech'
WHEN 'Books' THEN 'Literature'
WHEN 'Clothing' THEN 'Fashion'
ELSE 'Other'
END AS category_group
FROM products;Searched CASE Expression
sql
-- Basic syntax
SELECT
name,
age,
CASE
WHEN age < 18 THEN 'Minor'
WHEN age >= 18 AND age < 65 THEN 'Adult'
ELSE 'Senior'
END AS age_group
FROM users;
-- Price tiers
SELECT
product_name,
price,
CASE
WHEN price < 50 THEN 'Budget'
WHEN price >= 50 AND price < 200 THEN 'Mid-range'
WHEN price >= 200 AND price < 1000 THEN 'Premium'
ELSE 'Luxury'
END AS price_tier
FROM products;
-- Order priority
SELECT
order_id,
total_amount,
customer_type,
CASE
WHEN customer_type = 'VIP' THEN 1
WHEN total_amount > 1000 THEN 2
WHEN total_amount > 500 THEN 3
ELSE 4
END AS priority
FROM orders;Nested CASE Expressions
sql
SELECT
product_name,
category,
price,
stock,
CASE
WHEN stock = 0 THEN 'Out of Stock'
WHEN stock < 10 THEN
CASE
WHEN price > 500 THEN 'Low Stock - High Value'
ELSE 'Low Stock - Regular'
END
ELSE 'In Stock'
END AS stock_status
FROM products;COALESCE Expression
sql
-- Return first non-NULL value
SELECT COALESCE(phone, 'No phone') AS contact_phone FROM users;
-- Multiple fallback values
SELECT COALESCE(mobile, home_phone, work_phone, 'No contact') AS phone FROM users;
-- Practical application
SELECT
name,
COALESCE(email, 'no-email@example.com') AS email,
COALESCE(phone, 'N/A') AS phone,
COALESCE(address, 'No address provided') AS address
FROM users;
-- Use in calculations
SELECT
product_name,
price,
COALESCE(discount_price, price) AS final_price,
price - COALESCE(discount_price, price) AS savings
FROM products;NULLIF Expression
sql
-- Return NULL if two values are equal
SELECT NULLIF(price, 0) FROM products;
SELECT NULLIF(email, '') FROM users;
-- Avoid division by zero
SELECT
total_sales,
total_orders,
total_sales / NULLIF(total_orders, 0) AS average_order_value
FROM sales_summary;
-- Practical application
SELECT
product_name,
old_price,
new_price,
NULLIF(old_price, new_price) AS price_changed
FROM products;GREATEST and LEAST Expressions
sql
-- GREATEST returns maximum value
SELECT GREATEST(10, 20, 15) AS max_value; -- 20
SELECT GREATEST(price, cost, 0) AS highest FROM products;
-- LEAST returns minimum value
SELECT LEAST(10, 20, 15) AS min_value; -- 10
SELECT LEAST(stock, reorder_level) AS min_stock FROM products;
-- Practical application
SELECT
product_name,
price,
competitor_price,
LEAST(price, competitor_price) AS best_price,
GREATEST(price, competitor_price) - LEAST(price, competitor_price) AS price_difference
FROM products;Aggregate Expressions
sql
-- Basic aggregates
SELECT COUNT(*) AS total_users FROM users;
SELECT SUM(amount) AS total_sales FROM orders;
SELECT AVG(price) AS average_price FROM products;
SELECT MIN(price) AS min_price, MAX(price) AS max_price FROM products;
-- Conditional aggregates
SELECT
COUNT(*) AS total_orders,
COUNT(*) FILTER (WHERE status = 'completed') AS completed_orders,
SUM(total_amount) AS total_revenue,
SUM(total_amount) FILTER (WHERE status = 'completed') AS completed_revenue
FROM orders;
-- Grouped aggregates
SELECT
category,
COUNT(*) AS product_count,
AVG(price) AS avg_price,
MIN(price) AS min_price,
MAX(price) AS max_price
FROM products
GROUP BY category;
-- String aggregation
SELECT
category,
STRING_AGG(product_name, ', ' ORDER BY product_name) AS products
FROM products
GROUP BY category;Type Cast Expressions
sql
-- Using :: operator
SELECT '100'::INTEGER AS int_value;
SELECT '2024-01-15'::DATE AS date_value;
SELECT NOW()::DATE AS today;
SELECT 10::NUMERIC(10, 2) AS decimal_value;
-- Using CAST function
SELECT CAST('100' AS INTEGER) AS int_value;
SELECT CAST('2024-01-15' AS DATE) AS date_value;
SELECT CAST(price AS TEXT) AS price_text FROM products;
-- Practical application
SELECT
order_id,
order_date::DATE AS date_only,
total_amount::INTEGER AS rounded_amount,
CAST(customer_id AS TEXT) AS customer_id_text
FROM orders;Array Expressions
sql
-- Array construction
SELECT ARRAY[1, 2, 3, 4, 5] AS numbers;
SELECT ARRAY['apple', 'banana', 'orange'] AS fruits;
-- Array operations
SELECT ARRAY[1,2,3] || ARRAY[4,5,6] AS combined; -- {1,2,3,4,5,6}
SELECT ARRAY[1,2,3] || 4 AS appended; -- {1,2,3,4}
-- Array access
SELECT tags[1] AS first_tag FROM products;
SELECT tags[1:3] AS first_three_tags FROM products;
-- Array functions
SELECT array_length(tags, 1) AS tag_count FROM products;
SELECT unnest(ARRAY[1,2,3]) AS value;Subquery Expressions
Scalar Subqueries
sql
-- Subquery returning single value
SELECT
name,
price,
(SELECT AVG(price) FROM products) AS avg_price,
price - (SELECT AVG(price) FROM products) AS price_diff
FROM products;
-- Correlated subquery
SELECT
name,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS order_count
FROM users;EXISTS Subqueries
sql
-- Check if exists
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
);
-- NOT EXISTS
SELECT * FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi
WHERE oi.product_id = p.id
);IN Subqueries
sql
-- Value in subquery results
SELECT * FROM products
WHERE category_id IN (
SELECT id FROM categories WHERE active = true
);
-- NOT IN
SELECT * FROM users
WHERE id NOT IN (
SELECT DISTINCT user_id FROM orders
);ANY/ALL Subqueries
sql
-- ANY: satisfies any one
SELECT * FROM products
WHERE price > ANY (
SELECT price FROM products WHERE category = 'Books'
);
-- ALL: satisfies all
SELECT * FROM products
WHERE price > ALL (
SELECT price FROM products WHERE category = 'Books'
);Window Expressions
sql
-- ROW_NUMBER
SELECT
name,
price,
ROW_NUMBER() OVER (ORDER BY price DESC) AS price_rank
FROM products;
-- RANK and DENSE_RANK
SELECT
name,
category,
price,
RANK() OVER (PARTITION BY category ORDER BY price DESC) AS rank,
DENSE_RANK() OVER (PARTITION BY category ORDER BY price DESC) AS dense_rank
FROM products;
-- Aggregate window functions
SELECT
order_date,
total_amount,
SUM(total_amount) OVER (ORDER BY order_date) AS running_total,
AVG(total_amount) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg
FROM orders;Practical Examples
Example 1: E-commerce Product Display
sql
SELECT
product_name,
price,
CASE
WHEN price < 50 THEN '💰 Great Value'
WHEN price < 200 THEN '💵 Good Deal'
ELSE '💎 Premium'
END AS price_badge,
COALESCE(discount_price, price) AS final_price,
ROUND((price - COALESCE(discount_price, price)) / NULLIF(price, 0) * 100, 0) AS discount_percent,
stock > 0 AS in_stock,
CASE
WHEN stock = 0 THEN 'Out of Stock'
WHEN stock < 10 THEN 'Only ' || stock || ' left'
ELSE 'In Stock'
END AS stock_message
FROM products
WHERE NOT discontinued = true;Example 2: User Statistics Report
sql
SELECT
first_name || ' ' || last_name AS full_name,
email,
EXTRACT(YEAR FROM AGE(birth_date)) AS age,
CASE
WHEN EXTRACT(YEAR FROM AGE(birth_date)) < 18 THEN 'Minor'
WHEN EXTRACT(YEAR FROM AGE(birth_date)) BETWEEN 18 AND 35 THEN 'Young Adult'
WHEN EXTRACT(YEAR FROM AGE(birth_date)) BETWEEN 36 AND 60 THEN 'Middle Age'
ELSE 'Senior'
END AS age_group,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS total_orders,
COALESCE((SELECT SUM(total_amount) FROM orders WHERE user_id = users.id), 0) AS lifetime_value,
CASE
WHEN (SELECT SUM(total_amount) FROM orders WHERE user_id = users.id) > 10000 THEN 'VIP'
WHEN (SELECT SUM(total_amount) FROM orders WHERE user_id = users.id) > 5000 THEN 'Gold'
WHEN (SELECT SUM(total_amount) FROM orders WHERE user_id = users.id) > 1000 THEN 'Silver'
ELSE 'Bronze'
END AS customer_tier
FROM users
WHERE status = 'active';Example 3: Order Analysis
sql
SELECT
order_id,
customer_id,
order_date,
total_amount,
total_amount * 0.08 AS tax,
total_amount * 1.08 AS total_with_tax,
CASE
WHEN total_amount > 1000 THEN total_amount * 0.1
WHEN total_amount > 500 THEN total_amount * 0.05
ELSE 0
END AS discount,
total_amount * 1.08 - CASE
WHEN total_amount > 1000 THEN total_amount * 0.1
WHEN total_amount > 500 THEN total_amount * 0.05
ELSE 0
END AS final_total,
EXTRACT(DAY FROM CURRENT_DATE - order_date) AS days_since_order,
CASE
WHEN EXTRACT(DAY FROM CURRENT_DATE - order_date) > 30 THEN 'Over 30 days'
WHEN EXTRACT(DAY FROM CURRENT_DATE - order_date) > 7 THEN '7-30 days'
ELSE 'Last 7 days'
END AS order_age
FROM orders
WHERE status IN ('completed', 'shipped');Best Practices
- Use parentheses for clarity: Use parentheses in complex expressions to clarify precedence
- Avoid excessive nesting: Deeply nested expressions are hard to maintain
- Use meaningful aliases: Provide clear aliases for complex expressions
- Handle NULL properly: Use COALESCE or NULLIF to handle NULL values
- Consider performance: Complex expressions may impact query performance
- Match types: Ensure data types are compatible in expressions
Summary
PostgreSQL expressions are fundamental to building powerful queries:
- Constants and column references: Basic building blocks
- Arithmetic and string expressions: Data calculation and transformation
- Conditional expressions: CASE, COALESCE, NULLIF
- Aggregate expressions: Data summarization
- Subquery expressions: Complex data retrieval
- Window expressions: Advanced analytics
Mastering expressions enables you to write more flexible and powerful SQL queries.