Powerful analytics
Go beyond basic metrics with composable calculations and reusable analytical functions.
AMQL · Analytics Modeling & Query Language
Define reusable metrics and business logic as code. Compose complex analytical queries with functions that operate on metrics, and compile them into native warehouse SQL.
Model orders {
table_name: 'ecommerce.orders'
dimension status { type: 'text' }
dimension created_at { type: 'datetime' }
}
// assemble models into a governed dataset
Dataset ecommerce {
models: [orders, order_items, products, users]
relationships: [
relationship(order_items.order_id > orders.id, true),
relationship(order_items.product_id > products.id, true)
]
metric revenue {
type: 'number'
definition: @aql
sum(order_items,
order_items.quantity * products.price) ;;
}
}revenue
| where(orders.status != 'refunded')
| relative_period(orders.created_at,
interval(-1 year))Go beyond basic metrics with composable calculations and reusable analytical functions.
Own your definitions in Git. Keep core semantic models portable through open standards.
Keep metrics consistent across reports and AI. Catch errors early and inspect how results are calculated.
The semantic layer behind analytics at





AMQL is two languages over one model. AML defines what your data means. You ask your question in AQL, and the compiler turns that AQL into SQL on your warehouse.
Read the docsextend, Func and Partial instead of copying blocks.A metric is written one time and every query, dashboard and agent reads that one definition.
The model knows the joins and the grain, so a total stays right when you slice, filter or join.
Queries ask for the metrics and dimensions your team already defined. Closer to how the business thinks, and expressive enough for an AI agent to reason from.
AML models every proven warehouse pattern, from a single star to shared dimensions, deep hierarchies, role-playing dates, composite keys and validity windows. Declare facts, dimensions and relationships once, and the model stays accurate and easy to maintain as tables and questions grow.
Explore AMLOne business process, many ways to slice it.
Star schema in the docsDataset ecommerce {
models: [
fct_orders, // fact
dim_users, // dimension
dim_products, // dimension
dim_dates // dimension
]
relationships: [
relationship(fct_orders.user_id > dim_users.id, true),
relationship(fct_orders.product_id > dim_products.id, true),
relationship(fct_orders.created_at > dim_dates.date, true)
]
}Two business processes that share dimensions.
Galaxy schema in the docsDataset galaxy_schema {
models: [fct_orders, fct_inventory, dim_products, dim_dates]
relationships: [
// all four active, the compiler picks the path
relationship(fct_orders.product_id > dim_products.id, true),
relationship(fct_orders.created_at > dim_dates.date, true),
relationship(fct_inventory.product_id > dim_products.id,
true),
relationship(fct_inventory.created_at > dim_dates.date, true)
]
metric total_gmv {
definition: @aql
fct_orders | sum(fct_orders.item_value);;
}
metric total_available_products {
definition: @aql
fct_inventory | sum(fct_inventory.quantity_available);;
}
}Dimensions normalised into hierarchies.
Snowflake schema in the docsDataset sales {
models: [
fct_sales,
dim_products, dim_subcategories, dim_categories,
dim_customers, dim_cities, dim_countries
]
relationships: [
relationship(fct_sales.product_id > dim_products.id, true),
relationship(dim_products.subcategory_id
> dim_subcategories.id, true),
relationship(dim_subcategories.category_id
> dim_categories.id, true),
relationship(fct_sales.customer_id > dim_customers.id, true),
relationship(dim_customers.city_id > dim_cities.id, true),
relationship(dim_cities.country_id > dim_countries.id, true)
]
}One dimension, several meanings in the same fact.
Role-playing dimensions in the docsDataset role_playing_example {
models: [fct_orders, dim_dates]
relationships: [
// one active, three switched on per metric
relationship(fct_orders.created_at > dim_dates.date, true),
relationship(fct_orders.delivered_at > dim_dates.date,
false),
relationship(fct_orders.cancelled_at > dim_dates.date,
false),
relationship(fct_orders.refunded_at > dim_dates.date, false)
]
metric total_created_orders {
definition: @aql fct_orders | count(fct_orders.id);;
}
metric total_delivered_orders {
definition: @aql
fct_orders
| count(fct_orders.id)
| with_relationships(
fct_orders.delivered_at > dim_dates.date)
;;
}
}A fact keyed by two columns at once.
Composite keys in the docsRelationship order_line_shipment {
type: 'many_to_one'
from: r(order_lines.order_id)
to: r(shipments.order_id)
on { from: r(order_lines.warehouse_id)
to: r(shipments.warehouse_id) }
}
Dataset logistics {
models: [order_lines, shipments]
relationships: [
relationship(order_line_shipment, true)
]
}A dimension that changes over time, with a start and an end.
Validity windows in the docsRelationship order_price_band {
type: 'range'
from: r(orders.id)
to: r(price_bands.id)
match: @aql
orders.currency == price_bands.currency
and orders.ordered_at >= price_bands.valid_from
and orders.ordered_at
< coalesce(price_bands.valid_to, @9999-12-31)
;;
}
Dataset pricing {
models: [orders, price_bands]
relationships: [
relationship(order_price_band, true)
]
}One foreign key, several target tables.
Polymorphic associations in the docsRelationship comment_post {
type: 'many_to_one'
from: r(comments.commentable_id)
to: r(posts.id)
where { from: @aql
comments.commentable_type == 'Post' ;; }
}
// same again for photos, with 'Photo'
Dataset community {
models: [comments, posts, photos]
relationships: [
relationship(comment_post, true),
relationship(comment_photo, true)
]
}Two tables with date ranges that overlap, and no key between them.
Interval overlap in the docsRelationship order_open_period {
type: 'many_to_many'
from: r(orders.id)
to: r(periods.label)
match: @aql
orders.order_date <= periods.end_date
and coalesce(orders.deliver_date, @9999-12-31)
> periods.start_date
;;
}
Dataset operations {
models: [orders, periods]
relationships: [
relationship(order_open_period, true)
]
}Six real analytical questions, from time comparisons to cohort retention. AQL expresses reusable analytical logic, while SQL spells out the query mechanics. See how the same calculations can be expressed with composable metrics instead of repeatedly writing joins, aggregations and window functions.
Explore AQL// Report: by order month, showing
// sum_revenue beside this metric
metric revenue_previous_year =
sum_revenue
| relative_period(orders.created_at,
interval(-1 year));WITH monthly AS (
SELECT date_trunc('month', o.created_at) AS month,
SUM(oi.quantity * p.price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
GROUP BY 1
)
SELECT curr.month,
curr.revenue AS sum_revenue,
prev.revenue AS revenue_previous_year
FROM monthly curr
LEFT JOIN monthly prev
ON prev.month = curr.month - INTERVAL '1 year'
ORDER BY curr.month// Report: by signup year. Averages
// over months that have signups.
metric avg_monthly_acquisition =
users
| group(users.created_at | month())
| select(new_customers)
| average();WITH monthly AS (
SELECT date_trunc('year', created_at) AS year,
date_trunc('month', created_at) AS month,
COUNT(*) AS new_customers
FROM users
GROUP BY 1, 2
)
SELECT year,
AVG(new_customers) AS avg_monthly_acquisition
FROM monthly
GROUP BY year
ORDER BY year// Report: by country. Shares are of the
// filtered total; sales with no country
// stay in, as a NULL country group.
metric total_sales =
sum(sales.amount)
| of_all(countries.name, keep_filters: true);
metric pct_of_total =
safe_divide(sum(sales.amount), total_sales);SELECT
c.name AS country,
SUM(s.amount) AS revenue,
SUM(s.amount) * 1.0
/ NULLIF(SUM(SUM(s.amount)) OVER (), 0)
AS pct_of_total
FROM sales s
LEFT JOIN countries c ON c.id = s.country_id
GROUP BY c.name
ORDER BY pct_of_total DESC// Report: by order month, across all available years
metric cumulative_revenue =
running_total(revenue, orders.created_at);WITH monthly AS (
SELECT date_trunc('month', o.created_at) AS month,
SUM(oi.quantity * p.price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
GROUP BY 1
)
SELECT month, revenue,
SUM(revenue) OVER (ORDER BY month) AS cumulative_revenue
FROM monthly
ORDER BY month// Report: every product, by category
metric ranking_in_category =
rank(order: revenue | desc(),
partition: categories.name);SELECT c.name AS category, p.name AS product,
SUM(oi.quantity * p.price) AS revenue,
RANK() OVER (
PARTITION BY c.name
ORDER BY SUM(oi.quantity * p.price) DESC
) AS ranking_in_category
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN categories c ON c.id = p.category_id
GROUP BY c.name, p.id, p.name
ORDER BY category, ranking_in_categorydimension first_purchase_cohort =
min(orders.created_at | month())
| dimensionalize(users.id);
dimension month_no =
date_diff('month', users.first_purchase_cohort,
orders.created_at | month());
metric total_users =
count(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 DISTINCT
o.user_id,
f.cohort_month,
(EXTRACT(year FROM o.created_at)
- EXTRACT(year FROM f.cohort_month)) * 12
+ (EXTRACT(month FROM o.created_at)
- EXTRACT(month FROM f.cohort_month)) AS month_no
FROM orders o
JOIN first_orders f ON f.user_id = o.user_id
),
cohort_counts AS (
SELECT cohort_month, month_no, COUNT(DISTINCT user_id) AS users
FROM activity
GROUP BY 1, 2
),
cohort_sizes AS (
SELECT cohort_month, users AS cohort_size
FROM cohort_counts
WHERE month_no = 0
)
SELECT c.cohort_month, c.month_no, c.users,
c.users * 1.0 / s.cohort_size AS retention
FROM cohort_counts c
JOIN cohort_sizes s ON s.cohort_month = c.cohort_month
ORDER BY 1, 2Turn business rules and analytical patterns into reusable functions. Pass metrics as inputs, transform how they’re calculated, and reuse the same logic across your analytics.
Try in playgroundDefine a business rule as a function that accepts a metric. Apply the same eligibility logic consistently to revenue, order count and customer count.
Read about AQL functionsAqlFunctionModule Acme {
// what counts as a sale, company-wide
function Billable {
param pMetric {
type: 'metric'
}
return_type: 'number'
definition: @aql
pMetric
| where(users.is_internal == false,
orders.status != 'refunded') ;;
}
}Dataset ecommerce {
function_module Acme: Acme
metric revenue {
definition: @aql
Acme.Billable(sum(orders.amount)) ;;
}
metric order_count {
definition: @aql
Acme.Billable(count(orders.id)) ;;
}
metric customer_count {
definition: @aql
Acme.Billable(
count_distinct(orders.user_id)) ;;
}
}Package analytical patterns into reusable functions. Apply the same classification logic to different metrics and groupings without rewriting the algorithm.
ABC classification in the docsAqlFunctionModule Abc {
// A: first 70% of the total, B: next 20%, C: the tail
function Class {
param pMeasure {
type: 'metric'
}
param pItems {
type: 'model'
}
return_type: 'text'
definition: @aql
const pct = safe_divide(
window_sum(pMeasure, order: pMeasure | desc()),
pMeasure | of_all(pItems, keep_filters: true));
case(when: pct <= 0.7, then: 'A',
when: pct <= 0.9, then: 'B',
else: 'C') ;;
}
}Dataset ecommerce {
function_module Abc: Abc
metric product_class {
definition: @aql
Abc.Class(sales, products) ;;
}
metric customer_class {
definition: @aql
Abc.Class(revenue, users) ;;
}
}Share metric definitions across datasets and teams without duplicating the underlying business logic.
Metric store in the docsPartialDataset revenue_metrics {
metric gmv {
definition: @aql
sum(orders.amount) ;;
}
metric revenue {
definition: @aql
gmv | where(orders.status != 'refunded') ;;
}
metric aov {
definition: @aql
revenue / count(orders.id) ;;
}
}Dataset sales = sales_base
.extend(revenue_metrics)
Dataset finance = finance_base
.extend(revenue_metrics)
Dataset marketing = marketing_base
.extend(revenue_metrics)Extend shared semantic models for different clients while keeping common definitions consistent.
AML extend in the docsDataset ecommerce {
data_source_name: 'demo_dw'
models: [orders, users, products]
relationships: [
relationship(orders.user_id > users.id,
true),
relationship(orders.product_id
> products.id, true)
]
}Dataset acme = ecommerce.extend({
data_source_name: 'acme_dw'
})
Dataset globex = ecommerce.extend({
data_source_name: 'globex_dw'
})The fear with any semantic layer is that your business logic ends up inside a vendor. Here it is plain text in your Git, compiles to SQL you can read, and moves to an open interchange format on one command.
See the SQL in the playgroundAML and AQL are plain-text files in your own repo, not rows in a vendor database. Clone, grep, diff, move.
No proprietary runtime. The output runs on your warehouse with nothing of AMQL in the path.
Export any dataset to Ossie, the open semantic interchange format, and import an Ossie model back. Holistics maintains the converter inside the Apache Ossie repo.
Ossie ecosystem“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.”
IanBI Engineer, DNSFilter“I have KPI widgets that appear in 50 rows of my dashboard. I took one function, parameterized it, and just pass in the variables. The ability to parameterize a widget and reuse it was a game changer.”
Mike K.Director of Enterprise Analytics, Strongmind“The semantic layer is Holistics’ biggest draw, because I was able to build reusable models, dimensions, and metrics, and then describe them as code.”
Sterling ParamoreSr Staff Data Engineer, Mainspring EnergyDefine it once in AMQL. Run it in anfra or Holistics, on your warehouse, for every consumer.
Turn AI-generated pages into live, governed data apps.
Explore anfraThe BI platform built on AMQL: dashboards, embedded analytics, self-service and AI.
Explore HolisticsNo. YAML in Git is still YAML: no functions, no types, no real composition. Reuse means copy-paste or Jinja string-templating. AML is a typed language where reuse (extend, Func, Partial) is a first-class feature the compiler checks for you, and AQL lets you write your own functions that take whole metrics.
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, but it has no native way to define a metric once and compose it. AQL covers exactly that gap. When you need raw SQL, passthrough functions (sql_number, agg_text, …) are the escape hatch.
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. When you do want to write it yourself, anyone who reads SQL picks it up from the 30-minute guided tour.
Go deeper: the AML modeling language, the AQL query language, and learn AQL in 30 minutes