Skip to content

Time Dimensions

Time dimensions are specialized dimensions that represent dates, timestamps, and time-based data in your analytics cubes. They enable powerful time-series analysis, trend visualization, and date-based filtering with granular control over time periods and date ranges.

Time dimensions in Drizzle Cube provide:

  • Granular Time Analysis: Break down data by year, quarter, month, week, day, or hour
  • Flexible Date Ranges: Support for relative dates (“last 30 days”) and absolute ranges
  • Continuous Time Series: Automatic gap filling ensures charts display without discontinuities
  • Type-Safe Queries: Full TypeScript support with Drizzle schema integration
  • Time Zone Handling: Consistent UTC-based calculations
  • Performance Optimization: Efficient query generation for time-based filters
dimensions: {
createdAt: {
name: "createdAt",
title: "Created Date",
type: "time",
sql: employees.createdAt
},
date: {
name: "date",
title: "Activity Date",
type: "time",
sql: productivity.date
}
}

Time dimensions are used in the timeDimensions array of queries, not in the regular dimensions array:

const query = {
measures: ["Employees.count"],
dimensions: ["Employees.departmentName"],
timeDimensions: [{
dimension: "Employees.createdAt",
granularity: "month",
dateRange: ["2023-01-01", "2023-12-31"]
}]
}

Time dimensions support various granularity levels for grouping data:

GranularityDescriptionFormat ExampleUse Case
yearAnnual grouping2023Year-over-year trends
quarterQuarterly grouping2023-Q1Quarterly reports
monthMonthly grouping2023-01Monthly analysis
weekWeekly grouping2023-W01Weekly trends
dayDaily grouping2023-01-15Daily metrics
hourHourly grouping2023-01-15 14:00Hourly patterns
// Monthly sales trends
const monthlyTrends = {
measures: ["Sales.totalRevenue"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "month",
dateRange: "this year"
}]
}
// Daily activity for the last 30 days
const dailyActivity = {
measures: ["Users.activeCount"],
timeDimensions: [{
dimension: "Users.lastLoginAt",
granularity: "day",
dateRange: "last 30 days"
}]
}
// Hourly patterns for today
const hourlyPatterns = {
measures: ["Orders.count"],
timeDimensions: [{
dimension: "Orders.createdAt",
granularity: "hour",
dateRange: "today"
}]
}

Time dimensions support flexible date range filtering with both relative and absolute date ranges.

Relative date ranges automatically calculate based on the current date. See the Complete Date Range Reference table below for all supported options.

Specify exact start and end dates using arrays:

// Specific date range
dateRange: ["2023-01-01", "2023-12-31"]
// Single date (same start and end)
dateRange: ["2023-06-15", "2023-06-15"]
// Partial dates (will be parsed appropriately)
dateRange: ["2023-01", "2023-12"] // January to December 2023
// Relative date range examples
const examples = [
{
title: "Today"s Activity",
query: {
measures: ["Orders.count"],
timeDimensions: [{
dimension: "Orders.createdAt",
granularity: "hour",
dateRange: "today"
}]
}
},
{
title: "This Month"s Sales",
query: {
measures: ["Sales.totalRevenue"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "day",
dateRange: "this month"
}]
}
},
{
title: "Last 90 Days Trend",
query: {
measures: ["Users.signupCount"],
timeDimensions: [{
dimension: "Users.createdAt",
granularity: "week",
dateRange: "last 90 days"
}]
}
},
{
title: "Quarterly Comparison",
query: {
measures: ["Revenue.total"],
timeDimensions: [{
dimension: "Revenue.date",
granularity: "quarter",
dateRange: "last 4 quarters"
}]
}
},
{
title: "Specific Campaign Period",
query: {
measures: ["Campaigns.conversions"],
timeDimensions: [{
dimension: "Campaigns.startDate",
granularity: "day",
dateRange: ["2023-06-01", "2023-06-30"]
}]
}
}
]
Range StringDescriptionCalculation
todayCurrent day00:00:00 to 23:59:59 today
yesterdayPrevious day00:00:00 to 23:59:59 yesterday
this weekCurrent weekMonday 00:00 to Sunday 23:59
this monthCurrent month1st 00:00 to last day 23:59
this quarterCurrent quarterQuarter start to quarter end
this yearCurrent yearJan 1st 00:00 to Dec 31st 23:59
last weekPrevious weekPrevious Monday to Sunday
last monthPrevious monthPrevious month start to end
last quarterPrevious quarterPrevious quarter start to end
last yearPrevious yearPrevious Jan 1st to Dec 31st
Range StringDescriptionCalculation
last 7 daysLast 7 days7 days ago 00:00 to now
last 30 daysLast 30 days30 days ago 00:00 to now
last 90 daysLast 90 days90 days ago 00:00 to now
last 12 monthsLast 12 months12 months ago to now
PatternExampleDescription
last N dayslast 14 daysLast N days including today
last N weekslast 4 weeksLast N weeks rolling
last N monthslast 6 monthsLast N months rolling
last N quarterslast 2 quartersLast N quarters rolling
last N yearslast 3 yearsLast N years rolling

You can include multiple time dimensions in a single query:

const complexTimeQuery = {
measures: ["Orders.count", "Orders.totalValue"],
dimensions: ["Orders.status"],
timeDimensions: [
{
dimension: "Orders.createdAt",
granularity: "week",
dateRange: "last 12 weeks"
},
{
dimension: "Orders.shippedAt",
granularity: "day",
dateRange: "last 30 days"
}
]
}

When displaying time series data in charts, gaps in the data (dates with no records) can make visualizations look discontinuous. Drizzle Cube automatically fills these gaps by default, ensuring your charts show continuous time series.

Gap filling is enabled by default for time dimensions with both granularity and dateRange specified:

const query = {
measures: ["Sales.revenue"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "day",
dateRange: ["2024-01-01", "2024-01-07"]
// fillMissingDates defaults to true
}]
}
// Returns 7 rows - one for each day
// Days without data have revenue: 0

Set fillMissingDates: false to return only rows where data exists:

const query = {
measures: ["Sales.revenue"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "day",
dateRange: ["2024-01-01", "2024-01-07"],
fillMissingDates: false // Only return days with actual data
}]
}
// Returns only days that have sales records

By default, gaps are filled with 0 for all measures. Use fillMissingDatesValue to specify a different fill value:

const query = {
measures: ["Sales.revenue", "Sales.count"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "day",
dateRange: ["2024-01-01", "2024-01-07"]
}],
fillMissingDatesValue: null // Fill gaps with null instead of 0
}

When your query includes regular dimensions, gaps are filled within each dimension group:

const query = {
measures: ["Sales.revenue"],
dimensions: ["Sales.region"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "day",
dateRange: ["2024-01-01", "2024-01-03"]
}]
}
// Input data (from database):
// { date: "2024-01-01", region: "US", revenue: 100 }
// { date: "2024-01-03", region: "US", revenue: 150 }
// { date: "2024-01-01", region: "EU", revenue: 80 }
// Output (with gap filling):
// { date: "2024-01-01", region: "US", revenue: 100 }
// { date: "2024-01-02", region: "US", revenue: 0 } // Filled
// { date: "2024-01-03", region: "US", revenue: 150 }
// { date: "2024-01-01", region: "EU", revenue: 80 }
// { date: "2024-01-02", region: "EU", revenue: 0 } // Filled
// { date: "2024-01-03", region: "EU", revenue: 0 } // Filled

Gap filling is automatically skipped when:

  • No granularity is specified (raw timestamp queries)
  • No dateRange is specified (unbounded queries)
  • fillMissingDates: false is explicitly set
// No gap filling - missing granularity
const rawQuery = {
measures: ["Sales.revenue"],
timeDimensions: [{
dimension: "Sales.date",
dateRange: ["2024-01-01", "2024-01-07"]
// No granularity - gaps not filled
}]
}
// No gap filling - missing dateRange
const unboundedQuery = {
measures: ["Sales.revenue"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "day"
// No dateRange - cannot determine bounds
}]
}

Gap filling works with all time granularities:

GranularityGap Interval
year1 year
quarter3 months
month1 month
week7 days (aligned to Monday)
day1 day
hour1 hour
minute1 minute
second1 second
  1. Enable for charts: Keep gap filling enabled (default) for line charts and area charts
  2. Disable for tables: Consider disabling for data tables where you only want actual records
  3. Use null for averages: Set fillMissingDatesValue: null when computing averages to avoid skewing results with zeros
  4. Mind the date range: Large date ranges with fine granularities generate many rows

Time dimensions can also be used in filters for more complex queries:

const filteredQuery = {
measures: ["Sales.revenue"],
dimensions: ["Products.category"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "month",
dateRange: "this year"
}],
filters: [
{
member: "Sales.createdAt",
operator: "inDateRange",
values: ["2023-Q2"] // Second quarter only
}
]
}
const quarterlyBusiness = {
measures: ["Revenue.total", "Revenue.growth"],
dimensions: ["Revenue.region"],
timeDimensions: [{
dimension: "Revenue.date",
granularity: "quarter",
dateRange: "last 8 quarters" // 2 years of quarters
}]
}
const seasonalAnalysis = {
measures: ["Sales.volume"],
dimensions: ["Products.category"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "month",
dateRange: ["2022-12-01", "2024-02-28"] // Winter seasons
}]
}
const rollingAnalysis = {
measures: ["Users.retention"],
timeDimensions: [{
dimension: "Users.firstLoginAt",
granularity: "month",
dateRange: "last 12 months"
}]
}

Drizzle Cube handles time zones consistently:

  • Server Processing: All calculations use UTC internally
  • Database Queries: Times are normalized to UTC before comparison
  • Client Display: Format times according to user”s timezone in the frontend
  • Date Boundaries: Day, week, month boundaries calculated in UTC
// Time dimension definition with timezone awareness
dimensions: {
orderTime: {
name: "orderTime",
title: "Order Time",
type: "time",
sql: orders.createdAt, // Stored in UTC in database
description: "Order creation time (UTC)"
}
}

Ensure time dimension columns are indexed in your database:

-- PostgreSQL example
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_employees_hire_date ON employees(created_at);
// Good: Use appropriate granularity
const efficientQuery = {
measures: ["Sales.revenue"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "month", // Don"t use "day" for yearly analysis
dateRange: "this year"
}]
}
// Avoid: Over-granular queries for large time ranges
const inefficientQuery = {
measures: ["Sales.revenue"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "hour", // Too granular for 2-year analysis
dateRange: "last 2 years"
}]
}
// Employee hire dates
createdAt: {
name: "createdAt",
title: "Hire Date",
type: "time",
sql: employees.createdAt
}
// Activity dates
activityDate: {
name: "activityDate",
title: "Activity Date",
type: "time",
sql: activities.date
}
// Update timestamps
updatedAt: {
name: "updatedAt",
title: "Last Modified",
type: "time",
sql: records.updatedAt
}
// Business day flag
businessDay: {
name: "businessDay",
title: "Business Day",
type: "boolean",
sql: sql`EXTRACT(DOW FROM ${activities.date}) BETWEEN 1 AND 5`
}
// Time period categories
timePeriod: {
name: "timePeriod",
title: "Time Period",
type: "string",
sql: sql`
CASE
WHEN ${activities.date} >= CURRENT_DATE - INTERVAL "7 days" THEN "This Week"
WHEN ${activities.date} >= CURRENT_DATE - INTERVAL "30 days" THEN "This Month"
WHEN ${activities.date} >= CURRENT_DATE - INTERVAL "90 days" THEN "This Quarter"
ELSE "Earlier"
END
`
}
import { describe, it, expect } from "vitest"
describe("Time Dimensions", () => {
it("should handle relative date ranges", async () => {
const query = {
measures: ["Sales.count"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "day",
dateRange: "last 7 days"
}]
}
const result = await semanticLayer.load(query, {
organisationId: "test-org"
})
const data = result.rawData()
expect(data).toHaveLength(7) // Should have 7 days
expect(data[0]["Sales.date"]).toBeDefined()
})
it("should handle absolute date ranges", async () => {
const query = {
measures: ["Users.signupCount"],
timeDimensions: [{
dimension: "Users.createdAt",
granularity: "month",
dateRange: ["2023-01-01", "2023-12-31"]
}]
}
const result = await semanticLayer.load(query, {
organisationId: "test-org"
})
const data = result.rawData()
expect(data).toHaveLength(12) // 12 months
expect(data[0]["Users.createdAt"]).toMatch(/2023-/)
})
it("should handle multiple granularities", async () => {
const weeklyQuery = {
measures: ["Orders.count"],
timeDimensions: [{
dimension: "Orders.createdAt",
granularity: "week",
dateRange: "last 4 weeks"
}]
}
const monthlyQuery = {
measures: ["Orders.count"],
timeDimensions: [{
dimension: "Orders.createdAt",
granularity: "month",
dateRange: "last 4 months"
}]
}
const weeklyResult = await semanticLayer.load(weeklyQuery, { organisationId: "test" })
const monthlyResult = await semanticLayer.load(monthlyQuery, { organisationId: "test" })
expect(weeklyResult.rawData()).toHaveLength(4)
expect(monthlyResult.rawData()).toHaveLength(4)
})
})
  1. Use Appropriate Granularity: Match granularity to your analysis needs
  2. Index Time Columns: Ensure database performance with proper indexes
  3. Relative Ranges: Prefer relative ranges for dynamic dashboards
  4. UTC Consistency: Store all times in UTC in your database
  5. Boundary Awareness: Understand how date boundaries are calculated
  6. Performance Testing: Test queries with large date ranges
  7. Business Logic: Use computed dimensions for business-specific time logic
// Dynamic dashboard with user-selectable time ranges
const dashboardQuery = (timeRange: string) => ({
measures: ["Metrics.value"],
dimensions: ["Metrics.category"],
timeDimensions: [{
dimension: "Metrics.date",
granularity: "day",
dateRange: timeRange // "last 30 days", "this month", etc.
}]
})
// Year-over-year comparison
const yearOverYear = {
measures: ["Sales.revenue"],
timeDimensions: [{
dimension: "Sales.date",
granularity: "month",
dateRange: "last 24 months"
}]
}
// User cohorts by signup month
const userCohorts = {
measures: ["Users.count", "Users.retainedCount"],
dimensions: ["Users.cohortMonth"],
timeDimensions: [{
dimension: "Users.createdAt",
granularity: "month",
dateRange: "last 12 months"
}]
}