01

PromQL compatibility

PulsusDB uses PromQL as its metrics query language rather than introducing a product-specific syntax. Teams adopting PulsusDB can use existing Prometheus-compatible clients, dashboards and operational queries without translating them into a separate language.

PromQL provides a concise vocabulary for metric data. An operator can select series by label, inspect a range of samples, calculate a rate, aggregate away dimensions and combine the result with another vector in a single expression. The same language is already embedded in Grafana® software dashboards, command-line tools, API clients and established operational procedures.

A metric sample itself is simple: a name, a set of labels, a timestamp and a value. The questions asked of those samples are not. During an investigation, someone might need the request rate for one service, the proportion of failures across the fleet and the 95th percentile latency by region. PromQL lets them describe those questions without knowing how the samples are partitioned, which database table holds them or how the backend intends to execute the work.

For that reason, PulsusDB exposes the standard Prometheus query API and treats PromQL behaviour as part of the compatibility contract. Supporting the syntax is not enough. Instant queries, range queries, label matching, counter resets, vector matching and function semantics all have to produce the answers that existing clients expect.

A familiar query remains a familiar queryPromQL / SQL
sum by (service) (
  rate(http_requests_total{environment="production"}[5m])
)
02

PromQL data types and query composition

PromQL expressions become much easier to understand when they are built one operation at a time. A selector first identifies the series that belong to the question. Adding a range selector provides the recent history for each of those series. A function can then interpret that history, and an aggregation can reduce many instance-level results into a service-level answer.

Functions accept particular PromQL data types. An instant vector contains one sample for each matching series at the evaluation time, while a range vector contains a window of samples for each series. The rate function needs that history to calculate the average increase per second, account for counter resets and compensate for imperfect alignment at the edges of the window.

The examples below use the same request metric from the initial selector through service-level error and latency calculations. A small number of composable operations replaces application code or several intermediate queries.

A selector produces an instant vector, while a range selector provides its history

The first expression returns the most recent sample for every matching label set. The second asks for the preceding five minutes of samples from those same counter series, which gives a range function enough history to work with.

# Most recent sample for each matching series
http_requests_total{
  environment="production",
  service="checkout"
}

# Five minutes of samples for each matching series
http_requests_total{
  environment="production",
  service="checkout"
}[5m]

The instant selector returns one current value per series, while the range selector returns a separate five-minute sample window for each series.

The rate function interprets the counter history

Passing the range vector to rate produces an average per-second request rate for each original series. PromQL handles resets caused by process restarts, so every dashboard does not need to implement that logic independently.

rate(
  http_requests_total{
    environment="production"
  }[5m]
)

The result is an instant vector containing one request-rate value for every original label set.

Aggregation and vector matching calculate an error ratio for each service

Each side first uses rate and sum by service to produce one vector per service. The division then matches the two sets on the service label, dividing the error rate by the total request rate without depending on instance, pod, method or status labels.

sum by (service) (
  rate(http_requests_total{status=~"5.."}[5m])
)
/
on (service)
sum by (service) (
  rate(http_requests_total[5m])
)

The result contains a five-minute error ratio for each service that exists on both sides of the operation.

A composed expression calculates service-level latency percentiles

Histogram buckets are counters, so rate is applied before aggregation. The sum keeps the service and bucket boundary labels, after which histogram_quantile estimates the 95th percentile from the combined bucket rates.

histogram_quantile(
  0.95,
  sum by (service, le) (
    rate(
      http_request_duration_seconds_bucket[10m]
    )
  )
)

The result contains one estimated 95th percentile request latency for each service.

03

Planning a PromQL query for the ClickHouse database

PromQL is designed to express time-series calculations, not to describe the physical layout of a database. PulsusDB sits between those two concerns. It parses the expression, resolves label matchers to series fingerprints and works out the time window needed by every selector branch before it asks the ClickHouse database to read anything.

A complex expression can contain several independent queries. The multi-window error-budget query below calculates an error ratio over six hours, repeats the same calculation over thirty minutes and keeps only the services that exceed the threshold in both windows. PulsusDB can plan each selector separately while preserving the vector operations that join the results.

The compiler then generates SQL for the work that can safely be performed in the database tier. Metric names, time bounds, resolved fingerprints and required columns become a tightly constrained read. Depending on the expression, aggregation work can also be pushed down. PromQL operations whose semantics need to remain in the query engine are evaluated by the stateless PulsusDB tier after the database has returned a bounded result.

A chart might represent a few hundred values even though its source ranges contain billions of samples. Shipping those samples to a separate service would make the network and the stateless query tier part of the scan. Pushing the read plan into the database keeps the large intermediate dataset beside the storage engine and returns only what the remaining PromQL evaluation needs.

One expression can compare sustained and recent error-budget consumption

The two halves are valid service-level error-ratio queries in their own right. The and operator matches the resulting service vectors so a brief spike does not look the same as a sustained problem.

(
  sum by (service) (rate(http_requests_total{status=~"5.."}[6h]))
  /
  sum by (service) (rate(http_requests_total[6h]))
) / 0.001 > 6
and on (service)
(
  sum by (service) (rate(http_requests_total{status=~"5.."}[30m]))
  /
  sum by (service) (rate(http_requests_total[30m]))
) / 0.001 > 6

The result contains only services whose long and short windows both exceed six times the configured error budget.

Representative SQL for one selector branchPromQL / SQL
SELECT fingerprint, unix_milli, value
FROM metric_samples
PREWHERE metric_name = 'http_requests_total'
WHERE unix_milli > {start - window}
  AND unix_milli <= {end}
  AND fingerprint IN (
    SELECT fingerprint
    FROM metric_series
    WHERE metric_name = 'http_requests_total'
      AND match(JSONExtractString(labels, 'status'), '^(?:5..)$')
  )
ORDER BY fingerprint, unix_milli
04

Metric storage and execution in ClickHouse

Metric workloads are a natural fit for a columnar analytical database because most queries read a small number of fields across a large number of rows. Once PulsusDB has resolved the relevant series, the ClickHouse® database can read the timestamp, value and identity columns without also reading unrelated data. Its vectorised execution engine processes those values in batches, while ordering keys and sparse indexes reduce the ranges that need to be examined in the first place.

PulsusDB models metric data around the way PromQL reads it. Series identity and label lookup are separated from the dense sample path, allowing matchers to be resolved before a sample scan begins. That turns a general label expression into a bounded set of fingerprints and gives the generated SQL enough information to prune by metric, time and series.

Compression affects query performance as well as storage cost. Regularly spaced timestamps are well suited to delta-of-delta encoding, while Gorilla encoding is designed for sequences of floating-point values. When those columns occupy fewer bytes, the database has less data to read from disk or object storage and less compressed data to move through memory during a query.

For larger installations, ClickHouse can parallelise reads across cores and distribute work across shards. Partial aggregation lets the database reduce data near each shard before merging the result. PulsusDB remains stateless throughout this process, so query API capacity can scale independently from the stateful database tier.

05

Raw samples and rollup selection

A five-minute chart and a ninety-day chart have different resolution requirements. The short query needs raw samples because each detail can affect the result. A long-range dashboard cannot display every original point, so reading all of them wastes storage bandwidth and CPU on resolution that disappears when the chart is rendered.

PulsusDB can route eligible range selectors to rollups maintained by ClickHouse materialised views. The planner considers the requested window, step and PromQL function before choosing a source. It can use a rollup for the historical portion of a query, splice in recent raw data and leave expressions on the raw path when a coarser source would change their meaning.

None of this requires the dashboard author to choose a different metric name or maintain separate queries for each retention tier. The PromQL expression remains the same while the planner selects raw samples, a rollup or a combination of both. This separation gives us room to improve the physical data model without turning every storage decision into a client migration.

PromQL is the client contract while PulsusDB and the ClickHouse database handle series resolution, physical source selection, sample reads and aggregation. The physical storage layout is not exposed through metric names or separate query endpoints.

END OF NOTEPROMQL.ENGINE / 001