Date, string and numeric functions
Bucket dates into months and weeks, compute durations, clean text and round numbers — with notes for PostgreSQL and SQL Server.
This lesson builds on: Subqueries, CTEs and UNION.
- Extract date parts, truncate to month and compute day differences
- Clean and combine text with string functions
- Round, cast and safely divide numbers
- Translate common functions between SQLite, PostgreSQL and SQL Server
Date functions
Dates drive almost every business query: monthly trends, year-over-year, days to convert. Each database names date functions differently — the concepts are identical.
| Task | SQLite (practice DB) | PostgreSQL | SQL Server |
|---|---|---|---|
| Year | strftime('%Y', d) |
EXTRACT(YEAR FROM d) |
YEAR(d) |
| Month bucket | strftime('%Y-%m', d) |
DATE_TRUNC('month', d) |
DATETRUNC(month, d) (2022+) |
| Add one month | date(d, '+1 month') |
d + INTERVAL '1 month' |
DATEADD(month, 1, d) |
| Days between | julianday(b) - julianday(a) |
b - a (dates) |
DATEDIFF(day, a, b) |
| Weekday | strftime('%w', d) (0 = Sunday) |
EXTRACT(DOW FROM d) |
DATEPART(weekday, d) |
Monthly completed orders in 2025:
SELECT strftime('%Y-%m', order_date) AS month,
COUNT(*) AS completed_orders
FROM orders
WHERE status = 'completed' AND order_date >= '2025-01-01'
GROUP BY month
ORDER BY month;| month | completed_orders |
|---|---|
| 2025-01 | 868 |
| 2025-02 | 682 |
| 2025-03 | 1120 |
| 2025-04 | 938 |
| 2025-05 | 1109 |
| 2025-06 | 1077 |
| 2025-07 | 1140 |
| 2025-08 | 1016 |
| 2025-09 | 939 |
| 2025-10 | 959 |
| 2025-11 | 1373 |
| 2025-12 | 1492 |
The December peak and February dip are seasonality — something to remember when comparing months.
Days from signup to first order (customers who ordered at least once):
WITH first_order AS (
SELECT customer_id, MIN(order_date) AS first_order_date
FROM orders GROUP BY customer_id
)
SELECT ROUND(AVG(julianday(f.first_order_date) - julianday(c.signup_date)), 1) AS avg_days_to_first_order
FROM customers c JOIN first_order f ON f.customer_id = c.customer_id
WHERE f.first_order_date >= c.signup_date;| avg_days_to_first_order |
|---|
| 287.2 |
In databases with real timestamps, WHERE created_at = '2025-03-01' matches only midnight exactly. Use ranges (>= '2025-03-01' AND < '2025-03-02').
String functions
| Task | SQLite / PostgreSQL | SQL Server |
|---|---|---|
| Upper / lower | UPPER(s), LOWER(s) |
same |
| Trim spaces | TRIM(s) |
TRIM(s) (2017+) |
| Length | LENGTH(s) |
LEN(s) |
| Substring | SUBSTR(s, start, len) |
SUBSTRING(s, start, len) |
| Concatenate | `a | |
| Replace | REPLACE(s, 'x', 'y') |
same |
| Find position | INSTR(s, 'x') / POSITION('x' IN s) |
CHARINDEX('x', s) |
Cleaning the messy city column in customers_raw:
SELECT city AS raw_city,
UPPER(SUBSTR(TRIM(city), 1, 1)) || LOWER(SUBSTR(TRIM(city), 2)) AS clean_city,
COUNT(*) AS rows
FROM customers_raw
WHERE TRIM(LOWER(city)) = 'baku'
GROUP BY city;| raw_city | clean_city | rows |
|---|---|---|
| Baku | Baku | 17 |
| BAKU | Baku | 13 |
| Baku | Baku | 16 |
| baku | Baku | 22 |
Four spellings of one city collapse into one after TRIM and case normalisation. Extracting the email domain:
SELECT SUBSTR(email, INSTR(email, '@') + 1) AS domain, COUNT(*) AS customers
FROM customers GROUP BY domain;| domain | customers |
|---|---|
| example.com | 4000 |
Numeric functions
ROUND(x, n)rounds tondecimals (round only for display, at the end).CAST(x AS REAL)/CAST(x AS NUMERIC(10,2))changes type.ABS(x),CEIL/FLOOR(PostgreSQL, SQL Server; SQLite 3.35+ via math functions).- Safe division:
x / NULLIF(y, 0)returns NULL instead of a division-by-zero error.
SELECT store_id,
ROUND(100.0 * SUM(CASE WHEN status = 'returned' THEN 1 ELSE 0 END)
/ NULLIF(COUNT(*), 0), 2) AS return_rate_pct
FROM orders
GROUP BY store_id
ORDER BY return_rate_pct DESC;| store_id | return_rate_pct |
|---|---|
| 6 | 6.43 |
| 2 | 6.34 |
| 8 | 6.23 |
| 5 | 6.04 |
| 1 | 6.01 |
| 3 | 5.74 |
| 4 | 5.59 |
| 7 | 5.48 |
| 9 | 4.47 |
SUM(ROUND(x, 0)) accumulates rounding error across thousands of rows. Aggregate exact values, round the final result.
Practice
Return each month of 2024 (YYYY-MM) and the number of orders placed (any status), ordered by month.
In customers_raw, how many distinct cities are there after trimming spaces and ignoring case? One number.
What does x / NULLIF(y, 0) return when y is 0?
Summary
- Date bucketing (
strftime,DATE_TRUNC) powers trends; durations come from date differences; use ranges, not equality, for timestamps. - String functions clean and standardise text before grouping.
- Round at the end; use
NULLIFto avoid division by zero; cast when types are wrong.