Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
SQL — Module 42Intermediate
Date and Time Functions
Extract parts, calculate differences, truncate to periods, format for display, handle timezones — every temporal operation for time-series analytics and reporting
SQL has four primary temporal data types. Understanding which type a column uses determines which functions apply and how arithmetic works.
Type
Stores
Example
Use for
DATE
Calendar date only — no time
2024-01-15
Order dates, birthdays, hire dates
TIME
Time of day only — no date
14:30:00
Store hours, shift times
TIMESTAMP
Date + time, no timezone
2024-01-15 14:30:00
Event logs, created_at, updated_at
TIMESTAMPTZ
Date + time + timezone offset
2024-01-15 14:30:00+05:30
Multi-region events, audit logs
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
// Part 02
Current Date and Time — Getting Now
Current date/time functions
-- Current date (no time component)
CURRENT_DATE -- 2024-02-15 (SQL standard)
CURDATE() -- MySQL equivalent
-- Current timestamp (date + time)
CURRENT_TIMESTAMP -- 2024-02-15 09:30:00+05:30 (SQL standard)
NOW() -- same as CURRENT_TIMESTAMP (widely supported)
LOCALTIMESTAMP -- current timestamp without timezone
-- Current time only
CURRENT_TIME -- 09:30:00+05:30
-- DuckDB specific:
TODAY() -- current date
GET_CURRENT_TIMESTAMP() -- current timestamp
-- Key difference: CURRENT_DATE vs NOW()
-- CURRENT_DATE returns a DATE (no time)
-- NOW() returns a TIMESTAMP (date + time)
-- For date comparisons, CURRENT_DATE is cleaner
-- For audit trails and event logging, NOW() or CURRENT_TIMESTAMP
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
// Part 03
EXTRACT — Pulling Out Date Parts
EXTRACT pulls a single numeric component out of a date or timestamp. It is the most commonly used date function in analytics — grouping by month, filtering by year, or computing the day of the week.
EXTRACT fields
EXTRACT(field FROM date_or_timestamp)
-- Common fields:
EXTRACT(YEAR FROM order_date) -- 2024
EXTRACT(MONTH FROM order_date) -- 1-12
EXTRACT(DAY FROM order_date) -- 1-31
EXTRACT(DOW FROM order_date) -- 0=Sunday, 6=Saturday (PostgreSQL/DuckDB)
EXTRACT(ISODOW FROM order_date) -- 1=Monday, 7=Sunday (ISO standard)
EXTRACT(WEEK FROM order_date) -- ISO week number (1-53)
EXTRACT(QUARTER FROM order_date) -- 1-4
EXTRACT(HOUR FROM timestamp_col) -- 0-23
EXTRACT(MINUTE FROM timestamp_col) -- 0-59
EXTRACT(SECOND FROM timestamp_col) -- 0-59
EXTRACT(EPOCH FROM timestamp_col) -- seconds since 1970-01-01 00:00:00 UTC
EXTRACT(DAYOFYEAR FROM date_col) -- 1-366 (DuckDB)
-- Shorthand in DuckDB (more readable):
YEAR(order_date) -- same as EXTRACT(YEAR FROM order_date)
MONTH(order_date) -- same as EXTRACT(MONTH FROM order_date)
DAY(order_date) -- same as EXTRACT(DAY FROM order_date)
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
// Part 04
DATE_TRUNC — Rounding Down to a Period
DATE_TRUNC truncates a date or timestamp to the start of a specified period — the first day of the month, the first day of the week, the first hour of the day. It is the foundation of time-series grouping: every date in the same month truncates to the same value, making GROUP BY work cleanly across all dates in that period.
DATE_TRUNC periods
DATE_TRUNC(precision, date_or_timestamp)
-- Precision options:
DATE_TRUNC('year', '2024-03-15') -- 2024-01-01 00:00:00
DATE_TRUNC('quarter', '2024-03-15') -- 2024-01-01 00:00:00 (Q1 start)
DATE_TRUNC('month', '2024-03-15') -- 2024-03-01 00:00:00
DATE_TRUNC('week', '2024-03-15') -- 2024-03-11 00:00:00 (Monday)
DATE_TRUNC('day', '2024-03-15 14:30:00') -- 2024-03-15 00:00:00
DATE_TRUNC('hour', '2024-03-15 14:30:00') -- 2024-03-15 14:00:00
DATE_TRUNC('minute', '2024-03-15 14:30:45') -- 2024-03-15 14:30:00
-- DuckDB also supports:
DATE_TRUNC('day', order_date)::DATE -- cast back to DATE for clean display
-- Why DATE_TRUNC instead of EXTRACT for grouping:
-- EXTRACT(MONTH FROM date) gives just the number (1-12) — loses year
-- DATE_TRUNC('month', date) gives 2024-01-01 — preserves year AND month
-- With EXTRACT: January 2023 and January 2024 both appear as month=1
-- With DATE_TRUNC: 2023-01-01 and 2024-01-01 are distinct values
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
🎯 Pro Tip
Always use DATE_TRUNC for time-series grouping — never just EXTRACT(MONTH). EXTRACT loses the year, causing January 2023 and January 2024 to collapse into the same group. DATE_TRUNC('month', date) produces a unique value per calendar month that can be correctly sorted, compared, and joined to other month-level data.
// Part 05
Date Arithmetic — Adding and Subtracting Time
SQL dates support arithmetic — you can add or subtract intervals to compute future dates, calculate differences, and define rolling time windows.
Date arithmetic syntax
-- Adding intervals to dates
order_date + INTERVAL '7 days' -- one week later
order_date + INTERVAL '1 month' -- one month later
order_date + INTERVAL '1 year' -- one year later
order_date + INTERVAL '2 hours 30 mins' -- timestamp arithmetic
-- Subtracting intervals
CURRENT_DATE - INTERVAL '30 days' -- 30 days ago
CURRENT_DATE - INTERVAL '3 months' -- 3 months ago
CURRENT_DATE - INTERVAL '1 year' -- 1 year ago
-- Date difference: subtract two dates
order_date - '2024-01-01' -- integer (days)
delivery_date - order_date -- days between two dates
-- DATEDIFF (MySQL / some databases):
DATEDIFF(end_date, start_date) -- days between (MySQL order: end, start)
-- DuckDB / PostgreSQL: subtract dates directly
'2024-02-15'::DATE - '2024-01-01'::DATE -- returns 45 (days)
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
// Part 06
AGE — Human-Readable Time Differences
AGE computes the difference between two dates as an interval — expressed in years, months, and days rather than a raw number of days. It is the right tool when you want to display tenure, age, or time elapsed in a human-readable format.
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
// Part 07
Date Formatting — TO_CHAR and strftime
Date formatting converts a date or timestamp into a human-readable string for display. PostgreSQL uses TO_CHAR. DuckDB (this playground) uses strftime. Both follow format codes where letters represent date parts.
Format codes — TO_CHAR (PostgreSQL) and strftime (DuckDB)
Sometimes you need to build a date from parts — constructing the first day of a month, creating a date from year and month numbers, or parsing a date from a string.
Building dates from parts
-- MAKE_DATE: construct a date from year, month, day integers
MAKE_DATE(2024, 1, 15) -- 2024-01-15
MAKE_DATE(EXTRACT(YEAR FROM CURRENT_DATE)::INT, 1, 1) -- Jan 1 of current year
-- First day of the current month:
DATE_TRUNC('month', CURRENT_DATE)::DATE -- cleanest approach
MAKE_DATE(EXTRACT(YEAR FROM CURRENT_DATE)::INT,
EXTRACT(MONTH FROM CURRENT_DATE)::INT, 1) -- same result
-- Last day of the current month:
DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month')::DATE - 1
-- Casting strings to dates:
'2024-01-15'::DATE -- ISO format — always works
CAST('2024-01-15' AS DATE) -- same, SQL standard syntax
-- Date from epoch (seconds since 1970-01-01):
TO_TIMESTAMP(1705276800) -- timestamp from Unix epoch integer
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
// Part 09
Time-Series Analysis Patterns
Date functions are the engine of time-series analytics. The patterns in this part — period-over-period comparison, rolling windows, cohort dating, and gap detection — appear in almost every analytical dashboard.
Month-over-month growth
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Cohort analysis — when did customers first order?
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Rolling 7-day average
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
// Part 10
Timezone Handling
Timezones matter for any platform with users or stores in multiple regions. India uses IST (UTC+5:30) — a single timezone, which simplifies things. Global platforms need to store timestamps in UTC and convert for display.
Timezone functions
-- AT TIME ZONE: convert between timezones
-- Convert UTC timestamp to IST for display:
NOW() AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata'
-- Or with the offset directly:
NOW() AT TIME ZONE 'Asia/Kolkata'
-- Common Indian timezone: 'Asia/Kolkata' (IST = UTC+5:30)
-- TIMEZONE() in DuckDB:
timezone('Asia/Kolkata', NOW())
-- Best practices for multi-region platforms:
-- 1. Store all timestamps as TIMESTAMPTZ (with timezone) in UTC
-- 2. Convert to local timezone only at display time
-- 3. For filtering: convert the filter value to UTC, not the stored value
-- (applying timezone conversion to a column prevents index usage)
-- Correct pattern for filtering by local time:
WHERE created_at >= '2024-01-15 00:00:00+05:30'
AND created_at < '2024-01-16 00:00:00+05:30'
-- Database converts the literals to UTC internally — index can be used
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
// Part 11
Complete Date Function Reference
FunctionDescriptionExample → Result
CURRENT_DATEToday's dateCURRENT_DATE → 2024-02-15
CURRENT_TIMESTAMP / NOW()Current date + timeNOW() → 2024-02-15 09:30:00
EXTRACT(part FROM date)Pull one numeric componentEXTRACT(MONTH FROM order_date) → 2
DATE_TRUNC('period', date)Round down to period startDATE_TRUNC('month', date) → 2024-02-01
date + INTERVAL 'n unit'Add time to a date'2024-01-15' + INTERVAL '7 days' → 2024-01-22
date1 - date2Days between two dates'2024-02-15'::DATE - '2024-01-15'::DATE → 31
AGE(date2, date1)Human-readable intervalAGE('2024-04-01', '2022-01-15') → 2 years 2 mons 17 days
strftime(date, fmt)Format date as string (DuckDB)strftime(date, '%d %b %Y') → 15 Jan 2024
TO_CHAR(date, fmt)Format date as string (PostgreSQL)TO_CHAR(date, 'DD Mon YYYY') → 15 Jan 2024
MAKE_DATE(y, m, d)Build date from integersMAKE_DATE(2024, 1, 15) → 2024-01-15
date::DATECast to date type'2024-01-15'::DATE
DATEADD(unit, n, date)Add n units (MySQL/SQL Server)DATEADD(day, 7, order_date)
DATEDIFF(unit, d1, d2)Difference in units (MySQL)DATEDIFF(day, '2024-01-01', '2024-02-01') → 31
// Part 12
What This Looks Like at Work
You are a data analyst at Instacart. The growth team runs a weekly business review and needs three reports every Monday morning: (1) last week's revenue by day of week, (2) month-to-date revenue versus the same period last month, and (3) a 30-day delivery time trend. All three are date function queries.
8:00 AM
Report 1 — last week by day
Filter to the last 7 days, group by day name, sort by revenue.
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
8:20 AM
Report 2 — MTD vs prior MTD
Month-to-date uses DATE_TRUNC for the period boundary; prior MTD uses the same logic offset by one month.
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
8:45 AM
Report 3 — 30-day delivery trend
DATE_TRUNC to week gives a clean weekly trend line.
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
9:00 AM
All three reports delivered before stand-up
Three date-function queries — each under 15 lines — producing the complete weekly business review pack. The growth team has actionable numbers before the 9 AM stand-up.
🎯 Pro Tip
Save commonly used date boundaries as variables or CTEs at the top of your reporting queries: current month start, prior month start, MTD cutoff. Then reference them throughout rather than recomputing DATE_TRUNC('month', CURRENT_DATE) ten times. A single CTE named report_dates with all your period boundaries makes the query readable and ensures consistency — one change at the top updates every reference.
// Part 13
Interview Prep — 5 Questions With Complete Answers
Q: What is the difference between EXTRACT and DATE_TRUNC for grouping time-series data?
EXTRACT pulls a single numeric component from a date — EXTRACT(MONTH FROM date) returns the number 1 through 12. DATE_TRUNC rounds a date down to the start of a specified period — DATE_TRUNC('month', date) returns a complete date value representing the first day of that month.
For time-series grouping, this distinction is critical. EXTRACT(MONTH FROM date) loses the year — January 2023 and January 2024 both return 1 from EXTRACT(MONTH). If you GROUP BY EXTRACT(MONTH FROM order_date), orders from different years collapse into the same group, producing incorrect multi-year totals. DATE_TRUNC('month', date) returns 2023-01-01 and 2024-01-01 respectively — distinct values that sort correctly and do not merge across years.
Always use DATE_TRUNC for time-series GROUP BY. Use EXTRACT only when you specifically want just the numeric component — for example, filtering by month number across all years (WHERE EXTRACT(MONTH FROM order_date) = 1 to get all Januaries), or for day-of-week analysis where you want to aggregate all Mondays together regardless of the specific date. The rule: if ordering and uniqueness across years matters, use DATE_TRUNC. If you want to aggregate across multiple years intentionally, use EXTRACT.
Q: How do you calculate the number of days between two dates in SQL?
In PostgreSQL and DuckDB, subtract two DATE values directly — the result is an integer representing the number of days: delivery_date - order_date returns the number of days between them. For TIMESTAMP values, subtraction returns an INTERVAL — extract the day component with EXTRACT(DAY FROM (timestamp2 - timestamp1)) or convert to epoch seconds and divide: EXTRACT(EPOCH FROM (ts2 - ts1)) / 86400.
In MySQL, use DATEDIFF(end_date, start_date) — note the argument order is end then start, which returns positive values for end > start. DATEDIFF('2024-02-15', '2024-01-15') returns 31. In SQL Server, DATEDIFF(day, start_date, end_date) — note the unit is first and the argument order is reversed from MySQL.
For business days (excluding weekends), there is no standard SQL function — you need a calendar table or a more complex expression. A simple approximation: total_days - (total_weeks * 2) where total_weeks is FLOOR(total_days / 7) — this works for approximate business day counts. For precise business day calculations accounting for holidays, a calendar dimension table with an is_business_day flag is the correct approach. The AGE function provides a human-readable interval (years, months, days) rather than a raw day count — use AGE for display purposes (tenure display, customer age) and direct date subtraction for arithmetic calculations.
Q: How do you write a query to find orders placed in the last 30 days?
The standard pattern: WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'. This filters orders where the order_date is within the last 30 days from today. CURRENT_DATE gives today's date; subtracting INTERVAL '30 days' gives the cutoff date. The >= operator includes the cutoff date itself.
Variations for different databases: MySQL uses DATE_SUB(CURDATE(), INTERVAL 30 DAY) or NOW() - INTERVAL 30 DAY. SQL Server uses DATEADD(day, -30, GETDATE()). The PostgreSQL/DuckDB interval syntax (INTERVAL '30 days') is the most readable and closest to the SQL standard.
Important nuances: CURRENT_DATE - INTERVAL '30 days' is not the same as "one calendar month ago" — 30 days is always 30 days regardless of month length. For "same period last calendar month", use WHERE order_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' AND order_date < DATE_TRUNC('month', CURRENT_DATE). For "last N months", use INTERVAL 'N months' — this correctly handles month-length differences: CURRENT_DATE - INTERVAL '3 months' gives the date 3 calendar months ago, not 90 days ago. For performance, never apply functions to the indexed column — WHERE order_date >= CURRENT_DATE - INTERVAL '30 days' allows index range scan; WHERE EXTRACT(MONTH FROM order_date) = ... requires a full table scan.
Q: What is DATE_TRUNC used for and how does it enable time-series reporting?
DATE_TRUNC truncates a date or timestamp to the beginning of a specified time period — 'month' gives the first day of the month, 'year' gives January 1st, 'week' gives Monday of that week, 'hour' gives the top of the hour. The key behaviour: all dates within the same period truncate to the same value. Every day in January 2024 truncates to 2024-01-01. This makes DATE_TRUNC the foundation of time-series GROUP BY.
For time-series reporting: GROUP BY DATE_TRUNC('month', order_date) groups all January orders together, all February orders together, and produces one row per calendar month. The truncated date is a complete, sortable, comparable value — ORDER BY DATE_TRUNC('month', order_date) correctly sorts months chronologically. You can also join the truncated date to a calendar dimension table to fill in months with zero activity.
The practical reporting pattern: SELECT DATE_TRUNC('month', order_date)::DATE AS month, SUM(revenue) AS monthly_revenue FROM orders GROUP BY DATE_TRUNC('month', order_date) ORDER BY month. The ::DATE cast converts the TIMESTAMP result of DATE_TRUNC to a clean DATE for display. This pattern — DATE_TRUNC in both SELECT and GROUP BY — is the correct, idiomatic time-series aggregation in PostgreSQL and DuckDB. In MySQL, use DATE_FORMAT(date, '%Y-%m-01') to achieve the same effect: '%Y-%m-01' formats the date as the first day of its month, grouping all days in the same month to the same string value.
Q: How do you handle timezones in SQL for a multi-region application?
The foundational rule: store all timestamps in UTC. Use the TIMESTAMPTZ (timestamp with time zone) column type in PostgreSQL. When data is inserted, the database converts the local time to UTC for storage. When data is read, it can be converted back to any local timezone for display. Storing in UTC means the stored values are unambiguous — there is no confusion about which timezone an event occurred in, and daylight saving time transitions do not create duplicates or gaps.
To convert for display: SELECT created_at AT TIME ZONE 'Asia/Kolkata' AS ist_time FROM events. This converts the UTC-stored timestamp to IST for display without modifying the stored value. In DuckDB: timezone('Asia/Kolkata', created_at). The AT TIME ZONE expression handles DST transitions automatically when using named timezone identifiers ('Asia/Kolkata') rather than fixed offsets ('+05:30') — named identifiers know about DST rules, fixed offsets do not.
For filtering: never apply timezone conversion to the stored column in WHERE — this prevents index usage. Instead, convert the filter boundary values to UTC: WHERE created_at >= '2024-01-15 00:00:00+05:30' AND created_at < '2024-01-16 00:00:00+05:30'. The database converts the literal values to UTC internally and compares against the UTC-stored timestamps — the index can be used for a range scan. For reports aggregated by local day: convert to local timezone first (in a CTE or derived table), then truncate and group. India has a single timezone (IST, UTC+5:30) which simplifies timezone handling significantly — the real complexity arises in platforms serving users in multiple countries with different timezones and DST rules.
// Part 14
Errors You Will Hit — And Exactly Why They Happen
Time-series GROUP BY collapses multiple years into one — January 2023 and January 2024 merged
Cause: GROUP BY EXTRACT(MONTH FROM order_date) uses only the month number (1-12) with no year. Both January 2023 and January 2024 extract to month = 1 and collapse into the same group. This is the most common date aggregation mistake — months from different years are silently merged.
Fix: Replace EXTRACT(MONTH) with DATE_TRUNC('month', order_date): GROUP BY DATE_TRUNC('month', order_date). DATE_TRUNC returns a complete date value (2023-01-01 vs 2024-01-01) that is unique per calendar month across all years. Cast to DATE for clean display: DATE_TRUNC('month', order_date)::DATE. If you must use EXTRACT, include the year: GROUP BY EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date) — this prevents year-collapsing but the result is less convenient to sort and display.
Date filter is not using an index — query is slow on large tables
Cause: A function is applied to the date column in the WHERE clause: WHERE EXTRACT(YEAR FROM order_date) = 2024 or WHERE DATE_TRUNC('month', order_date) = '2024-01-01'. Applying any function to an indexed column in WHERE prevents the database from using a B-tree index on that column — it must evaluate the function for every row (full table scan).
Fix: Rewrite the filter to compare the raw column against literal date boundaries. Instead of WHERE EXTRACT(YEAR FROM order_date) = 2024, use WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'. Instead of WHERE DATE_TRUNC('month', order_date) = '2024-01-01', use WHERE order_date >= '2024-01-01' AND order_date < '2024-02-01'. The raw column comparison allows the database to use an index range scan — orders of magnitude faster on large tables. For frequently used date columns, ensure an index exists: CREATE INDEX idx_orders_date ON orders(order_date).
INTERVAL arithmetic gives wrong result — adding '1 month' skips or duplicates a day
Cause: Adding INTERVAL '1 month' to January 31 produces February 28 (or 29 in leap years) — the database clamps to the last valid day of the resulting month. This is correct behaviour but unexpected if you assumed months are always 30 or 31 days. Similarly, INTERVAL '30 days' and INTERVAL '1 month' are not the same — 1 month is always one calendar month regardless of actual day count.
Fix: Use INTERVAL '1 month' when you want one calendar month (January → February, regardless of day count). Use INTERVAL '30 days' when you want exactly 30 days. For month-end reporting, always derive the last day of month via DATE_TRUNC rather than adding a fixed interval: (DATE_TRUNC('month', date) + INTERVAL '1 month' - INTERVAL '1 day')::DATE gives the last day of any month correctly. Be explicit about which semantics you need — document in a comment whether your date arithmetic is calendar-month-based or fixed-day-based.
AGE() returns unexpected negative months or days for close dates
Cause: AGE(date1, date2) computes date1 - date2. If date1 is the earlier date (smaller value) and date2 is the later date, the result is a negative interval. Also, AGE can produce mixed signs within the interval for dates that cross month boundaries in complex ways — for example, AGE('2024-02-15', '2024-01-31') returns '0 years 0 mons 15 days' but AGE('2024-01-15', '2024-02-28') may return an unexpected result.
Fix: Always pass the later date as the first argument to AGE: AGE(end_date, start_date) or AGE(CURRENT_DATE, hire_date) — this gives a positive interval. For simple day-count differences, use date subtraction (end_date - start_date) rather than AGE — it is simpler and returns a plain integer. Use AGE only when you need a human-readable interval in years, months, and days. For tenure displays: EXTRACT(YEAR FROM AGE(CURRENT_DATE, hire_date)) gives complete years; EXTRACT(MONTH FROM AGE(CURRENT_DATE, hire_date)) gives the remaining months.
strftime / TO_CHAR returns wrong month or day name — locale issue
Cause: Date format functions return month and day names in the database server's locale setting. A server configured with a non-English locale may return 'janvier' instead of 'January', or 'lunes' instead of 'Monday'. This is a server configuration issue, not a query bug, but it affects query output.
Fix: For locale-independent month labels, use EXTRACT to get the month number and a CASE statement to map to the desired language: CASE EXTRACT(MONTH FROM order_date) WHEN 1 THEN 'January' WHEN 2 THEN 'February' ... END. Alternatively, set the session locale before running the query: SET lc_time = 'en_US.UTF-8' in PostgreSQL. For date formats that avoid language entirely, use ISO format: strftime(date, '%Y-%m-%d') or TO_CHAR(date, 'YYYY-MM-DD') — these are locale-independent numbers and separators.
Try It Yourself
Write a comprehensive date analytics query that produces a store performance summary for January and February 2024. For each store show: store_id, city, the month (formatted as 'Jan 2024' or 'Feb 2024'), order_count, total_revenue, avg_order_value, avg_delivery_days (rounded to 1 decimal, NULL if no deliveries), fastest_delivery (min days), and a month_label that is 'January' or 'February'. Use DATE_TRUNC for the period grouping, strftime for the display label, and date arithmetic for delivery days. Only include delivered orders. Sort by store_id then month.
🎯 Key Takeaways
✓SQL has four temporal types: DATE (date only), TIME (time only), TIMESTAMP (date+time, no timezone), TIMESTAMPTZ (date+time+timezone). Use TIMESTAMPTZ for audit trails and multi-region event logging.
✓CURRENT_DATE gives today's date. NOW() / CURRENT_TIMESTAMP gives the current date and time. Use CURRENT_DATE for date comparisons, NOW() for timestamps.
✓EXTRACT(part FROM date) returns a single numeric component. Use it to filter by month number or day-of-week — not for time-series grouping.
✓DATE_TRUNC('period', date) rounds down to the period start and preserves the year. Always use DATE_TRUNC for time-series GROUP BY — EXTRACT loses the year, collapsing different years into the same group.
✓Date arithmetic: date + INTERVAL 'n days' adds time. date1 - date2 returns integer days (PostgreSQL/DuckDB). INTERVAL '1 month' is one calendar month, not 30 days.
✓AGE(end_date, start_date) returns a human-readable interval in years, months, days. Always put the later date first to get a positive result.
✓strftime(date, format) in DuckDB and TO_CHAR(date, format) in PostgreSQL format dates as strings for display. ISO format codes (%Y-%m-%d) are locale-independent.
✓Never apply functions to indexed date columns in WHERE — WHERE EXTRACT(YEAR FROM date) = 2024 prevents index use. Use range comparisons: WHERE date >= '2024-01-01' AND date < '2025-01-01'.
✓Store timestamps in UTC using TIMESTAMPTZ. Convert to local timezone at display time only (AT TIME ZONE 'Asia/Kolkata'). Convert filter boundaries to UTC rather than applying timezone functions to stored columns.
✓For rolling windows: combine DATE_TRUNC for period grouping with LAG() window function for period-over-period comparisons. The date arithmetic gives the offset; the window function gives the prior period value.
What comes next
In Module 43, you learn math and numeric functions — ROUND, FLOOR, CEIL, ABS, MOD, POWER, SQRT, LOG, and every numeric operation for financial calculations, statistical summaries, and data transformation.