Support
Log In

Malloy vs. SQL

Malloy compiles to SQL and is not a replacement for it — what the language does that SQL cannot, where SQL stays the right tool, and the three error classes Malloy makes unwritable

Malloy is not a replacement for SQL. It compiles to SQL — BigQuery, Snowflake, PostgreSQL, MySQL, Databricks, Trino, DuckDB — and the SQL it produces is what your warehouse runs, bills for, and shows you in its query history. Every Malloy query can be inspected as the SQL it became.

So the useful question is not which language is better. It is what the compiler can do for you once it understands your data's structure, and where you still want to write SQL yourself.

The Same Question, Twice

Revenue by region, from orders joined to customers.

SELECT
  c.region,
  SUM(o.order_amount) AS total_revenue
FROM sales.orders o
JOIN sales.customers c ON o.customer_id = c.id
GROUP BY c.region
run: orders -> {
  group_by: customers.region
  aggregate: total_revenue
}

The Malloy version is shorter because the join condition and the revenue formula are not in it. They are in the model, declared once:

source: orders is conn.table('sales.orders') extend {
  join_one: customers on customer_id = customers.id
  measure: total_revenue is sum(order_amount)
}

That is the whole of the difference, and it compounds. The SQL restates o.customer_id = c.id and SUM(o.order_amount) in every query that needs them. The next analyst writes the join a slightly different way, or sums order_amount where they should have summed a net-of-refunds column, and now there are two revenue numbers. In Malloy, total_revenue means one thing because there is one definition of it, and every query, dashboard and AI-generated answer resolves to that definition.

Three Errors You Cannot Write

The argument for a compiler is not brevity. It is the class of mistakes it refuses to emit.

Fan-out

Join orders to order_items and sum order_amount, and SQL will happily multiply each order's revenue by its number of line items. The query is valid, the result is wrong, and nothing in the output says so. This is the most expensive bug in analytics because it looks like data.

Malloy computes symmetric aggregates: it knows the graph and the grain each measure belongs to, so total_revenue is correct whether or not order_items is in the query. The fan-out case is not caught and warned about — it cannot be expressed.

Stale references

SQL is a string until the database parses it. Rename a column and the failure surfaces when someone opens the dashboard, if anyone notices at all — a LEFT JOIN that stops matching returns nulls, not an error.

Malloy checks every field reference against the warehouse schema at compile time. A rename breaks the build and names what broke.

Drifting definitions

There is no way to save a calculation in SQL. You can save a query, or a view, but not "net revenue, which excludes refunds and counts at order completion" as a thing other queries reference. So the definition lives in a wiki, a dbt macro, a BI tool's config, and four analysts' heads, and they drift.

In Malloy a measure is a named language construct with a doc string attached to it:

#(doc) Net revenue recognized at order completion, in USD
measure: net_revenue is sum(order_amount) - sum(refund_amount)

Where SQL Stays the Right Tool

Malloy is a modeling and querying language for analytical questions. It is not trying to be the whole of SQL, and a Credible model normally contains some SQL:

  • Transformation and ingestion. Loading, deduplicating, type-fixing, slowly-changing dimensions — this is pipeline work, and it stays in SQL or in dbt. Malloy models the result.
  • Warehouse-specific features. Vendor functions, geospatial operators, ML predicates. Malloy has a SQL escape hatch and you should use it rather than contorting the model.
  • One-off questions against untouched tables. If a question is asked once and never again, the model earns nothing.
  • DDL and administration. Not Malloy's job at all.

The boundary is roughly: SQL describes how to compute, Malloy describes what things mean, and a healthy stack has both. A model that has no SQL in it anywhere is usually a model that has pushed transformation somewhere it does not belong.

What This Buys an Agent

An LLM writing SQL against raw tables has to infer the join keys, guess which revenue column is authoritative, and hope its aggregate lands at the right grain. It will produce something plausible every time, and it has no way to tell you which of those guesses it made.

An agent writing Malloy against a model asks for total_revenue and gets the definition a person signed off on. The failure mode changes shape: instead of a confident wrong number, a reference to something that does not exist, which the compiler rejects before a query runs. That is the property Credible is built on — see Built for AI.

Reading the SQL

Nothing is hidden. Credible can return the compiled SQL for any query, so a Malloy model is auditable the same way a view is: you can hand the generated SQL to a DBA, paste it into the warehouse console, or diff it against what the old dashboard ran. Migrations rely on this — the migration guides validate a rebuilt model row by row against the numbers the previous tool produced.

Next Steps

For the language itself, see the Malloy Language Documentation and the Malloy GitHub repository.

On this page