Semantic Routing

What you configure so the query planner picks the right table and datasource.

Overview

When a query requests measures and dimensions, the planner chooses which table (and thus datasource) to use. You don’t hard-code table names, you set tier, cost, and partitions in YAML, and the planner uses those to route queries. This page is about what each setting means and where to set it.

Rule you must respect: a spec is planned against exactly one datasource. 0sql never blends across datasources, so everything a spec asks for must be servable from a single one. When the same measures and dimensions are defined in more than one datasource, routing decides which one the SQL is written for; the response names it in datasource and adapter.

What You Configure

Datasource Tier

Where: In your datasource config (e.g. datasources.yml).
What it means: When more than one datasource can answer the query, the planner prefers the one with the lower tier number: hot over warm over cold.

TierTypical use
hotPrimary warehouse, fast queries
warmRead replicas, moderate speed
coldArchives, data lakes, slow
# datasources.yml
primary:
  adapter: snowflake
  tier: hot

replica:
  adapter: postgres
  tier: warm

archive:
  adapter: athena
  tier: cold

Example: If the same data exists in hot (last 3 months) and cold (full history), a query for “last 30 days” will use the hot datasource; a query for “year 2020” will use cold.

Table Cost

Where: In each table’s YAML (cost on the table).
What it means: When several tables can answer the same query, the planner prefers lower cost. Use cost to prefer pre-aggregated or summary tables over detail tables.

# Fact / detail table: higher cost
name: Store Sales
cost: 100

# Pre-aggregated table: lower cost, preferred when it can answer the query
name: Store Sales Daily
cost: 20

When two tables define the same measure (e.g. “Store Sales Price”), the one-name-one-concept rule makes them one measure with two sources. The planner first keeps the tables whose dimensions cover the query, then prefers the lower-cost one, so cost is how you break ties.

Partition Constraints

Where: In the table’s YAML under partitions (a list; the singular partition key is also accepted).
What it means: This table holds data for a specific range (e.g. “Sale Date ≥ 2024-01-01”). The planner prefers tables whose partitions match the spec’s filters and avoids tables whose partitions exclude them. The predicates that rank a table are between, greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to (number or date dimensions) and in_list (any dimension). See Partitions for full syntax.

name: Recent Sales
partitions:
  - dimension: Sale Date
    predicate: greater_than_or_equal_to
    filter_value: 2024-01-01

Dimension and Measure Coverage

The chosen table’s universe must contain paths to all requested dimensions and the requested measures. You don’t configure this per-query, you design your semantic model (joins, cardinality) so that the right universe exists. If you see errors like “dimension not in universe,” fix joins or cardinality so that one universe can reach all requested fields. See Universe Formation.

Best Practices

  1. Use meaningful costs: Pre-aggregated tables should have lower costs
  2. Configure tiers correctly: Hot for fast, cold for archives
  3. Add partitions: Help planner route time-filtered queries
  4. Test routing: zsql explain --expr "..." prints the datasource and the table route the planner chose for a spec
  5. Document tier boundaries: Know what data each tier contains

Next Steps