Math Functions
ROUND, FLOOR, CEIL, ABS, MOD, POWER, SQRT, LOG — every numeric operation for financial calculations, statistical analysis, data bucketing, and percentage computations
// Part 01
Why Math Functions Matter in SQL
Most analytical SQL produces numbers — revenue totals, percentage shares, growth rates, margins, ratios. Raw numbers rarely arrive in the format you need: a sum might need rounding to two decimal places, a ratio needs to be expressed as a percentage, a negative difference needs its absolute value. Math functions are the final layer that transforms raw computed numbers into accurate, display-ready values.
Beyond formatting, math functions enable bucketing (grouping values into ranges), statistical summaries (standard deviation, variance), financial calculations (compound growth, loan repayment), and safely handling division-by-zero — one of the most common runtime errors in analytical SQL.
// Part 02
Rounding — ROUND, FLOOR, CEIL, TRUNC
ROUND(number, decimal_places)Rounds to specified decimal places. ROUND(4.567, 2) = 4.57. ROUND(4.564, 2) = 4.56. ROUND(4.5, 0) = 5. Negative places round to tens/hundreds: ROUND(1234, -2) = 1200.
FLOOR(number)Always rounds down. FLOOR(4.9) = 4. FLOOR(-4.1) = -5. Useful for bucketing into ranges.
CEIL(number)Always rounds up. CEIL(4.1) = 5. CEIL(-4.9) = -4. Useful for page counts, minimum quantities.
TRUNC(number, decimal_places)Truncates (cuts off) without rounding. TRUNC(4.99, 1) = 4.9. TRUNC(4.99, 0) = 4. Differs from ROUND — never rounds up.
Banker's rounding vs standard rounding
// Part 03
ABS — Absolute Value
ABS returns the absolute (non-negative) value of a number. It is essential whenever you care about the magnitude of a difference rather than its direction — how far two values differ, not which is larger.
// Part 04
MOD — Remainder After Division
MOD returns the remainder after integer division. It is used for cycling through values, identifying even/odd rows, generating alternating patterns, and extracting the last N digits of a number.
// Part 05
POWER, SQRT, CBRT — Exponents and Roots
POWER(base, exponent)POWER(2, 10) = 1024. POWER(9, 0.5) = 3 (square root). POWER(amount, 2) = amount squared. Used for compound growth, geometric means.
SQRT(number)SQRT(25) = 5. SQRT(2) = 1.4142... Returns NULL for negative inputs in most databases. Used for standard deviation calculations.
CBRT(number)CBRT(27) = 3. CBRT(8) = 2. Available in PostgreSQL and DuckDB. Used in volume calculations.
EXP(number)EXP(1) = 2.71828 (Euler's number). EXP(0) = 1. Used for exponential growth models and log-scale transformations.
// Part 06
LOG and LN — Logarithms
// Part 07
Division and NULLIF — Preventing Division by Zero
Division by zero is one of the most common runtime errors in SQL — it crashes the query with no data returned. The fix is NULLIF: it returns NULL when its two arguments are equal, and NULL divided by anything is NULL (not an error). Wrapping the denominator with NULLIF(denominator, 0) silently converts zero-division attempts into NULL results.
// Part 08
Statistical Aggregates — STDDEV, VARIANCE, PERCENTILE
SQL includes statistical aggregate functions that go beyond SUM, AVG, and COUNT. These functions are essential for understanding data distributions — not just the average, but how spread out values are and what the typical range looks like.
// Part 09
Numeric Bucketing — WIDTH_BUCKET and CASE Ranges
Bucketing assigns each row to a numeric range — turning continuous values into discrete categories for distribution analysis and histograms. SQL provides WIDTH_BUCKET for uniform-width buckets and CASE for custom ranges.
// Part 10
Integer Division and Type Casting
SQL type rules for division are a frequent source of bugs. When both operands are integers, SQL performs integer division — discarding the remainder. 7 / 2 = 3, not 3.5. Casting one operand to NUMERIC or FLOAT forces decimal division.
// Part 11
Complete Math Function Reference
| Function | Description | Example → Result |
|---|---|---|
| ROUND(n, dp) | Round to dp decimal places | ROUND(3.456, 2) → 3.46 |
| FLOOR(n) | Largest integer ≤ n | FLOOR(3.9) → 3 |
| CEIL(n) | Smallest integer ≥ n | CEIL(3.1) → 4 |
| TRUNC(n, dp) | Truncate to dp decimal places | TRUNC(3.99, 1) → 3.9 |
| ABS(n) | Absolute value | ABS(-42) → 42 |
| MOD(a, b) | Remainder of a / b | MOD(10, 3) → 1 |
| POWER(b, e) | b raised to exponent e | POWER(2, 8) → 256 |
| SQRT(n) | Square root | SQRT(144) → 12 |
| CBRT(n) | Cube root | CBRT(27) → 3 |
| EXP(n) | e raised to power n | EXP(1) → 2.71828 |
| LN(n) | Natural logarithm (base e) | LN(1) → 0 |
| LOG(n) | Base-10 logarithm | LOG(1000) → 3 |
| LOG(b, n) | Logarithm base b of n | LOG(2, 1024) → 10 |
| SIGN(n) | Sign of n: -1, 0, or 1 | SIGN(-42) → -1 |
| PI() | Value of π | PI() → 3.14159... |
| RANDOM() | Random float 0 ≤ x < 1 | RANDOM() → 0.73421... |
| WIDTH_BUCKET(v, lo, hi, n) | Uniform bucket number for v | WIDTH_BUCKET(350, 0, 1000, 10) → 4 |
| NULLIF(a, b) | NULL if a=b, else a | NULLIF(0, 0) → NULL |
| STDDEV(col) | Sample standard deviation | STDDEV(salary) → 12450.3 |
| VARIANCE(col) | Sample variance | VARIANCE(salary) → 155010000 |
| PERCENTILE_CONT(f) WITHIN GROUP (ORDER BY col) | Interpolated percentile | PERCENTILE_CONT(0.5) → median |
| CORR(a, b) | Pearson correlation coefficient | CORR(price, sales) → -0.72 |
// Part 12
What This Looks Like at Work
You are a data analyst at Brex, a fintech platform. The finance team needs a monthly P&L summary showing revenue, cost of goods, gross margin, gross margin percentage, standard deviation of transaction values, and identification of outlier transactions. All require combinations of math functions.
🎯 Pro Tip
Always pair margin and percentage calculations with NULLIF on the denominator. In a P&L query, if a store has zero revenue (no delivered orders), dividing gross_profit / 0 crashes the entire query — not just that store's row. NULLIF converts the zero to NULL, the division returns NULL, and the query continues for all other stores. Add COALESCE if you want 0% instead of NULL for display.
// Part 13
Interview Prep — 5 Questions With Complete Answers
NULLIF(a, b) returns NULL when a equals b, and returns a unchanged when a does not equal b. Its primary use is preventing division-by-zero errors — NULLIF(denominator, 0) converts zero denominators to NULL, and NULL divided by anything is NULL (not a runtime error). Without this guard, a single row with a zero denominator crashes the entire query.
The pattern appears in every percentage and ratio calculation: ROUND(part::NUMERIC / NULLIF(total, 0) * 100, 1). For a revenue-per-order metric: ROUND(total_revenue / NULLIF(order_count, 0), 2). For growth rates: ROUND((new_val - old_val) / NULLIF(old_val, 0) * 100, 1). Any time you divide by an aggregate or by a value that could be zero in some rows, wrap the denominator in NULLIF.
Combine NULLIF with COALESCE for display purposes: COALESCE(value / NULLIF(denominator, 0), 0) returns 0 instead of NULL when the denominator is zero — useful when a zero percentage is a meaningful display value rather than a missing value. NULLIF also has a secondary use: returning NULL to suppress a specific value in aggregates — AVG(NULLIF(score, -1)) computes the average of scores while treating -1 as "not applicable" (NULLs are excluded from AVG).
All four reduce a decimal number to fewer decimal places but with different rounding behaviour. ROUND(n, dp) rounds to the nearest value at dp decimal places — values at exactly .5 round up in standard SQL. ROUND(3.456, 2) = 3.46 (rounds up). ROUND(3.454, 2) = 3.45 (rounds down). ROUND accepts negative dp to round to tens, hundreds: ROUND(1234, -2) = 1200.
FLOOR(n) always rounds down to the largest integer not greater than n. FLOOR(3.9) = 3. FLOOR(-3.1) = -4 (more negative, not -3). CEIL(n) always rounds up to the smallest integer not less than n. CEIL(3.1) = 4. CEIL(-3.9) = -3 (less negative, not -4). TRUNC(n, dp) cuts off at dp decimal places without rounding — TRUNC(3.99, 1) = 3.9 (not 4.0). TRUNC always truncates toward zero for both positive and negative numbers.
When to use each: ROUND for financial display where standard rounding is expected (₹ amounts, percentages). FLOOR for conservative estimates, bin lower-bounds, and page-down calculations (how many complete pages?). CEIL for coverage calculations, minimum quantities, and page-up calculations (how many pages needed to fit N items?). TRUNC when you need to cut precision without any rounding — for example, extracting the integer part of a number without rounding it up: TRUNC(salary / 1000, 0) gives the thousands-level band (₹45,678 → 45).
Three approaches. First, using a subquery for the total: SELECT store_id, revenue, ROUND(revenue::NUMERIC / (SELECT SUM(revenue) FROM store_revenues) * 100, 1) AS pct_of_total FROM store_revenues. The subquery computes the grand total once and each row divides its revenue by it. NULLIF protects against a zero total. The ::NUMERIC cast prevents integer division.
Second, using a window function — more efficient and cleaner: SELECT store_id, revenue, ROUND(revenue::NUMERIC / SUM(revenue) OVER () * 100, 1) AS pct_of_total FROM store_revenues. SUM(revenue) OVER () computes the grand total across all rows and attaches it to each row in one pass — no subquery needed.
Third, using CROSS JOIN to a single-row total: WITH totals AS (SELECT SUM(revenue) AS grand_total FROM store_revenues) SELECT sr.store_id, sr.revenue, ROUND(sr.revenue / NULLIF(t.grand_total, 0) * 100, 1) AS pct FROM store_revenues AS sr CROSS JOIN totals AS t. This is explicit and readable. Key rules for all three: always cast the numerator or denominator to NUMERIC before dividing (integer / integer discards the decimal part, giving 0 or 1 for values under 100%); always protect the denominator with NULLIF(total, 0); always ROUND to a reasonable number of decimal places (1 or 2 for percentages).
When both operands in a division are integer types, SQL performs integer division — discarding the fractional remainder. 7 / 2 = 3 (not 3.5). 1 / 3 = 0 (not 0.333). This is not a bug — it is the defined behaviour for integer arithmetic in SQL. But it produces incorrect results in percentage and ratio calculations where a decimal result is expected.
The most common symptom: a percentage calculation always returns 0 or 100 instead of a decimal value. SELECT COUNT(CASE WHEN status = 'Delivered' THEN 1 END) / COUNT(*) * 100 FROM orders — if the counts are both integers, the division produces 0 or 1 (integer), then multiplied by 100 gives 0 or 100. Never the expected decimal percentage.
Three fixes: cast one operand to NUMERIC — COUNT(...)::NUMERIC / COUNT(*) * 100; use a decimal literal — COUNT(...) / COUNT(*) * 100.0 (the 100.0 float forces decimal promotion); or use CAST explicitly — CAST(COUNT(...) AS NUMERIC) / COUNT(*) * 100. The cast must apply to the numerator or denominator before the division occurs — casting the result of an integer division (SELECT (7 / 2)::NUMERIC) still gives 3.0, not 3.5. The cast must come before the division: SELECT 7::NUMERIC / 2 → 3.5. The same trap applies to AVG of integers — AVG(integer_column) returns an integer in some databases (MySQL) but numeric in others (PostgreSQL). Always verify AVG results when working with integer columns across different databases.
The standard z-score approach: a value is an outlier if it is more than N standard deviations from the mean (commonly N=2 or N=3). The z-score formula is z = (value - mean) / stddev. Values with |z| > 2 are outside 95% of the distribution (assuming normal distribution); |z| > 3 are outside 99.7%.
SQL implementation: compute mean and stddev in a CTE or derived table, then JOIN to the original table and compute z-scores per row: WITH stats AS (SELECT AVG(total_amount) AS mean_val, STDDEV(total_amount) AS std_val FROM orders WHERE order_status = 'Delivered') SELECT order_id, total_amount, ROUND(ABS(total_amount - s.mean_val) / NULLIF(s.std_val, 0), 2) AS z_score FROM orders, stats AS s WHERE ABS(total_amount - s.mean_val) > 2 * s.std_val. NULLIF protects against the case where all values are identical (stddev = 0).
An alternative to z-score is the IQR (interquartile range) method — more robust to extreme outliers: outliers are values below Q1 - 1.5 × IQR or above Q3 + 1.5 × IQR. IQR = Q3 - Q1 computed with PERCENTILE_CONT. The IQR method does not assume a normal distribution and is preferred for skewed data (like revenue or order values which are typically right-skewed). For time-series data, per-period outlier detection (z-score within each month) is more meaningful than a global z-score across all periods — a ₹5,000 order might be normal in December but an outlier in January.
// Part 14
Errors You Will Hit — And Exactly Why They Happen
🎯 Key Takeaways
- ✓ROUND(n, dp) rounds to dp decimal places. Always specify decimal places explicitly for currency — ROUND(amount, 2). FLOOR always rounds down; CEIL always rounds up; TRUNC cuts without rounding.
- ✓ABS returns the magnitude regardless of sign. Use it when you care about how far apart two values are, not which is larger — deviations, tolerances, outlier distances.
- ✓MOD(a, b) returns the remainder after division. Use it for cycling (even/odd, A/B buckets), last-N-digit extraction, and identifying multiples.
- ✓NULLIF(denominator, 0) is mandatory for all division operations. It converts zero to NULL — NULL / anything = NULL (no error). Pair with COALESCE if you need 0 instead of NULL for display.
- ✓Integer division trap: 7 / 2 = 3 in SQL when both are integers. Cast the numerator to NUMERIC before dividing: 7::NUMERIC / 2 = 3.5. This applies to all percentage and ratio calculations.
- ✓POWER(base, exponent) for exponentiation. SQRT for square roots. Use POWER(x, 0.5) only on non-negative values — negative inputs return NULL or error.
- ✓LOG(n) is base-10 logarithm. LN(n) is natural logarithm. FLOOR(LOG10(value)) gives the order of magnitude — useful for log-scale bucketing.
- ✓STDDEV returns NULL for single-row groups (n-1 = 0). Use STDDEV_POP or guard with CASE WHEN COUNT(*) > 1 THEN STDDEV(col) END.
- ✓PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY col) computes the median. Use PERCENTILE_CONT for the five-number summary: min, Q1, median, Q3, max.
- ✓WIDTH_BUCKET(value, min, max, n) assigns values to n uniform-width buckets. Use CASE ranges for business-meaningful non-uniform buckets.
What comes next
In Module 44, you learn CAST and type conversion — converting between data types, implicit vs explicit casting, safe casting with TRY_CAST, and the type coercion rules that prevent silent errors.
Module 44 → CAST and Type ConversionDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.