discord9 4a4afeef07 feat(promql): execute a bound native tree over sparse evaluated series (#9777)
* 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>

* 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>

* 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): merge label groups in first-member order and drop unused accessors

A HashMap iteration order made the merged series order, and the first collision reported, differ between runs; the root chunk stream exposes that order. Also drop len/is_empty, which had no callers and is_empty was always false.

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

---------

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-21 19:44:43 +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%