Builder Lesson 37 of 63 ⏱ 5 min read

SQL formula reference — recipes and per-database syntax

Lesson summary

Recipe book and per-database syntax reference for DashboardFox formulas. The lesson that walks you in the door is report-formulas (Composer side) and formula-fields (App Builder side). This page is what to come back to when a formula gets harder than one function call.

By the end of this lesson

  • Quick reference for SQL functions by category and database platform
  • Reusable recipes for the patterns that recur — safe divide, decimal coercion, percentages, NULL-safe concat, CASE bucketing

Background

Composer's function-type picker (the dropdown that opens when you type @ in any Formula box) covers most of what new users need: aggregates, common string operations, date extraction, math basics. The picker fills in syntax for your specific database, so you don't have to remember MSSQL's LEN vs Postgres's LENGTH vs MySQL's CHAR_LENGTH.

This page picks up where the picker leaves off — the patterns that recur in real reporting but aren't pickable in one click, plus the per-database syntax tables you'll want when you're writing by hand or working with an AI tool.

What's on this page

Recipes

Patterns that solve a recurring problem in 3–5 lines of SQL. Copy, adapt, paste.

  • Safe divide (divide-by-zero protection)
  • Integer-to-decimal coercion
  • Percentage calculation
  • NULL-safe CONCAT
  • Multi-branch CASE bucketing

Per-DB syntax tables

One quick-lookup table per function category, with MSSQL / PostgreSQL / MySQL syntax side by side.

  • Aggregate (COUNT, SUM, AVG, MIN, MAX)
  • String (UPPER, LENGTH, TRIM, SUBSTRING, CONCAT, …)
  • DateTime (extraction, math, current)
  • Math (ROUND, ABS, CEILING, FLOOR, POWER)
  • Other (CASE, COALESCE, NULLIF, IS/IF)

Syntax convention used on this page

Examples use ? for the current card's field and [alias].[field] for cross-field references — matching what Composer's Insert Report Field emits on MSSQL. On PostgreSQL it emits "alias"."field" with double quotes; on MySQL it emits backticks. If you're writing a formula for App Builder's SQL Name field instead of Composer: App Builder has no card context, so replace every ? with the explicit alias-qualified reference (discover it in Composer first via Insert Report Field, then paste into App Builder). See formula-fields for the full App Builder workflow.

Skim, don't read — three ways to use this page

My formula errored
Jump to the pitfalls at the bottom — dialect mismatches, the ?-in-App-Builder issue, MSSQL TRIM on older versions, CAST target-type mismatches.
I need syntax for a function on my DB
Skip to the per-DB reference tables — Aggregate, String, DateTime, Math, and Other functions side-by-side across MSSQL, PostgreSQL, MySQL.
I keep writing the same shape of formula
Read the recipes first — safe divide, decimal coercion, percentage, NULL-safe concat, multi-branch CASE. Reusable patterns the @-picker won't hand you in one click.

Find what you need, copy the pattern, paste into your Formula box. Come back when the next formula puzzle shows up.

Stuck on a formula that isn't covered here? Email team@dashboardfox.com with the formula you're trying, your DB platform and version, and what error or wrong result you're seeing. We'll trace it. Same business day.

Do it

  1. Recipe — Safe divide (divide-by-zero protection)

    The problem. SQL throws a divide-by-zero error the moment your divisor hits 0. One bad row, and the whole report breaks.

    The fix. Wrap the divisor in NULLIF(..., 0) — converts 0 to NULL, and dividing by NULL returns NULL instead of erroring. The reporting row just shows a blank instead of crashing the query.

    ? / NULLIF([alias].[divisor], 0)

    Per-DB note. Identical syntax across MSSQL, PostgreSQL, and MySQL — NULLIF is one of the genuinely portable functions. Set Data Type to num (it returns a numeric result, possibly NULL).

    If you'd rather show 0 instead of NULL on the bad rows, wrap the whole thing in COALESCE: COALESCE(? / NULLIF([alias].[divisor], 0), 0).

  2. Recipe — Integer-to-decimal coercion

    The problem. When both operands of a division are integers, most databases return an integer result. 5 / 2 gives 2, not 2.5. Your "percentage of total" report shows 0% for everything that isn't a clean 100%.

    The fix. Cast at least one operand to a decimal type before the math. Syntax differs per DB:

    Database
    Cast expression
    MSSQL
    CAST(? AS DECIMAL(18,4))
    PostgreSQL
    ?::numeric or CAST(? AS NUMERIC)
    MySQL
    CAST(? AS DECIMAL(18,4))

    Set Data Type to num. If your displayed numbers need to be tidied, wrap with ROUND(..., 2) for two decimal places — see the next recipe for the combined pattern.

  3. Recipe — Percentage calculation (safe divide + casting + rounding)

    The three previous patterns combined. Use this for any "X as a percent of Y" calculation — % of total, completion rate, conversion rate, anything ratio-shaped.

    MSSQL / MySQL:

    ROUND( CAST(? AS DECIMAL(18,4)) / NULLIF([alias].[total], 0) * 100, 2 )

    PostgreSQL:

    ROUND( ?::numeric / NULLIF([alias].[total], 0) * 100, 2 )

    Set Data Type to num if you want the percent sign managed at the viz layer, or per if you want DashboardFox to render the % automatically. The per data type expects a value already expressed as a percent (75.5, not 0.755) — the * 100 at the end of the formula gets you there.

  4. Recipe — NULL-safe CONCAT (full name from first + last)

    The problem. If any input to CONCAT is NULL, some databases return NULL for the whole expression (or behave inconsistently depending on session settings). A customer with no middle name ends up with no name at all.

    The fix. Wrap each potentially-null input in COALESCE(..., '') — converts NULL to empty string before the concatenation.

    CONCAT( COALESCE(?, ''), ' ', COALESCE([alias].[last_name], '') )

    Per-DB note. CONCAT with multiple arguments works on MSSQL 2012+, PostgreSQL, and MySQL. COALESCE is universal. Set Data Type to cha.

    For three or more parts (first + middle + last, or street + city + state + zip), keep extending the same pattern. Each piece gets its own COALESCE; literal separators (spaces, commas) sit between as plain strings.

  5. Recipe — Multi-branch CASE bucketing

    Roll up many raw values into a smaller set of categories. The pattern from report-formulas generalized — match values into groups, with an ELSE catch-all for whatever doesn't match.

    CASE WHEN ? IN ('Shoplite Grocery', 'Genesis Groceries', 'FreshMart') THEN 'Grocery' WHEN ? IN ('Cafe X', 'Coffee Y') THEN 'Cafe' WHEN ? LIKE '%Restaurant%' OR ? LIKE '%Diner%' THEN 'Restaurant' ELSE 'Other' END

    Three things worth noting:

    • Order matters. The first matching WHEN wins, so put more-specific conditions before catch-alls. A row matching both Cafe and Restaurant would resolve to whichever WHEN comes first.
    • Mix operators. IN, LIKE, comparison operators, and BETWEEN all work inside CASE conditions. Combine with AND / OR as needed.
    • Numeric buckets work the same. Score ranges → letter grades, ages → age groups, anything where you want to fold a continuum into named bins.

    Set Data Type to cha (the output is text). Identical syntax across MSSQL, PostgreSQL, and MySQL — CASE is one of the most portable SQL constructs.

  6. Reference — Aggregate functions

    Aggregates collapse many rows into one summary value. Used in every "count by," "sum by," or "average by" report. Always set Data Type to num.

    Function
    What it does
    Expression
    Per-DB variants
    COUNT
    Number of non-null rows
    COUNT(?)
    COUNT DISTINCT
    Unique values
    COUNT(DISTINCT ?)
    SUM
    Total of numeric values, ignoring NULLs
    SUM(?)
    AVG
    Mean of numeric values, ignoring NULLs
    AVG(?)
    MIN
    Smallest value
    MIN(?)
    MAX
    Largest value
    MAX(?)

    Aggregates work cleanly across all three databases. The non-visible-column pattern from report-formulas is the most common way to apply them.

  7. Reference — String functions

    Operate on text columns. Output is text (cha) unless noted otherwise. LENGTH returns a number.

    Function
    What it does
    Default expression
    Per-DB variants
    UPPER
    Convert to uppercase
    UPPER(?)
    LOWER
    Convert to lowercase
    LOWER(?)
    LENGTH
    Number of characters (returns num)
    LENGTH(?)
    MSSQL: LEN(?) · MySQL: CHAR_LENGTH(?)
    TRIM
    Remove leading/trailing blanks
    TRIM(?)
    MSSQL pre-2017: LTRIM(RTRIM(?))
    LEFT
    Leftmost N characters
    LEFT(?, n)
    RIGHT
    Rightmost N characters
    RIGHT(?, n)
    SUBSTRING
    Slice from position, length N
    SUBSTRING(?, start_position, number_of_chars)
    MySQL also: SUBSTR(?, start_position, number_of_chars)
    REPLACE
    Replace all occurrences of a substring
    REPLACE(?, 'old', 'new')
    CONCAT
    Join strings together
    CONCAT(?, [insert_field])
    Postgres also: ? || [insert_field]
    POSITION
    Position of substring (returns num; 0 if not found)
    POSITION('text' IN ?)
    MSSQL: CHARINDEX('text', ?) · MySQL: INSTR(?, 'text')

    The TRIM row deserves a note: MSSQL only added TRIM(?) in 2017. On MSSQL 2016 or earlier, use LTRIM(RTRIM(?)) — works on every MSSQL version including current. If you're not sure what version your DB is on, the LTRIM(RTRIM()) form is the safe default.

  8. Reference — DateTime functions

    Date extraction and arithmetic. The function-type picker in Composer fills in the right syntax for your DB, but the table below is what you'll want when writing by hand or working with AI.

    Need
    Description
    MSSQL
    PostgreSQL / MySQL
    Current timestamp
    Right now, with time
    GETDATE()
    PG: NOW() · MySQL: NOW()
    Current date only
    Today, no time
    CAST(GETDATE() AS DATE)
    PG: CURRENT_DATE · MySQL: CURDATE()
    Year
    Numeric year (num)
    YEAR(?) or DATEPART(year, ?)
    PG: EXTRACT(YEAR FROM ?) · MySQL: YEAR(?)
    Month number
    1–12 (num)
    MONTH(?) or DATEPART(month, ?)
    PG: EXTRACT(MONTH FROM ?) · MySQL: MONTH(?)
    Month name
    "January" (cha)
    DATENAME(month, ?)
    PG: TO_CHAR(?, 'Month') · MySQL: MONTHNAME(?)
    Day of month
    1–31 (num)
    DAY(?) or DATEPART(day, ?)
    PG: EXTRACT(DAY FROM ?) · MySQL: DAY(?)
    Day of week (name)
    "Monday" (cha)
    DATENAME(weekday, ?)
    PG: TO_CHAR(?, 'Day') · MySQL: DAYNAME(?)
    Hour
    0–23 (num)
    DATEPART(hour, ?)
    PG: EXTRACT(HOUR FROM ?) · MySQL: HOUR(?)
    Difference (days)
    Days between two dates (num)
    DATEDIFF(day, [alias].[start], [alias].[end])
    PG: [end] - [start] · MySQL: DATEDIFF([end], [start])
    Add interval
    Date plus N days
    DATEADD(day, n, ?)
    PG: ? + INTERVAL 'n days' · MySQL: DATE_ADD(?, INTERVAL n DAY)

    Two extraction patterns recur often enough to flag: month name sorts alphabetically (Apr, Aug, Dec…) unless you also bring in the month-number column and sort by that, then mark month-number non-visible. The same trick applies to day-of-week name. See report-formulas for the non-visible-sort-key pattern.

  9. Reference — Math functions

    Numeric operations. All return num Data Type.

    Function
    What it does
    Expression
    Per-DB variants
    ABS
    Absolute (positive) value
    ABS(?)
    ROUND
    Round to N decimal places
    ROUND(?, 2)
    CEILING
    Round up to nearest integer
    CEILING(?)
    FLOOR
    Round down to nearest integer
    FLOOR(?)
    POWER
    Raise to a power
    POWER(?, n)
    SQRT
    Square root
    SQRT(?)
    MOD
    Remainder of division
    MOD(?, n)
    MSSQL: ? % n

    Math functions are the most portable category — six of the seven above are identical across all three databases. The integer-division-needs-casting pattern from the recipes section above is the one to remember.

  10. Reference — Other (conditional & NULL handling)

    Conditional logic and NULL handling. These are the most-asked-about functions after aggregates — they show up in almost every advanced formula.

    Function
    What it does
    Default expression
    Per-DB variants
    CASE
    Multi-branch conditional (universal)
    CASE WHEN ? = 'X' THEN 'A' ELSE 'B' END
    Inline IF
    Two-branch shorthand
    (use CASE)
    MSSQL: IIF(? > 0, 'matching_result', 'else_result') · MySQL: IF(? > 0, 'matching_result', 'else_result') · PG: use CASE
    COALESCE
    First non-NULL of N values
    COALESCE(?, [insert_fallback_field], 'default_value')
    NULLIF
    NULL if value equals X, else the value
    NULLIF(?, 0)
    NULL-to-value
    Two-argument NULL replacement
    COALESCE(?, 'default_value')
    MSSQL: ISNULL(?, 'default_value') · MySQL: IFNULL(?, 'default_value') · PG: use COALESCE

    Two practical notes:

    • Prefer COALESCE over ISNULL/IFNULL/NVL. COALESCE is part of the SQL standard and works on every database listed. The vendor-specific aliases (ISNULL on MSSQL, IFNULL on MySQL, NVL on Oracle) all do the same thing but reduce portability. If you ever migrate or write formulas across DBs, COALESCE saves rewrites.
    • NULLIF is the divide-by-zero hero. Almost every safe-divide pattern uses it. See the first recipe above.

If you're stuck

Cross-cutting issues that show up regardless of which recipe or function you're using.

I see "column 'X' does not exist" where X is a snake_case word

The picker dropped a placeholder you didn't replace. When you pick a function, Composer fills in the SQL shape with named placeholders — decimal_places, number_of_chars, start_position, number_of_days, [insert_field], [insert_divisor_field], [insert_end_date_field] — that you have to swap for real values before saving. The database treats anything else as a column name and errors when it can't find one. The placeholder name tells you what kind of value to put there: integer for decimal_places, a field reference (via Insert Report Field) for anything starting with insert_, text in quotes for things like 'matching_result'. Look at the formula, find the snake_case word the error names, and replace it.

PostgreSQL: "function round(real, integer) does not exist"

PostgreSQL has ROUND(numeric, integer) but no ROUND(real, integer) or ROUND(double precision, integer). If your field is stored as real or double precision in the database, the two-argument ROUND errors with code 42883. The fix is to cast first: ROUND(CAST(? AS NUMERIC), 2). The current picker's ROUND and ROUND_2_DECIMAL entries on PostgreSQL already wrap with this cast — if you're seeing the error, either the picker is older than this fix or you hand-wrote the formula without the cast.

SAFE_DIVIDE returned 0 when I expected a decimal

Integer division. On MSSQL and PostgreSQL, when both operands of / are integer-typed, the result is integer — 1 / 2 is 0, not 0.5. The fix is to cast the numerator to decimal/numeric: CAST(? AS DECIMAL(18, 4)) / NULLIF([insert_divisor_field], 0) on MSSQL, or CAST(? AS NUMERIC) / NULLIF([insert_divisor_field], 0) on PostgreSQL. The current picker's SAFE_DIVIDE entries on MSSQL and PG already include this cast. MySQL and Oracle do floating-point division natively, so they don't need the cast.

PostgreSQL: DATEDIFF_DAYS on a TIMESTAMP field returns something strange

The unfixed PG expression ([alias].[end_date] - ?) returns an INTERVAL (not an integer) when either operand is a TIMESTAMP. The fix is to cast both sides to DATE: (CAST([insert_end_date_field] AS DATE) - CAST(? AS DATE)). The current picker's DATEDIFF_DAYS entry on PostgreSQL already does this. Oracle has the same issue and the same fix using TRUNC.

MySQL: CONCAT returned NULL even though I had text in both fields

MySQL's CONCAT(NULL, anything) returns NULL — different from MSSQL, PostgreSQL, and Oracle, which treat NULL operands as empty strings. If any field in the concatenation has a NULL value on a row, that row's result is NULL. The fix is to wrap each operand with COALESCE(operand, ''): CONCAT(COALESCE(?, ''), COALESCE([insert_field], '')). The current picker's CONCAT entry on MySQL already does this. See the "NULL-safe CONCAT" recipe above for the multi-part pattern.

AI gave me SQL that errors on my database

Dialect mismatch. DATENAME(month, ?) works on MSSQL but not PostgreSQL; TO_CHAR(?, 'Month') works on PostgreSQL and Oracle but not MSSQL. Tell AI exactly which platform and version ("Microsoft SQL Server 2019", "PostgreSQL 14") in the first line of the prompt. Re-run; the SQL matches your database. The DateTime table above shows the most common per-DB differences.

My Composer formula works but breaks when I paste it into App Builder

App Builder's SQL Name field doesn't understand ? — there's no current-card context, so the token doesn't resolve. Replace every ? in your formula with the explicit [alias].[field] reference. Discover the reference in Composer first via Add Formula → @ → Insert Report Field, copy what it emits, paste into App Builder. The full App Builder workflow is in formula-fields.

MSSQL 2016 says TRIM doesn't exist

TRIM(?) was added to MSSQL in 2017. On 2016 or earlier, use LTRIM(RTRIM(?)) — same result, works on every MSSQL version. If you're not sure what version you're on (or you maintain reports for multiple environments), default to the LTRIM(RTRIM()) form everywhere. It's slightly longer but never breaks.

My CAST won't convert — error about the target type

Target type names differ slightly across databases. DECIMAL(18,4) works on MSSQL and MySQL; PostgreSQL prefers NUMERIC (or the shorthand ?::numeric). If you copy a cast from one DB to another, the most common fix is renaming the target type. The integer-to-decimal recipe above has the per-DB syntax side by side.

None of these match my situation

Email team@dashboardfox.com with the formula you're trying to write, your database platform and version, and what error or wrong result you're seeing. Screenshot of the Formula box helps. Same business day reply.

7-day free trial — no credit card

Built lean. Priced fairly. Supported by humans.

Full access to all features. No credit card required.

Prefer no subscriptions & full control? Self-hosted from $4,995 one-time →

Click once to extend to 14 days — need more time? Just reach out.

25+ years building BI tools Support from the team that builds it Available in US & EU regions
DashboardFox mascot