Wires the native PromQL route into the production planner so a real PromQL statement reaches `FusedSeriesExec` and runs. Scope is a standalone, single-region point of contact: `rate(x[5m])` and `abs(rate(x[5m]))` execute through the node, and every shape the compiler does not accept keeps the plan the existing planner builds. Depends on the node `FusedSeries` / `FusedSeriesExec`, which landed in `narthex` as #9820. ## The route A statement is offered to `native_compile::compile` at its root inside `PromPlanner`, before the expression is split into the nodes a range function and the math functions over it would have shared. The route is offered only when **both** hold: the engine-wide `query.experimental_native_promql` option (retained in `QueryEngineState`) and a per-request query-context extension of the same name. Either alone keeps the existing plan, so a client cannot turn the route on against the server's configuration. A refused shape is the route declining the query, not an approximation: planning continues exactly as it would with the flag off. ## Getting the node to the region **Placement.** `check_extension_plan` answered `Unsupported` for the node, which the analyzer treats as a boundary. It now answers commutative when the input is exactly the table scan (the region builds the node from the scan's own entrypoint and cannot look through a filter or projection), the node binds a metric name, and no node of its tree folds across series. **Encodability.** `should_expand` asks whether a plan encodes to Substrait before it asks whether a node commutes. That is right for a region reached over the wire and wrong for standalone, whose region server is in the same process. The engine now knows whether its regions are reached in process, and the exception is scoped to a plan whose **root** is `FusedSeries`; every other plan keeps the ordinary whole-plan fast path and the per-node check. Both transports are covered by the analyzer tests. **Scan hint.** A region only prepares its series scan for a per-series request. The existing `ScanHintRule` now attaches `PerSeries` to the scan that is the immediate input of a fused node; the duplicate helper that used to do it from the planner is deleted. **Time bounds.** The native scan is the selector's own scan, so it gets the same bounds the legacy selector path computes: `build_time_index_filter` with the selector range, `offset` and `@` anchor, pushed into `TableScan.filters` so the plan stays a bare `TableScan`. Without this the scan read the table's whole retention. ## Cross-region correctness A region evaluates the whole tree over the series its own scan returns, so a series split across storage regions loses window contributions instead of failing — `rate(cpu[2m])` on a time-partitioned table returns nothing where the complete series answers `1.0`. `single_partition_scan()` only covers scan partitions inside one region. The route therefore declines unless **every partition column of the table is one of the selector's own tag columns**: no partition columns is a single region, and a tag-determined partition key cannot split one series. Anything else, including internal columns, falls back to the existing plan. No exchange and no region-count probing. ## Verification Real standalone, `experimental_native_promql = true`, physical plan from `datafusion::physical_planner=debug`: ``` FusedSeriesExec: rate(cpu[5m]) NativeTree root Window rate [300000ms] ... SeriesScanExec ... ``` - `rate(cpu[5m])` and `abs(rate(cpu[5m]))` return the same series, timestamps and values as the existing path on a dense series, a sparse one and a two-point series. **Multi-partition, with a flush in the middle.** `[query] parallelism = 4`, 8 series with a `shard` tag written in two rounds with `ADMIN FLUSH_TABLE` between them, so each series' samples span a memtable and an SST. The physical plan reports `SeriesScanExec: ... partitions=4` and `FusedSeriesExec`, and for both `rate(cpu[5m])` and `abs(rate(cpu[5m]))` all 40 emitted rows match the legacy plan position by position (timestamps, values, `host`/`shard` labels). A series split across partitions would have made `rate` lose the earlier sample, so the row-for-row match is the check that each series stayed in one scan partition. **Scan parallelism is regression-tested in the workspace, not just at the seam.** Moving the range-assignment rule into `table::table::scan` initially broke two callers that no single-crate test covered (`src/cmd/src/datanode/scanbench.rs` and `parallelize_scan`'s own test module); `cargo check --workspace --all-targets` is what caught them, and it now passes along with `query --lib` 897 passed. - `/v1/prometheus/api/v1/query` and `/query_range` responses are identical between the two routes. - `/v1/promql` now emits the same column order as the existing path (time index first). Sqlness, standalone and distributed: `distinct`, `latest_per_series`, `neumann` pass. Focused unit tests, 13 passed / 0 failed (`native_fused_*` and the request-opt-in parser after the admission fix; the analyzer and scan-hint checks below are separate invocations): `native_fused_*` (including the time-partitioned fallback and the tag-partitioned still-native case), the request-opt-in parser, both analyzer encodability checks under **both** transports, the ordinary-plan fast-path assertion, and the three `ScanHintRule` cases. Clippy `-p query --all-targets -D warnings` and fmt clean. ## Admission: what the route refuses The region's raw-series preflight refuses an append-mode table and any time index that is not millisecond, and the physical layer turns that refusal into a planning error rather than a fallback. The logical route therefore declines those tables itself, so such a query keeps the existing plan instead of failing after the logical plan is already fixed. Both cases have fallback tests asserting the native and flag-off plans are identical. ## What the route refuses, and why that is a fallback rather than an error Every refusal below happens while the logical plan is still this function's to choose, so the query keeps the plan the existing planner builds. That matters because once a `FusedSeries` exists the logical plan is fixed, and the physical layer can only report an error - it cannot undo the choice. - **A shape the compiler refuses**, a table it cannot bind, a remaining label matcher, `__field__`, a subquery (the selector walk does not enter one), or more than one selector. - **A tree that reads the whole vector at a point** (`Aggregate`, vector-vector `Binary`, and `Vector`: `sort`, `absent`, `topk`, `histogram_quantile`, ...). One executor runs per scan partition, and a partition holds only part of a metric's series. - **A table whose own settings the region cannot frame**: a non-millisecond time index, append mode, a partition column that is not a tag, a field that is not numeric (a decimal is not: the region asks `ConcreteDataType::is_numeric`, which excludes them), or an engine without a raw-series scanner. Mito's series scanner is the one that implements the raw path; a file-engine table is read by a `SinglePartitionScanner`, which cannot frame a series. A metric-engine logical table is refused too: it reads through a mito scanner, but the planner only reaches this node with a bare scan when it could not resolve the physical table, and that table's `TableInfo` carries no `append_mode` - so the region's own eligibility could not be checked here. That is a false negative that costs the query its native route, not an error. - **A plan that cannot reach a region**: regions reached over the wire (a distributed frontend) and a PromQL statement embedded in a hybrid CTE, where the node would sit under a SQL root and the in-process encodability exception - which is scoped to a plan whose root is the node - would not apply. Two shapes fall back for a simpler reason, and are recorded as limits rather than fixed here because supporting them would widen this POC to scalar broadcast: a negation or a binary operation mentioning a selector (`-m`, `rate(m[5m]) * 100`) is not found by the selector walk, and a statement whose bounds precede the Unix epoch cannot be represented as a `SystemTime`. Both answer correctly on the existing path. - **`TQL EXPLAIN` / `TQL ANALYZE` still print the existing plan.** Those statements wrap the expression in an explain extension before this route sees it, and the selector walk does not enter a wrapper, so the route declines. The query itself answers the same way; what differs is that the displayed plan does not show the node the same statement executes through `TQL EVAL`. Closing that needs the wrapper recognised and re-applied around a native plan, which is deliberately not part of this POC. ## Known limits, stated plainly - **The value column is named `val`, not the expression-derived name** the legacy projection carries (`prom_rate(ts_range,val,ts,Int64(300000))`). Prometheus API output is unaffected because it reads column roles, not names; the greptimedb-shaped `/v1/promql` response shows the difference. - **Scan parallelism is preserved.** The fused leaf needs a *prepared* scan, because `ParallelizeScan` runs after physical planning and cannot see a `SeriesScanExec`. Preparation now uses the same range-assignment rule the parallelizer uses — `table::table::scan::assign_partition_ranges`, moved there and shared by both callers — with the session's `target_partitions`, and it leaves an already-prepared scan untouched. The inherited single-partition restriction in `resolve_named_series_binding` is gone, so a multi-partition scan resolves its binding; the series producer still keeps one series inside one scan partition, so each partition feeds `FusedSeriesExec` with whole series. - **Each series must stay inside one region.** The guard declines the route unless every partition column is one of the selector's own tag columns, so a table partitioned by time, or by anything that is not a series label, falls back to the existing plan instead of answering wrongly. A tag-partitioned table is accepted and does use the native route, because a tag-determined partition key cannot split one series. The guard is a sufficient condition, not a proof for every partitioning scheme: it has not been exercised against a concurrent repartition lifecycle. - **Anything with a label matcher falls back**, because a matcher that survives preprocessing selects fewer rows than the scan below the node reads. - **Distributed is unsupported**: in a frontend the metric scan is rewritten to `MergeScanLogicalPlan`, the node is not placed there, and the flag is off by default. - **No aggregates, scalars or subquery execution.**
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 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.
Benchmarks
- Agent RCA Bench: LLM agents doing root cause analysis over GreptimeDB versus Prometheus + Loki + Tempo. 40% fewer wrong diagnoses, 48% fewer input tokens, 45% lower cost (write-up)
- GreptimeDB tops JSONBench's billion-record cold run test
- TSBS Benchmark
- More benchmark reports
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.
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, and4003are not blocked by a firewall or used by other services. - Failed to start? Check the container logs with
docker logs greptimefor 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++/autoconfand the glibc dev package (libc6-devon Ubuntu,glibc-develon 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
- Kubernetes: GreptimeDB Operator
- Helm Charts: Greptime Helm Charts
- Dashboard: Web UI
- gRPC Ingester: Go, Java, C++, Erlang, Rust, .NET, TypeScript
- Grafana Data Source: GreptimeDB Grafana data source plugin
- Grafana Dashboard: Official Dashboard for monitoring
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.
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
- Read our Contribution Guidelines.
- Explore Internal Concepts and DeepWiki.
- Pick up a good first issue and join the #contributors Slack channel.
Acknowledgement
Special thanks to all contributors! See AUTHOR.md.
- Uses Apache Arrow™ (memory model)
- Apache Parquet™ (file storage)
- Apache DataFusion™ (query engine)
- Apache OpenDAL™ (data access abstraction)
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.
