Module 5: Date, Time & Text Functions45 min

Combining Functions

Progress Tracking

Log in to save this lesson and continue from where you left off.

Log in

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.

CASE WHEN refresher

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:

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'
END

CASE WHEN with Date Functions

Combine conditional logic with date extraction for business categorization:

Table: worker
worker_idfirst_namelast_namesalaryjoining_datedepartment
1MonikaArora1000002014-02-20HR
2NiharikaVerma800002014-06-11Admin
3VishalSinghal3000002014-02-20HR
4AmitahSingh5000002014-02-20Admin
5VivekBhati5000002014-06-11Admin
1
Try Quarter and Day-Type Labels

Categorize each employee by hire quarter (Q1-Q4 based on month) and day type (Weekend vs Weekday based on day of week).

Tables: worker

Weekend vs Weekday Analysis

Product teams love this analysis — is user behavior different on weekends?

PostgreSQL
-- 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;
DOW numbering and GROUP BY

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.

CASE WHEN in GROUP BY

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:

PostgreSQL
-- 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:

PostgreSQL
-- 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 availability

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.

2
Try Tenure Buckets

Calculate days employed and categorize: under 365 days = New, under 1095 days = Established, else Veteran. Sort by days descending.

Tables: worker

Type Casting and Conversion

Sometimes you need to convert between types. PostgreSQL’s :: syntax is the shortest:

3
Try Type Casting

Convert joining_date to text and extract the year as an integer. Use CAST() for cross-dialect portability.

Tables: worker

Dialect Comparison: Type Casting

  • Cast syntax:
    • PostgreSQL uses ::type or CAST()
    • MySQL uses CAST()
    • MSSQL uses CAST() or CONVERT()
    • Oracle uses CAST() or TO_DATE()
  • To date:
    • PostgreSQL uses ::DATE
    • MySQL uses CAST(DATE)
    • MSSQL uses CAST(DATE)
    • Oracle uses TO_DATE( 'YYYY-MM-DD')
  • To integer:
    • PostgreSQL uses '123'::INTEGER
    • MySQL uses CAST('123' AS SIGNED)
    • MSSQL uses CAST('123' AS INT)
    • Oracle uses TO_NUMBER('123')

Complex Real-World Examples

Let’s tackle some scenarios that combine multiple techniques.

Project Budget Analysis

Calculate project risk based on budget runway:

PostgreSQL
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 prevents division by zero

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.

Table: linkedin_projects
idtitlebudgetstart_dateend_date
1Project1294982018-08-312019-03-13
2Project2324872018-01-272018-12-13
3Project3439092019-11-052019-12-09
4Project4157762018-06-282018-11-20
5Project5362682019-03-132020-01-02
Table: linkedin_emp_projects
emp_idproject_id
105921
105932
105943
105954
105965
Table: linkedin_employees
idfirst_namelast_namesalary
10592JenniferRoberts20204
10593HaleyHo33154
10594EricMccarthy32360
10595GinaMartinez46388
10596JasonFields12348
4
Risky Projects
View solution

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.

Tables: linkedin_projects, linkedin_emp_projects, linkedin_employees

Year-Over-Year Comparison

Tag products by how recently they launched:

PostgreSQL
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:

PostgreSQL
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.

Table: fb_friend_requests
user_id_senderuser_id_receiverdateaction
ad4943sdz948ksx123d2020-01-04sent
ad4943sdz948ksx123d2020-01-06accepted
dfdfxf94839djjjd92832020-01-04sent
dfdfxf94839djjjd92832020-01-15accepted
ffdfff4234234lpjzjdi49492020-01-06sent
5
Acceptance Rate By Date
View solution

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).

Tables: fb_friend_requests

Pattern: CTEs for Readability

When your query needs multiple transformation steps, CTEs make it maintainable:

6
Try a CTE Pipeline

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.

Tables: worker
CTEs make debugging easy

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 WHEN combines with EXTRACT for date-based categorization (quarters, weekends, recency)
  • Use CASE with LOWER/TRIM to standardize messy text before categorizing
  • Type casting converts between DATE, TEXT, INTEGER — use CAST() for portability
  • NULLIF(x, 0) prevents division by zero — a production essential
  • CTEs 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.