Database systems · Chapter 4 · Section 4.11 · Built-in SQL functions · Study guide
SQL is not just SELECT, WHERE, and JOIN. Section 4.11 covers the library of built-in functions every query can reach for: reshaping dates, rounding and computing with numbers, cleaning and searching text, and converting a value from one data type to another. This page walks through all four groups, 4.11a through 4.11d, against the same PRODUCT, VENDOR, CUSTOMER, and INVOICE tables used in the Chapter 4 review for Sections 4.1–4.7 and the Chapter 4 review for Sections 4.8–4.13.
Section 4.11a
Dates are stored as dates, not text, precisely so the database can do arithmetic and comparisons on them directly. Date functions are how a query gets at the pieces of a date, or moves a date forward and back.
Two invoices dated 2026-03-14 and 2026-03-01 are not just different strings; the database knows one is 13 days after the other, and date functions are what let a query act on that fact.
Function names are the least portable part of SQL. Unlike SELECT, WHERE, and JOIN, which are standard everywhere, date and time function names vary a great deal from one DBMS to the next. The table below shows the same four operations in four common systems; the worked examples that follow pick one consistent, widely taught style, close to MySQL and standard SQL, so pay attention to your own DBMS's documentation when the function name does not match what you see running.
| Operation | MySQL | Oracle | SQL Server |
|---|---|---|---|
| Current date | CURDATE() | SYSDATE | GETDATE() |
| Extract year | YEAR(d) | EXTRACT(YEAR FROM d) | YEAR(d) |
| Add N days | DATE_ADD(d, INTERVAL n DAY) | d + n | DATEADD(day, n, d) |
| Difference in days | DATEDIFF(d1, d2) | d1 - d2 | DATEDIFF(day, d2, d1) |
YEAR(), MONTH(), and DAY() pull one component out of a date value, returning a plain number you can filter or group on:
SELECT INV_NUM, INV_DATE
FROM INVOICE
WHERE YEAR(INV_DATE) = 2026;
-- every invoice billed during 2026, regardless of month or day
SELECT MONTH(INV_DATE) AS BILLING_MONTH, COUNT(*) AS INVOICE_COUNT
FROM INVOICE
GROUP BY MONTH(INV_DATE)
ORDER BY BILLING_MONTH;
-- how many invoices were written in each calendar month
CURDATE() (MySQL) or CURRENT_DATE (standard SQL) returns the date the query is actually running, useful for anything relative to "now":
SELECT INV_NUM, INV_DATE
FROM INVOICE
WHERE INV_DATE = CURDATE();
-- invoices written today
Because a date is stored as a real date value, adding a number of days to it produces another valid date. This is exactly how a due date gets computed from an invoice date:
SELECT INV_NUM, INV_DATE,
DATE_ADD(INV_DATE, INTERVAL 30 DAY) AS DUE_DATE
FROM INVOICE;
-- every invoice's payment due date, 30 days after it was written
DATEDIFF() goes the other direction, subtracting two dates to get the number of days between them:
SELECT INV_NUM, INV_DATE,
DATEDIFF(CURDATE(), INV_DATE) AS DAYS_OLD
FROM INVOICE
WHERE DATEDIFF(CURDATE(), INV_DATE) > 60;
-- every invoice more than 60 days old, with its exact age in days
Filtering on a function result usually means the column itself is not sargable (search-argument-able): wrapping INV_DATE in YEAR(...) in the WHERE clause, as in the first example above, can prevent the database from using an index on INV_DATE efficiently. A range condition, WHERE INV_DATE >= '2026-01-01' AND INV_DATE < '2027-01-01', returns the identical rows and lets an index help. Both are worth knowing; the function form reads more clearly, the range form runs faster on a large table.
Section 4.11b
Numeric functions round, truncate, and reshape numbers, the difference between a price that displays cleanly and one that shows nine decimal places by accident.
| Function | What it does | Example |
|---|---|---|
| ROUND(n, d) | Rounds n to d decimal places | ROUND(19.947, 2) → 19.95 |
| TRUNCATE(n, d) | Cuts n off at d decimal places, no rounding | TRUNCATE(19.947, 2) → 19.94 |
| ABS(n) | Absolute value | ABS(-42) → 42 |
| MOD(n, m) | Remainder after dividing n by m | MOD(17, 5) → 2 |
| CEIL(n) / FLOOR(n) | Rounds up / down to the nearest whole number | CEIL(4.1) → 5, FLOOR(4.9) → 4 |
| POWER(n, p) | n raised to the power p | POWER(2, 10) → 1024 |
These two are confused constantly because they look similar and usually agree. They disagree exactly at the digit being cut: ROUND looks at the next digit and rounds up when appropriate; TRUNCATE never rounds, it simply drops everything past the cutoff.
SELECT P_PRICE,
ROUND(P_PRICE, 1) AS ROUNDED,
TRUNCATE(P_PRICE, 1) AS TRUNCATED
FROM PRODUCT;
-- a price of 14.97 rounds to 15.0 but truncates to 14.9
A 15 percent discount, cleanly rounded to two decimal places for display, needs both a computed expression and ROUND wrapped around it:
SELECT P_CODE, P_PRICE,
ROUND(P_PRICE * 0.85, 2) AS SALE_PRICE
FROM PRODUCT;
-- every product's price after a 15% discount, rounded to the cent
MOD is the function version of the % operator, and it shows up constantly for splitting rows into even and odd, or into a fixed number of buckets:
SELECT P_CODE, P_QOH,
CASE WHEN MOD(P_QOH, 2) = 0 THEN 'Even count' ELSE 'Odd count' END AS PARITY
FROM PRODUCT;
-- labels each product's quantity on hand as even or odd
ROUND does not change the underlying data type. ROUND(P_PRICE, 0) on a decimal column still returns a decimal, just one with nothing after the point, for example 15.00, not the integer 15. If a whole number is actually required, that is a job for a conversion function, not ROUND alone.
Section 4.11c
String functions clean up, search inside, and rebuild text values, the tools behind almost every "why doesn't this match" problem in a WHERE clause.
| Function | What it does | Example |
|---|---|---|
| CONCAT(a, b, …) | Joins strings end to end | CONCAT('Cruz', ' ', 'Johal') |
| UPPER(s) / LOWER(s) | Converts case | UPPER('sql') → 'SQL' |
| LENGTH(s) | Number of characters in s | LENGTH('Python') → 6 |
| SUBSTRING(s, start, len) | Extracts len characters starting at position start | SUBSTRING('database', 1, 4) → 'data' |
| TRIM(s) | Removes leading and trailing spaces | TRIM(' hi ') → 'hi' |
| REPLACE(s, old, new) | Swaps every occurrence of old with new | REPLACE('2026-03-14', '-', '/') |
CONCAT is how two separate columns, kept separate for good reason (sorting by last name, for instance), get combined only at display time:
SELECT CONCAT(CUS_FNAME, ' ', CUS_LNAME) AS FULL_NAME
FROM CUSTOMER
ORDER BY CUS_LNAME;
-- "Maria Alvarez", "James Chen", ... sorted by the LNAME column even though it never appears alone in the output
A search for 'smith' will miss a row stored as 'Smith' unless both sides of the comparison are forced to the same case first:
SELECT CUS_LNAME, CUS_FNAME
FROM CUSTOMER
WHERE UPPER(CUS_LNAME) = UPPER('smith');
-- matches 'Smith', 'SMITH', and 'smith' alike
SUBSTRING extracts a piece of a longer string by position, useful whenever a code packs more than one piece of information into a single column:
SELECT P_CODE, SUBSTRING(P_CODE, 1, 2) AS CATEGORY_PREFIX
FROM PRODUCT;
-- if product codes are built like 'AC-01452', this pulls out 'AC', the category prefix
Combining SUBSTRING, LENGTH, and CONCAT is how a query truncates a long text field and marks that it was cut off, without ever risking an error on a description that was already short:
SELECT P_DESCRIPT,
CASE WHEN LENGTH(P_DESCRIPT) > 20
THEN CONCAT(SUBSTRING(P_DESCRIPT, 1, 20), '...')
ELSE P_DESCRIPT
END AS SHORT_DESCRIPT
FROM PRODUCT;
-- descriptions over 20 characters are cut to 20 and marked with an ellipsis; shorter ones pass through untouched
Watch for stray whitespace, not just case. A column value stored as 'Smith ', with a trailing space from data entry, will fail an exact = comparison against 'Smith' even though the two look identical on screen. TRIM() on both sides of a comparison guards against exactly this.
Section 4.11d
Every value in SQL has a data type, and conversion functions are how a query moves a value from one type to another on purpose, instead of hoping the database guesses correctly.
Some type changes happen automatically; SQL will often convert a number to text to concatenate it without being asked. That automatic behavior is called implicit conversion. Explicit conversion, using a function like CAST, states the intended type directly and fails loudly, at the query, if the conversion is not actually possible, rather than silently producing a wrong answer somewhere downstream.
CAST(expression AS type) works across every major DBMS and should be the default choice when a conversion is genuinely needed:
SELECT P_CODE, CAST(P_PRICE AS INTEGER) AS WHOLE_DOLLAR_PRICE
FROM PRODUCT;
-- 14.97 becomes 14: CAST to an integer truncates, it does not round
SELECT CONCAT('Invoice total: $', CAST(INV_TOTAL AS CHAR)) AS SUMMARY_LINE
FROM INVOICE;
-- a numeric column explicitly converted to text before being joined with a literal string
Data entered as text, for example from a form or an import file, often needs to become a real number before it can be used in arithmetic:
SELECT CAST('1250.50' AS DECIMAL(10,2)) + 100;
-- 1350.50 -- the text '1250.50' is converted to a real decimal number first, then added to 100
A string that does not look like the target type raises an error, not a guess. CAST('abc' AS INTEGER) fails; SQL will not invent a number out of text that is not numeric. That failure is a feature: it surfaces a data problem immediately instead of letting a bad value quietly become 0 somewhere in a report.
A stored date and a nicely formatted date for a report are two different jobs. CAST handles the type change; a dedicated formatting function, named differently per DBMS as the table in Section 4.11a showed, handles the exact display format:
SELECT INV_NUM, CAST(INV_DATE AS CHAR) AS INV_DATE_TEXT
FROM INVOICE;
-- the date value becomes plain text, ready to concatenate into a longer message
| Form | Where it works |
|---|---|
| CAST(expr AS type) | Standard SQL; works in MySQL, SQL Server, PostgreSQL, Oracle, and SQLite alike |
| CONVERT(type, expr) | SQL Server's own form, with the type and expression in the opposite order from CAST |
| TO_CHAR / TO_NUMBER / TO_DATE | Oracle-specific, and also combine formatting with the type change in one call |
One table, every function from this page, for whenever you just need the name back.
| Function | Group | Purpose |
|---|---|---|
| CURDATE() / CURRENT_DATE | Date & time | Today's date |
| YEAR() / MONTH() / DAY() | Date & time | Extract one part of a date |
| DATE_ADD() | Date & time | Add an interval to a date |
| DATEDIFF() | Date & time | Days between two dates |
| ROUND() | Numeric | Round to a given number of decimals |
| TRUNCATE() | Numeric | Cut off decimals without rounding |
| ABS() | Numeric | Absolute value |
| MOD() | Numeric | Remainder after division |
| CEIL() / FLOOR() | Numeric | Round up / down to a whole number |
| POWER() | Numeric | Raise to a power |
| CONCAT() | String | Join strings together |
| UPPER() / LOWER() | String | Change case |
| LENGTH() | String | Number of characters |
| SUBSTRING() | String | Extract part of a string by position |
| TRIM() | String | Remove leading/trailing spaces |
| REPLACE() | String | Swap one substring for another |
| CAST() | Conversion | Change a value's data type, the standard, portable form |
| CONVERT() | Conversion | SQL Server's own form of CAST |
| TO_CHAR() / TO_NUMBER() / TO_DATE() | Conversion | Oracle's type-and-format conversion functions |
Ten problems, one small SQLite database, no installation beyond Python itself. Every problem needs at least one function from this page to answer.
The practice notebook uses a three table movie rental database: MOVIE (10 rows), CUSTOMER (8 rows), and RENTAL (20 rows). On purpose, RENTAL only stores a rental_date and a return_date, nothing else; a due date, a late fee, and "days rented" all have to be computed with the functions from Sections 4.11a–4.11d, which is exactly the point.
| Section | Problems |
|---|---|
| 4.11a Date and time | Movies released in 2023; days rented per completed rental; every rental's due date; which rentals came back late |
| 4.11b Numeric | Rounding rental prices to the nearest dollar; computing a per-day late fee |
| 4.11c String | Building a "Now Showing" list; a case-insensitive last-name search; shortening long titles for a receipt |
| 4.11d Conversion | A one-line, plain-text receipt built with CAST and string concatenation |
Get the notebook. Download movie_rental_practice.ipynb. That is the only file you need; the database setup code is built into the notebook's first cell, so there is nothing separate to keep track of and no risk of a ModuleNotFoundError from a missing companion file. Open it and run the first cell:
connection, cursor = create_database()
That builds the database and prints a short summary. Every problem after it is a markdown cell with the question and a hint, followed by an empty code cell for your own query.
Once you have given every problem a real attempt, download the solutions notebook to check your work. It is a single self-contained file too, the same ten problems with the SQL filled in and the actual output already shown, so you can read it without needing to run anything first.
Prefer to build the database in your own scripts outside a notebook? The same setup code is also available on its own as movie_rental_db.py, importable the usual way once it's saved next to your script.
SQLite, not MySQL. The notebook runs on Python's built-in sqlite3, so nothing needs installing, but that means a few function names above do not carry over unchanged: YEAR() becomes strftime('%Y', ...), DATE_ADD() becomes date(..., '+n days'), DATEDIFF() becomes a julianday() subtraction, and CONCAT() becomes ||. Both notebooks open with a short table mapping every function used to its SQLite spelling, the same "names are not portable" lesson from Section 4.11a, this time firsthand.