Level 6: Working with text and dates
Updated on
Beginner · level 6 of 20. Goal: Transform text and extract information from dates.
Topics: UPPER LOWER LENGTH || SUBSTR strftime date julianday
Lesson
SQL can transform text. UPPER and LOWER put text in uppercase or lowercase, which helps clean up badly typed data such as “bob durand” or “CLAIRE ROUX”. LENGTH gives the number of characters.
SUBSTR(text, start, length) extracts a piece of text. The first character is at position 1, not 0: SUBSTR('Paris', 1, 3) gives 'Par'.
The || operator sticks pieces of text together: name || ' (' || city || ')' gives “Lena (Lyon)”. Careful: if one of the joined values is NULL, the whole result becomes NULL.
In SpeedQL (SQLite), a date is text in the 'YYYY-MM-DD' format. This format has a big advantage: alphabetical order is also chronological order. So you compare dates directly: '2025-03-01' < '2025-04-15'.
To extract part of a date, you use strftime(format, date). '%Y' gives the year, '%m' the month, '%d' the day, '%Y-%m' the year and month. strftime('%Y', joined) gives for example '2022'. The result is text: you compare it with '2022', between quotes.
To shift a date, you use date(date, shift): date('2025-07-01', '+1 day') gives '2025-07-02'. Shifts are written '+7 days', '-1 month' or '+1 year'. strftime accepts the same shifts after the date.
To measure a gap, julianday(date) converts a date into a number of days. The difference between two julianday values therefore gives a gap in days: julianday(return_date) - julianday(loan_date) gives the length of a loan.
Finally, CAST(value AS type) converts a value from one type to another: CAST(strftime('%Y', joined) AS INTEGER) gives the number 2022 instead of the text '2022', which you can then use in calculations (seniority, for example).
Syntax
SELECT UPPER(name),
name || ' - ' || city,
SUBSTR(text, 1, 4),
strftime('%Y', date_col),
date(date_col, '+7 days')
FROM my_table;Worked example
SELECT name || ' (' || city || ')' AS label, strftime('%Y', joined) AS join_year
FROM members;We build a readable label by gluing the name and the city, and we extract the sign-up year.
| id | name | city | joined |
|---|---|---|---|
| 1 | Lena | Lyon | 2022-01-15 |
| 2 | Marc | Paris | 2021-06-03 |
| 3 | Nadia | Lyon | 2023-03-20 |
| 4 | Oscar | Lille | 2020-11-11 |
| 5 | Paula | Paris | 2024-02-01 |
| 6 | Quentin | Nantes | 2023-09-09 |
Example result
| label | join_year |
|---|---|
| Lena (Lyon) | 2022 |
| Marc (Paris) | 2021 |
| Nadia (Lyon) | 2023 |
| Oscar (Lille) | 2020 |
| Paula (Paris) | 2024 |
| Quentin (Nantes) | 2023 |
Key points
- || concatenates text.
- SUBSTR counts from 1.
- strftime extracts part of a date, julianday lets you compute gaps.
Common pitfalls
- strftime returns text: compare with '2023', between quotes.
- A date with a time is “greater” than the date alone: '2025-06-05 10:00' > '2025-06-05'. departs BETWEEN '2025-06-01' AND '2025-06-05' therefore misses the flights of 5 June. Write departs < '2025-06-06' as the end bound.
- If one of the values joined with || is NULL, the whole result becomes NULL.
SQLite specifics
SQLite has no real date type: a date is text in the 'YYYY-MM-DD' format, and strftime, date and julianday are specific to SQLite. Other engines have a DATE type and other functions (EXTRACT, DATE_TRUNC, DATEDIFF…).
Going further
TRIM(text) removes the spaces at the start and the end; REPLACE(text, '-', '/') replaces every occurrence; INSTR(text, '@') gives the position of a character (0 if it is absent). Combined: SUBSTR(email, INSTR(email, '@') + 1) extracts the domain of an address ('mail.com').
The level’s 5 exercises
- Show the name of each contact in uppercase (name_upper) and its number of characters (name_length).
- For each hotel guest, show a column guest in the format “name - country” (for example “Ana Silva - Portugal”).
- Show the airline and departure time of each flight (for example 08:10) in a column named hour.
- Show the name of the members who joined in 2023.
- A loan lasts at most 21 days. For the loans returned late, show the id, the return deadline (due_date), the return date and the number of days late (days_late).
In SpeedQL, every query is checked straight away: its result is compared with the expected one, then the query is run again on a hidden control database. Each exercise has written hints, to show only if you get stuck.
See also: the cheat sheet card · the SQL exercises with solutions on this topic
Do the level 6 exercises
Free, no sign-up: the lesson and the 5 exercises open right in your browser.
← Previous level: Summarising a table: aggregates · Next level: Grouping with GROUP BY →