← Back to Main Guide

Part 4: Date/Time, Numeric & Control Flow

Sections 7-9 of the MySQL 8.0 Learning Guide — 44 functions with business examples, code, and visual output tables.

Table of Contents

7
Date/Time Functions
BEGINNER

NOW()

Returns the current date and time as a single DATETIME value. Every row in the same statement gets the same value. Useful for recording when an order was created or when a task was started.

SELECT NOW() AS current_datetime;
current_datetime
2026-02-23 14:35:12
-- Record when an order was placed INSERT INTO orders (customer_id, product_id, order_date) VALUES (1042, 7, NOW());

CURDATE()

Returns today's date without the time portion. Handy for date-only comparisons like "find all employees hired today."

SELECT CURDATE() AS today;
today
2026-02-23
-- Find employees who started today SELECT name, hire_date FROM employees WHERE DATE(hire_date) = CURDATE();
namehire_date
Maria Santos2026-02-23 07:15:00
Jose Reyes2026-02-23 09:42:00

CURTIME()

Returns the current time without the date. Useful for checking shift schedules or whether the office is within business hours.

SELECT CURTIME() AS current_time_now;
current_time_now
14:35:12

DATE(datetime)

Strips the time portion from a DATETIME value, returning only the date. Commonly used in WHERE clauses to compare by date regardless of what time an event occurred.

SELECT order_id, order_date, DATE(order_date) AS date_only FROM orders WHERE order_id IN (501, 502, 503);
order_idorder_datedate_only
5012026-02-22 08:14:332026-02-22
5022026-02-22 11:47:052026-02-22
5032026-02-23 06:30:002026-02-23

TIME(datetime)

Extracts only the time portion from a DATETIME. Useful for analyzing what time of day orders are placed.

SELECT order_id, order_date, TIME(order_date) AS order_time FROM orders WHERE order_id IN (501, 502);
order_idorder_dateorder_time
5012026-02-22 08:14:3308:14:33
5022026-02-22 11:47:0511:47:05

YEAR(date)

Returns the 4-digit year from a date. Useful for annual reports or grouping orders by year.

SELECT YEAR(order_date) AS order_year, COUNT(*) AS total_orders FROM orders GROUP BY YEAR(order_date) ORDER BY order_year;
order_yeartotal_orders
202414320
202515887
20262764

MONTH(date)

Returns the month number (1-12) from a date. Great for monthly summaries of order volume.

SELECT MONTH(order_date) AS month_num, COUNT(*) AS orders FROM orders WHERE YEAR(order_date) = 2026 GROUP BY MONTH(order_date) ORDER BY month_num;
month_numorders
11538
21226

DAY(date)

Returns the day of the month (1-31). Also available as DAYOFMONTH(). Handy for daily breakdowns.

SELECT DAY(created_at) AS day_of_month, COUNT(*) AS tasks_created FROM tasks WHERE MONTH(created_at) = 2 AND YEAR(created_at) = 2026 GROUP BY DAY(created_at) ORDER BY day_of_month LIMIT 5;
day_of_monthtasks_created
167
272
355
461
568

HOUR(time)

Returns the hour (0-23) from a time or datetime value. Perfect for analyzing peak hours at the office.

SELECT HOUR(created_at) AS hour_of_day, COUNT(*) AS tasks FROM tasks WHERE DATE(created_at) = '2026-02-23' GROUP BY HOUR(created_at) ORDER BY hour_of_day LIMIT 5;
hour_of_daytasks
68
722
831
927
1019

MINUTE(time)

Returns the minute (0-59) from a time or datetime value.

SELECT order_id, order_date, MINUTE(order_date) AS minute_part FROM orders WHERE order_id IN (501, 502);
order_idorder_dateminute_part
5012026-02-22 08:14:3314
5022026-02-22 11:47:0547

SECOND(time)

Returns the second (0-59) from a time or datetime value.

SELECT order_id, order_date, SECOND(order_date) AS second_part FROM orders WHERE order_id IN (501, 502);
order_idorder_datesecond_part
5012026-02-22 08:14:3333
5022026-02-22 11:47:055

DATE_ADD(date, INTERVAL n unit)

Adds a time interval to a date. The unit can be DAY, MONTH, YEAR, HOUR, MINUTE, SECOND, and more. This is essential for calculating deadlines, expiry dates, and follow-up schedules.

Adding Days

-- When does the product warranty expire? (warranty = 90 days) SELECT product_name, purchase_date, DATE_ADD(purchase_date, INTERVAL 90 DAY) AS warranty_end FROM products LIMIT 3;
product_namepurchase_datewarranty_end
Office Chair2026-01-102026-04-10
Standing Desk2026-01-252026-04-25
Monitor Arm2026-02-052026-05-06

Adding Months

-- Schedule 6-month performance review for employees SELECT name, last_review, DATE_ADD(last_review, INTERVAL 6 MONTH) AS next_review FROM employees WHERE department = 'Engineering' LIMIT 2;
namelast_reviewnext_review
Ana Cruz2025-12-152026-06-15
Pedro Lim2026-01-202026-07-20

Adding Years

-- Equipment lease expiry (2-year lease) SELECT product_name, lease_start, DATE_ADD(lease_start, INTERVAL 2 YEAR) AS lease_ends FROM products LIMIT 2;
product_namelease_startlease_ends
Dell Server R7402024-06-152026-06-15
HP LaserJet Pro2025-01-102027-01-10

Adding Hours

-- Urgent task deadline (2 hours from creation) SELECT task_id, created_at, DATE_ADD(created_at, INTERVAL 2 HOUR) AS deadline FROM tasks WHERE priority = 'URGENT' LIMIT 2;
task_idcreated_atdeadline
88012026-02-23 09:15:002026-02-23 11:15:00
88052026-02-23 10:42:002026-02-23 12:42:00

DATE_SUB(date, INTERVAL n unit)

Subtracts a time interval from a date. Works exactly like DATE_ADD but in reverse. Great for looking at historical data windows.

-- Find all orders from the last 7 days SELECT order_id, customer_name, order_date FROM orders WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) ORDER BY order_date DESC LIMIT 4;
order_idcustomer_nameorder_date
8810Maria Santos2026-02-23
8807Jose Reyes2026-02-22
8795Ana Cruz2026-02-20
8788Luis Garcia2026-02-18

DATEDIFF(date1, date2)

Returns the number of days between two dates (date1 minus date2). Always returns an integer. Extremely useful for tracking delivery time and order fulfillment speed.

-- How many days between order and shipment? SELECT order_id, product_name, order_date, ship_date, DATEDIFF(ship_date, order_date) AS days_to_ship FROM orders WHERE ship_date IS NOT NULL LIMIT 4;
order_idproduct_nameorder_dateship_datedays_to_ship
8750Laptop Stand2026-02-182026-02-180
8751Ergonomic Keyboard2026-02-182026-02-191
8752Custom Server2026-02-172026-02-225
8753Webcam HD2026-02-192026-02-201

TIMEDIFF(time1, time2)

Returns the difference between two time or datetime values as a TIME value (HH:MM:SS). Unlike DATEDIFF which counts days, this gives you the exact time difference.

SELECT task_id, created_at, completed_at, TIMEDIFF(completed_at, created_at) AS duration FROM tasks LIMIT 3;
task_idcreated_atcompleted_atduration
5012026-02-22 08:14:332026-02-22 08:45:1000:30:37
5022026-02-22 11:47:052026-02-22 12:10:2200:23:17
5032026-02-23 06:30:002026-02-23 07:15:4400:45:44

TIMESTAMPDIFF(unit, start, end)

Returns the integer difference between two datetimes in the specified unit (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND). Unlike TIMEDIFF, you choose what unit you want back.

-- Employee tenure in years, months since hire, and task completion in hours SELECT name, birth_date, TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age_years, hire_date, TIMESTAMPDIFF(MONTH, hire_date, CURDATE()) AS months_employed, task_created, task_completed, TIMESTAMPDIFF(HOUR, task_created, task_completed) AS completion_hours FROM employee_tasks_view LIMIT 3;
namebirth_dateage_yearshire_datemonths_employedtask_createdtask_completedcompletion_hours
Maria Santos1958-07-14672026-02-2002026-02-20 08:00:002026-02-20 10:15:002
Jose Reyes1990-03-22352025-11-1032025-11-10 14:30:002025-11-11 09:00:0018
Ana Cruz2001-12-01242026-01-0512026-01-05 07:45:002026-01-05 08:30:000

DATE_FORMAT(date, format)

Formats a date/datetime value into a string using format specifiers. This is the most flexible way to display dates in reports. Common specifiers: %Y (4-digit year), %m (month 01-12), %d (day 01-31), %M (month name), %W (weekday name), %h (hour 12-hr), %H (hour 24-hr), %i (minute), %s (second), %p (AM/PM).

SELECT order_id, order_date, DATE_FORMAT(order_date, '%Y-%m-%d') AS iso_format, DATE_FORMAT(order_date, '%M %d, %Y') AS long_format, DATE_FORMAT(order_date, '%W') AS day_name, DATE_FORMAT(order_date, '%h:%i %p') AS time_12hr, DATE_FORMAT(order_date, '%d/%m/%Y %H:%i') AS euro_format FROM orders LIMIT 2;
order_idorder_dateiso_formatlong_formatday_nametime_12hreuro_format
88012026-02-23 09:15:002026-02-23February 23, 2026Monday09:15 AM23/02/2026 09:15
88022026-02-23 14:42:302026-02-23February 23, 2026Monday02:42 PM23/02/2026 14:42

STR_TO_DATE(string, format)

The reverse of DATE_FORMAT -- it parses a string into a date using the same format specifiers. Essential when importing data from external systems that use non-standard date formats.

-- Parse dates from an imported CSV SELECT STR_TO_DATE('23-Feb-2026', '%d-%b-%Y') AS parsed_date1, STR_TO_DATE('02/23/2026 09:30 AM', '%m/%d/%Y %h:%i %p') AS parsed_date2, STR_TO_DATE('2026.02.23', '%Y.%m.%d') AS parsed_date3;
parsed_date1parsed_date2parsed_date3
2026-02-232026-02-23 09:30:002026-02-23

LAST_DAY(date)

Returns the last day of the month for the given date. Very handy for month-end reports and billing cycle calculations. It correctly handles February in leap years.

SELECT LAST_DAY('2026-02-15') AS feb_end, LAST_DAY('2026-04-10') AS apr_end, LAST_DAY('2024-02-01') AS leap_feb_end;
feb_endapr_endleap_feb_end
2026-02-282026-04-302024-02-29

DAYNAME(date)

Returns the name of the day of the week (Monday, Tuesday, etc.). Useful for analyzing which days the business is busiest.

SELECT DAYNAME(order_date) AS day_name, COUNT(*) AS total_orders FROM orders WHERE order_date BETWEEN '2026-02-01' AND '2026-02-23' GROUP BY DAYNAME(order_date), DAYOFWEEK(order_date) ORDER BY DAYOFWEEK(order_date);
day_nametotal_orders
Sunday42
Monday198
Tuesday187
Wednesday175
Thursday182
Friday168
Saturday74

DAYOFWEEK(date)

Returns the day-of-week index where 1 = Sunday, 2 = Monday, ..., 7 = Saturday. Useful for numeric sorting of days and filtering weekends.

-- Find orders placed on weekends SELECT order_id, order_date, DAYNAME(order_date) AS day_name, DAYOFWEEK(order_date) AS dow_number FROM orders WHERE DAYOFWEEK(order_date) IN (1, 7) -- Sunday=1, Saturday=7 LIMIT 3;
order_idorder_dateday_namedow_number
87602026-02-21Saturday7
87652026-02-22Sunday1
87682026-02-22Sunday1

DAYOFYEAR(date)

Returns the day of the year (1-366). Useful for sequential numbering in reports and tracking progress through the year.

SELECT DAYOFYEAR('2026-01-01') AS jan1, DAYOFYEAR('2026-02-23') AS today, DAYOFYEAR('2026-12-31') AS dec31;
jan1todaydec31
154365

WEEK(date)

Returns the week number (0-53) for the given date. Useful for weekly reports. The default mode counts Sunday as the first day of the week.

SELECT WEEK('2026-02-23') AS week_num, WEEK('2026-01-01') AS jan1_week, WEEK('2026-12-31') AS dec31_week;
week_numjan1_weekdec31_week
8052
-- Weekly order volume SELECT WEEK(order_date) AS week_num, MIN(order_date) AS week_start, COUNT(*) AS orders FROM orders WHERE YEAR(order_date) = 2026 GROUP BY WEEK(order_date) ORDER BY week_num LIMIT 4;
week_numweek_startorders
02026-01-01185
12026-01-04342
22026-01-11358
32026-01-18371

WEEKDAY(date)

Returns the weekday index where 0 = Monday, 1 = Tuesday, ..., 6 = Sunday. Note this is different from DAYOFWEEK() which starts with 1 = Sunday.

SELECT '2026-02-23' AS the_date, DAYNAME('2026-02-23') AS day_name, WEEKDAY('2026-02-23') AS weekday_idx, DAYOFWEEK('2026-02-23') AS dayofweek_idx;
the_dateday_nameweekday_idxdayofweek_idx
2026-02-23Monday02
Note: WEEKDAY(Monday) = 0, but DAYOFWEEK(Monday) = 2. Keep this difference in mind when writing queries.

EXTRACT(unit FROM date)

Extracts a specific part from a date using the EXTRACT keyword. This is the SQL standard way to pull out date parts. It works with YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, and combined units like YEAR_MONTH.

SELECT order_date, EXTRACT(YEAR FROM order_date) AS yr, EXTRACT(MONTH FROM order_date) AS mo, EXTRACT(DAY FROM order_date) AS dy, EXTRACT(HOUR FROM order_date) AS hr, EXTRACT(YEAR_MONTH FROM order_date) AS yr_mo FROM orders WHERE order_id = 8801;
order_dateyrmodyhryr_mo
2026-02-23 09:15:0020262239202602

UNIX_TIMESTAMP(date)

Converts a date or datetime to a Unix timestamp (seconds since 1970-01-01 00:00:00 UTC). Useful for interfacing with systems that store timestamps as integers, like some external APIs.

SELECT UNIX_TIMESTAMP() AS current_ts, UNIX_TIMESTAMP('2026-02-23 09:15:00') AS specific_ts, UNIX_TIMESTAMP('1970-01-01 00:00:00') AS epoch_zero;
current_tsspecific_tsepoch_zero
177192091217719161000

FROM_UNIXTIME(timestamp)

Converts a Unix timestamp back to a readable DATETIME. The reverse of UNIX_TIMESTAMP(). Useful when reading timestamps from external APIs or third-party integrations.

SELECT FROM_UNIXTIME(1771916100) AS readable_datetime, FROM_UNIXTIME(1771916100, '%M %d, %Y at %h:%i %p') AS formatted;
readable_datetimeformatted
2026-02-23 09:15:00February 23, 2026 at 09:15 AM
8
Numeric/Math Functions
BEGINNER

ROUND(number, decimals)

Rounds a number to the specified number of decimal places. If decimals is omitted, it rounds to 0 decimal places (nearest integer). Uses "round half away from zero" (banker's rounding is NOT the default in MySQL).

-- Rounding product prices for display SELECT product_name, unit_price, ROUND(unit_price) AS rounded_0, ROUND(unit_price, 1) AS rounded_1, ROUND(unit_price, 2) AS rounded_2 FROM products LIMIT 4;
product_nameunit_pricerounded_0rounded_1rounded_2
Wireless Mouse126.847127126.8126.85
USB Hub1.234511.21.23
Monitor Stand14.55001514.614.55
Cable Kit7.891287.97.89

CEIL() / CEILING()

Rounds a number UP to the nearest integer (toward positive infinity). CEIL and CEILING are identical. Useful when you need "at least this many" -- like calculating how many shipping boxes to order.

-- How many boxes to order? (each box holds 100 items) SELECT product_name, monthly_demand, monthly_demand / 100.0 AS exact_boxes, CEIL(monthly_demand / 100.0) AS boxes_to_order FROM product_demand LIMIT 3;
product_namemonthly_demandexact_boxesboxes_to_order
Widget A125012.500013
Widget B8908.90009
Widget C4014.01005

FLOOR()

Rounds a number DOWN to the nearest integer (toward negative infinity). Useful when you want whole completed units only -- like "how many full pallets can we ship?"

-- How many full pallets of 20 can we ship with available inventory? SELECT product_name, stock_qty, FLOOR(stock_qty / 20) AS full_pallets, stock_qty MOD 20 AS remaining FROM inventory LIMIT 3;
product_namestock_qtyfull_palletsremaining
Laptop Stand8747
Desk Lamp4323
Cable Tray19019

ABS()

Returns the absolute (non-negative) value of a number. Useful for calculating how far a value deviates from a target regardless of direction.

-- How far are actual sales from the forecast? SELECT report_id, product_name, actual_sales, forecast, actual_sales - forecast AS deviation, ABS(actual_sales - forecast) AS abs_deviation FROM sales_reports LIMIT 4;
report_idproduct_nameactual_salesforecastdeviationabs_deviation
1001Widget A102.3100.02.32.3
1002Widget A97.1100.0-2.92.9
1003Widget B1.181.20-0.020.02
1004Widget B1.251.200.050.05

MOD(n, m)

Returns the remainder of n divided by m (modulo). Also available as the % operator. Handy for alternating row colors, cycling through assignments, or checking even/odd.

-- Assign support staff in round-robin by task_id SELECT task_id, MOD(task_id, 3) AS staff_slot, CASE MOD(task_id, 3) WHEN 0 THEN 'Alice' WHEN 1 THEN 'Bob' WHEN 2 THEN 'Carol' END AS assigned_to FROM tasks LIMIT 5;
task_idstaff_slotassigned_to
88012Carol
88020Alice
88031Bob
88042Carol
88050Alice

POWER(base, exp) / POW()

Raises a number to a given power. POWER and POW are identical. Used in statistical formulas, compound interest calculations, and analytics.

-- Calculate cost per square meter: price / area^2 SELECT name, salary, years_employed, ROUND(salary * POW(1.03, years_employed), 1) AS projected_salary FROM employees LIMIT 3;
namesalaryyears_employedprojected_salary
Maria Santos68500579420.8
Jose Reyes82000389578.9
Ana Cruz55000156650.0

SQRT()

Returns the square root of a number. Used in statistical calculations like standard deviation in performance analysis.

-- Standard deviation components for sales metrics SELECT product_name, variance_val, SQRT(variance_val) AS std_deviation, ROUND(SQRT(variance_val), 3) AS sd_rounded FROM sales_stats LIMIT 3;
product_namevariance_valstd_deviationsd_rounded
Widget A9.613.13.100
Widget B0.00250.050.050
Widget C0.490.70.700

RAND()

Returns a random floating-point number between 0 (inclusive) and 1 (exclusive). Pass an integer seed for reproducible results. Practical use: selecting random records for audit.

-- Select 3 random orders for quality audit SELECT order_id, customer_name, product_name, total FROM orders WHERE order_date = CURDATE() ORDER BY RAND() LIMIT 3;
order_idcustomer_nameproduct_nametotal
44217Pedro LimDesk Lamp18.50
44203Ana CruzCable Kit6.80
44251Maria SantosUSB Hub4.20
Performance Warning: ORDER BY RAND() on large tables is slow because it generates a random value for every row. For big tables, consider using a subquery with LIMIT on the primary key.

TRUNCATE(number, decimals)

Chops off digits after the specified decimal place WITHOUT rounding. Unlike ROUND, it always drops towards zero.

-- Compare TRUNCATE vs ROUND SELECT 126.847 AS original, ROUND(126.847, 1) AS rounded, TRUNCATE(126.847, 1) AS truncated UNION ALL SELECT 126.851, ROUND(126.851, 1), TRUNCATE(126.851, 1) UNION ALL SELECT -3.678, ROUND(-3.678, 1), TRUNCATE(-3.678, 1);
originalroundedtruncated
126.847126.8126.8
126.851126.9126.8
-3.678-3.7-3.6

GREATEST(val1, val2, ...)

Returns the largest value from a list of arguments. Useful for finding the highest score among multiple evaluations or picking the most recent of several dates.

-- Find the highest of three quarterly revenue figures SELECT project_id, project_name, q1_revenue, q2_revenue, q3_revenue, GREATEST(q1_revenue, q2_revenue, q3_revenue) AS highest FROM projects LIMIT 3;
project_idproject_nameq1_revenueq2_revenueq3_revenuehighest
501Alpha125.3127.1126.0127.1
502Beta4.14.34.24.3
503Gamma140.0139.5141.2141.2

LEAST(val1, val2, ...)

Returns the smallest value from a list of arguments. The counterpart of GREATEST.

SELECT project_id, project_name, q1_revenue, q2_revenue, q3_revenue, LEAST(q1_revenue, q2_revenue, q3_revenue) AS lowest, GREATEST(q1_revenue, q2_revenue, q3_revenue) - LEAST(q1_revenue, q2_revenue, q3_revenue) AS range_spread FROM projects LIMIT 3;
project_idproject_nameq1_revenueq2_revenueq3_revenuelowestrange_spread
501Alpha125.3127.1126.0125.31.8
502Beta4.14.34.24.10.2
503Gamma140.0139.5141.2139.51.7

Rounding Functions Comparison

This side-by-side comparison shows how ROUND, CEIL, FLOOR, and TRUNCATE behave on the same input values. Understanding these differences is critical for reporting accurate financial figures.

Input Value ROUND(x) CEIL(x) FLOOR(x) TRUNCATE(x, 0)
3.74433
3.33433
3.54433
-2.5-3-2-3-2
-2.3-2-2-3-2
-2.7-3-2-3-2
0.91100
-0.100-10
Key Insight: ROUND goes to nearest integer. CEIL always goes UP (toward +infinity). FLOOR always goes DOWN (toward -infinity). TRUNCATE always goes toward ZERO (drops the decimal). Notice how CEIL and FLOOR differ from TRUNCATE on negative numbers!
9
Control Flow
BEGINNER

IF(condition, true_val, false_val)

An inline if-else expression. If the condition is true, returns the second argument; otherwise returns the third. Think of it like a ternary operator. Simple and fast for two-way branching.

-- Flag orders that exceeded budget SELECT employee_name, expense_item, amount, budget_low, budget_high, IF(amount BETWEEN budget_low AND budget_high, 'Within Budget', 'OVER BUDGET') AS flag FROM expenses LIMIT 5;
employee_nameexpense_itemamountbudget_lowbudget_highflag
Maria SantosTravel126.870100OVER BUDGET
Maria SantosSupplies13.212.016.0Within Budget
Jose ReyesSoftware11.54.511.0OVER BUDGET
Ana CruzOffice Meals4.23.55.0Within Budget
Pedro LimShipping0.90.71.3Within Budget

IFNULL(value, default)

Returns the first argument if it is NOT NULL; otherwise returns the second argument. A shorthand for handling missing data -- very common when some tasks are pending or some employee fields are optional.

-- Show 'Pending' for tasks not yet completed SELECT task_id, task_name, IFNULL(status, 'Pending') AS display_status, IFNULL(assigned_to, 'Unassigned') AS assignee FROM tasks LIMIT 4;
task_idtask_namedisplay_statusassignee
8801Update HomepageDoneAlice
8802Fix Login BugIn ProgressBob
8803Deploy v2.1PendingUnassigned
8804Write TestsPendingUnassigned

NULLIF(val1, val2)

Returns NULL if val1 equals val2; otherwise returns val1. The classic use case is preventing division by zero: NULLIF(denominator, 0) turns a zero into NULL so division returns NULL instead of an error.

-- Calculate conversion rate without division-by-zero errors SELECT campaign_name, total_visits, signups, ROUND( signups / NULLIF(total_visits, 0) * 100, 1 ) AS conversion_pct FROM marketing_stats LIMIT 4;
campaign_nametotal_visitssignupsconversion_pct
Spring Sale450327.1
Summer Promo1204537.5
Fall Launch20084.0
Winter Draft00NULL
Without NULLIF: The Winter Draft row would cause a "Division by zero" error. With NULLIF(0, 0), the denominator becomes NULL, and any arithmetic with NULL returns NULL safely.

COALESCE(val1, val2, ...)

Returns the first non-NULL value in the list. More flexible than IFNULL because it accepts any number of arguments. Walk through them left to right and return the first one that is not NULL.

-- Use best available contact: mobile, then home, then work, then 'No Phone' SELECT name, mobile_phone, home_phone, work_phone, COALESCE(mobile_phone, home_phone, work_phone, 'No Phone') AS best_contact FROM employees LIMIT 4;
namemobile_phonehome_phonework_phonebest_contact
Maria Santos555-123-4567555-812-3456NULL555-123-4567
Jose ReyesNULL555-823-4567555-855-0001555-823-4567
Ana CruzNULLNULL555-877-1234555-877-1234
Pedro LimNULLNULLNULLNo Phone

CASE WHEN ... THEN ... ELSE ... END

The most powerful control flow expression in SQL. Evaluates conditions top-to-bottom and returns the value for the first true condition. If none match, returns the ELSE value (or NULL if no ELSE). Think of it as a multi-way IF or a switch statement.

How CASE Evaluates (Top to Bottom)
WHEN condition_1 TRUE? Return result_1
↓ FALSE
WHEN condition_2 TRUE? Return result_2
↓ FALSE
WHEN condition_3 TRUE? Return result_3
↓ FALSE
ELSE → Return default_value
Stops at the first match. Later conditions are never checked once a match is found.
-- Grade order fulfillment time SELECT order_id, product_name, fulfillment_hours, CASE WHEN fulfillment_hours <= 1 THEN 'Excellent' WHEN fulfillment_hours <= 4 THEN 'Acceptable' WHEN fulfillment_hours <= 12 THEN 'Delayed' WHEN fulfillment_hours <= 24 THEN 'Late' ELSE 'Critical Delay' END AS fulfillment_grade FROM orders WHERE fulfillment_hours IS NOT NULL LIMIT 6;
order_idproduct_namefulfillment_hoursfulfillment_grade
8801Laptop Stand0.5Excellent
8802Wireless Mouse1.2Acceptable
8803Ergonomic Keyboard3.8Acceptable
8804Custom Server18.0Late
8805Standing Desk6.5Delayed
8806Office Renovation Kit72.0Critical Delay

CASE with Budget Interpretation

-- Interpret project budget utilization SELECT project_name, budget_used_pct, CASE WHEN budget_used_pct < 50 THEN 'Under-utilized' WHEN budget_used_pct <= 85 THEN 'On Track' WHEN budget_used_pct <= 100 THEN 'Near Limit' ELSE 'Over Budget' END AS interpretation FROM projects LIMIT 5;
project_namebudget_used_pctinterpretation
Alpha142Over Budget
Beta88Near Limit
Gamma110Over Budget
Delta65On Track
Epsilon97Near Limit

CASE with GROUP BY — Pivoting Rows to Columns

One of the most practical uses of CASE is pivoting data: turning row-level categories into separate columns. This technique uses CASE inside aggregate functions (SUM, COUNT) combined with GROUP BY to transform vertical data into a horizontal, spreadsheet-like layout.

-- Pivot: Count tasks by status for each department SELECT department, COUNT(*) AS total, SUM(CASE WHEN status = 'Pending' THEN 1 ELSE 0 END) AS pending, SUM(CASE WHEN status = 'In Progress' THEN 1 ELSE 0 END) AS in_progress, SUM(CASE WHEN status = 'Completed' THEN 1 ELSE 0 END) AS completed, SUM(CASE WHEN status = 'Approved' THEN 1 ELSE 0 END) AS approved FROM tasks WHERE created_at >= CURDATE() GROUP BY department;
departmenttotalpendingin_progresscompletedapproved
Engineering45581220
Marketing6210151720
Operations186732
Sales243489

Pivot: Monthly Order Volume by Category

-- Turn months into columns for a year-over-month view SELECT product_category, SUM(CASE WHEN MONTH(order_date) = 1 THEN 1 ELSE 0 END) AS Jan, SUM(CASE WHEN MONTH(order_date) = 2 THEN 1 ELSE 0 END) AS Feb, SUM(CASE WHEN MONTH(order_date) = 3 THEN 1 ELSE 0 END) AS Mar, SUM(CASE WHEN MONTH(order_date) = 4 THEN 1 ELSE 0 END) AS Apr FROM orders WHERE YEAR(order_date) = 2026 GROUP BY product_category;
product_categoryJanFebMarApr
Electronics41237800
Furniture58952400
Office Supplies14513200
Software20319200
Pivot Pattern: The trick is wrapping CASE inside an aggregate function (SUM or COUNT). Each CASE acts as a filter that only counts rows matching a specific category. GROUP BY then collapses rows, giving you one row per group with each category in its own column.