Offerings
Features & Capabilities
Customer Stories
Learn
Engage
Books
AQL is the high-level query language for analytics. Expressive enough for an AI agent to answer any question, concise enough for a human to trust the answer.
metric revenue = sum(order_items.quantity * products.price) metric refunded_revenue = revenue | where(orders.status == 'refunded') metric refund_rate = safe_divide(refunded_revenue, revenue)
AQL works at the level of metrics, not tables. You define a metric once on your semantic model, then ask for it by name, and AQL compiles that to native SQL.
metric revenue = sum(order_items.quantity * products.price) explore { dimensions { customers.country } measures { revenue } }
SELECT c.country, SUM(oi.quantity * p.price) AS revenue FROM order_items oi JOIN products p ON oi.product_id = p.id JOIN orders o ON oi.order_id = o.id JOIN customers c ON o.customer_id = c.id GROUP BY c.country ORDER BY revenue DESC
"I'd score AQL a 9 or 10. It's up there with the best tools I've used. We can now define metrics based on other metrics, stacking them on top of each other."
Most semantic layers store each metric as a SQL string wrapped in YAML, so they inherit SQL's ceiling the moment a metric builds on another metric. AQL makes the metric itself a first-class object you compose like functions, then compiles it to native SQL.
The CTE-and-window gymnastics SQL forces on you are single functions here.
Break analytics into small steps and chain them with the pipe (|) — modular pieces that snap together like Lego blocks.
|
AQL runs on your semantic model, so queries reference your defined metrics, dimensions, and relationships by name — not raw schema.
The same AQL always compiles to the same SQL — and you can inspect exactly what ran. Nothing hidden.
Write once. The compiler speaks the dialect.
A strong type system validates queries as you write — errors surface in the IDE, not in production dashboards.
Define a metric once against your semantic model. From there, reshape it with the pipe and build new metrics on top. Each one is a reusable object, so logic is never copied or left to drift.
metric revenue = sum(order_items.quantity * products.price)
Spans two models, and the join resolves from relationships you defined once. No JOIN written, ever.
JOIN
revenue | where(products.category == 'Gadgets')
The pipe chains operations left to right. The same metric adapts to any dimension, filter, or time window around it.
metric yoy_change = safe_divide(revenue - revenue_ly, revenue_ly)
Metrics reference metrics. Change revenue once and every derived metric updates — nothing drifts.
revenue
Most natural-language tools translate your question straight into SQL. Holistics adds an intermediary step. The AI first writes concise, high-level AQL, which a compiler then turns into your warehouse's SQL.
metric count_users = count(users.id)dimensions { region: country.region }measures { avg_revenue: top(5, countries.id, by: count_users) | avg(revenue)}
WITH "aql__t3" AS ( SELECT "ecommerce_countries"."continent_name" AS "ecommerce_countries→continent_name", "ecommerce_countries"."name" AS "name", COUNT("ecommerce_users"."id") AS "count_ecommerce_users→id" FROM "ecommerce"."users" "ecommerce_users" JOIN "ecommerce"."countries" …
Routing the AI through AQL instead of straight to SQL pays off on every axis that matters for agentic analytics.
The answer comes back as a few lines of compact, high-level logic instead of pages of SQL. It's small enough to read, comprehend, and verify yourself.
Working in a high-level language, the AI never sweats database-specific syntax or SQL gymnastics for common analytics, so it's far more accurate than a model writing raw SQL directly.
AQL works directly with your semantic layer, so generated logic always uses the correct metric and dimension definitions, and clears access-control checks before it serves an end user.
Composability and 90+ pre-built analytical functions let it handle far more complex use cases, the long-tail questions that direct SQL generation stumbles on.
Period comparisons, cohorts, percent-of-total, nested aggregation — the analytics that mean a new dataset or a custom SQL block almost everywhere else. With AQL, each one is a single composable expression.
metric revenue_ly = revenue | relative_period(-1 year) metric yoy_change = safe_divide(revenue - revenue_ly, revenue_ly)
WITH monthly AS ( SELECT DATE_TRUNC('month', created_at) AS month, SUM(amount) AS revenue FROM orders GROUP BY 1 ) SELECT curr.month, curr.revenue, (curr.revenue - prev.revenue) * 100.0 / prev.revenue AS yoy_change FROM monthly curr LEFT JOIN monthly prev ON curr.month = prev.month + INTERVAL '1 year'
metric category_share = (revenue * 1.0) / (revenue | of_all(products.category))
SELECT p.category, SUM(oi.quantity * p.price) * 100.0 / SUM(SUM(oi.quantity * p.price)) OVER () AS pct_of_total FROM order_items oi JOIN products p ON p.id = oi.product_id GROUP BY 1
users | group(users.created_at | month()) | select(monthly_count: count(users.id)) | avg(monthly_count)
SELECT AVG(monthly_count) FROM ( SELECT DATE_TRUNC('month', created_at) AS month, COUNT(*) AS monthly_count FROM users GROUP BY 1 ) sub
dimension acquisition_cohort = min(orders.created_at | month()) | dimensionalize(users.id) metric retention = (total_users * 1.0) / (total_users | of_all(orders.month_no))
WITH first_orders AS ( SELECT user_id, DATE_TRUNC('month', MIN(created_at)) AS cohort_month FROM orders GROUP BY 1 ), activity AS ( SELECT o.user_id, f.cohort_month, …
metric moving_avg_3m = trailing_period(revenue, orders.created_at, interval(3 months)) / 3
SELECT month, AVG(revenue) OVER ( ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS moving_avg_3m FROM monthly_revenue -- counts rows, not months: a gap month -- silently skews the average; fixing it -- needs a generated calendar spine
dimension performance_tier = case( when: ntile(4, order: revenue | desc()) = 1, then: 'Top 25%', when: ntile(4, order: revenue | desc()) = 2, then: '25–50%', else: 'Bottom 50%' ) | dimensionalize(products.id)
SELECT product_id, CASE WHEN NTILE(4) OVER (ORDER BY revenue DESC) = 1 THEN 'Top 25%' WHEN NTILE(4) OVER (ORDER BY revenue DESC) = 2 THEN '25–50%' ELSE 'Bottom 50%' END AS tier FROM ( SELECT product_id, SUM(amount) AS revenue FROM order_items GROUP BY 1 ) t
The harder the question, the more the language matters.
The AI reuses your team's endorsed metrics and composes new ones from existing definitions — it doesn't reinvent "revenue" from raw tables, and access controls in the semantic layer apply automatically.
AQL compiles to native SQL you can inspect on every query — nothing is hidden. Your definitions are plain-text code in Git, not rows in a vendor database. If you leave, you leave with readable logic.
AQL is a complement, not a replacement. SQL is great at pipelines and ad-hoc exploration; it has no native way to define a metric once and compose it. AQL covers exactly that gap — and when you need raw SQL, passthrough functions (sql_number, agg_text, …) are the escape hatch.
sql_number
agg_text
AQL is designed for agents to write and humans to read. The AI writes most of it; your job is to read a few lines and confirm it understood the question — and because the logic is high-level, that read is fast. When you do want to write it yourself, anyone who reads SQL picks it up from the 30-minute guided tour.
"The AI does far more than just fetch answers — it understands the context and generates working solutions, even for edge cases."
In production across hundreds of enterprises.
Go deeper: the functions cheatsheet · learn AQL in 30 minutes · see the full Holistics AI product →