LPAD / RPAD / CONCAT_WS — Generate Report Codes with Zero Padding and Fixed-Width Formatting
These functions pad strings to a fixed length and concatenate strings safely when NULL values are present.
LPAD('5', 6, '0') -- → '000005' Pad on the left with '0' to six characters LPAD('ABCDEFGH', 6, '0') -- → 'ABCDEF' Truncate the right side when input exceeds the length RPAD('FOOD', 8, '.') -- → 'FOOD....' Pad on the right with '.' to eight characters UPPER('tokyo') -- → 'TOKYO' Convert all letters to uppercase CONCAT_WS('-', '2024', 'TKY', '000005') -- → '2024-TKY-000005' Join with a separator CONCAT_WS('-', '2024', NULL, '000005') -- → '2024-000005' Skip NULL ✓ CONCAT('2024', '-', NULL, '-', '000005') -- → '2024--000005' Ignore NULL but duplicate separators ✗
CONCAT_WS(separator, a, b, c) automatically skips NULL arguments and inserts the separator only between the remaining values. PostgreSQL's regular CONCAT also ignores NULL, but passing separators as separate arguments can produce duplicates such as '2024--000005'. Real-world data often contains NULL values, so prefer CONCAT_WS for separator-delimited concatenation.Generate the following columns from the invoices table.
① padded_seq: a six-digit, zero-padded sequence code from the integer seq_no
② invoice_no: a report number in the form {fiscal_year}-{UPPER(branch_code)}-{padded_seq}
③ display_label: customer_name padded on the right with dots to 16 characters.
| invoice_id | fiscal_year | branch_code | seq_no | customer_name |
|---|---|---|---|---|
| 1 | 2024 | tky | 5 | Alpine Cafe |
| 2 | 2024 | osk | 123 | Tech Corp |
| 3 | 2024 | tky | 42 | Garden Shop |
| 4 | 2025 | ngy | 1 | Book World |
| invoice_id | padded_seq | invoice_no | display_label |
|---|---|---|---|
| 1 | 000005 | 2024-TKY-000005 | Alpine Cafe..... |
| 2 | 000123 | 2024-OSK-000123 | Tech Corp....... |
| 3 | 000042 | 2024-TKY-000042 | Garden Shop..... |
| 4 | 000001 | 2025-NGY-000001 | Book World...... |
AGE / INTERVAL / NOW() — Calculate Expiration, Days Remaining, and Renewal Dates in Real Time
These functions calculate intervals relative to the current date and time.
DATE '2024-12-15' -- Reproducible fixed reference date (DATE) NOW() -- → '2024-12-15 09:30+09' Current date and time (TIMESTAMP WITH TIME ZONE) -- Subtract DATE values → INTEGER (number of days) '2025-01-14'::DATE - '2024-12-15'::DATE -- → 30 (integer total days) -- AGE → INTERVAL (human-readable relative representation) AGE('2025-11-30'::DATE, '2024-12-15'::DATE) -- → '11 mons 15 days' (not a total of 350 days!) EXTRACT(DAY FROM AGE(...)) -- → 15 ← day component only, not total days -- Add or subtract INTERVAL to calculate future or past dates '2025-01-14'::DATE + INTERVAL '1 year' -- → '2026-01-14 00:00:00' NOW() - INTERVAL '30 days' -- → timestamp 30 days ago
AGE('2025-11-30', '2024-12-15') returns the INTERVAL '11 mons 15 days'. Applying EXTRACT(DAY FROM ...) returns 15, the day component only, not the total of 350 days. Use expires_at::DATE - DATE '2024-12-15' when you need total days as an integer.Using DATE '2024-12-15' as the reference, calculate the following from the subscriptions table (the example output uses 2024-12-15).
① days_remaining: days until expiration, negative if already expired
② status: one of 'Expired', 'Expiring Soon' (within 30 days), or 'Active'
③ renewal_at: the next scheduled renewal date (current expiration date + one year) in 'YYYY-MM-DD' format.
Sort the result by days_remaining ascending.
| user_id | plan | started_at | expires_at |
|---|---|---|---|
| 1 | basic | 2024-01-15 | 2025-01-14 |
| 2 | pro | 2023-06-01 | 2024-07-31 |
| 3 | premium | 2024-11-01 | 2025-11-30 |
| 4 | basic | 2024-03-01 | 2024-12-31 |
| user_id | plan | days_remaining | status | renewal_at |
|---|---|---|---|---|
| 2 | pro | -137 | Expired | 2025-07-31 |
| 4 | basic | 16 | Expiring Soon | 2025-12-31 |
| 1 | basic | 30 | Expiring Soon | 2026-01-14 |
| 3 | premium | 350 | Active | 2026-11-30 |
CASE WHEN + ROUND + GREATEST — Apply Tiered Discounts and a Price Floor in One Query
This pattern combines multi-condition branching, lower and upper bounds, and a CTE (Common Table Expression).
-- Simple CASE form for equality: CASE value WHEN comparison THEN result CASE customer_rank WHEN 'gold' THEN 0.10 -- When customer_rank = 'gold' WHEN 'silver' THEN 0.05 ELSE 0.00 -- When nothing matches; make ELSE explicit END GREATEST(405, 500) -- → 500 Maximum argument, used as a lower bound GREATEST(NULL, 500) -- → 500 NULL is ignored in PostgreSQL LEAST(12000, 10000) -- → 10000 Minimum argument, used as an upper bound
Calculate prices from the orders table after applying a tiered discount based on customer rank.
Discount rates: platinum=20% / gold=10% / silver=5% / bronze=0%
① discount_rate: the rate for the customer's rank
② discounted: the discounted amount, rounded to an integer with ROUND
③ final_price: the final price clamped to a minimum of 500.
Use a CTE (WITH clause) to avoid repeating the CASE WHEN expression.
| order_id | customer_id | customer_rank | subtotal |
|---|---|---|---|
| 1 | 101 | platinum | 15000 |
| 2 | 102 | gold | 8500 |
| 3 | 103 | silver | 3200 |
| 4 | 104 | bronze | 980 |
| 5 | 105 | gold | 450 |
| order_id | customer_rank | subtotal | discount_rate | discounted | final_price |
|---|---|---|---|---|---|
| 1 | platinum | 15000 | 0.20 | 12000 | 12000 |
| 2 | gold | 8500 | 0.10 | 7650 | 7650 |
| 3 | silver | 3200 | 0.05 | 3040 | 3040 |
| 4 | bronze | 980 | 0.00 | 980 | 980 |
| 5 | gold | 450 | 0.10 | 405 | 500 |
FILTER + STRING_AGG(DISTINCT) — Conditional Aggregation with Deduplication
This pattern combines FILTER, which applies a condition to an aggregate, with deduplicated string aggregation.
-- FILTER: apply a WHERE-like condition inside an aggregate without changing group size COUNT(*) FILTER (WHERE status = 'completed') -- → count completed rows only SUM(amount) FILTER (WHERE status = 'completed') -- → sum completed amounts only -- STRING_AGG(DISTINCT ...): concatenate strings without duplicates STRING_AGG(DISTINCT product_name, ', ' ORDER BY product_name) -- → include each product name once in lexical order -- Note: ORDER BY can use only the same expression as the DISTINCT argument -- Multiply by 100.0 to use NUMERIC rather than integer division 100.0 * COUNT(*) FILTER (...) / COUNT(*) -- → 66.7 (NUMERIC division) 100 * COUNT(*) FILTER (...) / COUNT(*) -- → 66 (integer division truncates the fraction)
Aggregate the following values by category from the sales table.
① total_sales: all rows, regardless of status
② completed_count: rows where status = 'completed', using FILTER
③ completed_amount: total amount of completed transactions, using FILTER
④ completion_rate: completion percentage, rounded to one decimal place
⑤ products: a deduplicated, lexically sorted product-name list using STRING_AGG(DISTINCT).
Sort the result by category ascending.
| sale_id | category | product_name | amount | status |
|---|---|---|---|---|
| 1 | food | Coffee | 500 | completed |
| 2 | food | Sandwich | 800 | completed |
| 3 | food | Coffee | 500 | cancelled |
| 4 | tech | Mouse | 3000 | completed |
| 5 | tech | Keyboard | 8500 | completed |
| 6 | tech | Mouse | 3000 | refunded |
| 7 | books | SQL Guide | 1500 | completed |
| 8 | books | Design Book | 2200 | cancelled |
| category | total_sales | completed_count | completed_amount | completion_rate | products |
|---|---|---|---|---|---|
| books | 2 | 1 | 1500 | 50.0 | Design Book, SQL Guide |
| food | 3 | 2 | 1300 | 66.7 | Coffee, Sandwich |
| tech | 3 | 2 | 11500 | 66.7 | Keyboard, Mouse |
REGEXP_MATCHES / REGEXP_REPLACE — Extract and Transform Data from Structured Log Lines
This is an advanced pattern for extracting and reformatting data with regular-expression capture groups ().
-- REGEXP_MATCHES: return matched groups as an array (setof text[]) REGEXP_MATCHES('ERROR 404 /api/users', '(\w+) (\d+) (.*)') -- → {'ERROR','404','/api/users'} each group becomes an array element -- Extract each group with one-based array indexes (REGEXP_MATCHES(log, '(\w+) (\d+) (.*)'))[1] -- → 'ERROR' first group (REGEXP_MATCHES(log, '(\w+) (\d+) (.*)'))[2] -- → '404' second group -- REGEXP_REPLACE: reformat by referencing capture groups REGEXP_REPLACE('20240315', '^(\d{4})(\d{2})(\d{2})$', '\1-\2-\3') -- → '2024-03-15' \1=year, \2=month, \3=day -- Match a pattern with ~, which returns BOOLEAN 'ERROR 404 /api' ~ '^(ERROR|WARN)' -- → true (case-sensitive) 'error 404 /api' ~* '^(error|warn)' -- → true (~* is case-insensitive)
(). REGEXP_MATCHES returns no row for a nonmatch, so use LEFT JOIN, CASE, or the singular REGEXP_MATCH when nonmatching input must be retained.The app_logs table stores log messages as unstructured strings. Parse them with regular expressions and generate the following columns.
① log_level: log level (ERROR, WARN, or INFO)
② status_code: numeric HTTP status code
③ endpoint: API endpoint path
④ formatted_date: transform log_date from 'YYYYMMDD' to 'YYYY-MM-DD'
⑤ is_error: true when the log level is ERROR or WARN, using the ~ operator.
Sort the result by log_id ascending.
| log_id | log_date | log_message |
|---|---|---|
| 1 | 20240315 | ERROR 500 /api/users |
| 2 | 20240315 | INFO 200 /api/health |
| 3 | 20240316 | WARN 429 /api/orders |
| 4 | 20240316 | ERROR 404 /api/products |
| 5 | 20240317 | INFO 200 /api/users |
| log_id | log_level | status_code | endpoint | formatted_date | is_error |
|---|---|---|---|---|---|
| 1 | ERROR | 500 | /api/users | 2024-03-15 | true |
| 2 | INFO | 200 | /api/health | 2024-03-15 | false |
| 3 | WARN | 429 | /api/orders | 2024-03-16 | true |
| 4 | ERROR | 404 | /api/products | 2024-03-16 | true |
| 5 | INFO | 200 | /api/users | 2024-03-17 | false |