discord9 2e7ed9cbcd feat(promql): execute an instant selection as a tree node (#9798)
* feat(promql): execute a bound native tree node by node, over sparse evaluated series

Ports the native runtime onto the sparse evaluated-series API: a chunk is
addressed by a `TimeAxis` that may hold several runs, so a consumer reads
positions through the axis instead of assuming `start + row`.

The driver and both node kinds move to that API:

- `chunk.range().len()` becomes `chunk.series.time.len()`, the compact row
  count, in every metric and byte estimate.
- `occupied_runs` walks `time.positions()` with one field cursor, so a chunk
  of several runs records each run where it belongs. A run open across a hole
  now ends where the hole begins; leaving it open to the last row made two
  runs look like one and refused a series that occupies the hole.
- A window keeps the pieces its evaluator decided in a queue: one push can
  decide several runs, and reporting only what the push returned stranded the
  rest until the next push. Queued pieces are charged by `retained_bytes`, and
  the charge is recomputed when one leaves, so a released piece is not held.
- `map_chunk` answers `Option`: an operation that empties every position
  (`clamp` with `max < min`) yields no chunk at all, unless the chunk closes
  its series -- a closing chunk holding no row is handed over so a consumer
  can tell "nothing more here" from "no more input".

`RangeEvaluator::retained_bytes` is restored with the three byte helpers it
reads: a field's buffers, a series' identity and the pending batch's vectors,
all by capacity, and still counted as storage rather than as a peak over a
push.

Tests read absolute positions from the axis (`decided` no longer adds a row
number to a start), and two sparse cases cover what the old reading got wrong:
a `Window -> Map -> Window` chain over a chunk of two runs, and a series that
occupies the hole between another's runs.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* refactor(promql): drop the window queue the sparse evaluator never needed

`RangeEvaluator::push` and `push_series` each return `Result<Option<EvaluatedChunk>>`: the
evaluator gathers every run it decided into one chunk and hands back at most one. So
`pending.extend(decided)` inserted at most one chunk and `take_pending` removed it on the spot,
leaving the queue empty after every successful push.

That makes the queue dead weight: a `VecDeque` allocation per productive window, an accounting
term that is always zero, a second drain site in `pull`, and doc comments claiming a problem the
API does not have. The driver's "ask the input again when a window decides nothing" loop was
already the whole story.

Back to returning the evaluator's own answer, and drop `pending`, `take_pending` and
`take_window_output`.

Covers the empty-map protocol this port introduces while it is here: an operation that empties
every position hands on nothing while its series is open, and one empty chunk when it closes. A
non-closing empty chunk would be rejected downstream and a dropped closing one would leave the
series unclosed.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* test(promql): pin the empty-map output count at one

The test checked that every output was empty and that exactly one closed, which a stray extra
non-closing empty output would also satisfy.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): charge the memory the native evaluator actually retains

Four omissions made the reservation blind to most of what a window over native
histograms keeps, and the existing incremental-equals-recomputed test could not see
any of them because both sides used the same formula.

- `FloatHistogram` owns four heap allocations -- both bucket lists, the custom
  bounds, and the span layout -- that `FieldSamples::bytes` never looked at. A
  retained 4096-bucket sample is tens of kilobytes against a ledger that saw a
  struct header. `heap_bytes` counts them, and both the retained sample slice and
  the histograms a row boxes are charged for what they own.
- Queued chunks were estimated as one float per field per row, which is not what a
  histogram field holds: it holds indices into a shared `Arc<HistogramBatch>` and
  the buffers behind the Arrow arrays. The figure now walks the samples, and counts
  a shared table or a shared buffer slice once per pass rather than once per chunk
  that references it.
- `history_bytes` included `size_of::<History>()` while the map slot term charged a
  `History` per entry as well, so every entry paid two headers. The per-entry figure
  now covers only the heap the entry owns.
- The primary key is charged for the allocation its `Buffer` keeps alive rather than
  the bytes it shows: a scan slices the key out of a decoded dictionary-values
  buffer, so a short key can pin a batch-sized allocation.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): keep the source queue's charge incremental

The queue's reservation was recounted by walking every queued chunk on each
enqueue and each pop. `push_raw` and `push_evaluated` are public and put no bound
on how deep the queue gets, so a caller that queues a whole series before pulling it
paid `N(N+1)/2` chunk visits to hold `N` chunks -- the same shape of defect this
branch fixes in `absorb`.

`QueuedCharge` keeps a reference count and the bytes behind each shared allocation:
a table or buffer is charged when the first queued chunk reaches it and given back
when the last one leaves, so an operation walks the one chunk it names. The count is
per chunk rather than per field, because a pop re-walks the same chunk with the same
dedupe and must remove exactly the references that were added.

A chunk's own struct bytes are counted per chunk (`own_bytes`); only the allocations
behind its fields are counted by address, since a struct has no address to dedupe on.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* perf(promql): append a forward-moving chunk to the emitted history

`absorb` rebuilt the whole stored run array on every non-empty chunk. Legal input
made that quadratic across chunks with no label collision: one series in `K` chunks,
chunk `i` occupying only grid position `2*i`, stores one more run per chunk and moves
`K(K+1)/2` of them -- about five billion for a hundred thousand rows.

A chunk that starts at or past the end of the last stored run only ever adds to the
end, so its runs are appended and the boundary run is joined where it touches. One
that can interleave or reach backwards still takes the full one-pass merge, which can
split a run. Both leave the runs sorted, disjoint and joined, which is what
`Emitted::occupy` looks a position up by.

The test records the chunks through `Emitted::occupy` itself and reads the history
back from it, covering the forward case, the touching boundary, a hole that has to
join both neighbours, and a position before everything stored.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): report a truncated input from a tree with no window

`close` only asked windows whether a series was still open, so a tree of an
input alone, or of a map over one, accepted a final chunk without `is_last` and
handed the consumer a series that never closed. The input node now follows the
series of the chunk it last handed over, and closing such a tree reports the
truncation against it, which is the same refusal a window makes for the input it
reads.

`occupied_runs` read a row the axis names with `expect`, which panics on a
`Series` whose fields are shorter than its axis; the fields are public, so that
chunk is constructible by a caller. It now returns the misalignment as an error
naming the node, the field and both lengths.

The chunk protocol says one series closes once, which reads as one close per
label set. State that it is per input series: a node that rewrites labels can
hand two series over under one label set and both of them close.

Also say in `Emitted`'s doc that the table is one node's own, so a label set
costs its bytes again in every node that keeps one.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* feat(promql): execute an instant selection as a tree node

The tree could not run `NativeExpr::Instant`: the state builder recorded
`not_implemented` and refused the whole tree before reading a chunk, while the
compiler already produced the node for a bare selector. A query the plan and the
compiler both accept therefore had no runtime path at all.

`InstantRuntime` buffers one series to its `is_last` chunk -- the selection kernel
reads a complete axis -- then calls the shared `select_instants` once and gathers the
rows it picked onto the binding's own grid. Positions come from the grid lattice the
kernel was asked for, and a timestamp off that lattice is an error rather than a
skipped row. A series that selects nothing still closes, with an empty chunk on the
grid; an input that ends inside a series is reported on `finish()`.

The node does not record label-set occupancy: it keeps the metric verbatim, so it
cannot map two series onto one label set, and the occupancy key drops empty-valued
tags -- two genuinely distinct series would be refused as a collision.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): refuse an interleaved input, and close a label set only for its owner

Two series-level identity holes, both reported against `42ea525`.

The truncation flag followed the last chunk handed over, which is the same as
"is a series open" only while at most one series can be open. A tree with no
window still accepted `Interleaved`, and `a` open, `b` closed, end of input then
passed while `a` never closed. Tracking the open set would need an identity that
a raw chunk in bound-label mode does not have (`metric` is `None`, the key holds
it), so the ordering is refused when the state is built instead, for every tree
shape and not only under a window. The flag's doc now says what it means and why
that is the whole answer.

`History` closed on any series' `is_last`, so with `abs` dropping the metric name
a series that contributed nothing could close a label set another series was
still filling: `a{job="x"}` value, `b{job="x"}` empty close, `a{job="x"}`
continuation was refused with "already closed the label set". Contiguous input
hands over exactly that sequence, so it was not an interleaving artefact. A
history now remembers the series that filled it and closes only for that series'
closing chunk; a series that produced nothing under an open set is passed over,
as it already was under a closed one. The owner is charged as part of the entry.

The ordering check also reads every input node rather than only the first: a tree
with two inputs would otherwise have its second declaration read and dropped.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): reserve the instant gather's workspace and keep the histogram start

Two findings from the review of the instant node.

The gather allocates after the last chunk arrives: the concatenated axis, the
descriptors that read it, the two arrays the selection returns, and the rows the
builder keeps. Nothing asked the pool for any of it, so with one sample selected
by every point of a million-point grid the node took tens of megabytes while its
reservation fell to the buffered chunks' size -- nothing at all, on the chunk
that ends the series. `gather_bytes` now derives a bound from the values in hand
(rows, chunks, segments, fields, points, and the widest histogram row per field,
since one row can be selected by every point), and the reservation grows by it
before the gather runs. It shrinks back on both the success and the error path,
so the workspace never outlives the call.

A selection reads an existing sample, so it has to carry that sample's payload.
`FloatHistogram` had no `start_timestamp` and `push_to` wrote `None`, which lost
it for any histogram that had one: `detect_start_timestamp_reset` reads that
field to spot a counter reset, so a window over this output would mis-detect
resets. The field is now carried through a view and written back; the kernels
that compute a histogram instead of reading one leave it `None`, which is what
they produced before.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* docs(promql): describe the gather test by what this branch does

The doc on `an_oversized_selection_is_refused_by_the_pool` said the kernel has
no point limit any more. It does on this branch: the limit is removed on its own
branch, which this one does not carry, so the comment described a state that has
not arrived.

What is true here is the order: the estimate now applies to every gather, so the
pool is asked before the kernel is, and the pool's refusal is the one the caller
sees. That is worth saying on its own -- a bound in bytes on the path that
decides the points, rather than a count that says nothing about what they cost --
and it stays true after the limit goes.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* refactor(promql): simplify native execution memory estimates

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): merge rewritten logical series before window evaluation

Accept disjoint closed-source fragments under one output label set. Retain occupancy checks and merge affected window inputs once at EOF using shared typed sample segments. Keep direct input windows and root maps streaming, and release collected fragments on finish or error.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* docs(promql): clarify approximate merge workspace estimate

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): validate series boundaries before collected input merges

Keep each collecting window's open metric and last position so EOF grouping cannot hide reversed runs or identity changes before closing. Preserve source and root-map piece semantics, and test malformed input release, empty closing chunks, and EOF truncation.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* test(promql): supply the default binary fill in the unimplemented-node fixture

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): reserve an instant selection for the points it decides

The gather reserved the whole planned grid, while select_instants clips the range to the series' first and last sample and its lookback: a week-long query at a one-minute step reserved ~504 KB for a series that produces five points, and a small pool refused it before gathering. Reserve for the clipped candidate count, derived with the same alignment the kernel uses.

Also pin the start-timestamp semantics the histogram change spread to: last_over_time and first_over_time hand back the sample they selected with its start timestamp, and limitk and limit_ratio keep it on the sample they keep, so a reader downstream can still place a counter reset.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* test(promql): move the histogram_quantile doc back to its own test

The start-timestamp test was inserted into the middle of another test's doc comment, leaving the earlier test without one.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* test(promql): refuse an interleaved input under a selection when the tree is bound

The merged plan validates an input ordering in the arm that reads it, so a
selection over an interleaved input no longer binds at all. Assert that refusal
where it happens instead of asserting a runtime rejection the tree cannot reach,
and keep the map-over-a-selection case: a map above a selection does not shield
the check, because the check looks at the input the selection itself reads.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* docs(promql): say what the start timestamp pins, and cover every path that carries it

The field is GreptimeDB sample metadata, so the comments now say that instead of
arguing from what Prometheus omits: a FloatHistogram there has no such field, while
its rate and reset evaluation takes start timestamps through a separate channel, and
this crate's native range kernel does not read the field at all.

Comparison filtering and set selection were the two paths that carry the timestamp
without a test. Both keep the payload of the operand they hand back, including the
right side of an or at a point the left side does not cover, and a bool comparison
answers with a float so no histogram reaches the output.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(promql): drop the start timestamp a computed histogram never had

A window holding one histogram became the result as it arrived, while two or more
went through an add that clears the timestamp, so whether a summed histogram carried
the payload said how many samples the window held rather than what the function did.
Clear it where the fold returns, leaving the bucket layout to the caller.

Also correct two comments that described behaviour this branch does not have: the
selection kernel carries no point limit of its own, and the interleaved map sits
above the selection rather than between it and the input.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

---------

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-21 22:44:38 +08:00
2023-08-10 08:08:37 +00:00
2023-06-25 11:05:46 +08:00
2023-11-09 10:38:12 +00:00

GreptimeDB Logo

Metrics, logs, and traces.
One engine, on your infrastructure.

A columnar database for metrics, logs, and traces on object storage. Apache-2.0 licensed core.

User Guide  ·  API Docs  ·  Roadmap 2026  ·  Slack

Stable Canary Nightly Docker Pulls License

stable for production  ·  canary includes pre-releases  ·  nightly is a weekly snapshot of main

Introduction

GreptimeDB is an open-source observability database. Metrics, logs, and traces run on one columnar engine over object storage and share one table model: tags, timestamp, and fields. When signals carry common identifiers such as service, host, or trace ID, you can correlate them in SQL without moving data between databases.

Ingest through OpenTelemetry, Prometheus Remote Write, Loki Push, or Elasticsearch Bulk. Use SQL across observability data and PromQL for metrics. Migrate ingestion one signal at a time without rebuilding your collectors.

One Query Across Signals

OpenTelemetry ingestion writes spans to opentelemetry_traces and log records to opentelemetry_logs. Both tables carry trace_id, so correlating them is a join:

-- The slowest failed spans in the last hour,
-- with the log lines emitted inside those same traces.
SELECT
    t.service_name,
    t.span_name,
    t.duration_nano / 1000000 AS duration_ms,
    l.timestamp AS log_time,
    l.severity_text,
    l.body
FROM opentelemetry_traces t
JOIN opentelemetry_logs l ON l.trace_id = t.trace_id
WHERE t.timestamp > now() - INTERVAL '1' HOUR
  AND t.span_status_code = 'STATUS_CODE_ERROR'
ORDER BY t.duration_nano DESC
LIMIT 20;

Metrics join the same way, on any tag the tables share, such as service, host, or pod.

Why You Might Use It

  • You run Prometheus plus Loki or Elasticsearch and want one backend instead of three
  • You have outgrown Prometheus on cardinality or retention and don't want the Thanos/Mimir operational surface
  • You are hitting Loki's query performance limits as log volume grows
  • You need long retention on object storage without a separate analytics stack
  • You want to query telemetry with SQL, not only a domain query language
  • You are storing GenAI or agent telemetry (OTel GenAI conventions) alongside infrastructure signals

Learn more in Why GreptimeDB.

What's Supported

Ingest OpenTelemetry (OTLP), Prometheus Remote Write, Loki Push, Elasticsearch Bulk, InfluxDB line protocol, gRPC
Query SQL, PromQL, Jaeger-compatible trace queries, MySQL and PostgreSQL wire protocols
Storage S3, GCS, Azure Blob and S3-compatible endpoints as primary storage, with memory and local-disk caches
Built in Retention policies, downsampling, continuous aggregation, explicit table partitioning, and inverted / skipping / fulltext indexes

Compute and storage are disaggregated: object storage holds the data, while memory and local-disk caches keep recent and frequently queried data close to compute.

GreptimeDB Overview

Benchmarks

Compatibility and Migration

Compatibility is per protocol, and query-side coverage is narrower than ingestion.

Compatible Not compatible
Prometheus Remote Write ingestion; PromQL queries Gaps are listed in PromQL compatibility
Loki Push ingestion; dual-write through Grafana Alloy makes the cutover gradual LogQL and the rest of the Loki query API
Elasticsearch _bulk ingestion in the open-source core; QueryDSL partially, in Enterprise Most other Elasticsearch APIs

Limitations and Edition Boundary

Cluster deployment, object storage, the Flow engine, and every ingestion protocol listed above are in the Apache-2.0 build. Repartitioning, region migration, and index creation are manual operations there.

Read replicas, workload isolation, and automated repartitioning are GreptimeDB Enterprise features, along with enterprise security and governance. The Enterprise overview has the current list, and pricing has the edition comparison.

Architecture

GreptimeDB can run in two modes:

  • Standalone — single binary for development and small deployments.
  • Distributed — four components, each independently scalable:
    • Frontend — protocol entry (OTel, Prometheus, MySQL/PostgreSQL, gRPC, ingestion APIs for Elasticsearch/InfluxDB/Loki) and the distributed query engine. Stateless, scales horizontally.
    • Datanode — region engine with WAL, memtable, SST, cache, compaction, and indexes. Persists data to object storage. Elastic.
    • Metasrv — metadata, routing, repartitioning, and security. Backed by a pluggable KV layer (etcd or RDS).
    • Flownode (optional) — continuous flow computation (streaming and materialized views).

For deeper coverage, see the architecture doc or DeepWiki.

GreptimeDB System Overview

Try GreptimeDB

For AI agents — paste this prompt into your agent:

Read https://docs.greptime.com/SKILL.md and follow the instructions
to deploy, configure, ingest, and query GreptimeDB.
docker run -p 127.0.0.1:4000-4003:4000-4003 \
  -v "$(pwd)/greptimedb_data:/greptimedb_data" \
  --name greptime --rm \
  greptime/greptimedb:latest standalone start \
  --http-addr 0.0.0.0:4000 \
  --grpc-bind-addr 0.0.0.0:4001 \
  --mysql-addr 0.0.0.0:4002 \
  --postgres-addr 0.0.0.0:4003

Dashboard: http://localhost:4000/dashboard

Read more in the full Install Guide.

Troubleshooting:

  • Cannot connect to the database? Ensure that ports 4000, 4001, 4002, and 4003 are not blocked by a firewall or used by other services.
  • Failed to start? Check the container logs with docker logs greptime for further details.

Getting Started

Build From Source

Prerequisites:

  • Rust toolchain — stable, pinned by rust-toolchain.toml
  • Protobuf compiler (>= 3.15)
  • C/C++ building essentials: gcc / g++ / autoconf and the glibc dev package (libc6-dev on Ubuntu, glibc-devel on Fedora)
  • Python toolchain (optional, only for some test scripts)

Build and run:

make                          # build greptime binary
cargo run -- standalone start # start in standalone mode

Common dev commands:

make fmt            # format Rust code
make clippy         # lint (fails on warnings)
make test           # unit + integration tests (uses cargo-nextest)
make sqlness-test   # SQL regression tests

See the Contribution Guidelines for the full developer workflow.

Tools & Extensions

Project Status

GreptimeDB is generally available, with stable APIs and regular releases. It runs in production at scale — OceanBase Cloud operates 80+ GreptimeDB clusters managing 300 TB of logs, cutting log storage cost by 60%+ after migrating from Grafana Loki. See more in case studies.

Release lines and support windows are in the version reference. For where the project is going, read the v1.0 highlights and the 2026 roadmap.

Community

We invite you to engage and contribute!

If GreptimeDB is useful to you, please star the repo.

Known Users

License

GreptimeDB is an open-core project. Its core is licensed under the Apache License 2.0.

A small set of peripheral, enterprise-only features are gated behind the enterprise Cargo feature (not built by default) and are governed by the separate GreptimeDB Enterprise License. Source files under that license carry an explicit Enterprise License header.

Commercial Support

Scaling observability on your infrastructure? GreptimeDB Enterprise adds the operational, security, and support layer for production deployments. Contact us for details.

Contributing

Integration CI Codecov

Acknowledgement

Special thanks to all contributors! See AUTHOR.md.


All trademarks, logos, and brand names referenced in this README and in the Overview diagram are the property of their respective owners. Their use is for identification purposes only and does not imply endorsement or affiliation.

S
Description
Open-source, cloud-native, unified observability database for metrics, logs and traces, supporting SQL/PromQL/Streaming.
Readme Apache-2.0
1.9 GiB
Languages
Rust 97.4%
Python 1.9%
Shell 0.4%
JavaScript 0.2%