CAST(expr AS type) is a function that converts a value to a different data type. It is essential when fixing data that arrives with every column as TEXT—through CSV imports or external integrations—into the correct types. In PostgreSQL you can also use the shorthand notation expr::type.
CAST('42' AS INTEGER) -- '42' → 42 (integer) CAST('9.99' AS NUMERIC) -- '9.99' → 9.99 (decimal) CAST('2024-05-01' AS DATE) -- '2024-05-01'→ date type '42'::INTEGER -- PostgreSQL shorthand (equivalent)
'10' < '2' (lexicographic order). Date arithmetic such as '2024-05-01' + 7 also errors out. Proper type conversion enables arithmetic, date operations, and correct sorting.Every column in the raw_imports table was imported as TEXT. Convert id to INTEGER, amount to NUMERIC, and registered_at to DATE, and add a tax-included amount at a 10% tax rate (amount_with_tax). Output in ascending order of id.
| id | amount | registered_at |
|---|---|---|
| '1' | '8000' | '2024-05-01' |
| '2' | '12500' | '2024-05-15' |
| '3' | '3200' | '2024-06-02' |
Expected output (type conversion + tax calculation):
| id | amount | registered_at | amount_with_tax |
|---|---|---|---|
| 1 | 8000 | 2024-05-01 | 8800 |
| 2 | 12500 | 2024-05-15 | 13750 |
| 3 | 3200 | 2024-06-02 | 3520 |
amount_with_tax = CAST(amount AS NUMERIC) * 1.1. Multiplying TEXT by 1.1 directly errors out (or relies on implicit casting).
SELECT CAST(id AS INTEGER) AS id, -- TEXT '1','2','3' → integer CAST(amount AS NUMERIC) AS amount, -- TEXT → numeric (now arithmetic-capable) CAST(registered_at AS DATE) AS registered_at, -- TEXT → date type CAST(amount AS NUMERIC) * 1.1 AS amount_with_tax -- Arithmetic after CAST FROM raw_imports ORDER BY id; -- Ascending as integers (as TEXT, '10'<'2' — lexicographic order) /* Logical execution order: 1. FROM raw_imports → Read all 3 rows as TEXT 2. SELECT CAST(id AS INTEGER) → '1','2','3' 3. ORDER BY id → Sort ascending as integers */
LEGEND
① FROM — Source data (all TEXT)
FROM raw_importsEvery column imported from the CSV is TEXT. As is, neither numeric nor date operations are possible. Use CAST to convert to the right types.| id (TEXT) | amount (TEXT) | registered_at (TEXT) |
|---|---|---|
| '1' | '8000' | '2024-05-01' |
| '2' | '12500' | '2024-05-15' |
| '3' | '3200' | '2024-06-02' |
LEGEND
① FROM
FROM raw_importsData where every column is TEXT. What happens when we sort amount as is?| id | amount (TEXT) |
|---|---|
| '1' | '8000' |
| '2' | '12500' |
| '3' | '3200' |
CAST(col AS INTEGER) is standard SQL syntax and works on any DBMS. col::INTEGER is PostgreSQL-specific shorthand that reads well but does not work on MySQL or SQL Server. Choose based on your team's DBMS.'10' < '2' holds (comparison starts from the first character). Always CAST numeric data to NUMERIC/INTEGER before ORDER BY.CAST('abc' AS INTEGER) errors out. To return NULL when conversion fails, use PostgreSQL patterns such as CASE WHEN col ~ '^\d+$' THEN col::INTEGER END or TO_NUMBER.'2024-05-01' + INTERVAL '7 days' may work, but behavior varies by DBMS and type combination. Writing an explicit CAST clarifies intent and improves portability and readability.WHERE registered_at > '2024-05-01' (TEXT vs. TEXT) becomes a lexicographic comparison. Always store dates as DATE/TIMESTAMP, and use CAST only for conversion at data-ingestion time.ALTER TABLE ... ALTER COLUMN ... TYPE, but existing data must be re-validated.COALESCE(val1, val2, ...) evaluates its arguments left to right and returns the first non-NULL value. It is the most basic function for setting a substitute value for NULL. NULLIF(val1, val2) is the inverse: it returns NULL when val1 and val2 are equal (converting a specific value to NULL).
COALESCE(NULL, 'Not set') -- → 'Not set' (replaces NULL) COALESCE('Tanaka', 'Not set') -- → 'Tanaka' (non-NULL kept as is) COALESCE(NULL, NULL, 0) -- → 0 (multiple candidates allowed) NULLIF(0, 0) -- → NULL (0=0, so converted to NULL) NULLIF(42, 0) -- → 42 (42≠0, so kept as is)
total / NULLIF(count, 0) returns NULL for rows where count is 0, preventing divide-by-zero errors. This is a common pattern in aggregation queries.The users table has rows with NULL in nickname and score. Using COALESCE, produce (1) a display name (nickname if present, otherwise name) and (2) a score (0 when NULL). Also add a score_nullif column that uses NULLIF to convert a score of 0 back to NULL.
| user_id | name | nickname | score |
|---|---|---|---|
| 1 | Tanaka Taro | Tanaka | 85 |
| 2 | Sato Hanako | NULL | 92 |
| 3 | Suzuki Ichiro | Suzuki | 0 |
| 4 | Yamada Jiro | NULL | NULL |
Expected output:
| user_id | display_name | score | score_nullif |
|---|---|---|---|
| 1 | Tanaka | 85 | 85 |
| 2 | Sato Hanako | 92 | 92 |
| 3 | Suzuki | 0 | NULL |
| 4 | Yamada Jiro | 0 | NULL |
Row 3: score=0 → NULLIF(0,0)=NULL. Row 4: score=NULL → COALESCE(NULL,0)=0 → NULLIF(0,0)=NULL.
SELECT user_id, COALESCE(nickname, name) AS display_name, -- Use name when nickname is NULL COALESCE(score, 0) AS score, -- Use 0 when score is NULL NULLIF(score, 0) AS score_nullif -- Convert score=0 to NULL (inverse of COALESCE) FROM users ORDER BY user_id; /* Logical execution order: 1. FROM users → Read all 4 rows 2. COALESCE(nickname, name) → Evaluate left to right, return the first non-NULL value Tanaka (non-NULL) → Tanaka NULL, Sato Hanako → Sato Hanako (nickname is NULL, so name is used) Suzuki (non-NULL) → Suzuki NULL, Yamada Jiro → Yamada Jiro (same as above) 3. COALESCE(score, 0) → 0 when NULL, otherwise unchanged (0 is not NULL) 4. NULLIF(score, 0) → NULL when score=0, otherwise return score unchanged 5. ORDER BY user_id → Output in ascending user_id */
LEGEND
① FROM — Source data with NULLs
FROM usersnickname and score contain a mix of NULLs. Aggregating with NULL as is can produce unintended results in SUM/AVG.| user_id | name | nickname | score |
|---|---|---|---|
| 1 | Tanaka Taro | Tanaka | 85 |
| 2 | Sato Hanako | NULL | 92 |
| 3 | Suzuki Ichiro | Suzuki | 0 |
| 4 | Yamada Jiro | NULL | NULL |
LEGEND
① FROM
FROM usersSome rows have NULL nickname.| user_id | nickname |
|---|---|
| 1 | Tanaka |
| 2 | NULL |
| 3 | Suzuki |
| 4 | NULL |
COALESCE(nickname, display_name, name, 'No Name'). The first non-NULL from the left wins. This lets you write a fallback chain naturally.SUM(amount) / NULLIF(COUNT(*), 0), a group whose count is 0 returns NULL (preventing a divide-by-zero error). It appears often in aggregation queries.WHERE nickname = NULL is always FALSE. Always use IS NULL / IS NOT NULL to detect NULL.The SQL CASE expression branches per row and returns different values. There are two ways to write it.
-- (1) Simple CASE (direct value comparison) CASE status WHEN 'completed' THEN 'Completed' WHEN 'pending' THEN 'Pending' ELSE 'Other' -- Without ELSE, NULL is returned END -- (2) Searched CASE (arbitrary conditions) CASE WHEN amount >= 10000 THEN 'high' WHEN amount >= 5000 THEN 'medium' -- Evaluated top to bottom; returns the THEN of the first TRUE ELSE 'low' END
Add a status_label column that converts status (English codes) in the orders table into readable labels, and an amount_rank column that ranks by the size of amount (10000 or more: 'high', 5000 or more: 'medium', otherwise: 'low').
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 2 | 12500 | pending |
| 103 | 3 | 3200 | cancelled |
| 104 | 1 | 6700 | completed |
| 105 | 4 | 450 | pending |
Expected output:
| order_id | user_id | amount | status_label | amount_rank |
|---|---|---|---|---|
| 101 | 1 | 8000 | Completed | medium |
| 102 | 2 | 12500 | Pending | high |
| 103 | 3 | 3200 | Cancelled | low |
| 104 | 1 | 6700 | Completed | medium |
| 105 | 4 | 450 | Pending | low |
SELECT order_id, user_id, amount, CASE status -- Simple CASE: direct value comparison WHEN 'completed' THEN 'Completed' WHEN 'pending' THEN 'Pending' WHEN 'cancelled' THEN 'Cancelled' ELSE 'Unknown' -- Unexpected values become 'Unknown' END AS status_label, CASE -- Searched CASE: decide by range conditions WHEN amount >= 10000 THEN 'high' WHEN amount >= 5000 THEN 'medium' -- Evaluated only for rows where the WHEN above is FALSE ELSE 'low' END AS amount_rank FROM orders ORDER BY order_id; /* Logical execution order: 1. FROM orders → Read all 5 rows 2. CASE status WHEN ... END → Compare each row's status against WHENs top to bottom and convert 3. CASE WHEN amount >= ... END → Evaluate each row's amount top to bottom and rank 4. ORDER BY order_id → Output in ascending order_id */
LEGEND
① FROM — Read source data
FROM ordersstatus is an English code and amount is a raw integer. For display and analysis, use CASE WHEN to convert them into human-readable forms.| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 2 | 12500 | pending |
| 103 | 3 | 3200 | cancelled |
| 104 | 1 | 6700 | completed |
| 105 | 4 | 450 | pending |
LEGEND
① FROM
FROM ordersSuppose we want each status's total amount laid out side by side per user_id.| user_id | status | amount |
|---|---|---|
| 1 | completed | 8000 |
| 2 | pending | 12500 |
| 3 | cancelled | 3200 |
| 1 | completed | 6700 |
| 4 | pending | 450 |
ORDER BY CASE status WHEN 'completed' THEN 1 WHEN 'pending' THEN 2 ELSE 3 END, and you can also use it in WHERE and GROUP BY clauses.SUM(CASE WHEN status='completed' THEN amount ELSE 0 END) AS completed_total, lets you produce a "pivot aggregation" that lays out per-status totals side by side. It is a common pattern in practice.ELSE 'Unknown' or ELSE NULL explicitly.WHEN amount >= 5000 THEN 'medium' first, 12500 also gets classified as 'medium.' In a searched CASE, always write the higher (stricter) condition first.IF() and Oracle's DECODE() do not work in PostgreSQL. Using standard SQL CASE WHEN improves portability.User input and external data frequently contain mixed case and leading/trailing spaces. Let's cover the main string-normalization functions.
TRIM(' hello ') -- → 'hello' Removes leading/trailing spaces LTRIM(' hello ') -- → 'hello ' Removes left side only RTRIM(' hello ') -- → ' hello' Removes right side only LOWER('Hello@WORLD.COM') -- → 'hello@world.com' UPPER('hello') -- → 'HELLO' LENGTH('hello') -- → 5 (character count)
LOWER(TRIM(email)), functions apply from the inside out. You can write "(1) TRIM to remove whitespace → (2) LOWER to lowercase" in a single line.The user_inputs table has email and username values with inconsistent case and leading/trailing spaces. Retrieve the following columns. email: (1) after TRIM (email_trimmed), (2) TRIM + lowercase (email_normalized), (3) character count after TRIM (email_length); username: (1) TRIM (username_trimmed), (2) TRIM + uppercase (username_upper).
| id | username | |
|---|---|---|
| 1 | ' Alice@Example.COM ' | 'alice_dev' |
| 2 | 'BOB@GMAIL.COM' | ' Bob Smith ' |
| 3 | 'carol@test.org ' | 'CAROL' |
Expected output:
| id | email_trimmed | email_normalized | email_length | username_trimmed | username_upper |
|---|---|---|---|---|---|
| 1 | Alice@Example.COM | alice@example.com | 17 | alice_dev | ALICE_DEV |
| 2 | BOB@GMAIL.COM | bob@gmail.com | 13 | Bob Smith | BOB SMITH |
| 3 | carol@test.org | carol@test.org | 14 | CAROL | CAROL |
SELECT id, TRIM(email) AS email_trimmed, -- Remove leading/trailing spaces LOWER(TRIM(email)) AS email_normalized, -- (1) TRIM removes whitespace → (2) LOWER lowercases LENGTH(TRIM(email)) AS email_length, -- Character count after TRIM TRIM(username) AS username_trimmed, -- Remove leading/trailing spaces UPPER(TRIM(username)) AS username_upper -- Uppercase after TRIM FROM user_inputs ORDER BY id; /* Logical execution order: 1. FROM user_inputs → Read all 3 rows (leading/trailing spaces and mixed case intact) 2. TRIM(email) → Remove leading/trailing spaces from each row's email 3. LOWER(TRIM(email)) → Lowercase the TRIM result (evaluated inside out) 4. LENGTH(TRIM(email)) → Count characters after TRIM 5. TRIM(username), UPPER(...) → Convert username the same way 6. ORDER BY id → Output in ascending id */
LEGEND
① FROM — Data with spaces and mixed case
FROM user_inputsemail has leading/trailing spaces and mixed case, and username also has leading/trailing spaces and all-caps values. Normalization is needed before storing in the DB or at SELECT time.| id | email (original) | username (original) |
|---|---|---|
| 1 | ' Alice@Example.COM ' | 'alice_dev' |
| 2 | 'BOB@GMAIL.COM' | ' Bob Smith ' |
| 3 | 'carol@test.org ' | 'CAROL' |
LEGEND
① FROM
FROM user_inputsData with leading/trailing spaces.| email (original) |
|---|
| ' Alice@Example.COM ' |
| 'BOB@GMAIL.COM' |
| 'carol@test.org ' |
ILIKE operator is case-insensitive pattern matching. You can use it as WHERE email ILIKE '%@example.com'. However, indexing LOWER(email) is often faster on large data sets.TRIM(BOTH ' ' FROM col) is the full form, but TRIM(col) is equivalent. To remove a specific character, specify it as in TRIM(BOTH 'x' FROM 'xxxhelloxxx') → 'hello'.TRIM(LOWER(email)) and LOWER(TRIM(email)) give the same result, but generally "TRIM to clean up first, then convert" makes the intent clearer.LENGTH(email) returns a count that includes leading/trailing spaces. When you want the count after normalization, always use LENGTH(TRIM(email)).WHERE email = 'alice@example.com' does not match ' Alice@Example.COM '. It's important to normalize both the input and stored values with LOWER(TRIM(...)) before comparing.There are two ways to concatenate strings: the || operator (SQL standard) and the CONCAT function. The key difference is their behavior toward NULL.
'Tanaka' || ' ' || 'Taro' -- → 'Tanaka Taro' (SQL standard) CONCAT('Tanaka', ' ', 'Taro') -- → 'Tanaka Taro' (function version) CONCAT(NULL, 'Taro') -- → 'Taro' (treats NULL as empty string) NULL || 'Taro' -- → NULL (NULL propagates) LPAD('7', 4, '0') -- → '0007' (pad the left with '0') RPAD('ABC', 6, '-') -- → 'ABC---' (pad the right with '-')
CONCAT or COALESCE(col, '').From the customers table, retrieve full_name using last_name || ' ' || first_name, a CONCAT-based full_name_v2, full_address using pref || city, and a customer_label that includes a zero-padded 4-digit customer number via LPAD (e.g., 'Customer No. 0001').
| customer_id | last_name | first_name | pref | city |
|---|---|---|---|---|
| 1 | Tanaka | Taro | Tokyo | Shibuya |
| 2 | Sato | Hanako | Osaka | Kita |
| 3 | Suzuki | Ichiro | Aichi | Naka |
Expected output:
| customer_id | full_name | full_name_v2 | full_address | customer_label |
|---|---|---|---|---|
| 1 | Tanaka Taro | Tanaka Taro | TokyoShibuya | Customer No. 0001 |
| 2 | Sato Hanako | Sato Hanako | OsakaKita | Customer No. 0002 |
| 3 | Suzuki Ichiro | Suzuki Ichiro | AichiNaka | Customer No. 0003 |
SELECT customer_id, last_name || ' ' || first_name AS full_name, -- Concatenate strings with || (SQL standard) CONCAT(last_name, ' ', first_name) AS full_name_v2, -- CONCAT function version (NULL-safe) pref || city AS full_address, -- Prefecture + city 'Customer No. ' || LPAD(customer_id::TEXT, 4, '0') AS customer_label -- Cast to ::TEXT, then zero-pad FROM customers ORDER BY customer_id; /* Logical execution order: 1. FROM customers → Read all 3 rows 2. last_name || ' ' || first_name → Concatenate strings in order with || 3. CONCAT(last_name, ' ', first_name) → Function version (treats NULL as empty string) 4. pref || city → Concatenate prefecture and city 5. LPAD(customer_id::TEXT, 4, '0') → (1) cast integer to TEXT (2) pad left with '0' to 4 digits 'Customer No. ' || ... → Concatenate with the label string 6. ORDER BY customer_id → Output in ascending customer_id */
LEGEND
① FROM — Read source data
FROM customersThe name is split into last_name/first_name. It needs to be concatenated for app display.| customer_id | last_name | first_name | pref | city |
|---|---|---|---|---|
| 1 | Tanaka | Taro | Tokyo | Shibuya |
| 2 | Sato | Hanako | Osaka | Kita |
| 3 | Suzuki | Ichiro | Aichi | Naka |
LEGEND
① Virtual data
Virtual data (middle_name is NULL)Suppose there is customer data with no middle name (NULL).| first | middle | last |
|---|---|---|
| 'Taro' | NULL | 'Tanaka' |
CONCAT(first_name, COALESCE(' ' || middle_name, ''), ' ', last_name), or use CONCAT.LPAD(order_id::TEXT, 8, '0') can generate fixed-length codes like 'ORD00000001'. It appears often in report output, barcode generation, and file-name numbering.customer_id::TEXT or CAST(customer_id AS TEXT) before passing them to || or LPAD. Concatenating with mismatched types errors out.'prefix_' || NULL || '_suffix' becomes NULL. When first_name or middle_name can be NULL, use COALESCE(col, '') or CONCAT.42 || ' items' errors out (PostgreSQL). Type conversion is needed, as in 42::TEXT || ' items' or CONCAT(42, ' items').