Metric queries are the core query structure in Lightdash, defining what data to fetch from explores including dimensions, metrics, filters, sorts, and calculations.
A metric query specifies:
The core query type in Lightdash for defining what data to fetch from an explore.
type MetricQuery = {
exploreName: string;
dimensions: FieldId[];
metrics: FieldId[];
filters: Filters;
sorts: SortField[];
limit: number;
tableCalculations: TableCalculation[];
additionalMetrics?: AdditionalMetric[];
customDimensions?: CustomDimension[];
metricOverrides?: MetricOverrides;
timezone?: string;
metadata?: {
hasADateDimension: Pick<CompiledDimension, "label" | "name" | "table">;
};
periodOverPeriod?: PeriodOverPeriodComparison;
};exploreName: Name of the explore (data model) to query (e.g., "customers", "orders"). Must match an explore defined in your dbt project.
dimensions: Array of dimension field IDs to group by. Field IDs use underscore format: "tableName_fieldName" (e.g., "customers_customer_id", "customers_name")
metrics: Array of metric field IDs to aggregate. Field IDs use underscore format: "tableName_metricName" (e.g., "customers_total_orders", "customers_revenue")
filters: Filter configuration for dimensions, metrics, and table calculations. Required property containing nested filter groups with AND/OR logic.
sorts: Array of SortField configurations specifying sort order. Required property (can be empty array for no sorting).
limit: Maximum number of rows to return from the query. Required property. Use a large number like 500 or 5000 for "unlimited" results.
tableCalculations: Array of TableCalculation definitions for post-query calculations. Required property (can be empty array if no calculations needed).
additionalMetrics: Optional array of AdditionalMetric definitions for ad-hoc metrics created at query time without modifying the explore. These metrics are compiled and included in the query execution.
customDimensions: Optional array of CustomDimension definitions including binned dimensions and custom SQL dimensions created at query time.
metricOverrides: Optional record mapping metric field IDs to format option overrides. Allows temporarily changing metric formatting without modifying the explore definition. Type: Record<string, Pick<Metric, 'formatOptions'>>
timezone: Optional timezone string for date/time operations (e.g., "America/New_York", "UTC"). Used to convert timestamps to the specified local timezone.
metadata: Optional metadata object containing query information. Currently supports hasADateDimension property with the date dimension's label, name, and table.
periodOverPeriod: Optional period-over-period comparison configuration for time-based analysis comparing current period to previous period.
The compiled version of a MetricQuery with all calculations, metrics, and dimensions compiled and ready for SQL generation.
type CompiledMetricQuery = Omit<MetricQuery, "customDimensions"> & {
compiledTableCalculations: CompiledTableCalculation[];
compiledAdditionalMetrics: CompiledMetric[];
compiledCustomDimensions: CompiledCustomDimension[];
};Properties:
customDimensionsCompiledCustomDimension[] which includes both CompiledDimension and CustomBinDimensionThis type is used internally during query execution after the compilation phase transforms the user-defined query into executable SQL.
interface SortField {
fieldId: string;
descending: boolean;
nullsFirst?: boolean;
}Specifies sorting for a field.
Example:
const sorts: SortField[] = [
{ fieldId: "customers.revenue", descending: true }, // Highest revenue first
{ fieldId: "customers.name", descending: false }, // Then alphabetical
];Additional metrics are ad-hoc metrics defined at query time without modifying the explore definition.
interface AdditionalMetric {
name: string;
label?: string;
type: MetricType;
sql: string;
table: string;
hidden?: boolean;
baseDimensionName?: string;
percentile?: number;
formatOptions?: CustomFormat;
/** @deprecated Use format expression instead */
compact?: CompactOrAlias;
/** @deprecated Use format expression instead */
format?: Format | string;
/** @deprecated Use format expression instead */
round?: number;
filters?: MetricFilterRule[];
description?: string;
index?: number;
uuid?: string | null;
}Example:
import { MetricType, type AdditionalMetric } from "@lightdash/common";
// Count distinct customers
const uniqueCustomers: AdditionalMetric = {
label: "Unique Customers",
type: MetricType.COUNT_DISTINCT,
table: "orders",
baseDimensionName: "customer_id",
formatOptions: {
type: CustomFormatType.NUMBER,
separator: NumberSeparator.COMMA_PERIOD,
},
};
// Custom SQL metric
const averageOrderValue: AdditionalMetric = {
label: "Average Order Value",
type: MetricType.NUMBER,
sql: "SUM(${orders.total_amount}) / COUNT(DISTINCT ${orders.order_id})",
table: "orders",
formatOptions: {
type: CustomFormatType.CURRENCY,
currency: "USD",
round: 2,
},
};
// High-value orders (with filter)
const highValueOrders: AdditionalMetric = {
label: "High Value Orders",
type: MetricType.COUNT_DISTINCT,
table: "orders",
baseDimensionName: "order_id",
filters: [
{
id: "high_value",
target: { fieldId: "orders.total_amount" },
operator: FilterOperator.GREATER_THAN,
values: [1000],
},
],
};Type guard to check if a value is an AdditionalMetric.
function isAdditionalMetric(value: any): value is AdditionalMetric;Example:
import { isAdditionalMetric } from "@lightdash/common";
if (isAdditionalMetric(item)) {
console.log(`Ad-hoc metric: ${item.label}`);
}Checks if a metric has custom format options.
function hasFormatOptions(metric: Metric | AdditionalMetric): boolean;Example:
import { hasFormatOptions } from "@lightdash/common";
if (hasFormatOptions(metric)) {
// Apply custom formatting
const formatted = applyCustomFormat(metric, value, metric.formatOptions);
}Gets the dimension ID for a custom metric (used for metrics that aggregate a dimension).
function getCustomMetricDimensionId(additionalMetric: AdditionalMetric): string;Returns a field ID like "orders.customer_id" for the dimension being aggregated.
Example:
import { getCustomMetricDimensionId } from "@lightdash/common";
const dimensionId = getCustomMetricDimensionId(additionalMetric);
// For COUNT DISTINCT on orders.customer_id, returns "orders.customer_id"Gets the display label for an additional metric.
function getAdditionalMetricLabel(additionalMetric: AdditionalMetric): string;Returns the metric's label, or generates one from the metric configuration if no label is provided.
Example:
import { getAdditionalMetricLabel } from "@lightdash/common";
const label = getAdditionalMetricLabel(additionalMetric);
// Returns: "Unique Customers" or "Count distinct of Customer ID"Counts the number of custom dimensions in a metric query.
function countCustomDimensionsInMetricQuery(metricQuery: MetricQuery): {
numFixedWidthBinCustomDimensions: number;
numFixedBinsBinCustomDimensions: number;
numCustomRangeBinCustomDimensions: number;
numCustomSqlDimensions: number;
};Example:
import { countCustomDimensionsInMetricQuery } from "@lightdash/common";
const counts = countCustomDimensionsInMetricQuery(metricQuery);
console.log(`Fixed width bins: ${counts.numFixedWidthBinCustomDimensions}`);
console.log(`Fixed bins: ${counts.numFixedBinsBinCustomDimensions}`);
console.log(`Custom range bins: ${counts.numCustomRangeBinCustomDimensions}`);
console.log(`Custom SQL dimensions: ${counts.numCustomSqlDimensions}`);Checks if a metric query contains any custom bin dimensions.
function hasCustomBinDimension(metricQuery?: MetricQuery): boolean;Example:
import { hasCustomBinDimension } from "@lightdash/common";
if (hasCustomBinDimension(metricQuery)) {
// Special handling for binned data
}The response structure returned from executing a metric query.
interface MetricQueryResponse {
rows: Record<string, unknown>[];
metricQuery: MetricQuery;
fields: Record<string, Field | TableCalculation | AdditionalMetric | CustomDimension>;
cacheMetadata?: CacheMetadata;
}Example:
const response: MetricQueryResponse = {
rows: [
{
"customers.customer_id": 1,
"customers.name": "John Doe",
"customers.total_orders": 5,
"customers.revenue": 1250.5,
},
// ... more rows
],
metricQuery: {
exploreName: "customers",
dimensions: ["customers.customer_id", "customers.name"],
metrics: ["customers.total_orders", "customers.revenue"],
filters: { dimensions: { id: "root", and: [] } },
sorts: [{ fieldId: "customers.revenue", descending: true }],
limit: 100,
tableCalculations: [],
},
fields: {
"customers.customer_id": {
/* dimension definition */
},
"customers.name": {
/* dimension definition */
},
"customers.total_orders": {
/* metric definition */
},
"customers.revenue": {
/* metric definition */
},
},
};Response format for filters in API responses, similar to Filters but with AnyType values.
type FilterGroupResponse =
| {
id: string;
or: AnyType[];
}
| {
id: string;
and: AnyType[];
};
type FiltersResponse = {
dimensions?: FilterGroupResponse;
metrics?: FilterGroupResponse;
tableCalculations?: FilterGroupResponse;
};Used in API responses when filters need to be serialized. The structure matches Filters but uses AnyType for flexibility in serialization.
API request format for metric queries, simplified for HTTP API usage.
type MetricQueryRequest = {
exploreName: string;
dimensions: FieldId[];
metrics: FieldId[];
filters: {
dimensions?: AnyType;
metrics?: AnyType;
tableCalculations?: AnyType;
};
sorts: SortField[];
limit: number;
tableCalculations: TableCalculation[];
additionalMetrics?: AdditionalMetric[];
csvLimit?: number;
customDimensions?: CustomDimension[];
dateZoom?: DateZoom;
metadata?: MetricQuery["metadata"];
timezone?: string;
metricOverrides?: MetricOverrides;
periodOverPeriod?: PeriodOverPeriodComparison;
};Properties:
This type is used for API endpoints and differs from MetricQuery by using AnyType for filters instead of strongly typed filter structures.
Warning messages returned with query results to inform users about potential issues.
type QueryWarning = {
message: string;
fields?: string[];
tables?: string[];
};Properties:
Example:
const warning: QueryWarning = {
message: "Some filters could not be applied due to missing fields",
fields: ["customers.deleted_field"],
tables: ["customers"],
};Override format options for metrics in a query.
type MetricOverrides = { [key: string]: Pick<Metric, "formatOptions"> };A record mapping metric field IDs to format option overrides. Used to temporarily override metric formatting without modifying the explore definition. Each key is a metric field ID (e.g., "orders_total_revenue") and the value contains only the formatOptions property from the Metric type.
import { type MetricQuery } from "@lightdash/common";
const query: MetricQuery = {
exploreName: "orders",
dimensions: ["orders.order_date", "orders.status"],
metrics: ["orders.count", "orders.total_amount"],
filters: { dimensions: { id: "root", and: [] } },
sorts: [{ fieldId: "orders.order_date", descending: false }],
limit: 1000,
tableCalculations: [],
};import { type MetricQuery, FilterOperator, UnitOfTime } from "@lightdash/common";
const query: MetricQuery = {
exploreName: "orders",
dimensions: ["orders.status"],
metrics: ["orders.count", "orders.total_amount"],
filters: {
dimensions: {
id: "root",
and: [
{
id: "date_filter",
target: { fieldId: "orders.created_at" },
operator: FilterOperator.IN_THE_PAST,
values: [30],
settings: { unitOfTime: UnitOfTime.days },
},
{
id: "status_filter",
target: { fieldId: "orders.status" },
operator: FilterOperator.EQUALS,
values: ["completed"],
},
],
},
},
sorts: [{ fieldId: "orders.count", descending: true }],
limit: 100,
tableCalculations: [],
};import {
type MetricQuery,
type AdditionalMetric,
MetricType,
CustomFormatType,
} from "@lightdash/common";
const avgOrderValue: AdditionalMetric = {
label: "Average Order Value",
type: MetricType.AVERAGE,
table: "orders",
baseDimensionName: "total_amount",
formatOptions: {
type: CustomFormatType.CURRENCY,
currency: "USD",
round: 2,
},
};
const query: MetricQuery = {
exploreName: "orders",
dimensions: ["orders.customer_id"],
metrics: ["orders.count"],
filters: { dimensions: { id: "root", and: [] } },
additionalMetrics: [avgOrderValue],
sorts: [{ fieldId: "orders.count", descending: true }],
limit: 50,
tableCalculations: [],
};import {
type MetricQuery,
type TableCalculation,
TableCalculationType,
WindowFunctionType,
} from "@lightdash/common";
const runningTotal: TableCalculation = {
name: "running_total",
displayName: "Running Total",
calculationType: TableCalculationType.SQL,
windowFunction: {
type: WindowFunctionType.SUM,
field: "orders.total_amount",
orderBy: {
field: "orders.order_date",
ascending: true,
},
},
};
const query: MetricQuery = {
exploreName: "orders",
dimensions: ["orders.order_date"],
metrics: ["orders.total_amount"],
filters: { dimensions: { id: "root", and: [] } },
tableCalculations: [runningTotal],
sorts: [{ fieldId: "orders.order_date", descending: false }],
limit: 1000,
};import {
type MetricQuery,
type CustomBinDimension,
CustomDimensionType,
BinType,
} from "@lightdash/common";
const orderAmountBins: CustomBinDimension = {
id: "order_amount_bins",
name: "order_amount_bins",
type: CustomDimensionType.BIN,
table: "orders",
dimensionId: "orders.total_amount",
binType: BinType.CUSTOM_RANGE,
customRange: [
{ from: 0, to: 50, label: "Small (0-50)" },
{ from: 50, to: 200, label: "Medium (50-200)" },
{ from: 200, to: Infinity, label: "Large (200+)" },
],
};
const query: MetricQuery = {
exploreName: "orders",
dimensions: ["order_amount_bins"],
metrics: ["orders.count"],
filters: { dimensions: { id: "root", and: [] } },
customDimensions: [orderAmountBins],
sorts: [{ fieldId: "order_amount_bins", descending: false }],
limit: 100,
tableCalculations: [],
};import {
type MetricQuery,
type AdditionalMetric,
type TableCalculation,
FilterOperator,
MetricType,
CustomFormatType,
TableCalculationType,
} from "@lightdash/common";
// Ad-hoc metric: high-value customer count
const highValueCustomers: AdditionalMetric = {
label: "High Value Customers",
type: MetricType.COUNT_DISTINCT,
table: "orders",
baseDimensionName: "customer_id",
filters: [
{
id: "high_value",
target: { fieldId: "orders.total_amount" },
operator: FilterOperator.GREATER_THAN,
values: [500],
},
],
};
// Table calculation: percentage of total
const percentOfTotal: TableCalculation = {
name: "percent_of_total",
displayName: "% of Total",
sql: "${orders.count} / SUM(${orders.count}) OVER ()",
calculationType: TableCalculationType.SQL,
format: {
type: CustomFormatType.PERCENT,
round: 1,
},
};
const query: MetricQuery = {
exploreName: "orders",
dimensions: ["orders.order_date", "orders.status"],
metrics: ["orders.count", "orders.total_amount"],
additionalMetrics: [highValueCustomers],
tableCalculations: [percentOfTotal],
filters: {
dimensions: {
id: "root",
and: [
{
id: "date_filter",
target: { fieldId: "orders.created_at" },
operator: FilterOperator.IN_THE_PAST,
values: [90],
settings: { unitOfTime: UnitOfTime.days },
},
],
},
},
sorts: [
{ fieldId: "orders.order_date", descending: false },
{ fieldId: "orders.total_amount", descending: true },
],
limit: 500,
timezone: "America/New_York",
};import {
formatItemValue,
formatRows,
getItemMap,
type MetricQueryResponse,
} from "@lightdash/common";
function displayResults(response: MetricQueryResponse, explore: Explore) {
const itemsMap = getItemMap(
explore,
response.metricQuery.additionalMetrics,
response.metricQuery.tableCalculations,
response.metricQuery.customDimensions
);
// Format all rows
const formattedRows = formatRows(response.rows, itemsMap);
// Or format individual values
response.rows.forEach((row) => {
Object.entries(row).forEach(([fieldId, value]) => {
const item = itemsMap[fieldId];
const formatted = formatItemValue(item, value);
console.log(`${fieldId}: ${formatted}`);
});
});
return formattedRows;
}import { itemsInMetricQuery, getItemMap, type MetricQuery } from "@lightdash/common";
function validateQuery(query: MetricQuery, explore: Explore): string[] {
// Get all items referenced in query
const allItemIds = itemsInMetricQuery(query);
// Build item map
const itemsMap = getItemMap(
explore,
query.additionalMetrics,
query.tableCalculations,
query.customDimensions
);
// Find missing items
const missingItems = allItemIds.filter((id) => !itemsMap[id]);
return missingItems;
}Period-over-period (PoP) comparison allows time-based analysis comparing current period metrics to previous periods.
/**
* Configuration for period-over-period comparison in metric queries
*/
interface PeriodOverPeriodComparison {
type: "previousPeriod";
granularity: TimeFrames;
periodOffset?: number;
field: {
name: string;
table: string;
};
}Properties:
name and table properties/**
* Valid granularities for period-over-period comparison
*/
const validPeriodOverPeriodGranularities: TimeFrames[] = [
TimeFrames.DAY,
TimeFrames.WEEK,
TimeFrames.MONTH,
TimeFrames.QUARTER,
TimeFrames.YEAR,
];
/**
* Check if a granularity is supported for period-over-period comparison
*/
function isSupportedPeriodOverPeriodGranularity(granularity: TimeFrames): boolean;Period-over-period creates additional columns with a _previous suffix:
/**
* Suffix used for period-over-period comparison columns
*/
const POP_PREVIOUS_PERIOD_SUFFIX = "_previous";
/**
* Gets the PoP field ID for a base metric field ID
* @param baseFieldId - The field ID of the base metric (e.g., "orders_total_revenue")
* @returns The PoP field ID (e.g., "orders_total_revenue_previous")
*/
function getPopFieldId(baseFieldId: string): string;
/**
* Gets the base field ID from a PoP field ID
* @param fieldId - The field ID to check
* @returns The base field ID if this is a PoP field, null otherwise
*/
function getBaseFieldIdFromPop(fieldId: string): string | null;Example:
import {
type MetricQuery,
type PeriodOverPeriodComparison,
TimeFrames,
getPopFieldId,
getBaseFieldIdFromPop,
} from "@lightdash/common";
// Create a metric query with period-over-period comparison
const query: MetricQuery = {
exploreName: "orders",
dimensions: ["orders_created_at"],
metrics: ["orders_total_revenue", "orders_order_count"],
filters: { dimensions: undefined, metrics: undefined },
sorts: [{ fieldId: "orders_created_at", descending: false }],
limit: 100,
tableCalculations: [],
periodOverPeriod: {
type: "previousPeriod",
granularity: TimeFrames.MONTH,
periodOffset: 1, // Compare to previous month
field: {
name: "created_at",
table: "orders",
},
},
};
// Result columns will include:
// - orders_total_revenue (current period)
// - orders_total_revenue_previous (previous period)
// - orders_order_count (current period)
// - orders_order_count_previous (previous period)
// Get PoP field IDs
const popRevenueId = getPopFieldId("orders_total_revenue");
// Returns: "orders_total_revenue_previous"
// Check if a field is a PoP field
const baseFieldId = getBaseFieldIdFromPop("orders_total_revenue_previous");
// Returns: "orders_total_revenue"
const notPopField = getBaseFieldIdFromPop("orders_total_revenue");
// Returns: null