- Learn
- Computer Science & IT
- Programming
- SQL Basics
- SQL CASE and Functions
SQL CASE, Functions and Calculated Columns
BeginnerLesson 3 of 6 in SQL Basics Course
What you will learn
- Compute new values for every row with arithmetic and functions, give the results clear names, and keep data types from changing the answer.
- Compute new columns and name them with aliasesWrite expressions in the SELECT list, label the results with AS, and know where in a query those labels can be used.
- Calculate with arithmetic operators and number functionsUse +, -, *, / and % in the right order, avoid integer-division surprises, and round results with ROUND and related functions.
- Convert values between data typesRecognize the type of each value, convert it explicitly with CAST, and spot the automatic conversions that change sorting, comparisons and results.
- Compute new columns and name them with aliases
- Predict what NULL does to calculations, comparisons and filters, and replace or create missing values on purpose.
- Predict how NULL behaves in calculations and conditionsExplain what NULL means, how it spreads through expressions, and why a condition that meets NULL is neither true nor false.
- Replace and create missing values with COALESCE and NULLIFFill gaps with COALESCE, turn zeros and placeholders into NULL with NULLIF, and choose replacement values that keep the meaning of the data.
- Predict how NULL behaves in calculations and conditions
- Join, tidy, measure and cut text values so that names, email addresses and codes come out consistent and usable.
- Join and clean text valuesConcatenate text, change letter case, trim spaces and replace characters, and nest these functions to clean messy input.
- Measure and extract parts of textCount characters, find where a character appears, and cut out pieces of text using positions that start at 1.
- Join and clean text values
- Write and read date values correctly, take them apart and format them, and calculate with days, months and time spans.
- Write, store and read date and time valuesKnow the date and time types, write dates in the unambiguous ISO format, convert text to dates and get the current date and time.
- Extract and format parts of datesPull out the year, month or weekday, group dates by period, and turn dates into readable text.
- Calculate with dates and filter date rangesAdd days and months, measure the time between two dates, and write date filters that include every row in a period.
- Write, store and read date and time values
- Turn conditions into labels, categories and custom sort orders with CASE, and combine it with the other functions in complete report queries.
- Sort rows into categories with searched CASEWrite CASE WHEN … THEN … ELSE … END, order the conditions so each row lands in the right group, and handle boundary values and NULLs.
- Use simple CASE and put CASE to work across a queryTranslate codes with simple CASE, build flags and custom sort orders, and recognize the shorter forms that other databases offer.
- Combine functions into a complete report queryBuild a report with several calculated columns step by step, nest functions correctly, and check each column against rows worked out by hand.
- Sort rows into categories with searched CASE
SQL CASE and Functions study guide
1Build calculated columns with expressions and aliases
Calculated columns turn the values already in a row into useful answers: a line total, a discount, or a percentage change. Clear aliases make those results readable, while parentheses and data types determine what SQL actually calculates. You will build expressions from small steps, see why integer division can erase a fraction, and choose explicit conversions when the stored form does not suit the operation.
1.1 Compute new columns and name them with aliases
A result column can show a value that is not stored anywhere in the table. SQL calculates it from the current row. An expression combines columns, fixed values, operators, and function calls.
Calculate a value for each row
SELECT order_id,
quantity * unit_price AS line_total
FROM orders;
For quantity 3 and unit price 12.50, the new column shows 37.5. Each row supplies its own quantity and price. The query leaves the stored columns unchanged. A single-row function, such as ROUND(unit_price, 0), also returns one value per input row. Adding it to the SELECT list does not combine rows. An aggregate function such as SUM or AVG instead calculates a value for a group.
The name after AS is a column alias: a heading for the result. Without it, SQLite uses the expression text as the heading; PostgreSQL uses ?column? for this arithmetic expression. AS is optional, but it makes a name easier to distinguish from a missing comma.
Choose names and quotes
Use a simple name such as line_total: letters, digits, and underscores, starting with a letter. A name containing a space or a reserved word needs double quotes: AS "Line Total" or AS "order". Single quotes mark text values, such as 'Boston'. MySQL also accepts backticks for names, and SQL Server accepts square brackets.
PostgreSQL folds unquoted names to lowercase: AS LineTotal produces linetotal. Double quotes keep the spelling in AS "LineTotal". SQLite keeps an alias as typed. Lowercase names with underscores work neatly in both.
Use an alias at the right stage
The logical order is FROM, WHERE, SELECT, then ORDER BY. The table is chosen and rows are filtered before output expressions receive their aliases. Sorting happens afterward.
SELECT order_id,
quantity * unit_price AS line_total
FROM orders
WHERE quantity * unit_price > 100
ORDER BY line_total DESC;
ORDER BY can use line_total. Standard SQL, PostgreSQL, and MySQL cannot use that new alias in WHERE; repeat the expression there. SQLite accepts an alias in WHERE as an extension, but that form will fail when moved to PostgreSQL.
A new alias is also unavailable to another expression in the same SELECT list. After unit_price * 1.08 AS with_tax, writing with_tax - unit_price AS tax fails if the table has no with_tax column. Write unit_price * 1.08 - unit_price AS tax instead.
Diagnose a surprising result
SELECT first_name last_name FROM customers; is valid SQL. It displays one column of first names with the alias last_name. To retrieve both columns, write SELECT first_name, last_name FROM customers;. A missing comma can therefore give a misleading result without an error.
For a quick expression check, a table is unnecessary: SELECT 19.99 * 3 AS total; returns 59.97 in SQLite and PostgreSQL. Oracle before Release 23 requires FROM DUAL for this query; Release 23 and later also allow SELECT without FROM.
Key Takeaways
- A calculated column computes a value for each row without changing stored data.
- A single-row function preserves rows; an aggregate combines values across a group.
- Use double quotes for names that need quoting and single quotes for text values.
- ORDER BY can use an output alias; repeat the expression in a portable WHERE clause.
- A new alias is unavailable elsewhere in the same SELECT list.
- A missing comma can turn the next column name into an alias.
1.2 Calculate with arithmetic operators and number functions
Arithmetic expressions turn raw quantities into amounts, rates, and differences. The operators are +, -, *, /, and %; modulo gives the remainder after division.
Control the calculation order
Multiplication, division, and modulo come before addition and subtraction. Equal-rank operators run left to right. Thus 10 + 3 * 2 gives 16, while (10 + 3) * 2 gives 26.
For quantity 3, unit price 12.50, and discount percentage 10:
quantity * unit_price * (1 - discount_pct / 100.0)
First 10 / 100.0 gives 0.1. The parentheses give the fraction paid, 0.9. The gross amount is 37.5, so the net amount is 33.75. Without parentheses, quantity * unit_price * 1 - discount_pct / 100.0 subtracts 0.1 from 37.5 and gives 37.4.
Keep fractions during division
In SQLite, PostgreSQL, and SQL Server, integer divided by integer drops the fraction toward zero: 7 / 2 gives 3, and -7 / 2 gives -3. MySQL and Oracle give a decimal answer for /. In SQLite and PostgreSQL, make an operand decimal before dividing: 7 / 2.0, 7 * 1.0 / 2, or CAST(7 AS REAL) / 2 gives 3.5. CAST(7 / 2 AS REAL) is too late and gives 3.0.
For a rise from 80 to 92, (92 - 80) * 100.0 / 80 gives 15.0 percent. Dividing the integer difference by 80 first loses the fraction. Modulo keeps the leftover part: 135 / 60 is 2 hours and 135 % 60 is 15 minutes. In SQLite and PostgreSQL, -7 % 3 gives -1; the remainder follows the left operand's sign. Oracle writes MOD(17, 5) instead of %.
A zero divisor raises an error in PostgreSQL and SQL Server; SQLite and MySQL return NULL. amount / NULLIF(divisor, 0) makes the zero case return NULL across these systems.
Round or choose a whole number
ROUND(x, n) rounds to n decimal places. A lamp priced at 24.00 with cost 15.50 has profit 8.5. ROUND((price - cost) / price * 100, 1) gives a margin of 35.4 percent. PostgreSQL supports negative digits: ROUND(1234.5678, -2) gives 1200; SQLite treats the digits as zero and gives 1235.0.
| Function | Meaning | Example |
|---|---|---|
ABS | Distance from zero | ABS(-4.2) gives 4.2 |
CEIL | Smallest integer at least as large | CEIL(4.1) gives 5 |
FLOOR | Largest integer at most as large | FLOOR(-4.1) gives -5 |
TRUNC | Remove the fraction toward zero | TRUNC(42.8) gives 42 |
PostgreSQL provides these functions. SQLite provides ABS; CEIL, FLOOR, and TRUNC require a build with math functions, available from 3.35.
Match precision to the data
SQLite ROUND and PostgreSQL's numeric ROUND round exact halves away from zero: ROUND(-2.5) gives -3. PostgreSQL floating-point rounding usually takes ties to the nearest even integer; its two-argument ROUND requires numeric, so use ROUND(CAST(x AS numeric), 2) for a double-precision column.
Floating-point numbers approximate many decimal fractions. SQLite can make 0.1 + 0.2 = 0.3 false and ROUND(2.675, 2) return 2.67. PostgreSQL's decimal literals use exact numeric. For exact money, use PostgreSQL NUMERIC/DECIMAL or integer cents in SQLite. Declaring SQLite DECIMAL does not create exact decimal storage. Keep precision through calculations and round for display.
Key Takeaways
- Parentheses control which amounts are combined before multiplication or division.
- Convert an operand before integer division; converting the result cannot restore lost digits.
- Modulo returns a remainder, such as minutes left after full hours.
- ROUND, FLOOR, CEIL, and TRUNC make different choices about a fractional value.
- Floating-point rounding and available number functions depend on type and database.
- Use exact numeric storage in PostgreSQL or integer cents in SQLite for exact amounts.
- NULLIF can make a zero divisor produce NULL instead of an error.
1.3 Convert values between data types
A value's data type affects arithmetic, comparisons, and sorting. The number 42 is different from the text '42', even when a result grid displays them alike.
Convert a value explicitly
Common types include INTEGER for whole numbers, NUMERIC or DECIMAL for exact decimals in PostgreSQL, REAL or double precision for approximate numbers, TEXT or VARCHAR for text, and DATE, TIME, TIMESTAMP, and BOOLEAN where supported.
SELECT CAST('42' AS INTEGER) AS number_value,
CAST(42 AS TEXT) AS text_value;
CAST(expression AS type) is standard SQL. The first result is a number; the second is text. PostgreSQL also accepts '42'::integer. Casting NULL still gives NULL.
For decimal division in SQLite, write CAST(quantity AS REAL) / 4; in PostgreSQL, write CAST(quantity AS NUMERIC) / 4. Quantity 7 gives 1.75. SQLite's CAST(7 AS NUMERIC) / 2 still gives 3: NUMERIC leaves that whole value stored as an integer. REAL is the suitable SQLite cast for this purpose.
Know what an integer cast does
| Input and cast | SQLite | PostgreSQL |
|---|---|---|
CAST(3.9 AS INTEGER) | 3 | 4 |
CAST(-3.9 AS INTEGER) | -3 | -4 |
CAST('abc' AS INTEGER) | 0 | Error |
SQLite removes a fraction toward zero. PostgreSQL rounds a numeric value to the nearest integer. Use ROUND, TRUNC, FLOOR, or CEIL first when the intended rule matters. PostgreSQL rejects text that is not a valid number. SQLite reads a leading numeric part: '12abc' becomes 12 and '12.5' becomes 12 when cast to INTEGER. Zero after a SQLite cast therefore does not prove that the original text was a valid zero. SQL Server's TRY_CAST returns NULL on a failed conversion.
Separate quantities from identifiers
Text sorts character by character. The strings '9', '10', and '100' sort as '10', '100', '9'. To order genuine numeric quantities kept as text, use ORDER BY CAST(code AS INTEGER) after ensuring they are valid numbers.
A ZIP code such as '02134' is an identifier. Casting it gives 2134 and loses the first zero. Phone numbers also belong as text: their digits label something, rather than describe an amount to add or average.
Recognize automatic conversions
Both SQLite and PostgreSQL convert the number to text in 'Order #' || 42, producing 'Order #42'. Both also evaluate the quoted literal in '5' + 3 as a number. In PostgreSQL, that literal initially has an open type; a TEXT column does not have the same freedom. Adding 3 to a TEXT column fails, so cast the column explicitly.
SQLite gives columns a type preference, called type affinity, and can keep different value types in one ordinary column. typeof(42), typeof(4.2), typeof('42'), and typeof(NULL) report 'integer', 'real', 'text', and 'null'. SQLite has no dedicated DATE type: CAST('2026-03-14' AS DATE) gives 2026. Use date('2026-03-14') for its date functions.
Oracle provides TO_NUMBER, TO_CHAR, and TO_DATE. PostgreSQL's TO_CHAR and TO_DATE take format patterns such as 'MM/DD/YYYY'. SQL Server's CONVERT uses a numeric style code: CONVERT(date, '03/14/2026', 101) reads the US date layout.
Key Takeaways
- CAST explicitly changes a value to a requested type; NULL remains NULL.
- SQLite truncates numeric-to-integer casts, while PostgreSQL rounds them.
- SQLite can turn invalid numeric text into a partial number or zero.
- Keep ZIP codes and phone numbers as text to preserve their identity.
- Text sorting and numeric sorting can give different orders.
2Handle missing values with NULL logic and COALESCE
A missing value can change a calculation and also change which rows a filter keeps. Understanding NULL means following both the data and the condition that uses it. You will distinguish missingness from zero and empty text, trace TRUE, FALSE, and UNKNOWN through a query, and choose fallbacks that preserve the meaning of a report. COALESCE and NULLIF then let you fill gaps or create them deliberately.
2.1 Predict how NULL behaves in calculations and conditions
NULL marks a missing or unknown value. A blank result cell might display NULL, but NULL is not an ordinary value that you compare with =.
Follow a missing input through an expression
Zero is a known number. Empty text, '', is a known string with no characters. The text 'N/A' is a recorded label. NULL says that no value is available. Oracle treats empty text as NULL; SQLite and PostgreSQL distinguish them.
Most arithmetic and functions pass a missing input on as NULL. With salary 50000 and bonus NULL, salary + bonus gives NULL. The total is unknown because the bonus is unknown. UPPER(NULL), ROUND(NULL, 2), and 'Ana ' || NULL also return NULL in SQLite and PostgreSQL.
Test for NULL directly
bonus = NULL, bonus <> NULL, and even NULL = NULL produce UNKNOWN. Two missing values are not known to be equal. Use bonus IS NULL or bonus IS NOT NULL. These tests always return TRUE or FALSE.
SQL conditions have three possible results: TRUE, FALSE, and UNKNOWN. SQLite displays TRUE as 1, FALSE as 0, and UNKNOWN as NULL.
| Condition | Result | Reason |
|---|---|---|
FALSE AND UNKNOWN | FALSE | One false requirement rules out AND |
TRUE AND UNKNOWN | UNKNOWN | The second requirement is unresolved |
TRUE OR UNKNOWN | TRUE | One true alternative establishes OR |
FALSE OR UNKNOWN | UNKNOWN | The remaining alternative is unresolved |
NOT UNKNOWN | UNKNOWN | Reversing an unknown does not resolve it |
Understand which rows WHERE keeps
WHERE keeps only TRUE. FALSE and UNKNOWN both leave the row out. For bonuses 1000, 0, NULL, 2500, and NULL, WHERE bonus < 1000 keeps only the zero. WHERE bonus >= 1000 keeps 1000 and 2500. These filters cover three rows, not five. WHERE NOT (bonus < 1000) also keeps only the two known bonuses at least 1000.
The same issue affects <>. With cities Boston, Denver, NULL, Boston, and Austin, WHERE city <> 'Boston' keeps Denver and Austin. Include the unknown city explicitly:
WHERE city <> 'Boston' OR city IS NULL
This keeps three rows. NOT does not automatically bring missing values back.
Watch lists and comparisons
3 IN (1, 2, NULL) is UNKNOWN: the missing member could be 3. For x NOT IN (1, 2, NULL), a matching value fails NOT IN and any unmatched value gives UNKNOWN. In a WHERE clause this fixed list keeps no rows. Remove an unintended NULL from an exclusion list before using it.
For equality that treats two NULLs as equal, use a IS NOT DISTINCT FROM b. Its opposite, a IS DISTINCT FROM b, is TRUE when one value is NULL and the other is not; two NULLs are not distinct. PostgreSQL and SQLite 3.39 onward support these forms. SQLite also has IS and IS NOT; MySQL writes NULL-safe equality as <=>.
NULL sorting defaults differ: SQLite puts NULL first in ascending order; PostgreSQL puts it last. State the placement when it matters: ORDER BY bonus DESC NULLS LAST puts known bonuses in descending order and missing bonuses at the end.
Key Takeaways
- NULL is missing or unknown, not zero or empty text.
- Comparisons with NULL produce UNKNOWN; test missingness with IS NULL.
- WHERE keeps only TRUE, so NOT and <> do not restore unknown rows.
- AND and OR can resolve an unknown only when the other condition decides the result.
- A NULL in a NOT IN list prevents that condition from being TRUE.
- IS NOT DISTINCT FROM treats two NULLs as equal.
- NULLS FIRST or NULLS LAST makes sorting explicit.
2.2 Replace and create missing values with COALESCE and NULLIF
Missing values sometimes need a fallback, and sometimes should stay missing. The choice is part of what a report means. COALESCE fills a gap; NULLIF turns a matching placeholder into a gap.
Choose the first available value
COALESCE(a, b, ...) checks left to right and returns the first non-NULL argument. It returns NULL if every argument is NULL.
SELECT COALESCE(mobile_phone, home_phone, 'no phone') AS contact
FROM customers;
With no mobile number and home phone '555-0100', the contact is '555-0100'. If neither number is recorded, it is 'no phone'. A known zero or empty string counts as available: COALESCE skips NULL, not values that look blank or false.
For pay, salary + COALESCE(bonus, 0) gives 50000 when salary is 50000 and bonus is NULL. This is appropriate when a missing bonus means none was paid. A missing product rating does not mean a rating of zero; replacing it would invent a poor rating. Leave that number missing or create a text display label.
Keep fallback types compatible
PostgreSQL cannot combine integer rating with text 'not rated' in one numeric result. Convert the known ratings to text first:
COALESCE(CAST(rating AS TEXT), 'not rated')
A rating of 4 displays as text '4'; a NULL displays as 'not rated'. SQLite accepts mixed numeric and text arguments, but then the output can contain different types. Make the intended result type clear when moving a query between databases.
COALESCE is standard SQL and accepts two or more arguments. The common two-argument alternatives are IFNULL in SQLite and MySQL, ISNULL in SQL Server, and NVL in Oracle.
Turn a placeholder into NULL
NULLIF(a, b) returns NULL if the arguments are equal; otherwise it returns the first argument. NULLIF(0, 0) is NULL, while NULLIF(5, 0) is 5.
A useful rate expression is:
orders * 100.0 / NULLIF(visits, 0) AS conversion_pct
With 12 orders and 300 visits, the divisor stays 300 and the rate is 4.0. With zero visits, the divisor becomes NULL and the rate becomes NULL. PostgreSQL avoids a division-by-zero error, and the result correctly marks an undefined rate. Replacing that rate with zero would make a different claim.
Clean gaps before supplying a label
COALESCE(NULLIF(phone, ''), 'unknown')
The inner NULLIF turns an empty string into NULL. The outer COALESCE then labels either an original NULL or an empty string as 'unknown'. A recorded phone number passes through unchanged.
For an optional middle name, include its trailing space inside the fallback:
first_name || ' ' || COALESCE(middle_name || ' ', '') || last_name
A NULL middle name contributes nothing, producing 'Ana Lopez'. A middle name of 'Maria' contributes 'Maria ', producing 'Ana Maria Lopez'.
These expressions change the query result only. They leave the original values stored in the table. A filter such as WHERE COALESCE(bonus, 0) = 0 groups known zero bonuses and missing bonuses together, so use it when those are the two groups you want.
Key Takeaways
- COALESCE returns the first non-NULL argument, including a known zero or empty text.
- A fallback must express the meaning of the missing data and have a compatible type.
- NULLIF returns NULL on equality and otherwise returns its first argument.
- Use NULLIF around a divisor to make a zero denominator return NULL.
- Clean a placeholder with NULLIF before applying a display fallback with COALESCE.
3Clean, combine and take apart text with string functions
Text from a table often needs a usable form: a full name, a consistent email address, or a piece of a code. SQL can join and clean strings, count their characters, and extract the part before or after a separator. You will follow the character positions through each step and check how spaces, letter case, and NULL affect the result. SQLite and PostgreSQL share many functions but differ on several names and behaviors.
3.1 Join and clean text values
SQL treats a text value as a string of characters. Text functions can build a full name or tidy an email address for comparison. Their results appear in the query; stored text stays unchanged.
Write and join text
Text literals use single quotes. To include an apostrophe, double it: 'O''Brien' represents "O'Brien". A pair of single quotes, '', is empty text.
SELECT first_name || ' ' || last_name AS full_name
FROM customers;
The concatenation operator || joins the pieces exactly as written. With 'Ana' and 'Lopez', the result is 'Ana Lopez'. The middle literal supplies the space; concatenation does not insert one automatically.
SQLite, PostgreSQL, and Oracle use ||. MySQL's default || means OR, so use CONCAT there. SQL Server uses + and also supports || from 2025. To join a numeric order ID, SQLite and PostgreSQL convert it to text in 'Order #' || order_id. With SQL Server's +, cast the number first: 'Order #' + CAST(order_id AS VARCHAR(10)).
Choose how to join a missing part
In SQLite and PostgreSQL, a NULL operand makes a || result NULL. Oracle treats that missing part as empty text. CONCAT also differs by database:
| Database | CONCAT('Ana', NULL, 'Lopez') |
|---|---|
| PostgreSQL, SQL Server, SQLite 3.44 onward | 'AnaLopez' |
| MySQL | NULL |
Oracle's CONCAT takes exactly two arguments. CONCAT_WS(' ', first_name, middle_name, last_name) places spaces between non-NULL parts, so a missing middle name gives 'Ana Lopez'. SQLite added CONCAT and CONCAT_WS in 3.44. Skipping NULL still does not remove an empty string.
Change case and trim edges
UPPER('boston') gives 'BOSTON'; LOWER produces lowercase. PostgreSQL's INITCAP('ana LOPEZ') gives 'Ana Lopez'. SQLite has no INITCAP, and its built-in case conversion changes A–Z only: UPPER('é') stays 'é' without the ICU extension. PostgreSQL case conversion follows its locale.
TRIM removes spaces at both ends; LTRIM removes them only at the start and RTRIM only at the end. TRIM(' New York ') gives 'New York', keeping the two spaces inside. To remove x from both ends, SQLite writes TRIM('xxBostonxx', 'x'); PostgreSQL writes TRIM(BOTH 'x' FROM 'xxBostonxx').
Replace characters and nest cleaning steps
REPLACE('555-0100', '-', '') gives '5550100'. It replaces every exact occurrence. Letter case matters: replacing 'st' in 'Main St' changes nothing because 'St' has a capital S.
SELECT LOWER(TRIM(email)) AS email_clean
FROM customers;
For ' Ana@Example.COM ', TRIM first produces 'Ana@Example.COM', then LOWER produces 'ana@example.com'. Nesting means the innermost function supplies the input of the next.
Ordinary text equality is case-sensitive by default in SQLite and PostgreSQL. WHERE LOWER(TRIM(email)) = 'ana@example.com' can match differently capitalized input with edge spaces. MySQL's default collation ignores case, and SQLite LIKE ignores A–Z case even though its default = comparison does not.
Key Takeaways
- Use single quotes for text and double an apostrophe inside a literal.
- Concatenation joins exact pieces; include separators explicitly.
- NULL behavior differs between ||, CONCAT, and database dialects.
- TRIM removes edge spaces, while REPLACE changes matching characters throughout.
- Nested functions run from the inside out.
- Case conversion and case-sensitive matching depend on the database and locale.
3.2 Measure and extract parts of text
Text functions locate and cut pieces by character position. SQL starts positions at 1, so the first character is position 1. A found position and a number of characters are different inputs.
Measure and cut a string
LENGTH('Boston') gives 6. Spaces count: LENGTH(' Bo ') gives 4. Empty text has length 0; LENGTH(NULL) is NULL. MySQL's LENGTH counts bytes, so use CHAR_LENGTH for characters there. SQL Server uses LEN, which ignores trailing spaces.
SELECT SUBSTR('Boston', 1, 3) AS beginning,
SUBSTR('Boston', 4) AS ending;
The results are 'Bos' and 'ton'. The first call takes three characters starting at position 1. The second omits the count and takes everything from position 4 onward. PostgreSQL also writes SUBSTRING('Boston' FROM 4 FOR 3). SQLite accepts SUBSTRING as a name from 3.34. SQL Server uses SUBSTRING(text, start, count); the count is required through 2022 and optional from 2025.
SQLite can count from the right: SUBSTR('Boston', -3) gives 'ton'. PostgreSQL negative starts do not count backward from the end. Use RIGHT('Boston', 3) there; LEFT takes characters from the beginning. SQLite has neither LEFT nor RIGHT.
Find the split point
SQLite's INSTR(text, search) returns the first matching position. PostgreSQL uses STRPOS or POSITION(search IN text). These functions return 0 when no match is found. The search is case-sensitive: INSTR('Ana', 'a') is 3.
For 'ana@example.com', the @ is at position 4. The user name ends just before it; the domain starts just after it:
SUBSTR(email, 1, INSTR(email, '@') - 1) AS user_name,
SUBSTR(email, INSTR(email, '@') + 1) AS domain
The first expression becomes SUBSTR(email, 1, 3) and yields 'ana'. The second starts at 5 and yields 'example.com'. Omitting - 1 includes @ in the user name. Omitting + 1 includes it at the start of the domain. In PostgreSQL, substitute STRPOS for INSTR, or use SPLIT_PART(email, '@', 2) for the domain.
Handle a missing separator
For 'anaexample.com', INSTR returns 0. The domain formula starts at 1 and misleadingly returns the whole string. SQLite's user-name formula gives empty text; PostgreSQL rejects its negative substring length. Guard the extraction:
CASE
WHEN INSTR(email, '@') > 0
THEN SUBSTR(email, INSTR(email, '@') + 1)
END
This returns NULL when the separator is absent. The guard checks for a separator, not every rule of a valid email address.
Build short labels and fixed-width codes
SUBSTR(first_name, 1, 1) || '. ' || last_name gives 'A. Lopez' for Ana Lopez. WHERE LENGTH(zip_code) <> 5 finds recorded ZIP strings of the wrong length; NULL lengths produce UNKNOWN and are left out.
PostgreSQL's LPAD('42', 5, '0') gives '00042'; RPAD pads on the right. SQLite can build that code with SUBSTR('00000' || 42, -5). Keeping only five characters also cuts off a longer value, so this fixed-width expression needs a code that fits.
Key Takeaways
- SQL string positions start at 1; LENGTH includes spaces.
- SUBSTR takes a start position and an optional character count.
- INSTR, STRPOS, and POSITION return 0 when a match is absent.
- Use position minus 1 before a separator and position plus 1 after it.
- Check for a missing separator before extracting a piece.
- End extraction and padding have different names in SQLite and PostgreSQL.
4Work with dates and times
Date work starts with a clear distinction between a calendar day, a time, and a timestamp. From there, SQL can create month labels, calculate deadlines, measure elapsed time, and filter a complete period. You will keep date values separate from display text, account for month ends and birthdays, and compare the SQLite and PostgreSQL expressions that carry out the same task. Boundary cases make these calculations reliable.
4.1 Write, store and read date and time values
A date is easier to calculate with when its storage and spelling are clear. The text '03/04/2026' could mean March 4 or April 3. An ISO date, '2026-03-14', puts year, month, and day in an unambiguous order.
Match the value to its type
PostgreSQL has DATE for a calendar day, TIME for a time of day, and TIMESTAMP for a date and time. Its TIMESTAMP WITH TIME ZONE, also called timestamptz, stores a moment in UTC and displays it in the session's time zone. A plain TIMESTAMP does not attach that zone meaning.
SQLite stores dates and times as ISO text or as numbers, not dedicated date/time types. Numeric forms include Julian day numbers and Unix time. The date functions interpret these values and return a date, time, or numeric measure.
Use fixed-width ISO text such as '2026-03-14', including leading zeros. Add a time using the 24-hour clock: '2026-03-14 09:30:00'. PostgreSQL's typed literal DATE '2026-03-14' explicitly creates a date.
Preserve chronological order
Fixed-width ISO text compares in date order because it starts with the largest part. '2026-03-14' comes before '2026-11-02'. US display text does not have this property: as strings, '11/02/2025' sorts after '03/14/2026' even though its date is earlier.
SQLite's parser needs the supported layout. date('2026-3-14') returns NULL because the month is not two digits. It can also normalize an impossible date: date('2026-02-30') returns '2026-03-02'. A non-NULL result alone is not proof that the input was a valid calendar day. PostgreSQL rejects DATE '2026-02-30'.
Read text in another layout
PostgreSQL and Oracle use TO_DATE('03/14/2026', 'MM/DD/YYYY') to read a US layout. PostgreSQL also reads ISO text with CAST('2026-03-14' AS DATE) or '2026-03-14'::date.
SQLite has no TO_DATE parser. For fixed MM/DD/YYYY text in d, rebuild ISO text:
SUBSTR(d, 7, 4) || '-' ||
SUBSTR(d, 1, 2) || '-' || SUBSTR(d, 4, 2)
The pieces are the year '2026', month '03', and day '14'. The result is '2026-03-14', which SQLite's date functions can read. The positions rely on the input actually using the fixed layout.
Get the current date and time
CURRENT_DATE and CURRENT_TIMESTAMP work in SQLite and PostgreSQL. SQLite returns UTC text; date('now', 'localtime') gives the local date. Near midnight, UTC and the local calendar date can differ.
PostgreSQL uses the session's time zone. CURRENT_TIMESTAMP and NOW() reflect the start of the current transaction, so waiting inside that transaction does not advance their value. MySQL offers NOW() and CURDATE(); SQL Server has GETDATE() and SYSDATETIME(); Oracle has SYSDATE, which includes a time, and CURRENT_DATE.
To check a due-date calculation, first use a fixed anchor such as DATE '2026-10-21' in PostgreSQL or '2026-10-21' in SQLite. You can then compare the result with a calendar without the answer changing the next day.
Key Takeaways
- DATE is a calendar day; TIMESTAMP adds a time of day.
- Use fixed-width YYYY-MM-DD for portable, unambiguous date text.
- ISO text sorts chronologically when its layout and time basis are consistent.
- A SQLite date result can normalize an impossible input instead of rejecting it.
- Current-date functions use different time-zone and transaction rules.
4.2 Extract and format parts of dates
Extract a date part when you need a year or weekday to calculate with. Format a date when you need text for a reader. These operations can produce different types even when the displayed characters look alike.
Extract a year, month, or weekday
PostgreSQL uses standard EXTRACT(part FROM value). For DATE '2026-03-14', extracting YEAR gives 2026 and MONTH gives 3, both as numbers. MySQL and Oracle also support EXTRACT.
SQLite uses strftime(format, value), which returns text:
| SQLite pattern | Result for '2026-03-14' |
|---|---|
'%Y' | '2026' |
'%m' | '03' |
'%d' | '14' |
'%w' | '6' |
SQLite's %w and PostgreSQL's DOW number Sunday as 0 through Saturday as 6. March 14, 2026, is Saturday. PostgreSQL ISODOW instead numbers Monday as 1 through Sunday as 7. A SQLite weekend filter is WHERE strftime('%w', order_date) IN ('0', '6').
Compare the right types
strftime('%m', order_date) = 3 is false for a March date in SQLite: the function returns text '03', not number 3. Compare it with = '03', or convert first:
CAST(strftime('%m', order_date) AS INTEGER) = 3
This is also useful in the quarter expression (CAST(strftime('%m', d) AS INTEGER) + 2) / 3. Integer division gives quarter 1 for March and quarter 4 for November. PostgreSQL has EXTRACT(QUARTER FROM d) directly.
Identify a month as a whole period
strftime('%Y-%m', order_date) creates a SQLite label such as '2026-03'. date(order_date, 'start of month') creates '2026-03-01'. PostgreSQL can use TO_CHAR(order_date, 'YYYY-MM') for a label or DATE_TRUNC('month', order_date) for the first moment of that month.
Keep the year in a period label. A bare month value of 3 would combine March from different years. The first-of-month value can remain the calculation and sorting value while a separate expression supplies the display.
Format at the output stage
In PostgreSQL, TO_CHAR(DATE '2026-03-14', 'MM/DD/YYYY') gives '03/14/2026', and the pattern 'Mon DD, YYYY' gives 'Mar 14, 2026'. Oracle uses these patterns too. SQLite's US layout is strftime('%m/%d/%Y', '2026-03-14').
Pattern letters belong to their database. PostgreSQL MM means month and MI means minute. SQLite %m means month and %M means minute; changing that capital changes the information requested.
SELECT TO_CHAR(order_date, 'MM/DD/YYYY') AS order_day
FROM orders
ORDER BY order_date;
Here PostgreSQL displays readable text but sorts the actual dates. Sorting order_day would sort the display strings by month first and could put dates from different years out of order.
Key Takeaways
- PostgreSQL EXTRACT returns numeric parts; SQLite strftime returns text.
- Compare a zero-padded SQLite month with text or cast it to a number.
- Weekday numbering depends on the requested convention.
- Keep the year in month labels and format only for display.
4.3 Calculate with dates and filter date ranges
Calendar arithmetic must account for month lengths, time components, and the kind of answer needed. A number of days, a calendar month, and a whole year of age are different measures.
Add days and calendar months
PostgreSQL writes DATE '2026-03-14' + 30; SQLite writes date('2026-03-14', '+30 days'). Both reach April 13: 17 days finish March and 13 more reach April 13. SQLite's direct expression '2026-03-14' + 30 instead gives 2056, because arithmetic reads the leading year as a number.
Month ends need an explicit policy. PostgreSQL's DATE '2026-01-31' + INTERVAL '1 month' gives February 28 at midnight, using the last available day. SQLite's date('2026-01-31', '+1 month') rolls excess days into March and gives '2026-03-03'. From SQLite 3.46, add 'floor' after the shift to choose '2026-02-28'. Oracle ADD_MONTHS also moves to the shorter month's last day.
Measure elapsed time
PostgreSQL DATE subtraction returns whole days: DATE '2026-04-13' - DATE '2026-03-14' is 30. SQLite subtracts numeric day values: julianday('2026-04-13') - julianday('2026-03-14') is 30.0. SQL Server uses DATEDIFF(day, start, end); MySQL uses DATEDIFF(end, start).
For timestamps from March 14 at 09:30 to 18:00, PostgreSQL subtraction gives an interval of 08:30:00; EXTRACT(EPOCH FROM interval) / 3600 expresses it as 8.5 hours. SQLite 3.38 onward can write:
(unixepoch('2026-03-14 18:00') -
unixepoch('2026-03-14 09:30')) / 3600.0
This gives 8.5. A Julian-day difference multiplied by 24 can give 8.49999999627471 because of floating-point approximation; round the display. A fractional timestamp duration should not be cast to INTEGER before the desired unit is calculated.
Count completed birthdays
A person born November 2, 1990, is 35 on October 21, 2026. The year numbers differ by 36, but the birthday has not arrived. PostgreSQL uses EXTRACT(YEAR FROM AGE(DATE '2026-10-21', DATE '1990-11-02')), giving 35.
SQLite subtracts one if this year's birthday is still ahead:
strftime('%Y', '2026-10-21') - strftime('%Y', birth_date)
- (strftime('%m-%d', '2026-10-21') < strftime('%m-%d', birth_date))
The comparison yields 1 when the birthday is ahead and 0 otherwise. Dividing elapsed days by 365 is unreliable: on November 1, 2026, this person has lived 13148 days, which divided by 365 gives 36.02 even though the 36th birthday is tomorrow. SQL Server DATEDIFF counts boundaries: crossing December 31 to January 1 counts as a year boundary, not a completed year of age.
Include every timestamp in a period
BETWEEN includes both endpoints. For DATE columns, March 1 through March 31 covers March. For timestamps, an upper bound of '2026-03-31' means midnight at the start of the last day and misses the rest of it.
WHERE ordered_at >= '2026-03-01'
AND ordered_at < '2026-04-01'
This half-open range includes the start and excludes the next period's start, covering every March time without choosing a final fraction of a second.
Overdue dates satisfy due_date < CURRENT_DATE. PostgreSQL's last-30-days lower bound is CURRENT_DATE - 30; SQLite's is date('now', '-30 days'). Days until a deadline are due_date - CURRENT_DATE for PostgreSQL DATE values and julianday(due_date) - julianday(date('now')) in SQLite.
Key Takeaways
- Use date arithmetic functions or typed dates, not arithmetic on SQLite date text.
- Decide how an added month should handle a day missing from the next month.
- DATE differences count days; timestamp differences can include fractions.
- Age counts completed birthdays, not elapsed days divided by 365.
- A half-open timestamp range includes the period start and excludes the next start.
- Use a date-only current value for whole-day deadline calculations.
5Classify rows with CASE expressions
A report often needs a rule that turns data into a label or a fee: a small order, a pending shipment, or a priority rank. CASE expresses that choice inside a query. You will trace which condition wins, place exact boundaries and missing values in the right category, and translate codes with simple CASE. The final report combines classification with arithmetic, text, dates, and fallbacks, checking each output against known rows.
5.1 Sort rows into categories with searched CASE
A CASE expression chooses one result for each row. You can use it to label a price band, calculate a fee, or mark a record for attention.
Read the searched form
CASE
WHEN total < 50 THEN 'small'
WHEN total < 200 THEN 'medium'
ELSE 'large'
END AS size_band
Each WHEN supplies a condition, and THEN supplies the value to return if that condition is TRUE. The database checks from top to bottom and takes the first true condition. END closes the expression; an alias can follow it. If no condition is TRUE and ELSE is absent, the result is NULL.
A total of 35 meets the first condition and becomes 'small'. A total of 50 fails the first but meets the second, becoming 'medium'. The same is true for 199.99. A total of 200 reaches ELSE and becomes 'large'.
Make order and boundaries match the rule
The second branch needs no total >= 50: a value below 50 has already returned from the first branch. Ascending < thresholds work because earlier branches remove the smaller values. Descending >= thresholds can work for the same reason.
Putting WHEN total < 200 first makes a later < 50 branch unreachable. A total of 35 already satisfies < 200, so it stops there. CASE does not choose the most specific condition or check which label sounds best.
Compare the business rule with each endpoint. < 50 places exactly 50 in the medium band; <= 50 places it in the small band. Check 49.99, 50, 199.99, and 200 to see both sides and the boundaries. If conditions are written as explicit intervals, make their endpoints fit without a gap or unintended overlap.
Handle NULL before ordinary ranges
A NULL total makes both comparisons UNKNOWN. In the first expression it therefore reaches ELSE and becomes 'large', which is misleading. Catch missingness first:
CASE
WHEN total IS NULL THEN 'unknown'
WHEN total < 50 THEN 'small'
WHEN total < 200 THEN 'medium'
ELSE 'large'
END
ELSE means "no condition was true," so it can catch missing inputs as well as known values outside the tested ranges.
Return a consistent kind of value
PostgreSQL needs compatible THEN and ELSE types. CASE WHEN quantity > 0 THEN unit_price ELSE 'n/a' END fails if unit_price is numeric, because 'n/a' cannot become a number. Use NULL for an unavailable numeric result, or cast the price to text if the whole output is a display label. SQLite permits mixed result types, which can complicate later calculations.
A condition can combine columns:
CASE
WHEN country = 'US' AND subtotal >= 100 THEN 0
ELSE 7.99
END AS shipping_fee
The returned fee can appear inside subtotal + CASE ... END AS amount_due. A CASE is a value expression, not a command that edits several columns; each result column needs its own expression.
CASE can also guard a row-level division: CASE WHEN visits = 0 THEN NULL ELSE orders * 100.0 / visits END. A zero row selects the NULL branch. orders * 100.0 / NULLIF(visits, 0) expresses the same missing-rate rule more briefly.
Key Takeaways
- CASE returns the value from the first condition that is TRUE.
- Order overlapping range conditions so a broad test does not hide a narrower one.
- Choose < or <= to put an exact boundary in the intended band.
- Test NULL explicitly before a range when ELSE should not label it.
- Keep result types compatible and use CASE anywhere a value expression fits.
5.2 Use simple CASE and put CASE to work across a query
The simple CASE form translates one value into another. Searched CASE tests conditions; simple CASE compares the same expression with a series of values.
Translate codes with simple CASE
CASE status
WHEN 'S' THEN 'Shipped'
WHEN 'P' THEN 'Pending'
WHEN 'C' THEN 'Canceled'
ELSE 'Other'
END AS status_label
The implied tests are status = 'S', then status = 'P', then status = 'C'. A status outside these codes gets 'Other'. The searched form can write the same equality tests explicitly. Choose searched CASE for ranges, LIKE patterns, NULL tests, or conditions across several columns.
CASE region WHEN NULL THEN 'missing' ELSE region END cannot catch a NULL region. Its implied region = NULL is UNKNOWN, so the row reaches ELSE and remains NULL. Write CASE WHEN region IS NULL THEN 'missing' ELSE region END.
Assign a custom sort rank
Alphabetical priority order is high, low, medium. To sort by urgency, translate the labels to numbers:
SELECT ticket_id, priority
FROM tickets
ORDER BY CASE priority
WHEN 'high' THEN 1
WHEN 'medium' THEN 2
WHEN 'low' THEN 3
ELSE 4
END;
Ascending order now places high before medium before low, with other or missing values last. The CASE is a sort expression; it need not be a displayed column. Using DESC would reverse the numeric ranking.
Create a numeric flag
CASE WHEN discount_pct > 0 THEN 1 ELSE 0 END AS is_discounted
A positive percentage gets 1; a known zero gets 0. A NULL percentage also gets 0 because > 0 is UNKNOWN and ELSE catches it. This flag therefore means "a positive discount is recorded." It does not preserve the distinction between no discount and an unknown discount. If that distinction is needed, add an IS NULL branch with its own result.
Flags can be selected, sorted, filtered, or included in arithmetic. In a portable WHERE clause, repeat the expression or use its original condition instead of referring to a newly defined output alias.
Recognize related conditional functions
COALESCE(bonus, 0) is a short form of CASE WHEN bonus IS NOT NULL THEN bonus ELSE 0 END. NULLIF(a, b) corresponds to CASE WHEN a = b THEN NULL ELSE a END. Their names communicate these common purposes directly.
SQLite from 3.32 and SQL Server have IIF(condition, true_value, false_value); MySQL has IF. Oracle DECODE translates equality matches, for example DECODE(status, 'S', 'Shipped', 'P', 'Pending', 'Other'). DECODE treats two NULLs as equal, unlike the equality comparisons in simple CASE. Standard CASE works across these systems.
A CASE can contain another CASE, but combined conditions often make a flat expression clearer. A shipping rule can directly say WHEN country = 'US' AND subtotal >= 100 THEN 0, keeping the country and subtotal tests together.
Key Takeaways
- Simple CASE compares one expression with equality values.
- Use searched CASE for NULL tests, ranges, and combined conditions.
- A numeric CASE in ORDER BY creates a custom ranking.
- An ELSE 0 flag also assigns zero when its condition is UNKNOWN.
- COALESCE and NULLIF express common special cases of CASE.
- Oracle DECODE treats two NULLs as equal; simple CASE does not.
5.3 Combine functions into a complete report query
A report can clean names, calculate net totals, identify months, and label shipping speed in one SELECT. Build each expression from its inputs and check the result before combining it with the others.
Work from known rows
An orders table contains these records. Dates are ISO text for the SQLite query below; quantity is an integer and unit_price is numeric.
| Order | Customer | Quantity | Price | Discount % | Ordered | Shipped |
|---|---|---|---|---|---|---|
| 1001 | Ana Lopez | 3 | 12.50 | 10 | 2026-03-14 | 2026-03-17 |
| 1002 | Ben Okafor | 1 | 240.00 | 0 | 2026-03-30 | NULL |
| 1003 | Chen Wu | 12 | 4.25 | NULL | 2026-04-02 | 2026-04-03 |
Here a missing discount means no discount. Start by retrieving the raw fields. Add one calculated column at a time and use LIMIT 5 while checking a small sample.
Trace a nested calculation
ROUND(quantity * unit_price *
(1 - COALESCE(discount_pct, 0) / 100.0), 2)
COALESCE provides a known discount first. Dividing by 100.0 preserves its fraction. Subtracting from 1 gives the fraction paid, multiplication gives the net amount, and ROUND sets the final precision. Ana's amount is 33.75, Ben's is 240.0, and Chen's is 51.0. Chen's NULL discount becomes zero; a missing shipping date, by contrast, should stay missing in the day count.
Assemble the SQLite report
SELECT order_id,
first_name || ' ' || last_name AS customer,
ROUND(quantity * unit_price *
(1 - COALESCE(discount_pct, 0) / 100.0), 2) AS net_total,
strftime('%Y-%m', order_date) AS order_month,
CAST(julianday(shipped_date) - julianday(order_date)
AS INTEGER) AS days_to_ship,
CASE
WHEN shipped_date IS NULL THEN 'not shipped'
-- fast = shipped within 2 days
WHEN julianday(shipped_date) - julianday(order_date) <= 2
THEN 'fast'
ELSE 'standard'
END AS shipping
FROM orders
ORDER BY order_id;
Each expression has a clear output alias. The numeric day difference is cast to INTEGER because these are date-only values. The IS NULL branch comes before the speed comparison, so an unshipped order receives a useful label.
The output is:
| Order | Customer | Net total | Month | Days | Shipping |
|---|---|---|---|---|---|
| 1001 | Ana Lopez | 33.75 | 2026-03 | 3 | standard |
| 1002 | Ben Okafor | 240.0 | 2026-03 | NULL | not shipped |
| 1003 | Chen Wu | 51.0 | 2026-04 | 1 | fast |
Adapt types and query clauses
For PostgreSQL DATE columns, use TO_CHAR(order_date, 'YYYY-MM') for the month and shipped_date - order_date for the day count and CASE comparison. Keep unit_price as exact numeric for two-argument ROUND. The concatenation, discount fallback, and classification rule keep the same meaning.
To filter for rounded totals above 50 in both databases, repeat the full net expression in WHERE. Orders 1002 and 1003 remain. SQLite also permits WHERE net_total > 50, but PostgreSQL cannot see that new alias at the filtering stage. ORDER BY net_total DESC works in both.
When moving a report, check the types and function names in each expression. A text function must receive text, a date calculation must receive a supported date value, and a numeric fallback must express the intended rule. Keep each output on its own line; a short comment can define a business threshold without obscuring the SQL.
Key Takeaways
- Build and check one calculated column at a time on a small sample.
- Nested functions pass their results outward; keep each input type suitable.
- A missing discount and a missing shipping date can need different handling.
- Use dialect-appropriate date expressions while keeping the report rules consistent.
- Filter with the full expression and sort with its output alias when appropriate.
SQL CASE and Functions examples
1Build and check a discounted line total
16 steps
- 1IntroductionTurn one row into a useful amount
An order line records eight items at 7.50 each with a 20 percent discount. The report needs a net amount, but the table stores only the inputs. You will build the expression in stages, name its output, and check what integer division does to the discount.
The examples use SQLite and PostgreSQL. Where their type rules differ, the database name identifies the result.
- 2ExplanationStart with an expression
An expression combines values to produce another value. Here
quantity * unit_priceuses two columns from the current row.SELECT quantity * unit_price AS gross_total FROM order_lines;gross_totalis the output alias. For the sample row, the new value is60.0in SQLite. Each other row supplies its own inputs; the query does not write these totals back into the table. - 3ConnectionOne value still belongs to each row
A single-row function returns one value for each input row.
ROUND(unit_price, 0)changes the price displayed for a row, whileUPPER(item_name)changes its displayed text.These functions do not combine the order lines. An aggregate such as SUM does combine values across a group, so it answers a different kind of question. A calculation in SELECT can keep the detail of individual orders.
- 4TipName the result clearly
AS net_totalis a simple, readable alias. Use double quotes when a name has a space, as inAS "Net Total". Single quotes identify text values instead.Keep a comma between output expressions.
SELECT quantity unit_pricereturns one column of quantities headed unit_price: without a comma, the second name becomes an alias. That query can succeed while showing fewer columns than intended. - 5ChallengePlace the alias in the query
The table has only quantity and unit_price. A query defines
quantity * unit_price AS gross_total. Can a PostgreSQL WHERE filter usegross_total > 50, and can ORDER BY usegross_total DESC?---reveal---
WHERE cannot see that new alias because it logically precedes SELECT. Repeat
quantity * unit_price > 50. ORDER BY follows SELECT and can usegross_total DESC. SQLite allows the WHERE alias as an extension, but PostgreSQL does not. - 6ExampleTurn a discount into the fraction paid
The gross amount is
8 * 7.50, or 60. A 20 percent discount leaves 80 percent to pay. Express that as1 - discount_pct / 100.0.quantity * unit_price * (1 - discount_pct / 100.0)The division gives
0.2; the parentheses give0.8; multiplying gives a net amount of48.0. Parentheses keep the subtraction together as a multiplier. - 7Key InsightA later decimal cannot repair earlier division
Suppose discount_pct is an integer and the divisor is written as
100. SQLite and PostgreSQL compute20 / 100as integer division and get 0. The multiplier becomes 1, so the net wrongly stays 60.The decimal price elsewhere does not rescue that inner division. Use
100.0in the division itself. Casting the completed quotient afterward changes its type but cannot restore the missing fraction. - 8ChallengeFind the calculation that lost its fraction
A SQLite row has integer completed = 9 and integer assigned = 12. What does
completed / assigned * 100.0return, and how should the expression calculate the percentage instead?---reveal---
It returns
0.0: integer9 / 12becomes 0 first. Writecompleted * 100.0 / assigned, which gives75.0. The conversion must occur before division, not just before the final display. - 9ExplanationChoose the whole-number rule
ROUND chooses a nearby rounded value. FLOOR goes down to a whole number, CEIL goes up, and TRUNC cuts the fraction toward zero.
For
-6.4, FLOOR gives-7, while CEIL and TRUNC give-6. The direction matters for negative inputs. PostgreSQL provides these functions; SQLite CEIL, FLOOR, and TRUNC require a build with math functions. ABS, which gives distance from zero, is built into SQLite. - 10TipCheck the number type before rounding
In PostgreSQL, two-argument ROUND takes
numeric, not double precision. For a floating-point column, writeROUND(CAST(amount AS numeric), 2). SQLite accepts its own numeric ROUND form.Approximate floating-point values can produce surprising ties. For exact amounts, PostgreSQL NUMERIC/DECIMAL stores exact decimals; SQLite can keep whole cents as integers. Round the displayed result after the calculation.
- 11ExplanationMake a type change explicit
CAST(expression AS type)requests a conversion. The text'28'can become integer 28 withCAST('28' AS INTEGER). The number can become text withCAST(28 AS TEXT).For fractional division, SQLite needs REAL for a reliable noninteger operand.
CAST(7 AS NUMERIC)can still be integer 7 there. PostgreSQL NUMERIC gives an exact decimal type. A CAST of NULL remains NULL. - 12ExampleKeep identifiers separate from quantities
Suppose a label stores the ZIP code
'00421'. Converting it to an integer gives421, losing the leading zeros. It is an identifier, so arithmetic is not the task.A text column that genuinely holds quantities may need conversion for numeric sorting. Text
'12','3', and'80'sorts in that order; numeric sorting gives 3, 12, 80. Choose the conversion from the meaning of the column. - 13ChallengeInterpret a cast without trusting it as validation
SQLite runs
CAST('31units' AS INTEGER)successfully. What value does it return, and does success show that the original text was a valid integer?---reveal---
It returns 31 by reading the leading numeric portion. Success does not validate the full string. PostgreSQL rejects the same text as an integer. SQLite can also turn text with no numeric prefix into zero, masking a bad input.
- 14Key InsightAn integer cast is not the same operation everywhere
For numeric
4.7, SQLiteCAST(... AS INTEGER)truncates to 4, while PostgreSQL rounds to 5. If a report means "whole units not exceeding this value," express that with FLOOR before the cast.An implicit conversion is also database-specific. PostgreSQL can infer a number from a quoted literal in arithmetic, but a TEXT column needs an explicit cast. The visible characters alone do not establish the operand type.
- 15ChallengeCheck the complete net expression
For eight items at 7.50 with integer discount_pct = 20, a report writes
ROUND(quantity * unit_price * (1 - discount_pct / 100.0), 2) AS net_total. What is the result, and can that alias sort the output?---reveal---
The gross is 60, the fraction paid is 0.8, and the net is
48.0in SQLite.ORDER BY net_totalis valid in both SQLite and PostgreSQL because sorting can see the SELECT alias. - 16RecapRead the inputs, calculation, and output name
A calculated amount has a path: the row supplies values, arithmetic and functions produce the result, and an alias names it. Check the operation order and the operand types before accepting a displayed number.
When a result is unexpected, inspect an inner division or cast first. To filter a computed value across databases, repeat its expression in WHERE; to sort it, use the output alias in ORDER BY.
2Follow missing data through a query
12 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionA missing amount changes more than a cell
- 2ExplanationNULL is an unknown input
- 3ExampleUse a test that answers missingness
- 4Key InsightWHERE accepts only TRUE
- 5ChallengeCombine a known condition with an unknown one
- 6TipDo not leave NULL in an exclusion list
- 7ExplanationChoose a fallback from the meaning
- 8ExampleKeep the display type consistent
- 9ExampleTurn a zero denominator into a gap
- 10ChallengeClean empty text before the label
- 11ChallengeSeparate a display choice from a filter choice
- 12RecapKeep the meaning of a gap visible
3Clean a name and extract an email domain
14 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionMake text usable without losing its pieces
- 2ExampleSupply the space between names
- 3ExplanationDecide what a missing name part does
- 4ExampleClean the email from inside outward
- 5ChallengeChoose between trimming and replacing
- 6TipCheck character and dialect rules
- 7ExplanationCount from one
- 8ExampleLocate the separator
- 9ExampleTake the characters before and after @
- 10ChallengeFind the off-by-one result
- 11Key InsightA zero search result needs its own path
- 12TipTake endings and pad codes in the right dialect
- 13ChallengeCombine cleaning with extraction
- 14RecapTrace the exact characters
4Build date labels and reliable period filters
18 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionSeparate the calendar value from its label
- 2ExplanationChoose a clear date representation
- 3ExampleRead a fixed local layout
- 4ChallengeExplain which text sorts by date
- 5TipCheck parsing and the current time basis
- 6ExampleExtract the month with the right return type
- 7ExplanationKeep the year in the month label
- 8ExampleCreate a display without changing the sort
- 9ChallengeFind the correct quarter
- 10ConnectionChoose the weekday numbering convention
- 11ExampleAdd ten days to the booking
- 12Key InsightOne month is not a fixed number of days
- 13ExampleMeasure days and hours in suitable units
- 14ChallengeInclude the last afternoon of the month
- 15ExplanationCount birthdays that have actually arrived
- 16TipRead relative dates as moving boundaries
- 17ChallengeCombine a deadline and a month selection
- 18RecapCheck type, calendar rule, and endpoints
5Label orders and build the final report
17 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionGive each order a useful label
- 2ExplanationChoose the first true condition
- 3ExampleTrace the exact cutoff
- 4Key InsightELSE can also catch an unknown amount
- 5ChallengeRepair a hidden small band
- 6TipReturn compatible results
- 7ExampleTranslate one status value
- 8ChallengeWhy does WHEN NULL miss the row?
- 9ExampleSort priority by rank
- 10Key InsightA flag can merge known and unknown cases
- 11ExampleStart the report with known inputs
- 12ExampleCalculate the net amount first
- 13ExampleAdd dates with a separate missing rule
- 14ChallengeCheck a second row across both rules
- 15ConnectionTranslate the date expressions to PostgreSQL
- 16ChallengeFilter the amount that the report actually shows
- 17RecapCheck the report as a set of explicit choices
SQL CASE and Functions practice questions
Set 1: Clean text and work with calendar values10 Practice Questions
Question 1 of 10
LOWER(TRIM(' RAE@Mail.COM ')) return in SQLite?Show answerHide answer
'rae@mail.com'. Keeping the trailing space would mean that only the left edge had been trimmed; ordinary TRIM handles both edges and does not need a separate RTRIM.Question 2 of 10
first_name = 'Mira', middle_name = NULL, and last_name = 'Shah'. What do first_name || ' ' || middle_name || ' ' || last_name and CONCAT_WS(' ', first_name, middle_name, last_name) return, in that order?Show answerHide answer
|| expression propagates its NULL middle-name input, making the whole name NULL. CONCAT_WS skips the missing part and puts one separator between Mira and Shah. Two spaces would result from retaining an empty middle-name position, but the input here is NULL and CONCAT_WS skips it.Question 3 of 10
SUBSTR('Seattle', 3, 3) return?Show answerHide answer
'att'. The fragment 'eat' starts at position 2 and would follow from treating the supplied starting position as one character earlier than it actually is.Question 4 of 10
email may contain NULL or text with no @. For text containing @, return everything after its first @; otherwise return NULL. What expression fits?Show answerHide answer
>= 0 also accepts a missing separator, whose position is zero, and would return the whole malformed string.Question 5 of 10
Show answerHide answer
'2027-11-06'. The reversed ending 06-11 represents June 11, while a one-digit final day does not follow the fixed-width layout.Question 6 of 10
date('2027-04-31')?Show answerHide answer
'2027-05-01'. A non-NULL date result therefore does not guarantee that the original day existed in that month. PostgreSQL rejects the equivalent typed DATE literal, but that strict input behavior should not be assumed for SQLite's date function.Question 7 of 10
d holds '2027-08-19'. What are the results of strftime('%m', d) = 8 and CAST(strftime('%m', d) AS INTEGER) = 8, in that order?Show answerHide answer
'08', which does not equal the number 8 in the first expression. Casting that text to INTEGER produces 8 and makes the second comparison true. The leading zero is part of the returned text; displaying a month as digits does not make the function's result numeric.Question 8 of 10
event_date spans several years. A report displays dates as MM/DD/YYYY and must keep chronological order. How should it select and sort the column?Show answerHide answer
Question 9 of 10
d is '2027-04-24'. What does date(d, '+10 days') return?Show answerHide answer
'2027-05-04'. May 3 counts the starting date as one of the ten added days. A date shift measures days after the starting point and carries the remaining days into the next month.Question 10 of 10
Show answerHide answer
Set 2: Calculate amounts and handle missing data10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
units and unit_cost. What SELECT list returns their product with the heading extended_cost?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
lines contains only units and price. A query must run in SQLite and PostgreSQL. How should it keep rows with a product above 90 and sort the largest product first?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
SELECT 14 + 5 * 3; return in SQLite?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
old_count and new_count are integers holding 60 and 69. What is the result of (new_count - old_count) / old_count * 100?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
units = 5 and discount_pct = 12, with price = 18.0. What result does ROUND(units * price * (1 - discount_pct / 100), 2) produce?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
CAST('27boxes' AS INTEGER)?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
delivery_date?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
region values: 'East', 'West', NULL, and 'North'. How many rows satisfy WHERE NOT (region = 'East')?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
'unlisted' in SQLite. What expression does this while preserving recorded numbers?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
sales and visits. A rate should keep fractional percentages and stay NULL when visits is zero. It must not invent a zero rate. What expression fits?Show answerHide answer
Save this lesson to see this answer.
Set 3: Classify rows and diagnose report logic10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
score = 75: CASE WHEN score < 60 THEN 'review' WHEN score < 80 THEN 'ready' ELSE 'strong' END?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
CASE WHEN amount < 500 THEN 'regular' WHEN amount < 100 THEN 'small' ELSE 'large' END never return 'small' for non-NULL amounts?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
balance may be NULL. Required labels are 'unknown' for missing, 'credit' below 0, 'settled' at 0, and 'due' above 0. What CASE meets every rule?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
state value 'H' means held and 'R' means released. What does CASE state WHEN 'H' THEN 'held' WHEN 'R' THEN 'released' ELSE 'other' END return for 'R'?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
'urgent', 'normal', and 'deferred' in that order, with all other or NULL priorities last. What ORDER BY expression fits?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
qty = 4, price = 15.0, discount_pct = NULL, and shipped_date = NULL. Missing discount means none. What pair comes from ROUND(qty * price * (1 - COALESCE(discount_pct, 0) / 100.0), 2) and CASE WHEN shipped_date IS NULL THEN 'open' ELSE 'sent' END?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
LOWER(TRIM(customer)) AS name and date(due_date, '+7 days') AS reminder_date. Raw customer is ' IVAN ' and due_date is '2027-09-26'. What output pair should a sample check find?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
x values 2, 7, NULL, and 11. What are the row counts for WHERE x NOT IN (2, NULL) and WHERE x <> 2 OR x IS NULL, respectively?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
'1998-12-18'. As of '2027-12-17', what is the completed age, and why can subtracting the two year numbers be wrong?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
rating may be NULL. A display column must show the rating as text or 'unrated'. What expression fits?Show answerHide answer
Save this lesson to see this answer.
Set 4: Check complete reports and tricky boundaries10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
qty, NUMERIC price, and integer discount_pct. Missing discount means none. A report must keep rounded net totals above 75 and sort them highest first. What complete query fits?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
reading is DOUBLE PRECISION. How can a report round it to two decimal places without a function-signature error?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
COALESCE(NULL, '', 'backup') return in SQLite?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
email = 'nora@research.org'. What result does SUBSTR(email, INSTR(email, '@')) produce?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
'2027-01-31', compare SQLite 3.46+ date(d, '+1 month'), SQLite date(d, '+1 month', 'floor'), and PostgreSQL DATE '2027-01-31' + INTERVAL '1 month'. What calendar dates result, in that order?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
stock may be NULL. Required numeric flag: NULL for unknown stock, 1 for stock below 5, and 0 otherwise. What expression fits?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
'2027-05-08 10:15:00' to '2027-05-08 13:45:00'. What does (unixepoch(end_time) - unixepoch(start_time)) / 3600.0 return?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
qty = 6, price = 8.0, discount_pct = 25, ordered '2027-07-10' and shipped '2027-07-12'. Net uses ROUND(qty * price * (1 - COALESCE(discount_pct, 0) / 100.0), 2). A CASE labels NULL shipping as 'open', elapsed days <= 2 as 'fast', otherwise 'standard'. What net and label result?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
d. How can it calculate the quarter for every month?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
(qty 3, price 22.0, discount_pct NULL), B (qty 4, price 20.0, discount_pct 25), and C (qty 2, price 40.0, discount_pct 10). Missing discount means none. It computes ROUND(qty * price * (1 - COALESCE(discount_pct, 0) / 100.0), 2) AS net, keeps net > 60 using the full expression in WHERE, and orders by net DESC. What is the row order?Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
B
Boundary value
total < 50 is false; changing the test to total <= 50 makes it true.C
Calculated column
quantity * unit_price AS line_total even when the table has no line_total column.Case conversion
LOWER(TRIM(email)) makes a cleaned ASCII email address consistent for a case-normalized comparison.CEIL
CEIL(4.1) gives 5 because four is below the input and five is the next whole number.Character position
'ana@example.com' is at position 4, so the domain begins at position 5.COALESCE
COALESCE(mobile_phone, home_phone, 'no phone') uses the home number if the mobile number is missing.Collation
'Boston' = 'boston' true when default SQLite does not.Column alias
AS line_total and can sort it with ORDER BY line_total.Completed age
Concatenation
first_name || ' ' || last_name inserts a space between the two recorded name parts.Custom sort rank
D
Data type
'42' and the integer 42 can look alike in a grid while behaving differently in SQL.DATE
DATE '2026-03-14' explicitly marks March 14, 2026, as a PostgreSQL date value.E
ELSE fallback
ELSE 'large' unless an earlier branch explicitly tests total IS NULL.Explicit conversion
CAST(quantity AS REAL) / 4 changes the SQLite operand before division can lose a fraction.Expression
quantity * unit_price is an expression that produces the line amount for each row.EXTRACT
EXTRACT(MONTH FROM DATE '2026-03-14') returns the numeric month value 3.F
Fallback value
Floating-point number
0.1 + 0.2 = 0.3 false because the stored approximations do not sum to the same value.FLOOR
FLOOR(-4.1) is -5, so it differs from simply cutting off the fraction.Format pattern
'%m/%d/%Y' for a US date label; PostgreSQL TO_CHAR uses 'MM/DD/YYYY'.Function nesting
ROUND(COALESCE(amount, 0), 2), the fallback is resolved before the rounding function receives its input.H
Half-open range
ordered_at >= '2026-03-01' AND ordered_at < '2026-04-01' includes all March timestamps.I
Implicit conversion
'Order #' || 42, producing the label 'Order #42'.INSTR
INSTR('ana@example.com', '@') gives 4, while a string with no @ produces zero.Integer division
7 / 2 produces 3; making the second operand 2.0 keeps the fraction and produces 3.5.Interval
INTERVAL '1 month' to a PostgreSQL DATE returns a timestamp and follows its month-end rule.IS NULL
WHERE phone IS NULL retrieves customers with no recorded phone without comparing a value to NULL using equality.ISO date format
'2026-03-14' has leading zeros and sorts before '2026-11-02' in date order.J
Julian day number
julianday(end_date) - julianday(start_date) instead of subtracting ISO date strings directly.L
LENGTH
LENGTH(' Bo ') gives 4 because both edge spaces count as well as the two letters.Literal
first_name || ' ' || last_name is a text literal supplied by the query.Logical query order
M
Modulo
135 % 60 gives the 15 minutes remaining after two complete hours.Month label
'2026-03' keeps March 2026 separate from March 2025 in a report display.N
NULL
salary + bonus unknown even though the employee's salary is recorded.NULL-safe comparison
NULL IS NOT DISTINCT FROM NULL is TRUE in PostgreSQL and in SQLite from version 3.39.NULLIF
NULLIF(visits, 0) keeps positive visit counts but turns zero into a missing divisor for a rate.Numeric flag
CASE WHEN discount_pct > 0 THEN 1 ELSE 0 END assigns zero to missing as well as nonpositive discounts.Numeric text
'10', '100', and '9' appear in that ascending text order, which differs from numeric order.O
Operator precedence
10 + 3 * 2, so the result is 16 rather than 26.P
Padding
LPAD('42', 5, '0') returns '00042', giving a short code five characters.Placeholder
NULLIF(phone, '') turns an empty phone string into NULL before a display label is supplied.R
REPLACE
REPLACE('555-0100', '-', '') removes the hyphen and displays '5550100' without editing the table.Report query
ROUND
ROUND((price - cost) / price * 100, 1) to display a margin to one decimal place.S
Sample check
LIMIT 5 while building the report makes a missing space or integer-division error easier to locate.Searched CASE
CASE WHEN total < 50 THEN 'small' ELSE 'large' END chooses a label from the row's amount.Simple CASE
CASE status WHEN 'S' THEN 'Shipped' ELSE 'Other' END translates a stored status code.Single-row function
UPPER(city) in SELECT changes the displayed city text for each customer returned by the query.strftime
strftime('%m', '2026-03-14') returns '03', which should be compared with text or cast before numeric use.String
' New York ' includes edge spaces and two internal spaces that affect its length.Substring
SUBSTR('Boston', 1, 3) returns 'Bos', taking three characters beginning at the first position.T
Three-valued logic
FALSE AND UNKNOWN is FALSE, but TRUE AND UNKNOWN stays UNKNOWN because the second requirement is unresolved.TIMESTAMP
'2026-03-31 14:05:00' is later than midnight at the start of March 31.TRIM
TRIM(' New York '), the two spaces between the words remain in the returned text.Type affinity
U
Unix time
3600.0 to keep fractional hours in a duration.UNKNOWN
bonus = NULL is UNKNOWN, so it cannot identify a missing bonus in a WHERE filter.UTC
W
WHEN branch
< 200 branch first can prevent a later < 50 branch from ever being selected.- 1Relational Database Model
- 2SQL SELECT
- 3SQL CASE and Functions
- 4SQL GROUP BY
- 5SQL Joins
- 6SQL CREATE TABLE
About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.