Chapter 06 · Functions, NULLs, and Conditional Logic

Building Clean Derived Columns

Turn raw columns into stable analytical fields whose meaning remains clear to applications, reports, and future maintainers.

Beginner95–120 minutesProjection design + chapter capstoneLast reviewed: August 2026

Learning outcomes

A derived column is calculated by the query rather than stored directly in the source table. A clean derived column has a stable meaning, name, type, unit, NULL policy, and calculation order.

01

Design aliases that describe business meaning rather than implementation mechanics.

02

Control calculation order, NULL behavior, and rounding.

03

Layer complex expressions so each stage can be verified.

04

Build a complete reporting projection with an explicit output contract.

A derived column is an interface

Raw source columns
Normalize and validate inputs
Calculate business expression
Name and type the output
Consumer receives a contract

A SELECT result is an interface between the database and its consumers. Derived columns should be designed as deliberately as stored columns.

N

Name

Use a stable alias such as net_amount, not expr1 or total2.

U

Unit

State whether a value is currency, percentage, count, seconds, or text.

T

Type

Return a predictable type suitable for downstream consumers.

M

Missingness

Define what NULL, zero, empty text, and fallback labels mean.

Start with an explicit calculation contract

FieldDefinitionType/formatNULL policy
gross_amountunit price multiplied by quantitynumeric, two display decimalsNever NULL for valid source rows
effective_discount_raterecorded rate; missing treated as zero for this reportnumeric ratio 0–1Output never NULL
discount_amountgross amount multiplied by effective ratenumeric currencyOutput never NULL
net_amountgross amount minus discount amountnumeric currencyOutput never NULL
sold_datecalendar date from UTC timestamp textYYYY-MM-DD text in SQLiteInvalid source should be detected
stock_statusavailability label from stock quantitycontrolled text labelOutput never NULL

Use descriptive aliases

sqlite · explicit result contract
SELECT    s.sale_id,    p.product_name,    p.unit_price * s.quantity AS gross_amount,    COALESCE(s.discount_rate, 0) AS effective_discount_rate,    p.unit_price * s.quantity * COALESCE(s.discount_rate, 0)        AS discount_amountFROM sale AS sJOIN product AS p ON p.product_id = s.product_idORDER BY s.sale_id;

Aliases should survive a code review without requiring readers to reconstruct the expression. Avoid reusing a source-column name for a value whose semantics changed.

Do not rely on a select-list alias in another select-list expression

not portable · same-level alias reuse
SELECT    p.unit_price * s.quantity AS gross_amount,    gross_amount * COALESCE(s.discount_rate, 0) AS discount_amountFROM sale AS sJOIN product AS p ON p.product_id = s.product_id;

Most engines do not make one select-list alias available to a sibling expression. Layer the calculation in a subquery.

portable layering · calculate then reuse
SELECT    sale_id,    gross_amount,    effective_discount_rate,    gross_amount * effective_discount_rate AS discount_amountFROM (    SELECT        s.sale_id,        p.unit_price * s.quantity AS gross_amount,        COALESCE(s.discount_rate, 0) AS effective_discount_rate    FROM sale AS s    JOIN product AS p ON p.product_id = s.product_id) AS priced_saleORDER BY sale_id;

Round at the presentation boundary

sqlite · preserve precision until final output
SELECT    sale_id,    ROUND(gross_amount, 2) AS gross_amount,    ROUND(discount_amount, 2) AS discount_amount,    ROUND(gross_amount - discount_amount, 2) AS net_amountFROM (    SELECT        s.sale_id,        p.unit_price * s.quantity AS gross_amount,        p.unit_price * s.quantity * COALESCE(s.discount_rate, 0)            AS discount_amount    FROM sale AS s    JOIN product AS p ON p.product_id = s.product_id) AS calculated_saleORDER BY sale_id;

Rounding each intermediate factor can produce a different total from calculating at full available precision and rounding the final monetary fields. The correct boundary is a business decision.

Expose assumptions rather than hiding them

sqlite · value plus provenance flag
SELECT    sale_id,    COALESCE(discount_rate, 0) AS effective_discount_rate,    discount_rate IS NULL AS discount_was_missingFROM saleORDER BY sale_id;

A consumer can use the effective rate while still distinguishing a recorded zero from a missing source value.

Keep units consistent

sqlite · percentage and ratio are different contracts
SELECT    sale_id,    COALESCE(discount_rate, 0) AS discount_ratio,    ROUND(COALESCE(discount_rate, 0) * 100, 1) AS discount_percentFROM saleORDER BY sale_id;

Name the unit in the alias when confusion is possible. A value of 0.15 as a ratio and 15.0 as a percentage are not interchangeable.

Derived labels should use controlled categories

sqlite · stable stock label
SELECT    product_id,    product_name,    stock_qty,    CASE        WHEN stock_qty = 0 THEN 'out_of_stock'        WHEN stock_qty < 25 THEN 'low_stock'        ELSE 'available'    END AS stock_statusFROM productORDER BY product_id;

Machine-facing labels are often safer as stable codes such as low_stock. User-facing translation and wording can occur in the presentation layer.

Build one clean reporting projection

sqlite · chapter capstone report
SELECT    sale_id,    sold_date,    customer_name,    product_label,    sales_channel,    quantity,    gross_amount,    discount_percent,    discount_amount,    net_amount,    discount_was_missingFROM (    SELECT        s.sale_id,        DATE(s.sold_at) AS sold_date,        TRIM(c.full_name) AS customer_name,        UPPER(p.category) || ': ' || p.product_name AS product_label,        s.sales_channel,        s.quantity,        ROUND(p.unit_price * s.quantity, 2) AS gross_amount,        ROUND(COALESCE(s.discount_rate, 0) * 100, 1)            AS discount_percent,        ROUND(            p.unit_price * s.quantity * COALESCE(s.discount_rate, 0),            2        ) AS discount_amount,        ROUND(            p.unit_price * s.quantity            * (1 - COALESCE(s.discount_rate, 0)),            2        ) AS net_amount,        s.discount_rate IS NULL AS discount_was_missing    FROM sale AS s    JOIN customer AS c ON c.customer_id = s.customer_id    JOIN product AS p ON p.product_id = s.product_id) AS sale_reportORDER BY sold_date, sale_id;

The outer query declares the published column order. The inner query groups transformations and calculations into one reviewable layer.

Verify values and types

sqlite · contract checks
SELECT    sale_id,    net_amount,    TYPEOF(net_amount) AS net_amount_type,    discount_was_missing,    TYPEOF(discount_was_missing) AS flag_typeFROM (    SELECT        s.sale_id,        ROUND(            p.unit_price * s.quantity            * (1 - COALESCE(s.discount_rate, 0)),            2        ) AS net_amount,        s.discount_rate IS NULL AS discount_was_missing    FROM sale AS s    JOIN product AS p ON p.product_id = s.product_id) AS checked_reportORDER BY sale_id;

Automated tests should verify representative values, NULL cases, boundary values, result types, and column names. SQLite boolean expressions return integer 0 or 1; other engines may return a boolean type.

Derived columns versus stored generated columns

ChoiceUse whenTradeoff
Query-time expressionThe value is contextual or used by one queryRepeated calculation and possible duplicated logic.
ViewMany consumers need one governed projectionAdds a database object and versioning responsibility.
Generated/computed columnThe expression is stable and engine supports itPortability and write/storage behavior vary.
Stored ordinary columnValue must be preserved historically or externally suppliedRequires update rules to prevent drift.
Application calculationRule belongs to interaction or domain serviceDatabase-side reports may need the same logic separately.

Practice lab

  1. Create clean customer name and email columns.
  2. Create gross, discount, and net amounts.
  3. Expose both the effective discount rate and a missingness flag.
  4. Create stable product availability labels.
  5. Wrap the final report in a subquery and publish an explicit output order.
sqlite · compact solution
SELECT    sale_id,    sold_date,    customer_name,    product_name,    gross_amount,    discount_amount,    net_amount,    discount_was_missingFROM (    SELECT        s.sale_id,        DATE(s.sold_at) AS sold_date,        TRIM(c.full_name) AS customer_name,        p.product_name,        ROUND(p.unit_price * s.quantity, 2) AS gross_amount,        ROUND(p.unit_price * s.quantity * COALESCE(s.discount_rate, 0), 2)            AS discount_amount,        ROUND(p.unit_price * s.quantity * (1 - COALESCE(s.discount_rate, 0)), 2)            AS net_amount,        s.discount_rate IS NULL AS discount_was_missing    FROM sale AS s    JOIN customer AS c ON c.customer_id = s.customer_id    JOIN product AS p ON p.product_id = s.product_id) AS report_rowsORDER BY sold_date, sale_id;

Review checklist

Before publishing a derived column

  1. Does the alias state the field meaning and unit?
  2. Are NULL, empty, and zero states defined?
  3. Are all CASE branches type-compatible?
  4. Is rounding performed at the correct boundary?
  5. Can each calculation layer be tested independently?
  6. Will the same rule be duplicated elsewhere?
  7. Are result column names and order treated as an interface?

Common failures

Opaque aliases

Names such as calc1 force every consumer to rediscover the expression.

Same-level alias reuse

This is unsupported or behaves differently across engines.

Mixed units

Ratios, percentages, currency, and counts need explicit names.

Silent NULL replacement

A usable fallback can erase source-state information.

Premature rounding

Rounding intermediate values can change totals.

Copying the same business expression

Duplicate definitions drift; govern reusable logic deliberately.

Chapter 6 summary

  • Scalar functions transform string, numeric, date/time, and typed values.
  • COALESCE and NULLIF provide explicit missing-value policies.
  • CASE creates conditional values according to ordered rules.
  • Function portability requires testing names, syntax, types, and behavior.
  • Derived columns are output contracts with names, units, types, NULL rules, and calculation boundaries.

Chapter 7 introduces joins and explains how rows from related tables are matched, preserved, multiplied, and debugged.

References

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.