Combining Functions
Progress Tracking
Log in to save this lesson and continue from where you left off.
Putting It All Together
Here’s the truth about SQL interviews and real work: they rarely test one concept in isolation. You’ll get a messy dataset with timestamps in different formats, product names that need cleaning, and a request to “show me monthly trends by category.” That requires combining everything we’ve covered.
In this lesson, you’ll apply date extraction, string manipulation, and the CASE WHEN patterns from Module 2 to solve realistic problems.
We covered CASE WHEN in Module 2 (Lesson 2.5). Here we’ll combine it with date and string functions. If you need a refresher, the syntax is below.
Quick CASE WHEN Refresher
Two forms, same purpose — conditional logic inside SQL:
-- Simple CASE (compare column to specific values)
CASE column
WHEN 'value1' THEN 'result1'
WHEN 'value2' THEN 'result2'
ELSE 'default'
END
-- Searched CASE (any boolean conditions)
CASE
WHEN condition1 THEN 'result1'
WHEN condition2 THEN 'result2'
ELSE 'default'
ENDCASE WHEN with Date Functions
Combine conditional logic with date extraction for business categorization:
| worker_id | first_name | last_name | salary | joining_date | department |
|---|---|---|---|---|---|
| 1 | Monika | Arora | 100000 | 2014-02-20 | HR |
| 2 | Niharika | Verma | 80000 | 2014-06-11 | Admin |
| 3 | Vishal | Singhal | 300000 | 2014-02-20 | HR |
| 4 | Amitah | Singh | 500000 | 2014-02-20 | Admin |
| 5 | Vivek | Bhati | 500000 | 2014-06-11 | Admin |
Categorize each employee by hire quarter (Q1-Q4 based on month) and day type (Weekend vs Weekday based on day of week).
Weekend vs Weekday Analysis
Product teams love this analysis — is user behavior different on weekends?
-- Classify activity by day type (PostgreSQL)
SELECT
DATE_TRUNC('day', event_timestamp) AS event_date,
CASE
WHEN EXTRACT(DOW FROM event_timestamp) IN (0, 6) THEN 'Weekend'
ELSE 'Weekday'
END AS day_type,
COUNT(*) AS event_count
FROM events
GROUP BY 1, 2;Remember from Lesson 5.1: DOW numbering varies by dialect. PostgreSQL: 0=Sunday, 6=Saturday. MySQL’s DAYOFWEEK(): 1=Sunday, 7=Saturday. Always verify with your database.
When using CASE WHEN with GROUP BY, you must repeat the full CASE expression in the GROUP BY clause (or use a column alias number like GROUP BY 1, 2).
CASE WHEN with String Functions
Use CASE with string functions to categorize based on text patterns:
-- Categorize customers by email provider
SELECT
customer_id,
email,
CASE
WHEN email LIKE '%@gmail.com' THEN 'Gmail'
WHEN email LIKE '%@yahoo.com' THEN 'Yahoo'
WHEN email LIKE '%@outlook.com' THEN 'Outlook'
ELSE 'Other'
END AS email_provider
FROM customers;Cleaning Inconsistent Data
Real data has ‘HR’, ‘hr’, ‘Human Resources’, and ‘ HR ’ all meaning the same thing:
-- Standardize messy department names
SELECT
employee_id,
CASE LOWER(TRIM(department))
WHEN 'hr' THEN 'Human Resources'
WHEN 'human resources' THEN 'Human Resources'
WHEN 'admin' THEN 'Administration'
WHEN 'administration' THEN 'Administration'
ELSE INITCAP(TRIM(department))
END AS clean_department
FROM employees;INITCAP (title case) exists in PostgreSQL and Oracle, but not MySQL or MSSQL. For those dialects, you’ll need UPPER(LEFT(s,1)) + LOWER(SUBSTRING(s,2)) or handle it in application code.
Calculate days employed and categorize: under 365 days = New, under 1095 days = Established, else Veteran. Sort by days descending.
Type Casting and Conversion
Sometimes you need to convert between types. PostgreSQL’s :: syntax is the shortest:
Convert joining_date to text and extract the year as an integer. Use CAST() for cross-dialect portability.
Dialect Comparison: Type Casting
- Cast syntax:
- PostgreSQL uses
::typeorCAST() - MySQL uses
CAST() - MSSQL uses
CAST()orCONVERT() - Oracle uses
CAST()orTO_DATE()
- PostgreSQL uses
- To date:
- PostgreSQL uses
::DATE - MySQL uses
CAST(DATE) - MSSQL uses
CAST(DATE) - Oracle uses
TO_DATE( 'YYYY-MM-DD')
- PostgreSQL uses
- To integer:
- PostgreSQL uses
'123'::INTEGER - MySQL uses
CAST('123' AS SIGNED) - MSSQL uses
CAST('123' AS INT) - Oracle uses
TO_NUMBER('123')
- PostgreSQL uses
Complex Real-World Examples
Let’s tackle some scenarios that combine multiple techniques.
Project Budget Analysis
Calculate project risk based on budget runway:
SELECT
project_name,
budget,
end_date - CURRENT_DATE AS days_remaining,
CASE
WHEN budget / NULLIF(end_date - CURRENT_DATE, 0) > 1000 THEN 'High Burn'
WHEN budget / NULLIF(end_date - CURRENT_DATE, 0) > 500 THEN 'Medium Burn'
ELSE 'On Track'
END AS budget_status
FROM projects
WHERE end_date > CURRENT_DATE;NULLIF(x, 0) returns NULL if x equals 0. This prevents division-by-zero errors — a classic production bug. NULLIF(days, 0) means “treat zero days as NULL, which makes the division return NULL instead of crashing.”
Risky Projects
Combine date arithmetic to calculate remaining time, then use CASE WHEN to categorize project risk.
| id | title | budget | start_date | end_date |
|---|---|---|---|---|
| 1 | Project1 | 29498 | 2018-08-31 | 2019-03-13 |
| 2 | Project2 | 32487 | 2018-01-27 | 2018-12-13 |
| 3 | Project3 | 43909 | 2019-11-05 | 2019-12-09 |
| 4 | Project4 | 15776 | 2018-06-28 | 2018-11-20 |
| 5 | Project5 | 36268 | 2019-03-13 | 2020-01-02 |
| emp_id | project_id |
|---|---|
| 10592 | 1 |
| 10593 | 2 |
| 10594 | 3 |
| 10595 | 4 |
| 10596 | 5 |
| id | first_name | last_name | salary |
|---|---|---|---|
| 10592 | Jennifer | Roberts | 20204 |
| 10593 | Haley | Ho | 33154 |
| 10594 | Eric | Mccarthy | 32360 |
| 10595 | Gina | Martinez | 46388 |
| 10596 | Jason | Fields | 12348 |
You are given a set of projects and employee data. Each project has a name, a budget, and a specific duration, while each employee has an annual salary and may be assigned to one or more projects for particular periods. The task is to identify which projects are overbudget. A project is considered overbudget if the prorated cost of all employees assigned to it exceeds the project’s budget. To solve this, you must prorate each employee's annual salary based on the exact period they work on a given project, relative to a full year. For example, if an employee works on a six-month project, only half of their annual salary should be attributed to that project. Sum these prorated salary amounts for all employees assigned to a project and compare the total with the project’s budget. Your output should be a list of overbudget projects, where each entry includes the project’s name, its budget, and the total prorated employee expenses for that project. The total expenses should be rounded up to the nearest dollar. Assume all years have 365 days and disregard leap years.
Year-Over-Year Comparison
Tag products by how recently they launched:
SELECT
product_id,
product_name,
launch_year,
CASE
WHEN launch_year = EXTRACT(YEAR FROM CURRENT_DATE) THEN 'New This Year'
WHEN launch_year = EXTRACT(YEAR FROM CURRENT_DATE) - 1 THEN 'Last Year'
ELSE 'Catalog'
END AS product_recency
FROM products;Acceptance Rate Calculation
A common metric pattern — combine date casting with ratio calculation:
SELECT
CAST(sent_at AS DATE) AS sent_date,
COUNT(CASE WHEN action = 'accepted' THEN 1 END)::DECIMAL
/ NULLIF(COUNT(*), 0) AS acceptance_rate
FROM friend_requests
GROUP BY CAST(sent_at AS DATE)
ORDER BY sent_date;Note the pattern: COUNT(CASE WHEN condition THEN 1 END) counts only matching rows. Dividing by COUNT(*) gives you the ratio. NULLIF prevents errors if a day has zero requests.
Acceptance Rate By Date
Cast timestamps to dates, then calculate the acceptance ratio using conditional counting.
| user_id_sender | user_id_receiver | date | action |
|---|---|---|---|
| ad4943sdz | 948ksx123d | 2020-01-04 | sent |
| ad4943sdz | 948ksx123d | 2020-01-06 | accepted |
| dfdfxf9483 | 9djjjd9283 | 2020-01-04 | sent |
| dfdfxf9483 | 9djjjd9283 | 2020-01-15 | accepted |
| ffdfff4234234 | lpjzjdi4949 | 2020-01-06 | sent |
Calculate the friend acceptance rate for each `date` when friend requests were sent. A request is sent if `action` = `sent` and accepted if `action` = `accepted`. If a request is not accepted, there is no record of it being accepted in the table. The output will only include dates where requests were sent and at least one of them was accepted (acceptance can occur on any date after the request is sent).
Pattern: CTEs for Readability
When your query needs multiple transformation steps, CTEs make it maintainable:
Build a 3-step CTE: 1) clean names and department with TRIM/LOWER, 2) add a tenure band (New/Mid/Senior) and hire_year, 3) count employees by department and tenure band.
We use CTEs religiously in production. Each CTE is testable independently — you can SELECT * FROM cleaned to verify step 1 before adding step 2. This makes debugging much easier than nested subqueries.
Key Takeaways
CASE WHENcombines withEXTRACTfor date-based categorization (quarters, weekends, recency)- Use
CASEwithLOWER/TRIMto standardize messy text before categorizing - Type casting converts between
DATE,TEXT,INTEGER— useCAST()for portability NULLIF(x, 0)prevents division by zero — a production essentialCTEs separate cleaning, categorization, and aggregation for maintainable queries- Real problems combine multiple function types — practice these patterns
Module Complete
You’ve finished Module 5. You can now extract and manipulate dates across dialects, clean and transform text data, split delimited strings, use regex for pattern matching, and combine everything with conditional logic.
These are the daily tools of production SQL. Dates and strings are messy in the real world, and now you know how to handle them.
Next up: Module 6 covers Window Functions — the most powerful (and most interview-relevant) SQL feature. Get ready for ROW_NUMBER, LAG/LEAD, running totals, and the patterns that separate junior from senior analysts.