# Table of Contents - [Introduction | QuestDB](#introduction-questdb) - [Memory Management | QuestDB](#memory-management-questdb) - [Observability | QuestDB](#observability-questdb) - [Query Engine | QuestDB](#query-engine-questdb) - [Time Series Optimizations | QuestDB](#time-series-optimizations-questdb) - [SQL optimizer hints | QuestDB](#sql-optimizer-hints-questdb) - [Deduplication | QuestDB](#deduplication-questdb) - [Symbol | QuestDB](#symbol-questdb) - [Time Partitions | QuestDB](#time-partitions-questdb) - [JIT compiler | QuestDB](#jit-compiler-questdb) - [Working with time zones | QuestDB](#working-with-time-zones-questdb) - [Materialized views | QuestDB](#materialized-views-questdb) - [Query tracing | QuestDB](#query-tracing-questdb) - [SQL extensions | QuestDB](#sql-extensions-questdb) - [Designated timestamp | QuestDB](#designated-timestamp-questdb) - [Interval Scan | QuestDB](#interval-scan-questdb) - [Indexes | QuestDB](#indexes-questdb) - [Architecture Overview | QuestDB](#architecture-overview-questdb) - [Command-line options | QuestDB](#command-line-options-questdb) - [Root directory structure | QuestDB](#root-directory-structure-questdb) - [QuestDB Storage Engine | QuestDB](#questdb-storage-engine-questdb) - [Write-Ahead Log (WAL) | QuestDB](#write-ahead-log-wal-questdb) - [Views | QuestDB](#views-questdb) - [Configuration | QuestDB](#configuration-questdb) - [Time To Live (TTL) | QuestDB](#time-to-live-ttl-questdb) - [Cookbook overview | QuestDB](#cookbook-overview-questdb) - [Demo data schema | QuestDB](#demo-data-schema-questdb) - [Query multiple tables dynamically in Grafana | QuestDB](#query-multiple-tables-dynamically-in-grafana-questdb) - [Overlay two time series with time shift | QuestDB](#overlay-two-time-series-with-time-shift-questdb) - [Configure read-only user for Grafana | QuestDB](#configure-read-only-user-for-grafana-questdb) - [Grafana variable dropdown with name and value | QuestDB](#grafana-variable-dropdown-with-name-and-value-questdb) - [Collect OPC-UA data with Telegraf in dense format | QuestDB](#collect-opc-ua-data-with-telegraf-in-dense-format-questdb) - [Replication tuning | QuestDB](#replication-tuning-questdb) - [WAL cleanup | QuestDB](#wal-cleanup-questdb) - [Replication setup guide | QuestDB](#replication-setup-guide-questdb) - [Copy data between QuestDB instances | QuestDB](#copy-data-between-questdb-instances-questdb) - [ZFS Compression | QuestDB](#zfs-compression-questdb) - [AI Coding Agents | QuestDB](#ai-coding-agents-questdb) - [Replication overview | QuestDB](#replication-overview-questdb) - [Handle missing columns in C++ client | QuestDB](#handle-missing-columns-in-c-client-questdb) - [Deploying QuestDB on AWS | QuestDB](#deploying-questdb-on-aws-questdb) - [Deploying QuestDB on Azure | QuestDB](#deploying-questdb-on-azure-questdb) - [Check transaction applied after ingestion | QuestDB](#check-transaction-applied-after-ingestion-questdb) - [Deploying to Digital Ocean | QuestDB](#deploying-to-digital-ocean-questdb) - [Launch QuestDB with systemd | QuestDB](#launch-questdb-with-systemd-questdb) - [Show parameters with non-default values | QuestDB](#show-parameters-with-non-default-values-questdb) - [Upgrade to QuestDB Enterprise | QuestDB](#upgrade-to-questdb-enterprise-questdb) - [Dagster | QuestDB](#dagster-questdb) - [Insert data from PHP using ILP | QuestDB](#insert-data-from-php-using-ilp-questdb) - [Optimize disk and memory usage with many tables | QuestDB](#optimize-disk-and-memory-usage-with-many-tables-questdb) - [Insert data from Ruby using ILP | QuestDB](#insert-data-from-ruby-using-ilp-questdb) - [Pandas | QuestDB](#pandas-questdb) - [Import CSV with millisecond timestamps | QuestDB](#import-csv-with-millisecond-timestamps-questdb) - [Apache Airflow | QuestDB](#apache-airflow-questdb) - [Configure QuestDB with Docker Compose | QuestDB](#configure-questdb-with-docker-compose-questdb) - [Store QuestDB metrics in QuestDB | QuestDB](#store-questdb-metrics-in-questdb-questdb) - [Query performance histogram | QuestDB](#query-performance-histogram-questdb) - [QuestDB Flink connector | QuestDB](#questdb-flink-connector-questdb) - [InfluxDB Line Protocol Columnset Value Types | QuestDB](#influxdb-line-protocol-columnset-value-types-questdb) - [Apache Spark and Time-Series Analytics | QuestDB](#apache-spark-and-time-series-analytics-questdb) - [Last look detection | QuestDB](#last-look-detection-questdb) - [Telegraf | QuestDB](#telegraf-questdb) - [Polars | QuestDB](#polars-questdb) - [Java (embedded) | QuestDB](#java-embedded-questdb) - [TLS with PgBouncer for QuestDB | QuestDB](#tls-with-pgbouncer-for-questdb-questdb) - [Deploying to Google Cloud Platform (GCP) | QuestDB](#deploying-to-google-cloud-platform-gcp-questdb) - [Redpanda | QuestDB](#redpanda-questdb) - [Embeddable | QuestDB](#embeddable-questdb) - [Superset | QuestDB](#superset-questdb) - [Run QuestDB on Kubernetes | QuestDB](#run-questdb-on-kubernetes-questdb) - [Implementation shortfall decomposition | QuestDB](#implementation-shortfall-decomposition-questdb) - [Configure TLS certificate authorities | QuestDB](#configure-tls-certificate-authorities-questdb) - [Grafana | QuestDB](#grafana-questdb) - [qStudio | QuestDB](#qstudio-questdb) - [Capacity planning | QuestDB](#capacity-planning-questdb) - [Data retention | QuestDB](#data-retention-questdb) - [Alternatives to UPDATE | QuestDB](#alternatives-to-update-questdb) - [List of OS error codes | QuestDB](#list-of-os-error-codes-questdb) - [ECN scorecard | QuestDB](#ecn-scorecard-questdb) - [Client configuration string | QuestDB](#client-configuration-string-questdb) - [QuestDB Enterprise quick start | QuestDB](#questdb-enterprise-quick-start-questdb) - [How UPDATE works | QuestDB](#how-update-works-questdb) - [Automating QuestDB Tasks | QuestDB](#automating-questdb-tasks-questdb) - [Logging and metrics | QuestDB](#logging-and-metrics-questdb) - [Post-trade markout analysis | QuestDB](#post-trade-markout-analysis-questdb) - [SQLAlchemy | QuestDB](#sqlalchemy-questdb) - [CSV Import | QuestDB](#csv-import-questdb) - [Using Docker with QuestDB | QuestDB](#using-docker-with-questdb-questdb) - [Create a sample database | QuestDB](#create-a-sample-database-questdb) - [Order-level implementation shortfall | QuestDB](#order-level-implementation-shortfall-questdb) - [Import CSV Using Web Console | QuestDB](#import-csv-using-web-console-questdb) - [Databento | QuestDB](#databento-questdb) - [Web Console Overview | QuestDB](#web-console-overview-questdb) - [Query Log | QuestDB](#query-log-questdb) - [Schema Explorer | QuestDB](#schema-explorer-questdb) - [QuestDB AI | QuestDB](#questdb-ai-questdb) - [Go Client Documentation | QuestDB](#go-client-documentation-questdb) - [Advanced InfluxDB Line Protocol settings | QuestDB](#advanced-influxdb-line-protocol-settings-questdb) - [Code Editor | QuestDB](#code-editor-questdb) - [Monitoring and alerting | QuestDB](#monitoring-and-alerting-questdb) - [Backup and restore | QuestDB](#backup-and-restore-questdb) - [Metrics View | QuestDB](#metrics-view-questdb) - [Node.js Client Documentation | QuestDB](#node-js-client-documentation-questdb) - [PowerBI | QuestDB](#powerbi-questdb) - [Profiling | QuestDB](#profiling-questdb) - [Result Grid | QuestDB](#result-grid-questdb) - [Date to Timestamp Conversion in Different Programming Languages | QuestDB](#date-to-timestamp-conversion-in-different-programming-languages-questdb) - [Cube | QuestDB](#cube-questdb) - [Decimal | QuestDB](#decimal-questdb) - [MindsDB | QuestDB](#mindsdb-questdb) - [Ignition 8.3 | QuestDB](#ignition-8-3-questdb) - [Integrating Airbyte with QuestDB | QuestDB](#integrating-airbyte-with-questdb-questdb) - [Slippage per fill | QuestDB](#slippage-per-fill-questdb) - [Data Types Overview | QuestDB](#data-types-overview-questdb) - [List of QuestDB Error Codes | QuestDB](#list-of-questdb-error-codes-questdb) - [Deploying to Hetzner Cloud | QuestDB](#deploying-to-hetzner-cloud-questdb) - [Post-trade analysis overview | QuestDB](#post-trade-analysis-overview-questdb) - [Prometheus monitoring and alerting | QuestDB](#prometheus-monitoring-and-alerting-questdb) - [Rust Client Documentation | QuestDB](#rust-client-documentation-questdb) - [Parquet Export | QuestDB](#parquet-export-questdb) - [Java Client Documentation | QuestDB](#java-client-documentation-questdb) - [Ingestion from Kafka Overview | QuestDB](#ingestion-from-kafka-overview-questdb) - [N-Dimensional array | QuestDB](#n-dimensional-array-questdb) - [Create Table | QuestDB](#create-table-questdb) - [Quick start | QuestDB](#quick-start-questdb) - [TLS Encryption | QuestDB](#tls-encryption-questdb) - [OpenID Connect (OIDC) Integration | QuestDB](#openid-connect-oidc-integration-questdb) - [Create arrays from string literals | QuestDB](#create-arrays-from-string-literals-questdb) - [General and sampled aggregates | QuestDB](#general-and-sampled-aggregates-questdb) - [Aggregated slippage by venue and counterparty | QuestDB](#aggregated-slippage-by-venue-and-counterparty-questdb) - [Find local minimum and maximum | QuestDB](#find-local-minimum-and-maximum-questdb) - [Multiple conditional aggregates | QuestDB](#multiple-conditional-aggregates-questdb) - [Python Client Documentation | QuestDB](#python-client-documentation-questdb) - [Sankey and funnel diagrams | QuestDB](#sankey-and-funnel-diagrams-questdb) - [.NET Client Documentation | QuestDB](#-net-client-documentation-questdb) - [C & C++ Client Documentation | QuestDB](#c-c-client-documentation-questdb) - [Third-Party Tools Overview | QuestDB](#third-party-tools-overview-questdb) - [Consistent histogram buckets | QuestDB](#consistent-histogram-buckets-questdb) - [Access rows before and after current row | QuestDB](#access-rows-before-and-after-current-row-questdb) - [Pivot with "Others" column | QuestDB](#pivot-with-others-column-questdb) - [Schema design | QuestDB](#schema-design-questdb) - [Top N plus others row | QuestDB](#top-n-plus-others-row-questdb) - [Maximum drawdown | QuestDB](#maximum-drawdown-questdb) - [Ingestion overview | QuestDB](#ingestion-overview-questdb) - [Geospatial data | QuestDB](#geospatial-data-questdb) - [FAQ | QuestDB](#faq-questdb) - [Unpivoting query results | QuestDB](#unpivoting-query-results-questdb) - [InfluxDB Line Protocol Overview | QuestDB](#influxdb-line-protocol-overview-questdb) - [Bid-ask spread | QuestDB](#bid-ask-spread-questdb) - [Calculate compound interest | QuestDB](#calculate-compound-interest-questdb) - [REST API | QuestDB](#rest-api-questdb) - [Aggressor volume imbalance | QuestDB](#aggressor-volume-imbalance-questdb) - [ATR (Average True Range) | QuestDB](#atr-average-true-range-questdb) - [Cumulative product for random walk | QuestDB](#cumulative-product-for-random-walk-questdb) - [Liquidity comparison across instruments | QuestDB](#liquidity-comparison-across-instruments-questdb) - [Bollinger BandWidth | QuestDB](#bollinger-bandwidth-questdb) - [Gamma scalping signal | QuestDB](#gamma-scalping-signal-questdb) - [Donchian Channels | QuestDB](#donchian-channels-questdb) - [Bollinger bands | QuestDB](#bollinger-bands-questdb) - [Comparison Operators | QuestDB](#comparison-operators-questdb) - [Logical Operators | QuestDB](#logical-operators-questdb) - [SQL execution order | QuestDB](#sql-execution-order-questdb) - [Text Operators | QuestDB](#text-operators-questdb) - [Operator Precedence Table | QuestDB](#operator-precedence-table-questdb) - [MACD (Moving Average Convergence Divergence) | QuestDB](#macd-moving-average-convergence-divergence-questdb) - [OBV (On-Balance Volume) | QuestDB](#obv-on-balance-volume-questdb) - [Numeric Operators | QuestDB](#numeric-operators-questdb) - [IPv4 Operators | QuestDB](#ipv4-operators-questdb) - [Bitwise Operators | QuestDB](#bitwise-operators-questdb) - [Log returns | QuestDB](#log-returns-questdb) - [Rate of Change (ROC) | QuestDB](#rate-of-change-roc-questdb) - [TICK and TRIN indicators | QuestDB](#tick-and-trin-indicators-questdb) - [Misc Operators | QuestDB](#misc-operators-questdb) - [Keltner Channels | QuestDB](#keltner-channels-questdb) - [Volume profile | QuestDB](#volume-profile-questdb) - [R PGwire Guide | QuestDB](#r-pgwire-guide-questdb) - [PHP PGWire Guide | QuestDB](#php-pgwire-guide-questdb) - [Handling Large Result Sets | QuestDB](#handling-large-result-sets-questdb) - [Stochastic Oscillator | QuestDB](#stochastic-oscillator-questdb) - [Rolling standard deviation | QuestDB](#rolling-standard-deviation-questdb) - [VPIN (Volume-synchronized Probability of Informed Trading) | QuestDB](#vpin-volume-synchronized-probability-of-informed-trading-questdb) - [Date and Time Operators | QuestDB](#date-and-time-operators-questdb) - [Exchange calendars | QuestDB](#exchange-calendars-questdb) - [OHLC bars | QuestDB](#ohlc-bars-questdb) - [Spatial Operators | QuestDB](#spatial-operators-questdb) - [Volume spike detection | QuestDB](#volume-spike-detection-questdb) - [RSI (Relative Strength Index) | QuestDB](#rsi-relative-strength-index-questdb) - [Order book analytics using arrays | QuestDB](#order-book-analytics-using-arrays-questdb) - [PostgreSQL Wire Protocol | QuestDB](#postgresql-wire-protocol-questdb) - [C/C++ PGWire Guide | QuestDB](#c-c-pgwire-guide-questdb) - [Query & SQL Overview | QuestDB](#query-sql-overview-questdb) - [Elapsed time between rows | QuestDB](#elapsed-time-between-rows-questdb) - [Time-weighted average price (TWAP) | QuestDB](#time-weighted-average-price-twap-questdb) - [Query with epoch timestamps | QuestDB](#query-with-epoch-timestamps-questdb) - [FILL on keyed queries with arbitrary intervals | QuestDB](#fill-on-keyed-queries-with-arbitrary-intervals-questdb) - [Query last N minutes of activity | QuestDB](#query-last-n-minutes-of-activity-questdb) - [.NET PGWire Guide | QuestDB](#-net-pgwire-guide-questdb) --- # Introduction | QuestDB On this page QuestDB is an open source time-series database engineered for low latency. It uses a column-oriented, time-partitioned storage engine with memory-mapped files and vectorized (SIMD) execution to support high-throughput ingestion and millisecond-level analytical queries. The system is built from scratch with a zero-GC Java core and focused C++/Rust components, in a compact codebase optimized for cache locality and predictable tail latency. SQL is extended with time-series operators such as `SAMPLE BY`, `LATEST ON`, `ASOF JOIN`, and `WINDOW JOIN`. See [Architecture](https://questdb.com/docs/architecture/questdb-architecture/) for details. [Quick start](https://questdb.com/docs/getting-started/quick-start/) [Live demo](https://demo.questdb.io/) [Test your skills](https://questdb.com/quiz/) About this documentation[​](https://questdb.com/docs/#about-this-documentation "Direct link to About this documentation") -------------------------------------------------------------------------------------------------------------------------- This documentation covers both **QuestDB Open Source** and **QuestDB Enterprise**. QuestDB Enterprise builds on top of QuestDB Open Source, using it as its core library. Everything in open source works in Enterprise, but not the other way around. Enterprise adds features like high availability, advanced security, RBAC, automated backups, and multi-tier storage with seamless object storage integration. Get started[​](https://questdb.com/docs/#get-started "Direct link to Get started") ----------------------------------------------------------------------------------- 1. **[Quick start](https://questdb.com/docs/getting-started/quick-start/) ** - Install and run QuestDB 2. **[Schema design](https://questdb.com/docs/schema-design-essentials/) ** - Design your tables 3. **[Ingest data](https://questdb.com/docs/ingestion/overview/) ** - Bring your data using QuestDB clients 4. **[Query data](https://questdb.com/docs/query/overview/) ** - Analyze with SQL Guides[​](https://questdb.com/docs/#guides "Direct link to Guides") -------------------------------------------------------------------- ### Create database Set up your first QuestDB database and start storing time-series data. [Read more](https://questdb.com/docs/getting-started/create-database/) ### Capacity planning Select a storage medium, plan, size and compress your QuestDB deployment. [Read more](https://questdb.com/docs/getting-started/capacity-planning/) ### Working with time It's about time. Learn how to work with timestamps and timezones in QuestDB. [Read more](https://questdb.com/docs/concepts/timestamps-timezones/) ### Backup and restore See the methods to backup and restore your QuestDB deployment. [Read more](https://questdb.com/docs/operations/backup/) Resources[​](https://questdb.com/docs/#resources "Direct link to Resources") ----------------------------------------------------------------------------- ### [Query overview](https://questdb.com/docs/query/overview/) Learn about our powerful extended SQL and how to use it to query QuestDB. ### [Language clients](https://questdb.com/docs/ingestion/overview/#first-party-clients) Explore our language clients and how to use them to ingest data into QuestDB. ### [Configuration](https://questdb.com/docs/configuration/overview/) See all of our available configuration options and fine-tune to match your use case. ### [Third-Party Tools](https://questdb.com/docs/integrations/overview/) Our recommended third-party tools can aid you in analyzing and visualizing your data. * [About this documentation](https://questdb.com/docs/#about-this-documentation) * [Get started](https://questdb.com/docs/#get-started) * [Guides](https://questdb.com/docs/#guides) * [Resources](https://questdb.com/docs/#resources) --- # Memory Management | QuestDB On this page Memory management and native integration[​](https://questdb.com/docs/architecture/memory-management/#memory-management-and-native-integration "Direct link to Memory management and native integration") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB leverages both memory mapping and explicit memory management techniques, and integrates native code for performance-critical tasks. ### Memory-mapped files[​](https://questdb.com/docs/architecture/memory-management/#memory-mapped-files "Direct link to Memory-mapped files") * **Direct OS integration:** Memory-mapped files let QuestDB use the operating system's page cache. This reduces explicit I/O calls and speeds up sequential reads. * **Sequential access:** When data partitions by incremental timestamp, memory mapping ensures that reads are sequential and efficient. ### Direct memory management and native integration[​](https://questdb.com/docs/architecture/memory-management/#direct-memory-management-and-native-integration "Direct link to Direct memory management and native integration") * **Off-heap memory usage:** QuestDB allocates direct memory via memory mapping and low-level APIs (such as Unsafe) to bypass the JVM garbage collector. This reduces latency spikes and garbage collection delays. * **Hotpath efficiency:** The system pre-allocates and reuses memory in critical code paths, avoiding dynamic allocation on the hotpath. * **Native code integration:** QuestDB uses native libraries written in C++ and Rust for performance-critical tasks. These native components share off-heap buffers with Java via JNI. * **Zero-copy interoperability:** Sharing memory between Java and native code minimizes data copying and reduces latency. * **Hybrid architecture:** This integration lets QuestDB use Java for rapid development and C++/Rust for low-level, high-performance routines. Next up[​](https://questdb.com/docs/architecture/memory-management/#next-up "Direct link to Next up") ------------------------------------------------------------------------------------------------------ Continue to [Query Engine](https://questdb.com/docs/architecture/query-engine/) to learn how QuestDB parses, optimizes, and executes SQL queries. * [Memory management and native integration](https://questdb.com/docs/architecture/memory-management/#memory-management-and-native-integration) * [Memory-mapped files](https://questdb.com/docs/architecture/memory-management/#memory-mapped-files) * [Direct memory management and native integration](https://questdb.com/docs/architecture/memory-management/#direct-memory-management-and-native-integration) * [Next up](https://questdb.com/docs/architecture/memory-management/#next-up) --- # Observability | QuestDB On this page Observability & diagnostics[​](https://questdb.com/docs/architecture/observability/#observability--diagnostics "Direct link to Observability & diagnostics") ------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB provides real-time metrics, a health check endpoint, and logging to monitor performance and simplify troubleshooting. * **Metrics:** QuestDB exposes detailed [metrics in Prometheus format](https://questdb.com/docs/operations/logging-metrics/#metrics) , including query statistics, memory usage, and I/O details. * **Health check:** A [minimal HTTP server](https://questdb.com/docs/operations/logging-metrics/#minimal-http-server) monitors system health. * **Metadata tables:** The engine provides [metadata tables](https://questdb.com/docs/query/functions/meta/) to query table status, partition status, query execution, and latency. * **Extensive logging:** [Logging](https://questdb.com/docs/operations/logging-metrics/) covers SQL parsing, execution, background processing, and runtime exceptions. The framework minimizes performance impact. * **Real-time metric dashboards:** The web console lets you create dashboards that display per-table metrics. ![Metric dashboard at the QuestDB Console](https://questdb.com/docs/images/guides/questdb-internals/telemetry.webp) Metric dashboard at the QuestDB Console Next up[​](https://questdb.com/docs/architecture/observability/#next-up "Direct link to Next up") -------------------------------------------------------------------------------------------------- Back to [Architecture Overview](https://questdb.com/docs/architecture/questdb-architecture/) or continue to [Configuration](https://questdb.com/docs/configuration/overview/) . * [Observability & diagnostics](https://questdb.com/docs/architecture/observability/#observability--diagnostics) * [Next up](https://questdb.com/docs/architecture/observability/#next-up) --- # Query Engine | QuestDB On this page Query engine[​](https://questdb.com/docs/architecture/query-engine/#query-engine "Direct link to Query engine") ---------------------------------------------------------------------------------------------------------------- The QuestDB Query Engine includes A custom SQL parser, a just-in-time (JIT) compiler, and a vectorized execution engine to process data in table page frames for better CPU use. ### SQL parsing & optimization[​](https://questdb.com/docs/architecture/query-engine/#sql-parsing--optimization "Direct link to SQL parsing & optimization") * **Custom SQL parser:** The parser supports QuestDB's SQL dialect and time-series extensions. It converts SQL queries into an optimized abstract syntax tree (AST). * **Compilation pipeline:** The engine compiles SQL into an execution plan through stages that push down predicates and rewrite queries to remove unnecessary operations. * **Optimization techniques:** The planner applies rule-based rewrites and simple cost estimations to choose efficient execution paths. * **Columnar reads:** Table columns are randomly accessible. Columns with fixed size data types are read by translating the record number into a file offset by a simple bit shift. The offset in the column file is then translated into an offset in a lazily mapped memory page, where the required value is read from. ![Diagram showing how the data from a column file is mapped to the memory](https://questdb.com/docs/images/guides/questdb-internals/columnRead.webp) Diagram showing how the data from a column file is mapped to the memory ### Execution model[​](https://questdb.com/docs/architecture/query-engine/#execution-model "Direct link to Execution model") * **Operator pipeline:** The execution plan runs as a series of operators (filters, joins, aggregators) in a tightly integrated pipeline. ![Query Plan for a query with multi-threaded count with a group by](https://questdb.com/docs/images/guides/questdb-internals/query_plan.webp) Query Plan for a query with multi-threaded count with a group by * **JIT compilation and Vectorized processing:** Queries with a `WHERE` clause [compile](https://questdb.com/docs/concepts/deep-dive/jit-compiler/) critical parts of the execution plan to native machine code (SIMD AVX-2 instructions) just in time. Vectorized instructions apply the same operation to many data elements simultaneously. This maximizes CPU cache use and reduces overhead. * **Multi-threaded execution:** On top of the JIT, QuestDB tries to execute as many queries as possible in a multi-threaded, multi-core fashion. Some queries, for example those involving an index, are executed on a single thread. Other queries, like those involving `GROUP BY` and `SAMPLE BY`, execute a pipeline with some single-threaded stages and some multi-threaded stages to avoid slow downs when groups are unbalanced. * **Worker pools:** QuestDB allows to configure different pools for specialized functions, like parsing incoming data, applying WAL file changes, handling PostgreSQL-Wire protocol, or responding to HTTP connections. By default, most tasks are handled by a shared worker pool. * **Query plan caching:** The system caches query plans for reuse within the same connection. (Query results are not cached.) * **Column data caching:** Data pages read from disk are kept in system memory. Sufficient memory prevents frequent disk reads. Next up[​](https://questdb.com/docs/architecture/query-engine/#next-up "Direct link to Next up") ------------------------------------------------------------------------------------------------- Continue to [Time-series Optimizations](https://questdb.com/docs/architecture/time-series-optimizations/) to learn about QuestDB's time-series specific features. * [Query engine](https://questdb.com/docs/architecture/query-engine/#query-engine) * [SQL parsing & optimization](https://questdb.com/docs/architecture/query-engine/#sql-parsing--optimization) * [Execution model](https://questdb.com/docs/architecture/query-engine/#execution-model) * [Next up](https://questdb.com/docs/architecture/query-engine/#next-up) --- # Time Series Optimizations | QuestDB On this page Time-series optimizations[​](https://questdb.com/docs/architecture/time-series-optimizations/#time-series-optimizations "Direct link to Time-series optimizations") -------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB is specifically designed for time series, and it provides several optimizations such as a designated timestamp, sequential reads, materialized views, and in-memory processing. ### Designated timestamp[​](https://questdb.com/docs/architecture/time-series-optimizations/#designated-timestamp "Direct link to Designated timestamp") * **Timestamp sorting:** Data is stored in order of incremental timestamp. Since ingestion is usually chronological, the system uses a fast append-only strategy, except for updates and out-of-order data. * **Rapid interval queries and sequential reads:** Sorted data lets the system quickly locate the start and end of data files, which speeds up [interval queries](https://questdb.com/docs/concepts/deep-dive/interval-scan/) . When data is accessed by increasing timestamp, reads are sequential for each column file, which makes I/O very efficient. ![Interval scan](https://questdb.com/docs/images/guides/questdb-internals/intervalScan.webp) Interval scan * **Out-of-order data:** When data arrives out of order, QuestDB [rearranges it](https://questdb.com/docs/concepts/partitions/#partition-splitting-and-squashing) to maintain timestamp order. The engine splits partitions to minimize [write amplification](https://questdb.com/docs/getting-started/capacity-planning/#write-amplification) and compacts them in the background. ### Data partitioning and sequential reads[​](https://questdb.com/docs/architecture/time-series-optimizations/#data-partitioning-and-sequential-reads "Direct link to Data partitioning and sequential reads") * **Partitioning by time:** Data [partitions by timestamp](https://questdb.com/docs/concepts/partitions/) with hourly, daily, weekly, monthly, or yearly resolution. ![Diagram of data column files and how they are partitioned to form a table](https://questdb.com/docs/images/guides/questdb-internals/partitionModel.webp) Diagram of data column files and how they are partitioned to form a table * **Partition pruning:** The design lets the engine skip partitions that fall outside query filters. Combined with incremental timestamp sorting, this reduces latency. * **Lifecycle policies:** The system can delete partitions manually or automatically via TTL. It also supports detaching or attaching partitions using SQL commands. ### Materialized views[​](https://questdb.com/docs/architecture/time-series-optimizations/#materialized-views "Direct link to Materialized views") * [Materialized views](https://questdb.com/docs/concepts/materialized-views/) are auto-refreshing tables that store the precomputed results of a query. Unlike regular views, which compute their results at query time, materialized views persist their data to disk, making them particularly efficient for expensive aggregate queries that are run frequently. * QuestDB supports materialized views for `SAMPLE BY` queries, including those joining with other tables. * Materialized sampled intervals are automatically refreshed whenever the base table receives new or updated rows. QuestDB supports different strategies for self-refreshing views: Immediate, Timer, or Period (with an optional allowed delay). Views can also be configured to refresh only when manually triggered. * Materialized views can be chained, with the output of one serving as the input to another, and support TTLs for lifecycle management. ### Time-To-Live and Data lifecycle[​](https://questdb.com/docs/architecture/time-series-optimizations/#time-to-live-and-data-lifecycle "Direct link to Time-To-Live and Data lifecycle") QuestDB supports [Time To Live (TTL)](https://questdb.com/docs/concepts/ttl/) configuration for both regular tables and materialized views. With TTL enabled, partitions older than the configured horizon will automatically be removed. An alternative is to use QuestDB Enterprise to automatically move older partitions to [cold storage](https://questdb.com/docs/architecture/storage-engine/#tier-three-parquet-locally-or-in-an-object-store) , with old partitions converted to Parquet and stored in object storage, while still being available for querying by the query engine. ### In-memory processing[​](https://questdb.com/docs/architecture/time-series-optimizations/#in-memory-processing "Direct link to In-memory processing") * **Caching:** The engine uses the OS cache to access recent and frequently accessed data in memory, reducing disk reads. * **Off-heap buffers:** Off-heap memory, managed via memory mapping and direct allocation, avoids garbage collection overhead. * **Optimized in-memory handling:** Apart from using CPU-level optimizations such as SIMD, QuestDB uses specialized hash tables (all of them with open addressing and linear probing) and implements algorithms for reducing the memory footprint of many operations. * **Custom memory layout for different data types:** Specialized data types such as `Symbol`, `VARCHAR`, `Array`, or `UUID` are designed to use minimal disk and memory. For example, character sequences shorter than 9 bytes are fully inlined within the `VARCHAR` header and do not occupy any additional data space. Internal Representation of the VARCHAR data typeVarchar header (column file):+------------+-------------------+-------------------+| 32 bits | 48 bits | 48 bits || len + flags| prefix | offset |+------------+-------------------+-------------------+ │+------------------------------------+ points to│▼Varchar data (column file):+---+---+---+---+---+---+---+---+---+---+---+| H | e | l | l | o | | w | o | r | l | d |+---+---+---+---+---+---+---+---+---+---+---+ Next up[​](https://questdb.com/docs/architecture/time-series-optimizations/#next-up "Direct link to Next up") -------------------------------------------------------------------------------------------------------------- Continue to [Observability](https://questdb.com/docs/architecture/observability/) to learn about monitoring, metrics, and diagnostics. * [Time-series optimizations](https://questdb.com/docs/architecture/time-series-optimizations/#time-series-optimizations) * [Designated timestamp](https://questdb.com/docs/architecture/time-series-optimizations/#designated-timestamp) * [Data partitioning and sequential reads](https://questdb.com/docs/architecture/time-series-optimizations/#data-partitioning-and-sequential-reads) * [Materialized views](https://questdb.com/docs/architecture/time-series-optimizations/#materialized-views) * [Time-To-Live and Data lifecycle](https://questdb.com/docs/architecture/time-series-optimizations/#time-to-live-and-data-lifecycle) * [In-memory processing](https://questdb.com/docs/architecture/time-series-optimizations/#in-memory-processing) * [Next up](https://questdb.com/docs/architecture/time-series-optimizations/#next-up) --- # SQL optimizer hints | QuestDB On this page QuestDB's query optimizer automatically selects execution plans for SQL queries based on heuristics. While the default execution strategy should be the fastest for most scenarios, you can use hints to select a specific strategy that may better suit your data's characteristics. SQL hints influence the execution strategy of queries without changing their semantics. Hint Syntax[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#hint-syntax "Direct link to Hint Syntax") -------------------------------------------------------------------------------------------------------------------------- In QuestDB, you specify SQL hints in block comments with a plus sign after the opening comment marker. You must place the hint immediately after the `SELECT` keyword: SQL hint syntax SELECT /*+ HINT_NAME(parameter1 parameter2) */ columns FROM table; Only block comment hints (`/*+ HINT */`) are supported, not line comment hints (`--+ HINT`). Hints are designed to be a safe optimization mechanism: * without hints, QuestDB uses default optimization strategies * QuestDB silently ignores unknown hints and those that don't apply to a query * QuestDB silently ignores any syntax errors in a hint block * * * Temporal JOIN hints[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#temporal-join-hints "Direct link to Temporal JOIN hints") -------------------------------------------------------------------------------------------------------------------------------------------------- A significant factor in choosing the optimal algorithm for a [temporal join](https://questdb.com/docs/query/sql/asof-join/) (ASOF and LT) is the pattern in which the rows of the left-hand dataset are matched to the rows of the right-hand dataset. When there's no additional join condition, only the implied matching on timesatmp, the situation is simple: we search for a timestamp in a dataset which is already sorted by timestamp. We can use binary search, or linear search if the search space is small enough. When there is an additional JOIN condition, as in `left ASOF JOIN right ON (condition)`, the matching row can be anywhere in the past from the row that matches by timestamp. For this, we need a more sophisticated algorithm. Our optimized algorithms assume the JOIN condition matches additional columns by equality. Basically, there's a join key that must match on both sides. An even narrower common case we optimize more aggressively for is matching on a _symbol column_ on both sides. We distinguish these two cases: ### 1\. Localized matching[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#1-localized-matching "Direct link to 1. Localized matching") In this case, when scanning the right-hand table backward from the timestamp of the left-hand row, we find a match much sooner than reaching the timestamp of the previous left-hand row. We end up scanning only a small subset of the right-hand rows. In the diagram, we show the scanned portions of the right-hand dataset in red. The best way to perform this join is the straightforward one: first locate the right-hand row that matches by timestamp (marked with the dotted line), then scan backward to find the row satisfying additional join conditions. ![Diagram showing localized row matching](https://questdb.com/docs/images/docs/concepts/asof-join-sparse.svg) ### 2\. Distant matching[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#2-distant-matching "Direct link to 2. Distant matching") In this case, the matching row is in the more distant past, earlier than the previous left-hand row. The scanning ranges now ovelap, and we end up scanning almost the entire right-hand dataset. If we do a separate scan for each left-hand row, we'll end up going over the same rows many times. In the diagram, this shows up as more intensely red regions in the right-hand table. The best way in this case is to scan the entire red region once, collect the join keys in a hashtable, and match up with the left-hand rows as needed. ![Diagram showing distant row matching](https://questdb.com/docs/images/docs/concepts/asof-join-dense.svg) ASOF JOIN algorithms[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#asof-join-algorithms "Direct link to ASOF JOIN algorithms") ----------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB implements several algorithms to deal with keyed joins. * _Fast_ algorithm is the best for localized row matching * _Dense_ algorithm is the best for distant row matching In a real scenario, you may not have such a clear-cut situation. If your join pattern is mostly localized, but with some distant matching, the _Memoized_ algorithm may help. It remembers where the previous match was for a given join key, and can avoid rescanning to find it. When you use a WHERE clause on the right-hand dataset, and if it's highly selective (passing through a small fraction of rows), the _Light_ algorithm may be the best. It is the only one that allows QuestDB to use its parallelized filtering to quickly identify the filtered subset. The default algorithm is _Fast_, and you can enable others through query hints. ### List of hints[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#list-of-hints "Direct link to List of hints") ### `asof_dense(l r)`[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#asof_densel-r "Direct link to asof_densel-r") This hint enables the [Dense](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#dense-algo) algorithm, the best choice (when it's available) for the case of distant row matching. Applying the query hint for the Dense algorithm SELECT /*+ asof_dense(orders md) */ orders.timestamp, orders.symbol, orders.priceFROM ordersASOF JOIN (md) ON (symbol); ### `asof_linear(l r)`[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#asof_linearl-r "Direct link to asof_linearl-r") info This hint applies to `LT` joins as well. This enables the [Light](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#light-algo) algorithm, similar to Dense but simpler. It is more generic and selected automatically in queries where the Dense algo isn't applicable. Its downside is that it must scan the entire history in the RHS table, up to the most recent LHS timestamp. There's a case where the Light algo is at an advantage even when the Dense algo is also available: when the right-hand side is a subquery with a WHERE clause that is highly selective, passing through a small number of rows. QuestDB has parallelized filtering support, which cannot be used with the other algorithms. Applying the query hint for the Light algorithm SELECT /*+ asof_linear(orders md) */ orders.ts, orders.price, md.md_ts, md.bid, md.askFROM ordersASOF JOIN ( SELECT ts as md_ts, bid, ask FROM market_data WHERE state = 'INVALID' -- Highly selective filter) md; ### `asof_memoized(l r)`[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#asof_memoizedl-r "Direct link to asof_memoizedl-r") This hint enables [Memoized](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#memoized-algo) , a variant of the [Fast](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#fast-algo) algorithm. It works for queries that join on a symbol column, as in `left ASOF JOIN right ON (symbol)`. It helps when there's a mix of localized and distant matches by reusing the results of earlier backward scans. Appling the query hint for the Memoized algorithm SELECT /*+ asof_memoized(orders md) */ orders.timestamp, orders.symbol, orders.priceFROM ordersASOF JOIN (md) ON (symbol); * * * ### Check the Execution Plan[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#check-the-execution-plan "Direct link to Check the Execution Plan") You can verify how QuestDB executes your query by examining its execution plan with the `EXPLAIN` statement. #### Default Execution Plan (Binary Search)[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#default-execution-plan-binary-search "Direct link to Default Execution Plan (Binary Search)") Without any hints, a filtered `ASOF JOIN` will use the Fast algorithm. Observing the default execution plan[Demo this query](https://demo.questdb.io/?query=EXPLAIN%20SELECT%20%20*%0AFROM%20core_price%0AASOF%20JOIN%20market_data%0AON%20symbol%0AWHERE%20bids%5B1%2C1%5D%3D107.03%20--%20Highly%20selective%20filter%0A%3B&executeQuery=true) EXPLAIN SELECT *FROM core_priceASOF JOIN market_dataON symbolWHERE bids[1,1]=107.03 -- Highly selective filter; The execution plan will show a `Filtered AsOf Join Fast` operator, confirming the binary search strategy is being used. SelectedRecord    Filter filter: market_data.bids[1,1]=107.03        AsOf Join Fast          condition: market_data.symbol=core_price.symbol            PageFrame                Row forward scan                Frame forward scan on: core_price            PageFrame                Row forward scan                Frame forward scan on: market_data #### Hinted Execution Plan (Full Scan)[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#hinted-execution-plan-full-scan "Direct link to Hinted Execution Plan (Full Scan)") When you use the `asof_linear` hint, the plan changes. Observing execution plan with asof\_linear query hint[Demo this query](https://demo.questdb.io/?query=EXPLAIN%20SELECT%20%2F*%2B%20asof_linear(core_price%20market_data)%20*%2F%0A%20%20*%0AFROM%20core_price%0AASOF%20JOIN%20market_data%0AON%20symbol%0AWHERE%20bids%5B1%2C1%5D%3D107.03%20--%20Highly%20selective%20filter%0A%3B&executeQuery=true) EXPLAIN SELECT /*+ asof_linear(core_price market_data) */ *FROM core_priceASOF JOIN market_dataON symbolWHERE bids[1,1]=107.03 -- Highly selective filter; The execution plan will now show the `AsOf Join Light` operator and a separate, preceding filtering step on the joined table. SelectedRecord    Filter filter: market_data.bids[1,1]=107.03        AsOf Join Light          condition: market_data.symbol=core_price.symbol            PageFrame                Row forward scan                Frame forward scan on: core_price            PageFrame                Row forward scan                Frame forward scan on: market_data * * * ### Algorithms compared on an example[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#algorithms-compared-on-an-example "Direct link to Algorithms compared on an example") Let's use the diagram below to explain the key differences among algorithms. It shows two tables, LHS and RHS. We show the rows aligned on timestamp, so there are gaps in the LHS column. These gaps don't represent any LHS rows, it is just the way we visualize the two tables. The example assumes a JOIN condition on a symbol column. We show the values of that column in the table: row | LHS | RHS----|-----|---- 1 | | G 2 | | C 3 | | G 4 | | A 5 | | F 6 | A | B 7 | | D 8 | | B 9 | C | G10 | | F11 | | D12 | B | E13 | | D14 | | C15 | A | B Since the match for each LHS row occurs in the RHS table at a time earlier than the previous LHS row, the join pattern is "distant matching". #### Light algo[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#light-algo "Direct link to Light algo") Light algo uses a forward-only scan of the RHS table. When matching the first RHS symbol (row 6, symbol A), it starts from RHS row 1, and proceeds all the way to row 6, collecting all the symbols into a hashtable. When done, it looks up symbol A in the hashtable and finds the prevailing RHS row is row 4. When matching the next RHS symbol (row 9, symbol C), it resumes the forward scan, touching rows 7, 8 and 9. Then it looks up symbol C, and finds the prevailing row is row 2. #### Fast algo[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#fast-algo "Direct link to Fast algo") Fast algo uses binary search over RHS timestamps to zero in on row 6 as the most recent row not newer than the first LHS row. Then it scans backward: rows 6, 5, 4, and there it finds the matching symbol A. When matching the next LHS symbol (row 9, symbol C), it uses binary search to zero in on RHS row 9, then scans all the way back to row 2, where it finds symbol C. When matching symbol A in row LHS row 15, it uses binary search to zero in or RHS row 15, then scans backward, again all the way back to row 4. There's also an optimization that avoids the fixed cost of binary search by first searching linearly for the matching timestamp in the RHS row, for a smallish number of steps. This doesn't affect the backward search for the symbol. #### Memoized algo[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#memoized-algo "Direct link to Memoized algo") The Memoized algo is a variant of the Fast algo. It uses the exact same linear/binary search to find the matching timestamp in the RHS, and then uses the same backward search for the symbol. However, it memorizes for each symbol where it started the backward search, and where it found it. In our example, this means it handles the first LHS row (6) exactly the same way, scanning backward to row 4. But when it encounters the same symbol A in row 15, it scans backward only until reaching row 6, and then directly uses the remembered result of the previous scan, and matches up with row 4. #### Dense algo[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#dense-algo "Direct link to Dense algo") The Dense algo starts like the Fast algo, performing a binary search to zero in on RHS row 6 and searching backward to find symbol A in row 4 of RHS. From then on, it behaves more like the Light algo. To match up LHS row 9 (symbol C), it first does a linear scan forward from row 6 to row 9 (exactly like the Light algo). Since it didn't find C in this scan, it resumes the backward scan, touching rows 3 and 2, and there it finds the symbol C. At LHS row 12 (symbol B), it resumes the forward scan, touching rows 10, 11, and 12. Then it finds symbol B in the hashtable, getting row 8 as the prevailing row. No backward scan nedeed here. At LHS row 15 (symbol A), it resumes the forward scan, touching rows 13, 14, and 15. Then it looks up symbol A in the hashtable of the forward scan, finding nothing. Then it looks up symbol A in the hashtable of the backward scan, and finds it there. The prevailing row is number 4. Again, no backward search was needed. #### Discussion[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#discussion "Direct link to Discussion") As expected for distant matching, the Fast and Memoized algos had to touch the most rows. Especially, when matching row 15, Fast algo had to scan backward to row 4, and Memoized did only slighly better, scanning until row 6. Light algo had to initially scan all the history (rows 1 to 6), but from then on, it only needed to touch the additional rows that came into scope as the LHS timestamp was moving on. Dense algo had the same advantage as Light, but it didn't have to scan all the history. It scanned only as far back into history as needed to find the most recent occurence of a symbol not yet seen in the forward scan. ### RAM considerations[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#ram-considerations "Direct link to RAM considerations") The Fast algorithm is the only one that doesn't use any RAM to store the results of scanning. It is purely search-based, giving it an additional advantage when your symbol set is high-cardinality. * * * Deprecated hints[​](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#deprecated-hints "Direct link to Deprecated hints") ----------------------------------------------------------------------------------------------------------------------------------------- * `avoid_asof_binary_search` * superseded by `asof_linear` * `avoid_lt_binary_search` * superseded by `asof_linear` * `asof_linear_search` * superseded by `asof_linear` * `asof_index_search` * superseded by `asof_index` * `asof_memoized_search` * superseded by `asof_memoized` * [Hint Syntax](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#hint-syntax) * [Temporal JOIN hints](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#temporal-join-hints) * [1\. Localized matching](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#1-localized-matching) * [2\. Distant matching](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#2-distant-matching) * [ASOF JOIN algorithms](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#asof-join-algorithms) * [List of hints](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#list-of-hints) * [`asof_dense(l r)`](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#asof_densel-r) * [`asof_linear(l r)`](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#asof_linearl-r) * [`asof_memoized(l r)`](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#asof_memoizedl-r) * [Check the Execution Plan](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#check-the-execution-plan) * [Algorithms compared on an example](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#algorithms-compared-on-an-example) * [RAM considerations](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#ram-considerations) * [Deprecated hints](https://questdb.com/docs/concepts/deep-dive/sql-optimizer-hints/#deprecated-hints) --- # Deduplication | QuestDB On this page Deduplication ensures that only one row exists for a given set of key columns. When a new row matches an existing row's keys, the old row is replaced. ![Animation showing how deduplication handles incoming rows - inserting new keys, replacing duplicates, and skipping identical rows](https://questdb.com/docs/images/docs/concepts/deduplication.svg) When to use deduplication[​](https://questdb.com/docs/concepts/deduplication/#when-to-use-deduplication "Direct link to When to use deduplication") ---------------------------------------------------------------------------------------------------------------------------------------------------- **Use deduplication when:** * You need idempotent writes (safe to retry or resend data) * You're reloading data that may have changed (e.g., third-party data feeds) * You want "last write wins" behavior for a given key * You're recovering from ingestion errors and need to resend a time range **Skip deduplication when:** * Your timestamps are always unique (no duplicates possible) * You're doing append-only logging where duplicates are acceptable * Write performance is critical and you're certain duplicates won't occur Quick example[​](https://questdb.com/docs/concepts/deduplication/#quick-example "Direct link to Quick example") ---------------------------------------------------------------------------------------------------------------- CREATE TABLE prices ( ts TIMESTAMP, ticker SYMBOL, price DOUBLE) TIMESTAMP(ts) PARTITION BY DAY WALDEDUP UPSERT KEYS(ts, ticker); With this configuration, each `(ts, ticker)` combination can have only one row: INSERT INTO prices VALUES ('2026-01-15T10:00:00', 'AAPL', 185.50);INSERT INTO prices VALUES ('2026-01-15T10:00:00', 'AAPL', 186.00); -- replaces previousSELECT * FROM prices; | ts | ticker | price | | --- | --- | --- | | 2026-01-15T10:00:00 | AAPL | 186.00 | Only the last value is kept. How it works[​](https://questdb.com/docs/concepts/deduplication/#how-it-works "Direct link to How it works") ------------------------------------------------------------------------------------------------------------- When deduplication is enabled, QuestDB: 1. Checks if incoming rows match existing rows by UPSERT KEYS 2. If keys match, compares the full row content 3. If the row is identical, skips the write entirely (no disk I/O) 4. If the row differs, replaces the old row with the new one This full-row comparison significantly reduces write amplification when reloading large datasets where only a small portion has changed — common when consuming third-party data feeds that provide full snapshots. Performance[​](https://questdb.com/docs/concepts/deduplication/#performance "Direct link to Performance") ---------------------------------------------------------------------------------------------------------- Deduplication has minimal overhead when: * Timestamps are mostly unique across rows * Data arrives in roughly time-ordered fashion Deduplication is more expensive when: * Many rows share the same timestamp * Deduplication keys have high cardinality The full-row check optimization means that reloading unchanged data is cheap — QuestDB detects identical rows and skips unnecessary writes. Configuration[​](https://questdb.com/docs/concepts/deduplication/#configuration "Direct link to Configuration") ---------------------------------------------------------------------------------------------------------------- ### Create table with deduplication[​](https://questdb.com/docs/concepts/deduplication/#create-table-with-deduplication "Direct link to Create table with deduplication") CREATE TABLE prices ( ts TIMESTAMP, ticker SYMBOL, price DOUBLE) TIMESTAMP(ts) PARTITION BY DAY WALDEDUP UPSERT KEYS(ts, ticker); The designated timestamp must always be included in UPSERT KEYS. ### Enable on existing table[​](https://questdb.com/docs/concepts/deduplication/#enable-on-existing-table "Direct link to Enable on existing table") ALTER TABLE prices DEDUP ENABLE UPSERT KEYS(ts, ticker); ### Disable deduplication[​](https://questdb.com/docs/concepts/deduplication/#disable-deduplication "Direct link to Disable deduplication") ALTER TABLE prices DEDUP DISABLE; ### Change UPSERT KEYS[​](https://questdb.com/docs/concepts/deduplication/#change-upsert-keys "Direct link to Change UPSERT KEYS") ALTER TABLE prices DEDUP ENABLE UPSERT KEYS(ts, ticker, exchange); Checking configuration[​](https://questdb.com/docs/concepts/deduplication/#checking-configuration "Direct link to Checking configuration") ------------------------------------------------------------------------------------------------------------------------------------------- Check if deduplication is enabled: SELECT dedup FROM tables() WHERE table_name = 'prices'; Check which columns are UPSERT KEYS: SELECT "column", upsertKey FROM table_columns('prices'); Requirements[​](https://questdb.com/docs/concepts/deduplication/#requirements "Direct link to Requirements") ------------------------------------------------------------------------------------------------------------- * Deduplication requires [WAL tables](https://questdb.com/docs/concepts/write-ahead-log/) * The designated timestamp must be included in UPSERT KEYS * Enabling deduplication does not deduplicate existing data — only new inserts See also[​](https://questdb.com/docs/concepts/deduplication/#see-also "Direct link to See also") ------------------------------------------------------------------------------------------------- * [CREATE TABLE ... DEDUP](https://questdb.com/docs/query/sql/create-table/#deduplication) * [ALTER TABLE DEDUP ENABLE](https://questdb.com/docs/query/sql/alter-table-enable-deduplication/) * [ALTER TABLE DEDUP DISABLE](https://questdb.com/docs/query/sql/alter-table-disable-deduplication/) * [When to use deduplication](https://questdb.com/docs/concepts/deduplication/#when-to-use-deduplication) * [Quick example](https://questdb.com/docs/concepts/deduplication/#quick-example) * [How it works](https://questdb.com/docs/concepts/deduplication/#how-it-works) * [Performance](https://questdb.com/docs/concepts/deduplication/#performance) * [Configuration](https://questdb.com/docs/concepts/deduplication/#configuration) * [Create table with deduplication](https://questdb.com/docs/concepts/deduplication/#create-table-with-deduplication) * [Enable on existing table](https://questdb.com/docs/concepts/deduplication/#enable-on-existing-table) * [Disable deduplication](https://questdb.com/docs/concepts/deduplication/#disable-deduplication) * [Change UPSERT KEYS](https://questdb.com/docs/concepts/deduplication/#change-upsert-keys) * [Checking configuration](https://questdb.com/docs/concepts/deduplication/#checking-configuration) * [Requirements](https://questdb.com/docs/concepts/deduplication/#requirements) * [See also](https://questdb.com/docs/concepts/deduplication/#see-also) --- # Symbol | QuestDB On this page `SYMBOL` is a data type designed for columns with repetitive string values. Internally, symbols use dictionary encoding—each unique string is stored once in a lookup table, and rows store integer references to that table. This is the same approach used by columnar formats like Parquet and Arrow. The result is much faster filtering and grouping compared to regular strings. When to use SYMBOL[​](https://questdb.com/docs/concepts/symbol/#when-to-use-symbol "Direct link to When to use SYMBOL") ------------------------------------------------------------------------------------------------------------------------ Use `SYMBOL` for categorical data with a limited set of repeated values: * Stock tickers (`AAPL`, `GOOGL`, `MSFT`) * Country or region codes (`US`, `EU`, `APAC`) * Status values (`pending`, `completed`, `failed`) * Device or sensor IDs * Any column frequently used in `WHERE` or `GROUP BY` CREATE TABLE trades ( timestamp TIMESTAMP, symbol SYMBOL, -- Good: limited set of tickers side SYMBOL, -- Good: just BUY/SELL price DOUBLE, quantity DOUBLE) TIMESTAMP(timestamp) PARTITION BY DAY; When to use VARCHAR instead[​](https://questdb.com/docs/concepts/symbol/#when-to-use-varchar-instead "Direct link to When to use VARCHAR instead") --------------------------------------------------------------------------------------------------------------------------------------------------- Use `VARCHAR` when values are unique or very high cardinality: * User-generated text (comments, descriptions) * Log messages * UUIDs or unique identifiers (consider the `UUID` type instead) * Columns with hundreds of millions of distinct values Why SYMBOL is fast[​](https://questdb.com/docs/concepts/symbol/#why-symbol-is-fast "Direct link to Why SYMBOL is fast") ------------------------------------------------------------------------------------------------------------------------ | Operation | VARCHAR | SYMBOL | | --- | --- | --- | | Storage | Full string per row | Integer + shared dictionary | | Filtering (`WHERE symbol = 'X'`) | String comparison | Integer comparison | | Grouping (`GROUP BY`) | String hashing | Integer grouping | | Disk usage | Higher | Lower | Symbols provide: * **Faster queries** — integer comparisons instead of string operations * **Lower storage** — strings stored once in a dictionary, rows store integers * **Index support** — symbol columns can be indexed for even faster lookups Creating SYMBOL columns[​](https://questdb.com/docs/concepts/symbol/#creating-symbol-columns "Direct link to Creating SYMBOL columns") --------------------------------------------------------------------------------------------------------------------------------------- CREATE TABLE orders ( timestamp TIMESTAMP, symbol SYMBOL, side SYMBOL, order_type SYMBOL, price DOUBLE) TIMESTAMP(timestamp) PARTITION BY DAY; Symbol capacity scales automatically as new values are added. No manual configuration is needed. Note for users upgrading from versions before 9.0.0 Prior to QuestDB 9.0.0, symbol capacity required manual configuration. You had to estimate the number of distinct values upfront and set the capacity explicitly. Undersizing caused performance issues; oversizing wasted memory. From 9.0.0 onwards, symbol capacity is fully automatic. The `CAPACITY` setting is now obsolete and can be removed from your table definitions. NOCACHE option[​](https://questdb.com/docs/concepts/symbol/#nocache-option "Direct link to NOCACHE option") ------------------------------------------------------------------------------------------------------------ By default, QuestDB caches the symbol dictionary in memory for fast lookups. For columns with very high cardinality (10 million+ distinct values), this cache can consume significant memory. Use `NOCACHE` to disable dictionary caching: CREATE TABLE trades ( timestamp TIMESTAMP, client_id SYMBOL NOCACHE, symbol SYMBOL) TIMESTAMP(timestamp) PARTITION BY DAY; **Trade-off:** `NOCACHE` reduces memory usage but makes dictionary lookups slower. Only use it for symbols with millions of distinct values where memory is a concern. To toggle caching on an existing column: -- Disable cacheALTER TABLE trades ALTER COLUMN client_id NOCACHE;-- Re-enable cacheALTER TABLE trades ALTER COLUMN client_id CACHE; Indexing symbols[​](https://questdb.com/docs/concepts/symbol/#indexing-symbols "Direct link to Indexing symbols") ------------------------------------------------------------------------------------------------------------------ For columns frequently used in `WHERE` clauses, add an index: CREATE TABLE trades ( timestamp TIMESTAMP, symbol SYMBOL INDEX, price DOUBLE) TIMESTAMP(timestamp) PARTITION BY DAY; Or add an index later: ALTER TABLE trades ALTER COLUMN symbol ADD INDEX; See [Indexes](https://questdb.com/docs/concepts/deep-dive/indexes/) for more information. * [When to use SYMBOL](https://questdb.com/docs/concepts/symbol/#when-to-use-symbol) * [When to use VARCHAR instead](https://questdb.com/docs/concepts/symbol/#when-to-use-varchar-instead) * [Why SYMBOL is fast](https://questdb.com/docs/concepts/symbol/#why-symbol-is-fast) * [Creating SYMBOL columns](https://questdb.com/docs/concepts/symbol/#creating-symbol-columns) * [NOCACHE option](https://questdb.com/docs/concepts/symbol/#nocache-option) * [Indexing symbols](https://questdb.com/docs/concepts/symbol/#indexing-symbols) --- # Time Partitions | QuestDB On this page QuestDB partitions tables by time intervals, storing each interval's data in a separate directory. This physical separation is fundamental to time-series performance - it allows the database to skip irrelevant time ranges entirely during queries and enables efficient data lifecycle management. Why partition[​](https://questdb.com/docs/concepts/partitions/#why-partition "Direct link to Why partition") ------------------------------------------------------------------------------------------------------------- Partitioning provides significant benefits for time-series workloads: * **Query performance**: The SQL optimizer skips partitions outside your query's time range. A query for "last hour" on a table with years of data reads only one partition, not the entire table. * **Data lifecycle**: Drop old data instantly with [DROP PARTITION](https://questdb.com/docs/query/sql/alter-table-drop-partition/) - no expensive DELETE operations. Detach partitions to cold storage, reattach when needed. * **Write efficiency**: Out-of-order data only rewrites affected partitions, not the entire table. Smaller partitions mean less write amplification. * **Concurrent access**: Different partitions can be written and read simultaneously without contention. How partitions work[​](https://questdb.com/docs/concepts/partitions/#how-partitions-work "Direct link to How partitions work") ------------------------------------------------------------------------------------------------------------------------------- Partitioning requires a [designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) column. QuestDB uses this timestamp to determine which partition stores each row. ![Diagram showing how table data is organized into time-based partition directories, each containing column files](https://questdb.com/docs/images/docs/concepts/partitionModel.svg) Each partition is a directory on disk named by its time interval. Inside, each column is stored as a separate file (`.d` for data, plus index files for [SYMBOL](https://questdb.com/docs/concepts/symbol/) columns). Choosing a partition interval[​](https://questdb.com/docs/concepts/partitions/#choosing-a-partition-interval "Direct link to Choosing a partition interval") ------------------------------------------------------------------------------------------------------------------------------------------------------------- Available intervals: `HOUR`, `DAY`, `WEEK`, `MONTH`, `YEAR`, or `NONE`. **Target 30-80 million rows per partition** for tables with average-sized rows. Tables with many columns should aim for the lower end; tables with few columns can go higher. Choose your interval based on how much data you ingest: | Your data volume | Recommended interval | | --- | --- | | \>1 billion rows/day | `HOUR` | | 30-500 million rows/day | `DAY` | | 5-30 million rows/day | `WEEK` | | 1-5 million rows/day | `MONTH` | | <1 million rows/day | `YEAR` | **Why this matters:** * **Too many small partitions** increases syscall overhead. Each partition is a directory, and operations like queries and compaction must interact with many filesystem objects. * **Too few large partitions** can hurt out-of-order write performance. When late data arrives, QuestDB may need to rewrite portions of the partition. Smaller partitions limit how much data gets rewritten in worst-case scenarios. **Other considerations:** * Match your most common query patterns (if you typically query by day, `DAY` partitions align well) * You can change partitioning later, but it requires recreating the table For ILP (InfluxDB Line Protocol) ingestion, the default is `DAY`. Change it via `line.default.partition.by` in `server.conf`. Creating partitioned tables[​](https://questdb.com/docs/concepts/partitions/#creating-partitioned-tables "Direct link to Creating partitioned tables") ------------------------------------------------------------------------------------------------------------------------------------------------------- Specify partitioning at table creation: CREATE TABLE trades ( ts TIMESTAMP, symbol SYMBOL, price DOUBLE, amount DOUBLE) TIMESTAMP(ts) PARTITION BY DAY; ### Default behavior by creation method[​](https://questdb.com/docs/concepts/partitions/#default-behavior-by-creation-method "Direct link to Default behavior by creation method") | Creation method | Default partition | | --- | --- | | SQL `CREATE TABLE` (no `PARTITION BY`) | `NONE` | | SQL `CREATE TABLE` (with `PARTITION BY`) | As specified | | ILP auto-created tables | `DAY` | ### Partition directory naming[​](https://questdb.com/docs/concepts/partitions/#partition-directory-naming "Direct link to Partition directory naming") | Interval | Directory format | Example | | --- | --- | --- | | `HOUR` | `YYYY-MM-DDTHH` | `2026-01-15T09` | | `DAY` | `YYYY-MM-DD` | `2026-01-15` | | `WEEK` | `YYYY-Www` | `2026-W03` | | `MONTH` | `YYYY-MM` | `2026-01` | | `YEAR` | `YYYY` | `2026` | Inspecting partitions[​](https://questdb.com/docs/concepts/partitions/#inspecting-partitions "Direct link to Inspecting partitions") ------------------------------------------------------------------------------------------------------------------------------------- Use `SHOW PARTITIONS` or the `table_partitions()` function: SHOW PARTITIONS FROM trades; | index | partitionBy | name | minTimestamp | maxTimestamp | numRows | diskSizeHuman | | --- | --- | --- | --- | --- | --- | --- | | 0 | DAY | 2026-01-15 | 2026-01-15T00:00:00Z | 2026-01-15T23:59:59Z | 1440000 | 68.0 MiB | | 1 | DAY | 2026-01-16 | 2026-01-16T00:00:00Z | 2026-01-16T12:30:00Z | 750000 | 35.2 MiB | The `table_partitions()` function returns the same data and can be used in queries with `WHERE`, `JOIN`, or `UNION`: SELECT name, numRows, diskSizeHumanFROM table_partitions('trades')WHERE numRows > 1000000; Storage on disk[​](https://questdb.com/docs/concepts/partitions/#storage-on-disk "Direct link to Storage on disk") ------------------------------------------------------------------------------------------------------------------- A partitioned table's directory structure: db/trades/├── 2026-01-15/ # Partition directory│ ├── ts.d # Timestamp column data│ ├── symbol.d # Symbol column data│ ├── symbol.k # Symbol column index│ ├── symbol.v # Symbol column values│ ├── price.d # Price column data│ └── amount.d # Amount column data├── 2026-01-16/│ ├── ts.d│ ├── ...└── _txn # Transaction metadata Partition splitting and squashing[​](https://questdb.com/docs/concepts/partitions/#partition-splitting-and-squashing "Direct link to Partition splitting and squashing") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- When out-of-order data arrives for an existing partition, QuestDB may split that partition to avoid rewriting all its data. This is an optimization for write performance. A split occurs when: * The existing partition prefix is larger than the new data plus suffix * The prefix exceeds `cairo.o3.partition.split.min.size` (default: 50MB) Split partitions appear with timestamp suffixes in `SHOW PARTITIONS`: | name | numRows | | --- | --- | | 2026-01-15 | 1259999 | | 2026-01-15T205959-880001 | 60002 | QuestDB automatically squashes splits: * Non-active partitions: squashed at end of each commit * Active (latest) partition: squashed when splits exceed `cairo.o3.last.partition.max.splits` (default: 20) To manually squash all splits: ALTER TABLE trades SQUASH PARTITIONS; Partition operations (`ATTACH`, `DETACH`, `DROP`) treat all splits of a partition as a single unit. See also[​](https://questdb.com/docs/concepts/partitions/#see-also "Direct link to See also") ---------------------------------------------------------------------------------------------- * [Designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) — Required for partitioning * [DROP PARTITION](https://questdb.com/docs/query/sql/alter-table-drop-partition/) — Remove old partitions * [DETACH PARTITION](https://questdb.com/docs/query/sql/alter-table-detach-partition/) — Move to cold storage * [ATTACH PARTITION](https://questdb.com/docs/query/sql/alter-table-attach-partition/) — Restore detached data * [TTL](https://questdb.com/docs/concepts/ttl/) — Automatic partition cleanup by age * [Why partition](https://questdb.com/docs/concepts/partitions/#why-partition) * [How partitions work](https://questdb.com/docs/concepts/partitions/#how-partitions-work) * [Choosing a partition interval](https://questdb.com/docs/concepts/partitions/#choosing-a-partition-interval) * [Creating partitioned tables](https://questdb.com/docs/concepts/partitions/#creating-partitioned-tables) * [Default behavior by creation method](https://questdb.com/docs/concepts/partitions/#default-behavior-by-creation-method) * [Partition directory naming](https://questdb.com/docs/concepts/partitions/#partition-directory-naming) * [Inspecting partitions](https://questdb.com/docs/concepts/partitions/#inspecting-partitions) * [Storage on disk](https://questdb.com/docs/concepts/partitions/#storage-on-disk) * [Partition splitting and squashing](https://questdb.com/docs/concepts/partitions/#partition-splitting-and-squashing) * [See also](https://questdb.com/docs/concepts/partitions/#see-also) --- # JIT compiler | QuestDB On this page QuestDB includes a JIT compiler which is run on queries (and sub-queries) that perform a full scan over a table or table partitions. The main goal behind this feature is to improve performance for filters with arithmetical expressions. To do so, the JIT compiler emits machine code with a single function that may also use SIMD (vector) instructions. For details on the implementation, motivation, and internals of this feature, see our [article about SQL JIT compilation](https://questdb.com/blog/2022/01/12/jit-sql-compiler) . This post describes our storage model, how we built a JIT compiler for SQL and our plans for improving it in future. Queries eligible for JIT compilation[​](https://questdb.com/docs/concepts/deep-dive/jit-compiler/#queries-eligible-for-jit-compilation "Direct link to Queries eligible for JIT compilation") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The types of queries that are eligible for performance improvements via JIT compilation are those which contain `WHERE` clauses. Here are some examples you can execute on the [QuestDB Public Demo Datasets](https://demo.questdb.io/) : basic filtering in WHERE clauses with JIT[Demo this query](https://demo.questdb.io/?query=--%20basic%20filtering%20in%20WHERE%20clauses%0ASELECT%20count()%2C%20max(bid_price)%20FROM%20core_price%0AWHERE%0Atimestamp%20in%20today()%20AND%20bid_price%20%3E%201%20AND%20Symbol%20%3D%20%27EURUSD%27%3B&executeQuery=true) -- basic filtering in WHERE clausesSELECT count(), max(bid_price) FROM core_priceWHEREtimestamp in today() AND bid_price > 1 AND Symbol = 'EURUSD'; Filtering and aggregating with JIT[Demo this query](https://demo.questdb.io/?query=--%20sub-queries%0AEXPLAIN%20SELECT%20symbol%2C%20count()%2C%20max(bid_price)%20FROM%20core_price%0AWHERE%20timestamp%20in%20today()%20AND%20bid_price%20%3E%201%3B&executeQuery=true) -- sub-queriesEXPLAIN SELECT symbol, count(), max(bid_price) FROM core_priceWHERE timestamp in today() AND bid_price > 1; JIT compiler usage[​](https://questdb.com/docs/concepts/deep-dive/jit-compiler/#jit-compiler-usage "Direct link to JIT compiler usage") ---------------------------------------------------------------------------------------------------------------------------------------- The JIT compiler is enabled by default for QuestDB 6.3 onwards. If you wish to disable it, change the `cairo.sql.jit.mode` setting in the [server configuration](https://questdb.com/docs/configuration/overview/) file from `on` to `off`: path/to/server.conf cairo.sql.jit.mode=off Embedded API users are able to enable or disable the compiler globally by providing their `CairoConfiguration` implementation. Alternatively, JIT compilation can be changed for a single query by using the `SqlExecutionContext#setJitMode` method. The latter may look like the following: final CairoConfiguration configuration = new DefaultCairoConfiguration(temp.getRoot().getAbsolutePath());try (CairoEngine engine = new CairoEngine(configuration)) { final SqlExecutionContextImpl ctx = new SqlExecutionContextImpl(engine, 1); // Enable SQL JIT compiler ctx.setJitMode(SqlJitMode.JIT_MODE_ENABLED); // Subsequent query execution (called as usual) with have JIT enabled try (SqlCompiler compiler = new SqlCompiler(engine)) { try (RecordCursorFactory factory = compiler.compile("abc", ctx).getRecordCursorFactory()) { try (RecordCursor cursor = factory.getCursor(ctx)) { // ... } } }} Server logs should contain references to `SQL JIT compiler mode`: 2021-12-16T09:25:34.472450Z A server-main SQL JIT compiler mode: on Due to certain limitations noted below, JIT compilation won't take place for all queries. To understand whether JIT compilation took place for a query, one will see something similar in the server logs: 2021-12-16T09:35:01.738910Z I i.q.g.SqlCompiler plan [q=`select-group-by count() count from (select [usage_user] from cpu timestamp (timestamp) where usage_user > 75)`, fd=73]2021-12-16T09:35:01.742777Z I i.q.g.SqlCodeGenerator JIT enabled for (sub)query [tableName=cpu, fd=73] Known limitations[​](https://questdb.com/docs/concepts/deep-dive/jit-compiler/#known-limitations "Direct link to Known limitations") ------------------------------------------------------------------------------------------------------------------------------------- The current implementation of the JIT SQL compiler has a number of limitations: * Only x86-64 CPUs are currently supported. * Vectorized filter execution requires AVX2 instruction set. * Filters with any SQL function, such as `now()`, or `abs()`, or `round()`, are not supported. * Filters with any pseudo-function or operator, such as `in()` on symbol column, or `between` on non-designated timestamp column, or `within` on geohash column, are not supported. * Only the following arithmetic operations are allowed to be present in the filter: `+`, `-`, `*`, `/`. * Only filters with fixed-size columns are supported: BOOLEAN, BYTE, GEOBYTE, SHORT, GEOSHORT, CHAR, INT, GEOINT, SYMBOL, FLOAT, LONG, GEOLONG, DATE, TIMESTAMP, DOUBLE, UUID. * [Queries eligible for JIT compilation](https://questdb.com/docs/concepts/deep-dive/jit-compiler/#queries-eligible-for-jit-compilation) * [JIT compiler usage](https://questdb.com/docs/concepts/deep-dive/jit-compiler/#jit-compiler-usage) * [Known limitations](https://questdb.com/docs/concepts/deep-dive/jit-compiler/#known-limitations) --- # Working with time zones | QuestDB On this page QuestDB stores all timestamps in UTC without time zone information. To query data at your local time, use [TICK syntax](https://questdb.com/docs/query/operators/tick/) . To display results in local time, use conversion functions. Key Points * All timestamps are stored in UTC — no time zone information is preserved * Use [TICK syntax](https://questdb.com/docs/query/operators/tick/) with `@timezone` to query data at your local time * Prefer full time zone names (`America/New_York`) over abbreviations (`EST`) * Use `to_timezone()` only when displaying local time in results How to refer to time zones[​](https://questdb.com/docs/concepts/timestamps-timezones/#how-to-refer-to-time-zones "Direct link to How to refer to time zones") -------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB uses the [IANA tz database](https://en.wikipedia.org/wiki/Tz_database) . Specify time zones by geographic region or UTC offset: | Format | Example | Recommended? | | --- | --- | --- | | Geographic region | `America/New_York` | ✅ Best | | UTC offset | `+02:00`, `-05:00` | ✅ Good | | Abbreviation | `EST`, `CST` | ⚠️ Avoid | **Avoid abbreviations** — the same abbreviation often maps to multiple time zones. For example, `CST` could mean U.S. Central Standard Time or China Standard Time. QuestDB can only recognize one, leading to unexpected results. For valid time zone names, see the [IANA time zone database](https://www.iana.org/time-zones) . note The tz database includes historic transitions. QuestDB applies the correct offset based on the timestamp value, accounting for historical daylight saving time changes. Querying by local time[​](https://questdb.com/docs/concepts/timestamps-timezones/#querying-by-local-time "Direct link to Querying by local time") -------------------------------------------------------------------------------------------------------------------------------------------------- You're in New York and want trades from 9am your time. Use [TICK syntax](https://questdb.com/docs/query/operators/tick/) with `@timezone`: SELECT * FROM tradesWHERE ts IN '2024-01-15T09:00@America/New_York;1h'; TICK converts your local time to UTC intervals, enabling efficient [interval scans](https://questdb.com/docs/concepts/deep-dive/interval-scan/) . More examples: -- London business hours (09:00-17:00) for January workdaysSELECT * FROM tradesWHERE ts IN '2024-01-[01..31]T09:00@Europe/London#wd;8h';-- NYSE trading hours (09:30-16:00 Eastern)SELECT * FROM tradesWHERE ts IN '2024-01-[01..31]T09:30@America/New_York#wd;6h30m';-- Last 5 business days, Tokyo morning sessionSELECT * FROM tradesWHERE ts IN '[$today-5bd..$today-1bd]T09:00@Asia/Tokyo;2h30m'; TICK handles DST transitions automatically — a 9 AM start time in New York maps to different UTC times in winter vs summer. ### Why TICK instead of conversion functions[​](https://questdb.com/docs/concepts/timestamps-timezones/#why-tick-instead-of-conversion-functions "Direct link to Why TICK instead of conversion functions") TICK generates UTC intervals at query planning time, enabling binary search. Converting each row forces a full table scan: -- Efficient: interval scan (sub-millisecond on billions of rows)WHERE ts IN '2024-01-[01..31]T09:00@Europe/London;8h'-- Inefficient: full table scan (must read every row)WHERE extract(hour FROM to_timezone(ts, 'Europe/London')) BETWEEN 9 AND 17 Converting timestamps for display[​](https://questdb.com/docs/concepts/timestamps-timezones/#converting-timestamps-for-display "Direct link to Converting timestamps for display") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- When you need local time in query results (not filtering), use `to_timezone()`: SELECT to_timezone(ts, 'Europe/Berlin') as local_time, symbol, priceFROM tradesWHERE ts IN '2024-01-15'; | local\_time | symbol | price | | --- | --- | --- | | 2024-01-15T10:30:00.000000Z | BTC-USD | 42000 | ### to\_utc() for ingestion[​](https://questdb.com/docs/concepts/timestamps-timezones/#to_utc-for-ingestion "Direct link to to_utc() for ingestion") If source data arrives in local time, convert to UTC before storing: INSERT INTO tradesSELECT to_utc(local_ts, 'America/New_York'), symbol, priceFROM source_data; This ensures consistent ordering and avoids ambiguity during DST transitions. See also[​](https://questdb.com/docs/concepts/timestamps-timezones/#see-also "Direct link to See also") -------------------------------------------------------------------------------------------------------- * [TICK intervals](https://questdb.com/docs/query/operators/tick/) — Complete `@timezone` syntax reference * [Designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) — How timestamps define table structure * [Date/time functions](https://questdb.com/docs/query/functions/date-time/) — `to_timestamp()`, `to_utc()`, `to_timezone()` * [How to refer to time zones](https://questdb.com/docs/concepts/timestamps-timezones/#how-to-refer-to-time-zones) * [Querying by local time](https://questdb.com/docs/concepts/timestamps-timezones/#querying-by-local-time) * [Why TICK instead of conversion functions](https://questdb.com/docs/concepts/timestamps-timezones/#why-tick-instead-of-conversion-functions) * [Converting timestamps for display](https://questdb.com/docs/concepts/timestamps-timezones/#converting-timestamps-for-display) * [to\_utc() for ingestion](https://questdb.com/docs/concepts/timestamps-timezones/#to_utc-for-ingestion) * [See also](https://questdb.com/docs/concepts/timestamps-timezones/#see-also) --- # Materialized views | QuestDB On this page A materialized view is a special QuestDB table that stores the pre-computed results of a query. Unlike [regular views](https://questdb.com/docs/concepts/views/) , which compute their results at query time, materialized views persist their data to disk, making them particularly efficient for expensive aggregate queries that are run frequently. What are materialized views for?[​](https://questdb.com/docs/concepts/materialized-views/#what-are-materialized-views-for "Direct link to What are materialized views for?") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Let's say your application ingests trade data into a table like this: trades table CREATE TABLE trades ( symbol SYMBOL, side SYMBOL, price DOUBLE, amount DOUBLE, timestamp TIMESTAMP) TIMESTAMP(timestamp) PARTITION BY DAY; As your QuestDB instance grows from gigabytes to terabytes, aggregation queries become a bottleneck. A common pattern is using `SAMPLE BY` to bucket data by time - for example, calculating notional value (price × amount) by the minute: SAMPLE BY query[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20timestamp%2C%0A%20%20symbol%2C%0A%20%20side%2C%0A%20%20sum(price%20*%20amount)%20AS%20notional%0AFROM%20trades%0AWHERE%20timestamp%20IN%20today()%0ASAMPLE%20BY%201m%3B&executeQuery=true) SELECT timestamp, symbol, side, sum(price * amount) AS notionalFROM tradesWHERE timestamp IN today()SAMPLE BY 1m; Thanks to partition pruning, this query only scans today's data. But even so, aggregating millions of rows takes time - and dashboards or applications may run this query repeatedly. Materialized views solve this by pre-computing and storing the aggregated results. When new data arrives, only the new rows are processed incrementally. Querying the materialized view becomes a simple lookup rather than a re-aggregation, making dashboard refreshes near-instant. When you create a materialized view you register your time-based grouping query with the QuestDB database against a base table. ![sampling into a materialized view](https://questdb.com/docs/assets/images/mat-view-agg-275818c827b6d0f56aee4e02ea333c17.svg) Conceptually a materialized view is an on-disk table tied to a query: As you add new data to the base table, the materialized view will efficiently update itself. You can then query the materialized view as a regular table without the impact of a full table scan of the base table. Quick example[​](https://questdb.com/docs/concepts/materialized-views/#quick-example "Direct link to Quick example") --------------------------------------------------------------------------------------------------------------------- Create a materialized view that calculates 15-minute OHLC bars: Create a materialized view CREATE MATERIALIZED VIEW trades_ohlc_15m ASSELECT timestamp, symbol, first(price) AS open, max(price) AS high, min(price) AS low, last(price) AS close, sum(amount) AS volumeFROM tradesSAMPLE BY 15m; Query it like any table: Query the materialized view[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20trades_ohlc_15m%0AWHERE%20timestamp%20IN%20today()%3B&executeQuery=true) SELECT * FROM trades_ohlc_15mWHERE timestamp IN today(); That's it. The view refreshes incrementally as new data arrives in `trades`. Details on customization and options follow below. When to use materialized views[​](https://questdb.com/docs/concepts/materialized-views/#when-to-use-materialized-views "Direct link to When to use materialized views") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Materialized views are ideal for: * **Heavy aggregations over large datasets**: Queries that scan millions of rows * **Frequently accessed summaries**: Dashboard queries that run repeatedly * **Historical summaries**: Data that doesn't need real-time accuracy * **OHLC calculations**: Candlestick charts, time-bucketed analytics Use regular [views](https://questdb.com/docs/concepts/views/) instead when: * Query execution cost is acceptable for your workload * You need parameterized queries with `DECLARE` * You need patterns not supported by materialized views (e.g., data enrichment) * Storage cost is a concern (materialized views consume disk space) The key tradeoff: views execute the full query each time (multi-threaded, can be resource-intensive), while materialized views pre-compute results so queries become simple lookups. For dashboards with many concurrent users, running parallel aggregations doesn't scale - materialized views reduce this to O(1) reads on a smaller, pre-aggregated dataset. ### Not suited for: data enrichment[​](https://questdb.com/docs/concepts/materialized-views/#not-suited-for-data-enrichment "Direct link to Not suited for: data enrichment") Materialized views support JOINs, but `SAMPLE BY` (aggregation) is mandatory. This means you can enrich aggregated results with data from other tables, but you cannot keep raw (non-aggregated) rows while adding enrichment columns. For example, joining aggregated trades with instrument metadata works: Supported: aggregation with JOIN CREATE MATERIALIZED VIEW trades_with_metadata ASSELECT t.timestamp, t.symbol, m.description, sum(t.amount) AS volumeFROM trades tJOIN instruments m ON t.symbol = m.symbolSAMPLE BY 1h; But this pattern does not work: Not supported: enrichment without aggregation -- Users try this but it won't workCREATE MATERIALIZED VIEW enriched_trades ASSELECT t.timestamp, t.symbol, t.price, t.amount, h.hourly_vwap -- aggregated value from another tableFROM trades tASOF JOIN hourly_stats h ON t.symbol = h.symbol; The view cannot maintain a 1:1 row mapping with the base table. Also note: only changes to the base table (the one in `SAMPLE BY`) trigger a refresh. Changes to joined tables do not trigger updates. **Coming soon**: We are actively developing a new type of materialized view that will support data enrichment use cases. Stay tuned for updates. Creating a materialized view[​](https://questdb.com/docs/concepts/materialized-views/#creating-a-materialized-view "Direct link to Creating a materialized view") ------------------------------------------------------------------------------------------------------------------------------------------------------------------ ### Basic syntax[​](https://questdb.com/docs/concepts/materialized-views/#basic-syntax "Direct link to Basic syntax") The simplest form requires only a `SAMPLE BY` query: Basic materialized view CREATE MATERIALIZED VIEW trades_hourly ASSELECT timestamp, symbol, avg(price) AS avg_price, sum(amount) AS volumeFROM tradesSAMPLE BY 1h; For full syntax, see [CREATE MATERIALIZED VIEW](https://questdb.com/docs/query/sql/create-mat-view/) . ### Extended syntax[​](https://questdb.com/docs/concepts/materialized-views/#extended-syntax "Direct link to Extended syntax") For more control, use the extended syntax with parentheses: Extended syntax CREATE MATERIALIZED VIEW trades_ohlc_15mWITH BASE trades REFRESH IMMEDIATE AS ( SELECT timestamp, symbol, first(price) AS open, max(price) AS high, min(price) AS low, last(price) AS close, sum(amount) AS volume FROM trades SAMPLE BY 15m) PARTITION BY MONTH; This allows specifying: * `WITH BASE`: Explicit base table (required for JOINs) * `REFRESH`: Refresh strategy * `PARTITION BY`: Partitioning scheme * `TTL`: Data retention policy ### Naming conventions[​](https://questdb.com/docs/concepts/materialized-views/#naming-conventions "Direct link to Naming conventions") We recommend naming views with reference to the base table, purpose, and sample interval: * `trades_ohlc_15m` - trades table, OHLC purpose, 15-minute buckets * `sensors_avg_1h` - sensors table, averages, hourly buckets ### The query[​](https://questdb.com/docs/concepts/materialized-views/#the-query "Direct link to The query") Materialized views require a `SAMPLE BY` or time-based `GROUP BY` query. **Supported:** * Aggregate functions: `sum`, `avg`, `min`, `max`, `first`, `last`, `count` * `JOIN` with other tables (only the base table triggers refresh) * `WHERE` clauses **Not supported:** * `FILL` clause * `FROM-TO` clause * `ALIGN TO FIRST OBSERVATION` * Non-deterministic functions like `now()` or `rnd_uuid4()` Keep queries simple. Move complex transformations to queries that run on the materialized view. ### Refresh strategies[​](https://questdb.com/docs/concepts/materialized-views/#refresh-strategies "Direct link to Refresh strategies") #### IMMEDIATE (default)[​](https://questdb.com/docs/concepts/materialized-views/#immediate-default "Direct link to IMMEDIATE (default)") Incrementally updates the view when new data is inserted into the base table: CREATE MATERIALIZED VIEW my_viewREFRESH IMMEDIATE ASSELECT ... FROM base_table SAMPLE BY 1h; This is the recommended strategy for most use cases. Only new data is processed, minimizing write overhead. #### MANUAL[​](https://questdb.com/docs/concepts/materialized-views/#manual "Direct link to MANUAL") Requires explicit refresh via SQL: CREATE MATERIALIZED VIEW my_viewREFRESH MANUAL ASSELECT ... FROM base_table SAMPLE BY 1h; Refresh manually with: REFRESH MATERIALIZED VIEW my_view; #### EVERY interval[​](https://questdb.com/docs/concepts/materialized-views/#every-interval "Direct link to EVERY interval") Refreshes on a timer: CREATE MATERIALIZED VIEW my_viewREFRESH EVERY 5m ASSELECT ... FROM base_table SAMPLE BY 1h; #### PERIOD refresh[​](https://questdb.com/docs/concepts/materialized-views/#period-refresh "Direct link to PERIOD refresh") For data that arrives at fixed intervals (e.g., end-of-day prices): Period refresh CREATE MATERIALIZED VIEW trades_dailyREFRESH PERIOD (LENGTH 1d TIME ZONE 'Europe/London' DELAY 2h) ASSELECT timestamp, symbol, avg(price) AS avg_priceFROM tradesSAMPLE BY 1d; Or use compact syntax to match the `SAMPLE BY` interval: Period refresh matching SAMPLE BY CREATE MATERIALIZED VIEW trades_dailyREFRESH PERIOD (SAMPLE BY INTERVAL) ASSELECT timestamp, symbol, avg(price) AS avg_priceFROM tradesSAMPLE BY 1d; Period refresh reduces transaction overhead during intensive real-time ingestion. Change refresh strategy anytime with [`ALTER MATERIALIZED VIEW SET REFRESH`](https://questdb.com/docs/query/sql/alter-mat-view-set-refresh/) . ### Partitioning[​](https://questdb.com/docs/concepts/materialized-views/#partitioning "Direct link to Partitioning") Specify a partitioning scheme larger than the sampling interval: CREATE MATERIALIZED VIEW my_view AS ( SELECT timestamp, symbol, sum(amount) AS total_amount FROM trades SAMPLE BY 8h) PARTITION BY DAY; An `8h` sample fits nicely with `DAY` partitioning (3 buckets per partition). #### Default partitioning[​](https://questdb.com/docs/concepts/materialized-views/#default-partitioning "Direct link to Default partitioning") If omitted, partitioning is inferred from `SAMPLE BY`: | Interval | Default partitioning | | --- | --- | | \> 1 hour | `PARTITION BY YEAR` | | \> 1 minute | `PARTITION BY MONTH` | | <= 1 minute | `PARTITION BY DAY` | ### TTL (Time-To-Live)[​](https://questdb.com/docs/concepts/materialized-views/#ttl-time-to-live "Direct link to TTL (Time-To-Live)") Limit how much history the materialized view retains: Materialized view with TTL CREATE MATERIALIZED VIEW trades_hourly AS ( SELECT timestamp, symbol, avg(price) AS avg_price FROM trades SAMPLE BY 1h) PARTITION BY WEEK TTL 8 WEEKS; The view's TTL is independent of the base table's TTL. ### Initial refresh[​](https://questdb.com/docs/concepts/materialized-views/#initial-refresh "Direct link to Initial refresh") When created, materialized views start an **asynchronous full refresh**: * `CREATE MATERIALIZED VIEW` returns immediately * The view is queryable right away but **returns no data** until refresh completes * For large base tables, this may take significant time Check if the initial refresh is complete: SELECT view_name, view_status, refresh_base_table_txn, base_table_txnFROM materialized_views()WHERE view_name = 'your_view'; When `refresh_base_table_txn` equals `base_table_txn`, the view is fully populated. To defer initial refresh, use `DEFERRED`: CREATE MATERIALIZED VIEW my_viewREFRESH MANUAL DEFERRED ASSELECT ... FROM trades SAMPLE BY 1h; Querying materialized views[​](https://questdb.com/docs/concepts/materialized-views/#querying-materialized-views "Direct link to Querying materialized views") --------------------------------------------------------------------------------------------------------------------------------------------------------------- note The example `trades_ohlc_15m` view is available on our [demo](https://demo.questdb.io/) , and contains realtime crypto data - try it out! Materialized views support **all the same queries** as regular QuestDB tables: Query today's data[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20trades_ohlc_15m%0AWHERE%20timestamp%20IN%20today()%3B&executeQuery=true) SELECT * FROM trades_ohlc_15mWHERE timestamp IN today(); | timestamp | symbol | open | high | low | close | volume | | --- | --- | --- | --- | --- | --- | --- | | 2025-03-31T00:00:00.000000Z | ETH-USD | 1807.94 | 1813.32 | 1804.69 | 1808.58 | 1784.144071999995 | | 2025-03-31T00:00:00.000000Z | BTC-USD | 82398.4 | 82456.5 | 82177.6 | 82284.5 | 34.47331241 | | ... | ... | ... | ... | ... | ... | ... | ### Performance comparison[​](https://questdb.com/docs/concepts/materialized-views/#performance-comparison "Direct link to Performance comparison") Without a materialized view, aggregating 1 month of data: Direct query - slow[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20timestamp%2C%20symbol%2C%0A%20%20first(price)%20AS%20open%2C%20max(price)%20AS%20high%2C%0A%20%20min(price)%20AS%20low%2C%20last(price)%20AS%20close%2C%0A%20%20sum(amount)%20AS%20volume%0AFROM%20trades%0AWHERE%20timestamp%20%3E%20dateadd(%27M%27%2C%20-1%2C%20now())%0ASAMPLE%20BY%2015m%3B&executeQuery=true) SELECT timestamp, symbol, first(price) AS open, max(price) AS high, min(price) AS low, last(price) AS close, sum(amount) AS volumeFROM tradesWHERE timestamp > dateadd('M', -1, now())SAMPLE BY 15m; This takes hundreds of milliseconds, scanning tens of millions of rows. With the materialized view: Materialized view - fast[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20trades_ohlc_15m%0AWHERE%20timestamp%20%3E%20dateadd(%27M%27%2C%20-1%2C%20now())%3B&executeQuery=true) SELECT * FROM trades_ohlc_15mWHERE timestamp > dateadd('M', -1, now()); This returns in single-digit milliseconds. The data is pre-aggregated, so no aggregation work is needed at query time. Managing materialized views[​](https://questdb.com/docs/concepts/materialized-views/#managing-materialized-views "Direct link to Managing materialized views") --------------------------------------------------------------------------------------------------------------------------------------------------------------- ### Listing views[​](https://questdb.com/docs/concepts/materialized-views/#listing-views "Direct link to Listing views") List all materialized views[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20view_name%2C%0A%20%20base_table_name%2C%0A%20%20view_status%2C%0A%20%20last_refresh_finish_timestamp%0AFROM%20materialized_views()%3B&executeQuery=true) SELECT view_name, base_table_name, view_status, last_refresh_finish_timestampFROM materialized_views(); ### Monitoring refresh status[​](https://questdb.com/docs/concepts/materialized-views/#monitoring-refresh-status "Direct link to Monitoring refresh status") Check refresh lag SELECT view_name, refresh_base_table_txn, base_table_txn, base_table_txn - refresh_base_table_txn AS lagFROM materialized_views(); When `refresh_base_table_txn` equals `base_table_txn`, the view is fully up-to-date. ### View invalidation[​](https://questdb.com/docs/concepts/materialized-views/#view-invalidation "Direct link to View invalidation") Materialized views become invalid when their base table schema or data is modified in incompatible ways: * Dropping columns referenced by the view * Dropping partitions * Renaming the base table * `TRUNCATE` or `UPDATE` operations Check for invalid views: Find invalid views SELECT view_name, view_status, invalidation_reasonFROM materialized_views()WHERE view_status = 'invalid'; ### Refreshing an invalid view[​](https://questdb.com/docs/concepts/materialized-views/#refreshing-an-invalid-view "Direct link to Refreshing an invalid view") To restore an invalid view with a full refresh: REFRESH MATERIALIZED VIEW view_name FULL; This deletes existing data and rebuilds from the base table. For large tables, this may take significant time. Cancel with [`CANCEL QUERY`](https://questdb.com/docs/query/sql/cancel-query/) if needed. Advanced: LATEST ON optimization[​](https://questdb.com/docs/concepts/materialized-views/#advanced-latest-on-optimization "Direct link to Advanced: LATEST ON optimization") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- `LATEST ON` queries can be slow when some symbols are infrequently updated, requiring scans across large amounts of data: Slow LATEST ON[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20trades%20LATEST%20ON%20timestamp%20PARTITION%20BY%20symbol%3B&executeQuery=true) SELECT * FROM trades LATEST ON timestamp PARTITION BY symbol; This might scan billions of rows to find the latest entry for rarely-updated symbols. ### Solution: Pre-aggregate with a materialized view[​](https://questdb.com/docs/concepts/materialized-views/#solution-pre-aggregate-with-a-materialized-view "Direct link to Solution: Pre-aggregate with a materialized view") Create a view that stores one row per symbol per day: LATEST ON materialized view CREATE MATERIALIZED VIEW trades_latest_1d ASSELECT timestamp, symbol, side, last(price) AS price, last(amount) AS amount, last(timestamp) AS latestFROM tradesSAMPLE BY 1d; Then query the view: Fast LATEST ON[Demo this query](https://demo.questdb.io/?query=SELECT%20symbol%2C%20side%2C%20price%2C%20amount%2C%20latest%20AS%20timestamp%0AFROM%20(%0A%20%20trades_latest_1d%0A%20%20LATEST%20ON%20timestamp%0A%20%20PARTITION%20BY%20symbol%2C%20side%0A)%0AORDER%20BY%20timestamp%20DESC%3B&executeQuery=true) SELECT symbol, side, price, amount, latest AS timestampFROM ( trades_latest_1d LATEST ON timestamp PARTITION BY symbol, side)ORDER BY timestamp DESC; **Result**: Seconds down to milliseconds - 100x to 1000x faster. Instead of scanning ~1.3 billion rows, the database scans ~25,000 pre-aggregated rows. Technical reference[​](https://questdb.com/docs/concepts/materialized-views/#technical-reference "Direct link to Technical reference") --------------------------------------------------------------------------------------------------------------------------------------- ### Query constraints[​](https://questdb.com/docs/concepts/materialized-views/#query-constraints "Direct link to Query constraints") Materialized view queries: * Must use `SAMPLE BY` or `GROUP BY` with a designated timestamp column * Must not use `FROM-TO`, `FILL`, or `ALIGN TO FIRST OBSERVATION` * Must not use non-deterministic functions (`now()`, `rnd_uuid4()`) * Must use join conditions compatible with incremental refresh * When the base table uses [deduplication](https://questdb.com/docs/concepts/deduplication/) , non-aggregate columns must be a subset of the `DEDUP` keys ### Base table relationship[​](https://questdb.com/docs/concepts/materialized-views/#base-table-relationship "Direct link to Base table relationship") Every materialized view is tied to a base table: * For single-table queries, the base table is automatically determined * For JOINs, specify the base table with `WITH BASE` Only inserts to the base table trigger `IMMEDIATE` refresh. Changes to joined tables do not trigger refresh. ### Storage model[​](https://questdb.com/docs/concepts/materialized-views/#storage-model "Direct link to Storage model") Materialized views use the same storage engine as regular tables: * Columnar storage * Partitioning * Independent TTL management ### Refresh mechanism[​](https://questdb.com/docs/concepts/materialized-views/#refresh-mechanism "Direct link to Refresh mechanism") Incremental refresh process: 1. New data is inserted into the base table 2. The time-range of new data is identified 3. Only affected time slices are recomputed This happens asynchronously, minimizing write performance impact. Enterprise features[​](https://questdb.com/docs/concepts/materialized-views/#enterprise-features "Direct link to Enterprise features") --------------------------------------------------------------------------------------------------------------------------------------- ### Replicated views[​](https://questdb.com/docs/concepts/materialized-views/#replicated-views "Direct link to Replicated views") Replication of the base table is independent of materialized view maintenance. Promoting a replica to primary may trigger a full materialized view refresh if the replica's view was not fully up-to-date. Related documentation[​](https://questdb.com/docs/concepts/materialized-views/#related-documentation "Direct link to Related documentation") --------------------------------------------------------------------------------------------------------------------------------------------- * **Related Concepts** * [Views](https://questdb.com/docs/concepts/views/) : Virtual tables that compute results at query time * **SQL Commands** * [`CREATE MATERIALIZED VIEW`](https://questdb.com/docs/query/sql/create-mat-view/) : Create a new materialized view * [`DROP MATERIALIZED VIEW`](https://questdb.com/docs/query/sql/drop-mat-view/) : Remove a materialized view * [`REFRESH MATERIALIZED VIEW`](https://questdb.com/docs/query/sql/refresh-mat-view/) : Manually refresh a materialized view * [`ALTER MATERIALIZED VIEW ADD INDEX`](https://questdb.com/docs/query/sql/alter-mat-view-alter-column-add-index/) : Adds an index to a materialized view * [`ALTER MATERIALIZED VIEW DROP INDEX`](https://questdb.com/docs/query/sql/alter-mat-view-alter-column-drop-index/) : Removes an index from a materialized view * [`ALTER MATERIALIZED VIEW RESUME WAL`](https://questdb.com/docs/query/sql/alter-mat-view-resume-wal/) : Resume WAL for a materialized view * [`ALTER MATERIALIZED VIEW SET REFRESH`](https://questdb.com/docs/query/sql/alter-mat-view-set-refresh/) : Changes a materialized view's refresh strategy and parameters * [`ALTER MATERIALIZED VIEW SET REFRESH LIMIT`](https://questdb.com/docs/query/sql/alter-mat-view-set-refresh-limit/) : Sets the time limit for incremental refresh on a materialized view * [`ALTER MATERIALIZED VIEW SET TTL`](https://questdb.com/docs/query/sql/alter-mat-view-set-ttl/) : Sets the time-to-live (TTL) period on a materialized view * **Configuration** * [Materialized views configs](https://questdb.com/docs/configuration/overview/#materialized-views) : Server configuration options for materialized views from `server.conf` * [What are materialized views for?](https://questdb.com/docs/concepts/materialized-views/#what-are-materialized-views-for) * [Quick example](https://questdb.com/docs/concepts/materialized-views/#quick-example) * [When to use materialized views](https://questdb.com/docs/concepts/materialized-views/#when-to-use-materialized-views) * [Not suited for: data enrichment](https://questdb.com/docs/concepts/materialized-views/#not-suited-for-data-enrichment) * [Creating a materialized view](https://questdb.com/docs/concepts/materialized-views/#creating-a-materialized-view) * [Basic syntax](https://questdb.com/docs/concepts/materialized-views/#basic-syntax) * [Extended syntax](https://questdb.com/docs/concepts/materialized-views/#extended-syntax) * [Naming conventions](https://questdb.com/docs/concepts/materialized-views/#naming-conventions) * [The query](https://questdb.com/docs/concepts/materialized-views/#the-query) * [Refresh strategies](https://questdb.com/docs/concepts/materialized-views/#refresh-strategies) * [Partitioning](https://questdb.com/docs/concepts/materialized-views/#partitioning) * [TTL (Time-To-Live)](https://questdb.com/docs/concepts/materialized-views/#ttl-time-to-live) * [Initial refresh](https://questdb.com/docs/concepts/materialized-views/#initial-refresh) * [Querying materialized views](https://questdb.com/docs/concepts/materialized-views/#querying-materialized-views) * [Performance comparison](https://questdb.com/docs/concepts/materialized-views/#performance-comparison) * [Managing materialized views](https://questdb.com/docs/concepts/materialized-views/#managing-materialized-views) * [Listing views](https://questdb.com/docs/concepts/materialized-views/#listing-views) * [Monitoring refresh status](https://questdb.com/docs/concepts/materialized-views/#monitoring-refresh-status) * [View invalidation](https://questdb.com/docs/concepts/materialized-views/#view-invalidation) * [Refreshing an invalid view](https://questdb.com/docs/concepts/materialized-views/#refreshing-an-invalid-view) * [Advanced: LATEST ON optimization](https://questdb.com/docs/concepts/materialized-views/#advanced-latest-on-optimization) * [Solution: Pre-aggregate with a materialized view](https://questdb.com/docs/concepts/materialized-views/#solution-pre-aggregate-with-a-materialized-view) * [Technical reference](https://questdb.com/docs/concepts/materialized-views/#technical-reference) * [Query constraints](https://questdb.com/docs/concepts/materialized-views/#query-constraints) * [Base table relationship](https://questdb.com/docs/concepts/materialized-views/#base-table-relationship) * [Storage model](https://questdb.com/docs/concepts/materialized-views/#storage-model) * [Refresh mechanism](https://questdb.com/docs/concepts/materialized-views/#refresh-mechanism) * [Enterprise features](https://questdb.com/docs/concepts/materialized-views/#enterprise-features) * [Replicated views](https://questdb.com/docs/concepts/materialized-views/#replicated-views) * [Related documentation](https://questdb.com/docs/concepts/materialized-views/#related-documentation) --- # Query tracing | QuestDB Query tracing is a feature that helps you diagnose performance issues with queries by recording each query's execution time, and the user who launched the query, in a system table called `_query_trace`. You can then analyze the data in this table using the full power of QuestDB's SQL statements. Query tracing is disabled by default. You can enable it using the following configuration property in `server.conf`: query.tracing.enabled=true You don't need to restart the database server for this property to take effect; just run the following query to reload the configuration: reload\_config() SELECT reload_config(); This is an example of what the `_query_trace` table may contain: \_query\_trace SELECT * from _query_trace; | ts | query\_text | execution\_micros | principal | | --- | --- | --- | --- | | 2025-01-15T08:52:56.600757Z | telemetry\_config LIMIT -1 | 1206 | admin | | 2025-01-15T08:53:03.815732Z | tables() | 1523 | admin | | 2025-01-15T08:53:22.971239Z | 'sys.query\_trace' | 5384 | admin | As a simple performance debugging example, to get the text of all queries that took more than 100 ms, run: \_query\_trace with filters SELECT query_textFROM _query_traceWHERE execution_micros > 100_000ORDER BY execution_micros DESC; The `_query_trace` table will drop data older than 24 hours in order to limit how much storage query tracing uses. --- # SQL extensions | QuestDB On this page QuestDB attempts to implement standard ANSI SQL. We also try to be compatible with PostgreSQL, although parts of this are a work in progress. This page presents the main extensions we bring to SQL and the main differences that one might find in SQL but not in QuestDB's dialect. SQL extensions[​](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#sql-extensions "Direct link to SQL extensions") ------------------------------------------------------------------------------------------------------------------------------ We have extended SQL to support our data storage model and simplify semantics of time series analytics. ### LATEST ON[​](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#latest-on "Direct link to LATEST ON") [LATEST ON](https://questdb.com/docs/query/sql/latest-on/) is a clause introduced to help find the latest entry by timestamp for a given key or combination of keys as part of a `SELECT` statement. LATEST ON symbol ID and side[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20trades%0AWHERE%20timestamp%20IN%20today()%0ALATEST%20ON%20timestamp%20PARTITION%20BY%20symbol%2C%20side%3B&executeQuery=true) SELECT * FROM tradesWHERE timestamp IN today()LATEST ON timestamp PARTITION BY symbol, side; ### Timestamp search[​](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#timestamp-search "Direct link to Timestamp search") Timestamp search can be performed with regular operators, e.g `>`, `<=` etc. However, QuestDB provides a [native notation](https://questdb.com/docs/query/sql/where/#timestamp-and-date) which is faster and less verbose. Results in a given year[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20trades%20WHERE%20timestamp%20IN%20%272025%27%3B&executeQuery=true) SELECT * FROM trades WHERE timestamp IN '2025'; ### SAMPLE BY[​](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#sample-by "Direct link to SAMPLE BY") [SAMPLE BY](https://questdb.com/docs/query/sql/select/#sample-by) is used for time-based [aggregations](https://questdb.com/docs/query/functions/aggregation/) with an efficient syntax. The short query below will return the average price from a list of symbols by one hour buckets. SAMPLE BY one month buckets[Demo this query](https://demo.questdb.io/?query=SELECT%20timestamp%2C%20symbol%2C%20sum(price)%20FROM%20trades%0AWHERE%20timestamp%20in%20today()%0ASAMPLE%20BY%201h%3B&executeQuery=true) SELECT timestamp, symbol, sum(price) FROM tradesWHERE timestamp in today()SAMPLE BY 1h; Differences from standard SQL[​](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#differences-from-standard-sql "Direct link to Differences from standard SQL") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ### SELECT \* FROM is optional[​](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#select--from-is-optional "Direct link to SELECT * FROM is optional") In QuestDB, using `SELECT * FROM` is optional, so `SELECT * FROM my_table;` will return the same result as `my_table;`. While adding `SELECT * FROM` makes SQL look more complete, there are examples where omitting these keywords makes queries a lot easier to read. Optional use of SELECT \* FROM[Demo this query](https://demo.questdb.io/?query=trades%3B%0A--%20equivalent%20to%3A%0ASELECT%20*%20FROM%20trades%3B&executeQuery=true) trades;-- equivalent to:SELECT * FROM trades; ### GROUP BY is optional[​](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#group-by-is-optional "Direct link to GROUP BY is optional") The `GROUP BY` clause is optional and can be omitted as the QuestDB optimizer derives group-by implementation from the `SELECT` clause. In standard SQL, users might write a query like the following: Standard SQL GROUP BY[Demo this query](https://demo.questdb.io/?query=SELECT%20symbol%2C%20side%2C%20sum(price)%20FROM%20trades%0AWHERE%20timestamp%20IN%20today()%0AGROUP%20BY%20symbol%2C%20side%3B&executeQuery=true) SELECT symbol, side, sum(price) FROM tradesWHERE timestamp IN today()GROUP BY symbol, side; However, enumerating a subset of `SELECT` columns in the `GROUP BY` clause is redundant and therefore unnecessary. The same SQL in QuestDB SQL-dialect can be written as: QuestDB Implicit GROUP BY[Demo this query](https://demo.questdb.io/?query=SELECT%20symbol%2C%20side%2C%20sum(price)%20FROM%20trades%0AWHERE%20timestamp%20IN%20today()%3B&executeQuery=true) SELECT symbol, side, sum(price) FROM tradesWHERE timestamp IN today(); ### Implicit HAVING[​](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#implicit-having "Direct link to Implicit HAVING") Let's look at another more complex example using `HAVING` in standard SQL: Standard SQL GROUP BY/HAVING SELECT symbol, side, sum(price) FROM tradesWHERE timestamp IN today()GROUP BY symbol, sideHAVING sum(price) > 1000; In QuestDB's dialect, featherweight sub-queries come to the rescue to create a smaller, more readable query, without unnecessary repetitive aggregations. `HAVING` functionality can be obtained implicitly as follows: QuestDB Implicit HAVING equivalent[Demo this query](https://demo.questdb.io/?query=(%0A%20%20SELECT%20symbol%2C%20side%2C%20sum(price)%20as%20total_price%0A%20%20FROM%20trades%20WHERE%20timestamp%20IN%20today()%0A)%0AWHERE%20total_price%20%3E%2010_000_000%3B&executeQuery=true) ( SELECT symbol, side, sum(price) as total_price FROM trades WHERE timestamp IN today())WHERE total_price > 10_000_000; * [SQL extensions](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#sql-extensions) * [LATEST ON](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#latest-on) * [Timestamp search](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#timestamp-search) * [SAMPLE BY](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#sample-by) * [Differences from standard SQL](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#differences-from-standard-sql) * [SELECT \* FROM is optional](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#select--from-is-optional) * [GROUP BY is optional](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#group-by-is-optional) * [Implicit HAVING](https://questdb.com/docs/concepts/deep-dive/sql-extensions/#implicit-having) --- # Designated timestamp | QuestDB On this page Every table in QuestDB should have a designated timestamp. This column defines the time axis for your data and unlocks QuestDB's core time-series capabilities including partitioning, time-series joins, and optimized interval scans. Without a designated timestamp, a table behaves like a generic append-only store—you lose partitioning, efficient time-range queries, and most time-series SQL features. Key Points * The designated timestamp column defines your table's time axis * Data is physically sorted by this column, enabling sub-millisecond time-range queries * Enables: partitioning, SAMPLE BY, LATEST ON, ASOF JOIN, TTL, deduplication, replication * Constraints: cannot be NULL, cannot be changed after creation, cannot be updated * Without it: no partitioning, time queries must load all data into RAM Why designated timestamp exists[​](https://questdb.com/docs/concepts/designated-timestamp/#why-designated-timestamp-exists "Direct link to Why designated timestamp exists") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Traditional databases store rows in insertion order or by primary key. When you query "show me the last 5 minutes of data," the database must scan the entire table to find matching rows—even if that's 0.001% of your data. For time-series workloads, this is catastrophically inefficient. Consider a table with 1 billion rows spanning 30 days. A query for "last hour" should read ~1.4 million rows, not 1 billion. QuestDB solves this with the designated timestamp: | Problem | Solution | | --- | --- | | Data scattered across disk | Data stored **physically sorted** by timestamp | | Must scan entire table for time queries | **Binary search** jumps directly to relevant rows | | Can't skip irrelevant data | **Partition pruning** skips entire time ranges | | Time-series operations require sorting | Data is **pre-sorted**, no runtime cost | The designated timestamp is not just metadata—it fundamentally changes how QuestDB stores and queries your data. Performance impact[​](https://questdb.com/docs/concepts/designated-timestamp/#performance-impact "Direct link to Performance impact") -------------------------------------------------------------------------------------------------------------------------------------- QuestDB's query engine leverages the designated timestamp aggressively: 1. **Timestamp predicates execute first** — Before any other filters 2. **Partition pruning** — Entire partitions outside the time range are skipped, reducing I/O 3. **Binary search within partitions** — Finds exact row boundaries without scanning 4. **Targeted column reads** — Only the relevant data frames from other columns are read from disk The result: most queries with timestamp predicates complete in **sub-millisecond** time, regardless of total table size. A query for "last hour" on a table with billions of rows performs the same as on a table with thousands—only the matching rows are touched. ### Advanced: TICK interval syntax[​](https://questdb.com/docs/concepts/designated-timestamp/#advanced-tick-interval-syntax "Direct link to Advanced: TICK interval syntax") For complex temporal patterns, use [TICK](https://questdb.com/docs/query/operators/tick/) syntax to generate multiple optimized interval scans from a single expression: -- NYSE trading hours on workdays for January (22 intervals, one query)SELECT * FROM tradesWHERE ts IN '2024-01-[01..31]T09:30@America/New_York#workday;6h30m';-- Last 5 business days at market openSELECT * FROM tradesWHERE ts IN '[$today-5bd..$today-1bd]T09:30;1h'; Each generated interval uses the same binary search optimization—complex schedules perform as fast as simple time-range queries. How it works[​](https://questdb.com/docs/concepts/designated-timestamp/#how-it-works "Direct link to How it works") -------------------------------------------------------------------------------------------------------------------- ### Physical storage order[​](https://questdb.com/docs/concepts/designated-timestamp/#physical-storage-order "Direct link to Physical storage order") When you designate a timestamp column, QuestDB stores all rows sorted by that column's values. New data appends efficiently when it arrives in chronological order. When data arrives out of order, QuestDB [rearranges it](https://questdb.com/docs/concepts/partitions/#partition-splitting-and-squashing) to maintain timestamp order. Without designated timestamp: With designated timestamp:(stored in insertion order) (stored sorted by time)┌─────────────────────────┐ ┌─────────────────────────┐│ Row 1: 10:05:00 │ │ Row 1: 10:00:00 ││ Row 2: 10:00:00 │ │ Row 2: 10:01:15 ││ Row 3: 10:02:30 │ │ Row 3: 10:02:30 ││ Row 4: 10:01:15 │ │ Row 4: 10:05:00 │└─────────────────────────┘ └─────────────────────────┘ ↓ ↓ Query for 10:01-10:03 Query for 10:01-10:03 must scan ALL rows jumps directly to rows 2-3 This physical ordering enables all downstream optimizations. ### Partition assignment[​](https://questdb.com/docs/concepts/designated-timestamp/#partition-assignment "Direct link to Partition assignment") The designated timestamp determines which [partition](https://questdb.com/docs/concepts/partitions/) stores each row. QuestDB uses the timestamp value to route rows to time-based directories (hourly, daily, weekly, monthly, or yearly). ![Animation showing how the designated timestamp determines which partition stores each row](https://questdb.com/docs/images/docs/concepts/designatedTimestamp.svg) For example, with daily partitioning: * A row with timestamp `2024-01-15T10:30:00Z` goes to the `2024-01-15` partition * A row with timestamp `2024-01-16T08:00:00Z` goes to the `2024-01-16` partition This physical separation allows QuestDB to skip entire partitions during queries. ### Interval scan optimization[​](https://questdb.com/docs/concepts/designated-timestamp/#interval-scan-optimization "Direct link to Interval scan optimization") When you query with a time filter on the designated timestamp, QuestDB performs an **interval scan** instead of a full table scan: 1. **Partition pruning**: Skip partitions entirely outside the time range 2. **Binary search**: Within relevant partitions, use binary search to find the exact start and end positions 3. **Sequential read**: Read only the rows within the boundaries -- This query on a 1-year table with daily partitions:SELECT * FROM tradesWHERE timestamp > '2024-01-15' AND timestamp < '2024-01-16';-- Skips 364 partitions, binary searches within 1 partition-- Reads only matching rows, not the entire table Use [EXPLAIN](https://questdb.com/docs/query/sql/explain/) to verify interval scans: EXPLAIN SELECT * FROM trades WHERE timestamp IN '2024-01-15'; | QUERY PLAN ||---------------------------------------------------------------|| DataFrame || Row forward scan || Interval forward scan on: trades | ← Interval scan!| intervals: [("2024-01-15T00:00:00.000000Z", || "2024-01-15T23:59:59.999999Z")] | If you see `Async Filter` or `Table scan` instead of `Interval forward scan`, the query is not using the designated timestamp optimization. What it enables[​](https://questdb.com/docs/concepts/designated-timestamp/#what-it-enables "Direct link to What it enables") ----------------------------------------------------------------------------------------------------------------------------- The designated timestamp unlocks these features: **Query features:** | Feature | Why it needs designated timestamp | | --- | --- | | [SAMPLE BY](https://questdb.com/docs/query/sql/sample-by/) | Aggregates by time buckets on sorted data | | [LATEST ON](https://questdb.com/docs/query/sql/latest-on/) | Finds most recent rows using sorted order | | [ASOF JOIN](https://questdb.com/docs/query/sql/asof-join/) | Matches rows by nearest timestamp | | [WINDOW JOIN](https://questdb.com/docs/query/sql/window-join/) | Time-windowed joins between tables | | [Interval scan](https://questdb.com/docs/concepts/deep-dive/interval-scan/) | Binary search on sorted data for time-range queries | **Storage and lifecycle:** | Feature | Why it needs designated timestamp | | --- | --- | | [Partitioning](https://questdb.com/docs/concepts/partitions/) | Routes rows to time-based partitions | | [TTL](https://questdb.com/docs/concepts/ttl/) | Drops partitions by age (requires partitioning) | | [Deduplication](https://questdb.com/docs/concepts/deduplication/) | Leverages sorted order to find overlapping timestamps for efficient upsert | | [Materialized views](https://questdb.com/docs/concepts/materialized-views/) | SAMPLE BY-based views inherit the requirement | | [Replication](https://questdb.com/docs/high-availability/setup/) | Requires WAL, which requires partitioning | ### Without a designated timestamp[​](https://questdb.com/docs/concepts/designated-timestamp/#without-a-designated-timestamp "Direct link to Without a designated timestamp") Tables without a designated timestamp lose all of the above. They are appropriate only for temporary tables during data manipulation. | Capability | Without designated timestamp | | --- | --- | | Time-range queries | Must load entire projection into RAM | | Partitioning | Not available — single partition | | Tiered storage | Not available | | Replication | Not available | | ILP ingestion | HTTP ILP protocol cannot be used | note **Exception**: Static lookup tables (country codes, currency mappings) with no time dimension don't need a designated timestamp. How to set it[​](https://questdb.com/docs/concepts/designated-timestamp/#how-to-set-it "Direct link to How to set it") ----------------------------------------------------------------------------------------------------------------------- ### At table creation (recommended)[​](https://questdb.com/docs/concepts/designated-timestamp/#at-table-creation-recommended "Direct link to At table creation (recommended)") Use the `TIMESTAMP(columnName)` clause: CREATE TABLE trades ( ts TIMESTAMP, symbol SYMBOL, price DOUBLE, amount DOUBLE) TIMESTAMP(ts) PARTITION BY DAY; The designated timestamp column must be defined in the column list before being referenced in the `TIMESTAMP()` clause. ### Via InfluxDB Line Protocol[​](https://questdb.com/docs/concepts/designated-timestamp/#via-influxdb-line-protocol "Direct link to Via InfluxDB Line Protocol") Tables created automatically via ILP include a `timestamp` column as the designated timestamp, partitioned by day by default: trades,symbol=BTC-USD price=50000,amount=1.5 1234567890000000000 └── Creates table with designated timestamp automatically ### On query results (dynamic timestamp)[​](https://questdb.com/docs/concepts/designated-timestamp/#on-query-results-dynamic-timestamp "Direct link to On query results (dynamic timestamp)") For queries that lose the designated timestamp (see [Troubleshooting](https://questdb.com/docs/concepts/designated-timestamp/#troubleshooting) ), use the `TIMESTAMP()` keyword to restore it: SELECT * FROM ( SELECT ts, symbol, price FROM trades UNION ALL SELECT ts, symbol, price FROM trades_archive ORDER BY ts) TIMESTAMP(ts); warning Dynamic `TIMESTAMP()` only works if the data is actually sorted by that column. If the data is not in order, query results will be incorrect. Always include `ORDER BY` before applying `TIMESTAMP()` on potentially unordered data. Properties[​](https://questdb.com/docs/concepts/designated-timestamp/#properties "Direct link to Properties") -------------------------------------------------------------------------------------------------------------- | Property | Value | | --- | --- | | Eligible column types | `TIMESTAMP` (microseconds) or `TIMESTAMP_NS` (nanoseconds) | | Columns per table | Exactly one (or none) | | NULL values | Not allowed | | Mutability | Cannot be changed after table creation | | Updatability | Cannot be modified with UPDATE | ### Timestamp resolution[​](https://questdb.com/docs/concepts/designated-timestamp/#timestamp-resolution "Direct link to Timestamp resolution") QuestDB supports two timestamp resolutions: | Type | Resolution | Precision | Use case | | --- | --- | --- | --- | | `TIMESTAMP` | microseconds | 10⁻⁶ s | Most applications | | `TIMESTAMP_NS` | nanoseconds | 10⁻⁹ s | High-frequency trading, scientific data | **Use `TIMESTAMP` unless you specifically need nanosecond precision.** Both types work identically with all time-series features. For more on timestamp handling, see [Timestamps and time zones](https://questdb.com/docs/concepts/timestamps-timezones/) . Limitations[​](https://questdb.com/docs/concepts/designated-timestamp/#limitations "Direct link to Limitations") ----------------------------------------------------------------------------------------------------------------- ### Cannot be changed after table creation[​](https://questdb.com/docs/concepts/designated-timestamp/#cannot-be-changed-after-table-creation "Direct link to Cannot be changed after table creation") The designated timestamp is set at `CREATE TABLE` and cannot be altered. To use a different column: -- 1. Create new table with correct designated timestampCREATE TABLE trades_new ( event_time TIMESTAMP, -- new designated timestamp ingest_time TIMESTAMP, symbol SYMBOL, price DOUBLE) TIMESTAMP(event_time) PARTITION BY DAY;-- 2. Copy data (will be reordered by new designated timestamp)INSERT INTO trades_newSELECT event_time, ingest_time, symbol, priceFROM tradesORDER BY event_time;-- 3. Swap tablesDROP TABLE trades;RENAME TABLE trades_new TO trades; For large tables (billions of rows), this migration can take significant time and disk space. Plan for: * Sufficient disk space for both tables temporarily * Application downtime or dual-write period * Data validation after migration ### Cannot be NULL[​](https://questdb.com/docs/concepts/designated-timestamp/#cannot-be-null "Direct link to Cannot be NULL") Every row must have a valid timestamp value. The designated timestamp column cannot contain NULL. If your source data has missing timestamps: * Filter out NULL rows before inserting * Use a default/sentinel value (e.g., `'1970-01-01T00:00:00Z'`) * Use a different column as designated timestamp ### Cannot be updated[​](https://questdb.com/docs/concepts/designated-timestamp/#cannot-be-updated "Direct link to Cannot be updated") The designated timestamp column cannot be modified with [UPDATE](https://questdb.com/docs/query/sql/update/) : -- This will fail:UPDATE trades SET ts = '2024-01-15T12:00:00Z' WHERE symbol = 'BTC-USD';-- Error: Designated timestamp column cannot be updated **Why?** Updating the timestamp would require reordering rows within the partition and potentially moving rows between partitions. This would break QuestDB's append-optimized storage model. **Workaround**: Copy data to a temp table, modify it, and re-insert: -- 1. Create temp table WITHOUT designated timestamp-- Copy the partition(s) containing rows you need to modifyCREATE TABLE trades_temp AS ( SELECT * FROM trades WHERE ts IN '2024-01-15');-- 2. Drop the partition from the source tableALTER TABLE trades DROP PARTITION LIST '2024-01-15';-- 3. Update timestamps freely in the temp table (no designated timestamp)UPDATE trades_tempSET ts = dateadd('h', 1, ts)WHERE symbol = 'BTC-USD';-- 4. Re-insert into main table (data will be sorted automatically)INSERT INTO trades SELECT * FROM trades_temp;-- 5. Clean upDROP TABLE trades_temp; For ongoing correction workflows where you expect duplicate keys, consider using [deduplication](https://questdb.com/docs/concepts/deduplication/) with UPSERT KEYS instead. ### Only one designated timestamp per table[​](https://questdb.com/docs/concepts/designated-timestamp/#only-one-designated-timestamp-per-table "Direct link to Only one designated timestamp per table") A table can have multiple `TIMESTAMP` columns, but only one can be the designated timestamp: CREATE TABLE orders ( exchange_ts TIMESTAMP, -- designated timestamp (when exchange received) gateway_ts TIMESTAMP, -- when our gateway received ack_ts TIMESTAMP, -- when exchange acknowledged symbol SYMBOL, side SYMBOL, qty DOUBLE) TIMESTAMP(exchange_ts) PARTITION BY DAY; Choose the column you'll filter by most often in WHERE clauses. Best practices[​](https://questdb.com/docs/concepts/designated-timestamp/#best-practices "Direct link to Best practices") -------------------------------------------------------------------------------------------------------------------------- ### Choosing the right column[​](https://questdb.com/docs/concepts/designated-timestamp/#choosing-the-right-column "Direct link to Choosing the right column") If your data has multiple timestamp columns: | Column type | Example | Recommended? | | --- | --- | --- | | **Event time** | When the trade executed | ✅ Best choice | | **Ingestion time** | When QuestDB received it | ⚠️ Only if event time unavailable | | **Processing time** | When downstream system handled it | ❌ Rarely appropriate | **Rule of thumb**: Choose the timestamp that: 1. You'll filter by most often in queries 2. Represents the actual time of the event 3. Has the most uniform distribution ### Common concerns[​](https://questdb.com/docs/concepts/designated-timestamp/#common-concerns "Direct link to Common concerns") **Duplicate timestamps**: Duplicate timestamp values are allowed. Multiple rows can have the same designated timestamp. If you need uniqueness, enable [deduplication](https://questdb.com/docs/concepts/deduplication/) with UPSERT KEYS. **Future timestamps and TTL**: If you use [TTL](https://questdb.com/docs/concepts/ttl/) for automatic data retention, be careful with future timestamps. By default, TTL uses wall-clock time as the reference to prevent accidental data loss from far-future timestamps. See the [TTL documentation](https://questdb.com/docs/concepts/ttl/) for details. **Timezones**: All timestamps are stored in UTC internally. When you query with a timezone (e.g., `SAMPLE BY 1d ALIGN TO CALENDAR TIME ZONE 'Europe/London'`), QuestDB converts from the specified timezone to UTC for the search, then converts results back. Your source data should ideally be in UTC; if not, use [to\_utc()](https://questdb.com/docs/query/functions/date-time/#to_utc) during ingestion. ### Multiple timestamp columns[​](https://questdb.com/docs/concepts/designated-timestamp/#multiple-timestamp-columns "Direct link to Multiple timestamp columns") Keep additional timestamps as regular columns: CREATE TABLE quotes ( exchange_ts TIMESTAMP, -- when exchange published (designated) received_ts TIMESTAMP, -- when we received it symbol SYMBOL, bid DOUBLE, ask DOUBLE) TIMESTAMP(exchange_ts) PARTITION BY DAY;-- Query by exchange time (uses interval scan):SELECT * FROM quotesWHERE exchange_ts > dateadd('h', -1, now());-- Query by received time (full scan, but still works):SELECT * FROM quotesWHERE received_ts > dateadd('h', -1, now()); ### Out-of-order data[​](https://questdb.com/docs/concepts/designated-timestamp/#out-of-order-data "Direct link to Out-of-order data") QuestDB handles out-of-order data automatically—no special configuration needed. Data arriving out of order is merged into the correct position. However, excessive out-of-order data increases write amplification. If most of your data arrives significantly out of order: * Consider using ingestion time as designated timestamp * Store event time as a separate indexed column * Use appropriate partition sizing (smaller partitions = less rewrite per out-of-order event) ### Partition size alignment[​](https://questdb.com/docs/concepts/designated-timestamp/#partition-size-alignment "Direct link to Partition size alignment") Match your partition interval to your designated timestamp's data distribution: | Data volume | Partition interval | | --- | --- | | < 100K rows/day | `MONTH` or `YEAR` | | 100K - 10M rows/day | `DAY` | | 10M - 100M rows/day | `HOUR` | | \> 100M rows/day | `HOUR` | See [Partitions](https://questdb.com/docs/concepts/partitions/) for detailed guidance. Troubleshooting[​](https://questdb.com/docs/concepts/designated-timestamp/#troubleshooting "Direct link to Troubleshooting") ----------------------------------------------------------------------------------------------------------------------------- Certain SQL operations produce results without a designated timestamp. This breaks time-series features like SAMPLE BY on the result set. ### Operations that lose designated timestamp[​](https://questdb.com/docs/concepts/designated-timestamp/#operations-that-lose-designated-timestamp "Direct link to Operations that lose designated timestamp") | Operation | Why | Solution | | --- | --- | --- | | `UNION` / `UNION ALL` | Combined results aren't guaranteed ordered | `ORDER BY` then `TIMESTAMP()` | | Subqueries | Derived tables lose table metadata | Apply `TIMESTAMP()` to subquery | | `read_parquet()` | External files have no QuestDB metadata | `ORDER BY` then `TIMESTAMP()` | | Type casting | `ts::STRING::TIMESTAMP` loses designation | Avoid round-trip casting | | Some expressions | Computed timestamps aren't designated | Use `TIMESTAMP()` on result | ### How to restore it[​](https://questdb.com/docs/concepts/designated-timestamp/#how-to-restore-it "Direct link to How to restore it") Use the `TIMESTAMP()` keyword on ordered data: -- UNION loses designated timestamp-- Solution: ORDER BY, then apply TIMESTAMP()SELECT * FROM ( SELECT ts, symbol, price FROM trades_2023 UNION ALL SELECT ts, symbol, price FROM trades_2024 ORDER BY ts) TIMESTAMP(ts)SAMPLE BY 1h; -- Parquet files have no designated timestamp-- Solution: ORDER BY, then apply TIMESTAMP()SELECT timestamp, avg(price)FROM ( (SELECT * FROM read_parquet('trades.parquet') ORDER BY timestamp) TIMESTAMP(timestamp))SAMPLE BY 1m; -- Subquery loses designated timestamp-- Solution: Apply TIMESTAMP() to the subquery resultWITH recent AS ( (SELECT * FROM trades WHERE timestamp > dateadd('d', -7, now())) TIMESTAMP(timestamp))SELECT * FROM recent SAMPLE BY 1h; ### Verifying designated timestamp[​](https://questdb.com/docs/concepts/designated-timestamp/#verifying-designated-timestamp "Direct link to Verifying designated timestamp") **Check if a table has a designated timestamp:** SELECT table_name, designatedTimestampFROM tables()WHERE table_name = 'trades'; | table\_name | designatedTimestamp | | --- | --- | | trades | ts | **Check column details:** SELECT "column", type, designatedFROM table_columns('trades'); | column | type | designated | | --- | --- | --- | | ts | TIMESTAMP | true | | symbol | SYMBOL | false | | price | DOUBLE | false | **Check if a query uses interval scan optimization:** EXPLAIN SELECT * FROM trades WHERE timestamp IN '2024-01-15'; Look for `Interval forward scan`—if you see `Async Filter` instead, the designated timestamp optimization isn't being used. FAQ[​](https://questdb.com/docs/concepts/designated-timestamp/#faq "Direct link to FAQ") ----------------------------------------------------------------------------------------- **Can I add a designated timestamp to an existing table?** No. The designated timestamp must be defined at table creation. To add one, create a new table with the designated timestamp and migrate your data. **What happens if I insert data with NULL timestamp?** The insert fails. The designated timestamp column cannot contain NULL values. **Can I have two designated timestamps?** No. Each table can have at most one designated timestamp. Use additional `TIMESTAMP` columns for other time values. **Does out-of-order data break anything?** No. QuestDB handles out-of-order data automatically by merging it into the correct sorted position. However, excessive out-of-order data increases write amplification. **Is designated timestamp the same as a primary key?** No. The designated timestamp: * Doesn't enforce uniqueness (use [deduplication](https://questdb.com/docs/concepts/deduplication/) for that) * Determines physical storage order * Cannot be updated * Is optional (though strongly recommended) **Why can't I UPDATE the designated timestamp?** Updating the timestamp would require reordering rows and potentially moving them between partitions, breaking QuestDB's append-optimized storage model. Delete and re-insert instead, or use deduplication for correction workflows. See also[​](https://questdb.com/docs/concepts/designated-timestamp/#see-also "Direct link to See also") -------------------------------------------------------------------------------------------------------- * [CREATE TABLE](https://questdb.com/docs/query/sql/create-table/) — Full syntax for table creation * [Partitions](https://questdb.com/docs/concepts/partitions/) — Time-based data organization * [Interval scan](https://questdb.com/docs/concepts/deep-dive/interval-scan/) — Query optimization details * [TICK intervals](https://questdb.com/docs/query/operators/tick/) — Complex temporal patterns in a single expression * [SAMPLE BY](https://questdb.com/docs/query/sql/sample-by/) — Time-based aggregation * [LATEST ON](https://questdb.com/docs/query/sql/latest-on/) — Finding most recent records * [Timestamps and time zones](https://questdb.com/docs/concepts/timestamps-timezones/) — Working with time values * [Why designated timestamp exists](https://questdb.com/docs/concepts/designated-timestamp/#why-designated-timestamp-exists) * [Performance impact](https://questdb.com/docs/concepts/designated-timestamp/#performance-impact) * [Advanced: TICK interval syntax](https://questdb.com/docs/concepts/designated-timestamp/#advanced-tick-interval-syntax) * [How it works](https://questdb.com/docs/concepts/designated-timestamp/#how-it-works) * [Physical storage order](https://questdb.com/docs/concepts/designated-timestamp/#physical-storage-order) * [Partition assignment](https://questdb.com/docs/concepts/designated-timestamp/#partition-assignment) * [Interval scan optimization](https://questdb.com/docs/concepts/designated-timestamp/#interval-scan-optimization) * [What it enables](https://questdb.com/docs/concepts/designated-timestamp/#what-it-enables) * [Without a designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/#without-a-designated-timestamp) * [How to set it](https://questdb.com/docs/concepts/designated-timestamp/#how-to-set-it) * [At table creation (recommended)](https://questdb.com/docs/concepts/designated-timestamp/#at-table-creation-recommended) * [Via InfluxDB Line Protocol](https://questdb.com/docs/concepts/designated-timestamp/#via-influxdb-line-protocol) * [On query results (dynamic timestamp)](https://questdb.com/docs/concepts/designated-timestamp/#on-query-results-dynamic-timestamp) * [Properties](https://questdb.com/docs/concepts/designated-timestamp/#properties) * [Timestamp resolution](https://questdb.com/docs/concepts/designated-timestamp/#timestamp-resolution) * [Limitations](https://questdb.com/docs/concepts/designated-timestamp/#limitations) * [Cannot be changed after table creation](https://questdb.com/docs/concepts/designated-timestamp/#cannot-be-changed-after-table-creation) * [Cannot be NULL](https://questdb.com/docs/concepts/designated-timestamp/#cannot-be-null) * [Cannot be updated](https://questdb.com/docs/concepts/designated-timestamp/#cannot-be-updated) * [Only one designated timestamp per table](https://questdb.com/docs/concepts/designated-timestamp/#only-one-designated-timestamp-per-table) * [Best practices](https://questdb.com/docs/concepts/designated-timestamp/#best-practices) * [Choosing the right column](https://questdb.com/docs/concepts/designated-timestamp/#choosing-the-right-column) * [Common concerns](https://questdb.com/docs/concepts/designated-timestamp/#common-concerns) * [Multiple timestamp columns](https://questdb.com/docs/concepts/designated-timestamp/#multiple-timestamp-columns) * [Out-of-order data](https://questdb.com/docs/concepts/designated-timestamp/#out-of-order-data) * [Partition size alignment](https://questdb.com/docs/concepts/designated-timestamp/#partition-size-alignment) * [Troubleshooting](https://questdb.com/docs/concepts/designated-timestamp/#troubleshooting) * [Operations that lose designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/#operations-that-lose-designated-timestamp) * [How to restore it](https://questdb.com/docs/concepts/designated-timestamp/#how-to-restore-it) * [Verifying designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/#verifying-designated-timestamp) * [FAQ](https://questdb.com/docs/concepts/designated-timestamp/#faq) * [See also](https://questdb.com/docs/concepts/designated-timestamp/#see-also) --- # Interval Scan | QuestDB On this page An **interval scan** is QuestDB's optimized method for querying time ranges. Instead of scanning all rows, QuestDB uses binary search on the [designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) column to jump directly to relevant data. For how interval scans work and their performance impact, see [Designated timestamp: Performance impact](https://questdb.com/docs/concepts/designated-timestamp/#performance-impact) . For complex multi-interval patterns, see [TICK interval syntax](https://questdb.com/docs/query/operators/tick/) . How it looks[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#how-it-looks "Direct link to How it looks") ----------------------------------------------------------------------------------------------------------------------- ![Interval scan using binary search to find row boundaries](https://questdb.com/docs/images/blog/2023-04-25/intervalScan.webp) The query engine: 1. Prunes partitions outside the time range 2. Binary searches within relevant partitions to find exact row boundaries 3. Reads only rows within those boundaries Verifying interval scan with EXPLAIN[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#verifying-interval-scan-with-explain "Direct link to Verifying interval scan with EXPLAIN") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Use [EXPLAIN](https://questdb.com/docs/query/sql/explain/) to confirm a query uses interval scan: Check for interval scan[Demo this query](https://demo.questdb.io/?query=EXPLAIN%20SELECT%20*%20FROM%20trades%0AWHERE%20timestamp%20IN%20%272024-01-20%27%3B&executeQuery=true) EXPLAIN SELECT * FROM tradesWHERE timestamp IN '2024-01-20'; **Good** - Interval scan is being used: | QUERY PLAN ||---------------------------------------------------------------|| DataFrame || Row forward scan || Interval forward scan on: trades || intervals: [("2024-01-20T00:00:00.000000Z", || "2024-01-20T23:59:59.999999Z")] | **Not optimal** - Full scan with async filter: | QUERY PLAN ||---------------------------------------------------------------|| Async Filter || workers: 4 || filter: timestamp IN '2024-01-20' || DataFrame || Full scan on: trades | If you see `Async Filter` or `Full scan` instead of `Interval forward scan`, the query is not using the designated timestamp optimization. Equivalent query forms[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#equivalent-query-forms "Direct link to Equivalent query forms") ----------------------------------------------------------------------------------------------------------------------------------------------------- These queries all produce the same interval scan plan: Using IN SELECT * FROM trades WHERE timestamp IN '2024-01-20'; Using BETWEEN SELECT * FROM tradesWHERE timestamp BETWEEN '2024-01-20T00:00:00.000000Z' AND '2024-01-20T23:59:59.999999Z'; Using comparison operators SELECT * FROM tradesWHERE timestamp >= '2024-01-20T00:00:00.000000Z' AND timestamp <= '2024-01-20T23:59:59.999999Z'; All three produce: Interval forward scan on: trades intervals: [("2024-01-20T00:00:00.000000Z","2024-01-20T23:59:59.999999Z")] Use whichever form is most readable for your use case. `IN` with partial timestamps is typically the most concise. Multiple intervals[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#multiple-intervals "Direct link to Multiple intervals") ----------------------------------------------------------------------------------------------------------------------------------------- For multiple time ranges, use [TICK syntax](https://questdb.com/docs/query/operators/tick/) : EXPLAIN SELECT * FROM tradesWHERE timestamp IN '2024-01-[15,16,17]'; Interval forward scan on: trades intervals: [("2024-01-15T00:00:00.000000Z","2024-01-15T23:59:59.999999Z"), ("2024-01-16T00:00:00.000000Z","2024-01-16T23:59:59.999999Z"), ("2024-01-17T00:00:00.000000Z","2024-01-17T23:59:59.999999Z")] Each interval uses binary search independently—complex patterns perform as fast as simple queries. Edge cases[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#edge-cases "Direct link to Edge cases") ----------------------------------------------------------------------------------------------------------------- ### Tables without designated timestamp[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#tables-without-designated-timestamp "Direct link to Tables without designated timestamp") Tables without a designated timestamp cannot use interval scan. Queries fall back to full table scan with async filter. To enable interval scan, recreate the table with a designated timestamp: CREATE TABLE trades_new ( ts TIMESTAMP, symbol SYMBOL, price DOUBLE) TIMESTAMP(ts) PARTITION BY DAY;INSERT INTO trades_new SELECT * FROM trades_old ORDER BY ts; ### Declaring timestamp on query results[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#declaring-timestamp-on-query-results "Direct link to Declaring timestamp on query results") For subqueries or tables without a designated timestamp, you can declare one using `TIMESTAMP(columnName)`: EXPLAIN SELECT * FROM trades_nodts TIMESTAMP(ts)WHERE ts IN '2024-01-20'; This enables interval scan on the result. warning `TIMESTAMP(columnName)` only works if the data is **actually ordered** by that column. If the data is not in timestamp order, query results will be incorrect. For unordered data, add `ORDER BY` first: SELECT * FROM (SELECT * FROM unordered_table ORDER BY ts) TIMESTAMP(ts)WHERE ts IN '2024-01-20'; ### Subqueries lose designated timestamp[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#subqueries-lose-designated-timestamp "Direct link to Subqueries lose designated timestamp") Subquery results don't inherit the designated timestamp from the source table: -- This does NOT use interval scan on the subquery result:SELECT * FROM (SELECT * FROM trades WHERE symbol = 'BTC-USD')WHERE timestamp IN '2024-01-20'; To restore interval scan, explicitly declare the timestamp: -- This uses interval scan:SELECT * FROM (SELECT * FROM trades WHERE symbol = 'BTC-USD') TIMESTAMP(timestamp)WHERE timestamp IN '2024-01-20'; See [Designated timestamp: Troubleshooting](https://questdb.com/docs/concepts/designated-timestamp/#troubleshooting) for more scenarios where designated timestamp is lost. See also[​](https://questdb.com/docs/concepts/deep-dive/interval-scan/#see-also "Direct link to See also") ----------------------------------------------------------------------------------------------------------- * [Designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) — Why interval scan works * [TICK intervals](https://questdb.com/docs/query/operators/tick/) — Complex multi-interval patterns * [EXPLAIN](https://questdb.com/docs/query/sql/explain/) — Query plan analysis * [How it looks](https://questdb.com/docs/concepts/deep-dive/interval-scan/#how-it-looks) * [Verifying interval scan with EXPLAIN](https://questdb.com/docs/concepts/deep-dive/interval-scan/#verifying-interval-scan-with-explain) * [Equivalent query forms](https://questdb.com/docs/concepts/deep-dive/interval-scan/#equivalent-query-forms) * [Multiple intervals](https://questdb.com/docs/concepts/deep-dive/interval-scan/#multiple-intervals) * [Edge cases](https://questdb.com/docs/concepts/deep-dive/interval-scan/#edge-cases) * [Tables without designated timestamp](https://questdb.com/docs/concepts/deep-dive/interval-scan/#tables-without-designated-timestamp) * [Declaring timestamp on query results](https://questdb.com/docs/concepts/deep-dive/interval-scan/#declaring-timestamp-on-query-results) * [Subqueries lose designated timestamp](https://questdb.com/docs/concepts/deep-dive/interval-scan/#subqueries-lose-designated-timestamp) * [See also](https://questdb.com/docs/concepts/deep-dive/interval-scan/#see-also) --- # Indexes | QuestDB On this page An index stores the row locations for each value of the target column in order to provide faster read access. It allows you to bypass full table scans by directly accessing the relevant rows during queries with `WHERE` conditions. Indexing is available for [symbol](https://questdb.com/docs/concepts/symbol/) columns in both tables and [materialized views](https://questdb.com/docs/concepts/materialized-views/) . Index support for other types will be added over time. Index creation and deletion[​](https://questdb.com/docs/concepts/deep-dive/indexes/#index-creation-and-deletion "Direct link to Index creation and deletion") -------------------------------------------------------------------------------------------------------------------------------------------------------------- The following are ways to index a `symbol` column: * At table creation time using [CREATE TABLE](https://questdb.com/docs/query/sql/create-table/#column-indexes) * Using [ALTER TABLE ALTER COLUMN ADD INDEX](https://questdb.com/docs/query/sql/alter-table-alter-column-add-index/) to index an existing `symbol` column in a table. * Using [ALTER MATERIALIZED VIEW ALTER COLUMN ADD INDEX](https://questdb.com/docs/query/sql/alter-mat-view-alter-column-add-index/) to index an existing `symbol` column in a materialized view. To delete an index: * From a table: [ALTER TABLE ALTER COLUMN DROP INDEX](https://questdb.com/docs/query/sql/alter-table-alter-column-drop-index/) * From a materialized view: [ALTER MATERIALIZED VIEW ALTER COLUMN DROP INDEX](https://questdb.com/docs/query/sql/alter-mat-view-alter-column-drop-index/) How indexes work[​](https://questdb.com/docs/concepts/deep-dive/indexes/#how-indexes-work "Direct link to How indexes work") ----------------------------------------------------------------------------------------------------------------------------- Index creates a table of row locations for each distinct value for the target [symbol](https://questdb.com/docs/concepts/symbol/) . Once the index is created, inserting data into the table (or materialized view) will update the index. Lookups on indexed values will be performed in the index table directly which will provide the memory locations of the items, thus avoiding unnecessary table scans. Here is an example of a table and its index table. Table Index|Row ID | Symbol | Value | | Symbol | Row IDs || 1 | A | 1 | | A | 1, 2, 4 || 2 | A | 0 | | B | 3 || 3 | B | 1 | | C | 5 || 4 | A | 1 || 5 | C | 0 | `INSERT INTO Table values(B, 1);` would trigger two updates: one for the Table, and one for the Index. Table Index|Row ID | Symbol | Value | | Symbol | Row IDs || 1 | A | 1 | | A | 1, 2, 4 || 2 | A | 0 | | B | 3, 6 || 3 | B | 1 | | C | 5 || 4 | A | 1 || 5 | C | 0 || 6 | B | 1 | Advantages[​](https://questdb.com/docs/concepts/deep-dive/indexes/#advantages "Direct link to Advantages") ----------------------------------------------------------------------------------------------------------- Index allows you to greatly reduce the complexity of queries that span a subset of an indexed column, typically when using `WHERE` clauses. Consider the following query applied to the above table `SELECT sum(Value) FROM Table WHERE Symbol='A';` * **Without Index**, the query engine would scan the whole table in order to perform the query. It will need to perform 6 operations (read each of the 6 rows once). * **With Index**, the query engine will first scan the index table, which is considerably smaller. In our example, it will find A in the first row. Then, the query engine would check the values at the specific locations 1, 2, 4 in the table to read the corresponding values. As a result, it would only scan the relevant rows in the table and leave irrelevant rows untouched. Trade-offs[​](https://questdb.com/docs/concepts/deep-dive/indexes/#trade-offs "Direct link to Trade-offs") ----------------------------------------------------------------------------------------------------------- * **Storage space**: The index will maintain a table with each distinct symbol value and the locations where these symbols can be found. As a result, there is a small cost of storage associated with indexing a symbol field. * **Ingestion performance**: Each new entry in the table or materialized view will trigger an entry in the Index table. This means that any write will now require two write operations, and therefore take twice as long. Index capacity[​](https://questdb.com/docs/concepts/deep-dive/indexes/#index-capacity "Direct link to Index capacity") ----------------------------------------------------------------------------------------------------------------------- warning We strongly recommend to rely on the default index capacity. Misconfiguring this property might lead to worse performance and increased disk usage. When in doubt, reach out via the QuestDB support channels for advice. note * The **index capacity** and [**symbol capacity**](https://questdb.com/docs/concepts/symbol/) are different settings. * The index capacity value should not be changed, unless a user is aware of all the implications. When a symbol column is indexed, an additional **index capacity** can be defined to specify how many row IDs to store in a single storage block on disk: * Server-wide setting: `cairo.index.value.block.size` with a default of `256` * Column-wide setting: The [`index` option](https://questdb.com/docs/query/sql/create-table/#column-indexes) for `CREATE TABLE` * Column-wide setting for a table: [ALTER TABLE COLUMN ADD INDEX](https://questdb.com/docs/query/sql/alter-table-alter-column-add-index/) * Column-wide setting for a materialized view: [ALTER MATERIALIZED VIEW COLUMN ADD INDEX](https://questdb.com/docs/query/sql/alter-mat-view-alter-column-add-index/) Fewer blocks used to store row IDs achieves better performance. At the same time over-sizing the setting will result in higher than necessary disk space usage. Consider an example table with 200 unique stock symbols and 1,000,000,000 records over time stored in a single partition. The index will have to store 1,000,000,000 / 200 row IDs for each symbol, i.e. 5,000,000 per symbol. Since indexes are per-partition, spreading data across multiple partitions reduces the row IDs stored in each partition's index. * If the index capacity is set to 1,048,576 in this case, QuestDB will use 5 blocks to store the row IDs. * If the index capacity is set to 1,024 in this case, the block count will be 4,883. Examples[​](https://questdb.com/docs/concepts/deep-dive/indexes/#examples "Direct link to Examples") ----------------------------------------------------------------------------------------------------- ### Table with index[​](https://questdb.com/docs/concepts/deep-dive/indexes/#table-with-index "Direct link to Table with index") An example of `CREATE TABLE` command: CREATE TABLE my_table(symb SYMBOL, price DOUBLE, ts TIMESTAMP), INDEX (symb) timestamp(ts); * [Index creation and deletion](https://questdb.com/docs/concepts/deep-dive/indexes/#index-creation-and-deletion) * [How indexes work](https://questdb.com/docs/concepts/deep-dive/indexes/#how-indexes-work) * [Advantages](https://questdb.com/docs/concepts/deep-dive/indexes/#advantages) * [Trade-offs](https://questdb.com/docs/concepts/deep-dive/indexes/#trade-offs) * [Index capacity](https://questdb.com/docs/concepts/deep-dive/indexes/#index-capacity) * [Examples](https://questdb.com/docs/concepts/deep-dive/indexes/#examples) * [Table with index](https://questdb.com/docs/concepts/deep-dive/indexes/#table-with-index) --- # Architecture Overview | QuestDB On this page QuestDB offers high-speed ingestion and low-latency analytics on time-series data. It stores data in a three-tier architecture (streaming WAL files, local binary storage, and local or remote storage in Parquet format) to improve interoperability and avoid vendor lock-in. WHERE symbol in ('AAPL', 'NVDA') LATEST ON timestamp PARTITION BY symbol CREATE MATERIALIZED VIEW 'trades\_OHLC' min(price) AS low timestamp IN today() SELECT spread\_bps(bids\[1\]\[1\], asks\[1\]\[1\]) FROM read\_parquet('trades.parquet') SAMPLE BY 15m **Tier One:** Hot ingest (WAL), durable by default Incoming data is appended to the write-ahead log (WAL) with ultra-low latency. Writes are made durable before any processing, preserving order and surviving failures without data loss. The WAL is asynchronously shipped to object storage, so new replicas can bootstrap quickly and read the same history. **Tier Two:** Real-time SQL on live data Data is time-ordered and de-duplicated into QuestDB's native, time-partitioned columnar format and becomes immediately queryable. Power real-time analysis with vectorized, multi-core execution, streaming materialized views, and time-series SQL (e.g., ASOF JOIN, SAMPLE BY). The query planner spans tiers seamlessly. **Tier Three:** Cold storage, open and queryable Older data is automatically tiered to object storage in Apache Parquet. Query it in-place through QuestDB or use any tool that reads Parquet. This delivers predictable costs, interoperability with AI/ML tooling, and zero lock-in. This document explains QuestDB's internal architecture. Key components[​](https://questdb.com/docs/architecture/questdb-architecture/#key-components "Direct link to Key components") ------------------------------------------------------------------------------------------------------------------------------ QuestDB is comprised of several key components: * **[Storage engine](https://questdb.com/docs/architecture/storage-engine/) :** Uses a column-oriented design to ensure high I/O performance and low latency. * **[Memory management and native integration](https://questdb.com/docs/architecture/memory-management/) :** The system leverages both memory mapping and explicit memory management techniques, integrating native code for performance-critical tasks. * **[Query engine](https://questdb.com/docs/architecture/query-engine/) :** A custom SQL parser, a just-in-time (JIT) compiler, and a vectorized execution engine process data in table page frames for better CPU use. * **[Time-series optimizations](https://questdb.com/docs/architecture/time-series-optimizations/) :** QuestDB is specifically designed for time-series, and it provides several optimizations, such as a designated timestamp, sequential reads, materialized views, and in-memory processing. * **[Replication](https://questdb.com/docs/high-availability/overview/) :** QuestDB Enterprise supports high availability with read replicas and multi-primary configurations. See [High Availability](https://questdb.com/docs/high-availability/overview/) for details. * **[Observability](https://questdb.com/docs/architecture/observability/) :** QuestDB exposes real-time metrics, health checks, and structured logs to monitor performance and streamline diagnostics. * **[Web console](https://questdb.com/docs/getting-started/web-console/overview/) :** The engine includes a web console for running SQL statements, bulk loading CSV files, and displaying monitoring dashboards. QuestDB Enterprise supports single sign-on (SSO) in the web console. Design patterns & best practices throughout the codebase[​](https://questdb.com/docs/architecture/questdb-architecture/#design-patterns--best-practices-throughout-the-codebase "Direct link to Design patterns & best practices throughout the codebase") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- * **Immutable data structures:** The system favors immutability to avoid concurrency issues and simplify state management. * **Modular architecture:** Each component (eg., storage, query processing, ingestion, etc.) has well-defined interfaces that enhance maintainability and decouple functionality. * **Factory & builder patterns:** These patterns are used to centralize construction logic for complex objects such as SQL execution plans and storage buffers. * **Lazy initialization:** Resource-intensive components initialize only when needed to reduce startup overhead. * **Rigorous testing & benchmarks:** [Unit tests, integration tests](https://github.com/questdb/questdb/tree/master/core/src/test) , and performance benchmarks ensure that new enhancements do not compromise reliability or performance. Next up[​](https://questdb.com/docs/architecture/questdb-architecture/#next-up "Direct link to Next up") --------------------------------------------------------------------------------------------------------- Continue to [Storage Engine](https://questdb.com/docs/architecture/storage-engine/) to learn how QuestDB stores and manages data on disk. * [Key components](https://questdb.com/docs/architecture/questdb-architecture/#key-components) * [Design patterns & best practices throughout the codebase](https://questdb.com/docs/architecture/questdb-architecture/#design-patterns--best-practices-throughout-the-codebase) * [Next up](https://questdb.com/docs/architecture/questdb-architecture/#next-up) --- # Command-line options | QuestDB On this page QuestDB may be started, stopped and passed configuration options from the command line. On Windows, the QuestDB server can also start an [interactive session](https://questdb.com/docs/configuration/command-line-options/#interactive-session-windows) . Options[​](https://questdb.com/docs/configuration/command-line-options/#options "Direct link to Options") ---------------------------------------------------------------------------------------------------------- The following sections describe the options that may be passed to QuestDB when starting the server from the command line. * Linux * macOS (Homebrew) * Windows ./questdb.sh [start|stop|status] [-d dir] [-f] [-n] [-t tag] questdb [start|stop|status] [-d dir] [-f] [-n] [-t tag] questdb.exe [start|stop|status|install|remove] \ [-d dir] [-f] [-j JAVA_HOME] [-t tag] ### Start[​](https://questdb.com/docs/configuration/command-line-options/#start "Direct link to Start") `start` - starts QuestDB as a service. | Option | Description | | --- | --- | | `-d` | Expects a `dir` directory value which is a folder that will be used as QuestDB's root directory. For more information and the default values, see the [default root](https://questdb.com/docs/configuration/command-line-options/#default-root-directory-1)
section below. | | `-t` | Expects a `tag` string value which will be as a tag for the service. This option allows users to run several QuestDB services and manage them separately. If this option is omitted, the default tag will be `questdb`. | | `-f` | Force re-deploying the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/)
. Without this option, the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/)
is cached and deployed only when missing. | | `-n` | Do not respond to the HUP signal. This keeps QuestDB alive after you close the terminal window where you started it. | | `-j` | **Windows only!** This option allows to specify a path to `JAVA_HOME`. | note * When running multiple QuestDB services, a tag must be used to disambiguate between services for `start` and `stop` commands. There will be conflicting ports and root directories if only the tag flag is specified when starting multiple services. Each new service should have its own config file or should be started with separate port and root directory options. * When running QuestDB as Windows service you can check status in both: * Windows Event Viewer - look for events with "QuestDB" source in Windows Logs | Application . * service log file - `$dataDir\log\service-%Y-%m-%dT%H-%M-%S.txt` (default is `C:\Windows\System32\qdbroot\log\service-%Y-%m-%dT%H-%M-%S.txt` ) * Linux * macOS (Homebrew) * Windows ./questdb.sh start [-d dir] [-f] [-n] [-t tag] questdb start [-d dir] [-f] [-n] [-t tag] questdb.exe start [-d dir] [-f] [-j JAVA_HOME] [-t tag] #### Default root directory[​](https://questdb.com/docs/configuration/command-line-options/#default-root-directory "Direct link to Default root directory") By default, QuestDB's [root directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/) will be the following: * Linux * macOS (Homebrew) * Windows $HOME/.questdb Path on Macs with Apple Silicon (M1 or M2) chip: /opt/homebrew/var/questdb Path on Macs with Intel chip: /usr/local/var/questdb C:\Windows\System32\qdbroot ### Stop[​](https://questdb.com/docs/configuration/command-line-options/#stop "Direct link to Stop") `stop` - stops a service. | Option | Description | | --- | --- | | `-t` | Expects a `tag` string value which to stop a service by tag. If this is omitted, the default tag will be `questdb` | * Linux * macOS (Homebrew) * Windows ./questdb.sh stop questdb stop questdb.exe stop ### Status[​](https://questdb.com/docs/configuration/command-line-options/#status "Direct link to Status") `status` - shows the status for a service. | Option | Description | | --- | --- | | `-t` | Expects a `tag` string value which to stop a service by tag. If this is omitted, the default will be `questdb` | * Linux * macOS (Homebrew) * Windows ./questdb.sh status questdb status questdb.exe status ### Install (Windows)[​](https://questdb.com/docs/configuration/command-line-options/#install-windows "Direct link to Install (Windows)") `install` - installs the Windows QuestDB service. The service will start automatically at startup. questdb.exe install ### Remove (Windows)[​](https://questdb.com/docs/configuration/command-line-options/#remove-windows "Direct link to Remove (Windows)") `remove` - removes the Windows QuestDB service. It will no longer start at startup. questdb.exe remove Interactive session (Windows)[​](https://questdb.com/docs/configuration/command-line-options/#interactive-session-windows "Direct link to Interactive session (Windows)") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- You can start QuestDB interactively by running `questdb.exe`. This will launch QuestDB interactively in the active `Shell` window. QuestDB will be stopped when the Shell is closed. ### Default root directory[​](https://questdb.com/docs/configuration/command-line-options/#default-root-directory-1 "Direct link to Default root directory") When started interactively, QuestDB's root directory defaults to the `current` directory. ### Stop[​](https://questdb.com/docs/configuration/command-line-options/#stop-1 "Direct link to Stop") To stop, press Ctrl+C in the terminal or close it directly. * [Options](https://questdb.com/docs/configuration/command-line-options/#options) * [Start](https://questdb.com/docs/configuration/command-line-options/#start) * [Stop](https://questdb.com/docs/configuration/command-line-options/#stop) * [Status](https://questdb.com/docs/configuration/command-line-options/#status) * [Install (Windows)](https://questdb.com/docs/configuration/command-line-options/#install-windows) * [Remove (Windows)](https://questdb.com/docs/configuration/command-line-options/#remove-windows) * [Interactive session (Windows)](https://questdb.com/docs/configuration/command-line-options/#interactive-session-windows) * [Default root directory](https://questdb.com/docs/configuration/command-line-options/#default-root-directory-1) * [Stop](https://questdb.com/docs/configuration/command-line-options/#stop-1) --- # Root directory structure | QuestDB On this page QuestDB creates the following file structure in its `root_directory`: questdb├── conf├── db├── log├── public└── snapshot (optional) By default, QuestDB's root directory will be the following: * Linux * macOS (Homebrew) * Windows $HOME/.questdb Path on Macs with Apple Silicon (M1 or M2) chip: /opt/homebrew/var/questdb Path on Macs with Intel chip: /usr/local/var/questdb C:\Windows\System32\qdbroot `conf` directory[​](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#conf-directory "Direct link to conf-directory") ------------------------------------------------------------------------------------------------------------------------------------------ Contains configuration files for QuestDB: ├── conf│   ├── date.formats│   ├── mime.types│   └── server.conf | file | description | | --- | --- | | `date.formats` | A list of date formats in plain text. | | `mime.types` | Mapping file used by the HTTP server to map file extension to response type when an user downloads a file. | | `server.conf` | Server configuration file. Find out more in the [server configuration](https://questdb.com/docs/configuration/overview/)
section. | `db` directory[​](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#db-directory "Direct link to db-directory") ------------------------------------------------------------------------------------------------------------------------------------ This directory contains all the files related to database tables. It is organised as follows: * Each table has its own `table_directory` under `root_directory/db/table_name` * Within a `table_directory`, each [partition](https://questdb.com/docs/concepts/partitions/) has its own `partition_directory`. * Within each `partition directory`, each column has its own `column_file`, for example `mycolumn.d` * If a given column has an [index](https://questdb.com/docs/concepts/deep-dive/indexes/) , then there will also be an `index_file`, for example `mycolumn.k` The table also stores metadata in `_meta` files: ├── db│   ├── Table│   │   │  │   │   ├── Partition 1│   │   │   ├── _archive│   │   │   ├── column1.d│   │   │   ├── column2.d│   │   │   ├── column2.k│   │   │   └── ...│   │   ├── Partition 2│   │   │   ├── _archive│   │   │   ├── column1.d│   │   │   ├── column2.d│   │   │   ├── column2.k│   │   │   └── ...│   │   │  │   │   ├── _meta│   │   ├── _txn│   │   └── _cv│   └── table_1.lock If the table is not partitioned, data is stored in a directory called `default`: ├── db│   ├── Table│   │   │  │   │   ├── default│   │   │   ├── _archive│   │   │   ├── column1.d│   │   │   ├── column2.d│   │   │   ├── column2.k│   │   │   └── ...│   │   ├── _meta│   │   └── _txn│   └── table_1.lock For a [WAL table](https://questdb.com/docs/concepts/write-ahead-log/) , the table structure contains one or more `wal` folders and a `seq` folder representing the Sequencer: ├── db│   ├── Table│   │   │  │   │   ├── Partition 1│   │   │   ├── _archive│   │   │   ├── column1.d│   │   │   ├── column2.d│   │   │   ├── column2.k│   │   │   └── ...│   │   ├── Partition 2│   │   │   ├── _archive│   │   │   ├── column1.d│   │   │   ├── column2.d│   │   │   ├── column2.k│   │   │   └── ...│   │   ├── txn_seq│   │   │   ├── _meta│   │   │   ├── _txnlog│   │   │   └── _wal_index.d│   │   ├── wal1│   │   │   └── 0│   │   │   ├── _meta│   │   │   ├── _event│   │   │   ├── column1.d│   │   │   ├── column2.d│   │   │ └── ...| | | │   │   ├── wal2│   │   │   └── 0│   │   │ │  ├── _meta│   │   │ │  ├── _event│   │   │ │  ├── column1.d│   │   │ │  ├── column2.d│   │   │ │  └── ...│   │   │   └── 1│   │   │      ├── _meta│   │   │   ├── _event│   │   │   ├── column1.d│   │   │   ├── column2.d│   │   │   └── ...│   │   │ │   │   ├── _meta│   │   ├── _txn│   │   └── _cv│   | caution As tempting as it may be to delete partitions by manually removing the directories from the file system, we really discourage this. The partitions are organised with metadata and deleting them directly could corrupt the table. We recommend you use [ALTER TABLE DROP PARTITION](https://questdb.com/docs/query/sql/alter-table-drop-partition/) for this effect. `log` directory[​](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#log-directory "Direct link to log-directory") --------------------------------------------------------------------------------------------------------------------------------------- Contains the [log files](https://questdb.com/docs/operations/logging-metrics/) for QuestDB: ├── log│   ├── stdout-2020-04-15T11-59-59.txt│   └── stdout-2020-04-12T13-31-22.txt Log files look like this: 2020-04-15T16:42:32.879970Z I i.q.c.TableReader new transaction [txn=2, transientRowCount=1, fixedRowCount=1, maxTimestamp=1585755801000000, attempts=0]2020-04-15T16:42:32.880051Z I i.q.g.FunctionParser call to_timestamp('2020-05-01:15:43:21','yyyy-MM-dd:HH:mm:ss') -> to_timestamp(Ss)2020-04-15T16:42:32.880657Z I i.q.c.p.WriterPool >> [table=`table_1`, thread=12]2020-04-15T16:42:32.881330Z I i.q.c.AppendMemory truncated and closed [fd=32]2020-04-15T16:42:32.881448Z I i.q.c.AppendMemory open /usr/local/var/questdb/db/table_1/2020-05/timestamp.d [fd=32, pageSize=16777216]2020-04-15T16:42:32.881708Z I i.q.c.AppendMemory truncated and closed [fd=33]2020-04-15T16:42:32.881830Z I i.q.c.AppendMemory open /usr/local/var/questdb/db/table_1/2020-05/temperature.d [fd=33, pageSize=16777216]2020-04-15T16:42:32.882092Z I i.q.c.AppendMemory truncated and closed [fd=34]2020-04-15T16:42:32.882210Z I i.q.c.AppendMemory open /usr/local/var/questdb/db/table_1/2020-05/humidity.d [fd=34, pageSize=16777216]2020-04-15T16:42:32.882248Z I i.q.c.TableWriter switched partition to '/usr/local/var/questdb/db/table_1/2020-05'2020-04-15T16:42:32.882571Z I i.q.c.p.WriterPool << [table=`table_1`, thread=12]2020-04-15T16:44:33.245144Z I i.q.c.AppendMemory truncated and closed [fd=32]2020-04-15T16:44:33.245418Z I i.q.c.AppendMemory truncated and closed [fd=33]2020-04-15T16:44:33.245712Z I i.q.c.AppendMemory truncated and closed [fd=34]2020-04-15T16:44:33.246096Z I i.q.c.ReadWriteMemory truncated and closed [fd=30]2020-04-15T16:44:33.246217Z I i.q.c.ReadOnlyMemory closed [fd=31]2020-04-15T16:44:33.246461Z I i.q.c.TableWriter closed 'table_1'2020-04-15T16:44:33.246492Z I i.q.c.p.WriterPool closed [table=`table_1`, reason=IDLE, by=12]2020-04-15T16:44:33.247184Z I i.q.c.OnePageMemory closed [fd=28]2020-04-15T16:44:33.247239Z I i.q.c.ReadOnlyMemory closed [fd=27]2020-04-15T16:44:33.247267Z I i.q.c.TableReader closed 'table_1'2020-04-15T16:44:33.247287Z I i.q.c.p.ReaderPool closed 'table_1' [at=0:0, reason=IDLE]2020-04-15T16:44:39.763406Z I http-server disconnected [ip=127.0.0.1, fd=24]2020-04-15T16:44:39.763729Z I i.q.c.h.HttpServer pushed `public` directory[​](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#public-directory "Direct link to public-directory") ------------------------------------------------------------------------------------------------------------------------------------------------ Contains the web files for the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) : └── public ├── assets │   ├── console-configuration.json │   └── favicon.webp ├── index.html ├── qdb.js ├── qdb.css └── ... `snapshot` directory[​](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#snapshot-directory "Direct link to snapshot-directory") ------------------------------------------------------------------------------------------------------------------------------------------------------ Created when a filesystem (disk) [snapshot](https://questdb.com/docs/query/sql/snapshot/) is collected. Contains table metadata file copies. `tmp` directory[​](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#tmp-directory "Direct link to tmp-directory") --------------------------------------------------------------------------------------------------------------------------------------- Created when a [`COPY`](https://questdb.com/docs/query/sql/copy/) SQL command is run for a partitioned table and no value is set for the `cairo.sql.copy.work.root` configuration setting. Contains temporary import files like indexes or temporary partitions. * [`conf` directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#conf-directory) * [`db` directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#db-directory) * [`log` directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#log-directory) * [`public` directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#public-directory) * [`snapshot` directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#snapshot-directory) * [`tmp` directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#tmp-directory) --- # QuestDB Storage Engine | QuestDB On this page Storage engine[​](https://questdb.com/docs/architecture/storage-engine/#storage-engine "Direct link to Storage engine") ------------------------------------------------------------------------------------------------------------------------ The QuestDB Storage Engine implements a row-based write path for maximum ingestion throughput and a column-based read path for maximum query performance. Table storage can be configured to use the QuestDB native binary format or to combine the QuestDB binary format for recent data with Parquet for older partitions. We refer to this model as a three-tier storage model. ### Tier One: Parallel Write-Ahead Log[​](https://questdb.com/docs/architecture/storage-engine/#tier-one-parallel-write-ahead-log "Direct link to Tier One: Parallel Write-Ahead Log") * **Two-phase writes**: All changes to data are recorded in a Write-Ahead Log (WAL) before they are written to the database files. This means that in case of a system crash or power failure, the database can recover to a consistent state by replaying the log entries. * **Commit and write separation**: By decoupling the transaction commit from the disk write process, a WAL improves the performance of write-intensive workloads, as it allows sequential disk writes, which are generally faster than random ones. * **Per-table WAL**: WAL files are separated per table, and also per active connection, allowing for concurrent data ingestion, modifications, and schema changes without locking the entire table. * **WAL consistency**: QuestDB implements a component called "Sequencer," which ensures that data appears consistent to all readers, even during ongoing write operations. ![Diagram showing WAL files consolidation](https://questdb.com/docs/images/guides/questdb-internals/walData.webp) The sequencer allocates unique txn numbers to transactions from different WALs chronologically and serves as the single source of truth, allowing for data deduplication and consolidation. ### Tier Two: QuestDB Binary Table Storage[​](https://questdb.com/docs/architecture/storage-engine/#tier-two-questdb-binary-table-storage "Direct link to Tier Two: QuestDB Binary Table Storage") Changes in the parallel WAL files are stored in columnar binary format by the TableWriter. The TableWriter also handles and resolves out-of-order data writes and enables deduplication. Column files use an append model. The active (most recent) partition for each table is always stored in this storage tier for minimum query latency and to optimize writes in the event of out-of-order data or when updating sampling intervals in materialized views. ### Tier Three: Parquet, Locally or in an Object Store[​](https://questdb.com/docs/architecture/storage-engine/#tier-three-parquet-locally-or-in-an-object-store "Direct link to Tier Three: Parquet, Locally or in an Object Store") Older partitions (any partition other than the most recent one) can be converted to [Parquet](https://questdb.com/docs/query/export-parquet/) for both interoperability and compression ratio. Partitions in Parquet format remain fully available for queries. Users don't need to know whether a partition is in QuestDB binary format or Parquet format. All the data types available in QuestDB can be converted to Parquet. When using QuestDB Enterprise, tables can be configured to convert to Parquet automatically and to send the Parquet files to object storage (Amazon S3, Microsoft Blob Storage, Google Cloud Storage, NFS...). This can help reduce the cost of storing historical data while keeping it fully available for queries. WHERE symbol in ('AAPL', 'NVDA') LATEST ON timestamp PARTITION BY symbol CREATE MATERIALIZED VIEW 'trades\_OHLC' min(price) AS low timestamp IN today() SELECT spread\_bps(bids\[1\]\[1\], asks\[1\]\[1\]) FROM read\_parquet('trades.parquet') SAMPLE BY 15m **Tier One:** Hot ingest (WAL), durable by default Incoming data is appended to the write-ahead log (WAL) with ultra-low latency. Writes are made durable before any processing, preserving order and surviving failures without data loss. The WAL is asynchronously shipped to object storage, so new replicas can bootstrap quickly and read the same history. **Tier Two:** Real-time SQL on live data Data is time-ordered and de-duplicated into QuestDB's native, time-partitioned columnar format and becomes immediately queryable. Power real-time analysis with vectorized, multi-core execution, streaming materialized views, and time-series SQL (e.g., ASOF JOIN, SAMPLE BY). The query planner spans tiers seamlessly. **Tier Three:** Cold storage, open and queryable Older data is automatically tiered to object storage in Apache Parquet. Query it in-place through QuestDB or use any tool that reads Parquet. This delivers predictable costs, interoperability with AI/ML tooling, and zero lock-in. ### Data Deduplication[​](https://questdb.com/docs/architecture/storage-engine/#data-deduplication "Direct link to Data Deduplication") When enabled, [data deduplication](https://questdb.com/docs/concepts/deduplication/) works on all the data inserted into the table and replaces matching rows with the new versions. Only new rows that do not match existing data will be inserted. Generally, if the data has mostly unique timestamps across all the rows, the performance impact of deduplication is low. Conversely, the most demanding data pattern occurs when there are many rows with the same timestamp that need to be deduplicated on additional columns. ### Column-oriented storage[​](https://questdb.com/docs/architecture/storage-engine/#column-oriented-storage "Direct link to Column-oriented storage") * **Data layout:** The system stores each table as separate files per column. Fixed-size data types use one file per column, while variable-size data types (such as `VARCHAR` or `STRING`) use two files per column. ![Architecture of the storage model with column files, readers/writers and the mapped memory](https://questdb.com/docs/images/guides/questdb-internals/columnarStorage.webp) Architecture of the storage model with multiple column files per partition * **CPU optimization:** Columnar storage improves CPU use during vectorized operations, which speeds up aggregations and computations. * **Compression:** Uniform data types allow efficient compression that reduces disk space and speeds up reads when [ZFS compression](https://questdb.com/docs/deployment/compression-zfs/) is enabled. Parquet files generated by QuestDB use native compression. ### Durability[​](https://questdb.com/docs/architecture/storage-engine/#durability "Direct link to Durability") By default, QuestDB relies on OS-level durability, letting the OS write dirty pages to disk. For stronger guarantees, enable sync commit mode: server.conf cairo.commit.mode=sync This invokes `fsync()` on each commit, ensuring data survives OS crashes or power loss at the cost of reduced write throughput. Next up[​](https://questdb.com/docs/architecture/storage-engine/#next-up "Direct link to Next up") --------------------------------------------------------------------------------------------------- Continue to [Memory Management](https://questdb.com/docs/architecture/memory-management/) to learn how QuestDB manages memory and integrates native code. * [Storage engine](https://questdb.com/docs/architecture/storage-engine/#storage-engine) * [Tier One: Parallel Write-Ahead Log](https://questdb.com/docs/architecture/storage-engine/#tier-one-parallel-write-ahead-log) * [Tier Two: QuestDB Binary Table Storage](https://questdb.com/docs/architecture/storage-engine/#tier-two-questdb-binary-table-storage) * [Tier Three: Parquet, Locally or in an Object Store](https://questdb.com/docs/architecture/storage-engine/#tier-three-parquet-locally-or-in-an-object-store) * [Data Deduplication](https://questdb.com/docs/architecture/storage-engine/#data-deduplication) * [Column-oriented storage](https://questdb.com/docs/architecture/storage-engine/#column-oriented-storage) * [Durability](https://questdb.com/docs/architecture/storage-engine/#durability) * [Next up](https://questdb.com/docs/architecture/storage-engine/#next-up) --- # Write-Ahead Log (WAL) | QuestDB On this page Write-Ahead Log (WAL) records all changes before applying them to storage. This enables concurrent writes, crash recovery, and replication. **WAL is enabled by default and recommended for all tables.** Why WAL matters[​](https://questdb.com/docs/concepts/write-ahead-log/#why-wal-matters "Direct link to Why WAL matters") ------------------------------------------------------------------------------------------------------------------------ | Capability | Description | | --- | --- | | **Concurrent writes** | Multiple clients can write simultaneously without blocking | | **Crash recovery** | Committed data is never lost — replay from log after restart | | **Replication** | WAL enables high availability and disaster recovery | | **Out-of-order handling** | Late-arriving data is merged efficiently | | **Deduplication** | Enables [DEDUP UPSERT KEYS](https://questdb.com/docs/concepts/deduplication/) | In QuestDB Enterprise, WAL segments are sent to object storage immediately on commit, enabling real-time replication to standby nodes. Creating WAL tables[​](https://questdb.com/docs/concepts/write-ahead-log/#creating-wal-tables "Direct link to Creating WAL tables") ------------------------------------------------------------------------------------------------------------------------------------ WAL is enabled by default for partitioned tables: CREATE TABLE prices ( ts TIMESTAMP, ticker SYMBOL, price DOUBLE) TIMESTAMP(ts) PARTITION BY DAY;-- This is a WAL table (default) You can be explicit with the `WAL` keyword: CREATE TABLE prices (...)TIMESTAMP(ts) PARTITION BY DAY WAL; Requirements[​](https://questdb.com/docs/concepts/write-ahead-log/#requirements "Direct link to Requirements") --------------------------------------------------------------------------------------------------------------- **WAL requires partitioning.** Non-partitioned tables cannot use WAL. | Table creation method | Default partitioning | WAL enabled? | | --- | --- | --- | | SQL `CREATE TABLE` without `PARTITION BY` | None | **No** | | SQL `CREATE TABLE` with `PARTITION BY` | As specified | Yes | | ILP auto-created tables | `PARTITION BY DAY` | Yes | -- Non-partitioned = no WAL (not recommended for time-series)CREATE TABLE static_data (key VARCHAR, value VARCHAR);-- Partitioned = WAL enabled (recommended)CREATE TABLE prices (...)TIMESTAMP(ts) PARTITION BY DAY; If you need WAL features (concurrent writes, replication, deduplication), always specify `PARTITION BY` when creating tables via SQL. Checking WAL status[​](https://questdb.com/docs/concepts/write-ahead-log/#checking-wal-status "Direct link to Checking WAL status") ------------------------------------------------------------------------------------------------------------------------------------ Check if a table uses WAL: SELECT name, walEnabled FROM tables() WHERE name = 'prices'; Check WAL table status: SELECT * FROM wal_tables(); If WAL transactions are suspended (rare), resume them: ALTER TABLE prices RESUME WAL; How WAL works[​](https://questdb.com/docs/concepts/write-ahead-log/#how-wal-works "Direct link to How WAL works") ------------------------------------------------------------------------------------------------------------------ When data is written to a WAL table: 1. Data is written to WAL segments (fast sequential writes) 2. Transaction is committed and acknowledged to client 3. WAL apply job merges data into table storage asynchronously 4. In Enterprise, WAL segments replicate to object storage This decouples the commit (fast) from storage application (background), enabling high write throughput. ![Diagram showing the sequencer allocating txn numbers to events chronologically](https://questdb.com/docs/images/docs/concepts/wal_sequencer.webp) The sequencer allocates unique transaction numbers and serves as the single source of truth. ![Diagram showing the WAL job application and WAL collect events and commit to QuestDB](https://questdb.com/docs/images/docs/concepts/wal_process.webp) The WAL apply job collects transactions sequentially for writing to storage. Configuration[​](https://questdb.com/docs/concepts/write-ahead-log/#configuration "Direct link to Configuration") ------------------------------------------------------------------------------------------------------------------ WAL behavior can be tuned via server configuration: * `cairo.wal.enabled.default` — WAL enabled by default (default: `true`) * Parallel threads for WAL application — see [WAL configuration](https://questdb.com/docs/configuration/overview/#wal-table-configurations) To convert an existing table between WAL and non-WAL: ALTER TABLE prices SET TYPE WAL;-- Requires database restart to take effect See [ALTER TABLE SET TYPE](https://questdb.com/docs/query/sql/alter-table-set-type/) for details. See also[​](https://questdb.com/docs/concepts/write-ahead-log/#see-also "Direct link to See also") --------------------------------------------------------------------------------------------------- * [Replication](https://questdb.com/docs/high-availability/overview/) — high availability and failover * [Deduplication](https://questdb.com/docs/concepts/deduplication/) — requires WAL * [CREATE TABLE](https://questdb.com/docs/query/sql/create-table/#write-ahead-log-wal-settings) — WAL syntax * [Why WAL matters](https://questdb.com/docs/concepts/write-ahead-log/#why-wal-matters) * [Creating WAL tables](https://questdb.com/docs/concepts/write-ahead-log/#creating-wal-tables) * [Requirements](https://questdb.com/docs/concepts/write-ahead-log/#requirements) * [Checking WAL status](https://questdb.com/docs/concepts/write-ahead-log/#checking-wal-status) * [How WAL works](https://questdb.com/docs/concepts/write-ahead-log/#how-wal-works) * [Configuration](https://questdb.com/docs/concepts/write-ahead-log/#configuration) * [See also](https://questdb.com/docs/concepts/write-ahead-log/#see-also) --- # Views | QuestDB On this page A view is a **virtual table** defined by a SQL `SELECT` statement. Views do not store data themselves; instead, their defining query is executed as a sub-query whenever the view is referenced. What are views for?[​](https://questdb.com/docs/concepts/views/#what-are-views-for "Direct link to What are views for?") ------------------------------------------------------------------------------------------------------------------------- Views provide several benefits: * **Abstraction**: Hide complex queries behind simple table-like interfaces * **Reusability**: Define queries once, use them everywhere * **Security**: Control data access without exposing underlying tables * **Maintainability**: Single source of truth for business logic Quick example -- Create a viewCREATE VIEW hourly_summary AS ( SELECT ts, symbol, sum(quantity) as volume FROM trades SAMPLE BY 1h);-- Query the view like a tableSELECT * FROM hourly_summary WHERE symbol = 'AAPL'; Creating views[​](https://questdb.com/docs/concepts/views/#creating-views "Direct link to Creating views") ----------------------------------------------------------------------------------------------------------- Use `CREATE VIEW` to define a new view: Basic view CREATE VIEW daily_prices AS ( SELECT ts, symbol, last(price) as closing_price FROM trades SAMPLE BY 1d) ### CREATE IF NOT EXISTS[​](https://questdb.com/docs/concepts/views/#create-if-not-exists "Direct link to CREATE IF NOT EXISTS") To avoid errors when the view already exists: CREATE VIEW IF NOT EXISTS price_view AS ( SELECT symbol, last(price) as price FROM trades SAMPLE BY 1h) ### CREATE OR REPLACE[​](https://questdb.com/docs/concepts/views/#create-or-replace "Direct link to CREATE OR REPLACE") To update an existing view or create it if it doesn't exist: CREATE OR REPLACE VIEW price_view AS ( SELECT symbol, last(price) as price, ts FROM trades SAMPLE BY 1h) For full syntax details, see [CREATE VIEW](https://questdb.com/docs/query/sql/create-view/) . Querying views[​](https://questdb.com/docs/concepts/views/#querying-views "Direct link to Querying views") ----------------------------------------------------------------------------------------------------------- Views are queried exactly like tables: SELECT * FROM my_viewSELECT ts, price FROM my_view WHERE symbol = 'AAPL'SELECT v1.ts, v2.valueFROM view1 v1JOIN view2 v2 ON v1.id = v2.id ### Optimizer transparency[​](https://questdb.com/docs/concepts/views/#optimizer-transparency "Direct link to Optimizer transparency") Views in QuestDB are fully transparent to the query optimizer. When you query a view, the optimizer treats it exactly as if you had written the view's query inline as a sub-query. This means views benefit from the complete suite of query optimizations: * **Filter push-down**: WHERE conditions are pushed to base tables * **Projection push-down**: Only required columns are read from storage * **Join optimization**: Join order and strategies are optimized across view boundaries * **ORDER BY optimization**: Sorting can leverage table indexes * **Timestamp optimizations**: Time-based operations use partition pruning -- View definitionCREATE VIEW trades_view AS ( SELECT ts, symbol, price, quantity FROM trades WHERE price > 0)-- This query is optimized as if written inlineSELECT ts, price FROM trades_view WHERE symbol = 'AAPL' ORDER BY ts-- Optimizer sees: SELECT ts, price FROM trades WHERE price > 0 AND symbol = 'AAPL' ORDER BY ts-- Only ts and price columns are read, filters applied at scan, ordering uses index Use `EXPLAIN` to see how the optimizer processes view queries: EXPLAIN SELECT * FROM trades_view WHERE symbol = 'AAPL' There is no performance penalty for using views compared to writing equivalent sub-queries directly. Parameterized views[​](https://questdb.com/docs/concepts/views/#parameterized-views "Direct link to Parameterized views") -------------------------------------------------------------------------------------------------------------------------- Views support the `DECLARE` statement to define parameters with default values. Use `OVERRIDABLE` to allow users to change parameter values at query time. ### Creating a parameterized view[​](https://questdb.com/docs/concepts/views/#creating-a-parameterized-view "Direct link to Creating a parameterized view") CREATE VIEW filtered_trades AS ( DECLARE OVERRIDABLE @min_price := 100 SELECT ts, symbol, price FROM trades WHERE price >= @min_price) ### Querying with default parameters[​](https://questdb.com/docs/concepts/views/#querying-with-default-parameters "Direct link to Querying with default parameters") SELECT * FROM filtered_trades-- Uses default @min_price = 100 ### Overriding parameters[​](https://questdb.com/docs/concepts/views/#overriding-parameters "Direct link to Overriding parameters") DECLARE @min_price := 500 SELECT * FROM filtered_trades-- Overrides @min_price to 500 ### Multiple parameters[​](https://questdb.com/docs/concepts/views/#multiple-parameters "Direct link to Multiple parameters") By default, parameters are **non-overridable**. Use `OVERRIDABLE` to allow override at query time: CREATE VIEW price_range AS ( DECLARE OVERRIDABLE @lo := 100, OVERRIDABLE @hi := 1000 SELECT ts, symbol, price FROM trades WHERE price >= @lo AND price <= @hi)-- Query with custom rangeDECLARE @lo := 50, @hi := 200 SELECT * FROM price_range ### Non-overridable parameters[​](https://questdb.com/docs/concepts/views/#non-overridable-parameters "Direct link to Non-overridable parameters") Parameters without `OVERRIDABLE` cannot be changed at query time, providing security for sensitive filters: CREATE VIEW secure_view AS ( DECLARE @min_value := 0 SELECT * FROM trades WHERE value >= @min_value)-- This will fail with "variable is not overridable: @min_value"DECLARE @min_value := -100 SELECT * FROM secure_view ### Mixed parameters[​](https://questdb.com/docs/concepts/views/#mixed-parameters "Direct link to Mixed parameters") Combine overridable and non-overridable parameters: CREATE VIEW mixed_params AS ( DECLARE @fixed_filter := 'active', OVERRIDABLE @limit := 100 SELECT * FROM data WHERE status = @fixed_filter LIMIT @limit)-- @limit can be overridden, @fixed_filter cannotDECLARE @limit := 50 SELECT * FROM mixed_params View hierarchies[​](https://questdb.com/docs/concepts/views/#view-hierarchies "Direct link to View hierarchies") ----------------------------------------------------------------------------------------------------------------- Views can reference other views, tables, and materialized views: -- Level 1: Raw data filteringCREATE VIEW valid_trades AS ( SELECT * FROM trades WHERE price > 0 AND quantity > 0)-- Level 2: AggregationCREATE VIEW hourly_stats AS ( SELECT ts, symbol, sum(quantity) as volume FROM valid_trades SAMPLE BY 1h)-- Level 3: Derived metricsCREATE VIEW hourly_vwap AS ( SELECT ts, symbol, volume, turnover / volume as vwap FROM hourly_stats WHERE volume > 0) tip Keep view hierarchies shallow (3-4 levels maximum) for better query planning and maintainability. View management[​](https://questdb.com/docs/concepts/views/#view-management "Direct link to View management") -------------------------------------------------------------------------------------------------------------- ### Listing views[​](https://questdb.com/docs/concepts/views/#listing-views "Direct link to Listing views") SELECT * FROM views() Returns: | Column | Description | | --- | --- | | `view_name` | Name of the view | | `view_sql` | The SQL definition | | `view_table_dir_name` | Internal directory name | | `invalidation_reason` | Error message if view is invalid | | `view_status` | `valid` or `invalid` | | `view_status_update_time` | Timestamp of last status change | ### Show view definition[​](https://questdb.com/docs/concepts/views/#show-view-definition "Direct link to Show view definition") SHOW CREATE VIEW my_view Returns the `CREATE VIEW` statement that would recreate the view. ### Show view columns[​](https://questdb.com/docs/concepts/views/#show-view-columns "Direct link to Show view columns") SHOW COLUMNS FROM my_view ### Altering views[​](https://questdb.com/docs/concepts/views/#altering-views "Direct link to Altering views") To modify an existing view's definition: ALTER VIEW my_view AS (SELECT col1, col2 FROM my_table WHERE col1 > 0) For full syntax, see [ALTER VIEW](https://questdb.com/docs/query/sql/alter-view/) . ### Dropping views[​](https://questdb.com/docs/concepts/views/#dropping-views "Direct link to Dropping views") DROP VIEW my_view-- Or safely:DROP VIEW IF EXISTS my_view For full syntax, see [DROP VIEW](https://questdb.com/docs/query/sql/drop-view/) . View invalidation[​](https://questdb.com/docs/concepts/views/#view-invalidation "Direct link to View invalidation") -------------------------------------------------------------------------------------------------------------------- Views are automatically invalidated when their dependencies change: | Operation | Effect | | --- | --- | | `DROP TABLE` | View becomes invalid | | `RENAME TABLE` | View becomes invalid | | `DROP COLUMN` | View becomes invalid if column is referenced | | `RENAME COLUMN` | View becomes invalid if column is referenced | | Column type change | View metadata is updated | ### Automatic recovery[​](https://questdb.com/docs/concepts/views/#automatic-recovery "Direct link to Automatic recovery") Views are automatically revalidated when the invalidating condition is reversed: * If a table is dropped and later recreated, dependent views become valid again * If a column is renamed back to its original name, dependent views become valid again ### Checking view status[​](https://questdb.com/docs/concepts/views/#checking-view-status "Direct link to Checking view status") SELECT view_name, view_status, invalidation_reasonFROM views()WHERE view_status = 'invalid' Views in tables() output[​](https://questdb.com/docs/concepts/views/#views-in-tables-output "Direct link to Views in tables() output") --------------------------------------------------------------------------------------------------------------------------------------- Views appear in the `tables()` function with `table_type = 'V'`: SELECT table_name, table_type FROM tables() | table\_type | Description | | --- | --- | | `T` | Regular table | | `V` | View | | `M` | Materialized view | Views vs materialized views[​](https://questdb.com/docs/concepts/views/#views-vs-materialized-views "Direct link to Views vs materialized views") -------------------------------------------------------------------------------------------------------------------------------------------------- Understanding when to use each type is important for performance: | Feature | View | Materialized View | | --- | --- | --- | | Data storage | None (virtual) | Physical storage | | Query execution | On every access | Pre-computed | | Data freshness | Always current | Depends on refresh | | Performance | Query-time cost | Read-time benefit | | Storage cost | Zero | Proportional to result size | ### When to use views[​](https://questdb.com/docs/concepts/views/#when-to-use-views "Direct link to When to use views") * Simple transformations that execute quickly * Data that must always be current * Ad-hoc analysis where requirements change frequently * Parameterized queries with `DECLARE` * Low-frequency queries ### When to use materialized views[​](https://questdb.com/docs/concepts/views/#when-to-use-materialized-views "Direct link to When to use materialized views") * Heavy aggregations over large datasets * Frequently accessed summary data * Dashboard queries that run repeatedly * Historical summaries that don't need real-time accuracy For detailed comparisons and examples, see [Materialized Views](https://questdb.com/docs/concepts/materialized-views/) . Security with views[​](https://questdb.com/docs/concepts/views/#security-with-views "Direct link to Security with views") -------------------------------------------------------------------------------------------------------------------------- Views provide a security boundary between users and underlying data. ### Definer security model (Enterprise)[​](https://questdb.com/docs/concepts/views/#definer-security-model-enterprise "Direct link to Definer security model (Enterprise)") Views use a **definer security model**. When a view is created, the creator's permissions are captured. Users querying the view only need `SELECT` permission on the view itself - they do **not** need permissions on the underlying tables. -- Admin creates a view on sensitive tableCREATE VIEW public_summary AS ( SELECT date, region, sum(sales) as total FROM sensitive_sales GROUP BY date, region);-- Grant SELECT on the view to analystsGRANT SELECT ON public_summary TO analyst_role;-- Analysts can query the view without access to sensitive_salesSELECT * FROM public_summary; -- Works!SELECT * FROM sensitive_sales; -- Access denied! The view's definer permissions are preserved even if the creator's account is later disabled or deleted. ### No column-level permissions on views[​](https://questdb.com/docs/concepts/views/#no-column-level-permissions-on-views "Direct link to No column-level permissions on views") Unlike tables, views do **not** support column-level permissions. You can only grant or revoke permissions on the entire view: -- This works: grant SELECT on entire viewGRANT SELECT ON my_view TO user1;-- Column-level permissions are NOT supported for views-- Use separate views to expose different column subsets To provide different column access to different users, create multiple views with different column selections. ### Security patterns[​](https://questdb.com/docs/concepts/views/#security-patterns "Direct link to Security patterns") Views enable several security patterns: * **Data subsetting**: Expose only specific rows or columns * **Column masking**: Hide sensitive columns from certain users * **Row-level security**: Filter rows based on business rules * **Aggregation-only access**: Provide summaries without raw data access ### Column-level security example[​](https://questdb.com/docs/concepts/views/#column-level-security-example "Direct link to Column-level security example") -- Base table with sensitive dataCREATE TABLE employees ( id LONG, name VARCHAR, salary DOUBLE, -- Sensitive department VARCHAR, hire_date TIMESTAMP);-- View exposing only non-sensitive columnsCREATE VIEW employees_public AS ( SELECT id, name, department, hire_date FROM employees);-- Grant access to public view onlyGRANT SELECT ON employees_public TO analyst_role; ### Row-level security example[​](https://questdb.com/docs/concepts/views/#row-level-security-example "Direct link to Row-level security example") -- View for specific trading deskCREATE VIEW desk_a_trades AS ( SELECT * FROM trades WHERE trader_id IN (101, 102, 103));GRANT SELECT ON desk_a_trades TO desk_a_users; For more details on permissions, see [Role-Based Access Control (RBAC)](https://questdb.com/docs/security/rbac/) . Performance considerations[​](https://questdb.com/docs/concepts/views/#performance-considerations "Direct link to Performance considerations") ----------------------------------------------------------------------------------------------------------------------------------------------- ### Views don't cache results[​](https://questdb.com/docs/concepts/views/#views-dont-cache-results "Direct link to Views don't cache results") Every query against a view executes the underlying query. For expensive aggregations accessed frequently, consider materialized views. ### Optimize with indexes[​](https://questdb.com/docs/concepts/views/#optimize-with-indexes "Direct link to Optimize with indexes") Create indexes on base table columns used in view filters: ALTER TABLE trades ALTER COLUMN symbol ADD INDEX ### Check query plans[​](https://questdb.com/docs/concepts/views/#check-query-plans "Direct link to Check query plans") Always examine query plans when optimizing: EXPLAIN SELECT * FROM my_view WHERE symbol = 'AAPL' ### Best practices[​](https://questdb.com/docs/concepts/views/#best-practices "Direct link to Best practices") * Use indexed columns in filters for best performance * Use parameterized views for common filter patterns * Avoid deeply nested view hierarchies (>3-4 levels) for maintainability * Consider materialized views for expensive aggregations that run frequently Limitations[​](https://questdb.com/docs/concepts/views/#limitations "Direct link to Limitations") -------------------------------------------------------------------------------------------------- 1. **No data storage**: Views don't store data - the query runs each time 2. **No indexes**: Views cannot have indexes; filtering relies on underlying table indexes 3. **Circular references**: Views cannot reference themselves or create circular dependencies 4. **Read-only**: You cannot INSERT, UPDATE, or DELETE on views 5. **No DDL operations**: You cannot run DDL operations (like `RENAME TABLE`) on views 6. **No column-level permissions**: Unlike tables, you cannot grant permissions on individual view columns (Enterprise) Related documentation[​](https://questdb.com/docs/concepts/views/#related-documentation "Direct link to Related documentation") -------------------------------------------------------------------------------------------------------------------------------- * **SQL Commands** * [`CREATE VIEW`](https://questdb.com/docs/query/sql/create-view/) : Create a new view * [`ALTER VIEW`](https://questdb.com/docs/query/sql/alter-view/) : Modify a view definition * [`DROP VIEW`](https://questdb.com/docs/query/sql/drop-view/) : Remove a view * **Related Concepts** * [Materialized Views](https://questdb.com/docs/concepts/materialized-views/) : Pre-computed query results * [DECLARE](https://questdb.com/docs/query/sql/declare/) : Parameter declaration for views * [What are views for?](https://questdb.com/docs/concepts/views/#what-are-views-for) * [Creating views](https://questdb.com/docs/concepts/views/#creating-views) * [CREATE IF NOT EXISTS](https://questdb.com/docs/concepts/views/#create-if-not-exists) * [CREATE OR REPLACE](https://questdb.com/docs/concepts/views/#create-or-replace) * [Querying views](https://questdb.com/docs/concepts/views/#querying-views) * [Optimizer transparency](https://questdb.com/docs/concepts/views/#optimizer-transparency) * [Parameterized views](https://questdb.com/docs/concepts/views/#parameterized-views) * [Creating a parameterized view](https://questdb.com/docs/concepts/views/#creating-a-parameterized-view) * [Querying with default parameters](https://questdb.com/docs/concepts/views/#querying-with-default-parameters) * [Overriding parameters](https://questdb.com/docs/concepts/views/#overriding-parameters) * [Multiple parameters](https://questdb.com/docs/concepts/views/#multiple-parameters) * [Non-overridable parameters](https://questdb.com/docs/concepts/views/#non-overridable-parameters) * [Mixed parameters](https://questdb.com/docs/concepts/views/#mixed-parameters) * [View hierarchies](https://questdb.com/docs/concepts/views/#view-hierarchies) * [View management](https://questdb.com/docs/concepts/views/#view-management) * [Listing views](https://questdb.com/docs/concepts/views/#listing-views) * [Show view definition](https://questdb.com/docs/concepts/views/#show-view-definition) * [Show view columns](https://questdb.com/docs/concepts/views/#show-view-columns) * [Altering views](https://questdb.com/docs/concepts/views/#altering-views) * [Dropping views](https://questdb.com/docs/concepts/views/#dropping-views) * [View invalidation](https://questdb.com/docs/concepts/views/#view-invalidation) * [Automatic recovery](https://questdb.com/docs/concepts/views/#automatic-recovery) * [Checking view status](https://questdb.com/docs/concepts/views/#checking-view-status) * [Views in tables() output](https://questdb.com/docs/concepts/views/#views-in-tables-output) * [Views vs materialized views](https://questdb.com/docs/concepts/views/#views-vs-materialized-views) * [When to use views](https://questdb.com/docs/concepts/views/#when-to-use-views) * [When to use materialized views](https://questdb.com/docs/concepts/views/#when-to-use-materialized-views) * [Security with views](https://questdb.com/docs/concepts/views/#security-with-views) * [Definer security model (Enterprise)](https://questdb.com/docs/concepts/views/#definer-security-model-enterprise) * [No column-level permissions on views](https://questdb.com/docs/concepts/views/#no-column-level-permissions-on-views) * [Security patterns](https://questdb.com/docs/concepts/views/#security-patterns) * [Column-level security example](https://questdb.com/docs/concepts/views/#column-level-security-example) * [Row-level security example](https://questdb.com/docs/concepts/views/#row-level-security-example) * [Performance considerations](https://questdb.com/docs/concepts/views/#performance-considerations) * [Views don't cache results](https://questdb.com/docs/concepts/views/#views-dont-cache-results) * [Optimize with indexes](https://questdb.com/docs/concepts/views/#optimize-with-indexes) * [Check query plans](https://questdb.com/docs/concepts/views/#check-query-plans) * [Best practices](https://questdb.com/docs/concepts/views/#best-practices) * [Limitations](https://questdb.com/docs/concepts/views/#limitations) * [Related documentation](https://questdb.com/docs/concepts/views/#related-documentation) --- # Configuration | QuestDB On this page This page describes methods for configuring QuestDB server settings. Configuration can be set either: * In the `server.conf` configuration file available in the [root directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/) * Using environment variables When a key is absent from both the config file and the environment variables, the default value is used. note **For Windows users** When entering path values, use either `\\` or `/` instead of the native path separator char `\`. * 👍 `C:\\path\\to\\file\\path` * 👍 `C:/path/to/file` * 👎 `C:\path\to\file` The single backslash is interpreted as an escape sequence start within [Java properties](https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html) . Environment variables[​](https://questdb.com/docs/configuration/overview/#environment-variables "Direct link to Environment variables") ---------------------------------------------------------------------------------------------------------------------------------------- All settings in the configuration file can be set or overridden using environment variables. If a key is set in both the `server.conf` file and via an environment variable, the environment variable will take precedence and the value in the server configuration file will be ignored. To make these configuration settings available to QuestDB via environment variables, they must be in the following format: QDB_ Where `` is equal to the configuration key name. To properly format a `server.conf` key as an environment variable it must have: 1. `QDB_` prefix 2. uppercase characters 3. all `.` period characters replaced with `_` underscore For example, the server configuration key for query timeout must be passed as described below: | `server.conf` key | env var | | --- | --- | | `query.timeout` | `QDB_QUERY_TIMEOUT` | note QuestDB applies these configuration changes on startup and a running instance must be restarted in order for configuration changes to take effect. ### Examples[​](https://questdb.com/docs/configuration/overview/#examples "Direct link to Examples") The following configuration property customizes the query timeout: conf/server.conf query.timeout=120s Customizing the query timeout via environment variable export QDB_QUERY_TIMEOUT=120s Secrets from files[​](https://questdb.com/docs/configuration/overview/#secrets-from-files "Direct link to Secrets from files") ------------------------------------------------------------------------------------------------------------------------------- QuestDB supports reading sensitive configuration values from files using the `_FILE` suffix convention. This is useful in containerized environments like Kubernetes, where secrets are typically mounted as files rather than passed as environment variables. When a `_FILE` variant is set, QuestDB reads the secret value from the specified file path. This works with both environment variables and properties in `server.conf`. ### Usage[​](https://questdb.com/docs/configuration/overview/#usage "Direct link to Usage") **Environment variable:** QDB_PG_PASSWORD_FILE=/run/secrets/pg-password **Property file:** server.conf pg.password.file=/run/secrets/pg-password ### Precedence[​](https://questdb.com/docs/configuration/overview/#precedence "Direct link to Precedence") If both a `_FILE` variant and the direct value are set, the `_FILE` variant takes precedence. For example, if both `QDB_PG_PASSWORD_FILE` and `QDB_PG_PASSWORD` are set, the value is read from the file. ### File requirements[​](https://questdb.com/docs/configuration/overview/#file-requirements "Direct link to File requirements") Secret files must meet the following requirements: * **Maximum size**: 64KB * **Encoding**: UTF-8 * **Content handling**: Leading and trailing whitespace is automatically trimmed The following paths are not allowed for security reasons: * Paths containing `..` (path traversal) * Paths starting with `/dev/`, `/proc/`, or `/sys/` * Directories (including symlinks to directories) If a secret file is empty or contains only whitespace, QuestDB logs an advisory warning, as this may weaken authentication. ### Error handling[​](https://questdb.com/docs/configuration/overview/#error-handling "Direct link to Error handling") If a secret file cannot be read at startup, QuestDB fails to start. This includes cases where the file does not exist, is too large, or the path is not allowed. During runtime, if `reload_config()` cannot read a secret file, the reload fails and the previous value is retained. This ensures the server continues operating if a secret file is temporarily unavailable. ### Reloading secrets[​](https://questdb.com/docs/configuration/overview/#reloading-secrets "Direct link to Reloading secrets") Secrets loaded from files support runtime reloading. After updating a secret file, call `reload_config()` to apply the new value. See [Reloadable settings](https://questdb.com/docs/configuration/overview/#reloadable-settings) for details. To verify that a secret was loaded from a file, run `SHOW PARAMETERS` and check the `value_source` column, which displays `file` for secrets loaded from files. ### Supported properties[​](https://questdb.com/docs/configuration/overview/#supported-properties "Direct link to Supported properties") The following properties support the `_FILE` suffix: | Property | Environment variable | | --- | --- | | `pg.password` | `QDB_PG_PASSWORD_FILE` | | `pg.readonly.password` | `QDB_PG_READONLY_PASSWORD_FILE` | | `http.password` | `QDB_HTTP_PASSWORD_FILE` | #### Enterprise properties[​](https://questdb.com/docs/configuration/overview/#enterprise-properties "Direct link to Enterprise properties") The following additional properties are available in [QuestDB Enterprise](https://questdb.com/enterprise/) : | Property | Environment variable | | --- | --- | | `acl.admin.password` | `QDB_ACL_ADMIN_PASSWORD_FILE` | | `acl.oidc.tls.keystore.password` | `QDB_ACL_OIDC_TLS_KEYSTORE_PASSWORD_FILE` | | `replication.object.store` | `QDB_REPLICATION_OBJECT_STORE_FILE` | | `cold.storage.object.store` | `QDB_COLD_STORAGE_OBJECT_STORE_FILE` | | `backup.object.store.*` | `QDB_BACKUP_OBJECT_STORE_*_FILE` | For Kubernetes-specific examples, see the [Kubernetes deployment guide](https://questdb.com/docs/deployment/kubernetes/#using-kubernetes-secrets) . Reloadable settings[​](https://questdb.com/docs/configuration/overview/#reloadable-settings "Direct link to Reloadable settings") ---------------------------------------------------------------------------------------------------------------------------------- Certain configuration settings can be reloaded without having to restart the server. To reload a setting, edit its value in the `server.conf` file and then run the `reload_config` SQL function: Reload server configuration SELECT reload_config(); If the value was reloaded successfully, the `reload_config` function returns `true` and a message is printed to the server log: 2025-01-02T09:52:40.833848UTC I i.q.DynamicPropServerConfiguration reloaded config option [update, key=http.net.connection.limit, old=100, new=200] Each key has a `reloadable` property that indicates whether the key can be reloaded. If yes, the `reload_config` function can be used to reload the configuration. All reloadable properties can be also queried from the server: Query reloadable properties (SHOW PARAMETERS) WHERE reloadable = true; Keys and default values[​](https://questdb.com/docs/configuration/overview/#keys-and-default-values "Direct link to Keys and default values") ---------------------------------------------------------------------------------------------------------------------------------------------- This section lists the configuration keys available to QuestDB by topic or subsystem. Parameters for specifying buffer and memory page sizes are provided in the format `n`, where `` can be one of the following: * `m` for **MB** * `k` for **kB** For example: Setting maximum send buffer size to 2MB per TCP socket http.net.connection.sndbuf=2m ### Shared worker[​](https://questdb.com/docs/configuration/overview/#shared-worker "Direct link to Shared worker") QuestDB uses three specialized worker pools to handle different workloads: * **Network pool**: handles HTTP, PostgreSQL, and ILP server I/O * **Query pool**: executes parallel query operations (filters, group-by) * **Write pool**: manages WAL apply jobs, table writes, materialized view refresh, and housekeeping tasks | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | shared.network.worker.count | max(2, CPU count - 2) if CPU count > 32, max(2, CPU count - 1) if CPU count > 16, otherwise max(2, CPU count) | No | Number of worker threads for the network pool, which handles HTTP, PostgreSQL, and ILP server I/O. Increasing this number will increase network I/O parallelism at the expense of CPU resources. | | shared.network.worker.affinity | none | No | Comma-delimited list of CPU ids, one per thread specified in `shared.network.worker.count`. By default, threads have no CPU affinity. | | shared.query.worker.count | max(2, CPU count - 2) if CPU count > 32, max(2, CPU count - 1) if CPU count > 16, otherwise max(2, CPU count) | No | Number of worker threads for the query pool, which executes parallel query operations (filters, group-by). Increasing this number will increase query parallelism at the expense of CPU resources. | | shared.query.worker.affinity | none | No | Comma-delimited list of CPU ids, one per thread specified in `shared.query.worker.count`. By default, threads have no CPU affinity. | | shared.write.worker.count | max(2, CPU count - 2) if CPU count > 32, max(2, CPU count - 1) if CPU count > 16, otherwise max(2, CPU count) | No | Number of worker threads for the write pool, which manages WAL apply jobs, table writes, materialized view refresh, and housekeeping tasks. Increasing this number will increase write parallelism at the expense of CPU resources. | | shared.write.worker.affinity | none | No | Comma-delimited list of CPU ids, one per thread specified in `shared.write.worker.count`. By default, threads have no CPU affinity. | | shared.worker.haltOnError | false | No | Flag that indicates if the worker thread must stop when an unexpected error occurs. | ### HTTP server[​](https://questdb.com/docs/configuration/overview/#http-server "Direct link to HTTP server") This section describes configuration settings for the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) and the REST API available by default on port `9000`. For details on the use of this component, refer to the [web console documentation](https://questdb.com/docs/getting-started/web-console/overview/) page. | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | http.enabled | true | No | Enable or disable HTTP server. | | http.bind.to | 0.0.0.0:9000 | No | IP address and port of HTTP server. A value of `0` means that the HTTP server will bind to all network interfaces. You can specify IP address of any individual network interface on your system. | | http.user | N/A | No | Username for HTTP Basic Authentication in QuestDB Open Source. QuestDB Enterprise Edition supports more advanced authentication mechanisms: RBAC | | http.password | N/A | No | Password for HTTP Basic Authentication in QuestDB Open Source. QuestDB Enterprise Edition supports more advanced authentication mechanisms: RBAC | | http.net.connection.limit | 64 | No | The maximum number permitted for simultaneous TCP connection to the HTTP server. The rationale of the value is to control server memory consumption. | | http.query.connection.limit | none | No | Soft limit for simultaneous HTTP query connections. When breached, new connections will be rejected but existing connections won't be closed immediately as long as http.net.connection.limit is not exceeded. | | http.ilp.connection.limit | none | No | Soft limit for simultaneous ILP connections. When breached, new connections will be rejected but existing connections won't be closed immediately as long as http.net.connection.limit is not exceeded. | | http.net.connection.timeout | 300000 | No | TCP connection idle timeout in milliseconds. Connection is closed by HTTP server when this timeout lapses. | | http.net.connection.sndbuf | 2M | No | Maximum send buffer size on each TCP socket. If this value is `-1`, the socket send buffer size remains unchanged from the OS defaults. | | http.net.connection.rcvbuf | 2M | No | Maximum receive buffer size on each TCP socket. If this value is `-1`, the socket receive buffer size remains unchanged from the OS defaults. | | http.net.connection.hint | false | No | Windows specific flag to overcome OS limitations on TCP backlog size | | http.net.connection.queue.timeout | 5000 | No | Amount of time in milliseconds a connection can wait in the listen backlog queue before it is refused. Connections will be aggressively removed from the backlog until the active connection limit is breached. | | http.net.bind.to | 0.0.0.0:9000 | No | IP address and port of HTTP server. | | http.connection.pool.initial.capacity | 4 | No | Initial size of pool of reusable objects that hold connection state. The pool should be configured to maximum realistic load so that it does not resize at runtime. | | http.connection.string.pool.capacity | 128 | No | Initial size of the string pool shared by the HTTP header and multipart content parsers. | | http.multipart.header.buffer.size | 512 | Yes | Buffer size in bytes used by the HTTP multipart content parser. | | http.multipart.idle.spin.count | 10000 | No | How long the code accumulates incoming data chunks for column and delimiter analysis. | | http.receive.buffer.size | 1M | Yes | Size of receive buffer. | | http.request.header.buffer.size | 64K | Yes | Size of internal buffer allocated for HTTP request headers. The value is rounded up to the nearest power of 2. When HTTP requests contain headers that exceed the buffer size server will disconnect the client with HTTP error in server log. | | http.worker.count | 0 | No | Number of threads in private worker pool. When `0`, HTTP server will be using shared worker pool of the server. Values above `0` switch on private pool. | | http.worker.affinity | | No | Comma separated list of CPU core indexes. The number of items in this list must be equal to the worker count. | | http.worker.haltOnError | false | No | **Changing the default value is strongly discouraged**. Flag that indicates if the worker thread must stop when an unexpected error occurs. | | http.send.buffer.size | 2M | Yes | Size of the internal send buffer. Larger buffer sizes result in fewer I/O interruptions the server is making at the expense of memory usage per connection. There is a limit of send buffer size after which increasing it stops being useful in terms of performance. 2MB seems to be optimal value. | | http.static.index.file.name | index.html | No | Name of index file for the Web Console. | | http.frozen.clock | false | No | Sets the clock to always return zero. This configuration parameter is used for internal testing. | | http.allow.deflate.before.send | false | No | Flag that indicates if Gzip compression of outgoing data is allowed. | | http.keep-alive.timeout | 5 | No | Used together with `http.keep-alive.max` to set the value of HTTP `Keep-Alive` response header. This instructs browser to keep TCP connection open. Has to be `0` when `http.version` is set to `HTTP/1.0`. | | http.keep-alive.max | 10000 | No | See `http.keep-alive.timeout`. Has to be `0` when `http.version` is set to `HTTP/1.0`. | | http.static.public.directory | public | No | The name of directory for public web site. | | http.text.date.adapter.pool.capacity | 16 | No | Size of date adapter pool. This should be set to the anticipated maximum number of `DATE` fields a text input can have. The pool is assigned to connection state and is reused alongside of connection state object. | | http.text.json.cache.limit | 16384 | No | JSON parser cache limit. Cache is used to compose JSON elements that have been broken up by TCP protocol. This value limits the maximum length of individual tag or tag value. | | http.text.json.cache.size | 8192 | No | Initial size of JSON parser cache. The value must not exceed `http.text.json.cache.limit` and should be set to avoid cache resizes at runtime. | | http.text.max.required.delimiter.stddev | 0.1222d | No | The maximum standard deviation value for the algorithm that calculates text file delimiter. Usually when text parser cannot recognise the delimiter it will log the calculated and maximum standard deviation for the delimiter candidate. | | http.text.max.required.line.length.stddev | 0.8 | No | Maximum standard deviation value for the algorithm that classifies input as text or binary. For the values above configured stddev input will be considered binary. | | http.text.metadata.string.pool.capacity | 128 | No | The initial size of pool for objects that wrap individual elements of metadata JSON, such as column names, date pattern strings and locale values. | | http.text.roll.buffer.limit | 4M | No | The limit of text roll buffer. See `http.text.roll.buffer.size` for description. | | http.text.roll.buffer.size | 1024 | No | Roll buffer is a structure in the text parser that holds a copy of a line that has been broken up by TCP. The size should be set to the maximum length of text line in text input. | | http.text.analysis.max.lines | 1000 | No | Number of lines to read on CSV import for heuristics which determine column names & types. Lower line numbers may detect CSV schemas quicker, but possibly with less accuracy. 1000 lines is the maximum for this value. | | http.text.lexer.string.pool.capacity | 64 | No | The initial capacity of string fool, which wraps `STRING` column types in text input. The value should correspond to the maximum anticipated number of STRING columns in text input. | | http.text.timestamp.adapter.pool.capacity | 64 | No | Size of timestamp adapter pool. This should be set to the anticipated maximum number of `TIMESTAMP` fields a text input can have. The pool is assigned to connection state and is reused alongside of connection state object. | | http.text.utf8.sink.size | 4096 | No | Initial size of UTF-8 adapter sink. The value should correspond the maximum individual field value length in text input. | | http.json.query.connection.check.frequency | 1000000 | No | **Changing the default value is strongly discouraged**. The value to throttle check if client socket has been disconnected. | | http.json.query.float.scale | 4 | No | The scale value of string representation of `FLOAT` values. | | http.json.query.double.scale | 12 | No | The scale value of string representation of `DOUBLE` values. | | http.query.cache.enabled | true | No | Enable or disable the query cache. Cache capacity is `number_of_blocks * number_of_rows`. | | http.query.cache.block.count | 4 | No | Number of blocks for the query cache. | | http.query.cache.row.count | 16 | No | Number of rows for the query cache. | | http.security.readonly | false | No | Forces HTTP read only mode when `true`, disabling commands which modify the data or data structure, e.g. INSERT, UPDATE, or CREATE TABLE. | | http.security.max.response.rows | 2^63-1 | No | Limit the number of response rows over HTTP. | | http.security.interrupt.on.closed.connection | true | No | Switch to enable termination of SQL processing if the HTTP connection is closed. The mechanism affects performance so the connection is only checked after `circuit.breaker.throttle` calls are made to the check method. The mechanism also reads from the input stream and discards it since some HTTP clients send this as a keep alive in between requests, `circuit.breaker.buffer.size` denotes the size of the buffer for this. | | http.pessimistic.health.check.enabled | false | No | When enabled, the health check returns HTTP 500 for any unhandled errors since the server started. | | circuit.breaker.throttle | 2000000 | No | Number of internal iterations such as loops over data before checking if the HTTP connection is still open | | circuit.breaker.buffer.size | 32 | No | Size of buffer to read from HTTP connection. If this buffer returns zero and the HTTP client is no longer sending data, SQL processing will be terminated. | | http.server.keep.alive | true | No | If set to `false`, the server will disconnect the client after completion of each request. | | http.version | HTTP/1.1 | No | Protocol version, other supported value is `HTTP/1.0`. | | http.context.web.console | / | No | Context path for the Web Console. If other REST services remain on the default context paths they will move to the same context path as the Web Console. InfluxDB Line Protocol (ILP) HTTP services are not affected and remain on their default paths. When default context paths are changed, moving the Web Console will not affect the configured paths. QuestDB creates copies of services on the Web Console paths so that both the Web Console and custom services remain operational. | | http.context.import | /imp | No | Context path of the file import service. | | http.context.table.status | /chk | No | Context path for the table statusservice used by the Import UI in the Web Console. | | http.context.export | /exp | No | Context path for the SQL result CSV export service. | | http.context.settings | /settings | No | Context path for the service which provides server-side settings to the Web Console. | | http.context.execute | /exec | No | Context path for the SQL execution service. | | http.context.warnings | /warnings | No | Context path for the Web Console specific service. | | http.context.ilp | /write,/api/v2/write | No | Context paths for the Influx Line Protocol (ILP) HTTP services. These are not used by the Web Console. | | http.context.ilp.ping | /ping | No | Context path for the Influx Line Protocol (ILP) ping endpoint. | | http.redirect.count | 1 | No | Number of HTTP redirects. All redirects are 301 - Moved Permanently. | | http.redirect.1 | / -> /index.html | No | Example redirect configuration. Format is 'source -> destination'. | ### Cairo engine[​](https://questdb.com/docs/configuration/overview/#cairo-engine "Direct link to Cairo engine") This section describes configuration settings for the Cairo SQL engine in QuestDB. | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | config.reload.enabled | true | No | When `false`, disables reload\_config() SQL function. | | query.timeout.sec | 60 | No | A global timeout (in seconds) for long-running queries. Timeout for each query can override the default by setting HTTP header [`Statement-Timeout`](https://questdb.com/docs/query/rest-api/#headers)
or Postgres [`options`](https://questdb.com/docs/query/pgwire/overview/#list-of-supported-connection-properties)
. | | cairo.max.uncommitted.rows | 500000 | No | Maximum number of uncommitted rows per table, when the number of pending rows reaches this parameter on a table, a commit will be issued. | | cairo.o3.max.lag | 10 minutes | No | The maximum size of in-memory buffer in milliseconds. The buffer is allocated dynamically through analyzing the shape of the incoming data, and `o3MaxLag` is the upper limit. | | cairo.o3.min.lag | 1 second | No | The minimum size of in-memory buffer in milliseconds. The buffer is allocated dynamically through analyzing the shape of the incoming data, and `o3MinLag` is the lower limit. | | cairo.sql.backup.root | null | No | Output root directory for backups. | | cairo.sql.backup.dir.datetime.format | null | No | Date format for backup directory. | | cairo.sql.backup.dir.tmp.name | tmp | No | Name of tmp directory used during backup. | | cairo.sql.backup.mkdir.mode | 509 | No | Permission used when creating backup directories. | | cairo.snapshot.instance.id | empty string | No | Instance id to be included into disk snapshots. | | cairo.snapshot.recovery.enabled | true | No | When `false`, disables snapshot recovery on database start. | | cairo.root | db | No | Directory for storing db tables and metadata. This directory is inside the server root directory provided at startup. | | cairo.commit.mode | nosync | No | How changes to table are flushed to disk upon commit. Choices: `nosync`, `async` (flush call schedules update, returns immediately), `sync` (waits for flush on the appended column files to complete). | | cairo.rnd.memory.max.pages | 128 | No | Sets the max number of pages for memory used by `rnd_` functions. Supports `rnd_str()` and `rnd_symbol()`. | | cairo.rnd.memory.page.size | 8K | No | Sets the memory page size used by `rnd_` functions. Supports `rnd_str()` and `rnd_symbol()`. | | cairo.create.as.select.retry.count | 5 | No | Number of types table creation or insertion will be attempted. | | cairo.default.map.type | fast | No | Type of map used. Options: `fast` (speed at the expense of storage), `compact`. | | cairo.default.symbol.cache.flag | true | No | When `true`, symbol values will be cached on Java heap instead of being looked up in the database files. | | cairo.default.symbol.capacity | 256 | No | Specifies approximate capacity for `SYMBOL` columns. It should be equal to number of unique symbol values stored in the table and getting this value badly wrong will cause performance degradation. Must be power of 2. | | cairo.file.operation.retry.count | 30 | No | Number of attempts to open files. | | cairo.idle.check.interval | 300000 | No | Frequency of writer maintenance job in milliseconds. | | cairo.inactive.reader.ttl | 120000 | No | TTL (Time-To-Live) to close inactive readers in milliseconds. | | cairo.wal.inactive.writer.ttl | 120000 | No | TTL (Time-To-Live) to close inactive WAL writers in milliseconds. | | cairo.inactive.writer.ttl | 600000 | No | TTL (Time-To-Live) to close inactive writers in milliseconds. | | cairo.index.value.block.size | 256 | No | Approximation of number of rows for a single index key, must be power of 2. | | cairo.max.swap.file.count | 30 | No | Number of attempts to open swap files. | | cairo.mkdir.mode | 509 | No | File permission mode for new directories. | | cairo.parallel.index.threshold | 100000 | No | Minimum number of rows before allowing use of parallel indexation. | | cairo.reader.pool.max.segments | 10 | No | Number of segments in the table reader pool. Each segment holds up to 32 readers. | | cairo.wal.writer.pool.max.segments | 10 | No | Number of segments in the WAL writer pool. Each segment holds up to 32 writers. | | cairo.spin.lock.timeout | 1000 | No | Timeout when attempting to get BitmapIndexReaders in millisecond. | | cairo.character.store.capacity | 1024 | No | Size of the CharacterStore. | | cairo.character.store.sequence.pool.capacity | 64 | No | Size of the CharacterSequence pool. | | cairo.column.pool.capacity | 4096 | No | Size of the Column pool in the SqlCompiler. | | cairo.compact.map.load.factor | 0.7 | No | Load factor for CompactMaps. | | cairo.expression.pool.capacity | 8192 | No | Size of the ExpressionNode pool in SqlCompiler. | | cairo.fast.map.load.factor | 0.5 | No | Load factor for all FastMaps. | | cairo.sql.join.context.pool.capacity | 64 | No | Size of the JoinContext pool in SqlCompiler. | | cairo.lexer.pool.capacity | 2048 | No | Size of FloatingSequence pool in GenericLexer. | | cairo.sql.map.key.capacity | 2M | No | Key capacity in FastMap and CompactMap. | | cairo.sql.map.max.resizes | 2^31 | No | Number of map resizes in FastMap and CompactMap before a resource limit exception is thrown, each resize doubles the previous size. | | cairo.sql.map.page.size | 4m | No | Memory page size for FastMap and CompactMap. | | cairo.sql.map.max.pages | 2^31 | No | Memory max pages for CompactMap. | | cairo.model.pool.capacity | 1024 | No | Size of the QueryModel pool in the SqlCompiler. | | cairo.sql.sort.key.page.size | 4M | No | Memory page size for storing keys in LongTreeChain. | | cairo.sql.sort.key.max.pages | 2^31 | No | Max number of pages for storing keys in LongTreeChain before a resource limit exception is thrown. | | cairo.sql.sort.light.value.page.size | 1048576 | No | Memory page size for storing values in LongTreeChain. | | cairo.sql.sort.light.value.max.pages | 2^31 | No | Max pages for storing values in LongTreeChain. | | cairo.sql.hash.join.value.page.size | 16777216 | No | Memory page size of the slave chain in full hash joins. | | cairo.sql.hash.join.value.max.pages | 2^31 | No | Max pages of the slave chain in full hash joins. | | cairo.sql.latest.by.row.count | 1000 | No | Number of rows for LATEST BY. | | cairo.sql.hash.join.light.value.page.size | 1048576 | No | Memory page size of the slave chain in light hash joins. | | cairo.sql.hash.join.light.value.max.pages | 2^31 | No | Max pages of the slave chain in light hash joins. | | cairo.sql.sort.value.page.size | 16777216 | No | Memory page size of file storing values in SortedRecordCursorFactory. | | cairo.sql.sort.value.max.pages | 2^31 | No | Max pages of file storing values in SortedRecordCursorFactory. | | cairo.work.steal.timeout.nanos | 10000 | No | Latch await timeout in nanos for stealing indexing work from other threads. | | cairo.parallel.indexing.enabled | true | No | Allows parallel indexation. Works in conjunction with cairo.parallel.index.threshold. | | cairo.sql.join.metadata.page.size | 16384 | No | Memory page size for JoinMetadata file. | | cairo.sql.join.metadata.max.resizes | 2^31 | No | Number of map resizes in JoinMetadata before a resource limit exception is thrown, each resize doubles the previous size. | | cairo.sql.analytic.column.pool.capacity | 64 | No | Size of AnalyticColumn pool in SqlParser. | | cairo.sql.create.table.model.batch.size | 1000000 | No | Batch size for non-atomic CREATE AS SELECT statements. | | cairo.sql.column.cast.model.pool.capacity | 16 | No | Size of CreateTableModel pool in SqlParser. | | cairo.sql.rename.table.model.pool.capacity | 16 | No | Size of RenameTableModel pool in SqlParser. | | cairo.sql.with.clause.model.pool.capacity | 128 | No | Size of WithClauseModel pool in SqlParser. | | cairo.sql.insert.model.pool.capacity | 64 | No | Size of InsertModel pool in SqlParser. | | cairo.sql.insert.model.batch.size | 1000000 | No | Batch size for non-atomic INSERT INTO SELECT statements. | | cairo.sql.copy.model.pool.capacity | 32 | No | Size of CopyModel pool in SqlParser. | | cairo.sql.copy.buffer.size | 2M | No | Size of buffer used when copying tables. | | cairo.sql.double.cast.scale | 12 | No | Maximum number of decimal places that types cast as doubles have. | | cairo.sql.float.cast.scale | 4 | No | Maximum number of decimal places that types cast as floats have. | | cairo.sql.copy.formats.file | /text\_loader.json | No | Name of file with user's set of date and timestamp formats. | | cairo.sql.jit.mode | on | No | JIT compilation for SQL queries. May be disabled by setting this value to `off`. | | cairo.sql.jit.debug.enabled | false | No | Sets debug flag for JIT compilation. When enabled, assembly will be printed into `stdout`. | | cairo.sql.jit.max.in.list.size.threshold | 10 | No | Controls whether or not JIT compilation will be used for a query that uses the IN predicate. If the IN list is longer than this threshold, JIT compilation will be cancelled. | | cairo.sql.jit.bind.vars.memory.page.size | 4K | No | Sets the memory page size for storing bind variable values for JIT compiled filter. | | cairo.sql.jit.bind.vars.memory.max.pages | 8 | No | Sets the max memory pages for storing bind variable values for JIT compiled filter. | | cairo.sql.jit.page.address.cache.threshold | 1M | No | Sets minimum cache size to shrink page address cache after query execution. | | cairo.sql.jit.ir.memory.page.size | 8K | No | Sets the memory page size for storing IR for JIT compilation. | | cairo.sql.jit.ir.memory.max.pages | 8 | No | Sets max memory pages for storing IR for JIT compilation. | | cairo.sql.page.frame.min.rows | 1000 | No | Sets the minimum number of rows in page frames used in SQL queries. | | cairo.sql.page.frame.max.rows | 1000000 | No | Sets the maximum number of rows in page frames used in SQL. queries | | cairo.sql.sampleby.page.size | 0 | No | SampleBy index query page size. Max values returned in single scan. 0 is default, and it means to use symbol block capacity. | | cairo.sql.sampleby.default.alignment.calendar | 0 | No | SampleBy default alignment behaviour. true corresponds to ALIGN TO CALENDAR, false corresponds to ALIGN TO FIRST OBSERVATION. | | cairo.date.locale | en | No | The locale to handle date types. | | cairo.timestamp.locale | en | No | The locale to handle timestamp types. | | cairo.o3.column.memory.size | 256k | No | Memory page size per column for O3 operations. Please be aware O3 will use 2x of the set value per column (therefore a default of 2x256kb). | | cairo.writer.data.append.page.size | 16M | No | mmap sliding page size that table writer uses to append data for each column. | | cairo.writer.data.index.key.append.page.size | 512K | No | mmap page size for appending index key data; key data is number of distinct symbol values times 4 bytes. | | cairo.writer.data.index.value.append.page.size | 16M | No | mmap page size for appending value data. | | cairo.writer.misc.append.page.size | 4K | No | mmap page size for mapping small files, default value is OS page size (4k Linux, 64K windows, 16k OSX M1). Overriding this rounds to the nearest (greater) multiple of the OS page size. | | cairo.writer.command.queue.capacity | 32 | No | Maximum writer ALTER TABLE and replication command capacity. Shared between all the tables. | | cairo.writer.tick.rows.count | 1024 | No | Row count to check writer command queue after on busy writing, e.g. tick after X rows written. | | cairo.writer.alter.busy.wait.timeout | 500 | No | Maximum wait timeout in milliseconds for `ALTER TABLE` SQL statement run via REST and PostgreSQL Wire Protocol interfaces when statement execution is `ASYNCHRONOUS`. | | cairo.sql.column.purge.queue.capacity | 128 | No | Purge column version job queue. Increase the size if column version not automatically cleanup after execution of UPDATE SQL statement. Reduce to decrease initial memory footprint. | | cairo.sql.column.purge.task.pool.capacity | 256 | No | Column version task object pool capacity. Increase to reduce GC, reduce to decrease memory footprint. | | cairo.sql.column.purge.retry.delay | 10000 | No | Initial delay (μs) before re-trying purge of stale column files. | | cairo.sql.column.purge.retry.delay.multiplier | 10.0 | No | Multiplier used to increases retry delay with each iteration. | | cairo.sql.column.purge.retry.delay.limit | 60000000 | No | Delay limit (μs), upon reaching which, the re-try delay remains constant. | | cairo.sql.column.purge.retry.limit.days | 31 | No | Number of days purge system will continue to re-try deleting stale column files before giving up. | | cairo.volumes | \- | No | A comma separated list of _alias -> root-path_ pairs defining allowed volumes to be used in [CREATE TABLE IN VOLUME](https://questdb.com/docs/query/sql/create-table/#table-target-volume)
statements. | | cairo.system.table.prefix | sys. | No | Prefix of the tables used for QuestDB internal data storage. These tables are hidden from QuestDB web console. | | cairo.wal.enabled.default | true | No | Setting defining whether WAL table is the default when using `CREATE TABLE`. | | cairo.o3.partition.split.min.size | 50MB | No | The estimated partition size on disk. This setting is one of the conditions to trigger [auto-partitioning](https://questdb.com/docs/getting-started/capacity-planning/#auto-partitioning)
. | | cairo.o3.last.partition.max.splits | 20 | No | The number of partition pieces allowed before the last partition piece is merged back to the physical partition. | | cairo.o3.partition.purge.list.initial.capacity | 1 | No | Number of partition expected on average. Initial value for purge allocation job, extended in runtime automatically. | | cairo.sql.parallel.groupby.enabled | true | No | Enables parallel GROUP BY execution; requires at least 4 shared worker threads. | | cairo.sql.parallel.groupby.merge.shard.queue.capacity | | No | Merge queue capacity for parallel GROUP BY; used for parallel tasks that merge shard hash tables. | | cairo.sql.parallel.groupby.sharding.threshold | 100000 | No | Threshold for parallel GROUP BY to shard the hash table holding the aggregates. | | cairo.sql.groupby.allocator.default.chunk.size | 128k | No | Default size for memory buffers in GROUP BY function native memory allocator. | | cairo.sql.groupby.allocator.max.chunk.size | 4gb | No | Maximum allowed native memory allocation for GROUP BY functions. | | cairo.sql.unordered.map.max.entry.size | 24 | No | Threshold in bytes for switching from single memory buffer hash table (unordered) to a hash table with separate heap for entries (ordered). | | cairo.sql.window.max.recursion | 128 | No | Prevents stack overflow errors when evaluating complex nested SQLs. The value is an approximate number of nested SELECT clauses. | | cairo.sql.query.registry.pool.size | | No | Pre-sizes the internal data structure that stores active query executions. The value is chosen automatically based on the number of threads in the shared worker pool. | | cairo.sql.analytic.initial.range.buffer.size | 32 | No | Window function buffer size in record counts. Pre-sizes buffer for every windows function execution to contain window records. | | cairo.system.writer.data.append.page.size | 256k | No | mmap sliding page size that TableWriter uses to append data for each column specifically for System tables. | | cairo.file.descriptor.cache.enabled | true | No | enables or disables the file-descriptor cache | | cairo.partition.encoder.parquet.raw.array.encoding.enabled | false | No | determines whether to export arrays in QuestDB-native binary format (true, less compatible) or Parquet-native format (false, more compatible). | | cairo.partition.encoder.parquet.version | 1 | No | Output parquet version to use for parquet-encoded partitions. Can be 1 or 2. | | cairo.partition.encoder.parquet.statistics.enabled | true | No | Controls whether or not statistics are included in parquet-encoded partitions. | | cairo.partition.encoder.parquet.compression.codec | ZSTD | No | Sets the default compression codec for parquet-encoded partitions. Alternatives include `LZ4_RAW`, `SNAPPY`. | | cairo.partition.encoder.parquet.compression.level | 9 (ZSTD), 0 (otherwise) | No | Sets the default compression level for parquet-encoded partitions. Dependent on underlying compression codec. | | cairo.partition.encoder.parquet.row.group.size | 100000 | No | Sets the default row-group size for parquet-encoded partitions. | | cairo.partition.encoder.parquet.data.page.size | 1048576 | No | Sets the default page size for parquet-encoded partitions. | | cairo.partition.encoder.parquet.min.compression.ratio | 1.2 | No | Minimum compression ratio (uncompressed\_size / compressed\_size) for Parquet pages. When a compressed page does not meet this threshold, it is stored uncompressed instead. A value of 0.0 disables the check. | ### WAL table configurations[​](https://questdb.com/docs/configuration/overview/#wal-table-configurations "Direct link to WAL table configurations") The following WAL tables settings on parallel threads are configurable for applying WAL data to the table storage: | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | wal.apply.worker.count | equal to the CPU core count | No | Number of dedicated worker threads assigned to handle WAL table data. | | wal.apply.worker.affinity | equal to the CPU core count | No | Comma separated list of CPU core indexes. | | wal.apply.worker.haltOnError | false | No | Flag that indicates if the worker thread must stop when an unexpected error occurs. | | cairo.wal.purge.interval | 30000 | No | Period in ms of how often WAL-applied files are cleaned up from the disk | | cairo.wal.segment.rollover.row.count | 200000 | No | Row count of how many rows are written to the same WAL segment before starting a new segment. Triggers in conjunction with `cairo.wal.segment.rollover.size` (whichever is first). | | cairo.wal.squash.uncommitted.rows.multiplier | 20.0 | No | Multiplier to cairo.max.uncommitted.rows to calculate the limit of rows that can be kept invisible when writing to WAL table under heavy load, when multiple transactions are to be applied. It is used to reduce the number Out-Of-Order (O3) commits when O3 commits are unavoidable by squashing multiple commits together. Setting it very low can increase O3 commit frequency and decrease the throughput. Setting it too high may cause excessive memory usage and increase the latency. | | cairo.wal.max.lag.txn.count | 20 | No | Maximum number of transactions that can be kept invisible when writing to WAL table. Once the number is reached, full commit occurs. If not set, defaults to the rounded value of cairo.wal.squash.uncommitted.rows.multiplier. | | cairo.wal.apply.parallel.sql.enabled | true | No | When disabled, SQL executed by the WAL apply job will always run single-threaded. | ### COPY settings[​](https://questdb.com/docs/configuration/overview/#copy-settings "Direct link to COPY settings") #### Import[​](https://questdb.com/docs/configuration/overview/#import "Direct link to Import") This section describes configuration settings for using `COPY` to import large CSV files, or export parquet files. Settings for `COPY FROM` (import): | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | cairo.sql.copy.root | import | No | Input root directory for CSV imports via `COPY` SQL and for Parquet file reading. This path must not overlap with other directory (e.g. db, conf) of running instance, otherwise import may delete or overwrite existing files. Relative paths are resolved against the server root directory. | | cairo.sql.copy.work.root | null | No | Temporary import file directory. Defaults to `root_directory/tmp` if not set explicitly. | | cairo.iouring.enabled | true | No | Enable or disable io\_uring implementation. Applicable to newer Linux kernels only. Can be used to switch io\_uring interface usage off if there's a kernel bug affecting it. | | cairo.sql.copy.buffer.size | 2 MiB | No | Size of read buffers used in import. | | cairo.sql.copy.log.retention.days | 3 | No | Number of days to keep import messages in `sys.text_import_log`. | | cairo.sql.copy.max.index.chunk.size | 100M | No | Maximum size of index chunk file used to limit total memory requirements of import. Indexing phase should use roughly `thread_count * cairo.sql.copy.max.index.chunk.size` of memory. | | cairo.sql.copy.queue.capacity | 32 | No | Size of copy task queue. Should be increased if there's more than 32 import workers. | **CSV import configuration for Docker** For QuestDB instances using Docker: * `cairo.sql.copy.root` must be defined using one of the following settings: * The environment variable `QDB_CAIRO_SQL_COPY_ROOT`. * The `cairo.sql.copy.root` in `server.conf`. * The path for the source CSV file is mounted. * The source CSV file path and the path defined by `QDB_CAIRO_SQL_COPY_ROOT` are identical. * It is optional to define `QDB_CAIRO_SQL_COPY_WORK_ROOT`. The following is an example command to start a QuestDB instance on Docker, in order to import a CSV file: docker run -p 9000:9000 \-v "/tmp/questdb:/var/lib/questdb" \-v "/tmp/questdb/my_input_root:/var/lib/questdb/questdb_import" \-e QDB_CAIRO_SQL_COPY_ROOT=/var/lib/questdb/questdb_import \questdb/questdb Where: * `-v "/tmp/questdb/my_input_root:/var/lib/questdb/questdb_import"`: Defining a source CSV file location to be `/tmp/questdb/my_input_root` on local machine and mounting it to `/var/lib/questdb/questdb_import` in the container. * `-e QDB_CAIRO_SQL_COPY_ROOT=/var/lib/questdb/questdb_import`: Defining the copy root directory to be `/var/lib/questdb/questdb_import`. It is important that the two path are identical (`/var/lib/questdb/questdb_import` in the example). #### Export[​](https://questdb.com/docs/configuration/overview/#export "Direct link to Export") | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | cairo.sql.copy.export.root | export | No | Root directory for parquet exports via `COPY-TO` SQL. This path must not overlap with other directory (e.g. db, conf) of running instance, otherwise export may delete or overwrite existing files. Relative paths are resolved against the server root directory. | Parquet export is also generally impacted by query execution and parquet conversion parameters. If not overridden, the following default setting will be used. | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | cairo.partition.encoder.parquet.raw.array.encoding.enabled | false | No | determines whether to export arrays in QuestDB-native binary format (true, less compatible) or Parquet-native format (false, more compatible). | | cairo.partition.encoder.parquet.version | 1 | No | Output parquet version to use for parquet-encoded partitions. Can be 1 or 2. | | cairo.partition.encoder.parquet.statistics.enabled | true | No | Controls whether or not statistics are included in parquet-encoded partitions. | | cairo.partition.encoder.parquet.compression.codec | ZSTD | No | Sets the default compression codec for parquet-encoded partitions. Alternatives include `LZ4_RAW`, `SNAPPY`. | | cairo.partition.encoder.parquet.compression.level | 9 (ZSTD), 0 (otherwise) | No | Sets the default compression level for parquet-encoded partitions. Dependent on underlying compression codec. | | cairo.partition.encoder.parquet.row.group.size | 100000 | No | Sets the default row-group size for parquet-encoded partitions. | | cairo.partition.encoder.parquet.data.page.size | 1048576 | No | Sets the default page size for parquet-encoded partitions. | ### Parallel SQL execution[​](https://questdb.com/docs/configuration/overview/#parallel-sql-execution "Direct link to Parallel SQL execution") This section describes settings that can affect the level of parallelism during SQL execution, and therefore can also have an impact on performance. | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | cairo.sql.parallel.filter.enabled | true | No | Enable or disable parallel SQL filter execution. JIT compilation takes place only when this setting is enabled. | | cairo.sql.parallel.filter.pretouch.enabled | true | No | Enable column pre-touch as part of the parallel SQL filter execution, to improve query performance for large tables. | | cairo.page.frame.shard.count | 4 | No | Number of shards for both dispatch and reduce queues. Shards reduce queue contention between SQL statements that are executed concurrently. | | cairo.page.frame.reduce.queue.capacity | 64 | No | Reduce queue is used for data processing and should be large enough to supply tasks for worker threads (shared worked pool). | | cairo.page.frame.rowid.list.capacity | 256 | No | Row ID list initial capacity for each slot of the reduce queue. Larger values reduce memory allocation rate, but increase minimal RSS size. | | cairo.page.frame.column.list.capacity | 16 | No | Column list capacity for each slot of the reduce queue. Used by JIT-compiled filter functions. Larger values reduce memory allocation rate, but increase minimal RSS size. | ### Postgres wire protocol[​](https://questdb.com/docs/configuration/overview/#postgres-wire-protocol "Direct link to Postgres wire protocol") This section describes configuration settings for client connections using PostgresSQL wire protocol. | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | pg.enabled | true | No | Configuration for enabling or disabling the Postres interface. | | pg.net.bind.to | 0.0.0.0:8812 | No | IP address and port of Postgres wire protocol server. 0 means that the server will bind to all network interfaces. You can specify IP address of any individual network interface on your system. | | pg.net.connection.limit | 64 | Yes | The maximum number permitted for simultaneous Postgres connections to the server. This value is intended to control server memory consumption. | | pg.net.connection.timeout | 300000 | No | Connection idle timeout in milliseconds. Connections are closed by the server when this timeout lapses. | | pg.net.connection.rcvbuf | \-1 | No | Maximum send buffer size on each TCP socket. If value is -1 socket send buffer remains unchanged from OS default. | | pg.net.connection.sndbuf | \-1 | No | Maximum receive buffer size on each TCP socket. If value is -1, the socket receive buffer remains unchanged from OS default. | | pg.net.connection.hint | false | No | Windows specific flag to overcome OS limitations on TCP backlog size | | pg.net.connection.queue.timeout | 300000 | No | Amount of time in milliseconds a connection can wait in the listen backlog queue before it is refused. Connections will be aggressively removed from the backlog until the active connection limit is breached. | | pg.security.readonly | false | No | Forces PostgreSQL Wire Protocol read only mode when `true`, disabling commands which modify the data or data structure, e.g. INSERT, UPDATE, or CREATE TABLE. | | pg.character.store.capacity | 4096 | No | Size of the CharacterStore. | | pg.character.store.pool.capacity | 64 | No | Size of the CharacterStore pool capacity. | | pg.connection.pool.capacity | 64 | No | The maximum amount of pooled connections this interface may have. | | pg.password | quest | Yes | Postgres database password. | | pg.user | admin | Yes | Postgres database username. | | pg.readonly.user.enabled | false | Yes | Enable or disable Postgres database read-only user account. When enabled, this additional user can be used to open read-only connections to the database. | | pg.readonly.password | quest | Yes | Postgres database read-only user password. | | pg.readonly.user | user | Yes | Postgres database read-only user username. | | pg.select.cache.enabled | true | No | Enable or disable the SELECT query cache. Cache capacity is `number_of_blocks * number_of_rows`. | | pg.select.cache.block.count | 16 | No | Number of blocks to cache SELECT query execution plan against text to speed up execution. | | pg.select.cache.row.count | 16 | No | Number of rows to cache for SELECT query execution plan against text to speed up execution. | | pg.insert.cache.enabled | true | No | Enable or disable the INSERT query cache. Cache capacity is `number_of_blocks * number_of_rows`. | | pg.insert.cache.block.count | 8 | No | Number of blocks to cache INSERT query execution plan against text to speed up execution. | | pg.insert.cache.row.count | 8 | No | Number of rows to cache for INSERT query execution plan against text to speed up execution. | | pg.update.cache.enabled | true | No | Enable or disable the UPDATE query cache. Cache capacity is `number_of_blocks * number_of_rows`. | | pg.update.cache.block.count | 8 | No | Number of blocks to cache UPDATE query execution plan against text to speed up execution. | | pg.update.cache.row.count | 8 | No | Number of rows to cache for UPDATE query execution plan against text to speed up execution. | | pg.max.blob.size.on.query | 512k | No | For binary values, clients will receive an error when requesting blob sizes above this value. | | pg.recv.buffer.size | 1M | Yes | Size of the buffer for receiving data. | | pg.send.buffer.size | 1M | Yes | Size of the buffer for sending data. | | pg.date.locale | en | No | The locale to handle date types. | | pg.timestamp.locale | en | No | The locale to handle timestamp types. | | pg.worker.count | 0 | No | Number of dedicated worker threads assigned to handle PostgreSQL Wire Protocol queries. When `0`, the jobs will use the shared pool. | | pg.worker.affinity | | No | Comma-separated list of thread numbers which should be pinned for Postgres ingestion. Example `pg.worker.affinity=1,2,3`. | | pg.halt.on.error | false | No | Whether ingestion should stop upon internal error. | | pg.daemon.pool | true | No | Defines whether to run all PostgreSQL Wire Protocol worker threads in daemon mode (`true`) or not (`false`). | | pg.binary.param.count.capacity | 2 | No | Size of the initial capacity for the pool used for binary bind variables. | | pg.named.statement.limit | 64 | Yes | Size of the named statement pool. | ### InfluxDB Line Protocol (ILP)[​](https://questdb.com/docs/configuration/overview/#influxdb-line-protocol-ilp "Direct link to InfluxDB Line Protocol (ILP)") This section describes ingestion settings for incoming messages using InfluxDB Line Protocol. | Property | Default | Description | | --- | --- | --- | | line.default.partition.by | DAY | Table partition strategy to be used with tables that are created automatically by InfluxDB Line Protocol. Possible values are: `HOUR`, `DAY`, `WEEK`, `MONTH`, and `YEAR`. | | line.auto.create.new.columns | true | When enabled, automatically creates new columns when they appear in the ingested data. When disabled, messages with new columns will be rejected. | | line.auto.create.new.tables | true | When enabled, automatically creates new tables when they appear in the ingested data. When disabled, messages for non-existent tables will be rejected. | | line.log.message.on.error | true | Controls whether malformed ILP messages are printed to the server log when errors occur. | #### HTTP specific settings[​](https://questdb.com/docs/configuration/overview/#http-specific-settings "Direct link to HTTP specific settings") ILP over HTTP is the preferred way of ingesting data. | Property | Default | Description | | --- | --- | --- | | line.http.enabled | true | Enable ILP over HTTP. Default port is 9000. Enabled by default within open source versions, defaults to false and must be enabled for Enterprise. | | line.http.ping.version | v2.2.2 | Version information for the ping response of ILP over HTTP. | | HTTP properties | Various | See [HTTP settings](https://questdb.com/docs/configuration/overview/#http-server)
for general HTTP configuration. ILP over HTTP inherits from HTTP settings. | #### TCP specific settings[​](https://questdb.com/docs/configuration/overview/#tcp-specific-settings "Direct link to TCP specific settings") | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | line.tcp.enabled | true | No | Enable or disable line protocol over TCP. | | line.tcp.net.bind.to | 0.0.0.0:9009 | No | IP address of the network interface to bind listener to and port. By default, TCP receiver listens on all network interfaces. | | line.tcp.net.connection.limit | 256 | Yes | The maximum number permitted for simultaneous connections to the server. This value is intended to control server memory consumption. | | line.tcp.net.connection.timeout | 300000 | No | Connection idle timeout in milliseconds. Connections are closed by the server when this timeout lapses. | | line.tcp.net.connection.hint | false | No | Windows specific flag to overcome OS limitations on TCP backlog size | | line.tcp.net.connection.rcvbuf | \-1 | No | Maximum buffer receive size on each TCP socket. If value is -1, the socket receive buffer remains unchanged from OS default. | | line.tcp.net.connection.queue.timeout | 5000 | No | Amount of time in milliseconds a connection can wait in the listen backlog queue before its refused. Connections will be aggressively removed from the backlog until the active connection limit is breached. | | line.tcp.auth.db.path | | No | Path which points to the authentication db file. | | line.tcp.connection.pool.capacity | 64 | No | The maximum amount of pooled connections this interface may have. | | line.tcp.timestamp | n | No | Input timestamp resolution. Possible values are `n`, `u`, `ms`, `s` and `h`. | | line.tcp.msg.buffer.size | 32768 | No | Size of the buffer read from queue. Maximum size of write request, regardless of the number of measurements. | | line.tcp.maintenance.job.interval | 1000 | No | Maximum amount of time (in milliseconds) between maintenance jobs committing any uncommitted data on inactive tables. | | line.tcp.min.idle.ms.before.writer.release | 500 | No | Minimum amount of idle time (in milliseconds) before a table writer is released. | | line.tcp.commit.interval.fraction | 0.5 | No | Commit lag fraction. Used to calculate commit interval for the table according to the following formula: `commit_interval = commit_lag ∗ fraction`. The calculated commit interval defines how long uncommitted data will need to remain uncommitted. | | line.tcp.commit.interval.default | 1000 | No | Default commit interval in milliseconds. | | line.tcp.max.measurement.size | 32768 | No | Maximum size of any measurement. | | line.tcp.writer.worker.count | | No | Number of dedicated I/O worker threads assigned to write data to tables. When `0`, the writer jobs will use the shared pool. | | line.tcp.writer.worker.affinity | | No | Comma-separated list of thread numbers which should be pinned for line protocol ingestion over TCP. CPU core indexes are 0-based. | | line.tcp.writer.worker.sleep.threshold | 1000 | No | Amount of subsequent loop iterations with no work done before the worker goes to sleep. | | line.tcp.writer.worker.yield.threshold | 10 | No | Amount of subsequent loop iterations with no work done before the worker thread yields. | | line.tcp.writer.queue.capacity | 128 | No | Size of the queue between the IO jobs and the writer jobs, each queue entry represents a measurement. | | line.tcp.writer.halt.on.error | false | No | Flag that indicates if the worker thread must stop when an unexpected error occurs. | | line.tcp.io.worker.count | | No | Number of dedicated I/O worker threads assigned to parse TCP input. When `0`, the writer jobs will use the shared pool. | | line.tcp.io.worker.affinity | | No | Comma-separated list of thread numbers which should be pinned for line protocol ingestion over TCP. CPU core indexes are 0-based. | | line.tcp.io.worker.sleep.threshold | 1000 | No | Amount of subsequent loop iterations with no work done before the worker goes to sleep. | | line.tcp.io.worker.yield.threshold | 10 | No | Amount of subsequent loop iterations with no work done before the worker thread yields. | | line.tcp.disconnect.on.error | true | No | Disconnect TCP socket that sends malformed messages. | | line.tcp.acl.enabled | true | No | Enable or disable Access Control List (ACL) authentication for InfluxDB Line Protocol over TCP. Enterprise only. | #### UDP specific settings[​](https://questdb.com/docs/configuration/overview/#udp-specific-settings "Direct link to UDP specific settings") note The UDP receiver is deprecated since QuestDB version 6.5.2. We recommend ILP over HTTP instead, or less frequently [ILP over TCP](https://questdb.com/docs/ingestion/ilp/overview/) . | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | line.udp.join | 232.1.2.3 | No | Multicast address receiver joins. This values is ignored when receiver is in "unicast" mode. | | line.udp.bind.to | 0.0.0.0:9009 | No | IP address of the network interface to bind listener to and port. By default UDP receiver listens on all network interfaces. | | line.udp.commit.rate | 1000000 | No | For packet bursts the number of continuously received messages after which receiver will force commit. Receiver will commit irrespective of this parameter when there are no messages. | | line.udp.msg.buffer.size | 2048 | No | Buffer used to receive single message. This value should be roughly equal to your MTU size. | | line.udp.msg.count | 10000 | No | Only for Linux. On Linux, QuestDB will use the `recvmmsg()` system call. This is the max number of messages to receive at once. | | line.udp.receive.buffer.size | 8388608 | No | UDP socket buffer size. Larger size of the buffer will help reduce message loss during bursts. | | line.udp.enabled | false | No | Enable or disable UDP receiver. | | line.udp.own.thread | false | No | When `true`, UDP receiver will use its own thread and busy spin that for performance reasons. "false" makes receiver use worker threads that do everything else in QuestDB. | | line.udp.own.thread.affinity | \-1 | No | \-1 does not set thread affinity. OS will schedule thread and it will be liable to run on random cores and jump between the. 0 or higher pins thread to give core. This property is only valid when UDP receiver uses own thread. | | line.udp.unicast | false | No | When `true`, UDP will use unicast. Otherwise multicast. | | line.udp.timestamp | n | No | Input timestamp resolution. Possible values are `n`, `u`, `ms`, `s` and `h`. | | line.udp.commit.mode | nosync | No | Commit durability. Available values are `nosync`, `sync` and `async`. | ### Database replication[​](https://questdb.com/docs/configuration/overview/#database-replication "Direct link to Database replication") note Replication is [Enterprise](https://questdb.com/enterprise/) only. Replication enables high availability clusters. For setup instructions, see the [replication operations](https://questdb.com/docs/high-availability/setup/) guide. For an overview of the concept, see the [replication concept](https://questdb.com/docs/high-availability/overview/) page. For a tuning guide see, the [replication tuning guide](https://questdb.com/docs/high-availability/tuning/) . | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | replication.role | none | No | Defaults to `none` for stand-alone instances. To enable replication set to one of: `primary`, `replica`. | | replication.object.store | | No | A configuration string that allows connecting to an object store. The format is **scheme::key1=value;key2=value2;…**. The various keys and values are detailed in a later section. Ignored if replication is disabled. No default given variability. | | cairo.wal.segment.rollover.size | 2097152 | No | The size of the WAL segment before it is rolled over. Default is `2MiB`. However, defaults to `0` unless `replication.role=primary` is set. | | cairo.writer.command.queue.capacity | 32 | No | Maximum writer ALTER TABLE and replication command capacity. Shared between all the tables. | | replication.primary.throttle.window.duration | 10000 | No | The millisecond duration of the sliding window used to process replication batches. Default is `10000` ms. | | replication.requests.max.concurrent | 0 | No | A limit to the number of concurrent object store requests. The default is `0` for unlimited. | | replication.requests.retry.attempts | 3 | No | Maximum number of times to retry a failed object store request before logging an error and reattempting later after a delay. Default is `3`. | | replication.requests.retry.interval | 200 | No | How long to wait before retrying a failed operation. Default is `200` ms. | | replication.primary.compression.threads | calculated | No | Max number of threads used to perform file compression operations before uploading to the object store. The default value is calculated as half the number of CPU cores. | | replication.primary.compression.level | 1 | No | Zstd compression level. Defaults to `1`. Valid values are from 1 to 22. | | replication.replica.poll.interval | 1000 | No | Millisecond polling rate of a replica instance to check for the availability of new changes. | | replication.primary.sequencer.part.txn.count | 5000 | No | Sets the txn chunking size for each compressed batch. Smaller is better for constrained networks (but more costly). | | replication.primary.checksum=service-dependent | service-dependent | No | Where a checksum should be calculated for each uploaded artifact. Required for some object stores. Other options: never, always | | replication.primary.upload.truncated | true | No | Skip trailing, empty column data inside a WAL column file. | | replication.requests.buffer.size | 32768 | No | Buffer size used for object-storage downloads. | | replication.summary.interval | 1m | No | Frequency for printing replication progress summary in the logs. | | replication.metrics.per.table | true | No | Enable per-table replication metrics on the prometheus metrics endpoint. | | replication.metrics.dropped.table.poll.count | 10 | No | How many scrapes of prometheus metrics endpoint before dropped tables will no longer appear. | | replication.requests.max.batch.size.fast | 64 | No | Number of parallel requests allowed during the 'fast' process (non-resource constrained). | | replication.requests.max.batch.size.slow | 2 | No | Number of parallel requests allowed during the 'slow' process (error/resource constrained path). | | replication.requests.base.timeout | 10s | No | Replication upload/download request timeout. | | replication.requests.min.throughput | 262144 | No | Expected minimum network speed for replication transfers. Used to expand the timeout and account for network delays. | | native.async.io.threads | cpuCount | No | The number of async (network) io threads used for replication (and in the future cold storage). The default should be appropriate for most use cases. | | native.max.blocking.threads | cpuCount \* 4 | No | Maximum number of threads for parallel blocking disk IO read/write operations for replication (and other). These threads are ephemeral: They are spawned per need and shut down after a short duration if no longer in use. These are not cpu-bound threads, hence the relative large number. The default should be appropriate for most use cases. | | replication.primary.cleaner.enabled | true | No | Master switch for the WAL cleaner. | | replication.primary.cleaner.interval | 10m | No | Time between cleanup cycles. Range: 1s – 24h. | | replication.primary.cleaner.checkpoint.source | true | No | Use checkpoint history as a cleanup trigger source. | | replication.primary.cleaner.backup.window.count | backup.cleanup.keep.latest.n or 5 | No | Minimum complete backups/checkpoints per instance before cleanup starts. Defaults to `backup.cleanup.keep.latest.n` if backups are enabled, otherwise `5`. | | replication.primary.cleaner.delete.concurrency | 4 – 12 (auto) | No | Concurrent deletion tasks. Derived from `replication.requests.max.concurrent`. Range: 4 – 32. | | replication.primary.cleaner.max.requests.per.second | service-dependent | No | Rate limit for object store delete requests. Set to `0` for unlimited. Range: 0 – 10000. | | replication.primary.cleaner.progress.write.interval | 5s | No | How often progress is persisted during a cleanup cycle. Lower values mean less re-work after a crash but more writes. Range: 100ms – 60s. | | replication.primary.cleaner.dropped.table.cooloff | 1h | No | Wait time after `DROP TABLE` before removing the table's data from object storage. Guards against clock skew. | | replication.primary.cleaner.retry.attempts | 20 | No | Retries for transient object store failures during cleanup. Range: 0 – 100. | | replication.primary.cleaner.retry.interval | 2s | No | Delay between cleanup retries. Range: 0 – 5m. | | checkpoint.history.enabled | true (when replication is enabled) | No | Enable the checkpoint history tracker. Requires replication. | | checkpoint.history.keep.count | 100 | No | Maximum checkpoint records retained per instance. | | checkpoint.history.long.retry.interval | 1m | No | Retry interval for syncing checkpoint history to the object store after burst retries fail. | ### Identity and Access Management (IAM)[​](https://questdb.com/docs/configuration/overview/#identity-and-access-management-iam "Direct link to Identity and Access Management (IAM)") note Identity and Access Management is available within [QuestDB Enterprise](https://questdb.com/enterprise/) . Identity and Access Management (IAM) ensures that data can be accessed only by authorized users. The below configuration properties relate to various authentication and authorization features. For a full explanation of IAM, see the [Identity and Access Management (IAM) documentation](https://questdb.com/docs/security/rbac/) . | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | acl.enabled | true | No | Enables/disables Identity and Access Management. | | acl.admin.user.enabled | true | No | Enables/disables the built-in admin user. | | acl.admin.user | admin | No | Name of the built-in admin user. | | acl.admin.password | quest | Yes | The password of the built-in admin user. | | acl.basic.auth.realm.enabled | false | No | When enabled the browser's basic auth popup window is used instead of the Web Console's login screen. Only present for backwards compatibility. | | acl.entity.name.max.length | 255 | No | Maximum length of user, group and service account names. | | acl.password.hash.iteration.count | 100000 | No | QuestDB Enterprise never stores passwords in plain text, it stores password hashes only. This is the number of hash iterations used in password hashing. Higher means safer, almost never should be changed. | | acl.rest.token.refresh.threshold | 10 | No | When a REST token is created in REFRESH mode, its TTL is extended on every successful authentication, unless the last successful authentication was within this threshold. This setting removes unnecessary overhead of continuously refreshing REST tokens if they are used often. The value is expressed in seconds. | | tls.enabled | false | No | Enables/disables TLS encryption globally for all QuestDB interfaces (HTTP endpoints, ILP over TCP). | | tls.cert.path | | No | Path to certificate used for TLS encryption globally. The certificate should be DER-encoded and saved in PEM format. | | tls.private.key.path | | No | Path to private key used for TLS encryption globally. | | http.tls.enabled | false | No | Enables/disables TLS encryption for the HTTP server only. | | http.tls.cert.path | | No | Path to certificate used for TLS encryption for the HTTP server only. The certificate should be DER-encoded and saved in PEM format. | | http.tls.private.key.path | | No | Path to private key used for TLS encryption for the HTTP server only. | | http.min.tls.enabled | false | No | Enables/disables TLS encryption for the minimal HTTP server only. | | http.min.tls.cert.path | | No | Path to certificate used for TLS encryption for the minimal HTTP server only. The certificate should be DER-encoded and saved in PEM format. | | http.min.tls.private.key.path | | No | Path to private key used for TLS encryption for the minimal HTTP server only. | | line.tcp.tls.enabled | false | No | Enables/disables TLS encryption for ILP over TCP only. | | line.tcp.tls.cert.path | | No | Path to certificate used for TLS encryption for ILP over TCP only. The certificate should be DER-encoded and saved in PEM format. | | line.tcp.tls.private.key.path | | No | Path to private key used for TLS encryption for ILP over TCP only. | | line.tcp.acl.enabled | true | No | Enables/disables authentication for the ILP over TCP endpoint only. | ### OpenID Connect (OIDC)[​](https://questdb.com/docs/configuration/overview/#openid-connect-oidc "Direct link to OpenID Connect (OIDC)") note OpenID Connect is [Enterprise](https://questdb.com/enterprise/) only. OpenID Connect (OIDC) support is part of QuestDB's Identity and Access Management. The database can be integrated with any OAuth2/OIDC Identity Provider (IdP). For detailed information about OIDC, see the [OpenID Connect (OIDC) integration guide](https://questdb.com/docs/security/oidc/) . | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | acl.oidc.enabled | false | No | Enables/disables OIDC authentication. When enabled, few other configuration options must also be set. | | acl.oidc.pkce.enabled | true | No | Enables/disables PKCE for the Authorization Code Flow. This should always be enabled in a production environment, the Web Console is not fully secure without it. | | acl.oidc.ropc.flow.enabled | false | No | Enables/disables Resource Owner Password Credentials flow. When enabled, this flow also has to be configured in the OIDC Provider. | | acl.oidc.configuration.url | | No | URL where the OpenID Provider's configuration information cna be loaded in json format, should always end with `/.well-known/openid-configuration`. | | acl.oidc.host | | No | OIDC provider hostname. Required when OIDC is enabled, unless the OIDC configuration URL is set. | | acl.oidc.port | 443 | No | OIDC provider port number. | | acl.oidc.tls.enabled | true | No | Whether the OIDC provider requires a secure connection or not. It is highly unlikely in a production environment, but if the OpenID Provider endpoints do not require a secure connection, this option can be set to `false`. | | acl.oidc.tls.validation.enabled | true | No | Enables/disables TLS certificate validation. If you are working with self-signed certificates that you would like QuestDB to trust, disable this option. Validation is strongly recommended in production environments. QuestDB will check that the certificate is valid, and that it is issued for the server to which it connects. | | acl.oidc.tls.keystore.path | | No | Path to a keystore file that contains trusted Certificate Authorities. Will be used when validating the certificate of the OIDC provider. Not required if your OIDC provider's certificate is signed by a public CA. | | acl.oidc.tls.keystore.password | | No | Keystore password, required if there is a keystore file and it is password protected. | | acl.oidc.http.timeout | 30000 | No | OIDC provider HTTP request timeout in milliseconds. | | acl.oidc.client.id | | No | Client name assigned to QuestDB in the OIDC server, required when OIDC is enabled. | | acl.oidc.audience | | No | OAuth2 audience as set on the tokens issued by the OIDC Provider, defaults to the client id. | | acl.oidc.redirect.uri | | No | The redirect URI tells the OIDC server where to redirect the user after successful authentication. If not set, the Web Console defaults it to the location where it was loaded from (`window.location.href`). | | acl.oidc.scope | openid | No | The OIDC server should ask consent for the list of scopes provided in this property. The scope `openid` is mandatory, and always should be included. | | acl.oidc.public.keys.endpoint | /pf/JWKS | No | JSON Web Key Set (JWKS) Endpoint, the default value should work for the Ping Identity Platform. This endpoint provides the list of public keys can be used to decode and validate ID tokens issued by the OIDC Provider. | | acl.oidc.authorization.endpoint | /as/authorization.oauth2 | No | OIDC Authorization Endpoint, the default value should work for the Ping Identity Platform. | | acl.oidc.token.endpoint | /as/token.oauth2 | No | OIDC Token Endpoint, the default value should work for the Ping Identity Platform. | | acl.oidc.userinfo.endpoint | /idp/userinfo.openid | No | OIDC User Info Endpoint, the default value should work for the Ping Identity Platform. Used to retrieve additional user information which contains the user's group memberships. | | acl.oidc.groups.encoded.in.token | false | No | Should be set to false, if the OIDC Provider is configured to encode the group memberships of the user into the id token. When set to true, QuestDB will look for the groups in the token instead of calling the User Info endpoint. | | acl.oidc.sub.claim | sub | No | The name of the claim in the user information, which contains the name of the user. Could be a username, the user's full name or email. It will be displayed in the Web Console, and logged for audit purposes. | | acl.oidc.groups.claim | groups | No | The name of the custom claim in the user information, which contains the group memberships of the user. | | acl.oidc.cache.ttl | 30000 | No | User info cache entry TTL (time to live) in milliseconds, default value is 30 seconds. For improved performance QuestDB caches user info responses for each valid access token, this settings drives how often the access token should be validated and the user info updated. | | acl.oidc.pg.token.as.password.enabled | false | No | When enabled, the PGWire endpoint supports OIDC authentication. The OAuth2 token should be sent in the password field, while the username field should contain the string `_sso`, or left empty if that is an option. | ### Config Validation[​](https://questdb.com/docs/configuration/overview/#config-validation "Direct link to Config Validation") The database startup phase checks for configuration issues, such as invalid or deprecated settings. Issues may be classified as advisories or errors. Advisory issues are [logged](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#log-directory) without causing the database to stop its startup sequence: These are usually setting deprecation warnings. Configuration errors can optionally cause the database to fail its startup. | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | config.validation.strict | false | No | When enabled, startup fails if there are configuration errors. | _We recommended enabling strict validation._ ### Telemetry[​](https://questdb.com/docs/configuration/overview/#telemetry "Direct link to Telemetry") QuestDB sends anonymous telemetry data with information about usage which helps us improve the product over time. We do not collect any personally-identifying information, and we do not share any of this data with third parties. | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | telemetry.enabled | true | No | Enable or disable anonymous usage metrics collection. | | telemetry.hide.tables | false | No | Hides telemetry tables from `select * from tables()` output. As a result, telemetry tables will not be visible in the Web Console table view. | | telemetry.queue.capacity | 512 | No | Capacity of the internal telemetry queue, which is the gateway of all telemetry events. This queue capacity does not require tweaking. | Materialized views[​](https://questdb.com/docs/configuration/overview/#materialized-views "Direct link to Materialized views") ------------------------------------------------------------------------------------------------------------------------------- info Materialized View support is now generally available (GA) and ready for production use. If you are using versions earlier than `8.3.1`, we suggest you upgrade at your earliest convenience. The following settings are available in `server.conf`: | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | cairo.mat.view.enabled | true | No | Enables or disables SQL support and refresh job for materialized views. | | cairo.mat.view.parallel.sql.enabled | true | No | When disabled, SQL executed by the materialized view refresh job will always run single-threaded. | | mat.view.refresh.worker.count | 0 | No | Number of dedicated worker threads assigned to refresh materialized views. When `0`, the jobs will use the shared pool. | | mat.view.refresh.worker.affinity | Equal to the CPU core count | No | Comma separated list of numerical CPU core indexes. | | mat.view.refresh.worker.haltOnError | false | No | Flag that indicates if the worker thread must stop when an unexpected error occurs. | Logging & Metrics[​](https://questdb.com/docs/configuration/overview/#logging--metrics "Direct link to Logging & Metrics") --------------------------------------------------------------------------------------------------------------------------- The following settings are available in `server.conf`: | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | log.level.verbose | false | No | Converts short-hand log level indicators (E, C, I) into long-hand (ERROR, CRITICAL, INFO) | | log.timezone | UTC | No | Sets the timezone for log timestamps. Can be a timezone ID such as 'Antarctica/McMurdo', 'SystemDefault' to use system timezone, or the default UTC with 'Z' suffix | Further settings are available in `log.conf`. For more information, and details of our Prometheus metrics, please visit the [Logging & Metrics](https://questdb.com/docs/operations/logging-metrics/) documentation. * [Environment variables](https://questdb.com/docs/configuration/overview/#environment-variables) * [Examples](https://questdb.com/docs/configuration/overview/#examples) * [Secrets from files](https://questdb.com/docs/configuration/overview/#secrets-from-files) * [Usage](https://questdb.com/docs/configuration/overview/#usage) * [Precedence](https://questdb.com/docs/configuration/overview/#precedence) * [File requirements](https://questdb.com/docs/configuration/overview/#file-requirements) * [Error handling](https://questdb.com/docs/configuration/overview/#error-handling) * [Reloading secrets](https://questdb.com/docs/configuration/overview/#reloading-secrets) * [Supported properties](https://questdb.com/docs/configuration/overview/#supported-properties) * [Reloadable settings](https://questdb.com/docs/configuration/overview/#reloadable-settings) * [Keys and default values](https://questdb.com/docs/configuration/overview/#keys-and-default-values) * [Shared worker](https://questdb.com/docs/configuration/overview/#shared-worker) * [HTTP server](https://questdb.com/docs/configuration/overview/#http-server) * [Cairo engine](https://questdb.com/docs/configuration/overview/#cairo-engine) * [WAL table configurations](https://questdb.com/docs/configuration/overview/#wal-table-configurations) * [COPY settings](https://questdb.com/docs/configuration/overview/#copy-settings) * [Parallel SQL execution](https://questdb.com/docs/configuration/overview/#parallel-sql-execution) * [Postgres wire protocol](https://questdb.com/docs/configuration/overview/#postgres-wire-protocol) * [InfluxDB Line Protocol (ILP)](https://questdb.com/docs/configuration/overview/#influxdb-line-protocol-ilp) * [Database replication](https://questdb.com/docs/configuration/overview/#database-replication) * [Identity and Access Management (IAM)](https://questdb.com/docs/configuration/overview/#identity-and-access-management-iam) * [OpenID Connect (OIDC)](https://questdb.com/docs/configuration/overview/#openid-connect-oidc) * [Config Validation](https://questdb.com/docs/configuration/overview/#config-validation) * [Telemetry](https://questdb.com/docs/configuration/overview/#telemetry) * [Materialized views](https://questdb.com/docs/configuration/overview/#materialized-views) * [Logging & Metrics](https://questdb.com/docs/configuration/overview/#logging--metrics) --- # Time To Live (TTL) | QuestDB On this page TTL (Time To Live) automatically drops old partitions based on data age. Set a retention period, and QuestDB removes partitions that fall entirely outside that window - no cron jobs or manual cleanup required. ![Timeline showing how TTL drops partitions when their entire time range falls outside the retention window](https://questdb.com/docs/images/docs/concepts/ttl.svg) Requirements[​](https://questdb.com/docs/concepts/ttl/#requirements "Direct link to Requirements") --------------------------------------------------------------------------------------------------- TTL requires: * A [designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) column * [Partitioning](https://questdb.com/docs/concepts/partitions/) enabled These are standard for time-series tables in QuestDB. Setting TTL[​](https://questdb.com/docs/concepts/ttl/#setting-ttl "Direct link to Setting TTL") ------------------------------------------------------------------------------------------------ ### At table creation[​](https://questdb.com/docs/concepts/ttl/#at-table-creation "Direct link to At table creation") CREATE TABLE trades ( ts TIMESTAMP, symbol SYMBOL, price DOUBLE) TIMESTAMP(ts) PARTITION BY DAY TTL 7 DAYS; ### On existing tables[​](https://questdb.com/docs/concepts/ttl/#on-existing-tables "Direct link to On existing tables") ALTER TABLE trades SET TTL 7 DAYS; Supported units: `HOUR`/`H`, `DAY`/`D`, `WEEK`/`W`, `MONTH`/`M`, `YEAR`/`Y`. -- These are equivalentALTER TABLE trades SET TTL 2 WEEKS;ALTER TABLE trades SET TTL 2w; For full syntax, see [ALTER TABLE SET TTL](https://questdb.com/docs/query/sql/alter-table-set-ttl/) . How TTL works[​](https://questdb.com/docs/concepts/ttl/#how-ttl-works "Direct link to How TTL works") ------------------------------------------------------------------------------------------------------ TTL drops partitions based on the **partition's time range**, not individual row timestamps. A partition is dropped only when its **entire period** falls outside the TTL window. **Key rule**: A partition is dropped when `partition_end_time < reference_time - TTL`. ### Reference time[​](https://questdb.com/docs/concepts/ttl/#reference-time "Direct link to Reference time") By default, TTL uses wall-clock time as the reference, not the maximum timestamp in the table. This protects against accidental data loss if a row with a far-future timestamp is inserted (which would otherwise cause all existing data to appear "expired"). The reference time is: `min(max_timestamp_in_table, wall_clock_time)` To restore legacy behavior (using only max timestamp), set in `server.conf`: cairo.ttl.use.wall.clock=false caution Disabling wall-clock protection means inserting a row with a future timestamp (e.g., year 2100) will immediately drop all partitions that fall outside the TTL window relative to that future time. ### Example[​](https://questdb.com/docs/concepts/ttl/#example "Direct link to Example") Table partitioned by `HOUR` with `TTL 1 HOUR`: | Wall-clock time | Action | Partitions remaining | | --- | --- | --- | | 08:00 | Insert row at 08:00 | `08:00-09:00` | | 09:00 | Insert row at 09:00 | `08:00-09:00`, `09:00-10:00` | | 09:59 | Insert row at 09:59 | `08:00-09:00`, `09:00-10:00` | | 10:00 | Insert row at 10:00 | `09:00-10:00`, `10:00-11:00` | The `08:00-09:00` partition survives until 10:00 because its **end time** (09:00) must be more than 1 hour behind the reference time. At 10:00, the partition end (09:00) is exactly 1 hour old, so it's dropped. Checking TTL settings[​](https://questdb.com/docs/concepts/ttl/#checking-ttl-settings "Direct link to Checking TTL settings") ------------------------------------------------------------------------------------------------------------------------------ SELECT table_name, ttlValue, ttlUnit FROM tables(); | table\_name | ttlValue | ttlUnit | | --- | --- | --- | | trades | 7 | DAY | | metrics | 0 | _null_ | A `ttlValue` of `0` means TTL is not configured. Removing TTL[​](https://questdb.com/docs/concepts/ttl/#removing-ttl "Direct link to Removing TTL") --------------------------------------------------------------------------------------------------- To disable automatic retention and keep all data: ALTER TABLE trades SET TTL 0; Guidelines[​](https://questdb.com/docs/concepts/ttl/#guidelines "Direct link to Guidelines") --------------------------------------------------------------------------------------------- | Data type | Typical TTL | Rationale | | --- | --- | --- | | Real-time metrics | 1-7 days | High volume, recent data most valuable | | Trading data | 30-90 days | Compliance requirements vary | | Aggregated data | 1-2 years | Lower volume, longer analysis windows | | Audit logs | Per compliance | Often legally mandated retention | **Tips:** * Match TTL to your longest typical query range plus a buffer * TTL should be significantly larger than your partition interval * For manual control instead of automatic TTL, see [Data Retention](https://questdb.com/docs/operations/data-retention/) * [Requirements](https://questdb.com/docs/concepts/ttl/#requirements) * [Setting TTL](https://questdb.com/docs/concepts/ttl/#setting-ttl) * [At table creation](https://questdb.com/docs/concepts/ttl/#at-table-creation) * [On existing tables](https://questdb.com/docs/concepts/ttl/#on-existing-tables) * [How TTL works](https://questdb.com/docs/concepts/ttl/#how-ttl-works) * [Reference time](https://questdb.com/docs/concepts/ttl/#reference-time) * [Example](https://questdb.com/docs/concepts/ttl/#example) * [Checking TTL settings](https://questdb.com/docs/concepts/ttl/#checking-ttl-settings) * [Removing TTL](https://questdb.com/docs/concepts/ttl/#removing-ttl) * [Guidelines](https://questdb.com/docs/concepts/ttl/#guidelines) --- # Cookbook overview | QuestDB On this page The Cookbook is a collection of **short, actionable recipes** that demonstrate how to accomplish specific tasks with QuestDB. Each recipe follows a problem-solution-result format, making it easy to find and apply solutions quickly. What is the cookbook?[​](https://questdb.com/docs/cookbook/#what-is-the-cookbook "Direct link to What is the cookbook?") ------------------------------------------------------------------------------------------------------------------------- Unlike comprehensive reference documentation, the Cookbook focuses on practical examples for: * **Common SQL patterns** - Window functions, pivoting, time-series aggregations * **Programmatic integration** - Language-specific client examples * **Operations** - Deployment and configuration tasks Each recipe provides a focused solution to a specific problem, with working code examples and expected results. Structure[​](https://questdb.com/docs/cookbook/#structure "Direct link to Structure") -------------------------------------------------------------------------------------- The Cookbook is organized into the following sections: * **SQL Recipes** - Common SQL patterns, window functions, and time-series queries * **[Capital Markets](https://questdb.com/docs/cookbook/sql/finance/) ** - Technical indicators, execution analysis, and risk metrics for financial data * **[Time-Series Patterns](https://questdb.com/docs/cookbook/sql/time-series/elapsed-time/) ** - Common patterns for working with time-series data * **[Advanced SQL](https://questdb.com/docs/cookbook/sql/advanced/rows-before-after-value-match/) ** - Complex query patterns like pivoting, funnels, and histograms * **Programmatic** - Language-specific client examples and integration patterns * **Operations** - Deployment, configuration, and operational tasks ### Post-trade and execution analysis[​](https://questdb.com/docs/cookbook/#post-trade-and-execution-analysis "Direct link to Post-trade and execution analysis") QuestDB's time-series joins (`ASOF JOIN`, `HORIZON JOIN`) and high-resolution timestamps make it well-suited for **Transaction Cost Analysis (TCA)** and post-trade workflows. The [Execution & Post-Trade Analysis](https://questdb.com/docs/cookbook/sql/finance/) section includes recipes for: * [Slippage measurement](https://questdb.com/docs/cookbook/sql/finance/slippage/) - Per-fill and aggregated slippage against mid and top-of-book * [Markout analysis](https://questdb.com/docs/cookbook/sql/finance/markout/) - Post-trade price reversion curves and adverse selection detection * [Last look detection](https://questdb.com/docs/cookbook/sql/finance/last-look/) - Millisecond-granularity counterparty analysis * [Implementation shortfall](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/) - Cost decomposition into spread, permanent, and temporary impact Running the examples[​](https://questdb.com/docs/cookbook/#running-the-examples "Direct link to Running the examples") ----------------------------------------------------------------------------------------------------------------------- **Most recipes run directly on our [live demo instance at demo.questdb.com](https://demo.questdb.com/) ** without any local setup. Queries that can be executed on the demo site are marked with a direct link to run them. For recipes that require write operations or specific configuration, the recipe will indicate what setup is needed. The demo instance contains live FX market data with tables for core prices and order book snapshots. See the [Demo Data Schema](https://questdb.com/docs/cookbook/demo-data-schema/) page for details about available tables and their structure. Using the cookbook[​](https://questdb.com/docs/cookbook/#using-the-cookbook "Direct link to Using the cookbook") ----------------------------------------------------------------------------------------------------------------- Each recipe follows a consistent format: 1. **Problem statement** - What you're trying to accomplish 2. **Solution** - Code example with explanation 3. **Results** - Expected output or verification 4. **Additional context** - Tips, variations, or related documentation links Start by browsing the SQL Recipes section for common patterns, or jump directly to the recipe that matches your needs. * [What is the cookbook?](https://questdb.com/docs/cookbook/#what-is-the-cookbook) * [Structure](https://questdb.com/docs/cookbook/#structure) * [Post-trade and execution analysis](https://questdb.com/docs/cookbook/#post-trade-and-execution-analysis) * [Running the examples](https://questdb.com/docs/cookbook/#running-the-examples) * [Using the cookbook](https://questdb.com/docs/cookbook/#using-the-cookbook) --- # Demo data schema | QuestDB On this page The [QuestDB demo instance at demo.questdb.com](https://demo.questdb.io/) contains two datasets that you can query directly: simulated FX market data and real cryptocurrency trades. This page describes the available tables and their structure. tip The demo instance is read-only. For testing write operations (INSERT, UPDATE, DELETE), you'll need to run QuestDB locally. See the [Quick Start guide](https://questdb.com/docs/getting-started/quick-start/) for installation instructions. Overview[​](https://questdb.com/docs/cookbook/demo-data-schema/#overview "Direct link to Overview") ---------------------------------------------------------------------------------------------------- The demo instance provides two independent datasets: 1. **FX Market Data (Simulated)** - Foreign exchange prices and order books 2. **Cryptocurrency Trades (Real)** - Live cryptocurrency trades from OKX exchange * * * FX market data (simulated)[​](https://questdb.com/docs/cookbook/demo-data-schema/#fx-market-data-simulated "Direct link to FX market data (simulated)") -------------------------------------------------------------------------------------------------------------------------------------------------------- The FX dataset contains simulated foreign exchange market data for 30 currency pairs. We fetch real reference prices from Yahoo Finance every few seconds, but all order book levels and price updates are generated algorithmically based on these reference prices. ### core\_price table[​](https://questdb.com/docs/cookbook/demo-data-schema/#core_price-table "Direct link to core_price table") The `core_price` table contains individual FX price updates from various liquidity providers. Each row represents a bid/ask quote update for a specific currency pair from a specific ECN. #### Schema[​](https://questdb.com/docs/cookbook/demo-data-schema/#schema "Direct link to Schema") core\_price table structure CREATE TABLE 'core_price' ( timestamp TIMESTAMP, symbol SYMBOL, ecn SYMBOL, bid_price DOUBLE, bid_volume LONG, ask_price DOUBLE, ask_volume LONG, reason SYMBOL, indicator1 DOUBLE, indicator2 DOUBLE) timestamp(timestamp) PARTITION BY HOUR TTL 3 DAYS; #### Columns[​](https://questdb.com/docs/cookbook/demo-data-schema/#columns "Direct link to Columns") * **`timestamp`** - Time of the price update (designated timestamp) * **`symbol`** - Currency pair from the 30 tracked symbols (see list below) * **`ecn`** - Electronic Communication Network providing the quote: **LMAX**, **EBS**, **Currenex**, or **Hotspot** * **`bid_price`** - Bid price (price at which market makers are willing to buy) * **`bid_volume`** - Volume available at the bid price * **`ask_price`** - Ask price (price at which market makers are willing to sell) * **`ask_volume`** - Volume available at the ask price * **`reason`** - Reason for the price update: "normal", "liquidity\_event", or "news\_event" * **`indicator1`**, **`indicator2`** - Additional market indicators The table tracks **30 currency pairs**: EURUSD, GBPUSD, USDJPY, USDCHF, AUDUSD, USDCAD, NZDUSD, EURJPY, GBPJPY, EURGBP, AUDJPY, CADJPY, NZDJPY, EURAUD, EURNZD, AUDNZD, GBPAUD, GBPNZD, AUDCAD, NZDCAD, EURCAD, EURCHF, GBPCHF, USDNOK, USDSEK, USDZAR, USDMXN, USDSGD, USDHKD, USDTRY. #### Sample data[​](https://questdb.com/docs/cookbook/demo-data-schema/#sample-data "Direct link to Sample data") Recent core\_price updates[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20core_price%0AWHERE%20timestamp%20IN%20%27%24today%27%0ALIMIT%20-10%3B&executeQuery=true) SELECT * FROM core_priceWHERE timestamp IN '$today'LIMIT -10; **Results:** | timestamp | symbol | ecn | bid\_price | bid\_volume | ask\_price | ask\_volume | reason | indicator1 | indicator2 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 2025-12-18T11:46:13.059566Z | USDCHF | LMAX | 0.7959 | 219884 | 0.7971 | 223174 | liquidity\_event | 0.641 | | | 2025-12-18T11:46:13.060542Z | USDSGD | Currenex | 1.291 | 295757049 | 1.2982 | 301215620 | normal | 0.034 | | | 2025-12-18T11:46:13.061853Z | EURAUD | LMAX | 1.7651 | 6207630 | 1.7691 | 5631029 | liquidity\_event | 0.027 | | | 2025-12-18T11:46:13.064138Z | AUDNZD | LMAX | 1.1344 | 227668 | 1.1356 | 212604 | liquidity\_event | 0.881 | | | 2025-12-18T11:46:13.065041Z | GBPNZD | LMAX | 2.3307 | 2021166 | 2.3337 | 1712096 | normal | 0.308 | | | 2025-12-18T11:46:13.065187Z | USDCAD | EBS | 1.3837 | 2394978 | 1.3869 | 2300556 | normal | 0.084 | | | 2025-12-18T11:46:13.065722Z | USDZAR | EBS | 16.7211 | 28107021 | 16.7263 | 23536519 | liquidity\_event | 0.151 | | | 2025-12-18T11:46:13.066128Z | EURAUD | EBS | 1.763 | 810471822 | 1.7712 | 883424752 | news\_event | 0.027 | | | 2025-12-18T11:46:13.066700Z | CADJPY | Currenex | 113.63 | 20300827 | 114.11 | 19720915 | normal | 0.55 | | | 2025-12-18T11:46:13.071607Z | NZDJPY | Currenex | 89.95 | 35284228 | 90.46 | 30552528 | liquidity\_event | 0.69 | | ### market\_data table[​](https://questdb.com/docs/cookbook/demo-data-schema/#market_data-table "Direct link to market_data table") The `market_data` table contains order book snapshots for currency pairs. Each row represents a complete view of the order book at a specific timestamp, with bid and ask prices and volumes stored as 2D arrays. #### Schema[​](https://questdb.com/docs/cookbook/demo-data-schema/#schema-1 "Direct link to Schema") market\_data table structure CREATE TABLE 'market_data' ( timestamp TIMESTAMP, symbol SYMBOL CAPACITY 16384 CACHE, bids DOUBLE[][], asks DOUBLE[][], best_bid DOUBLE, best_ask DOUBLE) timestamp(timestamp) PARTITION BY HOUR TTL 3 DAYS; #### Columns[​](https://questdb.com/docs/cookbook/demo-data-schema/#columns-1 "Direct link to Columns") * **`timestamp`** - Time of the order book snapshot (designated timestamp) * **`symbol`** - Currency pair (e.g., EURUSD, GBPJPY) * **`bids`** - 2D array containing bid prices and volumes: `[[price1, price2, ...], [volume1, volume2, ...]]` * **`asks`** - 2D array containing ask prices and volumes: `[[price1, price2, ...], [volume1, volume2, ...]]` * **`best_bid`** - Best (highest) bid price. Equivalent to `bids[1][1]` but preferred when only the top-of-book price is needed, as it scans much less data * **`best_ask`** - Best (lowest) ask price. Equivalent to `asks[1][1]` but preferred when only the top-of-book price is needed, as it scans much less data The arrays are structured so that: * `bids[1]` contains bid prices (descending order - highest first) * `bids[2]` contains corresponding bid volumes * `asks[1]` contains ask prices (ascending order - lowest first) * `asks[2]` contains corresponding ask volumes #### Sample query[​](https://questdb.com/docs/cookbook/demo-data-schema/#sample-query "Direct link to Sample query") Recent order book snapshots[Demo this query](https://demo.questdb.io/?query=SELECT%20timestamp%2C%20symbol%2C%0A%20%20%20%20%20%20%20array_count(bids%5B1%5D)%20as%20bid_levels%2C%0A%20%20%20%20%20%20%20array_count(asks%5B1%5D)%20as%20ask_levels%0AFROM%20market_data%0AWHERE%20timestamp%20IN%20%27%24today%27%0ALIMIT%20-5%3B&executeQuery=true) SELECT timestamp, symbol, array_count(bids[1]) as bid_levels, array_count(asks[1]) as ask_levelsFROM market_dataWHERE timestamp IN '$today'LIMIT -5; **Results:** | timestamp | symbol | bid\_levels | ask\_levels | | --- | --- | --- | --- | | 2025-12-18T12:04:07.071512Z | EURAUD | 40 | 40 | | 2025-12-18T12:04:07.072060Z | USDJPY | 40 | 40 | | 2025-12-18T12:04:07.072554Z | USDMXN | 40 | 40 | | 2025-12-18T12:04:07.072949Z | USDCAD | 40 | 40 | | 2025-12-18T12:04:07.073002Z | USDSEK | 40 | 40 | Each order book snapshot contains 40 bid levels and 40 ask levels. ### fx\_trades table[​](https://questdb.com/docs/cookbook/demo-data-schema/#fx_trades-table "Direct link to fx_trades table") The `fx_trades` table contains simulated FX trade executions. Each row represents a trade that executed against the order book, with realistic partial fills and level walking. #### Schema[​](https://questdb.com/docs/cookbook/demo-data-schema/#schema-2 "Direct link to Schema") fx\_trades table structure CREATE TABLE 'fx_trades' ( timestamp TIMESTAMP_NS, symbol SYMBOL, ecn SYMBOL, trade_id UUID, side SYMBOL, passive BOOLEAN, price DOUBLE, quantity DOUBLE, counterparty SYMBOL, order_id UUID) timestamp(timestamp) PARTITION BY HOUR TTL 1 MONTH; #### Columns[​](https://questdb.com/docs/cookbook/demo-data-schema/#columns-2 "Direct link to Columns") * **`timestamp`** - Time of trade execution with nanosecond precision (designated timestamp) * **`symbol`** - Currency pair (same 30 pairs as `core_price`) * **`ecn`** - ECN where trade executed: **LMAX**, **EBS**, **Currenex**, or **Hotspot** * **`trade_id`** - Unique identifier for this specific trade * **`side`** - Trade direction: **buy** or **sell** * **`passive`** - Whether this was a passive (limit) or aggressive (market) order * **`price`** - Execution price * **`quantity`** - Trade size * **`counterparty`** - 20-character LEI (Legal Entity Identifier) of the counterparty * **`order_id`** - Parent order identifier (multiple trades can share the same `order_id` for partial fills) #### Sample data[​](https://questdb.com/docs/cookbook/demo-data-schema/#sample-data-1 "Direct link to Sample data") Recent FX trades[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20fx_trades%0AWHERE%20timestamp%20IN%20%27%24today%27%0ALIMIT%20-10%3B&executeQuery=true) SELECT * FROM fx_tradesWHERE timestamp IN '$today'LIMIT -10; **Results:** | timestamp | symbol | ecn | trade\_id | side | passive | price | quantity | counterparty | order\_id | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 2026-01-12T12:18:57.138282586Z | EURUSD | LMAX | d14e6e54-6c6b-495d-865d-47311a36519b | sell | false | 1.1705 | 193615.0 | 004409EA0ED5B9FF954B | db3cd1e6-c3e7-4909-8a64-31a2b6f0f9c0 | | 2026-01-12T12:18:57.138912209Z | EURUSD | LMAX | be857ed7-848f-4d23-83ff-3e5636cbc9de | sell | false | 1.1707 | 107749.0 | 000A4FB276D1BE98F143 | db3cd1e6-c3e7-4909-8a64-31a2b6f0f9c0 | | 2026-01-12T12:18:57.259555330Z | GBPUSD | EBS | 446cac16-9b25-4205-b1e1-3eda4a3bb539 | sell | false | 1.3401 | 192701.0 | 00119FEF98D9EC079D15 | d0d74987-8929-4c48-bc18-7164b1a956e3 | | 2026-01-12T12:18:57.303333947Z | GBPUSD | EBS | 27515a12-9ab6-4175-8fa3-422d4529f365 | sell | true | 1.3404 | 66295.0 | 00363EC8480C058FD36C | 239eae98-fc45-4e1c-bd45-8933909a67fc | | 2026-01-12T12:18:57.334406432Z | USDTRY | EBS | c82453b3-9961-40ea-a6ac-43c33fe0f235 | sell | true | 43.1001 | 65849.0 | 002A80CCE4AFD37D0642 | 2ce77a03-0f21-4241-8ca7-903080848dc0 | | 2026-01-12T12:18:57.365445776Z | USDJPY | LMAX | bf918a88-60c2-4a20-8f53-65298b5a10fe | buy | false | 156.82 | 55548.0 | 00EB428CCC1C1C240F71 | 7458b51d-65fa-4ffb-8fa8-840e88d2c316 | | 2026-01-12T12:18:57.479674129Z | USDJPY | EBS | c7c902bd-7075-4952-88d1-76d39ba4c706 | buy | false | 156.82 | 98591.0 | 00A10D27678CC03A0161 | 5992296a-684f-4783-9e8c-7206519a85f8 | | 2026-01-12T12:18:57.480051522Z | USDJPY | EBS | a20b6f91-7148-4b64-8a36-85da5bec66f9 | buy | false | 156.85 | 178152.0 | 00CBD8490AE2844C8554 | 5992296a-684f-4783-9e8c-7206519a85f8 | | 2026-01-12T12:18:57.509773474Z | GBPUSD | Currenex | ae6b771b-5abd-44c7-9e0e-3527ce6fb5b4 | sell | false | 1.3404 | 62305.0 | 006728CF215E44412D18 | 54ff8191-1891-4a5c-8b67-d5cd961ec5e8 | | 2026-01-12T12:18:57.334732460Z | USDTRY | EBS | 469637a5-6553-4aad-aad9-f7114c8a442d | sell | true | 43.1 | 101177.0 | 002CAC92E93AB4B3D30C | 2ce77a03-0f21-4241-8ca7-903080848dc0 | ### FX materialized views[​](https://questdb.com/docs/cookbook/demo-data-schema/#fx-materialized-views "Direct link to FX materialized views") The FX dataset includes several materialized views providing pre-aggregated data at different time intervals: #### Best bid/offer (BBO) views[​](https://questdb.com/docs/cookbook/demo-data-schema/#best-bidoffer-bbo-views "Direct link to Best bid/offer (BBO) views") * **`bbo_1s`** - Best bid and offer aggregated every 1 second * **`bbo_1m`** - Best bid and offer aggregated every 1 minute * **`bbo_1h`** - Best bid and offer aggregated every 1 hour * **`bbo_1d`** - Best bid and offer aggregated every 1 day #### Core price aggregations[​](https://questdb.com/docs/cookbook/demo-data-schema/#core-price-aggregations "Direct link to Core price aggregations") * **`core_price_1s`** - Core prices aggregated every 1 second * **`core_price_1d`** - Core prices aggregated every 1 day #### Market data OHLC[​](https://questdb.com/docs/cookbook/demo-data-schema/#market-data-ohlc "Direct link to Market data OHLC") * **`market_data_ohlc_1m`** - Open, High, Low, Close candlesticks at 1-minute intervals * **`market_data_ohlc_15m`** - OHLC candlesticks at 15-minute intervals * **`market_data_ohlc_1d`** - OHLC candlesticks at 1-day intervals #### FX trades OHLC[​](https://questdb.com/docs/cookbook/demo-data-schema/#fx-trades-ohlc "Direct link to FX trades OHLC") * **`fx_trades_ohlc_1m`** - OHLC candlesticks from trade executions at 1-minute intervals * **`fx_trades_ohlc_1d`** - OHLC candlesticks from trade executions at 1-day intervals These views are continuously updated and optimized for dashboard and analytics queries on FX data. ### FX data volume[​](https://questdb.com/docs/cookbook/demo-data-schema/#fx-data-volume "Direct link to FX data volume") * **`market_data`**: Approximately **160 million rows** per day (order book snapshots) * **`core_price`**: Approximately **73 million rows** per day (price updates across all ECNs and symbols) * **`fx_trades`**: Approximately **5.1 million rows** per day (trade executions) * * * Cryptocurrency trades (real)[​](https://questdb.com/docs/cookbook/demo-data-schema/#cryptocurrency-trades-real "Direct link to Cryptocurrency trades (real)") -------------------------------------------------------------------------------------------------------------------------------------------------------------- The cryptocurrency dataset contains **real market data** streamed live from the OKX exchange using FeedHandler. These are actual executed trades, not simulated data. ### trades table[​](https://questdb.com/docs/cookbook/demo-data-schema/#trades-table "Direct link to trades table") The `trades` table contains real cryptocurrency trade data. Each row represents an actual executed trade for a cryptocurrency pair. #### Schema[​](https://questdb.com/docs/cookbook/demo-data-schema/#schema-3 "Direct link to Schema") trades table structure CREATE TABLE 'trades' ( symbol SYMBOL CAPACITY 256 CACHE, side SYMBOL CAPACITY 256 CACHE, price DOUBLE, amount DOUBLE, timestamp TIMESTAMP) timestamp(timestamp) PARTITION BY DAY; #### Columns[​](https://questdb.com/docs/cookbook/demo-data-schema/#columns-3 "Direct link to Columns") * **`timestamp`** - Time when the trade was executed (designated timestamp) * **`symbol`** - Cryptocurrency trading pair from the active symbol set (see common pairs below) * **`side`** - Trade side: **buy** or **sell** * **`price`** - Execution price of the trade * **`amount`** - Trade size (volume in base currency) Common actively traded pairs include: ADA-USDT, AVAX-USDT, BTC-USDT, DAI-USDT, DOT-USDT, ETH-BTC, ETH-USDT, LTC-USDT, SOL-BTC, SOL-USDT, UNI-USDT, XLM-USDT. Historical data may include additional symbols. #### Sample data[​](https://questdb.com/docs/cookbook/demo-data-schema/#sample-data-2 "Direct link to Sample data") Recent cryptocurrency trades[Demo this query](https://demo.questdb.io/?query=SELECT%20*%20FROM%20trades%0ALIMIT%20-10%3B&executeQuery=true) SELECT * FROM tradesLIMIT -10; **Results:** | symbol | side | price | amount | timestamp | | --- | --- | --- | --- | --- | | BTC-USDT | buy | 85721.6 | 0.00045714 | 2025-12-18T19:31:11.203000Z | | BTC-USDT | buy | 85721.6 | 0.00045714 | 2025-12-18T19:31:11.203000Z | | BTC-USDT | buy | 85726.6 | 0.00001501 | 2025-12-18T19:31:11.206000Z | | BTC-USDT | buy | 85726.6 | 0.00001501 | 2025-12-18T19:31:11.206000Z | | BTC-USDT | buy | 85726.9 | 0.000887 | 2025-12-18T19:31:11.206000Z | | BTC-USDT | buy | 85726.9 | 0.000887 | 2025-12-18T19:31:11.206000Z | | BTC-USDT | buy | 85731.3 | 0.00004393 | 2025-12-18T19:31:11.206000Z | | BTC-USDT | buy | 85731.3 | 0.00004393 | 2025-12-18T19:31:11.206000Z | | ETH-USDT | sell | 2827.54 | 0.006929 | 2025-12-18T19:31:11.595000Z | | ETH-USDT | sell | 2827.54 | 0.006929 | 2025-12-18T19:31:11.595000Z | ### Cryptocurrency materialized views[​](https://questdb.com/docs/cookbook/demo-data-schema/#cryptocurrency-materialized-views "Direct link to Cryptocurrency materialized views") The cryptocurrency dataset includes materialized views for aggregated trade data: #### Trades aggregations[​](https://questdb.com/docs/cookbook/demo-data-schema/#trades-aggregations "Direct link to Trades aggregations") * **`trades_latest_1d`** - Latest trade data aggregated daily * **`trades_OHLC_15m`** - OHLC candlesticks for cryptocurrency trades at 15-minute intervals These views are continuously updated and provide faster query performance for cryptocurrency trade analysis. ### Cryptocurrency data volume[​](https://questdb.com/docs/cookbook/demo-data-schema/#cryptocurrency-data-volume "Direct link to Cryptocurrency data volume") * **`trades`**: Approximately **3.7 million rows** per day (real cryptocurrency trades) * * * Data retention[​](https://questdb.com/docs/cookbook/demo-data-schema/#data-retention "Direct link to Data retention") ---------------------------------------------------------------------------------------------------------------------- **FX tables** (`core_price` and `market_data`) use a **3-day TTL (Time To Live)**, meaning data older than 3 days is automatically removed. This keeps the demo instance responsive while providing sufficient recent data. **Cryptocurrency trades table** has **no retention policy** and contains historical data dating back to **March 8, 2022**. This provides multiple years of real cryptocurrency trade history for long-term analysis and backtesting. Using the demo data[​](https://questdb.com/docs/cookbook/demo-data-schema/#using-the-demo-data "Direct link to Using the demo data") ------------------------------------------------------------------------------------------------------------------------------------- You can run queries against both datasets directly on [demo.questdb.com](https://demo.questdb.io/) . Throughout the Cookbook, recipes using demo data will include a direct link to execute the query. Related Documentation * [SYMBOL type](https://questdb.com/docs/concepts/symbol/) * [Arrays in QuestDB](https://questdb.com/docs/query/datatypes/array/) * [Designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) * [Time-series aggregations](https://questdb.com/docs/query/functions/aggregation/) * [Overview](https://questdb.com/docs/cookbook/demo-data-schema/#overview) * [FX market data (simulated)](https://questdb.com/docs/cookbook/demo-data-schema/#fx-market-data-simulated) * [core\_price table](https://questdb.com/docs/cookbook/demo-data-schema/#core_price-table) * [market\_data table](https://questdb.com/docs/cookbook/demo-data-schema/#market_data-table) * [fx\_trades table](https://questdb.com/docs/cookbook/demo-data-schema/#fx_trades-table) * [FX materialized views](https://questdb.com/docs/cookbook/demo-data-schema/#fx-materialized-views) * [FX data volume](https://questdb.com/docs/cookbook/demo-data-schema/#fx-data-volume) * [Cryptocurrency trades (real)](https://questdb.com/docs/cookbook/demo-data-schema/#cryptocurrency-trades-real) * [trades table](https://questdb.com/docs/cookbook/demo-data-schema/#trades-table) * [Cryptocurrency materialized views](https://questdb.com/docs/cookbook/demo-data-schema/#cryptocurrency-materialized-views) * [Cryptocurrency data volume](https://questdb.com/docs/cookbook/demo-data-schema/#cryptocurrency-data-volume) * [Data retention](https://questdb.com/docs/cookbook/demo-data-schema/#data-retention) * [Using the demo data](https://questdb.com/docs/cookbook/demo-data-schema/#using-the-demo-data) --- # Query multiple tables dynamically in Grafana | QuestDB On this page Query multiple QuestDB tables dynamically in Grafana using dashboard variables. This is useful when you have many tables with identical schemas (e.g., sensor data, metrics from different sources) and want to visualize them together without hardcoding table names in your queries. Problem: Visualize many similar tables[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#problem-visualize-many-similar-tables "Direct link to Problem: Visualize many similar tables") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- You have 100+ tables with the same structure (e.g., `sensor_1`, `sensor_2`, ..., `sensor_n`) and want to: 1. Display data from all tables on a single Grafana chart 2. Avoid manually updating queries when tables are added or removed 3. Allow users to select which tables to visualize via dashboard controls Solution: Use Grafana variables with dynamic SQL[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#solution-use-grafana-variables-with-dynamic-sql "Direct link to Solution: Use Grafana variables with dynamic SQL") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Create Grafana dashboard variables that query QuestDB for table names, then use string aggregation functions to build the SQL query dynamically. ### Step 1: Get table names[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#step-1-get-table-names "Direct link to Step 1: Get table names") First, query QuestDB to get all relevant table names: SELECT table_name FROM tables()WHERE table_name LIKE 'sensor_%'; This returns a list of all tables matching the pattern. ### Step 2: Create Grafana variables[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#step-2-create-grafana-variables "Direct link to Step 2: Create Grafana variables") Create two dashboard variables to construct the dynamic query: **Variable 1: `$table_list`** - Build the JOIN clause WITH tbs AS ( SELECT string_agg(table_name, ',') as names FROM tables() WHERE table_name LIKE 'sensor_%')SELECT replace(names, ',', ' ASOF JOIN ') FROM tbs; **Output:** `sensor_1 ASOF JOIN sensor_2 ASOF JOIN sensor_3 ASOF JOIN sensor_4` This creates the table list with ASOF JOIN operators between them. **Variable 2: `$column_avgs`** - Build the column list SELECT string_agg(concat('avg(', table_name, '.value)'), ',') as columnsFROM tables()WHERE table_name LIKE 'sensor_%'; **Output:** `avg(sensor_1.value),avg(sensor_2.value),avg(sensor_3.value),avg(sensor_4.value)` This creates the column selection list with aggregation functions. ### Step 3: Use variables in dashboard query[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#step-3-use-variables-in-dashboard-query "Direct link to Step 3: Use variables in dashboard query") Now reference these variables in your Grafana chart query: SELECT sensor_1.timestamp, $column_avgsFROM $table_listSAMPLE BY 1s FROM $__fromTime TO $__toTime FILL(PREV); When Grafana executes this query, it interpolates the variables: SELECT sensor_1.timestamp, avg(sensor_1.value),avg(sensor_2.value),avg(sensor_3.value),avg(sensor_4.value)FROM sensor_1 ASOF JOIN sensor_2 ASOF JOIN sensor_3 ASOF JOIN sensor_4SAMPLE BY 1s FROM cast(1571176800000000 as timestamp) TO cast(1571349600000000 as timestamp) FILL(PREV); How it works[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#how-it-works "Direct link to How it works") ------------------------------------------------------------------------------------------------------------------------------------------ The solution uses three key QuestDB features: 1. **`tables()` function**: Returns metadata about all tables in the database 2. **`string_agg()`**: Concatenates multiple rows into a single comma-separated string 3. **`replace()`**: Swaps commas for JOIN operators to build the FROM clause Combined with Grafana's variable interpolation: * `$column_avgs`: Replaced with the aggregated column list * `$table_list`: Replaced with the joined table expression * `$__fromTime` / `$__toTime`: Grafana macros for the dashboard's time range ### Understanding ASOF JOIN[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#understanding-asof-join "Direct link to Understanding ASOF JOIN") `ASOF JOIN` is ideal for time-series data with different update frequencies: * Joins tables on timestamp * For each row in the first table, finds the closest past timestamp in other tables * Works like a LEFT JOIN but with time-based matching This ensures that even if tables update at different rates, you get a complete dataset with the most recent known value from each table. Adapting the pattern[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#adapting-the-pattern "Direct link to Adapting the pattern") ------------------------------------------------------------------------------------------------------------------------------------------------------------------ **Filter by different patterns:** -- Tables starting with "metrics_"WHERE table_name LIKE 'metrics_%'-- Tables matching a regex patternWHERE table_name ~ 'sensor_[0-9]+'-- Exclude certain tablesWHERE table_name LIKE 'sensor_%' AND table_name NOT IN ('sensor_test', 'sensor_backup') Programmatic alternative[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#programmatic-alternative "Direct link to Programmatic alternative") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ If you're not using Grafana, you can achieve the same result programmatically: 1. **Query for table names:** SELECT table_name FROM tables() WHERE table_name LIKE 'sensor_%'; 2. **Build the query on the client side:** # Python exampletables = ['sensor_1', 'sensor_2', 'sensor_3']# Build JOIN clausejoin_clause = ' ASOF JOIN '.join(tables)# Build column listcolumns = ','.join([f'avg({t}.value)' for t in tables])# Final queryquery = f""" SELECT {tables[0]}.timestamp, {columns} FROM {join_clause} SAMPLE BY 1s FILL(PREV)""" Handling different sampling intervals[​](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#handling-different-sampling-intervals "Direct link to Handling different sampling intervals") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- When tables have different update frequencies, use FILL to handle gaps: -- Fill with previous value (holds last known value)SAMPLE BY 1s FILL(PREV)-- Fill with linear interpolationSAMPLE BY 1s FILL(LINEAR)-- Fill with NULL (show actual gaps)SAMPLE BY 1s FILL(NULL)-- Fill with zeroSAMPLE BY 1s FILL(0) **Choose based on your data:** * **PREV**: Best for metrics that persist (temperatures, prices, statuses) * **LINEAR**: Best for continuous values that change smoothly * **NULL**: Best when you want to see actual data gaps * **0 or constant**: Best for counting or rate metrics Performance Optimization Joining many tables can be expensive. To improve performance: * Use `SAMPLE BY` to reduce the number of rows * Add timestamp filters early in the query * Consider pre-aggregating data into a single table for frequently-accessed views * Limit the number of tables joined (split into multiple charts if needed) Table Schema Consistency This pattern assumes all tables have identical schemas. If schemas differ: * The query will fail at runtime * You'll need to handle missing columns explicitly * Consider using separate queries for tables with different structures Related Documentation * [ASOF JOIN](https://questdb.com/docs/query/sql/join/#asof-join) * [tables() function](https://questdb.com/docs/query/functions/meta/#tables) * [string\_agg()](https://questdb.com/docs/query/functions/aggregation/#string_agg) * [SAMPLE BY](https://questdb.com/docs/query/sql/sample-by/) * [Grafana QuestDB data source](https://grafana.com/grafana/plugins/questdb-questdb-datasource/) * [Problem: Visualize many similar tables](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#problem-visualize-many-similar-tables) * [Solution: Use Grafana variables with dynamic SQL](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#solution-use-grafana-variables-with-dynamic-sql) * [Step 1: Get table names](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#step-1-get-table-names) * [Step 2: Create Grafana variables](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#step-2-create-grafana-variables) * [Step 3: Use variables in dashboard query](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#step-3-use-variables-in-dashboard-query) * [How it works](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#how-it-works) * [Understanding ASOF JOIN](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#understanding-asof-join) * [Adapting the pattern](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#adapting-the-pattern) * [Programmatic alternative](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#programmatic-alternative) * [Handling different sampling intervals](https://questdb.com/docs/cookbook/integrations/grafana/dynamic-table-queries/#handling-different-sampling-intervals) --- # Overlay two time series with time shift | QuestDB On this page Compare yesterday's data against today's data on the same Grafana chart by overlaying them. Problem[​](https://questdb.com/docs/cookbook/integrations/grafana/overlay-timeshift/#problem "Direct link to Problem") ----------------------------------------------------------------------------------------------------------------------- You have a query with Grafana's `timeshift` set to `1d/d` to display yesterday's data. You want to overlay today's data on the same chart, starting from scratch each day, so you can compare the shapes of both time series. Solution[​](https://questdb.com/docs/cookbook/integrations/grafana/overlay-timeshift/#solution "Direct link to Solution") -------------------------------------------------------------------------------------------------------------------------- Leave the timeshift as `1d/d` to cover yesterday, and add a new query to the same chart. In this new query, filter for timestamp plus 1 day to cover today's datapoints, then shift them back by 1 day for display. **Query 1 (Yesterday's data):** DECLARE @symbol := 'BTC-USDT'WITH sampled AS ( SELECT timestamp, symbol, volume AS volume, ((open+close)/2) * volume AS traded_value FROM trades_OHLC_15m WHERE $__timeFilter(timestamp) AND symbol = @symbol), cumulative AS ( SELECT timestamp, symbol, SUM(traded_value) OVER w AS cumulative_value, SUM(volume) OVER w AS cumulative_volume FROM sampled WINDOW w AS (ORDER BY timestamp))SELECT timestamp as time, cumulative_value/cumulative_volume AS vwap_yesterday FROM cumulative; **Query 2 (Today's data, shifted back):** DECLARE @symbol := 'BTC-USDT'WITH sampled AS ( SELECT timestamp, symbol, volume AS volume, ((open+close)/2) * volume AS traded_value FROM trades_OHLC_15m WHERE timestamp BETWEEN dateadd('d',1,$__unixEpochFrom()*1000000) AND dateadd('d',1,$__unixEpochTo() * 1000000) AND symbol = @symbol), cumulative AS ( SELECT timestamp, symbol, SUM(traded_value) OVER w AS cumulative_value, SUM(volume) OVER w AS cumulative_volume FROM sampled WINDOW w AS (ORDER BY timestamp))SELECT dateadd('d',-1,timestamp) as time, cumulative_value/cumulative_volume AS vwap_today FROM cumulative; **Note:** This example uses `$__unixEpochFrom()` and `$__unixEpochTo()` macros from the PostgreSQL Grafana plugin. When using the QuestDB plugin, the equivalent macros are `$__fromTime` and `$__toTime` and don't need epoch conversion as those are native timestamps. This creates an overlay chart where yesterday's and today's data align on the same time axis, allowing direct comparison. Related Documentation * [UNION ALL](https://questdb.com/docs/query/sql/union-except-intersect/) * [Window functions](https://questdb.com/docs/query/functions/window-functions/syntax/) * [Grafana integration](https://questdb.com/docs/integrations/visualization/grafana/) * [Problem](https://questdb.com/docs/cookbook/integrations/grafana/overlay-timeshift/#problem) * [Solution](https://questdb.com/docs/cookbook/integrations/grafana/overlay-timeshift/#solution) --- # Configure read-only user for Grafana | QuestDB On this page Configure a dedicated read-only user for Grafana to improve security by preventing accidental data modifications through dashboards. This allows you to maintain separate credentials for visualization (read-only) and administration (full access), following the principle of least privilege. QuestDB Enterprise For QuestDB Enterprise, use the comprehensive [Role-Based Access Control (RBAC)](https://questdb.com/docs/security/rbac/) system to create granular user permissions and roles. The configuration below applies to QuestDB Open Source. Problem: Separate read and write access[​](https://questdb.com/docs/cookbook/integrations/grafana/read-only-user/#problem-separate-read-and-write-access "Direct link to Problem: Separate read and write access") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- You want to: 1. Connect Grafana with read-only credentials 2. Prevent accidental `INSERT`, `UPDATE`, `DELETE`, or `DROP` operations from dashboards 3. Still be able to execute DDL statements (`CREATE TABLE`, etc.) from the QuestDB web console However, QuestDB's PostgreSQL wire protocol doesn't support standard PostgreSQL user management commands like `CREATE USER` or `GRANT`. Solution: Enable the read-only user[​](https://questdb.com/docs/cookbook/integrations/grafana/read-only-user/#solution-enable-the-read-only-user "Direct link to Solution: Enable the read-only user") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB Open Source supports a built-in read-only user that can be enabled via configuration. This gives you two users: * **Admin user** (default: `admin`): Full access for DDL and DML operations * **Read-only user** (default: `user`): Query-only access for dashboards ### Configuration[​](https://questdb.com/docs/cookbook/integrations/grafana/read-only-user/#configuration "Direct link to Configuration") Add these settings to your `server.conf` file or set them as environment variables: **Via server.conf:** # Enable the read-only userpg.readonly.user.enabled=true# Optional: Customize username (default is "user")pg.readonly.user=grafana_reader# Optional: Customize password (default is "quest")pg.readonly.password=secure_password_here **Via environment variables:** export QDB_PG_READONLY_USER_ENABLED=trueexport QDB_PG_READONLY_USER=grafana_readerexport QDB_PG_READONLY_PASSWORD=secure_password_here **Via Docker:** docker run \ -p 9000:9000 -p 8812:8812 \ -e QDB_PG_READONLY_USER_ENABLED=true \ -e QDB_PG_READONLY_USER=grafana_reader \ -e QDB_PG_READONLY_PASSWORD=secure_password_here \ questdb/questdb:latest ### Using the read-only user[​](https://questdb.com/docs/cookbook/integrations/grafana/read-only-user/#using-the-read-only-user "Direct link to Using the read-only user") After enabling, you have two separate users: **Admin user (web console):** * Username: `admin` (default) * Password: `quest` (default) * Permissions: Full access - `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER` * Use for: QuestDB web console, administrative tasks, schema changes **Read-only user (Grafana):** * Username: `grafana_reader` (or whatever you configured) * Password: `secure_password_here` (or whatever you configured) * Permissions: `SELECT` queries only * Use for: Grafana dashboards, monitoring tools, analytics applications Related Documentation * [PostgreSQL wire protocol](https://questdb.com/docs/query/pgwire/overview/) * [QuestDB Enterprise RBAC](https://questdb.com/docs/security/rbac/) * [Configuration reference](https://questdb.com/docs/configuration/overview/) * [Grafana QuestDB data source](https://grafana.com/grafana/plugins/questdb-questdb-datasource/) * [Problem: Separate read and write access](https://questdb.com/docs/cookbook/integrations/grafana/read-only-user/#problem-separate-read-and-write-access) * [Solution: Enable the read-only user](https://questdb.com/docs/cookbook/integrations/grafana/read-only-user/#solution-enable-the-read-only-user) * [Configuration](https://questdb.com/docs/cookbook/integrations/grafana/read-only-user/#configuration) * [Using the read-only user](https://questdb.com/docs/cookbook/integrations/grafana/read-only-user/#using-the-read-only-user) --- # Grafana variable dropdown with name and value | QuestDB On this page Create Grafana variable dropdowns where the displayed label differs from the value used in queries. This is useful when you want to show user-friendly names in the dropdown while using different values (like IDs, prices, or technical identifiers) in your actual SQL queries. Problem: Separate display and query values[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#problem-separate-display-and-query-values "Direct link to Problem: Separate display and query values") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- You want a Grafana variable dropdown that: * **Displays:** Readable labels like `"BTC-USDT"`, `"ETH-USDT"`, `"SOL-USDT"` * **Uses in queries:** Different values like prices (`37779.62`, `2615.54`, `98.23`) or IDs For example, with this query result: | symbol | price | | --- | --- | | BTC-USDT | 37779.62 | | ETH-USDT | 2615.54 | | SOL-USDT | 98.23 | You want the dropdown to show `"BTC-USDT"` but use `37779.62` in your queries. Solution: Use regex variable filters[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#solution-use-regex-variable-filters "Direct link to Solution: Use regex variable filters") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- When using the QuestDB data source plugin, you can use Grafana's regex variable filters to parse a concatenated string into separate `text` and `value` fields. ### Step 1: Concatenate columns in query[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#step-1-concatenate-columns-in-query "Direct link to Step 1: Concatenate columns in query") First, combine both columns into a single string with a separator that doesn't appear in your data: WITH t AS ( SELECT symbol, first(price) as price FROM trades WHERE symbol LIKE '%BTC%')SELECT concat(symbol, '#', price) FROM t; **Query results:** DOGE-BTC#0.00000204ETH-BTC#0.05551BTC-USDT#37779.62SOL-BTC#0.0015282MATIC-BTC#0.00002074BTC-USDC#60511.1 Each row is now a single string with symbol and price separated by `#`. ### Step 2: Apply regex filter in Grafana variable[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#step-2-apply-regex-filter-in-grafana-variable "Direct link to Step 2: Apply regex filter in Grafana variable") In your Grafana variable configuration: **Query:** WITH t AS ( SELECT symbol, first(price) as price FROM trades WHERE symbol LIKE '%BTC%')SELECT concat(symbol, '#', price) FROM t; **Regex Filter:** /(?[^#]+)#(?.*)/ This regex pattern: * `(?[^#]+)`: Captures everything before `#` into the `text` group (the display label) * `#`: Matches the separator * `(?.*)`: Captures everything after `#` into the `value` group (the query value) ### Step 3: Use variable in queries[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#step-3-use-variable-in-queries "Direct link to Step 3: Use variable in queries") Now you can reference the variable in your dashboard queries: SELECT timestamp, priceFROM tradesWHERE price = $your_variable_name AND timestamp >= $__fromTime AND timestamp <= $__toTime; When a user selects "BTC-USDT" from the dropdown, Grafana will substitute the corresponding price value (`37779.62`) into the query. How it works[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#how-it-works "Direct link to How it works") -------------------------------------------------------------------------------------------------------------------------------------- Grafana's regex filter with named capture groups enables the separation: 1. **Named capture groups**: `(?...)` and `(?...)` tell Grafana which parts to use 2. **`text` group**: Becomes the visible label in the dropdown 3. **`value` group**: Becomes the interpolated value in queries 4. **Pattern matching**: The regex must match the entire string returned by your query ### Regex pattern breakdown[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#regex-pattern-breakdown "Direct link to Regex pattern breakdown") /(?[^#]+)#(?.*)/ * `/`: Regex delimiters * `(?...)`: Named capture group called "text" * `[^#]+`: One or more characters that are NOT `#` (greedy match) * `#`: Literal separator character * `(?.*)`: Named capture group called "value" * `.*`: Zero or more characters of any type (captures rest of string) Choosing a separator[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#choosing-a-separator "Direct link to Choosing a separator") -------------------------------------------------------------------------------------------------------------------------------------------------------------- Pick a separator that **never** appears in your data: **Good separators:** * `#` - Uncommon in most data * `|` - Clear visual separator * `::` - Two characters, unlikely to appear * `~` - Rarely used in trading symbols or prices * `^^^` - Multi-character separator for extra safety **Bad separators:** * `-` - Common in trading pairs (BTC-USDT) * `.` - Common in decimal numbers * `,` - Common in CSV-like data * Space - Can cause parsing issues Alternative patterns[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#alternative-patterns "Direct link to Alternative patterns") -------------------------------------------------------------------------------------------------------------------------------------------------------------- ### Multiple data fields[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#multiple-data-fields "Direct link to Multiple data fields") If you need more than two fields, use additional separators: SELECT concat(symbol, '#', price, '#', volume) FROM trades; /(?[^#]+)#(?[^#]+)#(?.*)/ Now you have three captured groups, though Grafana's variable system typically only uses `text` and `value`. ### Numeric IDs with descriptions[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#numeric-ids-with-descriptions "Direct link to Numeric IDs with descriptions") Common pattern for entity selection: SELECT concat(name, '#', id) FROM users; /(?[^#]+)#(?\d+)/ Output in dropdown: User sees "John Doe", query uses `42`. ### Escaping special characters[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#escaping-special-characters "Direct link to Escaping special characters") If your data contains regex special characters, escape them in the pattern: -- If data contains parenthesesSELECT concat(name, ' (', id, ')', '#', id) FROM users;-- Result: "John Doe (42)#42" /(?.*?)#(?\d+)/ PostgreSQL data source alternative[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#postgresql-data-source-alternative "Direct link to PostgreSQL data source alternative") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- If using the PostgreSQL data source (instead of the QuestDB plugin), you can use special column aliases: SELECT symbol AS __text, price AS __valueFROM tradesWHERE symbol LIKE '%BTC%'; The PostgreSQL data source recognizes `__text` and `__value` as special column names for dropdown variables. **Note:** This works with the PostgreSQL data source plugin pointing to QuestDB, but NOT with the native QuestDB data source plugin. Adapting the pattern[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#adapting-the-pattern "Direct link to Adapting the pattern") -------------------------------------------------------------------------------------------------------------------------------------------------------------- **Different filter conditions:** -- Filter by time rangeWHERE timestamp IN '$yesterday'-- Filter by multiple criteriaWHERE symbol LIKE '%USDT' AND price > 1000-- Dynamic filter using another variableWHERE symbol LIKE concat('%', $base_currency, '%') **Sorting the dropdown:** -- Sort alphabetically by symbolSELECT concat(symbol, '#', price) FROM tradesORDER BY symbol;-- Sort by price (highest first)SELECT concat(symbol, '#', price) FROM tradesORDER BY price DESC;-- Sort by volumeWITH t AS ( SELECT symbol, first(price) as price, sum(amount) as volume FROM trades GROUP BY symbol)SELECT concat(symbol, '#', price) FROM tORDER BY volume DESC; **Include additional context in label:** -- Show symbol and volume in the labelSELECT concat(symbol, ' (Vol: ', round(sum(amount), 2), ')', '#', first(price))FROM tradesGROUP BY symbol; Result: "BTC-USDT (Vol: 1234.56)#37779.62" Troubleshooting[​](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#troubleshooting "Direct link to Troubleshooting") ----------------------------------------------------------------------------------------------------------------------------------------------- **Dropdown shows concatenated string:** * Verify the regex pattern is correct * Check that the regex delimiters are `/.../` (forward slashes) * Ensure named capture groups are spelled correctly: `(?...)` and `(?...)` **Variable not interpolating in queries:** * Verify you're using `$variable_name` syntax in queries * Check that the variable is defined at the dashboard level * Test the query manually with a hardcoded value **Regex not matching:** * Test your regex pattern with a regex tester (regex101.com) * Verify your separator doesn't appear in the data itself * Check for trailing whitespace in query results **Dropdown is empty:** * Verify the query returns data * Check that QuestDB is accessible from Grafana * Review Grafana logs for error messages Multi-Select Variables This pattern works with multi-select variables too. Enable "Multi-value" in the variable configuration, and users can select multiple options. Use `IN ($variable)` in your queries to handle multiple selected values. Variable Preview Grafana shows a preview of what the dropdown will look like when you configure the regex filter. Use this to verify your pattern is working correctly before applying it. Related Documentation * [Grafana variables documentation](https://grafana.com/docs/grafana/latest/dashboards/variables/) * [Grafana regex filters](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#filter-variables-with-regex) * [concat() function](https://questdb.com/docs/query/functions/text/#concat) * [Grafana QuestDB data source](https://grafana.com/grafana/plugins/questdb-questdb-datasource/) * [Problem: Separate display and query values](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#problem-separate-display-and-query-values) * [Solution: Use regex variable filters](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#solution-use-regex-variable-filters) * [Step 1: Concatenate columns in query](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#step-1-concatenate-columns-in-query) * [Step 2: Apply regex filter in Grafana variable](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#step-2-apply-regex-filter-in-grafana-variable) * [Step 3: Use variable in queries](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#step-3-use-variable-in-queries) * [How it works](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#how-it-works) * [Regex pattern breakdown](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#regex-pattern-breakdown) * [Choosing a separator](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#choosing-a-separator) * [Alternative patterns](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#alternative-patterns) * [Multiple data fields](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#multiple-data-fields) * [Numeric IDs with descriptions](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#numeric-ids-with-descriptions) * [Escaping special characters](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#escaping-special-characters) * [PostgreSQL data source alternative](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#postgresql-data-source-alternative) * [Adapting the pattern](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#adapting-the-pattern) * [Troubleshooting](https://questdb.com/docs/cookbook/integrations/grafana/variable-dropdown/#troubleshooting) --- # Collect OPC-UA data with Telegraf in dense format | QuestDB On this page Configure Telegraf to collect OPC-UA industrial automation data and insert it into QuestDB in a dense format. By default, Telegraf creates one row per metric with sparse columns, but for QuestDB it's more efficient to merge all metrics from the same timestamp into a single dense row. Problem: Sparse data format[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#problem-sparse-data-format "Direct link to Problem: Sparse data format") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- When using Telegraf's OPC-UA input plugin with the default configuration, each metric value generates a separate row. Even when multiple metrics are collected at the same timestamp, they arrive as individual sparse rows: **Sparse format (inefficient):** | timestamp | ServerLoad | ServerRAM | ServerIO | | --- | --- | --- | --- | | 2024-01-15T10:00:00.000000Z | 45.2 | NULL | NULL | | 2024-01-15T10:00:00.000000Z | NULL | 8192.0 | NULL | | 2024-01-15T10:00:00.000000Z | NULL | NULL | 1250.5 | This wastes storage space and makes queries more complex. **Dense format (efficient):** | timestamp | ServerLoad | ServerRAM | ServerIO | | --- | --- | --- | --- | | 2024-01-15T10:00:00.000000Z | 45.2 | 8192.0 | 1250.5 | Solution: Use Telegraf's merge aggregator[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#solution-use-telegrafs-merge-aggregator "Direct link to Solution: Use Telegraf's merge aggregator") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Configure Telegraf to merge metrics with matching timestamps and tags before sending to QuestDB. This requires two key changes: 1. Add a common tag to all metrics 2. Use the `merge` aggregator to combine rows ### Complete configuration[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#complete-configuration "Direct link to Complete configuration") [agent] omit_hostname = true# OPC-UA Input Plugin[[inputs.opcua]] endpoint = "${OPCUA_ENDPOINT}" connect_timeout = "30s" request_timeout = "30s" security_policy = "None" security_mode = "None" auth_method = "Anonymous" name_override = "${METRICS_TABLE_NAME}" [[inputs.opcua.nodes]] name = "ServerLoad" namespace = "2" identifier_type = "s" identifier = "Server/Load" default_tags = { source="opcua_merge" } [[inputs.opcua.nodes]] name = "ServerRAM" namespace = "2" identifier_type = "s" identifier = "Server/RAM" default_tags = { source="opcua_merge" } [[inputs.opcua.nodes]] name = "ServerIO" namespace = "2" identifier_type = "s" identifier = "Server/IO" default_tags = { source="opcua_merge" }# Merge Aggregator[[aggregators.merge]] drop_original = true tags = ["source"]# QuestDB Output via ILP[[outputs.influxdb_v2]] urls = ["${QUESTDB_HTTP_ENDPOINT}"] token = "${QUESTDB_HTTP_TOKEN}" content_encoding = "identity" ### Key configuration elements[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#key-configuration-elements "Direct link to Key configuration elements") **1\. Common tag** default_tags = { source="opcua_merge" } Adds the same tag value (`source="opcua_merge"`) to all metrics. The merge aggregator uses this to identify which metrics should be combined. **2\. Merge aggregator** [[aggregators.merge]] drop_original = true tags = ["source"] * `drop_original = true`: Discards the original sparse rows after merging * `tags = ["source"]`: Merges metrics with matching `source` tag values and the same timestamp **3\. QuestDB output** [[outputs.influxdb_v2]] urls = ["${QUESTDB_HTTP_ENDPOINT}"] content_encoding = "identity" * Uses the InfluxDB Line Protocol (ILP) over HTTP * `content_encoding = "identity"`: Disables gzip compression (QuestDB doesn't require it) How it works[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#how-it-works "Direct link to How it works") ------------------------------------------------------------------------------------------------------------------------------- The data flow is: 1. **OPC-UA server** → Telegraf collects metrics 2. **Telegraf input** → Creates separate rows for each metric with the `source="opcua_merge"` tag 3. **Merge aggregator** → Combines rows with matching timestamp + `source` tag 4. **QuestDB output** → Sends merged dense rows via ILP ### Merging logic[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#merging-logic "Direct link to Merging logic") The merge aggregator combines metrics when: * **Timestamps match**: Metrics collected at the same moment * **Tags match**: All specified tags (in this case, `source`) have the same values If metrics have different timestamps or tag values, they won't be merged. Handling tag conflicts[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#handling-tag-conflicts "Direct link to Handling tag conflicts") ------------------------------------------------------------------------------------------------------------------------------------------------------------- If your OPC-UA nodes have additional tags with **different** values, those tags will prevent merging. Solutions: ### Remove conflicting tags[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#remove-conflicting-tags "Direct link to Remove conflicting tags") Use the `override` processor to remove unwanted tags: [[processors.override]] [processors.override.tags] node_id = "" # Removes the 'node_id' tag namespace = "" # Removes the 'namespace' tag ### Convert tags to fields[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#convert-tags-to-fields "Direct link to Convert tags to fields") Use the `converter` processor to convert tags to fields (fields don't affect merging): [[processors.converter]] [processors.converter.tags] string = ["node_id", "namespace"] This converts the tags to string fields, which won't interfere with the merge aggregator. ### Remove the common tag after merging[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#remove-the-common-tag-after-merging "Direct link to Remove the common tag after merging") If you don't want the `source` tag in your final QuestDB table: # Place this AFTER the merge aggregator[[processors.override]] [processors.override.tags] source = "" # Removes the 'source' tag Environment variables[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#environment-variables "Direct link to Environment variables") ---------------------------------------------------------------------------------------------------------------------------------------------------------- Use environment variables for sensitive configuration: export OPCUA_ENDPOINT="opc.tcp://your-opcua-server:4840"export METRICS_TABLE_NAME="industrial_metrics"export QUESTDB_HTTP_ENDPOINT="http://questdb-host:9000"export QUESTDB_HTTP_TOKEN="your_token_here" Alternatively, use a `.env` file: # .env fileOPCUA_ENDPOINT=opc.tcp://localhost:4840METRICS_TABLE_NAME=opcua_metricsQUESTDB_HTTP_ENDPOINT=http://localhost:9000QUESTDB_HTTP_TOKEN= Then start Telegraf with: telegraf --config telegraf.conf Verification[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#verification "Direct link to Verification") ------------------------------------------------------------------------------------------------------------------------------- Query QuestDB to verify the data format: SELECT * FROM opcua_metricsORDER BY timestamp DESCLIMIT 10; **Expected: Dense rows** with all metrics populated: | timestamp | source | ServerLoad | ServerRAM | ServerIO | | --- | --- | --- | --- | --- | | 2024-01-15T10:05:00.000000Z | opcua\_merge | 47.8 | 8256.0 | 1305.2 | | 2024-01-15T10:04:00.000000Z | opcua\_merge | 45.2 | 8192.0 | 1250.5 | **Problem: Sparse rows** with NULL values: | timestamp | source | ServerLoad | ServerRAM | ServerIO | | --- | --- | --- | --- | --- | | 2024-01-15T10:05:00.000000Z | opcua\_merge | 47.8 | NULL | NULL | | 2024-01-15T10:05:00.000000Z | opcua\_merge | NULL | 8256.0 | NULL | If you see sparse rows, check: * All nodes have the same `default_tags` * The merge aggregator is configured correctly * Timestamps are identical (not just close) Alternative: TCP output[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#alternative-tcp-output "Direct link to Alternative: TCP output") --------------------------------------------------------------------------------------------------------------------------------------------------------------- For higher throughput, use TCP instead of HTTP: [[outputs.socket_writer]] address = "tcp://questdb-host:9009" **Differences:** * **TCP**: Higher throughput, no acknowledgments, potential data loss on connection failure * **HTTP**: Reliable delivery, acknowledgments, slightly lower throughput Choose TCP when: * You need maximum performance * Occasional data loss is acceptable * You're on a reliable local network Choose HTTP when: * Data integrity is critical * You need error feedback * You're sending over the internet Multiple OPC-UA sources[​](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#multiple-opc-ua-sources "Direct link to Multiple OPC-UA sources") ---------------------------------------------------------------------------------------------------------------------------------------------------------------- To collect from multiple OPC-UA servers into separate tables: # Server 1[[inputs.opcua]] endpoint = "opc.tcp://server1:4840" name_override = "server1_metrics" [[inputs.opcua.nodes]] name = "Temperature" namespace = "2" identifier_type = "s" identifier = "Sensor/Temp" default_tags = { source="server1" }# Server 2[[inputs.opcua]] endpoint = "opc.tcp://server2:4840" name_override = "server2_metrics" [[inputs.opcua.nodes]] name = "Pressure" namespace = "2" identifier_type = "s" identifier = "Sensor/Press" default_tags = { source="server2" }# Merge by source tag[[aggregators.merge]] drop_original = true tags = ["source"] This creates two tables (`server1_metrics`, `server2_metrics`) with merged metrics from each server. Performance Tuning For high-frequency OPC-UA data: * Increase Telegraf's `flush_interval` to batch more data * Use `aggregators.merge.period` to specify merge window duration * Monitor QuestDB's ingestion rate and adjust accordingly Timestamp Precision OPC-UA timestamps may have different precision than QuestDB expects. Ensure: * Telegraf agent precision matches your requirements (default: nanoseconds) * OPC-UA server timestamps are synchronized (use NTP) * Clock drift between systems is minimal Related Documentation * [Telegraf OPC-UA plugin](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/opcua) * [Telegraf merge aggregator](https://github.com/influxdata/telegraf/tree/master/plugins/aggregators/merge) * [QuestDB ILP reference](https://questdb.com/docs/ingestion/ilp/overview/) * [InfluxDB Line Protocol](https://questdb.com/docs/ingestion/ilp/overview/) * [Problem: Sparse data format](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#problem-sparse-data-format) * [Solution: Use Telegraf's merge aggregator](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#solution-use-telegrafs-merge-aggregator) * [Complete configuration](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#complete-configuration) * [Key configuration elements](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#key-configuration-elements) * [How it works](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#how-it-works) * [Merging logic](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#merging-logic) * [Handling tag conflicts](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#handling-tag-conflicts) * [Remove conflicting tags](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#remove-conflicting-tags) * [Convert tags to fields](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#convert-tags-to-fields) * [Remove the common tag after merging](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#remove-the-common-tag-after-merging) * [Environment variables](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#environment-variables) * [Verification](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#verification) * [Alternative: TCP output](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#alternative-tcp-output) * [Multiple OPC-UA sources](https://questdb.com/docs/cookbook/integrations/opcua-dense-format/#multiple-opc-ua-sources) --- # Replication tuning | QuestDB On this page Enterprise— Tune replication for lower latency or reduced network costs. [Learn more](https://questdb.com/enterprise/) Three settings control replication latency. The main decision is your transport layer — **object store** (S3, GCS, Azure Blob) is simplest and cheapest at rest, while **NFS** (EFS, Filestore, Azure Files, NetApp) removes per-operation costs and unlocks sub-second latency. Pick a transport, choose a profile below, and restart. The three settings that matter[​](https://questdb.com/docs/high-availability/tuning/#the-three-settings-that-matter "Direct link to The three settings that matter") --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Setting | Node | Default | What it does | | --- | --- | --- | --- | | `replication.primary.throttle.window.duration` | Primary | `10000` (10s) | Maximum time before an incomplete WAL segment is flushed | | `replication.replica.poll.interval` | Replica | `1000` (1s) | How often the replica checks for new data | | `cairo.wal.segment.rollover.size` | Primary | `2097152` (2 MiB) | Max WAL segment size before rollover | A segment is uploaded when **either** the size limit or the throttle window is reached, whichever comes first. Under heavy write load, segments fill and flush well before the throttle window expires. Under light load, the throttle window controls when the partially-filled segment is flushed. Configuration profiles[​](https://questdb.com/docs/high-availability/tuning/#configuration-profiles "Direct link to Configuration profiles") --------------------------------------------------------------------------------------------------------------------------------------------- ### Sub-200ms latency (NFS transport)[​](https://questdb.com/docs/high-availability/tuning/#sub-200ms-latency-nfs-transport "Direct link to Sub-200ms latency (NFS transport)") # Primarycairo.wal.segment.rollover.size=262144replication.primary.throttle.window.duration=50# Replicareplication.replica.poll.interval=50 ### Sub-500ms latency (NFS or object store)[​](https://questdb.com/docs/high-availability/tuning/#sub-500ms-latency-nfs-or-object-store "Direct link to Sub-500ms latency (NFS or object store)") # Primarycairo.wal.segment.rollover.size=524288replication.primary.throttle.window.duration=100# Replicareplication.replica.poll.interval=100 ### Default / balanced[​](https://questdb.com/docs/high-availability/tuning/#default--balanced "Direct link to Default / balanced") No configuration needed. The defaults are: * `replication.primary.throttle.window.duration=10000` (10s) * `replication.replica.poll.interval=1000` (1s) * `cairo.wal.segment.rollover.size=2097152` (2 MiB) ### Network efficiency[​](https://questdb.com/docs/high-availability/tuning/#network-efficiency "Direct link to Network efficiency") # Primarycairo.wal.segment.rollover.size=2097152replication.primary.throttle.window.duration=60000 Choosing a transport: cost vs latency[​](https://questdb.com/docs/high-availability/tuning/#choosing-a-transport-cost-vs-latency "Direct link to Choosing a transport: cost vs latency") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ### Object store (S3, GCS, Azure Blob)[​](https://questdb.com/docs/high-availability/tuning/#object-store-s3-gcs-azure-blob "Direct link to Object store (S3, GCS, Azure Blob)") * **Per-request pricing**: every WAL upload is a write op, every replica poll is a read op * Lower latency settings = more ops = higher cost * Best for: simplest setup, low storage cost, moderate latency tolerance * Storage cost: ~$20/TB/month across major clouds GCP users Replication over GCS has a latency floor of roughly 1 second. If you need sub-second replication on GCP, use an NFS transport such as Filestore or NetApp Volumes instead. ### NFS / managed file storage (EFS, Filestore, Azure Files, NetApp)[​](https://questdb.com/docs/high-availability/tuning/#nfs--managed-file-storage-efs-filestore-azure-files-netapp "Direct link to NFS / managed file storage (EFS, Filestore, Azure Files, NetApp)") * **Fixed monthly cost** regardless of how aggressively you tune * No per-operation charges — poll every 50ms at no extra cost * Best for: low-latency requirements, high-throughput ingestion * Storage cost: ~$60–300/TB/month depending on service tier and provider * NFS is usually priced by provisioned capacity, not usage — you pay for the full volume whether it's 10% or 100% full ### The cost tradeoff[​](https://questdb.com/docs/high-availability/tuning/#the-cost-tradeoff "Direct link to The cost tradeoff") The storage cost gap (object store at ~$20/TB vs NFS at $60–300/TB) looks large, but the replication working set — WAL files in transit — is typically well under 1 TB. At that scale the per-TB premium is modest in absolute terms. The real cost difference is **operations**. With object store, every flush and every poll is a billable request. Each actively-written table generates one write op per throttle window and one read op per poll interval. Across major clouds, write ops typically cost ~$5/million and read ops ~$0.40/million. **Object store ops cost per active table:** | Throttle / poll interval | Ops cost per table per month | | --- | --- | | 50ms / 50ms | ~$280 | | 100ms / 100ms | ~$140 | | 1s / 1s | ~$14 | | 10s / 1s (default) | ~$2 | Multiply by the number of tables being actively written to. With 10 tables at 100ms intervals, that's ~$1,400/month in API charges alone. With NFS, that same configuration costs nothing extra. The rough breakeven: > **ops cost per month** ≈ active tables × $14,000 / interval\_ms > > If that exceeds the NFS premium over object storage (typically $40–180/TB/mo × your working set in TB), **NFS is cheaper**. At default settings with a handful of tables, object store wins easily. Once you push below ~200ms intervals or have many actively-written tables, NFS pays for itself on API savings alone — and you get lower latency as a bonus. note For long-term data retention (cold/archive tier), object storage is always significantly cheaper and should be used regardless of your replication transport choice. ### Summary[​](https://questdb.com/docs/high-availability/tuning/#summary "Direct link to Summary") | | Object store | NFS / file storage | | --- | --- | --- | | Pricing model | Per-request + per-GB stored | Fixed monthly (provisioned) | | Storage cost | ~$20/TB/mo | ~$60–300/TB/mo | | Cost of aggressive tuning | Higher (more ops) | No change | | Setup complexity | Low | Medium (mount on all nodes) | | Best for | Default settings, few tables | Sub-second latency, many tables | How replication works[​](https://questdb.com/docs/high-availability/tuning/#how-replication-works "Direct link to How replication works") ------------------------------------------------------------------------------------------------------------------------------------------ Understanding the data flow helps you tune effectively: 1. **Ingestion** — Data is written to Write-Ahead Log (WAL) segments 2. **Upload** — WAL segments are flushed to the transport (object store or NFS) 3. **Download** — Replicas poll the transport and apply new WAL segments The key insight: **smaller, more frequent uploads = lower latency but more network traffic**. Larger, less frequent uploads = higher latency but lower costs. ![Network traffic with default settings](https://questdb.com/docs/images/guides/replication-tuning/one_row_sec_defaults.webp) Default settings: balanced latency and throughput ![Network traffic with network efficiency settings](https://questdb.com/docs/images/guides/replication-tuning/one_row_sec_small.webp) Tuned settings: optimized for network efficiency Settings reference[​](https://questdb.com/docs/high-availability/tuning/#settings-reference "Direct link to Settings reference") --------------------------------------------------------------------------------------------------------------------------------- ### WAL segment size[​](https://questdb.com/docs/high-availability/tuning/#wal-segment-size "Direct link to WAL segment size") cairo.wal.segment.rollover.size=2097152 Controls the size threshold at which WAL segments are closed and uploaded. Smaller segments upload sooner (lower latency) but create more files. Works in tandem with the throttle window — whichever limit is hit first triggers the upload. | Value | Behavior | | --- | --- | | `262144` (256 KiB) | Lower latency, but more network traffic. | | `2097152` (2 MiB) | Higher latency, but less network traffic. | note Some object stores have minimum file size requirements. AWS S3 Intelligent Tiering requires files over 128 KiB. ### Throttle window[​](https://questdb.com/docs/high-availability/tuning/#throttle-window "Direct link to Throttle window") replication.primary.throttle.window.duration=10000 # 10 seconds (default) Maximum time before uploading an incomplete segment. If a segment hasn't reached the rollover size within this window, it is flushed anyway. Longer windows let segments fill up before upload, reducing redundant uploads (write amplification). | Value | Behavior | | --- | --- | | `50` (50ms) | Ultra-low latency. Best with NFS transport. | | `100` (100ms) | Low latency. Good balance for NFS transport. | | `1000` (1s) | Low latency for object store transport. | | `10000` (10s) | Default. Balanced. | | `60000` (60s) | 1 minute delay OK. Fewer uploads. | | `300000` (5 min) | Cost-sensitive. Batches more data. | This is your **maximum replication latency tolerance**. QuestDB still actively manages replication to prevent backlogs during bursts. ### Replica poll interval[​](https://questdb.com/docs/high-availability/tuning/#replica-poll-interval "Direct link to Replica poll interval") replication.replica.poll.interval=1000 # 1 second (default) How often the replica checks the transport layer for new data. This setting is configured on the **replica** node. | Value | Behavior | | --- | --- | | `50` (50ms) | Ultra-low latency. Pair with aggressive primary settings. | | `100` (100ms) | Low latency. Good for NFS transport. | | `1000` (1s) | Default. Balanced. | note Reducing the poll interval below the throttle window duration has diminishing returns, since the replica cannot consume data faster than the primary produces it. Advanced settings[​](https://questdb.com/docs/high-availability/tuning/#advanced-settings "Direct link to Advanced settings") ------------------------------------------------------------------------------------------------------------------------------ These settings are available for power users but rarely need adjustment: | Setting | Default | Description | | --- | --- | --- | | `replication.primary.sequencer.part.txn.count` | `5000` | Transactions per sequencer part file. Lower values mean smaller parts and faster incremental uploads but more storage requests. **Fixed at table creation** — cannot be changed for existing tables. | | `replication.primary.compression.level` | `1` | Zstd compression level for WAL uploads. Higher values reduce transfer size at the cost of CPU. | | `replication.primary.compression.threads` | `2` | Number of threads used for compressing WAL data before upload. | | `replication.requests.max.concurrent` | `32` | Maximum concurrent replication requests (uploads and downloads). | | `replication.requests.retry.attempts` | `3` | Number of retry attempts for failed replication requests. | | `replication.requests.retry.interval` | `500` | Milliseconds between retry attempts. | Compression (reference)[​](https://questdb.com/docs/high-availability/tuning/#compression-reference "Direct link to Compression (reference)") ---------------------------------------------------------------------------------------------------------------------------------------------- WAL data is compressed before upload (the level and thread count are configurable in [Advanced settings](https://questdb.com/docs/high-availability/tuning/#advanced-settings) above). The typical ratios are useful for estimating storage and network requirements: | Data type | Typical compression ratio | | --- | --- | | WAL segments | ~8x | | Sequencer parts | ~6x | For example, a 2 MiB WAL segment becomes ~256 KiB in the transport layer. Next steps[​](https://questdb.com/docs/high-availability/tuning/#next-steps "Direct link to Next steps") --------------------------------------------------------------------------------------------------------- * [Replication overview](https://questdb.com/docs/high-availability/overview/) - How replication works * [Setup guide](https://questdb.com/docs/high-availability/setup/) - Configure replication * [Configuration reference](https://questdb.com/docs/configuration/overview/) - All server settings * [The three settings that matter](https://questdb.com/docs/high-availability/tuning/#the-three-settings-that-matter) * [Configuration profiles](https://questdb.com/docs/high-availability/tuning/#configuration-profiles) * [Sub-200ms latency (NFS transport)](https://questdb.com/docs/high-availability/tuning/#sub-200ms-latency-nfs-transport) * [Sub-500ms latency (NFS or object store)](https://questdb.com/docs/high-availability/tuning/#sub-500ms-latency-nfs-or-object-store) * [Default / balanced](https://questdb.com/docs/high-availability/tuning/#default--balanced) * [Network efficiency](https://questdb.com/docs/high-availability/tuning/#network-efficiency) * [Choosing a transport: cost vs latency](https://questdb.com/docs/high-availability/tuning/#choosing-a-transport-cost-vs-latency) * [Object store (S3, GCS, Azure Blob)](https://questdb.com/docs/high-availability/tuning/#object-store-s3-gcs-azure-blob) * [NFS / managed file storage (EFS, Filestore, Azure Files, NetApp)](https://questdb.com/docs/high-availability/tuning/#nfs--managed-file-storage-efs-filestore-azure-files-netapp) * [The cost tradeoff](https://questdb.com/docs/high-availability/tuning/#the-cost-tradeoff) * [Summary](https://questdb.com/docs/high-availability/tuning/#summary) * [How replication works](https://questdb.com/docs/high-availability/tuning/#how-replication-works) * [Settings reference](https://questdb.com/docs/high-availability/tuning/#settings-reference) * [WAL segment size](https://questdb.com/docs/high-availability/tuning/#wal-segment-size) * [Throttle window](https://questdb.com/docs/high-availability/tuning/#throttle-window) * [Replica poll interval](https://questdb.com/docs/high-availability/tuning/#replica-poll-interval) * [Advanced settings](https://questdb.com/docs/high-availability/tuning/#advanced-settings) * [Compression (reference)](https://questdb.com/docs/high-availability/tuning/#compression-reference) * [Next steps](https://questdb.com/docs/high-availability/tuning/#next-steps) --- # WAL cleanup | QuestDB On this page Enterprise— Automatic cleanup of replicated WAL data in object storage. [Learn more](https://questdb.com/enterprise/) QuestDB's [replication feature](https://questdb.com/docs/high-availability/setup/) streams write-ahead log (WAL) data from a primary node to object storage, where replica nodes consume it. Without cleanup, this replicated WAL data accumulates indefinitely. The **WAL cleaner** runs on the primary node and automatically deletes data that is no longer needed, keeping storage usage under control. Requires: QuestDB Enterprise with replication enabled. warning The WAL cleaner is enabled by default, **but it will not delete anything until at least 5 completed backups or checkpoints exist.** Without configuring backups or checkpoint history, and executing regular backups, WAL data accumulates indefinitely regardless of this setting. Quick start[​](https://questdb.com/docs/high-availability/wal-cleanup/#quick-start "Direct link to Quick start") ----------------------------------------------------------------------------------------------------------------- The WAL cleaner is _enabled by default_. With either backups or checkpoint history active, no additional configuration is needed: # server.conf (these are defaults — no action needed)replication.primary.cleaner.enabled=truereplication.primary.cleaner.backup.window.count=5 It runs every 10 minutes (`replication.primary.cleaner.interval`). The cleaner requires at least one **trigger source** with sufficient history before it will delete anything. The two supported sources are: * **[Enterprise backups](https://questdb.com/docs/operations/backup/) ** — the cleaner reads backup manifests to determine what can be safely deleted. * **[Checkpoint integration](https://questdb.com/docs/high-availability/wal-cleanup/#checkpoint-integration) ** — the cleaner reads `CHECKPOINT RELEASE` records synced to the replication object store. Both sources are enabled by default when replication is active. If you only use one backup method, the cleaner simply ignores the source that has no history. Verifying cleanup is running[​](https://questdb.com/docs/high-availability/wal-cleanup/#verifying-cleanup-is-running "Direct link to Verifying cleanup is running") -------------------------------------------------------------------------------------------------------------------------------------------------------------------- Search the QuestDB logs for `wal::uploader::cleaner`. Each cleanup cycle logs a line like: prune requested [c=1, trigger=backup, instance=door-echo-yoyo, backup_ts=1771597937483926 (2026-02-20T14:32:17.483926Z), tables=42] Key fields: | Field | Meaning | | --- | --- | | `trigger` | Which source determined the boundary (`backup` or `checkpoint`). | | `instance` | The backup instance name whose entry set the boundary. | | `backup_ts` | Timestamp of the boundary entry. Data up to and including this entry is deleted. | | `tables` | Number of tables processed in this cycle. | If the cleaner does not have enough history to act, it logs: insufficient backup history, skipping WAL cleanup [backup_window_count=5, history={door-echo-yoyo:3, park-sugar-system:2}] This means no instance has reached the N-entry threshold yet. Check that backups or checkpoints are running successfully. You can find a node's backup instance name by running: SELECT backup_instance_name; Backup integration[​](https://questdb.com/docs/high-availability/wal-cleanup/#backup-integration "Direct link to Backup integration") -------------------------------------------------------------------------------------------------------------------------------------- The cleaner automatically reads your backup manifests to determine what can be safely deleted. The backup feature must be enabled and configured on the primary, even if you only run backups from a replica. # server.conf (primary)replication.role=primaryreplication.object.store=...backup.enabled=truebackup.object.store=s3::bucket=my-backup-bucket;... # same on all cluster nodes The cleaner waits until at least N complete backups exist before it starts deleting anything. N defaults to your [`backup.cleanup.keep.latest.n`](https://questdb.com/docs/operations/backup/#backup-retention) setting (itself default 5) and can be overridden with `replication.primary.cleaner.backup.window.count`. For example, with the default of 5 the cleaner deletes data up to and including the 5th-newest complete backup, which becomes the oldest backup from which a node can be restored into the replication cluster. warning All nodes in a replication cluster should use the **same `backup.object.store`** connection string. The cleaner on the primary reads backup manifests from every node to compute the cleanup boundary. If nodes back up to different object stores, the cleaner cannot see all manifests and will not trigger correctly. Checkpoint integration[​](https://questdb.com/docs/high-availability/wal-cleanup/#checkpoint-integration "Direct link to Checkpoint integration") -------------------------------------------------------------------------------------------------------------------------------------------------- If you take filesystem snapshots, AWS EBS volume snapshots, or use custom backup scripts that issue `CHECKPOINT` / `CHECKPOINT RELEASE`, checkpoint history tracking is all you need. Both `checkpoint.history.enabled` and `replication.primary.cleaner.checkpoint.source` default to `true` when replication is enabled, so no extra configuration is required: # server.conf — checkpoint history works out of the boxreplication.role=primary # or replicareplication.object.store=... Checkpoint history does not need to be configured on the primary. It only needs to be enabled on the node(s) where you actually run checkpoints. For example, you might run a primary and two replicas, and back up both replicas but not the primary. As long as each node that issues checkpoints is part of the same replication cluster and has checkpoint history enabled, the cleaner on the primary will see their checkpoint records. Each time `CHECKPOINT RELEASE` runs on any node with checkpoint history enabled, QuestDB records the per-table transaction state to the shared replication object store. The cleaner uses these records the same way it uses backup manifests. As with backups, the cleaner waits until at least N complete checkpoints exist before deleting anything. N is controlled by `replication.primary.cleaner.backup.window.count` (default 5). Checkpoint records are synced to the replication object store at `checkpoint_history/{instance_name}/history.msgpack`. If the sync fails transiently, QuestDB retries in the background (controlled by `checkpoint.history.long.retry.interval`). note `CHECKPOINT` itself is available in both OSS and Enterprise, but checkpoint history tracking — the mechanism that syncs checkpoint records to the replication object store for WAL cleanup — requires QuestDB Enterprise with replication enabled. How the cleanup boundary works[​](https://questdb.com/docs/high-availability/wal-cleanup/#how-the-cleanup-boundary-works "Direct link to How the cleanup boundary works") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The cleanup boundary determines how far back you can restore. WAL data up to and including the boundary is deleted; data after the boundary is retained. Any [point-in-time recovery](https://questdb.com/docs/high-availability/setup/#point-in-time-recovery) target must be **after** this boundary. Backup manifests and checkpoint history records are stored per backup instance name. The cleaner computes the boundary as follows: 1. For each backup instance name, collect the most recent N entries (backups or checkpoints, regardless of source). N is `replication.primary.cleaner.backup.window.count` (default 5). 2. Skip any instance that has fewer than N entries. 3. Compare the Nth-newest entry from each eligible instance. The entry with the **earliest timestamp** is the cleanup boundary — WAL data up to and including that entry's transactions is deleted. ### Example[​](https://questdb.com/docs/high-availability/wal-cleanup/#example "Direct link to Example") Consider three nodes with N=5: **door-echo-yoyo** has 7 entries (Jan 1–7), **park-sugar-system** has 6 entries (Jan 6–11), and **apple-parrot-baby** has 3 entries (Jan 10–12). Skipped and not considered: * **apple-parrot-baby** has only 3 entries, fewer than N=5, so it is skipped. The following entries are considered by the algorithm: * **door-echo-yoyo** has 7 entries. Its 5th newest entry is **Jan 3**. * **park-sugar-system** has 6 entries. Its 5th newest entry is **Jan 7**. Comparing Jan 3 (door-echo-yoyo) vs Jan 7 (park-sugar-system): the earliest is **Jan 3**, so the cleanup boundary falls there. All replication WAL data up to and including Jan 3 is deleted. After cleanup, restoring from a backup older than Jan 3 (such as door-echo-yoyo's Jan 1 or Jan 2 backups) is only possible as a standalone instance, not as part of the replication cluster. Any point-in-time recovery target must be **after** Jan 3. Greyed-out entries in the diagram are not considered by the algorithm. Using both backups and checkpoints[​](https://questdb.com/docs/high-availability/wal-cleanup/#using-both-backups-and-checkpoints "Direct link to Using both backups and checkpoints") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- By default, both trigger sources are active: backup.enabled=truecheckpoint.history.enabled=truereplication.primary.cleaner.checkpoint.source=true Backups and checkpoints are merged into a single list per backup instance name before the boundary is computed. The effect depends on your cluster topology: * **Same instance produces both backups and checkpoints** — more entries accumulate per instance, crossing the N threshold sooner and pushing the Nth-newest entry forward in time. This more eagerly reduces the time window of data kept in the replication object store. * **Different instances produce backups vs checkpoints** — each instance has fewer entries individually, and the cross-instance comparison in step 3 picks the oldest boundary. This increases the time window of data retained. To restrict the cleaner to a single source, set `replication.primary.cleaner.checkpoint.source=false` to ignore checkpoint history, or disable `backup.enabled` to ignore backup manifests. Troubleshooting[​](https://questdb.com/docs/high-availability/wal-cleanup/#troubleshooting "Direct link to Troubleshooting") ----------------------------------------------------------------------------------------------------------------------------- ### Storage growing despite cleaner being enabled[​](https://questdb.com/docs/high-availability/wal-cleanup/#storage-growing-despite-cleaner-being-enabled "Direct link to Storage growing despite cleaner being enabled") 1. **Check that trigger sources have enough history.** The cleaner needs at least N entries (default 5) from at least one backup instance name. If you recently set up replication and have fewer than 5 backups or checkpoints, the cleaner has not started yet. Run `SELECT * FROM backups();` or check your checkpoint schedule. 2. **Check the logs.** Search for `wal::uploader::cleaner`. If you see `insufficient backup history, skipping WAL cleanup`, the cleaner is not finding enough history to act. 3. **Check for abandoned backup instance names.** A decommissioned node whose history remains in the object store drags the cleanup boundary backward indefinitely. See [Abandoned backup instance names](https://questdb.com/docs/high-availability/wal-cleanup/#abandoned-backup-instance-names) . 4. **Verify both sources are producing entries.** When both backups and checkpoints are enabled, entries from both are merged per instance. If one source stopped producing entries (e.g., scheduled backups are failing), the total entry count per instance may drop below N, preventing cleanup. Check that all configured sources are running on schedule. ### Abandoned backup instance names[​](https://questdb.com/docs/high-availability/wal-cleanup/#abandoned-backup-instance-names "Direct link to Abandoned backup instance names") A decommissioned node whose history is still in the object store holds back the cleanup boundary for the entire cluster. In the [example above](https://questdb.com/docs/high-availability/wal-cleanup/#example) , door-echo-yoyo has no entries after Jan 7. If it is decommissioned, its old history pins the boundary to Jan 3. Without it, the boundary would advance to Jan 7 (park-sugar-system's 5th newest entry). You can identify this from the cleaner log: prune requested [c=1, trigger=backup, instance=door-echo-yoyo, backup_ts=1771597937483926 (2026-02-20T14:32:17.483926Z), tables=42] If `instance` shows a name you don't recognize or one belonging to a decommissioned node, and `backup_ts` is unexpectedly old, that instance is the problem. To unblock cleanup, delete the abandoned instance's directory from the object store: In the **backup** object store: backup/{backup_instance_name}/ In the **replication** object store: checkpoint_history/{backup_instance_name}/ You can discover which backup instance names exist by listing these prefixes in your object store. ### Cleanup boundary not advancing[​](https://questdb.com/docs/high-availability/wal-cleanup/#cleanup-boundary-not-advancing "Direct link to Cleanup boundary not advancing") If the `backup_ts` in the cleaner log stays the same across cycles: * An instance may have stopped producing new backups or checkpoints. Check that all active nodes are backing up on schedule. * The N-entry threshold may be too high. Lowering `replication.primary.cleaner.backup.window.count` reduces how many entries are required before cleanup starts, but also reduces your recovery window. ### Disabling the cleaner[​](https://questdb.com/docs/high-availability/wal-cleanup/#disabling-the-cleaner "Direct link to Disabling the cleaner") replication.primary.cleaner.enabled=false With the cleaner disabled, WAL data accumulates indefinitely. Useful for debugging, not recommended for production. Configuration reference[​](https://questdb.com/docs/high-availability/wal-cleanup/#configuration-reference "Direct link to Configuration reference") ----------------------------------------------------------------------------------------------------------------------------------------------------- All settings go in `server.conf`. Defaults are tuned for typical production use — most deployments only need the quick-start settings above. ### Core settings[​](https://questdb.com/docs/high-availability/wal-cleanup/#core-settings "Direct link to Core settings") | Property | Default | Description | | --- | --- | --- | | `replication.primary.cleaner.enabled` | `true` | Master switch for the cleaner. | | `replication.primary.cleaner.interval` | `10m` | Time between cleanup cycles. Range: 1s – 24h. | | `replication.primary.cleaner.checkpoint.source` | `true` | Use checkpoint history as a cleanup trigger source. | | `replication.primary.cleaner.backup.window.count` | `backup.cleanup.keep.latest.n` (if backups enabled) or 5 | Minimum complete backups/checkpoints per instance before cleanup starts. | ### Performance tuning[​](https://questdb.com/docs/high-availability/wal-cleanup/#performance-tuning "Direct link to Performance tuning") | Property | Default | Description | | --- | --- | --- | | `replication.primary.cleaner.delete.concurrency` | 4 – 12 (auto) | Concurrent deletion tasks. Derived from `replication.requests.max.concurrent`. Range: 4 – 32. | | `replication.primary.cleaner.max.requests.per.second` | Service-dependent | Rate limit for object store requests. Set to 0 for unlimited. Range: 0 – 10000. | | `replication.primary.cleaner.progress.write.interval` | `5s` | How often progress is persisted during a cycle. Lower = less re-work after crash, more writes. Range: 100ms – 60s. | Default rate limits per object store: | Service | Default | Basis | | --- | --- | --- | | GCS | 500 req/s | 50% of ~1,000 write ops/s per bucket | | Azure Blob | 10,000 req/s | 50% of ~20,000 requests/s per account | | S3 / R2 / DO Spaces | 1,750 req/s | 50% of ~3,500 DELETE/s per prefix | | Filesystem | 100 req/s | Conservative default for potential NFS | ### Safety settings[​](https://questdb.com/docs/high-availability/wal-cleanup/#safety-settings "Direct link to Safety settings") | Property | Default | Description | | --- | --- | --- | | `replication.primary.cleaner.dropped.table.cooloff` | `1h` | Wait time after `DROP TABLE` before removing the table's data from object storage. Guards against clock skew. | | `replication.primary.cleaner.retry.attempts` | `20` | Retries for transient object store failures. Range: 0 – 100. | | `replication.primary.cleaner.retry.interval` | `2s` | Delay between retries. Range: 0 – 5m. | ### Checkpoint history settings[​](https://questdb.com/docs/high-availability/wal-cleanup/#checkpoint-history-settings "Direct link to Checkpoint history settings") Only relevant when `checkpoint.history.enabled=true`. | Property | Default | Description | | --- | --- | --- | | `checkpoint.history.enabled` | `true` (when replication is enabled) | Enable the checkpoint history tracker. Requires replication. | | `checkpoint.history.keep.count` | `100` | Maximum checkpoint records retained per instance. | | `checkpoint.history.long.retry.interval` | `1m` | Retry interval for syncing to object store after burst retries fail. | The remaining checkpoint history settings (`requests.retry.attempts`, `requests.retry.interval`, `requests.max.concurrent`, timeouts, throughput) default to the corresponding `replication.requests.*` values and rarely need to be overridden. * [Quick start](https://questdb.com/docs/high-availability/wal-cleanup/#quick-start) * [Verifying cleanup is running](https://questdb.com/docs/high-availability/wal-cleanup/#verifying-cleanup-is-running) * [Backup integration](https://questdb.com/docs/high-availability/wal-cleanup/#backup-integration) * [Checkpoint integration](https://questdb.com/docs/high-availability/wal-cleanup/#checkpoint-integration) * [How the cleanup boundary works](https://questdb.com/docs/high-availability/wal-cleanup/#how-the-cleanup-boundary-works) * [Example](https://questdb.com/docs/high-availability/wal-cleanup/#example) * [Using both backups and checkpoints](https://questdb.com/docs/high-availability/wal-cleanup/#using-both-backups-and-checkpoints) * [Troubleshooting](https://questdb.com/docs/high-availability/wal-cleanup/#troubleshooting) * [Storage growing despite cleaner being enabled](https://questdb.com/docs/high-availability/wal-cleanup/#storage-growing-despite-cleaner-being-enabled) * [Abandoned backup instance names](https://questdb.com/docs/high-availability/wal-cleanup/#abandoned-backup-instance-names) * [Cleanup boundary not advancing](https://questdb.com/docs/high-availability/wal-cleanup/#cleanup-boundary-not-advancing) * [Disabling the cleaner](https://questdb.com/docs/high-availability/wal-cleanup/#disabling-the-cleaner) * [Configuration reference](https://questdb.com/docs/high-availability/wal-cleanup/#configuration-reference) * [Core settings](https://questdb.com/docs/high-availability/wal-cleanup/#core-settings) * [Performance tuning](https://questdb.com/docs/high-availability/wal-cleanup/#performance-tuning) * [Safety settings](https://questdb.com/docs/high-availability/wal-cleanup/#safety-settings) * [Checkpoint history settings](https://questdb.com/docs/high-availability/wal-cleanup/#checkpoint-history-settings) --- # Replication setup guide | QuestDB On this page Enterprise— This guide covers setting up primary-replica replication. [Learn more](https://questdb.com/enterprise/) This guide walks you through setting up QuestDB Enterprise replication. **Prerequisites:** Read the [Replication overview](https://questdb.com/docs/high-availability/overview/) to understand how replication works. Setup steps[​](https://questdb.com/docs/high-availability/setup/#setup-steps "Direct link to Setup steps") ----------------------------------------------------------------------------------------------------------- 1. Configure object storage (AWS S3, Azure Blob, GCS, or NFS) 2. Configure the **primary** node 3. Take a snapshot of the primary 4. Configure **replica** node(s) 1\. Configure object storage[​](https://questdb.com/docs/high-availability/setup/#1-configure-object-storage "Direct link to 1. Configure object storage") ----------------------------------------------------------------------------------------------------------------------------------------------------------- Choose your object storage provider and build the connection string for `replication.object.store` in `server.conf`. ### AWS S3[​](https://questdb.com/docs/high-availability/setup/#aws-s3 "Direct link to AWS S3") Create an S3 bucket following [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) . **Recommendations:** * Select a region close to your primary node * Disable blob versioning **Connection string:** replication.object.store=s3::bucket=${BUCKET_NAME};root=${DB_INSTANCE_NAME};region=${AWS_REGION};access_key_id=${AWS_ACCESS_KEY};secret_access_key=${AWS_SECRET_ACCESS_KEY}; `DB_INSTANCE_NAME` can be any unique alphanumeric string (dashes allowed). Use the same value across all nodes in your replication cluster. Using IAM roles If your instance has an IAM role attached (EC2 instance profile, EKS pod identity, or ECS task role), you can omit the credentials: replication.object.store=s3::bucket=${BUCKET_NAME};root=${DB_INSTANCE_NAME};region=${AWS_REGION}; QuestDB will automatically use the instance's IAM role for authentication. ### Azure Blob Storage[​](https://questdb.com/docs/high-availability/setup/#azure-blob-storage "Direct link to Azure Blob Storage") Create a Storage Account following [Azure documentation](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal) , then create a Blob Container. **Recommendations:** * Select a region close to your primary node * Disable blob versioning **Connection string:** replication.object.store=azblob::endpoint=https://${STORE_ACCOUNT}.blob.core.windows.net;container=${BLOB_CONTAINER};root=${DB_INSTANCE_NAME};account_name=${STORE_ACCOUNT};account_key=${STORE_KEY}; Using Managed Identity If your instance has a Managed Identity assigned (Azure VM, AKS pod identity, or Container Apps), you can omit the `account_key`: replication.object.store=azblob::endpoint=https://${STORE_ACCOUNT}.blob.core.windows.net;container=${BLOB_CONTAINER};root=${DB_INSTANCE_NAME};account_name=${STORE_ACCOUNT}; QuestDB will automatically use the Managed Identity for authentication. Ensure the identity has the **Storage Blob Data Contributor** role on the container. ### Google Cloud Storage[​](https://questdb.com/docs/high-availability/setup/#google-cloud-storage "Direct link to Google Cloud Storage") Create a GCS bucket, then create a service account with `Storage Admin` (or equivalent) permissions. Download the JSON key and encode it as Base64: cat .json | base64 **Connection string:** replication.object.store=gcs::bucket=${BUCKET_NAME};root=/;credential=${BASE64_ENCODED_KEY}; Alternatively, use `credential_path` to reference the key file directly. Using Workload Identity If your instance uses Workload Identity (GKE) or runs on a GCE VM with a service account attached, you can omit the credentials entirely: replication.object.store=gcs::bucket=${BUCKET_NAME};root=/; QuestDB will automatically use Application Default Credentials for authentication. ### NFS[​](https://questdb.com/docs/high-availability/setup/#nfs "Direct link to NFS") Mount the shared filesystem on all nodes. Ensure the QuestDB user has read/write permissions. **Important:** Both the WAL folder and scratch folder must be on the same NFS mount to prevent write corruption. **Connection string:** replication.object.store=fs::root=/mnt/nfs_replication/final;atomic_write_dir=/mnt/nfs_replication/scratch; 2\. Configure the primary node[​](https://questdb.com/docs/high-availability/setup/#2-configure-the-primary-node "Direct link to 2. Configure the primary node") ----------------------------------------------------------------------------------------------------------------------------------------------------------------- Add to `server.conf`: | Setting | Value | | --- | --- | | `replication.role` | `primary` | | `replication.object.store` | Your connection string from step 1 | | `cairo.snapshot.instance.id` | Unique UUID for this node | Restart QuestDB. 3\. Take a snapshot[​](https://questdb.com/docs/high-availability/setup/#3-take-a-snapshot "Direct link to 3. Take a snapshot") -------------------------------------------------------------------------------------------------------------------------------- Replicas are initialized from a snapshot of the primary's data. This involves creating a backup of the primary and preparing it for restoration on replica nodes. See [Backup and restore](https://questdb.com/docs/operations/backup/) for the full procedure. tip Set up regular snapshots (daily or weekly). 4\. Configure replica node(s)[​](https://questdb.com/docs/high-availability/setup/#4-configure-replica-nodes "Direct link to 4. Configure replica node(s)") ------------------------------------------------------------------------------------------------------------------------------------------------------------ Create a new QuestDB instance. Add to `server.conf`: | Setting | Value | | --- | --- | | `replication.role` | `replica` | | `replication.object.store` | Same connection string as primary | | `cairo.snapshot.instance.id` | Unique UUID for this replica | warning Do not copy `server.conf` from the primary. Two nodes configured as primary with the same object store will break replication. Restore the `db` directory from the primary's snapshot, then start the replica. It will download and apply WAL files to catch up with the primary. Configuration reference[​](https://questdb.com/docs/high-availability/setup/#configuration-reference "Direct link to Configuration reference") ----------------------------------------------------------------------------------------------------------------------------------------------- All replication settings go in `server.conf`. After changes, restart QuestDB. tip Use environment variables for sensitive settings: export QDB_REPLICATION_OBJECT_STORE="azblob::..." | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | replication.role | none | No | Defaults to `none` for stand-alone instances. To enable replication set to one of: `primary`, `replica`. | | replication.object.store | | No | A configuration string that allows connecting to an object store. The format is **scheme::key1=value;key2=value2;…**. The various keys and values are detailed in a later section. Ignored if replication is disabled. No default given variability. | | cairo.wal.segment.rollover.size | 2097152 | No | The size of the WAL segment before it is rolled over. Default is `2MiB`. However, defaults to `0` unless `replication.role=primary` is set. | | cairo.writer.command.queue.capacity | 32 | No | Maximum writer ALTER TABLE and replication command capacity. Shared between all the tables. | | replication.primary.throttle.window.duration | 10000 | No | The millisecond duration of the sliding window used to process replication batches. Default is `10000` ms. | | replication.requests.max.concurrent | 0 | No | A limit to the number of concurrent object store requests. The default is `0` for unlimited. | | replication.requests.retry.attempts | 3 | No | Maximum number of times to retry a failed object store request before logging an error and reattempting later after a delay. Default is `3`. | | replication.requests.retry.interval | 200 | No | How long to wait before retrying a failed operation. Default is `200` ms. | | replication.primary.compression.threads | calculated | No | Max number of threads used to perform file compression operations before uploading to the object store. The default value is calculated as half the number of CPU cores. | | replication.primary.compression.level | 1 | No | Zstd compression level. Defaults to `1`. Valid values are from 1 to 22. | | replication.replica.poll.interval | 1000 | No | Millisecond polling rate of a replica instance to check for the availability of new changes. | | replication.primary.sequencer.part.txn.count | 5000 | No | Sets the txn chunking size for each compressed batch. Smaller is better for constrained networks (but more costly). | | replication.primary.checksum=service-dependent | service-dependent | No | Where a checksum should be calculated for each uploaded artifact. Required for some object stores. Other options: never, always | | replication.primary.upload.truncated | true | No | Skip trailing, empty column data inside a WAL column file. | | replication.requests.buffer.size | 32768 | No | Buffer size used for object-storage downloads. | | replication.summary.interval | 1m | No | Frequency for printing replication progress summary in the logs. | | replication.metrics.per.table | true | No | Enable per-table replication metrics on the prometheus metrics endpoint. | | replication.metrics.dropped.table.poll.count | 10 | No | How many scrapes of prometheus metrics endpoint before dropped tables will no longer appear. | | replication.requests.max.batch.size.fast | 64 | No | Number of parallel requests allowed during the 'fast' process (non-resource constrained). | | replication.requests.max.batch.size.slow | 2 | No | Number of parallel requests allowed during the 'slow' process (error/resource constrained path). | | replication.requests.base.timeout | 10s | No | Replication upload/download request timeout. | | replication.requests.min.throughput | 262144 | No | Expected minimum network speed for replication transfers. Used to expand the timeout and account for network delays. | | native.async.io.threads | cpuCount | No | The number of async (network) io threads used for replication (and in the future cold storage). The default should be appropriate for most use cases. | | native.max.blocking.threads | cpuCount \* 4 | No | Maximum number of threads for parallel blocking disk IO read/write operations for replication (and other). These threads are ephemeral: They are spawned per need and shut down after a short duration if no longer in use. These are not cpu-bound threads, hence the relative large number. The default should be appropriate for most use cases. | | replication.primary.cleaner.enabled | true | No | Master switch for the WAL cleaner. | | replication.primary.cleaner.interval | 10m | No | Time between cleanup cycles. Range: 1s – 24h. | | replication.primary.cleaner.checkpoint.source | true | No | Use checkpoint history as a cleanup trigger source. | | replication.primary.cleaner.backup.window.count | backup.cleanup.keep.latest.n or 5 | No | Minimum complete backups/checkpoints per instance before cleanup starts. Defaults to `backup.cleanup.keep.latest.n` if backups are enabled, otherwise `5`. | | replication.primary.cleaner.delete.concurrency | 4 – 12 (auto) | No | Concurrent deletion tasks. Derived from `replication.requests.max.concurrent`. Range: 4 – 32. | | replication.primary.cleaner.max.requests.per.second | service-dependent | No | Rate limit for object store delete requests. Set to `0` for unlimited. Range: 0 – 10000. | | replication.primary.cleaner.progress.write.interval | 5s | No | How often progress is persisted during a cleanup cycle. Lower values mean less re-work after a crash but more writes. Range: 100ms – 60s. | | replication.primary.cleaner.dropped.table.cooloff | 1h | No | Wait time after `DROP TABLE` before removing the table's data from object storage. Guards against clock skew. | | replication.primary.cleaner.retry.attempts | 20 | No | Retries for transient object store failures during cleanup. Range: 0 – 100. | | replication.primary.cleaner.retry.interval | 2s | No | Delay between cleanup retries. Range: 0 – 5m. | | checkpoint.history.enabled | true (when replication is enabled) | No | Enable the checkpoint history tracker. Requires replication. | | checkpoint.history.keep.count | 100 | No | Maximum checkpoint records retained per instance. | | checkpoint.history.long.retry.interval | 1m | No | Retry interval for syncing checkpoint history to the object store after burst retries fail. | For tuning options, see the [Tuning guide](https://questdb.com/docs/high-availability/tuning/) . WAL data cleanup[​](https://questdb.com/docs/high-availability/setup/#wal-data-cleanup "Direct link to WAL data cleanup") -------------------------------------------------------------------------------------------------------------------------- Replicated WAL data accumulates in object storage over time. The **WAL cleaner** runs on the primary node and automatically removes data that is no longer needed, based on your backup and checkpoint history. The cleaner is enabled by default and requires no configuration when backups or checkpoint history are active. By default, it retains replication data for the most recent 5 backups or checkpoints and deletes everything older. See the [WAL Cleanup guide](https://questdb.com/docs/high-availability/wal-cleanup/) for configuration options, tuning, and troubleshooting. Disaster recovery[​](https://questdb.com/docs/high-availability/setup/#disaster-recovery "Direct link to Disaster recovery") ----------------------------------------------------------------------------------------------------------------------------- ### Failure scenarios[​](https://questdb.com/docs/high-availability/setup/#failure-scenarios "Direct link to Failure scenarios") | Node | Recoverable | Unrecoverable | | --- | --- | --- | | Primary | Restart | Promote replica, create new replica | | Replica | Restart | Destroy and recreate | ### Network partitions[​](https://questdb.com/docs/high-availability/setup/#network-partitions "Direct link to Network partitions") Temporary partitions cause replicas to lag, then catch up when connectivity restores. This is normal operation. Permanent partitions require [emergency primary migration](https://questdb.com/docs/high-availability/setup/#emergency-primary-migration) . ### Instance crashes[​](https://questdb.com/docs/high-availability/setup/#instance-crashes "Direct link to Instance crashes") If a crash corrupts transactions, tables may suspend on restart. You can skip the corrupted transaction and reload missing data, or follow the emergency migration flow. ### Disk failures[​](https://questdb.com/docs/high-availability/setup/#disk-failures "Direct link to Disk failures") Symptoms: high latency, unmounted disk, suspended tables. Follow the emergency migration flow to move to new storage. Migration procedures[​](https://questdb.com/docs/high-availability/setup/#migration-procedures "Direct link to Migration procedures") -------------------------------------------------------------------------------------------------------------------------------------- ### Planned primary migration[​](https://questdb.com/docs/high-availability/setup/#planned-primary-migration "Direct link to Planned primary migration") Use when the current primary is healthy but you want to switch to a new one. 1. Stop the primary 2. Restart with `replication.role=primary-catchup-uploads` 3. Wait for uploads to complete (exits with code 0) 4. Follow emergency migration steps below ### Emergency primary migration[​](https://questdb.com/docs/high-availability/setup/#emergency-primary-migration "Direct link to Emergency primary migration") Use when the primary has failed. 1. Stop the failed primary (ensure it cannot restart) 2. Stop the replica 3. Set `replication.role=primary` on the replica 4. Create an empty `_migrate_primary` file in the installation directory 5. Start the replica (now the new primary) 6. Create a new replica to replace the promoted one warning Data committed to the primary but not yet replicated will be lost. Use planned migration if the primary is still functional. ### Point-in-time recovery[​](https://questdb.com/docs/high-availability/setup/#point-in-time-recovery "Direct link to Point-in-time recovery") Restore the database to a specific historical timestamp. 1. Locate a snapshot from before your target timestamp 2. Create a new instance from the snapshot (do not start it) 3. Create a `_recover_point_in_time` file containing: replication.object.store=replication.recovery.timestamp=YYYY-MM-DDThh:mm:ss.mmmZ 4. If using a snapshot, create a `_restore` file to trigger recovery 5. Optionally configure `server.conf` to replicate to a **new** object store 6. Start the instance Next steps[​](https://questdb.com/docs/high-availability/setup/#next-steps "Direct link to Next steps") -------------------------------------------------------------------------------------------------------- * [Tuning guide](https://questdb.com/docs/high-availability/tuning/) - Optimize replication performance * [Setup steps](https://questdb.com/docs/high-availability/setup/#setup-steps) * [1\. Configure object storage](https://questdb.com/docs/high-availability/setup/#1-configure-object-storage) * [AWS S3](https://questdb.com/docs/high-availability/setup/#aws-s3) * [Azure Blob Storage](https://questdb.com/docs/high-availability/setup/#azure-blob-storage) * [Google Cloud Storage](https://questdb.com/docs/high-availability/setup/#google-cloud-storage) * [NFS](https://questdb.com/docs/high-availability/setup/#nfs) * [2\. Configure the primary node](https://questdb.com/docs/high-availability/setup/#2-configure-the-primary-node) * [3\. Take a snapshot](https://questdb.com/docs/high-availability/setup/#3-take-a-snapshot) * [4\. Configure replica node(s)](https://questdb.com/docs/high-availability/setup/#4-configure-replica-nodes) * [Configuration reference](https://questdb.com/docs/high-availability/setup/#configuration-reference) * [WAL data cleanup](https://questdb.com/docs/high-availability/setup/#wal-data-cleanup) * [Disaster recovery](https://questdb.com/docs/high-availability/setup/#disaster-recovery) * [Failure scenarios](https://questdb.com/docs/high-availability/setup/#failure-scenarios) * [Network partitions](https://questdb.com/docs/high-availability/setup/#network-partitions) * [Instance crashes](https://questdb.com/docs/high-availability/setup/#instance-crashes) * [Disk failures](https://questdb.com/docs/high-availability/setup/#disk-failures) * [Migration procedures](https://questdb.com/docs/high-availability/setup/#migration-procedures) * [Planned primary migration](https://questdb.com/docs/high-availability/setup/#planned-primary-migration) * [Emergency primary migration](https://questdb.com/docs/high-availability/setup/#emergency-primary-migration) * [Point-in-time recovery](https://questdb.com/docs/high-availability/setup/#point-in-time-recovery) * [Next steps](https://questdb.com/docs/high-availability/setup/#next-steps) --- # Copy data between QuestDB instances | QuestDB On this page Copy a subset of data from one QuestDB instance to another for testing or development purposes. Problem[​](https://questdb.com/docs/cookbook/operations/copy-data-between-instances/#problem "Direct link to Problem") ----------------------------------------------------------------------------------------------------------------------- You want to copy data between QuestDB instances. This method allows you to copy any arbitrary query result, but if you want a full database copy please check the [backup and restore documentation](https://questdb.com/docs/operations/backup/) . Solution: Table2Ilp utility[​](https://questdb.com/docs/cookbook/operations/copy-data-between-instances/#solution-table2ilp-utility "Direct link to Solution: Table2Ilp utility") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB ships with a `utils` folder that includes a tool to read from one instance (using the PostgreSQL protocol) and write into another (using ILP). You would need to [compile the jar](https://github.com/questdb/questdb/tree/master/utils) , and then use it like this: java -cp utils.jar io.questdb.cliutil.Table2Ilp \ -d trades \ -dilp "https::addr=localhost:9000;username=admin;password=quest;" \ -s "trades WHERE start_time in '2022-06'" \ -sc "jdbc:postgresql://localhost:8812/qdb?user=account&password=secret&ssl=false" \ -sym "ticker,exchange" \ -sts start_time This reads from the source instance using PostgreSQL wire protocol and writes to the destination using ILP. Alternative: Export endpoint[​](https://questdb.com/docs/cookbook/operations/copy-data-between-instances/#alternative-export-endpoint "Direct link to Alternative: Export endpoint") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- You can also use [the export endpoint](https://questdb.com/docs/query/rest-api/#exp---export-data) to export data to CSV or other formats. Related Documentation * [ILP ingestion](https://questdb.com/docs/ingestion/overview/) * [PostgreSQL wire protocol](https://questdb.com/docs/query/pgwire/overview/) * [REST API export](https://questdb.com/docs/query/rest-api/#exp---export-data) * [Problem](https://questdb.com/docs/cookbook/operations/copy-data-between-instances/#problem) * [Solution: Table2Ilp utility](https://questdb.com/docs/cookbook/operations/copy-data-between-instances/#solution-table2ilp-utility) * [Alternative: Export endpoint](https://questdb.com/docs/cookbook/operations/copy-data-between-instances/#alternative-export-endpoint) --- # ZFS Compression | QuestDB On this page QuestDB can use [ZFS](https://openzfs.org/wiki/Main_Page) for system-level compression, reducing disk usage without application changes. The following example shows how to set up ZFS on Ubuntu: Ubuntu - Install ZFS sudo apt updatesudo apt install zfsutils-linux To enable compression, create a ZPool with compression enabled: Ubuntu - Enable compression zpool create -m legacy -o feature@lz4_compress=enabled -o autoexpand=on -O compression=lz4 -t volume1 questdb-pool sdf The exact commands depend on which version of ZFS you are running. Use the [ZFS docs](https://openzfs.github.io/openzfs-docs/man/master/8/zpool-create.8.html) to customize your ZFS to meet your requirements. Once created, ZFS provides system-level compression. Compression choices, LZ4 and zstd[​](https://questdb.com/docs/deployment/compression-zfs/#compression-choices-lz4-and-zstd "Direct link to Compression choices, LZ4 and zstd") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ZFS offers a number of compression choices when constructing the volume. [LZ4](https://github.com/lz4/lz4) offers a good balance of compression ratio versus increased CPU usage, and slowed performance. For general usage, we recommend using LZ4. [zstd](https://github.com/facebook/zstd) is another strong option. This will provide higher compression ratios, but take longer to decompress. We recommend this when storage size is an absolute priority, or for embedded-style deployments (i.e. Raspberry Pi, home IoT setups). As always, it is best to benchmark your choice to ensure that the performance matches your use case. note We regularly test ZFS with LZ4 compression. If you encounter issues with other compression algorithms, please let us know. * [Compression choices, LZ4 and zstd](https://questdb.com/docs/deployment/compression-zfs/#compression-choices-lz4-and-zstd) --- # AI Coding Agents | QuestDB On this page AI coding agents like [Claude Code](https://claude.ai/code) and [OpenAI Codex](https://openai.com/index/openai-codex/) can help you build applications that use QuestDB. These agents work with QuestDB out of the box by reading the online documentation. For deeper integration, agent skills embed rich context directly into the agent so it can work faster and more accurately. The [QuestDB agent skill](https://questdb.com/docs/getting-started/ai-coding-agents/#questdb-agent-skill) covers SQL, ingestion, and Grafana dashboards, while the [TSBS Benchmark skill](https://questdb.com/docs/getting-started/ai-coding-agents/#tsbs-benchmark-skill) automates end-to-end performance benchmarking. Getting started[​](https://questdb.com/docs/getting-started/ai-coding-agents/#getting-started "Direct link to Getting started") -------------------------------------------------------------------------------------------------------------------------------- No setup required. Use the public QuestDB demo with Claude Code: You: "Use QuestDB's REST API at https://demo.questdb.io/ to list all tables"Claude Code: [Queries /exec endpoint and lists available tables including trades]You: "Query the trades table and show me the last 10 trades. Data is time-ordered natively, no ORDER BY needed"Claude Code: [Sends SQL via HTTP: SELECT * FROM trades LIMIT -10]You: "What's the total volume traded per symbol, sampled by 1 hour? Use SAMPLE BY"Claude Code: [Writes and executes SAMPLE BY 1h query grouped by symbol]You: "Plot the price of BTC-USDT over the last 30 days"Claude Code: [Queries data and generates a chart using matplotlib] ### Connect to your own QuestDB[​](https://questdb.com/docs/getting-started/ai-coding-agents/#connect-to-your-own-questdb "Direct link to Connect to your own QuestDB") 1. Install Claude Code: [https://claude.ai/code](https://claude.ai/code) 2. Start QuestDB (default port 9000) 3. Ask Claude Code to connect and explore You: "Connect to my QuestDB at localhost:9000 and show me what tables I have"Claude Code: I'll query the QuestDB REST API to list your tables.[Executes curl command and shows results] QuestDB agent skill[​](https://questdb.com/docs/getting-started/ai-coding-agents/#questdb-agent-skill "Direct link to QuestDB agent skill") -------------------------------------------------------------------------------------------------------------------------------------------- The [QuestDB agent skill](https://github.com/questdb/questdb-agent-skill) is an experimental skill for Claude Code and Codex. It embeds QuestDB-specific knowledge directly into the agent's context - SQL syntax, common mistakes, ingestion patterns, Grafana templates, and financial indicator recipes - so the agent can build complete data pipelines without searching the docs for every step. For topics not covered by the skill, the agent falls back to the online documentation automatically. ### Installation[​](https://questdb.com/docs/getting-started/ai-coding-agents/#installation "Direct link to Installation") Copy the `questdb/` folder from the [repository](https://github.com/questdb/questdb-agent-skill) into your skills directory: **Claude Code:** * `~/.claude/skills/questdb/` - available in all projects * `/.claude/skills/questdb/` - available in a specific project **Codex:** * `~/.codex/skills/questdb/` - available in all projects * `/.codex/skills/questdb/` - available in a specific project The folder must contain `SKILL.md` and the `references/` directory. ### What's included[​](https://questdb.com/docs/getting-started/ai-coding-agents/#whats-included "Direct link to What's included") * **SQL reference** - QuestDB-specific syntax including `SAMPLE BY`, `LATEST ON`, `ASOF JOIN`, window functions, and materialized views * **Common mistakes** - 50+ patterns to avoid when coming from PostgreSQL (e.g., `time_bucket()` does not exist, use `SAMPLE BY` instead) * **Ingestion patterns** - Python templates for ILP ingestion, including array support for order book data * **Grafana integration** - Dashboard deployment via API, datasource configuration, and ready-to-use panel queries * **Financial indicators** - 20+ indicator recipes ready for Grafana panels * **Enterprise authentication** - REST and ILP token configuration, ACL setup ### Example prompt[​](https://questdb.com/docs/getting-started/ai-coding-agents/#example-prompt "Direct link to Example prompt") With QuestDB and Grafana running locally: Build a real-time crypto market data pipeline using cryptofeed (OKX exchange)ingesting trades and L2 order book data into QuestDB.Symbols: BTC-USDT, ETH-USDT, SOL-USDT.Then create a Grafana dashboard with OHLC candlesticks, VWAP,Bollinger Bands, and RSI panels, with a symbol dropdown. The agent will create the database schema, write the ingestion script, wait for data, and deploy a Grafana dashboard - all in a single pass. ### Built-in indicators[​](https://questdb.com/docs/getting-started/ai-coding-agents/#built-in-indicators "Direct link to Built-in indicators") The following indicators are embedded in the skill and can be generated without online lookups: Aggressor imbalance, ATR, Bid-ask spread, Bollinger Bands, Bollinger BandWidth, Compound interest, Cumulative product, Donchian Channels, Keltner Channels, Liquidity comparison, MACD, Maximum drawdown, OBV, OHLC bars, Rate of Change, Realized volatility, Rolling std dev, RSI, Stochastic Oscillator, TICK & TRIN, Volume profile, Volume spikes, VWAP. TSBS Benchmark skill[​](https://questdb.com/docs/getting-started/ai-coding-agents/#tsbs-benchmark-skill "Direct link to TSBS Benchmark skill") ----------------------------------------------------------------------------------------------------------------------------------------------- The [TSBS Benchmark skill](https://github.com/questdb/tsbs-benchmark-agent-skill) is a skill for Claude Code and Codex that runs end-to-end [Time Series Benchmark Suite](https://github.com/timescale/tsbs) (TSBS) benchmarks against QuestDB. It installs prerequisites, spins up QuestDB in Docker, builds TSBS, generates data, loads it, runs all query benchmarks, and cleans up - all from a single prompt. ### Installation[​](https://questdb.com/docs/getting-started/ai-coding-agents/#installation-1 "Direct link to Installation") Copy `claude/SKILL.md` from the [repository](https://github.com/questdb/tsbs-benchmark-agent-skill) into your skills directory: **Claude Code:** * `~/.claude/skills/tsbs-benchmark/SKILL.md` - available in all projects * `/.claude/skills/tsbs-benchmark/SKILL.md` - available in a specific project **Codex:** * `~/.codex/skills/tsbs-benchmark/SKILL.md` - available in all projects * `/.codex/skills/tsbs-benchmark/SKILL.md` - available in a specific project ### What it handles[​](https://questdb.com/docs/getting-started/ai-coding-agents/#what-it-handles "Direct link to What it handles") The skill orchestrates eight sequential steps: 1. **Prerequisites** - validates and installs Docker, Go 1.22.5, and build tools (make, gcc, gzip) 2. **QuestDB deployment** - launches the latest QuestDB container with ports 9000, 9009, 8812, and 9003 3. **TSBS compilation** - clones and builds four QuestDB-specific binaries from the TSBS repository 4. **Data generation** - creates ~12 GB of uncompressed benchmark data (34.5M rows, 345.6M metrics) 5. **Data loading** - ingests via ILP with worker threads matching CPU cores (capped at 32) 6. **Query generation** - produces 1,000 queries for each of the 16 query types 7. **Benchmark execution** - runs all queries in single-worker mode so QuestDB's internal parallelization is measured accurately 8. **Cleanup** - removes the Docker container and temporary files ### Benchmark parameters[​](https://questdb.com/docs/getting-started/ai-coding-agents/#benchmark-parameters "Direct link to Benchmark parameters") | Parameter | Value | | --- | --- | | Use case | `cpu-only` | | Scale | 4,000 hosts | | Time window | 1 day (2016-01-01 to 2016-01-02) | | Log interval | 10 seconds | | Rows generated | 34.5M | | Metrics generated | 345.6M | | Query types | 16 (cpu-max variants, single/double-groupby, high-cpu, lastpoint, groupby-orderby-limit) | | Queries per type | 1,000 | ### Example prompt[​](https://questdb.com/docs/getting-started/ai-coding-agents/#example-prompt-1 "Direct link to Example prompt") Run the full TSBS benchmark against QuestDB with the default cpu-only dataset. The agent will handle everything from installing prerequisites through reporting the final query-by-query results. Tips[​](https://questdb.com/docs/getting-started/ai-coding-agents/#tips "Direct link to Tips") ----------------------------------------------------------------------------------------------- * **Provide context** - Tell the agent about your use case, data volume, and requirements * **Ask follow-up questions** - Agents remember context within a session * **Request explanations** - Ask "why?" to understand recommendations * **Iterate on code** - Ask the agent to modify or improve generated code Next steps[​](https://questdb.com/docs/getting-started/ai-coding-agents/#next-steps "Direct link to Next steps") ----------------------------------------------------------------------------------------------------------------- * [REST API reference](https://questdb.com/docs/query/rest-api/) - API documentation * [SQL overview](https://questdb.com/docs/query/overview/) - QuestDB SQL syntax * [Client libraries](https://questdb.com/docs/ingestion/overview/) - Official client libraries * [Sample datasets](https://github.com/questdb/sample-datasets) - Example data to try * [Getting started](https://questdb.com/docs/getting-started/ai-coding-agents/#getting-started) * [Connect to your own QuestDB](https://questdb.com/docs/getting-started/ai-coding-agents/#connect-to-your-own-questdb) * [QuestDB agent skill](https://questdb.com/docs/getting-started/ai-coding-agents/#questdb-agent-skill) * [Installation](https://questdb.com/docs/getting-started/ai-coding-agents/#installation) * [What's included](https://questdb.com/docs/getting-started/ai-coding-agents/#whats-included) * [Example prompt](https://questdb.com/docs/getting-started/ai-coding-agents/#example-prompt) * [Built-in indicators](https://questdb.com/docs/getting-started/ai-coding-agents/#built-in-indicators) * [TSBS Benchmark skill](https://questdb.com/docs/getting-started/ai-coding-agents/#tsbs-benchmark-skill) * [Installation](https://questdb.com/docs/getting-started/ai-coding-agents/#installation-1) * [What it handles](https://questdb.com/docs/getting-started/ai-coding-agents/#what-it-handles) * [Benchmark parameters](https://questdb.com/docs/getting-started/ai-coding-agents/#benchmark-parameters) * [Example prompt](https://questdb.com/docs/getting-started/ai-coding-agents/#example-prompt-1) * [Tips](https://questdb.com/docs/getting-started/ai-coding-agents/#tips) * [Next steps](https://questdb.com/docs/getting-started/ai-coding-agents/#next-steps) --- # Replication overview | QuestDB On this page Enterprise— Replication provides high availability and disaster recovery for your QuestDB cluster. [Learn more](https://questdb.com/enterprise/) QuestDB Enterprise provides **primary-replica replication** for high availability and disaster recovery. Your data is automatically synced to replica instances via an object store, with no direct network connections required between nodes. Why use replication?[​](https://questdb.com/docs/high-availability/overview/#why-use-replication "Direct link to Why use replication?") ---------------------------------------------------------------------------------------------------------------------------------------- * **High availability** - Replicas can take over if the primary fails * **Read scaling** - Distribute query load across multiple replicas * **Disaster recovery** - Restore from any point in time using stored WAL files * **Geographic distribution** - Place replicas closer to users in different regions * **Zero performance impact** - Replicas don't affect primary performance How it works[​](https://questdb.com/docs/high-availability/overview/#how-it-works "Direct link to How it works") ----------------------------------------------------------------------------------------------------------------- The **primary** instance writes data to a [Write Ahead Log (WAL)](https://questdb.com/docs/concepts/write-ahead-log/) and uploads these files to an object store (AWS S3, Azure Blob Storage, GCS, or NFS). **Replica** instances download and apply these files to stay in sync. This decoupled architecture means: * Add or remove replicas without touching the primary * Replicas can be in different regions or availability zones * Object store provides durability and point-in-time recovery Availability strategies[​](https://questdb.com/docs/high-availability/overview/#availability-strategies "Direct link to Availability strategies") -------------------------------------------------------------------------------------------------------------------------------------------------- **Hot availability** - Run replicas continuously alongside the primary for instant failover. Faster recovery, higher cost. **Cold availability** - Reconstruct a new primary from the latest snapshot and WAL files when needed. Slower recovery, lower cost. Supported object stores[​](https://questdb.com/docs/high-availability/overview/#supported-object-stores "Direct link to Supported object stores") -------------------------------------------------------------------------------------------------------------------------------------------------- | Store | Status | | --- | --- | | AWS S3 | Supported | | Azure Blob Storage | Supported | | Google Cloud Storage | Supported | | NFS filesystem | Supported | | HDFS | Planned | Need something else? [Contact us](https://questdb.com/enterprise/contact) . Requirements[​](https://questdb.com/docs/high-availability/overview/#requirements "Direct link to Requirements") ----------------------------------------------------------------------------------------------------------------- Replication works with **WAL-enabled tables** - tables that have a [designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) and are [partitioned](https://questdb.com/docs/concepts/partitions/) . This covers most time-series use cases. Tables without timestamps (typically used for reference/lookup data) are not replicated automatically and should be populated separately on each instance. Bring Your Own Cloud (BYOC)[​](https://questdb.com/docs/high-availability/overview/#bring-your-own-cloud-byoc "Direct link to Bring Your Own Cloud (BYOC)") ------------------------------------------------------------------------------------------------------------------------------------------------------------ QuestDB Enterprise can be self-managed or operated by QuestDB's team under the [BYOC model](https://questdb.com/byoc/) . With BYOC, QuestDB handles operations of all primary and replica instances on your infrastructure. Managed infrastructure uses standard cloud provider tools (CloudFormation for AWS, Lighthouse for Azure) and is fully owned and auditable by you. Next steps[​](https://questdb.com/docs/high-availability/overview/#next-steps "Direct link to Next steps") ----------------------------------------------------------------------------------------------------------- Ready to set up replication? Continue to the [Setup Guide](https://questdb.com/docs/high-availability/setup/) . * [Why use replication?](https://questdb.com/docs/high-availability/overview/#why-use-replication) * [How it works](https://questdb.com/docs/high-availability/overview/#how-it-works) * [Availability strategies](https://questdb.com/docs/high-availability/overview/#availability-strategies) * [Supported object stores](https://questdb.com/docs/high-availability/overview/#supported-object-stores) * [Requirements](https://questdb.com/docs/high-availability/overview/#requirements) * [Bring Your Own Cloud (BYOC)](https://questdb.com/docs/high-availability/overview/#bring-your-own-cloud-byoc) * [Next steps](https://questdb.com/docs/high-availability/overview/#next-steps) --- # Handle missing columns in C++ client | QuestDB On this page Send rows with missing or optional columns to QuestDB using the C++ client. Problem[​](https://questdb.com/docs/cookbook/programmatic/cpp/missing-columns/#problem "Direct link to Problem") ----------------------------------------------------------------------------------------------------------------- In Python, you can handle missing columns easily with dictionaries: {"price1": 10.0, "price2": 10.1} And if price2 is not available: {"price1": 10.0, "price2": None} Which is equivalent to: {"price1": 10.0} You can pass the dict as the columns argument to `sender.rows` and it transparently sends the rows, with or without missing columns, to the server. In C++, the buffer API requires explicit method calls: buffer .table("trades") .symbol("symbol", "ETH-USD") .symbol("side", "sell") .column("price", 2615.54) .column("amount", 0.00044) .at(questdb::ingress::timestamp_nanos::now());sender.flush(buffer); How do you handle "ragged" rows with missing columns in C++? Solution[​](https://questdb.com/docs/cookbook/programmatic/cpp/missing-columns/#solution "Direct link to Solution") -------------------------------------------------------------------------------------------------------------------- You need to call `at` at the end of the buffer so the data gets queued to be sent, but you can call `symbol` and `column` as many times as needed for each row, and you can do this conditionally. The example below builds a vector with three rows, one of them with an empty column, then it iterates over the vector and checks if the optional `price` column is null. If it is, it skips invoking `column` for the buffer on that column. #include #include #include #include #include #include int main(){ try { auto sender = questdb::ingress::line_sender::from_conf( "http::addr=localhost:9000;username=admin;password=quest;retry_timeout=20000;"); auto now = std::chrono::system_clock::now(); auto duration = now.time_since_epoch(); auto nanos = std::chrono::duration_cast(duration).count(); struct Row { std::string symbol; std::string side; std::optional price; double amount; }; std::vector rows = { {"ETH-USD", "sell", 2615.54, 0.00044}, {"BTC-USD", "sell", 39269.98, 0.001}, {"SOL-USD", "sell", std::nullopt, 5.5} // Missing price }; questdb::ingress::line_sender_buffer buffer; for (const auto& row : rows) { buffer.table("trades") .symbol("symbol", row.symbol) .symbol("side", row.side); if (row.price.has_value()) { buffer.column("price", row.price.value()); } buffer.column("amount", row.amount) .at(questdb::ingress::timestamp_nanos(nanos)); } sender.flush(buffer); sender.close(); std::cout << "Data successfully sent!" << std::endl; return 0; } catch (const questdb::ingress::line_sender_error& err) { std::cerr << "Error running example: " << err.what() << std::endl; return 1; }} Related Documentation * [QuestDB C++ client documentation](https://github.com/questdb/c-questdb-client) * [ILP reference](https://questdb.com/docs/ingestion/ilp/overview/) * [Problem](https://questdb.com/docs/cookbook/programmatic/cpp/missing-columns/#problem) * [Solution](https://questdb.com/docs/cookbook/programmatic/cpp/missing-columns/#solution) --- # Deploying QuestDB on AWS | QuestDB On this page Quick reference[​](https://questdb.com/docs/deployment/aws/#quick-reference "Direct link to Quick reference") -------------------------------------------------------------------------------------------------------------- | Component | Recommended | Notes | | --- | --- | --- | | Instance | `m7i.xlarge` or `r7i.2xlarge` | 4-8 vCPUs, 16-64 GiB RAM | | Storage | `gp3`, 200+ GiB | 16000 IOPS / 1000 MBps | | File system | `zfs` with `lz4` | Or `ext4` if compression not needed | | Ports | 9000, 8812, 9009, 9003 | Restrict to known IPs only | * * * Infrastructure[​](https://questdb.com/docs/deployment/aws/#infrastructure "Direct link to Infrastructure") ----------------------------------------------------------------------------------------------------------- Plan your infrastructure before launching. This section covers instance types, storage, and networking requirements. ### Instance sizing[​](https://questdb.com/docs/deployment/aws/#instance-sizing "Direct link to Instance sizing") | Workload | Instance | vCPUs | RAM | Use case | | --- | --- | --- | --- | --- | | Development | `m7i.large` | 2 | 8 GiB | Testing, small datasets | | Production (starter) | `m7i.xlarge` | 4 | 16 GiB | Light ingestion, moderate queries | | Production (standard) | `r7i.2xlarge` | 8 | 64 GiB | High ingestion, complex queries | | Production (heavy) | `r7i.4xlarge` | 16 | 128 GiB | Heavy workloads, large datasets | **Choosing an instance family:** * **`m7i` / `m7a`** - Balanced compute and memory. Good starting point. * **`r7i` / `r7a`** - Memory-optimized. Better for large datasets or complex queries. * **`m8i` / `r8i`** - Latest generation. Best performance if available in your region. Intel (`i`) and AMD (`a`) variants perform similarly. Choose based on availability and pricing. **ARM instances (Graviton):** Graviton instances (`r7g`, `r8g`) cost less and perform well for ingestion. However, queries using JIT compilation or SIMD vectorization run slower on ARM. Choose Graviton when your workload is primarily ingestion or cost is a priority. **Storage-optimized instances:** Instances with local NVMe (`i7i`, `i8i`) provide fastest disk I/O but lose data on termination. Only use with QuestDB Enterprise, which replicates to S3. ### Storage[​](https://questdb.com/docs/deployment/aws/#storage "Direct link to Storage") **EBS configuration:** | Workload | Volume | Size | IOPS | Throughput | | --- | --- | --- | --- | --- | | Development | `gp3` | 50 GiB | 3000 | 125 MBps | | Production | `gp3` | 200+ GiB | 16000 | 1000 MBps | | High I/O | `gp3` | 500+ GiB | 16000+ | 1000+ MBps | Use `gp3` volumes. They offer better price-performance than `gp2` or `io1`. Separate your OS disk (30 GiB) from your data disk. note EBS throughput is limited by instance type. Smaller instances cannot sustain high IOPS or throughput regardless of volume provisioning. Check your instance's EBS bandwidth limits in the [AWS documentation](https://docs.aws.amazon.com/ec2/latest/instancetypes/gp.html) before provisioning storage. **File system:** Use `zfs` with `lz4` compression to reduce storage costs. If you don't need compression, `ext4` or `xfs` offer slightly better performance. **Unsupported storage:** * **EFS** - Not supported. Network latency is too high for database workloads. * **S3** - Not supported as primary storage. Use for replication (Enterprise only). ### Networking[​](https://questdb.com/docs/deployment/aws/#networking "Direct link to Networking") **Security group rules:** | Port | Protocol | Source | Purpose | | --- | --- | --- | --- | | 22 | TCP | Your IP | SSH access | | 9000 | TCP | Your IP / VPC | Web Console & REST API | | 8812 | TCP | Your IP / VPC | PostgreSQL wire protocol | | 9009 | TCP | Application servers | InfluxDB line protocol | | 9003 | TCP | Monitoring servers | Health check & Prometheus | warning Never expose ports 9000, 8812, or 9009 to `0.0.0.0/0`. Restrict access to known IP ranges or use a bastion host. **VPC recommendations:** * Deploy QuestDB in a private subnet * Use a NAT gateway for outbound access (package updates, etc.) * Use VPC endpoints for S3 if using Enterprise replication * Consider placement groups for low-latency application access * * * Deployment[​](https://questdb.com/docs/deployment/aws/#deployment "Direct link to Deployment") ----------------------------------------------------------------------------------------------- Choose your deployment method: * **[AWS Marketplace](https://questdb.com/docs/deployment/aws/#aws-marketplace) ** - Pre-configured AMI, fastest setup * **[Manual EC2](https://questdb.com/docs/deployment/aws/#manual-ec2) ** - Full control, use your own AMI ### AWS Marketplace[​](https://questdb.com/docs/deployment/aws/#aws-marketplace "Direct link to AWS Marketplace") The QuestDB AMI comes pre-configured and ready to run. **Steps:** 1. Go to the [QuestDB Marketplace listing](https://aws.amazon.com/marketplace/search/results?searchTerms=questdb) 2. Click **Continue to Subscribe** and accept terms 3. Click **Continue to Configure**, select your region 4. Click **Continue to Launch** 5. Select instance type, VPC, subnet, and security group 6. Click **Launch** **After launch:** Connect to the Web Console at `http://:9000` Default credentials: * **Web Console**: `admin` / `quest` * **PostgreSQL**: `admin` / random (check `/var/lib/questdb/conf/server.conf`) warning Change default credentials immediately. See [Security](https://questdb.com/docs/deployment/aws/#security) below. **Configuration file location:** /var/lib/questdb/conf/server.conf ### Manual EC2[​](https://questdb.com/docs/deployment/aws/#manual-ec2 "Direct link to Manual EC2") Deploy QuestDB on any EC2 instance you configure yourself. **Steps:** 1. Launch an EC2 instance with your preferred AMI (Ubuntu 22.04+ recommended) 2. Attach a `gp3` EBS volume for data 3. Configure the security group per the [Networking](https://questdb.com/docs/deployment/aws/#networking) section 4. SSH into the instance 5. Install QuestDB via [Docker](https://questdb.com/docs/deployment/docker/) or [systemd](https://questdb.com/docs/deployment/systemd/) You can also download the binary directly: curl -L https://questdb.com/download -o questdb.tar.gztar xzf questdb.tar.gz./questdb.sh start * * * Security[​](https://questdb.com/docs/deployment/aws/#security "Direct link to Security") ----------------------------------------------------------------------------------------- ### Change default credentials[​](https://questdb.com/docs/deployment/aws/#change-default-credentials "Direct link to Change default credentials") Update credentials immediately after deployment. **Web Console and REST API** - edit `server.conf`: http.user=your_usernamehttp.password=your_secure_password **PostgreSQL** - edit `server.conf`: pg.user=your_usernamepg.password=your_secure_password **InfluxDB line protocol** - edit `conf/auth.json`. See [ILP authentication](https://questdb.com/docs/ingestion/ilp/overview/#authentication) . Restart after changes: sudo systemctl restart questdb ### Disable unused interfaces[​](https://questdb.com/docs/deployment/aws/#disable-unused-interfaces "Direct link to Disable unused interfaces") Reduce attack surface by disabling protocols you don't use: server.conf pg.enabled=false # Disable PostgreSQLline.tcp.enabled=false # Disable ILPhttp.enabled=false # Disable Web Console & REST APIhttp.security.readonly=true # Or make HTTP read-only * * * Operations[​](https://questdb.com/docs/deployment/aws/#operations "Direct link to Operations") ----------------------------------------------------------------------------------------------- ### Upgrading[​](https://questdb.com/docs/deployment/aws/#upgrading "Direct link to Upgrading") **Marketplace AMI:** 1. Stop QuestDB: sudo systemctl stop questdb 2. Back up data: sudo cp -r /var/lib/questdb /var/lib/questdb.backup 3. Download new version: wget https://github.com/questdb/questdb/releases/download/9.3.3/questdb-9.3.3-no-jre-bin.tar.gztar xzf questdb-9.3.3-no-jre-bin.tar.gzsudo cp questdb-9.3.3-no-jre-bin/questdb.jar /usr/local/bin/questdb.jar 4. Restart: sudo systemctl start questdb **Manual deployments:** Follow upgrade steps for [Docker](https://questdb.com/docs/deployment/docker/) or [systemd](https://questdb.com/docs/deployment/systemd/) . ### Monitoring[​](https://questdb.com/docs/deployment/aws/#monitoring "Direct link to Monitoring") **Health check:** curl http://localhost:9003/status **Prometheus metrics:** curl http://localhost:9003/metrics **CloudWatch integration:** Use the CloudWatch agent to collect: * System metrics (CPU, memory, disk I/O) * QuestDB logs from `/var/lib/questdb/log/` * Custom metrics scraped from the Prometheus endpoint * * * Enterprise on AWS[​](https://questdb.com/docs/deployment/aws/#enterprise-on-aws "Direct link to Enterprise on AWS") -------------------------------------------------------------------------------------------------------------------- QuestDB Enterprise adds production features for AWS: * **S3 replication** - Continuous backup for durability * **Cold storage** - Move old partitions to S3, query on-demand * **High availability** - Automatic failover across instances See [Enterprise Quick Start](https://questdb.com/docs/getting-started/enterprise-quick-start/) . * [Quick reference](https://questdb.com/docs/deployment/aws/#quick-reference) * [Infrastructure](https://questdb.com/docs/deployment/aws/#infrastructure) * [Instance sizing](https://questdb.com/docs/deployment/aws/#instance-sizing) * [Storage](https://questdb.com/docs/deployment/aws/#storage) * [Networking](https://questdb.com/docs/deployment/aws/#networking) * [Deployment](https://questdb.com/docs/deployment/aws/#deployment) * [AWS Marketplace](https://questdb.com/docs/deployment/aws/#aws-marketplace) * [Manual EC2](https://questdb.com/docs/deployment/aws/#manual-ec2) * [Security](https://questdb.com/docs/deployment/aws/#security) * [Change default credentials](https://questdb.com/docs/deployment/aws/#change-default-credentials) * [Disable unused interfaces](https://questdb.com/docs/deployment/aws/#disable-unused-interfaces) * [Operations](https://questdb.com/docs/deployment/aws/#operations) * [Upgrading](https://questdb.com/docs/deployment/aws/#upgrading) * [Monitoring](https://questdb.com/docs/deployment/aws/#monitoring) * [Enterprise on AWS](https://questdb.com/docs/deployment/aws/#enterprise-on-aws) --- # Deploying QuestDB on Azure | QuestDB On this page Quick reference[​](https://questdb.com/docs/deployment/azure/#quick-reference "Direct link to Quick reference") ---------------------------------------------------------------------------------------------------------------- | Component | Recommended | Notes | | --- | --- | --- | | Instance | `D4s_v5` or `E8s_v5` | 4-8 vCPUs, 16-64 GiB RAM | | Storage | Premium SSD v2, 200+ GiB | 16000 IOPS / 1000 MBps | | File system | `zfs` with `lz4` | Or `ext4` if compression not needed | | Ports | 9000, 8812, 9009, 9003 | Restrict to known IPs only | * * * Infrastructure[​](https://questdb.com/docs/deployment/azure/#infrastructure "Direct link to Infrastructure") ------------------------------------------------------------------------------------------------------------- Plan your infrastructure before launching. This section covers instance types, storage, and networking requirements. ### Instance sizing[​](https://questdb.com/docs/deployment/azure/#instance-sizing "Direct link to Instance sizing") | Workload | Instance | vCPUs | RAM | Use case | | --- | --- | --- | --- | --- | | Development | `D2s_v5` | 2 | 8 GiB | Testing, small datasets | | Production (starter) | `D4s_v5` | 4 | 16 GiB | Light ingestion, moderate queries | | Production (standard) | `E8s_v5` | 8 | 64 GiB | High ingestion, complex queries | | Production (heavy) | `E16s_v5` | 16 | 128 GiB | Heavy workloads, large datasets | **Understanding Azure instance names:** | Letter | Meaning | Recommendation | | --- | --- | --- | | `D` | General purpose | Good starting point | | `E` | Memory optimized | Better for large datasets | | `s` | Premium storage capable | **Required** for QuestDB | | `a` | AMD EPYC processor | Similar performance, often cheaper | | `p` | ARM architecture | **Avoid** - limited optimization support | Always choose instances with `s` in the name for Premium SSD support. **ARM instances:** Azure ARM instances (Cobalt, Ampere) are not recommended. QuestDB's JIT compilation and SIMD optimizations are limited on ARM. Use `x86_64` instances. ### Storage[​](https://questdb.com/docs/deployment/azure/#storage "Direct link to Storage") **Premium SSD v2 (recommended):** | Workload | Size | IOPS | Throughput | | --- | --- | --- | --- | | Development | 50 GiB | 3000 | 125 MBps | | Production | 200+ GiB | 16000 | 1000 MBps | | High I/O | 500+ GiB | 16000+ | 1000+ MBps | Premium SSD v2 lets you provision IOPS and throughput independently of size. Separate your OS disk (30 GiB) from your data disk. note Premium SSD v2 throughput is limited by VM size. Check your instance's maximum disk throughput in the [Azure documentation](https://learn.microsoft.com/en-us/azure/virtual-machines/sizes) before provisioning. **Premium SSD (alternative):** If Premium SSD v2 is unavailable, use Premium SSD with these minimum sizes: | Tier | Size | IOPS | Throughput | Use case | | --- | --- | --- | --- | --- | | P20 | 512 GiB | 2300 | 150 MBps | Development | | P30 | 1 TiB | 5000 | 200 MBps | Light production | | P40 | 2 TiB | 7500 | 250 MBps | Production | Premium SSD ties performance to disk size - you may need to over-provision capacity to get required IOPS. **Redundancy considerations:** * Premium SSD v2 only supports locally-redundant storage (LRS) * Premium SSD supports zone-redundant storage (ZRS) * For LRS disks, take regular ZRS snapshots or use QuestDB Enterprise replication **File system:** Use `zfs` with `lz4` compression to reduce storage costs. If you don't need compression, `ext4` or `xfs` offer slightly better performance. **Unsupported storage:** * **Azure NetApp Files** - Not supported as primary storage (NFS latency too high) * **blobfuse2** - Not supported for database workloads * **Blob Storage** - Supported for Enterprise replication only, not primary storage ### Networking[​](https://questdb.com/docs/deployment/azure/#networking "Direct link to Networking") **Network Security Group (NSG) rules:** | Port | Protocol | Source | Purpose | | --- | --- | --- | --- | | 22 | TCP | Your IP | SSH access | | 9000 | TCP | Your IP / VNet | Web Console & REST API | | 8812 | TCP | Your IP / VNet | PostgreSQL wire protocol | | 9009 | TCP | Application servers | InfluxDB line protocol | | 9003 | TCP | Monitoring servers | Health check & Prometheus | warning Never set source to `*` or `Any` for ports 9000, 8812, or 9009. Restrict access to known IP ranges or use Azure Bastion for secure access. **VNet recommendations:** * Deploy QuestDB in a private subnet * Use Azure Bastion or a jump box for SSH access * Use Private Endpoints for Blob Storage (Enterprise replication) * Consider proximity placement groups for low-latency application access * * * Deployment[​](https://questdb.com/docs/deployment/azure/#deployment "Direct link to Deployment") ------------------------------------------------------------------------------------------------- Deploy QuestDB on an Azure Virtual Machine. ### Prerequisites[​](https://questdb.com/docs/deployment/azure/#prerequisites "Direct link to Prerequisites") * [Microsoft Azure account](https://azure.microsoft.com/) with billing enabled * SSH key pair for secure access ### Create the VM[​](https://questdb.com/docs/deployment/azure/#create-the-vm "Direct link to Create the VM") 1. In the Azure Portal, navigate to **Virtual Machines** 2. Click **Create** → **Azure virtual machine** 3. Configure basics: * Select or create a **Resource group** * Enter a **Virtual machine name** * Select your **Region** and **Availability zone** * Choose **Ubuntu 24.04 LTS** for the image ![The Create Instance dialog on Microsoft Azure](https://questdb.com/docs/images/guides/microsoft-azure-ubuntu/create-vm.webp) Azure VM creation dialog 4. Select your instance size (see [Instance sizing](https://questdb.com/docs/deployment/azure/#instance-sizing) ) 5. Configure SSH authentication: * Select **SSH public key** * Create a new key pair or use existing ![SSH key configuration on Microsoft Azure](https://questdb.com/docs/images/guides/microsoft-azure-ubuntu/ssh-setup.webp) SSH key setup 6. Click **Review + create**, then **Create** 7. Download the private key when prompted ![Deployment complete on Microsoft Azure](https://questdb.com/docs/images/guides/microsoft-azure-ubuntu/deployment-complete.webp) Deployment complete ### Configure networking[​](https://questdb.com/docs/deployment/azure/#configure-networking "Direct link to Configure networking") 1. Go to your VM's **Networking** settings 2. Click **Add inbound port rule** 3. Add rules for QuestDB ports (see [Networking](https://questdb.com/docs/deployment/azure/#networking) ): * Set **Destination port ranges** to `9000,8812,9003` * Set **Source** to your IP range (not `Any`) * Set **Protocol** to `TCP` * Name the rule `questdb` ![Network security group rules for QuestDB](https://questdb.com/docs/images/guides/microsoft-azure-ubuntu/firewall-rules.webp) NSG rules configured warning Only add port 9009 if you need ILP ingestion, and restrict the source to your application servers. ### Install QuestDB[​](https://questdb.com/docs/deployment/azure/#install-questdb "Direct link to Install QuestDB") 1. Connect via SSH: chmod 400 ~/Downloads/your_key.pemssh -i ~/Downloads/your_key.pem azureuser@ 2. Download and start QuestDB: wget https://github.com/questdb/questdb/releases/download/9.3.3/questdb-9.3.3-rt-linux-x86-64.tar.gztar xzf questdb-9.3.3-rt-linux-x86-64.tar.gzcd questdb-9.3.3-rt-linux-x86-64/bin./questdb.sh start 3. Access the Web Console at `http://:9000` ![QuestDB Web Console running on Azure](https://questdb.com/docs/images/guides/microsoft-azure-ubuntu/web-console.webp) Web Console ready For production deployments, use [systemd](https://questdb.com/docs/deployment/systemd/) to manage the QuestDB service. * * * Security[​](https://questdb.com/docs/deployment/azure/#security "Direct link to Security") ------------------------------------------------------------------------------------------- ### Change default credentials[​](https://questdb.com/docs/deployment/azure/#change-default-credentials "Direct link to Change default credentials") Update credentials immediately after deployment. **Web Console and REST API** - edit `conf/server.conf`: http.user=your_usernamehttp.password=your_secure_password **PostgreSQL** - edit `conf/server.conf`: pg.user=your_usernamepg.password=your_secure_password **InfluxDB line protocol** - edit `conf/auth.json`. See [ILP authentication](https://questdb.com/docs/ingestion/ilp/overview/#authentication) . Restart after changes: ./questdb.sh stop./questdb.sh start ### Disable unused interfaces[​](https://questdb.com/docs/deployment/azure/#disable-unused-interfaces "Direct link to Disable unused interfaces") Reduce attack surface by disabling protocols you don't use: conf/server.conf pg.enabled=false # Disable PostgreSQLline.tcp.enabled=false # Disable ILPhttp.enabled=false # Disable Web Console & REST APIhttp.security.readonly=true # Or make HTTP read-only * * * Operations[​](https://questdb.com/docs/deployment/azure/#operations "Direct link to Operations") ------------------------------------------------------------------------------------------------- ### Upgrading[​](https://questdb.com/docs/deployment/azure/#upgrading "Direct link to Upgrading") 1. Stop QuestDB: ./questdb.sh stop 2. Back up your data directory 3. Download and extract the new version: wget https://github.com/questdb/questdb/releases/download/9.3.3/questdb-9.3.3-rt-linux-x86-64.tar.gztar xzf questdb-9.3.3-rt-linux-x86-64.tar.gz 4. Start the new version: cd questdb-*/bin./questdb.sh start ### Monitoring[​](https://questdb.com/docs/deployment/azure/#monitoring "Direct link to Monitoring") **Health check:** curl http://localhost:9003/status **Prometheus metrics:** curl http://localhost:9003/metrics **Azure Monitor integration:** Use the Azure Monitor agent to collect: * VM metrics (CPU, memory, disk I/O) * QuestDB logs from the `log/` directory * Custom metrics from the Prometheus endpoint * * * Enterprise on Azure[​](https://questdb.com/docs/deployment/azure/#enterprise-on-azure "Direct link to Enterprise on Azure") ---------------------------------------------------------------------------------------------------------------------------- QuestDB Enterprise adds production features for Azure: * **Blob Storage replication** - Continuous backup for durability * **Cold storage** - Move old partitions to Blob Storage, query on-demand * **High availability** - Automatic failover across instances * **EntraID SSO** - Single sign-on with Microsoft Entra ID For EntraID integration, see the [Microsoft EntraID OIDC guide](https://questdb.com/docs/security/oidc/#microsoft-entraid) . See [Enterprise Quick Start](https://questdb.com/docs/getting-started/enterprise-quick-start/) for setup. * [Quick reference](https://questdb.com/docs/deployment/azure/#quick-reference) * [Infrastructure](https://questdb.com/docs/deployment/azure/#infrastructure) * [Instance sizing](https://questdb.com/docs/deployment/azure/#instance-sizing) * [Storage](https://questdb.com/docs/deployment/azure/#storage) * [Networking](https://questdb.com/docs/deployment/azure/#networking) * [Deployment](https://questdb.com/docs/deployment/azure/#deployment) * [Prerequisites](https://questdb.com/docs/deployment/azure/#prerequisites) * [Create the VM](https://questdb.com/docs/deployment/azure/#create-the-vm) * [Configure networking](https://questdb.com/docs/deployment/azure/#configure-networking) * [Install QuestDB](https://questdb.com/docs/deployment/azure/#install-questdb) * [Security](https://questdb.com/docs/deployment/azure/#security) * [Change default credentials](https://questdb.com/docs/deployment/azure/#change-default-credentials) * [Disable unused interfaces](https://questdb.com/docs/deployment/azure/#disable-unused-interfaces) * [Operations](https://questdb.com/docs/deployment/azure/#operations) * [Upgrading](https://questdb.com/docs/deployment/azure/#upgrading) * [Monitoring](https://questdb.com/docs/deployment/azure/#monitoring) * [Enterprise on Azure](https://questdb.com/docs/deployment/azure/#enterprise-on-azure) --- # Check transaction applied after ingestion | QuestDB On this page When ingesting data to a WAL table using ILP protocol, inserts are asynchronous. This recipe shows how to ensure all ingested rows are visible for read-only queries. Problem[​](https://questdb.com/docs/cookbook/operations/check-transaction-applied/#problem "Direct link to Problem") --------------------------------------------------------------------------------------------------------------------- You're performing a single-time ingestion of a large data volume using ILP protocol to a table that uses Write-Ahead Log (WAL). Since inserts are asynchronous, you need to confirm that all ingested rows are visible for read-only queries before proceeding with operations. Solution[​](https://questdb.com/docs/cookbook/operations/check-transaction-applied/#solution "Direct link to Solution") ------------------------------------------------------------------------------------------------------------------------ Query the `wal_tables()` function to check if the writer transaction matches the sequencer transaction. When these values match, all rows have become visible: Check applied transactions from WAL files[Demo this query](https://demo.questdb.io/?query=SELECT%20*%0AFROM%20wal_tables()%0AWHERE%20name%20%3D%20%27core_price%27%20AND%20writerTxn%20%3D%20sequencerTxn%3B&executeQuery=true) SELECT *FROM wal_tables()WHERE name = 'core_price' AND writerTxn = sequencerTxn; This query returns a row when `writerTxn` equals `sequencerTxn` for your table: * `writerTxn` is the last committed transaction available for read-only queries * `sequencerTxn` is the last transaction committed to WAL When they match, all WAL transactions have been applied and all rows are visible for queries. Another viable approach is to run `SELECT count(*) FROM my_table` and verify the expected row count. Related Documentation * [Write-Ahead Log concept](https://questdb.com/docs/concepts/write-ahead-log/) * [Meta functions reference](https://questdb.com/docs/query/functions/meta/) * [InfluxDB Line Protocol overview](https://questdb.com/docs/ingestion/ilp/overview/) * [Problem](https://questdb.com/docs/cookbook/operations/check-transaction-applied/#problem) * [Solution](https://questdb.com/docs/cookbook/operations/check-transaction-applied/#solution) --- # Deploying to Digital Ocean | QuestDB On this page DigitalOcean is a platform with software listings from independent vendors that run on cloud resources. This guide describes how to launch QuestDB via the DigitalOcean marketplace using the official listing. This document also describes usage instructions after you have launched the instance, including hints for authentication, the available interfaces, and tips for accessing the REST API and [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) . Prerequisites[​](https://questdb.com/docs/deployment/digital-ocean/#prerequisites "Direct link to Prerequisites") ------------------------------------------------------------------------------------------------------------------ The prerequisites for deploying QuestDB on DigitalOcean are as follows: * A DigitalOcean account (sign up using [the QuestDB referral link](https://m.do.co/c/50d6b551562b) for 100 USD free credit) * Basic `shell` knowledge for executing commands on the DigitalOcean droplet Create a QuestDB Droplet[​](https://questdb.com/docs/deployment/digital-ocean/#create-a-questdb-droplet "Direct link to Create a QuestDB Droplet") --------------------------------------------------------------------------------------------------------------------------------------------------- DigitalOcean has a marketplace which offers **1-Click Apps** reviewed by their staff. QuestDB is available on the marketplace recently, so setup using this method is preferred: 1. Navigate to the [QuestDB listing](https://marketplace.digitalocean.com/apps/questdb?refcode=50d6b551562b) on DigitalOcean 2. Click **Create QuestDB Droplet** 3. Select the basic plan for your Droplet (4GB RAM is recommended) ![Choosing the RAM and CPU capacity for a QuestDB DigitalOcean Droplet](https://questdb.com/docs/images/blog/2021-07-09/choosing-droplet.webp) 4. Choose a region closest to you 5. At the **Authentication** section, enter your SSH public key, or set a password 6. Set a hostname for the droplet such as `questdb-demo` 7. Leave all other settings with their defaults, and click **Create Droplet** at the bottom of the page ![Finalizing the creation step of a DigitalOcean Droplet running QuestDB](https://questdb.com/docs/images/blog/2021-07-09/questdb-droplet.webp) After 30 seconds, QuestDB should be ready to use. To validate that we set everything up successfully, copy the Droplet's IP address by clicking on it and navigate to `http://:9000/` where `` is the IP address you just copied. The interactive console should load and we can start querying the database and inserting data. QuestDB droplet configuration[​](https://questdb.com/docs/deployment/digital-ocean/#questdb-droplet-configuration "Direct link to QuestDB droplet configuration") ------------------------------------------------------------------------------------------------------------------------------------------------------------------ The server configuration file is at the following location on the droplet: /home/questdb/server.conf For details on the server properties and using this file, see the [server configuration documentation](https://questdb.com/docs/configuration/overview/) . The default ports used by QuestDB interfaces are as follows: * [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) & REST API is available on port `9000` * PostgreSQL wire protocol available on `8812` * InfluxDB line protocol `9009` (TCP and UDP) * Health monitoring & Prometheus `/metrics` `9003` ### QuestDB Credentials[​](https://questdb.com/docs/deployment/digital-ocean/#questdb-credentials "Direct link to QuestDB Credentials") Credentials may be configured in the server configuration file: /home/questdb/server.conf The default Postgres credentials should be changed: pg.user=...pg.password=... For details on authentication using InfluxDB line protocol, see the [InfluxDB line protocol authentication guide](https://questdb.com/docs/ingestion/ilp/overview/#authentication) . ### Disabling authentication[​](https://questdb.com/docs/deployment/digital-ocean/#disabling-authentication "Direct link to Disabling authentication") If you would like to disable authentication for Postgres wire protocol or InfluxDB line protocol, comment out the following lines in the server configuration file: /home/questdb/server.conf # pg.password=...# line.tcp.auth.db.path=conf/auth.txt ### Disabling interfaces[​](https://questdb.com/docs/deployment/digital-ocean/#disabling-interfaces "Direct link to Disabling interfaces") Interfaces may be **disabled completely** with the following configuration: /home/questdb/server.conf # disable postgrespg.enabled=false# disable InfluxDB line protocol over TCP and UDPline.tcp.enabled=falseline.udp.enabled=false# disable HTTP (web console and REST API)http.enabled=false The HTTP interface may alternatively be set to **readonly**: /home/questdb/server.conf # set HTTP interface to readonlyhttp.security.readonly=true API creation[​](https://questdb.com/docs/deployment/digital-ocean/#api-creation "Direct link to API creation") --------------------------------------------------------------------------------------------------------------- In addition to creating a Droplet from the QuestDB 1-Click App via the control panel, you can also [use the DigitalOcean API](https://digitalocean.com/docs/api/) . As an example, to create a 4GB QuestDB Droplet in the SFO2 region, you can use the following curl command. You’ll need to either save your API access token to an environment variable or substitute it into the command below. curl -X POST -H 'Content-Type: application/json' \ -H 'Authorization: Bearer '$TOKEN'' -d \ '{"name":"choose_a_name","region":"sfo2","size":"s-2vcpu-4gb","image":"questdb-20-04"}' \ "https://api.digitalocean.com/v2/droplets" * [Prerequisites](https://questdb.com/docs/deployment/digital-ocean/#prerequisites) * [Create a QuestDB Droplet](https://questdb.com/docs/deployment/digital-ocean/#create-a-questdb-droplet) * [QuestDB droplet configuration](https://questdb.com/docs/deployment/digital-ocean/#questdb-droplet-configuration) * [QuestDB Credentials](https://questdb.com/docs/deployment/digital-ocean/#questdb-credentials) * [Disabling authentication](https://questdb.com/docs/deployment/digital-ocean/#disabling-authentication) * [Disabling interfaces](https://questdb.com/docs/deployment/digital-ocean/#disabling-interfaces) * [API creation](https://questdb.com/docs/deployment/digital-ocean/#api-creation) --- # Launch QuestDB with systemd | QuestDB On this page Use systemd to run QuestDB as a system or user service. This guide will demonstrate an initial configuration which you can use as the basis for your installation scripts. It will also demonstrate how to setup and start a QuestDB systemd service. Prerequisites[​](https://questdb.com/docs/deployment/systemd/#prerequisites "Direct link to Prerequisites") ------------------------------------------------------------------------------------------------------------ The prerequisites for deploying QuestDB with systemd are: * A Unix machine supporting systemd Initial system configuration[​](https://questdb.com/docs/deployment/systemd/#initial-system-configuration "Direct link to Initial system configuration") --------------------------------------------------------------------------------------------------------------------------------------------------------- The following commands inform a basis for your systemd service. Prior to running systemd, you will require some directory structure and a binary for QuestDB. Depending on your specific needs and operational preferences, your commands may differ. The goal is to give you a helpful starting point for the example service. The example presumes that you have used a privileged user to create a user with appropriately scoped permissions. #!/bin/bash# Download and install the JDKcurl -s https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz -o jdk.tar.gzmkdir -p ~/jdktar -xzf jdk.tar.gz -C ~/jdk --strip-components=1export JAVA_HOME=~/jdkexport PATH=$JAVA_HOME/bin:$PATH# Download and set up QuestDBcurl -s https://dl.questdb.io/snapshots/questdb-latest-no-jre-bin.tar.gz -o questdb.tar.gzmkdir -p ~/questdb/binarytar -xzf questdb.tar.gz -C ~/questdb/binary --strip-components 1mv ~/questdb/binary/questdb.jar ~/bin/ ### Using a QuestDB server.conf[​](https://questdb.com/docs/deployment/systemd/#using-a-questdb-serverconf "Direct link to Using a QuestDB server.conf") Your QuestDB configuration is done in a `server.conf` file. The `server.conf` file is populated with safe defaults on first startup if it does not exist. It is common for user's of QuestDB to stick with the default configuration. However, should you choose to update your own and serve it via a scripted method or similar, you may do so. Read more about the available options in our [Configuration reference page](https://questdb.com/docs/configuration/overview/) . Example questdb.service[​](https://questdb.com/docs/deployment/systemd/#example-questdbservice "Direct link to Example questdb.service") ----------------------------------------------------------------------------------------------------------------------------------------- Create a new file called `questdb.service`: touch questdb.service The example below is a recommended starting point. Note the default QuestDB service configuration and system paths in line with the above example. Next, open the `questdb.service` file and add the following: [Unit]Description=QuestDBDocumentation=https://www.questdb.com/docs/After=network.target[Service]Type=simpleRestart=alwaysRestartSec=2# Adjust java path to match requirements of a given distroExecStart=/home/[USER_NAME]/jdk/bin/java \--add-exports java.base/jdk.internal.math=io.questdb \-p /home/[USER_NAME]/bin/questdb.jar \-m io.questdb/io.questdb.ServerMain \-DQuestDB-Runtime-66535 \-ea -Dnoebug \-XX:+UnlockExperimentalVMOptions \-XX:+AlwaysPreTouch \-XX:+UseParallelOldGC \-d /home/[USER_NAME]/var/lib/questdbExecReload=/bin/kill -s HUP $MAINPID# Prevent writes to /usr, /boot, and /etcProtectSystem=fullStandardError=syslogSyslogIdentifier=questdb[Install]WantedBy=multi-user.target Next, move your `questdb.service` file into your user's `systemd` config: mv questdb.service ~/.config/systemd/user/questdb.service Enable the service: systemctl --user enable questdb.service Start the service: systemctl --user start questdb Check out the service status: systemctl --user status questdb.service Your QuestDB instance should now be accessible at localhost, with services available at the following default ports: * [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) & REST API is available on port `9000` * PostgreSQL wire protocol available on `8812` * InfluxDB line protocol `9009` (TCP and UDP) * Health monitoring & Prometheus `/metrics` `9003` User vs. System[​](https://questdb.com/docs/deployment/systemd/#user-vs-system "Direct link to User vs. System") ----------------------------------------------------------------------------------------------------------------- As an operator, you can decide whether to run systemd as the "system" from root permissions, or a user with its own privileges. At the system level, root is required to make changes to the `systemctl` application. Services created this way will start and stop when the system is restarted. Unlike at the system level, user services will start & stop as the user session is activated or de-activated. You also do not need to apply `sudo` to make changes to the services. Consistent with the examples on this page, we recommend scoped users. Daily timers[​](https://questdb.com/docs/deployment/systemd/#daily-timers "Direct link to Daily timers") --------------------------------------------------------------------------------------------------------- If running QuestDB on a `systemd` based Linux (for example, `Ubuntu`) you may find that, by default, there are a number of daily upgrade timers enabled. When executed, these tasks restart `systemd` services, which can cause interruptions to QuestDB. It will appear that QuestDB restarted with no errors or apparent trigger. To resolve it, either: * Force services to be listed for restart, but not restarted automatically. * Modify `/etc/needrestart/needrestart.conf` to contain `$nrconf{restart} = 'l'`. * Disable the auto-upgrade services entirely: sudo systemctl disable --now apt-daily-upgrade.timersudo systemctl disable --now apt-daily.timersudo systemctl disable --now unattended-upgrades.service You can check the status of the timers using: systemctl list-timers --all | grep apt * [Prerequisites](https://questdb.com/docs/deployment/systemd/#prerequisites) * [Initial system configuration](https://questdb.com/docs/deployment/systemd/#initial-system-configuration) * [Using a QuestDB server.conf](https://questdb.com/docs/deployment/systemd/#using-a-questdb-serverconf) * [Example questdb.service](https://questdb.com/docs/deployment/systemd/#example-questdbservice) * [User vs. System](https://questdb.com/docs/deployment/systemd/#user-vs-system) * [Daily timers](https://questdb.com/docs/deployment/systemd/#daily-timers) --- # Show parameters with non-default values | QuestDB On this page When troubleshooting or auditing your QuestDB configuration, it's useful to see which parameters have been changed from their defaults. Problem[​](https://questdb.com/docs/cookbook/operations/show-non-default-params/#problem "Direct link to Problem") ------------------------------------------------------------------------------------------------------------------- You need to identify which configuration parameters have been explicitly set via the configuration file or environment variables, filtering out all parameters that are still using their default values. Solution[​](https://questdb.com/docs/cookbook/operations/show-non-default-params/#solution "Direct link to Solution") ---------------------------------------------------------------------------------------------------------------------- Query the `SHOW PARAMETERS` command and filter by `value_source` to exclude defaults: Find which params where modified from default values[Demo this query](https://demo.questdb.io/?query=--%20Show%20all%20parameters%20modified%20from%20their%20defaults%2C%20via%20conf%20file%20or%20env%20variable%0A(SHOW%20PARAMETERS)%20WHERE%20value_source%20%3C%3E%20%27default%27%3B&executeQuery=true) -- Show all parameters modified from their defaults, via conf file or env variable(SHOW PARAMETERS) WHERE value_source <> 'default'; This query returns only the parameters that have been explicitly configured, showing their current values and the source of the configuration (e.g., `conf` file or `env` variable). Related Documentation * [SHOW PARAMETERS reference](https://questdb.com/docs/query/sql/show/#show-parameters) * [Configuration reference](https://questdb.com/docs/configuration/overview/) * [Problem](https://questdb.com/docs/cookbook/operations/show-non-default-params/#problem) * [Solution](https://questdb.com/docs/cookbook/operations/show-non-default-params/#solution) --- # Upgrade to QuestDB Enterprise | QuestDB On this page Upgrading from QuestDB Open Source to QuestDB Enterprise is straightforward: **download the Enterprise binaries, swap them in, and restart**. Your data stays in place and works immediately. What you get with QuestDB Enterprise[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#what-you-get-with-questdb-enterprise "Direct link to What you get with QuestDB Enterprise") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- * **TLS encryption** for all network interfaces * **Role-based access control (RBAC)** with users, groups, and permissions * **Single Sign-On (SSO)** via OpenID Connect * **Database replication** for high availability * **Multi-tier storage** with seamless object storage integration * **Automated backup and recovery** for data protection Upgrade steps[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#upgrade-steps "Direct link to Upgrade steps") ------------------------------------------------------------------------------------------------------------------------------- ### 1\. Download Enterprise binaries[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#1-download-enterprise-binaries "Direct link to 1. Download Enterprise binaries") You should have received an email with download credentials for the Enterprise binaries. Download the version matching your operating system and architecture. tip Check the [release notes](https://questdb.com/release-notes/?ref=docs&type=enterprise) for the latest features and improvements. ### 2\. Swap binaries and restart[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#2-swap-binaries-and-restart "Direct link to 2. Swap binaries and restart") 1. Stop your running QuestDB instance 2. Replace the existing QuestDB binaries with the Enterprise ones 3. Start QuestDB with the new binaries That's it! The database will automatically prepare your existing tables for Enterprise features on first startup. Optional: Create a backup first While upgrades are safe, you can create a restore point before upgrading: CHECKPOINT CREATE Then back up your data directory (e.g., create a `.tar` archive or cloud snapshot). See [Backup and restore](https://questdb.com/docs/operations/backup/) for details. Configure QuestDB Enterprise features[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#configure-questdb-enterprise-features "Direct link to Configure QuestDB Enterprise features") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- These steps are **optional** - configure only the features you need. ### TLS encryption[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#tls-encryption "Direct link to TLS encryption") Secure all network connections with TLS. You'll need a certificate in PEM format, or you can use a [self-signed demo certificate](https://questdb.com/docs/security/tls/#demo-certificates) to get started. See the [TLS Encryption guide](https://questdb.com/docs/security/tls/) . ### User accounts and permissions[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#user-accounts-and-permissions "Direct link to User accounts and permissions") Replace the default admin credentials in `server.conf`: server.conf acl.admin.user=myadminacl.admin.password=mypwd For production, create proper admin accounts and disable the built-in admin: CREATE USER administrator WITH PASSWORD adminpwd;GRANT ALL TO administrator WITH GRANT OPTION; server.conf acl.admin.user.enabled=false See the [RBAC documentation](https://questdb.com/docs/security/rbac/) for complete setup. ### Single Sign-On (SSO)[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#single-sign-on-sso "Direct link to Single Sign-On (SSO)") Integrate with your identity provider (Microsoft Entra ID, PingFederate, etc.) for centralized authentication. See the [OIDC Integration guide](https://questdb.com/docs/security/oidc/) . ### Replication[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#replication "Direct link to Replication") Set up database replication for high availability and disaster recovery. See the [Database Replication guide](https://questdb.com/docs/high-availability/setup/) . Important notes[​](https://questdb.com/docs/getting-started/migrate-to-enterprise/#important-notes "Direct link to Important notes") ------------------------------------------------------------------------------------------------------------------------------------- The upgrade process modifies table metadata to enable Enterprise features. For this reason: * Always perform an **in-place upgrade** (swap binaries in the same installation) * Don't copy data directories between Open Source and Enterprise installations * If reusing an object store from a test Enterprise instance, clear it first Have a complex migration scenario? [Contact us](https://questdb.com/enterprise/contact/) and we'll help with your setup. * [What you get with QuestDB Enterprise](https://questdb.com/docs/getting-started/migrate-to-enterprise/#what-you-get-with-questdb-enterprise) * [Upgrade steps](https://questdb.com/docs/getting-started/migrate-to-enterprise/#upgrade-steps) * [1\. Download Enterprise binaries](https://questdb.com/docs/getting-started/migrate-to-enterprise/#1-download-enterprise-binaries) * [2\. Swap binaries and restart](https://questdb.com/docs/getting-started/migrate-to-enterprise/#2-swap-binaries-and-restart) * [Configure QuestDB Enterprise features](https://questdb.com/docs/getting-started/migrate-to-enterprise/#configure-questdb-enterprise-features) * [TLS encryption](https://questdb.com/docs/getting-started/migrate-to-enterprise/#tls-encryption) * [User accounts and permissions](https://questdb.com/docs/getting-started/migrate-to-enterprise/#user-accounts-and-permissions) * [Single Sign-On (SSO)](https://questdb.com/docs/getting-started/migrate-to-enterprise/#single-sign-on-sso) * [Replication](https://questdb.com/docs/getting-started/migrate-to-enterprise/#replication) * [Important notes](https://questdb.com/docs/getting-started/migrate-to-enterprise/#important-notes) --- # Dagster | QuestDB On this page Dagster is a modern data orchestrator that enables structured and scalable workflow automation. With Dagster, you can automate tasks such as executing SQL queries on QuestDB and managing data pipelines with built-in monitoring and logging. Alternatively, checkout our [Automating QuestDB Tasks](https://questdb.com/docs/operations/task-automation/) guide for a scripted approach. Prerequisites[​](https://questdb.com/docs/integrations/orchestration/dagster/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------------- * Python 3.9 or later * QuestDB running locally or remotely * `psycopg` library for PostgreSQL interaction * Dagster installed Installation[​](https://questdb.com/docs/integrations/orchestration/dagster/#installation "Direct link to Installation") ------------------------------------------------------------------------------------------------------------------------- To install Dagster and the required dependencies, run: pip install dagster dagster-webserver psycopg Please refer to the [Dagster Docs](https://docs.dagster.io/getting-started/installation) for other options. Basic integration[​](https://questdb.com/docs/integrations/orchestration/dagster/#basic-integration "Direct link to Basic integration") ---------------------------------------------------------------------------------------------------------------------------------------- On Dagster you write your automation either using a dependency graph approach, similar to Apache Airflow, or following a data resource model. Whichever approach you take, the automation is written in Python and the easiest way to automate QuestDB tasks is by using `Psycopg`. Example: Running a Query on QuestDB[​](https://questdb.com/docs/integrations/orchestration/dagster/#example-running-a-query-on-questdb "Direct link to Example: Running a Query on QuestDB") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The following example defines a Dagster operation (`op`) to execute a SQL query on QuestDB: from dagster import op, jobimport psycopg@opdef execute_query(): conn = psycopg.connect("postgresql://admin:quest@localhost:8812/qdb") with conn.cursor() as cursor: cursor.execute("ALTER TABLE my_table DROP PARTITION WHERE timestamp < dateadd('d', -30, now());") conn.commit()@jobdef questdb_cleanup_job(): execute_query() Running the Dagster Job[​](https://questdb.com/docs/integrations/orchestration/dagster/#running-the-dagster-job "Direct link to Running the Dagster Job") ---------------------------------------------------------------------------------------------------------------------------------------------------------- 1. Start the Dagster UI: dagster dev 2. Open `http://localhost:3000` and trigger the `questdb_cleanup_job` manually. Scheduling the Job[​](https://questdb.com/docs/integrations/orchestration/dagster/#scheduling-the-job "Direct link to Scheduling the Job") ------------------------------------------------------------------------------------------------------------------------------------------- To schedule the job to run daily at midnight: from dagster import schedule@schedule(cron_schedule="0 0 * * *", job=questdb_cleanup_job, execution_timezone="UTC")def daily_questdb_cleanup_schedule(): return {} Next Steps[​](https://questdb.com/docs/integrations/orchestration/dagster/#next-steps "Direct link to Next Steps") ------------------------------------------------------------------------------------------------------------------- For further details and resources, refer to the following links: * **Dagster Documentation**: [https://docs.dagster.io/](https://docs.dagster.io/) * **Full Example Repository**: [https://github.com/questdb/data-orchestration-and-scheduling-samples](https://github.com/questdb/data-orchestration-and-scheduling-samples) * [Prerequisites](https://questdb.com/docs/integrations/orchestration/dagster/#prerequisites) * [Installation](https://questdb.com/docs/integrations/orchestration/dagster/#installation) * [Basic integration](https://questdb.com/docs/integrations/orchestration/dagster/#basic-integration) * [Example: Running a Query on QuestDB](https://questdb.com/docs/integrations/orchestration/dagster/#example-running-a-query-on-questdb) * [Running the Dagster Job](https://questdb.com/docs/integrations/orchestration/dagster/#running-the-dagster-job) * [Scheduling the Job](https://questdb.com/docs/integrations/orchestration/dagster/#scheduling-the-job) * [Next Steps](https://questdb.com/docs/integrations/orchestration/dagster/#next-steps) --- # Insert data from PHP using ILP | QuestDB On this page QuestDB doesn't maintain an official PHP library, but since the ILP (InfluxDB Line Protocol) is text-based, you can easily send your data using PHP's built-in HTTP or socket functions, or use the official InfluxDB PHP client library. Available approaches[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#available-approaches "Direct link to Available approaches") ------------------------------------------------------------------------------------------------------------------------------------------------------ This guide covers three methods for sending ILP data to QuestDB from PHP: 1. **HTTP with cURL** (recommended for most use cases) * Full control over ILP formatting and timestamps * No external dependencies beyond PHP's built-in cURL * Requires manual ILP string construction 2. **InfluxDB v2 PHP Client** (easiest to use) * Clean Point builder API * Automatic batching and error handling * **Limitation:** Cannot use custom timestamps with QuestDB (must use server timestamps) * Requires Composer packages: `influxdata/influxdb-client-php` and `guzzlehttp/guzzle` 3. **TCP Socket** (highest throughput) * Best performance for high-volume scenarios * No acknowledgments - data loss possible * Manual implementation required ILP protocol overview[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#ilp-protocol-overview "Direct link to ILP protocol overview") --------------------------------------------------------------------------------------------------------------------------------------------------------- The ILP protocol allows you to send data to QuestDB using a simple line-based text format: table_name,comma_separated_symbols comma_separated_non_symbols optional_timestamp\n Each line represents one row of data. For example, these two lines are well-formed ILP messages: readings,city=London,make=Omron temperature=23.5,humidity=0.343 1465839830100400000\nreadings,city=Bristol,make=Honeywell temperature=23.2,humidity=0.443\n The format consists of: * **Table name**: The target table for the data * **Symbols** (tags): Comma-separated key-value pairs for indexed categorical data * **Columns** (fields): Space-separated, then comma-separated key-value pairs for numerical or string data * **Timestamp** (optional): Nanosecond-precision timestamp; if omitted, QuestDB uses server time For complete ILP specification, see the [ILP reference documentation](https://questdb.com/docs/ingestion/ilp/overview/) . ILP over HTTP[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#ilp-over-http "Direct link to ILP over HTTP") --------------------------------------------------------------------------------------------------------------------------------- QuestDB supports ILP data via HTTP or TCP. **HTTP is the recommended approach** for most use cases as it provides better reliability and easier debugging. To send data via HTTP: 1. Send a POST request to `http://localhost:9000/write` (or your QuestDB instance endpoint) 2. Set `Content-Type: text/plain` header 3. Include ILP-formatted rows in the request body 4. For higher throughput, batch multiple rows in a single request ### HTTP buffering example[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#http-buffering-example "Direct link to HTTP buffering example") The following PHP class provides buffered insertion with automatic flushing based on either row count or elapsed time: Buffered ILP insertion via HTTP bufferSize = $bufferSize; $this->flushInterval = $flushInterval; $this->lastFlushTime = time(); } public function __destruct() { // Attempt to flush any remaining data when script is terminating $this->flush(); } public function insertRow($tableName, $symbols, $columns, $timestamp = null) { $row = $this->formatRow($tableName, $symbols, $columns, $timestamp); $this->buffer[] = $row; $this->checkFlushConditions(); } private function formatRow($tableName, $symbols, $columns, $timestamp) { $escape = function($value) { return str_replace([' ', ',', "\n"], ['\ ', '\,', '\n'], $value); }; $symbolString = implode(',', array_map( function($k, $v) use ($escape) { return "$k={$escape($v)}"; }, array_keys($symbols), $symbols )); $columnString = implode(',', array_map( function($k, $v) use ($escape) { return "$k={$escape($v)}"; }, array_keys($columns), $columns )); // Check if timestamp is provided $timestampPart = is_null($timestamp) ? '' : " $timestamp"; return "$tableName,$symbolString $columnString$timestampPart"; } private function checkFlushConditions() { if (count($this->buffer) >= $this->bufferSize || (time() - $this->lastFlushTime) >= $this->flushInterval) { $this->flush(); } } private function flush() { if (empty($this->buffer)) { return; // Nothing to flush } $data = implode("\n", $this->buffer); $this->buffer = []; $this->lastFlushTime = time(); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->endpoint); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: text/plain']); curl_exec($ch); curl_close($ch); }}// Usage example:$inserter = new DataInserter(10, 30);// Inserting rows for London$inserter->insertRow("test_readings", ["city" => "London", "make" => "Omron"], ["temperature" => 23.5, "humidity" => 0.343], "1650573480100400000");$inserter->insertRow("test_readings", ["city" => "London", "make" => "Sony"], ["temperature" => 21.0, "humidity" => 0.310]);$inserter->insertRow("test_readings", ["city" => "London", "make" => "Philips"], ["temperature" => 22.5, "humidity" => 0.333], "1650573480100500000");$inserter->insertRow("test_readings", ["city" => "London", "make" => "Samsung"], ["temperature" => 24.0, "humidity" => 0.350]);// Inserting rows for Madrid$inserter->insertRow("test_readings", ["city" => "Madrid", "make" => "Omron"], ["temperature" => 25.5, "humidity" => 0.360], "1650573480100600000");$inserter->insertRow("test_readings", ["city" => "Madrid", "make" => "Sony"], ["temperature" => 23.0, "humidity" => 0.340]);$inserter->insertRow("test_readings", ["city" => "Madrid", "make" => "Philips"], ["temperature" => 26.0, "humidity" => 0.370], "1650573480100700000");$inserter->insertRow("test_readings", ["city" => "Madrid", "make" => "Samsung"], ["temperature" => 22.0, "humidity" => 0.355]);// Inserting rows for New York$inserter->insertRow("test_readings", ["city" => "New York", "make" => "Omron"], ["temperature" => 20.5, "humidity" => 0.330], "1650573480100800000");$inserter->insertRow("test_readings", ["city" => "New York", "make" => "Sony"], ["temperature" => 19.0, "humidity" => 0.320]);$inserter->insertRow("test_readings", ["city" => "New York", "make" => "Philips"], ["temperature" => 21.0, "humidity" => 0.340], "1650573480100900000");$inserter->insertRow("test_readings", ["city" => "New York", "make" => "Samsung"], ["temperature" => 18.5, "humidity" => 0.335]);?> This class: * Buffers rows until either 10 rows are accumulated or 30 seconds have elapsed * Properly escapes special characters (spaces, commas, newlines) in values * Automatically flushes remaining data when the script terminates * Uses cURL for HTTP communication tip For production use, consider adding error handling to check the HTTP response status and implement retry logic for failed requests. Using the InfluxDB v2 PHP client[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#using-the-influxdb-v2-php-client "Direct link to Using the InfluxDB v2 PHP client") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Another approach is to use the official [InfluxDB PHP client library](https://github.com/influxdata/influxdb-client-php) , which supports the InfluxDB v2 write API. QuestDB is compatible with this API, making the client library a convenient option. ### Installation[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#installation "Direct link to Installation") Install the required packages via Composer: composer require influxdata/influxdb-client-php guzzlehttp/guzzle **Required dependencies:** * `influxdata/influxdb-client-php` - The InfluxDB v2 PHP client library * `guzzlehttp/guzzle` - A PSR-18 compatible HTTP client (required by the InfluxDB client) Alternative HTTP Clients The InfluxDB client requires a PSR-18 compatible HTTP client. While we recommend Guzzle, you can use alternatives like `php-http/guzzle7-adapter` or `symfony/http-client` if preferred. ### Configuration[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#configuration "Direct link to Configuration") When using the InfluxDB client with QuestDB: * **URL**: Use your QuestDB HTTP endpoint (default: `http://localhost:9000`) * **Token**: Not required - can be left empty or use any string * **Bucket**: Not required - can be any string (ignored by QuestDB) * **Organization**: Not required - can be any string (ignored by QuestDB) Write API Only QuestDB only supports the **InfluxDB v2 write API** when using this client. Query operations are not supported through the InfluxDB client - use QuestDB's PostgreSQL wire protocol or REST API for queries instead. ### Example code[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#example-code "Direct link to Example code") Using InfluxDB v2 PHP client with QuestDB "http://localhost:9000", "token" => "", // Not required for QuestDB "bucket" => "default", // Not used by QuestDB "org" => "default", // Not used by QuestDB "precision" => WritePrecision::NS]);$writeApi = $client->createWriteApi();// Write points using the Point builder// Note: Omit ->time() to let QuestDB assign server timestamps$point = Point::measurement("readings") ->addTag("city", "London") ->addTag("make", "Omron") ->addField("temperature", 23.5) ->addField("humidity", 0.343);$writeApi->write($point);// Write multiple points$points = [ Point::measurement("readings") ->addTag("city", "Madrid") ->addTag("make", "Sony") ->addField("temperature", 25.5) ->addField("humidity", 0.360), Point::measurement("readings") ->addTag("city", "New York") ->addTag("make", "Philips") ->addField("temperature", 20.5) ->addField("humidity", 0.330)];$writeApi->write($points);// Always close the client$client->close();?> ### Benefits and limitations[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#benefits-and-limitations "Direct link to Benefits and limitations") The Point builder provides several advantages: * **Automatic ILP formatting and escaping** - No need to manually construct ILP strings * **Built-in error handling** - The client handles HTTP errors and retries * **Batching support** - Automatically batches writes for better performance * **Clean API** - Fluent Point builder interface is easy to use Timestamp Limitation The InfluxDB PHP client **cannot be used with custom timestamps** when writing to QuestDB. When you call `->time()` with a nanosecond timestamp, the client serializes it in scientific notation (e.g., `1.76607297E+18`), which QuestDB's ILP parser rejects. **Solution:** Always omit the `->time()` call and let QuestDB assign server-side timestamps automatically. This is the only reliable way to use the InfluxDB PHP client with QuestDB. **If you need client-side timestamps:** Use the raw HTTP cURL approach (documented above) where you manually format the ILP string with full control over timestamp formatting. ILP over TCP socket[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#ilp-over-tcp-socket "Direct link to ILP over TCP socket") --------------------------------------------------------------------------------------------------------------------------------------------------- TCP over socket provides higher throughput but is less reliable than HTTP. The message format is identical - only the transport changes. Use TCP when: * You need maximum ingestion throughput * Your application can handle potential data loss on connection failures * You're willing to implement your own connection management and error handling ### TCP socket example[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#tcp-socket-example "Direct link to TCP socket example") Here's a basic example using PHP's socket functions: Send ILP data via TCP socket This basic example: * Connects to QuestDB's ILP port (default 9009) * Sends a single row of data * Closes the connection For production use with TCP, you should: * Keep connections open and reuse them for multiple rows * Implement batching to reduce network overhead * Add proper error handling and reconnection logic * Consider using a connection pool for concurrent writes TCP Considerations TCP ILP does not provide acknowledgments for successful writes. If the connection drops, you may lose data without notification. For critical data, use HTTP ILP instead. Choosing the right approach[​](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#choosing-the-right-approach "Direct link to Choosing the right approach") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Feature | HTTP (cURL) | HTTP (InfluxDB Client) | TCP Socket | | --- | --- | --- | --- | | **Reliability** | High - responses indicate success/failure | High - responses indicate success/failure | Low - no acknowledgment | | **Throughput** | Good | Good | Excellent | | **Error handling** | Manual via cURL | Built-in via client library | Manual implementation required | | **Ease of use** | Medium - manual ILP formatting | High - Point builder API | Low - manual everything | | **Custom timestamps** | ✅ Full control | ❌ Must use server timestamps | ✅ Full control | | **Dependencies** | None (cURL built-in) | `influxdb-client-php`
`guzzlehttp/guzzle` | None (sockets built-in) | | **Authentication** | Standard HTTP auth | Standard HTTP auth | Limited options | | **Recommended for** | Custom timestamps required | Ease of development, server timestamps acceptable | High-volume, loss-tolerant scenarios | Related Documentation * [ILP reference documentation](https://questdb.com/docs/ingestion/ilp/overview/) * [HTTP REST API](https://questdb.com/docs/query/rest-api/) * [Authentication and security](https://questdb.com/docs/security/rbac/) * [Available approaches](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#available-approaches) * [ILP protocol overview](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#ilp-protocol-overview) * [ILP over HTTP](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#ilp-over-http) * [HTTP buffering example](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#http-buffering-example) * [Using the InfluxDB v2 PHP client](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#using-the-influxdb-v2-php-client) * [Installation](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#installation) * [Configuration](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#configuration) * [Example code](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#example-code) * [Benefits and limitations](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#benefits-and-limitations) * [ILP over TCP socket](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#ilp-over-tcp-socket) * [TCP socket example](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#tcp-socket-example) * [Choosing the right approach](https://questdb.com/docs/cookbook/programmatic/php/inserting-ilp/#choosing-the-right-approach) --- # Optimize disk and memory usage with many tables | QuestDB On this page When operating QuestDB with many tables, the default settings may consume more memory and disk space than necessary. This recipe shows how to optimize these resources. Problem[​](https://questdb.com/docs/cookbook/operations/optimize-many-tables/#problem "Direct link to Problem") ---------------------------------------------------------------------------------------------------------------- QuestDB allocates memory for out-of-order inserts per column and table. With the default setting of `cairo.o3.column.memory.size=256K`, each table and column uses 512K of memory (2x the configured size). When you have many tables, this memory overhead can become significant. Similarly, QuestDB allocates disk space in chunks for columns and indexes. While larger chunks make sense for a single large table, multiple smaller tables benefit from smaller allocation sizes, which can noticeably decrease disk storage usage. Solution[​](https://questdb.com/docs/cookbook/operations/optimize-many-tables/#solution "Direct link to Solution") ------------------------------------------------------------------------------------------------------------------- Reduce memory allocation for out-of-order inserts by setting a smaller `cairo.o3.column.memory.size`. Start with 128K and adjust based on your needs: cairo.o3.column.memory.size=128K Reduce disk space allocation by configuring smaller page sizes for data and indexes: cairo.system.writer.data.append.page.size=128Kcairo.writer.data.append.page.size=128Kcairo.writer.data.index.key.append.page.size=128Kcairo.writer.data.index.value.append.page.size=128K These settings should be added to your `server.conf` file or set as environment variables. Related Documentation * [Configuration reference](https://questdb.com/docs/configuration/overview/) * [Capacity planning](https://questdb.com/docs/getting-started/capacity-planning/) * [Problem](https://questdb.com/docs/cookbook/operations/optimize-many-tables/#problem) * [Solution](https://questdb.com/docs/cookbook/operations/optimize-many-tables/#solution) --- # Insert data from Ruby using ILP | QuestDB On this page Send time-series data from Ruby to QuestDB using the InfluxDB Line Protocol (ILP). While QuestDB doesn't maintain an official Ruby client, you can easily use the official InfluxDB Ruby gem to send data via ILP over HTTP, which QuestDB fully supports. Available approaches[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#available-approaches "Direct link to Available approaches") ------------------------------------------------------------------------------------------------------------------------------------------------------- Two methods for sending ILP data from Ruby: 1. **InfluxDB v2 Ruby Client** (recommended) * Official InfluxDB gem with clean API * Automatic batching and error handling * Compatible with QuestDB's ILP endpoint * Requires: `influxdb-client` gem 2. **TCP Socket** (for custom implementations) * Direct socket communication * Manual ILP message formatting * Higher throughput, no dependencies * Requires: Built-in Ruby socket library Using the InfluxDB v2 Ruby client[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#using-the-influxdb-v2-ruby-client "Direct link to Using the InfluxDB v2 Ruby client") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The InfluxDB v2 client provides a convenient Point builder API that works with QuestDB. ### Installation[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#installation "Direct link to Installation") gem install influxdb-client Or add to your `Gemfile`: gem 'influxdb-client', '~> 3.1' ### Example code[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#example-code "Direct link to Example code") require 'influxdb-client'# Create clientclient = InfluxDB2::Client.new( 'http://localhost:9000', 'ignore-token', # Token not required for QuestDB bucket: 'ignore-bucket', # Bucket not used by QuestDB org: 'ignore-org', # Organization not used by QuestDB precision: InfluxDB2::WritePrecision::NANOSECOND, use_ssl: false)write_api = client.create_write_api# Write a single pointpoint = InfluxDB2::Point.new(name: 'readings') .add_tag('city', 'London') .add_tag('make', 'Omron') .add_field('temperature', 23.5) .add_field('humidity', 0.343)write_api.write(data: point)# Write multiple pointspoints = [ InfluxDB2::Point.new(name: 'readings') .add_tag('city', 'Madrid') .add_tag('make', 'Sony') .add_field('temperature', 25.5) .add_field('humidity', 0.360), InfluxDB2::Point.new(name: 'readings') .add_tag('city', 'New York') .add_tag('make', 'Philips') .add_field('temperature', 20.5) .add_field('humidity', 0.330)]write_api.write(data: points)# Always close the clientclient.close! ### Configuration notes[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#configuration-notes "Direct link to Configuration notes") When using the InfluxDB client with QuestDB: * **`token`**: Not required - can be empty string or any value * **`bucket`**: Ignored by QuestDB - can be any string * **`org`**: Ignored by QuestDB - can be any string * **`precision`**: Use `NANOSECOND` for compatibility (QuestDB's native precision) * **`use_ssl`**: Set to `false` for local development, `true` for production with TLS ### Data types[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#data-types "Direct link to Data types") The InfluxDB client automatically handles type conversions: point = InfluxDB2::Point.new(name: 'measurements') .add_tag('sensor_id', '001') # SYMBOL in QuestDB .add_field('temperature', 23.5) # DOUBLE .add_field('humidity', 0.343) # DOUBLE .add_field('pressure', 1013) # LONG (integer) .add_field('status', 'active') # STRING .add_field('online', true) # BOOLEAN TCP socket approach[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#tcp-socket-approach "Direct link to TCP socket approach") ---------------------------------------------------------------------------------------------------------------------------------------------------- For maximum control and performance, send ILP messages directly via TCP sockets. ### Basic TCP example[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#basic-tcp-example "Direct link to Basic TCP example") require 'socket'HOST = 'localhost'PORT = 9009# Helper method to get current time in nanosecondsdef time_in_nsec now = Time.now return now.to_i * (10 ** 9) + now.nsecendbegin s = TCPSocket.new(HOST, PORT) # Single record with timestamp s.puts "trades,symbol=BTC-USDT,side=buy price=37779.62,amount=0.5 #{time_in_nsec}\n" # Omitting timestamp - server assigns one s.puts "trades,symbol=ETH-USDT,side=sell price=2615.54,amount=1.2\n" # Multiple records (newline-delimited) s.puts "trades,symbol=SOL-USDT,side=buy price=98.23,amount=10.0\n" + "trades,symbol=BTC-USDT,side=sell price=37800.00,amount=0.3\n"rescue SocketError => ex puts "Socket error: #{ex.inspect}"ensure s.close if send ### ILP message format[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#ilp-message-format "Direct link to ILP message format") The ILP format is: table_name,tag1=value1,tag2=value2 field1=value1,field2=value2 timestamp\n Breaking it down: * **Table name**: Target table (created automatically if doesn't exist) * **Tags** (symbols): Comma-separated key=value pairs for indexed categorical data * **Space separator**: Separates tags from fields * **Fields** (columns): Comma-separated key=value pairs for numerical or string data * **Space separator**: Separates fields from timestamp * **Timestamp** (optional): Nanosecond-precision timestamp; if omitted, server assigns **Example:** readings,city=London,make=Omron temperature=23.5,humidity=0.343 1465839830100400000\n ### Escaping special characters[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#escaping-special-characters "Direct link to Escaping special characters") ILP requires escaping for certain characters: def escape_ilp(value) value.to_s .gsub(' ', '\\ ') # Space .gsub(',', '\\,') # Comma .gsub('=', '\\=') # Equals .gsub("\n", '\\n') # Newlineend# Usagetag_value = "London, UK"escaped = escape_ilp(tag_value) # "London\\, UK"s.puts "readings,city=#{escaped} temperature=23.5\n" ### Batching for performance[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#batching-for-performance "Direct link to Batching for performance") Send multiple rows in a single TCP write: require 'socket'HOST = 'localhost'PORT = 9009def time_in_nsec now = Time.now return now.to_i * (10 ** 9) + now.nsecendbegin s = TCPSocket.new(HOST, PORT) # Build batch of rows batch = [] (1..1000).each do |i| timestamp = time_in_nsec + i * 1000000 # 1ms apart batch << "readings,sensor_id=#{i} value=#{rand(100.0)},status=\"ok\" #{timestamp}" end # Send entire batch at once s.puts batch.join("\n") + "\n" s.flushrescue SocketError => ex puts "Socket error: #{ex.inspect}"ensure s.close if send Comparison: InfluxDB client vs TCP socket[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#comparison-influxdb-client-vs-tcp-socket "Direct link to Comparison: InfluxDB client vs TCP socket") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Feature | InfluxDB Client | TCP Socket | | --- | --- | --- | | **Ease of use** | High - Point builder API | Medium - Manual ILP formatting | | **Dependencies** | Requires `influxdb-client` gem | None (stdlib only) | | **Error handling** | Automatic with retries | Manual implementation | | **Batching** | Automatic | Manual | | **Performance** | Good | Excellent (direct TCP) | | **Type safety** | Automatic type conversion | Manual string formatting | | **Reliability** | HTTP with acknowledgments | No acknowledgments (fire and forget) | | **Escaping** | Automatic | Manual implementation required | | **Recommended for** | Most applications | High-throughput scenarios, custom needs | Best practices[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#best-practices "Direct link to Best practices") ------------------------------------------------------------------------------------------------------------------------------------- ### Connection management[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#connection-management "Direct link to Connection management") **InfluxDB Client:** # Reuse client for multiple writesclient = InfluxDB2::Client.new(...)write_api = client.create_write_api# ... perform many writes ...client.close! # Always close when done **TCP Socket:** # Keep connection open for multiple writessocket = TCPSocket.new(HOST, PORT)begin # ... send multiple batches ...ensure socket.close if socketend ### Error handling[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#error-handling "Direct link to Error handling") **InfluxDB Client:** begin write_api.write(data: points)rescue InfluxDB2::InfluxError => e puts "Failed to write to QuestDB: #{e.message}" # Implement retry logic or loggingend **TCP Socket:** begin socket.puts(ilp_messages) socket.flushrescue Errno::EPIPE, Errno::ECONNRESET => e puts "Connection lost: #{e.message}" # Reconnect and retryrescue StandardError => e puts "Unexpected error: #{e.message}"end ### Timestamp generation[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#timestamp-generation "Direct link to Timestamp generation") Use nanosecond precision for maximum compatibility: # Current time in nanosecondsdef current_nanos now = Time.now now.to_i * 1_000_000_000 + now.nsecend# Specific time to nanosecondsdef time_to_nanos(time) time.to_i * 1_000_000_000 + time.nsecend# Usagetimestamp = current_nanos# ortimestamp = time_to_nanos(Time.parse("2024-09-05 14:30:00 UTC")) ### Batching strategy[​](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#batching-strategy "Direct link to Batching strategy") For high-throughput scenarios: BATCH_SIZE = 1000FLUSH_INTERVAL = 5 # secondsbatch = []last_flush = Time.nowdata_stream.each do |record| batch << format_ilp_message(record) if batch.size >= BATCH_SIZE || (Time.now - last_flush) >= FLUSH_INTERVAL socket.puts batch.join("\n") + "\n" socket.flush batch.clear last_flush = Time.now endend# Flush remaining recordssocket.puts batch.join("\n") + "\n" unless batch.empty? Choosing an Approach * **Use InfluxDB client** for most Ruby applications - it's easier, safer, and handles edge cases * **Use TCP sockets** only when you need maximum throughput and can handle reliability concerns Data Loss with TCP TCP ILP has no acknowledgments. If the connection drops, data may be lost silently. For critical data, use HTTP (via the InfluxDB client) which provides delivery confirmation. Related Documentation * [ILP reference](https://questdb.com/docs/ingestion/ilp/overview/) * [ILP over HTTP](https://questdb.com/docs/ingestion/ilp/overview/#transport-selection) * [ILP over TCP](https://questdb.com/docs/ingestion/ilp/overview/#transport-selection) * [InfluxDB Ruby client](https://github.com/influxdata/influxdb-client-ruby) * [Available approaches](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#available-approaches) * [Using the InfluxDB v2 Ruby client](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#using-the-influxdb-v2-ruby-client) * [Installation](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#installation) * [Example code](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#example-code) * [Configuration notes](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#configuration-notes) * [Data types](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#data-types) * [TCP socket approach](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#tcp-socket-approach) * [Basic TCP example](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#basic-tcp-example) * [ILP message format](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#ilp-message-format) * [Escaping special characters](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#escaping-special-characters) * [Batching for performance](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#batching-for-performance) * [Comparison: InfluxDB client vs TCP socket](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#comparison-influxdb-client-vs-tcp-socket) * [Best practices](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#best-practices) * [Connection management](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#connection-management) * [Error handling](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#error-handling) * [Timestamp generation](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#timestamp-generation) * [Batching strategy](https://questdb.com/docs/cookbook/programmatic/ruby/inserting-ilp/#batching-strategy) --- # Pandas | QuestDB On this page [Pandas](https://pandas.pydata.org/) is a fast, powerful, flexible, and easy-to-use open-source data analysis and manipulation tool, built on top of the Python programming language. The [QuestDB Python client](https://py-questdb-client.readthedocs.io/en/latest/index.html) provides native support for ingesting Pandas dataframes via the InfluxDB Line Protocol. Prerequisites[​](https://questdb.com/docs/integrations/data-processing/pandas/#prerequisites "Direct link to Prerequisites") ----------------------------------------------------------------------------------------------------------------------------- * QuestDB must be running and accessible. Checkout the [quick start](https://questdb.com/docs/getting-started/quick-start/) . * Python 3.8 or later * [Pandas](https://pandas.pydata.org/) * [pyarrow](https://pypi.org/project/pyarrow/) * [NumPy](https://numpy.org/) Querying vs. Ingestion[​](https://questdb.com/docs/integrations/data-processing/pandas/#querying-vs-ingestion "Direct link to Querying vs. Ingestion") ------------------------------------------------------------------------------------------------------------------------------------------------------- This page focuses on ingestion, which is the process of inserting data into QuestDB. For querying data, see [PGWire client guide](https://questdb.com/docs/query/pgwire/python/#integration-with-pandas) . Overview[​](https://questdb.com/docs/integrations/data-processing/pandas/#overview "Direct link to Overview") -------------------------------------------------------------------------------------------------------------- The QuestDB Python client implements the `dataframe()` method to transform Pandas DataFrames into QuestDB-flavored InfluxDB Line Protocol messages. The following example shows how to insert data from a Pandas DataFrame to the `trades` table: from questdb.ingress import Sender, IngressErrorimport sysimport pandas as pddef example(host: str = 'localhost', port: int = 9009): df = pd.DataFrame({ 'pair': ['USDGBP', 'EURJPY'], 'traded_price': [0.83, 142.62], 'qty': [100, 400], 'limit_price': [0.84, None], 'timestamp': [ pd.Timestamp('2022-08-06 07:35:23.189062', tz='UTC'), pd.Timestamp('2022-08-06 07:35:23.189062', tz='UTC')]}) try: with Sender(host, port) as sender: sender.dataframe( df, table_name='trades', # Table name to insert into. symbols=['pair'], # Columns to be inserted as SYMBOL types. at='timestamp') # Column containing the designated timestamps. except IngressError as e: sys.stderr.write(f'Got error: {e}\n')if __name__ == '__main__': example() See also[​](https://questdb.com/docs/integrations/data-processing/pandas/#see-also "Direct link to See also") -------------------------------------------------------------------------------------------------------------- For detailed documentation, please see: * [`Sender.dataframe()`](https://py-questdb-client.readthedocs.io/en/latest/api.html#questdb.ingress.Sender.dataframe) * [`Buffer.dataframe()`](https://py-questdb-client.readthedocs.io/en/latest/api.html#questdb.ingress.Buffer.dataframe) * [Examples using `dataframe()`](https://py-questdb-client.readthedocs.io/en/latest/examples.html#data-frames) * [Prerequisites](https://questdb.com/docs/integrations/data-processing/pandas/#prerequisites) * [Querying vs. Ingestion](https://questdb.com/docs/integrations/data-processing/pandas/#querying-vs-ingestion) * [Overview](https://questdb.com/docs/integrations/data-processing/pandas/#overview) * [See also](https://questdb.com/docs/integrations/data-processing/pandas/#see-also) --- # Import CSV with millisecond timestamps | QuestDB On this page Import CSV files containing epoch timestamps in milliseconds into QuestDB. Problem[​](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#problem "Direct link to Problem") ------------------------------------------------------------------------------------------------------------------- QuestDB expects either date/timestamp literals, or epochs in microseconds or nanoseconds. Solution options[​](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#solution-options "Direct link to Solution options") ---------------------------------------------------------------------------------------------------------------------------------------------- Here are the options available: ### Option 1: Pre-process the dataset[​](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#option-1-pre-process-the-dataset "Direct link to Option 1: Pre-process the dataset") Convert timestamps from milliseconds to microseconds before import. If importing lots of data, create Parquet files, copy them to the QuestDB import folder, and read them with `read_parquet('file.parquet')`. Then use `INSERT INTO SELECT` to copy to another table. ### Option 2: Staging table[​](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#option-2-staging-table "Direct link to Option 2: Staging table") Import into a non-partitioned table as DATE, then `INSERT INTO` a partitioned table as TIMESTAMP: -- Create staging tableCREATE TABLE trades_staging ( timestamp_ms LONG, symbol SYMBOL, price DOUBLE, amount DOUBLE);-- Import CSV to staging table (via web console or REST API)-- Create final tableCREATE TABLE trades ( timestamp TIMESTAMP, symbol SYMBOL INDEX, price DOUBLE, amount DOUBLE) TIMESTAMP(timestamp) PARTITION BY DAY;-- Convert and insertINSERT INTO tradesSELECT cast(timestamp_ms * 1000 AS TIMESTAMP) as timestamp, symbol, price, amountFROM trades_staging;-- Drop staging tableDROP TABLE trades_staging; You would be using twice the storage temporarily, but then you can drop the initial staging table. ### Option 3: ILP client[​](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#option-3-ilp-client "Direct link to Option 3: ILP client") Read the CSV line-by-line and convert, then send via the ILP client. Related Documentation * [CSV import](https://questdb.com/docs/getting-started/web-console/import-csv/) * [ILP ingestion](https://questdb.com/docs/ingestion/overview/) * [read\_parquet()](https://questdb.com/docs/query/functions/parquet/) * [Problem](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#problem) * [Solution options](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#solution-options) * [Option 1: Pre-process the dataset](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#option-1-pre-process-the-dataset) * [Option 2: Staging table](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#option-2-staging-table) * [Option 3: ILP client](https://questdb.com/docs/cookbook/operations/csv-import-milliseconds/#option-3-ilp-client) --- # Apache Airflow | QuestDB On this page Apache Airflow is a powerful workflow automation tool that allows you to schedule and monitor tasks through directed acyclic graphs (DAGs). Airflow provides built-in operators for executing SQL queries, making it easy to automate QuestDB tasks. Alternatively, checkout our [Automating QuestDB Tasks](https://questdb.com/docs/operations/task-automation/) guide for a scripted approach. Prerequisites[​](https://questdb.com/docs/integrations/orchestration/airflow/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------------- * QuestDB running locally or remotely * Docker or Python 3, depending on how you want to install Airflow * Airflow installed and configured Installation[​](https://questdb.com/docs/integrations/orchestration/airflow/#installation "Direct link to Installation") ------------------------------------------------------------------------------------------------------------------------- We recommended installing Airflow via Docker Compose, but any other supported method should also work. Follow the official guide: * [Airflow Installation Documentation](https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html) QuestDB Connection[​](https://questdb.com/docs/integrations/orchestration/airflow/#questdb-connection "Direct link to QuestDB Connection") ------------------------------------------------------------------------------------------------------------------------------------------- On the Airflow UI you can find the `Admin > Connections` option. You can create a named connection to your QuestDB instance by adding a new connection of type `Postgres`. Just point to your host (if running Airflow inside of Docker, this might be either the name of the container running QuestDB or `host.docker.internal`), port (defaults to `8812`), database (`qdb`), user (`admin`) and password (`quest`). Basic integration[​](https://questdb.com/docs/integrations/orchestration/airflow/#basic-integration "Direct link to Basic integration") ---------------------------------------------------------------------------------------------------------------------------------------- On Airflow you write a DAG, which is a graph of all the tasks you want to automate, together with its dependencies and in which order they will be executed. DAGs are written as Python files, so you can virtually integrate with any data tool, but in the case of automating QuestDB queries, the easiest way to proceed is yo use the built-in `PostgresOperator`, which accepts a connection\_id, and a query to execute. Example: Running a Query on QuestDB[​](https://questdb.com/docs/integrations/orchestration/airflow/#example-running-a-query-on-questdb "Direct link to Example: Running a Query on QuestDB") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The following example defines an Airflow DAG to execute a SQL query on QuestDB: import pendulumfrom airflow import DAGfrom airflow.providers.postgres.operators.postgres import PostgresOperatordefault_args = { 'owner': 'airflow', 'depends_on_past': False, 'start_date': pendulum.datetime(2025, 1, 1, tz="UTC"), 'email_on_failure': False, 'email_on_retry': False, 'retries': 1,}dag = DAG( 'questdb_cleanup', default_args=default_args, description='Drops old partitions in QuestDB', schedule_interval='@daily', catchup=False,)cleanup_task = PostgresOperator( task_id='drop_old_partitions', postgres_conn_id='questdb', sql=""" ALTER TABLE my_table DROP PARTITION WHERE timestamp < dateadd('d', -30, now()); """, dag=dag,) Running the Airflow DAG[​](https://questdb.com/docs/integrations/orchestration/airflow/#running-the-airflow-dag "Direct link to Running the Airflow DAG") ---------------------------------------------------------------------------------------------------------------------------------------------------------- 1. Open the Airflow UI at `http://localhost:8080`. 2. Enable and trigger the `questdb_cleanup` DAG manually. Next Steps[​](https://questdb.com/docs/integrations/orchestration/airflow/#next-steps "Direct link to Next Steps") ------------------------------------------------------------------------------------------------------------------- For further details and resources, refer to the following links: * **Airflow Documentation**: [https://airflow.apache.org/docs/apache-airflow/stable/](https://airflow.apache.org/docs/apache-airflow/stable/) * **Full Example Repository**: [https://github.com/questdb/data-orchestration-and-scheduling-samples](https://github.com/questdb/data-orchestration-and-scheduling-samples) * [Prerequisites](https://questdb.com/docs/integrations/orchestration/airflow/#prerequisites) * [Installation](https://questdb.com/docs/integrations/orchestration/airflow/#installation) * [QuestDB Connection](https://questdb.com/docs/integrations/orchestration/airflow/#questdb-connection) * [Basic integration](https://questdb.com/docs/integrations/orchestration/airflow/#basic-integration) * [Example: Running a Query on QuestDB](https://questdb.com/docs/integrations/orchestration/airflow/#example-running-a-query-on-questdb) * [Running the Airflow DAG](https://questdb.com/docs/integrations/orchestration/airflow/#running-the-airflow-dag) * [Next Steps](https://questdb.com/docs/integrations/orchestration/airflow/#next-steps) --- # Configure QuestDB with Docker Compose | QuestDB On this page You can override any QuestDB configuration parameter using environment variables in Docker Compose. This is useful for setting custom ports, authentication credentials, memory limits, and other operational settings without modifying configuration files. Environment variable format[​](https://questdb.com/docs/cookbook/operations/docker-compose-config/#environment-variable-format "Direct link to Environment variable format") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- To override configuration parameters via environment variables: 1. **Prefix with `QDB_`**: Add `QDB_` before the parameter name 2. **Capitalize**: Convert to uppercase 3. **Replace dots with underscores**: Change `.` to `_` For example: * `pg.user` becomes `QDB_PG_USER` * `pg.password` becomes `QDB_PG_PASSWORD` * `cairo.sql.copy.buffer.size` becomes `QDB_CAIRO_SQL_COPY_BUFFER_SIZE` tip Keep sensitive configuration like passwords in a `.env` file and reference them in `docker-compose.yml`: environment: - QDB_PG_PASSWORD=${QUESTDB_PASSWORD} Then create a `.env` file: QUESTDB_PASSWORD=your_secure_password Example: Custom PostgreSQL credentials[​](https://questdb.com/docs/cookbook/operations/docker-compose-config/#example-custom-postgresql-credentials "Direct link to Example: Custom PostgreSQL credentials") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- This Docker Compose file overrides the default PostgreSQL wire protocol credentials: docker-compose.yml - Override pg.user and pg.password version: "3.9"services: questdb: image: questdb/questdb container_name: custom_questdb restart: always ports: - "8812:8812" - "9000:9000" - "9009:9009" - "9003:9003" extra_hosts: - "host.docker.internal:host-gateway" environment: - QDB_PG_USER=borat - QDB_PG_PASSWORD=clever_password volumes: - ./questdb/questdb_root:/var/lib/questdb/ This configuration: * Sets PostgreSQL wire protocol username to `borat` * Sets password to `clever_password` * Persists data to `./questdb/questdb_root` on the host machine * Exposes all QuestDB ports (web console, HTTP, ILP, PostgreSQL wire) Volume Permissions If you encounter permission errors with mounted volumes, ensure the QuestDB container user has write access to the host directory. You may need to set ownership with `chown -R 1000:1000 ./questdb_root` or run the container with a specific user ID. Custom data directory permissions[​](https://questdb.com/docs/cookbook/operations/docker-compose-config/#custom-data-directory-permissions "Direct link to Custom data directory permissions") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Run with specific user/group for volume permissions services: questdb: image: questdb/questdb user: "1000:1000" environment: - QDB_CAIRO_ROOT=/var/lib/questdb volumes: - ./questdb_data:/var/lib/questdb Complete configuration reference[​](https://questdb.com/docs/cookbook/operations/docker-compose-config/#complete-configuration-reference "Direct link to Complete configuration reference") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- For a full list of available configuration parameters, see: * [Server Configuration Reference](https://questdb.com/docs/configuration/overview/) - All configurable parameters with descriptions * [Docker Deployment Guide](https://questdb.com/docs/deployment/docker/) - Docker-specific setup instructions Related Documentation * [Server Configuration](https://questdb.com/docs/configuration/overview/) * [Docker Deployment Guide](https://questdb.com/docs/deployment/docker/) * [PostgreSQL Wire Protocol](https://questdb.com/docs/query/pgwire/overview/) * [Environment variable format](https://questdb.com/docs/cookbook/operations/docker-compose-config/#environment-variable-format) * [Example: Custom PostgreSQL credentials](https://questdb.com/docs/cookbook/operations/docker-compose-config/#example-custom-postgresql-credentials) * [Custom data directory permissions](https://questdb.com/docs/cookbook/operations/docker-compose-config/#custom-data-directory-permissions) * [Complete configuration reference](https://questdb.com/docs/cookbook/operations/docker-compose-config/#complete-configuration-reference) --- # Store QuestDB metrics in QuestDB | QuestDB On this page Store QuestDB's operational metrics in QuestDB itself by scraping Prometheus metrics using Telegraf. Solution: Telegraf configuration[​](https://questdb.com/docs/cookbook/operations/store-questdb-metrics/#solution-telegraf-configuration "Direct link to Solution: Telegraf configuration") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- You could use Prometheus to scrape those metrics, but you can also use any server agent that understands the Prometheus format. It turns out Telegraf has input plugins for Prometheus and output plugins for QuestDB, so you can use it to get the metrics from the endpoint and insert them into a QuestDB table. This is a `telegraf.conf` configuration which works (using default ports): # Configuration for Telegraf agent[agent] ## Default data collection interval for all inputs interval = "5s" omit_hostname = true precision = "1ms" flush_interval = "5s"# -- INPUT PLUGINS ------------------------------------------------------ #[[inputs.prometheus]] ## An array of urls to scrape metrics from. urls = ["http://questdb-origin:9003/metrics"] url_tag="" metric_version = 2 # all entries will be on a single table ignore_timestamp = false# -- AGGREGATOR PLUGINS ------------------------------------------------- ## Merge metrics into multifield metrics by series key[[aggregators.merge]] ## If true, the original metric will be dropped by the ## aggregator and will not get sent to the output plugins. drop_original = true# -- OUTPUT PLUGINS ----------------------------------------------------- #[[outputs.socket_writer]] # Write metrics to a local QuestDB instance over TCP address = "tcp://questdb-target:9009" A few things to note: * `omit_hostname` avoids an extra column. When monitoring multiple QuestDB instances, keep it enabled. * `url_tag` is set to blank for the same reason - by default the Prometheus plugin adds the URL as an extra column. * `metric_version = 2` ensures all metrics go into a single table, rather than one table per metric. * The `aggregators.merge` plugin rolls up metrics into a single row per data point (with multiple columns), rather than one row per metric. Without it, the table becomes very sparse. * The config uses a different hostname for the QuestDB output to collect metrics on a separate instance. This is recommended for production, but for development the same host can be used. Related Documentation * [QuestDB metrics](https://questdb.com/docs/operations/logging-metrics/) * [ILP ingestion](https://questdb.com/docs/ingestion/overview/) * [Telegraf documentation](https://docs.influxdata.com/telegraf/) * [Solution: Telegraf configuration](https://questdb.com/docs/cookbook/operations/store-questdb-metrics/#solution-telegraf-configuration) --- # Query performance histogram | QuestDB On this page Create a histogram of query execution times using the `_query_trace` system table. Enable Query Tracing [Query tracing](https://questdb.com/docs/concepts/deep-dive/query-tracing/) needs to be enabled for the `_query_trace` table to be populated. Solution: Percentile-based histogram[​](https://questdb.com/docs/cookbook/operations/query-times-histogram/#solution-percentile-based-histogram "Direct link to Solution: Percentile-based histogram") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We can create a subquery that first calculates the percentiles for each bucket, in this case at 10% intervals. Then on a second query we can do a `UNION` of 10 subqueries where each is doing a `CROSS JOIN` against the calculated percentiles and finding how many queries are below the threshold for the bucket. Note in this case the histogram is cumulative, and each bucket includes the results from the smaller buckets as well. If we prefer non-cumulative, the condition would change from less than to `BETWEEN`. WITH quantiles AS ( SELECT approx_percentile(execution_micros, 0.10, 5) AS p10, approx_percentile(execution_micros, 0.20, 5) AS p20, approx_percentile(execution_micros, 0.30, 5) AS p30, approx_percentile(execution_micros, 0.40, 5) AS p40, approx_percentile(execution_micros, 0.50, 5) AS p50, approx_percentile(execution_micros, 0.60, 5) AS p60, approx_percentile(execution_micros, 0.70, 5) AS p70, approx_percentile(execution_micros, 0.80, 5) AS p80, approx_percentile(execution_micros, 0.90, 5) AS p90, approx_percentile(execution_micros, 1.0, 5) AS p100 FROM _query_trace), cumulative_hist AS (SELECT '10' AS bucket, p10 as micros_threshold, count(*) AS frequencyFROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p10UNION ALLSELECT '20', p20 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p20UNION ALLSELECT '30', p30 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p30UNION ALLSELECT '40', p40 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p40UNION ALLSELECT '50', p50 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p50UNION ALLSELECT '60', p60 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p60UNION ALLSELECT '70', p70 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p70UNION ALLSELECT '80', p80 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p80UNION ALLSELECT '90', p90 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantilesWHERE execution_micros < p90UNION ALLSELECT '100', p100 as micros_threshold, count(*)FROM _query_trace CROSS JOIN quantiles ) SELECT * FROM cumulative_hist; **Output:** "bucket","micros_threshold","frequency""10",215.0,26"20",348.0,53"30",591.0,80"40",819.0,106"50",1088.0,133"60",1527.0,160"70",2293.0,186"80",4788.0,213"90",23016.0,240"100",1078759.0,267 Related Documentation * [Query tracing](https://questdb.com/docs/concepts/deep-dive/query-tracing/) * [approx\_percentile() function](https://questdb.com/docs/query/functions/aggregation/#approx_percentile) * [Solution: Percentile-based histogram](https://questdb.com/docs/cookbook/operations/query-times-histogram/#solution-percentile-based-histogram) --- # QuestDB Flink connector | QuestDB On this page [Apache Flink](https://flink.apache.org/) is a popular framework and [stream processing](https://questdb.com/glossary/stream-processing) engine. QuestDB ships a [QuestDB Flink Sink connector](https://github.com/questdb/flink-questdb-connector) for fast ingestion from Apache Flink into QuestDB. The connector implements the [Table API and SQL](https://nightlies.apache.org/flink/flink-docs-release-1.16/docs/dev/table/overview/) for Flink. ![Apache Flink logo](https://questdb.com/docs/assets/images/flink-c07b4f947c1cde3356cdb19fa674ce56.svg) Quick start[​](https://questdb.com/docs/ingestion/message-brokers/flink/#quick-start "Direct link to Quick start") ------------------------------------------------------------------------------------------------------------------- This section shows the steps to use the QuestDB Flink connector to ingest data from Flink into QuestDB. The connector uses the SQL interface to interact with Flink. The overall steps are the followings: 1. The connector creates a table in Flink backed by QuestDB. 2. The connector inserts data into the table. 3. Finally it queries the data in QuestDB. ### Prerequisites[​](https://questdb.com/docs/ingestion/message-brokers/flink/#prerequisites "Direct link to Prerequisites") * A local JDK version 11 installation * Docker for running QuestDB ### Connector installation[​](https://questdb.com/docs/ingestion/message-brokers/flink/#connector-installation "Direct link to Connector installation") * Start the QuestDB container image: docker run -p 9000:9000 -p 9009:9009 questdb/questdb:9.3.3 * Download [Apache Flink distribution](https://flink.apache.org/downloads/) and unpack it. * [Download](https://repo1.maven.org/maven2/org/questdb/flink-questdb-connector/0.2/flink-questdb-connector-0.2.jar) the QuestDB Flink connector from Maven Central and place it in the `lib` directory of your Flink installation. * Go to the `bin` directory of your Flink installation and run the following to start a Flink cluster: ./start-cluster.sh * While still in the `bin` directory, start a Flink SQL console by running: ./sql-client.sh Then, run the following SQL command in the Flink SQL console: CREATE TABLE Orders ( order_number BIGINT, price BIGINT, buyer STRING ) WITH ( 'connector'='questdb', 'host'='localhost' ); Expected output: `[INFO] Execute statement succeed.` This command created a Flink table backed by QuestDB. The table is called `Orders` and has three columns: `order_number`, `price`, and `buyer`. The `connector` option specifies the QuestDB Flink connector. The `host` option specifies the host and port where QuestDB is running. The default port is `9009`. * While still in the Flink SQL console execute: INSERT INTO Orders values (0, 42, 'IBM'); Expected output: [INFO] SQL update statement has been successfully submitted to the cluster:Job ID: This command used Flink SQL to insert a row into the `Orders` table in Flink. The table is connected to QuestDB, so the row is also into QuestDB. * Go to the QuestDB [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) at `http://localhost:9000` and execute this query: SELECT * FROM Orders; You should see a table with one row. ![QuestDB web console screenshot with the query result](https://questdb.com/docs/assets/images/flink-questdb-console-374ad37cc8c9d0f70dbc8c6752e395bc.webp) Congratulations! You have successfully used the QuestDB Flink connector to ingest data from Flink into QuestDB. You can now build Flink data pipelines that use QuestDB as a sink. See the [QuestDB Flink connector GitHub repository](https://github.com/questdb/flink-questdb-connector/tree/main/samples) for more examples. Configuration[​](https://questdb.com/docs/ingestion/message-brokers/flink/#configuration "Direct link to Configuration") ------------------------------------------------------------------------------------------------------------------------- The QuestDB Flink connector supports the following configuration options: | Name | Type | Example | Default | Meaning | | --- | --- | --- | --- | --- | | host | `string` | localhost:9009 | N/A | Host and port where QuestDB server is running | | username | `string` | testUser1 | admin | Username for authentication. The default is used when also `token` is set. | | token | `string` | GwBXoGG5c6NoUTLXnzMxw | admin | Token for authentication | | table | `string` | my\_table | Same as Flink table name | Target table in QuestDB | | tls | `boolean` | true | false | Whether to use TLS/SSL for connecting to QuestDB server | | buffer.size.kb | `integer` | 32 | 64 | Size of the QuestDB client send buffer | | sink.parallelism | `integer` | 2 | Same as upstream processors | QuestDB Sink Parallelism | Example configuration for connecting to QuestDB running on localhost: CREATE TABLE Orders ( order_number BIGINT, price BIGINT, buyer STRING ) WITH ( 'connector'='questdb', 'host'='localhost', 'table' = 'orders'); Connector Distribution[​](https://questdb.com/docs/ingestion/message-brokers/flink/#connector-distribution "Direct link to Connector Distribution") ---------------------------------------------------------------------------------------------------------------------------------------------------- The connector is distributed as a single jar file. The jar file is available in the [Maven Central repository](https://repo1.maven.org/maven2/org/questdb/flink-questdb-connector/) and it's available under the following coordinates: org.questdb flink-questdb-connector LATEST FAQ[​](https://questdb.com/docs/ingestion/message-brokers/flink/#faq "Direct link to FAQ") ------------------------------------------------------------------------------------------- Q: Why is QuestDB client not repackaged into a different Java package? A: QuestDB client uses native code, this makes repackaging hard. Q: I need to use QuestDB as a Flink source, what should I do? A: This connector is Sink only. If you want to use QuestDB as a Source then your best chance is to use [Flink JDBC source](https://nightlies.apache.org/flink/flink-docs-release-1.15/docs/connectors/table/jdbc/) and rely on [QuestDB Postgres compatibility](https://questdb.com/docs/query/overview/#postgresql) . * [Quick start](https://questdb.com/docs/ingestion/message-brokers/flink/#quick-start) * [Prerequisites](https://questdb.com/docs/ingestion/message-brokers/flink/#prerequisites) * [Connector installation](https://questdb.com/docs/ingestion/message-brokers/flink/#connector-installation) * [Configuration](https://questdb.com/docs/ingestion/message-brokers/flink/#configuration) * [Connector Distribution](https://questdb.com/docs/ingestion/message-brokers/flink/#connector-distribution) * [FAQ](https://questdb.com/docs/ingestion/message-brokers/flink/#faq) --- # InfluxDB Line Protocol Columnset Value Types | QuestDB On this page This page lists the supported InfluxDB Line Protocol columnset value types and details about type casting. If a target column does not exist, QuestDB will create a column using the same type that the ILP client sends. Type casts that cause data loss will cause the entire line to be rejected. Integer[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#integer "Direct link to Integer") ----------------------------------------------------------------------------------------------------- 64-bit signed integer values, which correspond to QuestDB type `long`. The values are required to have `i` suffix. For example: temps,device=cpu,location=south value=96i 1638202821000000000\n Sometimes integer values are small and do not warrant 64 bits to store them. To reduce storage for such values it is possible to create a table upfront with smaller type, for example: CREATE TABLE temps (device SYMBOL, location SYMBOL, value SHORT); The line above will be accepted and `96i` will be cast to `short`. ### Cast table[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table "Direct link to Cast table") The following `cast` operations are supported when the existing table column type is not `long`: | | `byte` | `short` | `int` | `long` | `float` | `double` | `date` | `timestamp` | `decimal` | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | `integer` | cast | cast | cast | `native` | cast | cast | cast | cast | cast | Long256[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#long256 "Direct link to Long256") ----------------------------------------------------------------------------------------------------- Custom type, which corresponds to QuestDB type `long256`. The values are hex encoded 256-bit unsigned integer values with `i` suffix. For example: temps,device=cpu,location=south value=0x123a4i 1638202821000000000\n When column does not exist, it will be created with type `long256`. Values overflowing 256-bit integer will cause the entire line to be rejected. `long256` cannot be cast to anything else. Float[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#float "Direct link to Float") ----------------------------------------------------------------------------------------------- These values correspond to QuestDB type `double`. They actually do not have any suffix, which might lead to a confusion. For example: trade,ticker=BTCUSD price=30 1638202821000000000\n `price` value will be stored as `double` even though it does not look like a conventional double value would. ### Cast table[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table-1 "Direct link to Cast table") The following `cast` operations are supported when the existing table column type is not `double`: | | `float` | `double` | `decimal` | | --- | --- | --- | --- | | `float` | cast | `native` | cast | Decimal[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#decimal "Direct link to Decimal") ----------------------------------------------------------------------------------------------------- Decimal values, which correspond to QuestDB type `decimal`. The values are required to have a `d` suffix. For example: trade,ticker=BTCUSD price=30000.50d 1638202821000000000\n When the column does not exist, it will be created with the `decimal` type using the default precision of 18 and scale of 3. To specify custom precision and scale, create the table upfront: CREATE TABLE trade (ticker SYMBOL, price DECIMAL(18, 2)); The line above will be accepted and `30000.50` will be stored as `decimal`. ### Cast table[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table-2 "Direct link to Cast table") The following `cast` operations are supported when the existing table column type is not `decimal`: | | `decimal` | `float` | `double` | | --- | --- | --- | --- | | `decimal` | `native` | cast | cast | Boolean[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#boolean "Direct link to Boolean") ----------------------------------------------------------------------------------------------------- These values correspond to QuestDB type `boolean`. In InfluxDB Line Protocol `boolean` values can be represented in any of the following ways: | Actual value | Single char lowercase | Single char uppercase | Full lowercase | Full camelcase | Full uppercase | | --- | --- | --- | --- | --- | --- | | `true` | `t` | `T` | `true` | `True` | `TRUE` | | `false` | `f` | `F` | `false` | `False` | `FALSE` | Example: sensors,location=south warning=false\n ### Cast table[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table-3 "Direct link to Cast table") The following `cast` operations are supported when the existing table column type is not `boolean`: | | `boolean` | `byte` | `short` | `int` | `float` | `long` | `double` | | --- | --- | --- | --- | --- | --- | --- | --- | | `boolean` | `native` | cast | cast | cast | cast | cast | cast | When cast to numeric type, boolean `true` is `1` and `false` is `0` String[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#string "Direct link to String") -------------------------------------------------------------------------------------------------- These values correspond to QuestDB type `varchar`. They must be enclosed in quotes. The following characters in values must be escaped with a `\`: `"`, `\n`, `\r` and `\`. For example: trade,ticker=BTCUSD description="this is a \"rare\" value",user="John" 1638202821000000000\n The result: | timestamp | ticker | description | user | | --- | --- | --- | --- | | 1638202821000000000 | BTCUSD | this is a "rare" value | John | note String values must be UTF-8 encoded before sending. ### Cast table[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table-4 "Direct link to Cast table") The following `cast` operations are supported when the existing table column type is not `varchar`: | | `varchar` | `char` | `string` | `geohash` | `symbol` | `uuid` | `decimal` | | --- | --- | --- | --- | --- | --- | --- | --- | | `string` | `native` | cast | cast | cast | cast | cast | cast | ### Cast to CHAR[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-char "Direct link to Cast to CHAR") String value can be cast to `char` type if its length is less than 2 characters. The following examples are valid lines: trade,ticker=BTCUSD status="A" 1638202821000000000\ntrade,ticker=BTCUSD status="" 1638202821000000001\n The result: | timestamp | ticker | status | | --- | --- | --- | | 1638202821000000000 | BTCUSD | A | | 1638202821000000001 | BTCUSD | `null` | Casting strings with 2 or more characters to `char` will cause the entire line to be rejected. ### Cast to GEOHASH[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-geohash "Direct link to Cast to GEOHASH") String value can be cast to `geohash` type when the destination column exists and is of a `GEOHASH` type already. Do make sure that column is created upfront. Otherwise, InfluxDB Line Protocol will create `STRING` column regardless of the value. Example: Upcasting is an attempt to store higher resolution `geohash` in a lower resolution column. Let's create table before sending a message. Our `geohash` column has resolution of 4 bits. CREATE TABLE tracking ( geohash GEOHASH(4b), ts TIMESTAMP) TIMESTAMP(ts) PARTITION BY HOUR; Send message including `16c` `geohash` value: tracking,obj=VLCC\ STEPHANIE gh="9v1s8hm7wpkssv1h" 1000000000\n The result: the `geohash` value has been truncated to size of the column. | ts | gh | | --- | --- | | 1970-01-01T00:00:01.000000Z | 0100 | Sending empty string value will insert `null` into `geohash` column of any size: tracking,obj=VLCC\ STEPHANIE gh="" 2000000000\n | ts | gh | | --- | --- | | 1970-01-01T00:00:01.000000Z | `null` | note Downcast of `geohash` value, which is inserting of lower resolution values into higher resolution column, will cause the entire line to be rejected. ### Cast to SYMBOL[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-symbol "Direct link to Cast to SYMBOL") The symbol values correspond to the QuestDB type [`symbol`](https://questdb.com/docs/concepts/symbol/) . String values can be cast to the `symbol` type when the destination column exists and its type is `symbol`. This gives clients an option to populate `symbol` columns without knowing the type of the columns. CREATE TABLE trade ( ticker SYMBOL, timestamp TIMESTAMP) TIMESTAMP(ts) PARTITION BY HOUR; Send message including `BTCUSD` as `string`: trade ticker="BTCUSD" 1638202821000000000\ntrade ticker="BTCUSD" 1638402821000000000\n The `ticker` column is populated with `symbol` values: | timestamp | ticker | | --- | --- | | 2021-11-29T16:20:21.000000Z | BTCUSD | | 2021-12-01T23:53:41.000000Z | BTCUSD | We recommend sending `symbol` values directly as the `symbol` type because it will automatically create a `symbol` column if it doesn't exist. When sending `symbol` values as the `string` type and the column does not exist, then it will be created as the `string` type. ### Cast to UUID[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-uuid "Direct link to Cast to UUID") String values can be cast to the `uuid` type when all the following are true: * The destination column exists. * The destination column type is `uuid`. * The `string` values are valid UUID. CREATE TABLE trade ( ticker SYMBOL, uuid UUID, timestamp TIMESTAMP) TIMESTAMP(timestamp) PARTITION BY HOUR; Send messages including the UUID value `a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11` as `string`: trade,ticker="BTCUSD" uuid="a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" 1638202821000000000\ntrade,ticker="BTCUSD" uuid="a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" 1638402821000000000\n The `uuid` column is populated with `uuid` values: | timestamp | ticker | uuid | | --- | --- | --- | | 2021-11-29T16:20:21.000000Z | BTCUSD | a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 | | 2021-12-01T23:53:41.000000Z | BTCUSD | a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 | When the `string` value is not a valid UUID, the entire line will be rejected. ### Cast to DECIMAL[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-decimal "Direct link to Cast to DECIMAL") String values can be cast to the `decimal` type when all the following are true: * The destination column exists. * The destination column type is `decimal`. * The `string` values are valid IEEE-754 decimal values. CREATE TABLE trade ( ticker SYMBOL, price DECIMAL(18, 2), timestamp TIMESTAMP) TIMESTAMP(timestamp) PARTITION BY HOUR; Send messages including decimal values as `string`: trade,ticker="BTCUSD" price="30000.50" 1638202821000000000\ntrade,ticker="BTCUSD" price="29999.99" 1638402821000000000\n The `price` column is populated with `decimal` values: | timestamp | ticker | price | | --- | --- | --- | | 2021-11-29T16:20:21.000000Z | BTCUSD | 30000.50 | | 2021-12-01T23:53:41.000000Z | BTCUSD | 29999.99 | When the `string` value is not a valid IEEE-754 decimal value, the entire line will be rejected. Timestamp[​](https://questdb.com/docs/ingestion/ilp/columnset-types/#timestamp "Direct link to Timestamp") ----------------------------------------------------------------------------------------------------------- These values correspond to QuestDB type `timestamp`. Timestamp values are epoch `microseconds` suffixed with `t`. In this example we're populating _non-designated_ timestamp field `ts1`: tracking,obj=VLCC\ STEPHANIE gh="9v1s8hm7wpkssv1h",ts1=10000t 1000000000\n It is possible to populate _designated_ timestamp using `columnset`, although this is not recommended. Let's see how this works in practice. Assuming table: CREATE TABLE (loc SYMBOL, ts TIMESTAMP) TIMESTAMP(ts) PARTITION BY DAY; When we send: Sending mixed designated timestamp values tracking,loc=north ts=2000000000t 1000000000\ntracking,loc=south ts=3000000000t\n The `columnset` value always wins: | loc | ts | | --- | --- | | north | 2000000000 | | south | 3000000000 | * [Integer](https://questdb.com/docs/ingestion/ilp/columnset-types/#integer) * [Cast table](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table) * [Long256](https://questdb.com/docs/ingestion/ilp/columnset-types/#long256) * [Float](https://questdb.com/docs/ingestion/ilp/columnset-types/#float) * [Cast table](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table-1) * [Decimal](https://questdb.com/docs/ingestion/ilp/columnset-types/#decimal) * [Cast table](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table-2) * [Boolean](https://questdb.com/docs/ingestion/ilp/columnset-types/#boolean) * [Cast table](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table-3) * [String](https://questdb.com/docs/ingestion/ilp/columnset-types/#string) * [Cast table](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-table-4) * [Cast to CHAR](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-char) * [Cast to GEOHASH](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-geohash) * [Cast to SYMBOL](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-symbol) * [Cast to UUID](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-uuid) * [Cast to DECIMAL](https://questdb.com/docs/ingestion/ilp/columnset-types/#cast-to-decimal) * [Timestamp](https://questdb.com/docs/ingestion/ilp/columnset-types/#timestamp) --- # Apache Spark and Time-Series Analytics | QuestDB On this page High-level instructions for loading data from QuestDB to Spark and back. What is Spark?[​](https://questdb.com/docs/integrations/data-processing/spark/#what-is-spark "Direct link to What is Spark?") ------------------------------------------------------------------------------------------------------------------------------ [Apache Spark](https://spark.apache.org/) is an analytics engine for large-scale data engineering and [stream processing](https://questdb.com/glossary/stream-processing) , well-known in the big data landscape. It is suitable for executing data engineering, data science, and machine learning on single-node machines or clusters. QuestDB Spark integration[​](https://questdb.com/docs/integrations/data-processing/spark/#questdb-spark-integration "Direct link to QuestDB Spark integration") ---------------------------------------------------------------------------------------------------------------------------------------------------------------- A typical Spark application processes data in the following steps: 1. Loading data from different sources 2. Transforming and analyzing the data 3. Saving the result to a data storage Our example demonstrates these steps using QuestDB as the data source and storage. It loads data from QuestDB into a Spark Dataframe; then the data is enriched with new features, and eventually, it is written back into QuestDB. Prerequisites[​](https://questdb.com/docs/integrations/data-processing/spark/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------------- * **Package manager**: This depends on your choice of OS. The below instructions are for macOS using Homebrew. * **QuestDB**: An instance must be running and accessible. Not running? Checkout the [quick start](https://questdb.com/docs/getting-started/quick-start/) . Installing Apache Spark[​](https://questdb.com/docs/integrations/data-processing/spark/#installing-apache-spark "Direct link to Installing Apache Spark") ---------------------------------------------------------------------------------------------------------------------------------------------------------- Spark can be installed and set up in many ways, depending on requirements. Typically, it is part of a Big Data stack, installed on multiple nodes with an external cluster manager, such as [Yarn](https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html) or [Apache Mesos](https://mesos.apache.org/) . In this tutorial, we will work with a single-node standalone Spark installation. Spark has a multi-language environment. It is written in Scala, runs on the Java Virtual Machine, and also integrates with R and Python. Our example is written using Python. By running the below commands Spark will be installed with all required dependencies: brew install openjdk@11brew install python@3.10brew install scalabrew install apache-spark The exact versions used for this example: openjdk@11 11.0.12python@3.10 3.10.10_1scala 3.2.2apache-spark 3.3.2 Installing the JDBC driver[​](https://questdb.com/docs/integrations/data-processing/spark/#installing-the-jdbc-driver "Direct link to Installing the JDBC driver") ------------------------------------------------------------------------------------------------------------------------------------------------------------------- Spark communicates with QuestDB via JDBC, connecting to its Postgres Wire Protocol endpoint. This requires the Postgres JDBC driver to be present. * Create a working directory: mkdir sparktestcd sparktest * Download the JDBC driver from [here](https://jdbc.postgresql.org/download/) into the working directory. The exact version used for this example: postgresql-42.5.1.jar Setting up database tables[​](https://questdb.com/docs/integrations/data-processing/spark/#setting-up-database-tables "Direct link to Setting up database tables") ------------------------------------------------------------------------------------------------------------------------------------------------------------------- First, start QuestDB. If you are using Docker run the following command: docker run -p 9000:9000 -p 8812:8812 questdb/questdb:9.3.3 The port mappings allow us to connect to QuestDB's REST and PostgreSQL Wire Protocol endpoints. The former is required for opening the Web Console, and the latter is used by Spark to connect to the database. Open the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) in your browser at `http://localhost:9000`. Run the following SQL commands using the console: CREATE TABLE trades ( symbol SYMBOL, side SYMBOL, price DOUBLE, amount DOUBLE, timestamp TIMESTAMP) timestamp (timestamp) PARTITION BY DAY;CREATE TABLE trades_enriched ( symbol SYMBOL, volume DOUBLE, mid DOUBLE, ts TIMESTAMP, ma10 DOUBLE, std DOUBLE) timestamp (ts) PARTITION BY DAY;INSERT INTO trades SELECT * FROM ( SELECT 'BTC-USD' symbol, rnd_symbol('buy', 'sell') side, rnd_double() * 10000 price, rnd_double() amount, timestamp_sequence(1677628800000000, 10000000) ts FROM long_sequence(25920)) timestamp (ts); The `INSERT` command generates 3 days' worth of test data, and stores it in the `trades` table. Feature engineering examples[​](https://questdb.com/docs/integrations/data-processing/spark/#feature-engineering-examples "Direct link to Feature engineering examples") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Save the below Python code into a file called `sparktest.py` inside the working directory: from pyspark.sql import SparkSessionfrom pyspark.sql.window import Windowfrom pyspark.sql.functions import avg, stddev, when# create Spark sessionspark = SparkSession.builder.appName("questdb_test").getOrCreate()# load 1-minute aggregated trade data into the dataframedf = spark.read.format("jdbc") \ .option("url", "jdbc:postgresql://localhost:8812/questdb") \ .option("driver", "org.postgresql.Driver") \ .option("user", "admin").option("password", "quest") \ .option("dbtable", "(SELECT symbol, sum(amount) as volume, " "round((max(price)+min(price))/2, 2) as mid, " "timestamp as ts " "FROM trades WHERE symbol = 'BTC-USD' " "SAMPLE BY 1m ALIGN to CALENDAR) AS mid_prices") \ .option("partitionColumn", "ts") \ .option("numPartitions", "3") \ .option("lowerBound", "2023-03-01T00:00:00.000000Z") \ .option("upperBound", "2023-03-04T00:00:00.000000Z") \ .load()# extract new features, clean datawindow_10 = Window.partitionBy(df.symbol).rowsBetween(-10, Window.currentRow)df = df.withColumn("ma10", avg(df.mid).over(window_10))df = df.withColumn("std", stddev(df.mid).over(window_10))df = df.withColumn("std", when(df.std.isNull(), 0.0).otherwise(df.std))# save the data as 'trades_enriched', overwrite if already existsdf.write.format("jdbc") \ .option("url", "jdbc:postgresql://localhost:8812/questdb") \ .option("driver", "org.postgresql.Driver") \ .option("user", "admin").option("password", "quest") \ .option("dbtable", "trades_enriched") \ .option("truncate", True) \ .option("createTableColumnTypes", "volume DOUBLE, mid DOUBLE, ma10 DOUBLE, std DOUBLE") \ .save(mode="overwrite") This Spark application loads aggregated data from the `trades` table into a Dataframe, then adds two new features, a 10-minute moving average and the standard deviation. Finally, it writes the enriched data back into QuestDB and saves it to the `trades_enriched` table. Run the example[​](https://questdb.com/docs/integrations/data-processing/spark/#run-the-example "Direct link to Run the example") ---------------------------------------------------------------------------------------------------------------------------------- Submit the application to Spark for execution using `spark-submit`: spark-submit --jars postgresql-42.5.1.jar sparktest.py The example requires the JDBC driver at runtime. This dependency is submitted to Spark using the `--jars` option. After the execution is completed, we can check the content of the `trades_enriched` table: SELECT * FROM trades_enriched; The enriched data should be displayed in the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) . See also[​](https://questdb.com/docs/integrations/data-processing/spark/#see-also "Direct link to See also") ------------------------------------------------------------------------------------------------------------- For a more detailed explanation of the QuestDB Spark integration, please also see our tutorial [Integrate Apache Spark and QuestDB for Time-Series Analytics](https://questdb.com/blog/integrate-apache-spark-questdb-time-series-analytics/#loading-data-to-spark/) . * [What is Spark?](https://questdb.com/docs/integrations/data-processing/spark/#what-is-spark) * [QuestDB Spark integration](https://questdb.com/docs/integrations/data-processing/spark/#questdb-spark-integration) * [Prerequisites](https://questdb.com/docs/integrations/data-processing/spark/#prerequisites) * [Installing Apache Spark](https://questdb.com/docs/integrations/data-processing/spark/#installing-apache-spark) * [Installing the JDBC driver](https://questdb.com/docs/integrations/data-processing/spark/#installing-the-jdbc-driver) * [Setting up database tables](https://questdb.com/docs/integrations/data-processing/spark/#setting-up-database-tables) * [Feature engineering examples](https://questdb.com/docs/integrations/data-processing/spark/#feature-engineering-examples) * [Run the example](https://questdb.com/docs/integrations/data-processing/spark/#run-the-example) * [See also](https://questdb.com/docs/integrations/data-processing/spark/#see-also) --- # Last look detection | QuestDB On this page In FX markets, some liquidity providers operate under a **last look** window — a brief period (typically 1–100ms) after receiving an order during which they can reject or re-price the trade. While last look is a legitimate risk management practice (allowing LPs to verify that prices haven't moved during order transit), it can be exploited through asymmetric rejection — accepting trades only when the price has moved in the LP's favor during the hold window, and rejecting when it hasn't. This recipe uses millisecond-granularity [markout analysis](https://questdb.com/docs/cookbook/sql/finance/markout/) to detect whether specific counterparties are exploiting last look. The signature is a sharp price movement against you in the first few milliseconds after a fill — if the mid-price consistently moves in the counterparty's favor within their last-look window, they may be selectively accepting only trades that benefit them. Problem[​](https://questdb.com/docs/cookbook/sql/finance/last-look/#problem "Direct link to Problem") ------------------------------------------------------------------------------------------------------ You want to detect whether specific counterparties show signs of last-look adverse selection. You need markout measurements at millisecond resolution — much finer than the second-level analysis in the [general markout recipe](https://questdb.com/docs/cookbook/sql/finance/markout/) — to catch behavior that happens within typical last-look windows (1–100ms). Solution[​](https://questdb.com/docs/cookbook/sql/finance/last-look/#solution "Direct link to Solution") --------------------------------------------------------------------------------------------------------- Use `HORIZON JOIN` with a `LIST` of millisecond-spaced offsets to build a high-resolution markout curve for the first few seconds after each fill: Millisecond-granularity markout by counterparty[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.counterparty%2C%0A%20%20%20%20t.passive%2C%0A%20%20%20%20h.offset%20%2F%201000000%20AS%20horizon_ms%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg(%0A%20%20%20%20%20%20%20%20CASE%20t.side%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27buy%27%20%20THEN%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27sell%27%20THEN%20(t.price%20-%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20END%0A%20%20%20%20)%20AS%20avg_markout_bps%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20LIST%20(0%2C%201T%2C%205T%2C%2010T%2C%2050T%2C%20100T%2C%0A%20%20%20%20%20%20%20%20%20%20500T%2C%201000T%2C%205000T)%20AS%20h%0AWHERE%20t.timestamp%20IN%20%27%24yesterday%27%0AGROUP%20BY%20t.symbol%2C%20t.counterparty%2C%20t.passive%2C%20horizon_ms%0AORDER%20BY%20t.symbol%2C%20t.counterparty%2C%20horizon_ms%3B&executeQuery=true) SELECT t.symbol, t.counterparty, t.passive, h.offset / 1000000 AS horizon_ms, count() AS n, avg( CASE t.side WHEN 'buy' THEN ((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000 WHEN 'sell' THEN (t.price - (m.best_bid + m.best_ask) / 2) / t.price * 10000 END ) AS avg_markout_bpsFROM fx_trades tHORIZON JOIN market_data m ON (symbol) LIST (0, 1T, 5T, 10T, 50T, 100T, 500T, 1000T, 5000T) AS hWHERE t.timestamp IN '$yesterday'GROUP BY t.symbol, t.counterparty, t.passive, horizon_msORDER BY t.symbol, t.counterparty, horizon_ms; The `LIST` offsets are: 0ms, 1ms, 5ms, 10ms, 50ms, 100ms, 500ms, 1s, and 5s — concentrated in the sub-100ms range where last-look behavior is visible. h.offset resolution Since `fx_trades` uses nanosecond timestamps (`TIMESTAMP_NS`), `h.offset` is in nanoseconds. Dividing by 1,000,000 converts to milliseconds for readability. How it works[​](https://questdb.com/docs/cookbook/sql/finance/last-look/#how-it-works "Direct link to How it works") --------------------------------------------------------------------------------------------------------------------- The key difference from the [general markout recipe](https://questdb.com/docs/cookbook/sql/finance/markout/) is the time scale. Instead of uniform 1-second steps over minutes, this uses non-uniform `LIST` offsets clustered in the millisecond range where last-look decisions happen. The `LIST` syntax is ideal here because the offsets are non-uniform — dense at the start (1ms, 5ms, 10ms) where you need precision, and sparse further out (1s, 5s) for context. Interpreting results[​](https://questdb.com/docs/cookbook/sql/finance/last-look/#interpreting-results "Direct link to Interpreting results") --------------------------------------------------------------------------------------------------------------------------------------------- Compare the markout curve across counterparties at the same symbol: * **Neutral counterparty**: Markout near zero at 0ms, with gradual random drift. No systematic pattern. * **Last-look adverse selection**: Sharp negative markout in the 1–100ms range that stabilizes or worsens. The counterparty is filling you only when the market is about to move against you. * **Last-look with reversion**: Negative markout spike at 5–50ms that then reverts toward zero by 1–5s. This suggests the counterparty rejects trades when the price would move in your favor, but the moves are temporary. * **Passive vs aggressive**: Last-look behavior primarily affects aggressive orders (taker flow). Passive fills from the same counterparty may show a different pattern. ### What to look for[​](https://questdb.com/docs/cookbook/sql/finance/last-look/#what-to-look-for "Direct link to What to look for") A counterparty is likely using last look adversely if: 1. **Markout drops sharply in 1–50ms** — faster than you can react 2. **The drop is counterparty-specific** — other counterparties at the same symbol don't show it 3. **The pattern is persistent** — it appears consistently across days, not just in isolated events 4. **Passive fills are unaffected** — the behavior targets your aggressive flow specifically Related documentation * [HORIZON JOIN](https://questdb.com/docs/query/sql/horizon-join/) * [Markout analysis recipe](https://questdb.com/docs/cookbook/sql/finance/markout/) * [Slippage per fill recipe](https://questdb.com/docs/cookbook/sql/finance/slippage/) * [Problem](https://questdb.com/docs/cookbook/sql/finance/last-look/#problem) * [Solution](https://questdb.com/docs/cookbook/sql/finance/last-look/#solution) * [How it works](https://questdb.com/docs/cookbook/sql/finance/last-look/#how-it-works) * [Interpreting results](https://questdb.com/docs/cookbook/sql/finance/last-look/#interpreting-results) * [What to look for](https://questdb.com/docs/cookbook/sql/finance/last-look/#what-to-look-for) --- # Telegraf | QuestDB On this page [Telegraf](https://docs.influxdata.com/telegraf/v1.17/) is a client for collecting metrics from many inputs and has support for sending it on to various outputs. It is plugin-driven for the collection and delivery of data, so it is easily configurable and customizable. Telegraf is compiled as a standalone binary, which means there are no external dependencies required to manage. QuestDB supports ingesting from Telegraf via the InfluxDB Line Protocol. This page provides examples for collecting CPU and memory usage metrics using Telegraf and sends these metrics to a locally-running QuestDB instance for querying and visualization. Prerequisites[​](https://questdb.com/docs/ingestion/message-brokers/telegraf/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------------- * **QuestDB** must be running and accessible. Checkout the [quick start](https://questdb.com/docs/getting-started/quick-start/) . * **Telegraf** can be installed using [homebrew](https://formulae.brew.sh/formula/telegraf) , [docker](https://hub.docker.com/_/telegraf) , or directly as a binary. For more details, refer to the official Telegraf [installation instructions](https://docs.influxdata.com/telegraf/v1.17/introduction/installation/) . Configuring Telegraf[​](https://questdb.com/docs/ingestion/message-brokers/telegraf/#configuring-telegraf "Direct link to Configuring Telegraf") ------------------------------------------------------------------------------------------------------------------------------------------------- As Telegraf is a plugin-driven agent, the configuration file provided when Telegraf is launched will determine which metrics to collect, if and how processing of the metrics should be performed, and the destination outputs. The default location that Telegraf can pick up configuration files is `/usr/local/etc/` on macOS and `/etc/telegraf/` on Linux. After installation, default configuration files are in the following locations: * Homebrew install: `/usr/local/etc/telegraf.conf` * Linux, Deb and RPM: `/etc/telegraf/telegraf.conf` Full configuration files for writing are provided below and can be placed in these directories and picked up by Telegraf. To view a comprehensive configuration file with example inputs and outputs, the following command can generate an example: telegraf -sample-config > example.conf ### Example Inputs[​](https://questdb.com/docs/ingestion/message-brokers/telegraf/#example-inputs "Direct link to Example Inputs") The examples on this page will use input plugins that read CPU and memory usage statistics of the host machine and send this to the outputs specified in the configuration file. The following snippet includes code comments which describe the inputs in more detail: Example inputs sending host data to QuestDB ...# -- INPUT PLUGINS -- #[[inputs.cpu]] # Read metrics about cpu usage ## Whether to report per-cpu stats or not percpu = true ## Whether to report total system cpu stats or not totalcpu = true ## If true, collect raw CPU time metrics collect_cpu_time = false ## If true, compute and report the sum of all non-idle CPU states report_active = false# Read metrics about memory usage[[inputs.mem]] # no customisation Writing to QuestDB over HTTP[​](https://questdb.com/docs/ingestion/message-brokers/telegraf/#writing-to-questdb-over-http "Direct link to Writing to QuestDB over HTTP") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB expects Influx Line Protocol messages over HTTP on port `9000`. To change the default port, see the [HTTP server configuration](https://questdb.com/docs/configuration/overview/#http-server) section of the server configuration page. Create a new file named `questdb.conf` in one of the locations Telegraf can load configuration files from and paste the following example: /path/to/telegraf/config/questdb.conf # Configuration for Telegraf agent[agent] ## Default data collection interval for all inputs interval = "5s" hostname = "qdb"# -- OUTPUT PLUGINS -- #[[outputs.influxdb_v2]]# Use InfluxDB Line Protocol to write metrics to QuestDB urls = ["http://localhost:9000"]# Disable gzip compression content_encoding = "identity"# -- INPUT PLUGINS -- #[[inputs.cpu]] percpu = true totalcpu = true collect_cpu_time = false report_active = false[[inputs.mem]] # no customisation Optionally, we recommend applying an aggregator plugin. The InfluxDB Line Protocol default in many cases will lead to data in the form of multiple, fairly sparse rows. QuestDB prefers rows that are **"more dense"**. To that end, the aggregator plugin takes all the metrics for the same tag - equivalent to a symbol - and the timestamp. It then outputs them into single row. If metrics are arriving in the usual ILP style with a metric per tag, the aggregator plugin will instead roll them into a more "dense" row as desired. /path/to/telegraf/config/questdb.conf - Aggregator plugin # -- AGGREGATOR PLUGINS ------------------------------------------------- ## Merge metrics into multifield metrics by series key[[aggregators.merge]] ## If true, the original metric will be dropped by the ## aggregator and will not get sent to the output plugins. drop_original = true Run Telegraf and specify the configuration file with the QuestDB output: telegraf --config questdb.conf Telegraf should report the following if configured correctly: 2021-01-29T12:11:32Z I! Loaded inputs: cpu mem2021-01-29T12:11:32Z I! Loaded aggregators:2021-01-29T12:11:32Z I! Loaded processors:2021-01-29T12:11:32Z I! Loaded outputs: influxdb_v2... Verifying the integration[​](https://questdb.com/docs/ingestion/message-brokers/telegraf/#verifying-the-integration "Direct link to Verifying the integration") ---------------------------------------------------------------------------------------------------------------------------------------------------------------- 1. Navigate to the QuestDB [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) at `http://127.0.0.1:9000/`. The Schema Navigator in the top left should display two new tables: * `cpu` generated from `inputs.cpu` * `mem` generated from `inputs.mem` 2. Type `cpu` in the query editor and click **RUN** The `cpu` table will have a column for each metric collected by the Telegraf plugin for monitoring memory: ![Querying CPU metrics using the QuestDB Web Console](https://questdb.com/docs/images/docs/telegraf/select_from_cpu.webp) ### Graphing system CPU[​](https://questdb.com/docs/ingestion/message-brokers/telegraf/#graphing-system-cpu "Direct link to Graphing system CPU") To create a graph that visualizes CPU usage over time, run the following example query: SELECTavg(usage_system) cpu_average,max(usage_system) cpu_max,timestampFROM cpu SAMPLE BY 1m; Select the **Chart** tab and set the following values: * Chart type **line** * Labels **timestamp** * Series **cpu\_average** and **cpu\_max** ![Graphing CPU metrics using the QuestDB Web Console](https://questdb.com/docs/images/docs/telegraf/cpu_stats_chart.webp) * [Prerequisites](https://questdb.com/docs/ingestion/message-brokers/telegraf/#prerequisites) * [Configuring Telegraf](https://questdb.com/docs/ingestion/message-brokers/telegraf/#configuring-telegraf) * [Example Inputs](https://questdb.com/docs/ingestion/message-brokers/telegraf/#example-inputs) * [Writing to QuestDB over HTTP](https://questdb.com/docs/ingestion/message-brokers/telegraf/#writing-to-questdb-over-http) * [Verifying the integration](https://questdb.com/docs/ingestion/message-brokers/telegraf/#verifying-the-integration) * [Graphing system CPU](https://questdb.com/docs/ingestion/message-brokers/telegraf/#graphing-system-cpu) --- # Polars | QuestDB On this page [Polars](https://pola.rs/) is a fast DataFrame library implemented in Rust and Python. It is designed to process large datasets efficiently and is particularly well-suited for time-series data. Polars provides a DataFrame API similar to Pandas, but with a focus on performance and parallel execution. Overview[​](https://questdb.com/docs/integrations/data-processing/polars/#overview "Direct link to Overview") -------------------------------------------------------------------------------------------------------------- ConnectorX is a Rust library that provides fast data transfer between Python and various databases, including QuestDB. It includes a connector for PostgreSQL which is compatible with QuestDB's PGWire protocol. This allows you to use ConnectorX to read data from QuestDB into a Polars DataFrame. caution **Note**: By default ConnectorX for PostgreSQL uses features not supported by QuestDB. If you instruct ConnectorX to use the Redshift protocol, it will work with QuestDB. Prerequisites[​](https://questdb.com/docs/integrations/data-processing/polars/#prerequisites "Direct link to Prerequisites") ----------------------------------------------------------------------------------------------------------------------------- * QuestDB must be running and accessible. Checkout the [quick start](https://questdb.com/docs/getting-started/quick-start/) . * Python 3.8 or later * [Polars](https://pola.rs/) * [pyarrow](https://pypi.org/project/pyarrow/) * [ConnectorX](https://sfu-db.github.io/connector-x/intro.html) pip install polars pyarrow connectorx Example[​](https://questdb.com/docs/integrations/data-processing/polars/#example "Direct link to Example") ----------------------------------------------------------------------------------------------------------- import polars as plQUESTDB_URI = "redshift://admin:quest@localhost:8812/qdb"QUERY = "SELECT * FROM tables() LIMIT 5;"df = pl.read_database_uri(query=QUERY, uri=QUESTDB_URI)print("Received DataFrame:")print(df) Note that the URL uses the `redshift` schema. This is important because it makes ConnectorX to avoid using features not supported by QuestDB. Ingestion vs Querying[​](https://questdb.com/docs/integrations/data-processing/polars/#ingestion-vs-querying "Direct link to Ingestion vs Querying") ----------------------------------------------------------------------------------------------------------------------------------------------------- This guides deals with querying data from QuestDB using Polars. For ingestion to QuestDB we recommend using the [QuestDB Python client](https://questdb.com/docs/ingestion/clients/python/) . Additional Resources[​](https://questdb.com/docs/integrations/data-processing/polars/#additional-resources "Direct link to Additional Resources") -------------------------------------------------------------------------------------------------------------------------------------------------- * [Integration with Pandas](https://questdb.com/docs/integrations/data-processing/pandas/) * [QuestDB Client for fast ingestion](https://questdb.com/docs/ingestion/clients/python/) * [Python clients guide](https://questdb.com/docs/query/pgwire/python/) * [Overview](https://questdb.com/docs/integrations/data-processing/polars/#overview) * [Prerequisites](https://questdb.com/docs/integrations/data-processing/polars/#prerequisites) * [Example](https://questdb.com/docs/integrations/data-processing/polars/#example) * [Ingestion vs Querying](https://questdb.com/docs/integrations/data-processing/polars/#ingestion-vs-querying) * [Additional Resources](https://questdb.com/docs/integrations/data-processing/polars/#additional-resources) --- # Java (embedded) | QuestDB On this page QuestDB is written in Java and can be used as any other Java library. Moreover, it is a single JAR with no additional dependencies. To include QuestDB in your project, use the following: * Maven * Gradle JDK11 org.questdbquestdb9.3.3 JDK11 implementation 'org.questdb:questdb:9.3.3' Writing data[​](https://questdb.com/docs/ingestion/java-embedded/#writing-data "Direct link to Writing data") -------------------------------------------------------------------------------------------------------------- This section provides example codes to write data to WAL and non-WAL tables. See [Write Ahead Log](https://questdb.com/docs/concepts/write-ahead-log/) for details about the differences between WAL and non-WAL tables. The following writers are available for data ingestion: * `WalWriter` for WAL tables * `TableWriter` for non-WAL tables * `TableWriterAPI` for both WAL and non-WAL tables as it is an interface for `WalWriter` and `Table Writer` ### Writing data using `WalWriter`[​](https://questdb.com/docs/ingestion/java-embedded/#writing-data-using-walwriter "Direct link to writing-data-using-walwriter") The `WalWriter` facilitates table writes to WAL tables. To successfully create an instance of `WalWriter`, the table must already exist. Example WalWriter final CairoConfiguration configuration = new DefaultCairoConfiguration("data_dir");try (CairoEngine engine = new CairoEngine(configuration)) { final SqlExecutionContext ctx = new SqlExecutionContextImpl(engine, 1) .with(AllowAllSecurityContext.INSTANCE, null); engine.ddl("CREATE TABLE testTable (" + "a int, b byte, c short, d long, e float, g double, h date, " + "i symbol, j string, k boolean, l geohash(8c), ts timestamp" + ") TIMESTAMP(ts) PARTITION BY DAY WAL", ctx); // write data into WAL final TableToken tableToken = engine.getTableTokenIfExists("testTable"); try (WalWriter writer = engine.getWalWriter(tableToken)) { for (int i = 0; i < 3; i++) { TableWriter.Row row = writer.newRow(Os.currentTimeMicros()); row.putInt(0, 123); row.putByte(1, (byte) 1111); row.putShort(2, (short) 222); row.putLong(3, 333); row.putFloat(4, 4.44f); row.putDouble(5, 5.55); row.putDate(6, System.currentTimeMillis()); row.putSym(7, "xyz"); row.putStr(8, "abc"); row.putBool(9, true); row.putGeoHash(10, GeoHashes.fromString("u33dr01d", 0, 8)); row.append(); } writer.commit(); } // apply WAL to the table try (ApplyWal2TableJob walApplyJob = new ApplyWal2TableJob(engine, 1, 1)) { while (walApplyJob.run(0)) ; }} ### Writing data using `TableWriter`[​](https://questdb.com/docs/ingestion/java-embedded/#writing-data-using-tablewriter "Direct link to writing-data-using-tablewriter") Non-WAL tables do not allow concurrent writes via multiple interfaces. To successfully create an instance, the table must: * Already exist * Have no other open writers against it as the `TableWriter` constructor will attempt to obtain an exclusive cross-process lock on the table. Example TableWriter try (CairoEngine engine = new CairoEngine(configuration)) { final SqlExecutionContext ctx = new SqlExecutionContextImpl(engine, 1) .with(AllowAllSecurityContext.INSTANCE, null); engine.ddl("CREATE TABLE testTable (" + "a int, b byte, c short, d long, e float, g double, h date, " + "i symbol, j string, k boolean, l geohash(8c), ts timestamp" + ") TIMESTAMP(ts) PARTITION BY DAY BYPASS WAL", ctx); // write data into WAL final TableToken tableToken = engine.getTableTokenIfExists("testTable"); try (TableWriter writer = engine.getWriter(tableToken, "test")) { for (int i = 0; i < 3; i++) { TableWriter.Row row = writer.newRow(Os.currentTimeMicros()); row.putInt(0, 123); row.putByte(1, (byte) 1111); row.putShort(2, (short) 222); row.putLong(3, 333); row.putFloat(4, 4.44f); row.putDouble(5, 5.55); row.putDate(6, System.currentTimeMillis()); row.putSym(7, "xyz"); row.putStr(8, "abc"); row.putBool(9, true); row.putGeoHash(10, GeoHashes.fromString("u33dr01d", 0, 8)); row.append(); } writer.commit(); }} ### Writing data using `TableWriterAPI`[​](https://questdb.com/docs/ingestion/java-embedded/#writing-data-using-tablewriterapi "Direct link to writing-data-using-tablewriterapi") `TableWriterAPI` allows writing to both WAL and non-WAL tables by returning the suitable `Writer` based on the table configurations. The table must already exist: Example TableWriterAPI try (CairoEngine engine = new CairoEngine(configuration)) { final SqlExecutionContext ctx = new SqlExecutionContextImpl(engine, 1) .with(AllowAllSecurityContext.INSTANCE, null); engine.ddl("CREATE TABLE testTable (" + "a int, b byte, c short, d long, e float, g double, h date, " + "i symbol, j string, k boolean, l geohash(8c), ts timestamp" + ") TIMESTAMP(ts) PARTITION BY DAY WAL", ctx); // write data into WAL final TableToken tableToken = engine.getTableTokenIfExists("testTable"); try (TableWriterAPI writer = engine.getTableWriterAPI(tableToken, "test")) { for (int i = 0; i < 3; i++) { TableWriter.Row row = writer.newRow(Os.currentTimeMicros()); row.putInt(0, 123); row.putByte(1, (byte) 1111); row.putShort(2, (short) 222); row.putLong(3, 333); row.putFloat(4, 4.44f); row.putDouble(5, 5.55); row.putDate(6, System.currentTimeMillis()); row.putSym(7, "xyz"); row.putStr(8, "abc"); row.putBool(9, true); row.putGeoHash(10, GeoHashes.fromString("u33dr01d", 0, 8)); row.append(); } writer.commit(); } // apply WAL to the table try (ApplyWal2TableJob walApplyJob = new ApplyWal2TableJob(engine, 1, 1)) { while (walApplyJob.run(0)) ; }} ### Detailed steps[​](https://questdb.com/docs/ingestion/java-embedded/#detailed-steps "Direct link to Detailed steps") #### Configure Cairo engine[​](https://questdb.com/docs/ingestion/java-embedded/#configure-cairo-engine "Direct link to Configure Cairo engine") `CairoEngine` is the resource manager for the embedded QuestDB. Its main function is to facilitate concurrent access to pools of `TableReader` and suitable writer instances. New CairoEngine instance final CairoConfiguration configuration = new DefaultCairoConfiguration("data_dir");try (CairoEngine engine = new CairoEngine(configuration)) { A typical application will need only one instance of `CairoEngine`. This instance will start when the application starts and shuts down when the application closes. You will need to close `CairoEngine` gracefully when the application stops. QuestDB provides a default configuration which only requires the data directory to be specified. For a more advanced usage, the whole `CairoConfiguration` interface can be overridden. #### Create an instance of SqlExecutionContext[​](https://questdb.com/docs/ingestion/java-embedded/#create-an-instance-of-sqlexecutioncontext "Direct link to Create an instance of SqlExecutionContext") Execution context is a conduit for passing SQL execution artifacts to the execution site. This instance is not thread-safe and it must not be shared between threads. Example of execution context final SqlExecutionContext ctx = new SqlExecutionContextImpl(engine, 1) .with(AllowAllSecurityContext.INSTANCE, null); The second argument of the constructor is the number of threads that will be helping to execute SQL statements. Unless you are building another QuestDB server, this value should always be 1. #### SqlCompiler and blank table[​](https://questdb.com/docs/ingestion/java-embedded/#sqlcompiler-and-blank-table "Direct link to SqlCompiler and blank table") Before we start writing data using a writer, the target table has to exist. There are several ways to create a new table and we recommend using `CairoEngine`: Creating new table // Create a non-WAL table:engine.ddl("CREATE TABLE abc (" + "a int, b byte, c short, d long, e float, g double, h date, " + "i symbol, j string, k boolean, l geohash(8c), ts timestamp" + ") TIMESTAMP(ts) PARTITION BY DAY BYPASS WAL", ctx);// Create a WAL table:engine.ddl("CREATE TABLE abc (" + "a int, b byte, c short, d long, e float, g double, h date, " + "i symbol, j string, k boolean, l geohash(8c), ts timestamp" + ") TIMESTAMP(ts) PARTITION BY DAY WAL", ctx); As you will be able to see below, the table field types and indexes must match the code that is populating the table. Another way to create a table is to obtain a `SqlCompiler` from the engine and use it to run the DDL statement: Creating new table with a SqlCompiler try (SqlCompiler compiler = engine.getSqlCompiler()) { engine.ddl(compiler, "CREATE TABLE abc (" + "a int, b byte, c short, d long, e float, g double, h date, " + "i symbol, j string, k boolean, l geohash(8c), ts timestamp" + ") TIMESTAMP(ts) PARTITION BY DAY WAL", ctx, null);} This way the obtained `SqlCompiler` can be reused to run other SQL statements. Note that `CairoEngine` has a number of helper methods for different types of SQL statements. These are: * `CairoEngine#ddl()` - meant to execute CREATE TABLE and ALTER statements. * `CairoEngine#insert()` - meant to execute INSERT statements. * `CairoEngine#drop()` - meant to execute DROP TABLE statements. * `CairoEngine#select()` - meant to execute SELECT queries. #### A new writer instance[​](https://questdb.com/docs/ingestion/java-embedded/#a-new-writer-instance "Direct link to A new writer instance") We use `CairoEngine` to obtain an instance of the writer. This will enable reusing this writer instance later, when we use the same method of creating table writer again. New table writer instance for a non-WAL table final TableToken tableToken = engine.getTableTokenIfExists("abc");try (TableWriter writer = engine.getWriter(tableToken, "test")) { New table writer instance for a WAL table final TableToken tableToken = engine.getTableTokenIfExists("abc");try (WalWriter writer = engine.getWalWriter(tableToken)) { New table writer instance for either a WAL or non-WAL table final TableToken tableToken = engine.getTableTokenIfExists("abc");try (TableWriterAPI writer = engine.getTableWriterAPI(tableToken, "test")) { `TableWriter` - A non-WAL table uses `TableWriter`, which will hold an exclusive lock on table `abc` until it is closed and `testing` will be used as the lock reason. This lock is both intra- and inter-process. If you have two Java applications accessing the same table only one will succeed at one time. `WalWriter` - A WAL table uses `WalWriter` to enable concurrent data ingestion, data modification, and schema changes, as the table is not locked. `TableWriterAPI` - Both WAL and Non-WAL tables can use `TableWriterAPI`. It is an interface implemented by both writers. Note the all of the writer classes are not thread-safe, so they should not be used concurrently. #### Create a new row[​](https://questdb.com/docs/ingestion/java-embedded/#create-a-new-row "Direct link to Create a new row") Creating new table row with timestamp TableWriter.Row row = writer.newRow(Os.currentTimeMicros()); Although this operation semantically looks like a new object creation, the row instance is actually being re-used under the hood. A timestamp is necessary to determine a partition for the new row. Its value has to be either increment or stay the same as the last row. When the table is not partitioned and does not have a designated timestamp column, the timestamp value can be omitted. Creating new table row without timestamp TableWriter.Row row = writer.newRow(); #### Populate columns[​](https://questdb.com/docs/ingestion/java-embedded/#populate-columns "Direct link to Populate columns") There are `put*` methods for every supported data type. Columns are updated by an index as opposed to by name. Populating table column row.putLong(3, 333); Column update order is not important and updates can be sparse. All unset columns will default to NULL values. #### Append a row[​](https://questdb.com/docs/ingestion/java-embedded/#append-a-row "Direct link to Append a row") Following method call: Appending a new row row.append(); Appended rows are not visible to readers until they are committed. An unneeded row can also be canceled if required. Cancelling half-populated row row.cancel(); A pending row is automatically cancelled when `writer.newRow()` is called. Consider the following scenario: TableWriter.Row row = writer.newRow(Os.currentTimeMicros());row.putInt(0, 123);row.putByte(1, (byte) 1111);row.putShort(2, (short) 222);row.putLong(3, 333);row = writer.newRow(Os.currentTimeMicros());... Second `newRow()` call would cancel all the updates to the row since the last `append()`. #### Commit changes[​](https://questdb.com/docs/ingestion/java-embedded/#commit-changes "Direct link to Commit changes") To make changes visible to readers, writer has to commit. `writer.commit` does this job. Unlike traditional SQL databases, the size of the transaction does not matter. You can commit anything between 1 and 1 trillion rows. We also spent considerable effort to ensure `commit()` is lightweight. You can drip one row at a time in applications that require such behaviour. Note that WAL writer commits aren't immediately visible to readers. The committed data becomes visible once it was applied by the `ApplyWal2TableJob` job. Executing queries[​](https://questdb.com/docs/ingestion/java-embedded/#executing-queries "Direct link to Executing queries") ----------------------------------------------------------------------------------------------------------------------------- We provide a single API for executing all kinds of SQL queries. The example below focuses on SELECT and how to fetch data from a cursor. Compiling SQL final CairoConfiguration configuration = new DefaultCairoConfiguration(temp.getRoot().getAbsolutePath());try (CairoEngine engine = new CairoEngine(configuration)) { final SqlExecutionContext ctx = new SqlExecutionContextImpl(engine, 1) .with(AllowAllSecurityContext.INSTANCE, null); try (RecordCursorFactory factory = engine.select("SELECT * FROM abc", ctx)) { try (RecordCursor cursor = factory.getCursor(ctx)) { final Record record = cursor.getRecord(); while (cursor.hasNext()) { // access 'record' instance for field values } } } }} ### Detailed steps[​](https://questdb.com/docs/ingestion/java-embedded/#detailed-steps-1 "Direct link to Detailed steps") The steps to setup `CairoEngine`, execution context and `SqlCompiler` are the same as those we explained in the sections. We will skip them here and focus on fetching data. Note the all of the classes described below are not thread-safe, so they should not be used concurrently. #### RecordCursorFactory[​](https://questdb.com/docs/ingestion/java-embedded/#recordcursorfactory "Direct link to RecordCursorFactory") You can think of `RecordCursorFactory` as a prepared statement. This is the entity that holds SQL execution plan with all of the execution artefacts. Factories are designed to be reused and we strongly encourage caching them. You also need to make sure that you close factories explicitly when you no longer need them. Failing to do so can cause memory and/or other resources leak. #### RecordCursor[​](https://questdb.com/docs/ingestion/java-embedded/#recordcursor "Direct link to RecordCursor") This instance allows iterating over the dataset produced by SQL. Cursors are relatively short-lived and do not imply fetching all the data. Note that you have to close a cursor as soon as enough data is fetched ; the closing process can happen at any time. #### Record[​](https://questdb.com/docs/ingestion/java-embedded/#record "Direct link to Record") This is cursor's data access API. Record instance is obtained from the cursor outside of the fetch loop. Example of fetching data from cursor final Record record = cursor.getRecord();while (cursor.hasNext()) { // access 'record' instance for field values} Record does not hold the data. Instead, it is an API to pull data when data is needed. Record instance remains the same while cursor goes over the data, making caching of records pointless. InfluxDB Line Protocol client library[​](https://questdb.com/docs/ingestion/java-embedded/#influxdb-line-protocol-client-library "Direct link to InfluxDB Line Protocol client library") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We have [Java Client Library](https://questdb.com/docs/ingestion/clients/java/) to allow fast data ingestion. * [Writing data](https://questdb.com/docs/ingestion/java-embedded/#writing-data) * [Writing data using `WalWriter`](https://questdb.com/docs/ingestion/java-embedded/#writing-data-using-walwriter) * [Writing data using `TableWriter`](https://questdb.com/docs/ingestion/java-embedded/#writing-data-using-tablewriter) * [Writing data using `TableWriterAPI`](https://questdb.com/docs/ingestion/java-embedded/#writing-data-using-tablewriterapi) * [Detailed steps](https://questdb.com/docs/ingestion/java-embedded/#detailed-steps) * [Executing queries](https://questdb.com/docs/ingestion/java-embedded/#executing-queries) * [Detailed steps](https://questdb.com/docs/ingestion/java-embedded/#detailed-steps-1) * [InfluxDB Line Protocol client library](https://questdb.com/docs/ingestion/java-embedded/#influxdb-line-protocol-client-library) --- # TLS with PgBouncer for QuestDB | QuestDB On this page Configure PgBouncer to provide TLS termination for QuestDB Open Source PostgreSQL wire protocol connections. QuestDB Enterprise For QuestDB Enterprise, there is native TLS support, so you can connect directly with TLS or use PgBouncer with full TLS end-to-end encryption. Solution: TLS termination at PgBouncer[​](https://questdb.com/docs/cookbook/operations/tls-pgbouncer/#solution-tls-termination-at-pgbouncer "Direct link to Solution: TLS termination at PgBouncer") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB Open Source does not implement TLS on the PostgreSQL wire protocol, so TLS termination needs to be done at the PgBouncer level. Configure PgBouncer with: [databases]questdb = host=127.0.0.1 port=8812 dbname=questdb user=admin password=quest[pgbouncer]listen_addr = 127.0.0.1listen_port = 5432auth_type = trustauth_file = /path/to/pgbouncer/userlist.txtclient_tls_sslmode = requireclient_tls_key_file = /path/to/pgbouncer/pgbouncer.keyclient_tls_cert_file = /path/to/pgbouncer/pgbouncer.crtclient_tls_ca_file = /etc/ssl/cert.pemserver_tls_sslmode = disablelogfile = /path/to/pgbouncer/pgbouncer.logpidfile = /path/to/pgbouncer/pgbouncer.pid The key setting is `server_tls_sslmode = disable`. This makes psql connect using TLS to PgBouncer, but PgBouncer will connect without TLS to your QuestDB instance. Connect with: psql "host=127.0.0.1 port=5432 dbname=questdb user=admin sslmode=require" Unencrypted Traffic Traffic will be unencrypted between PgBouncer and QuestDB. This setup is only suitable when both services run on the same host or within a trusted network. Related Documentation * [PostgreSQL wire protocol](https://questdb.com/docs/query/pgwire/overview/) * [QuestDB security](https://questdb.com/docs/security/tls/) * [PgBouncer documentation](https://www.pgbouncer.org/config.html) * [Solution: TLS termination at PgBouncer](https://questdb.com/docs/cookbook/operations/tls-pgbouncer/#solution-tls-termination-at-pgbouncer) --- # Deploying to Google Cloud Platform (GCP) | QuestDB On this page Hardware recommendations[​](https://questdb.com/docs/deployment/gcp/#hardware-recommendations "Direct link to Hardware recommendations") ----------------------------------------------------------------------------------------------------------------------------------------- #### CPU/RAM[​](https://questdb.com/docs/deployment/gcp/#cpuram "Direct link to CPU/RAM") A production instance for QuestDB should be provisioned with at least `4 vCPUs` and `8 GiB` of memory. If possible, a 1:4 `vCPU/RAM` ratio should be used. Some use cases may benefit from a `1:8` ratio, if this means that all the working set data will fit into memory; this can increase query performance by as much as `10x`. It is **not recommended** to run production workloads on less than `4 vCPUs`, as below this number, parallel querying optimisations may be disabled. This is to ensure there is sufficient resources available for ingestion. #### Architecture[​](https://questdb.com/docs/deployment/gcp/#architecture "Direct link to Architecture") QuestDB should be deployed on Intel/AMD architectures i.e. `x86_64` and **not** on `ARM` i.e. `aarch64`. Some optimisations are not available on `ARM`, e.g. `SIMD`. #### Storage[​](https://questdb.com/docs/deployment/gcp/#storage "Direct link to Storage") Data should be stored on a data disk with at minimum 3000 IOPS/125 MBps, and ideally at least 5000 IOPS/300 MBps. Higher end workloads should scale up the disks appropriately, or use multiple disks if necessary. It is also worth checking the burst capacity of your storage. This capacity should only be used during heavy I/O spikes/infrequent out-of-order (O3) writes. It is useful to have in case you hit unexpected load bursts. ### Google Compute Engine with Google Cloud Hyperdisk[​](https://questdb.com/docs/deployment/gcp/#google-compute-engine-with-google-cloud-hyperdisk "Direct link to Google Compute Engine with Google Cloud Hyperdisk") Google Compute Engine offers a variety of VM instances tuned for different workloads. Do **not** use instances containing the letter `A`, such as `C4A`. These are `ARM` architecture instances, using Axion processors. Either `AMD EPYC` CPUs (`D` letter) or `Intel Xeon` (no letter) are appropriate for `x86_64` deployments. We recommend starting with `C-Series` instances, and reviewing other instance types if your workload demands it. You should deploy using an `x86_64` Linux distribution, such as Ubuntu. For storage, we recommend using [Hyperdisk Balanced](https://cloud.google.com/compute/docs/disks/hyperdisks) disks, and provisioning them at `5000 IOPS/300 MBps` until you have tested your workload. warning Hyperdisk Balanced is not supported on all machine types. N2 instances do not support Hyperdisk. Use N4, C3, or C4 series instances with Hyperdisk Balanced. `Hyperdisk Extreme` generally requires much higher `vCPU` counts - for example, it cannot be used on `C3` machines smaller than `88 vCPUs`. For the file system, use `zfs` with `lz4`, as this will compress your data. If compression is not a concern, then use `ext4` or `xfs` for a little higher performance. ### Google Filestore[​](https://questdb.com/docs/deployment/gcp/#google-filestore "Direct link to Google Filestore") Google Filestore is a managed NFS service that can be used as a replication transport layer in QuestDB Enterprise. Filestore should **not** be used as primary storage for QuestDB. However, it is well-suited for replication when low latency is required. The `fs::` transport over NFS provides sub-200ms replication lag with [aggressive tuning](https://questdb.com/docs/high-availability/tuning/) , compared to ~1s+ with object store transport (GCS). To use Filestore for replication: 1. Create a Filestore instance in the same region as your QuestDB VMs 2. Mount the NFS share on both primary and replica nodes 3. Configure the `fs::` transport in `server.conf`: replication.object.store=fs::root=/mnt/questdb-repl/final;atomic_write_dir=/mnt/questdb-repl/scratch; Use the [backup](https://questdb.com/docs/operations/backup/) feature to manage WAL file retention on the NFS mount. On GKE, expose the Filestore share as a `PersistentVolume` with `ReadWriteMany` access mode using the [Filestore CSI driver](https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/filestore-csi-driver) , so both primary and replica pods can mount it simultaneously. note Filestore Zonal and Basic SSD tiers may require a [quota increase](https://cloud.google.com/docs/quotas/view-manage) before use. Basic HDD is typically available by default. ### Google Cloud Storage[​](https://questdb.com/docs/deployment/gcp/#google-cloud-storage "Direct link to Google Cloud Storage") QuestDB supports Google Cloud Storage as its replication object store in the Enterprise edition. GCS is the simplest and cheapest replication transport, but has higher latency (~1s+) due to object store API overhead. To get started, create a bucket for the database to use. Then follow the [Enterprise Quick Start](https://questdb.com/docs/getting-started/enterprise-quick-start/) steps to create a connection string and configure QuestDB. ### NetApp Volumes[​](https://questdb.com/docs/deployment/gcp/#netapp-volumes "Direct link to NetApp Volumes") [NetApp Volumes](https://cloud.google.com/netapp/volumes/docs/discover/overview) is a managed NFS service on GCP backed by NetApp ONTAP. Like Filestore, it can be used as a low-latency replication transport via the `fs::` prefix. The QuestDB configuration is identical to Filestore. note NetApp Volumes requires enabling the `netapp.googleapis.com` API and may require separate quota allocation. ### Minimum specification[​](https://questdb.com/docs/deployment/gcp/#minimum-specification "Direct link to Minimum specification") * **Instance**: `c3-standard-4` or `c3d-standard-4` `(4 vCPUs, 16 GB RAM)` * **Storage** * **OS disk**: `Hyperdisk Balanced (30 GiB)` volume provisioned with `3000 IOPS/140 MBps`. * **Data disk**: `Hyperdisk Balanced (100 GiB)` volume provisioned with `3000 IOPS/140 MBps`. * **Operating System**: `Linux Ubuntu 24.04 LTS x86_64`. * **File System**: `ext4` ### Better specification[​](https://questdb.com/docs/deployment/gcp/#better-specification "Direct link to Better specification") * **Instance**: `c3-highmem-8` or `c3d-highmem-8` `(8 vCPUs, 64 GB RAM)` * **Storage** * **OS disk**: `Hyperdisk Balanced (30 GiB)` volume provisioned with `5000 IOPS/300 MBps`. * **Data disk**: `Hyperdisk Balanced (300 GiB)` volume provisioned with `5000 IOPS/300 MBps`. * **Operating System**: `Linux Ubuntu 24.04 LTS x86_64`. * **File System**: `zfs` note You can use the `highcpu` and `highmem` variants to adjust the `standard` `4:1` RAM/vCPU ratio to `2:1` or `8:1` respectively. Higher RAM can improve performance dramatically if it means your working set data will fit entirely into memory. Launching QuestDB on Google Compute Engine[​](https://questdb.com/docs/deployment/gcp/#launching-questdb-on-google-compute-engine "Direct link to Launching QuestDB on Google Compute Engine") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- This guide describes how to run QuestDB on a new Google Cloud Platform (GCP) Compute Engine instance. After completing this guide, you will have an instance with QuestDB running in a container using the official QuestDB Docker image, as well as a network rule that enables communication over HTTP and PostgreSQL wire protocol. ### Prerequisites[​](https://questdb.com/docs/deployment/gcp/#prerequisites "Direct link to Prerequisites") * A [Google Cloud Platform](https://console.cloud.google.com/getting-started) (GCP) account and a GCP Project * The [Compute Engine API](https://console.cloud.google.com/apis/api/compute.googleapis.com) must be enabled for the corresponding Google Cloud Platform project ### Create a Compute Engine VM[​](https://questdb.com/docs/deployment/gcp/#create-a-compute-engine-vm "Direct link to Create a Compute Engine VM") 1. In the Google Cloud Console, navigate to [Compute Engine](https://console.cloud.google.com/compute/instances) and click **Create Instance** ![The Create Instance wizard on Google Cloud platform](https://questdb.com/docs/images/guides/google-cloud-platform/create-instance.webp) 2. Give the instance a name - this example uses `questdb-europe-west3` 3. Choose a **Region** and **Zone** where you want to deploy the instance - this example uses `europe-west3 (Frankfurt)` and the default zone 4. Choose a machine configuration. The default choice, `ec2-medium`, is a general-purpose instance with 4GB memory and should be enough to run this example. ![Deploying a QuestDB instance on Google Cloud Platform Compute Engine](https://questdb.com/docs/images/guides/google-cloud-platform/create-vm.webp) 5. To add a running QuestDB container on instance startup, scroll down and click the **Deploy Container** button. Then, provide the `latest` QuestDB Docker image in the **Container image** textbox. questdb/questdb:latest Click the **Select** button at the bottom of the dropdown to complete the container configuration. Your docker configuration should look like this: ![Configuring a Docker container to launch in a new QuestDB instance on Google Cloud Platform Compute Engine](https://questdb.com/docs/images/guides/google-cloud-platform/create-vm-docker.webp) Before creating the instance, we need to assign it a **Network tag** so that we can add a firewall rule that exposes QuestDB-related ports to the internet. This is required for you to access the database from outside your VPC. To create a **Network tag**: 1. Expand the **Advanced options** menu below the **firewall** section, and then expand the **Networking** panel 2. In the **Networking** panel add a **Network tag** to identify the instance. This example uses `questdb` ![Applying a Network tag to a Compute Engine VM Instance on Google Cloud Platform](https://questdb.com/docs/images/guides/google-cloud-platform/add-network-tag.webp) You can now launch the instance by clicking **Create** at the bottom of the dialog. ### Create a firewall rule[​](https://questdb.com/docs/deployment/gcp/#create-a-firewall-rule "Direct link to Create a firewall rule") Now that we've created our instance with a `questdb` network tag, we need to create a corresponding firewall rule to associate with that tag. This rule will expose the required ports for accessing QuestDB. With a network tag, we can easily apply the new firewall rule to our newly created instance as well as any other QuestDB instances that we create in the future. 1. Navigate to the [Firewall configuration](https://console.cloud.google.com/net-security/firewall-manager/firewall-policies) page under **Network Security** -> **Firewall policies** 2. Click the **Create firewall rule** button at the top of the page 3. Enter `questdb` in the **Name** field 4. Scroll down to the **Targets** dropdown and select "Specified target tags" 5. Enter `questdb` in the **Target tags** textbox. This will apply the firewall rule to the new instance that was created above 6. Under **Source filter**, enter an IP range that this rule applies to. This example uses `0.0.0.0/0`, which allows ingress from any IP address. We recommend that you make this rule more restrictive, and naturally that you include your current IP address within the chosen range. 7. In the **Protocols and ports** section, select **Specified protocols and ports**, check the **TCP** option, and type `8812,9000` in the textbox. 8. Scroll down and click the **Create** button ![Creating a firewall rule in for VPC networking on Google Cloud Platform](https://questdb.com/docs/images/guides/google-cloud-platform/firewall-rules.webp) All VM instances on Compute Engine in this account which have the **Network tag** `questdb` will now have this firewall rule applied. The ports we have opened are: * `9000` for the REST API and [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) * `8812` for the PostgreSQL wire protocol Verify the deployment[​](https://questdb.com/docs/deployment/gcp/#verify-the-deployment "Direct link to Verify the deployment") -------------------------------------------------------------------------------------------------------------------------------- To verify that the instance is running, navigate to **Compute Engine** -> [VM Instances](https://console.cloud.google.com/compute/instances) . A status indicator should show the instance as **running**: ![A QuestDB instance running on Google Cloud Platform showing a success status indicator](https://questdb.com/docs/images/guides/google-cloud-platform/instance-available.webp) To verify that the QuestDB deployment is operating as expected: 1. Copy the **External IP** of the instance 2. Navigate to `http://:9000` in a browser The [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) should now be visible: ![The QuestDB Web Console running on a VM instance on Google Cloud Platform](https://questdb.com/docs/images/guides/google-cloud-platform/gcp-portal.webp) Alternatively, a request may be sent to the REST API exposed on port 9000: curl -G \ --data-urlencode "query=SELECT * FROM telemetry_config" \ :9000/exec ### Set up GCP with Pulumi[​](https://questdb.com/docs/deployment/gcp/#set-up-gcp-with-pulumi "Direct link to Set up GCP with Pulumi") If you're using [Pulumi](https://www.pulumi.com/gcp/) to manage your infrastructure, you can create a QuestDB instance with the following: import pulumiimport pulumi_gcp as gcp# Create a Google Cloud Networkfirewall = gcp.compute.Firewall( "questdb-firewall", network="default", allows=[ gcp.compute.FirewallAllowArgs( protocol="tcp", ports=["9000", "8812"], ), ], target_tags=["questdb"], source_ranges=["0.0.0.0/0"],)# Create a Compute Engine Instanceinstance = gcp.compute.Instance( "questdb-instance", machine_type="e2-medium", zone="us-central1-a", boot_disk={ "initialize_params": { "image": "ubuntu-os-cloud/ubuntu-2004-lts", }, }, network_interfaces=[ gcp.compute.InstanceNetworkInterfaceArgs( network="default", access_configs=[{}], # Ephemeral public IP ) ], metadata_startup_script="""#!/bin/bash sudo apt-get update sudo apt-get install -y docker.io sudo docker run -d -p 9000:9000 -p 8812:8812 \ --env QDB_HTTP_USER="admin" \ --env QDB_HTTP_PASSWORD="quest" \ questdb/questdb """, tags=["questdb"],)# Export the instance's name and public IPpulumi.export("instanceName", instance.name)pulumi.export("instance_ip", instance.network_interfaces[0].access_configs[0].nat_ip) * [Hardware recommendations](https://questdb.com/docs/deployment/gcp/#hardware-recommendations) * [Google Compute Engine with Google Cloud Hyperdisk](https://questdb.com/docs/deployment/gcp/#google-compute-engine-with-google-cloud-hyperdisk) * [Google Filestore](https://questdb.com/docs/deployment/gcp/#google-filestore) * [Google Cloud Storage](https://questdb.com/docs/deployment/gcp/#google-cloud-storage) * [NetApp Volumes](https://questdb.com/docs/deployment/gcp/#netapp-volumes) * [Minimum specification](https://questdb.com/docs/deployment/gcp/#minimum-specification) * [Better specification](https://questdb.com/docs/deployment/gcp/#better-specification) * [Launching QuestDB on Google Compute Engine](https://questdb.com/docs/deployment/gcp/#launching-questdb-on-google-compute-engine) * [Prerequisites](https://questdb.com/docs/deployment/gcp/#prerequisites) * [Create a Compute Engine VM](https://questdb.com/docs/deployment/gcp/#create-a-compute-engine-vm) * [Create a firewall rule](https://questdb.com/docs/deployment/gcp/#create-a-firewall-rule) * [Verify the deployment](https://questdb.com/docs/deployment/gcp/#verify-the-deployment) * [Set up GCP with Pulumi](https://questdb.com/docs/deployment/gcp/#set-up-gcp-with-pulumi) --- # Redpanda | QuestDB On this page [Redpanda](https://redpanda.com/) is an open-source, Kafka-compatible streaming platform that uses C++ and Raft to replace Java and Zookeeper. Since it is Kafka compatible, it can be used with the [QuestDB Kafka connector](https://questdb.com/docs/ingestion/message-brokers/kafka/#questdb-kafka-connect-connector) , providing an alternative data [streaming](https://questdb.com/glossary/stream-processing) option. This guide also covers [Redpanda Connect](https://questdb.com/docs/ingestion/message-brokers/redpanda/#redpanda-connect) , a stream processing tool that can be used to build data pipelines. ### Prerequisites[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#prerequisites "Direct link to Prerequisites") * Docker * A local JDK installation * A running QuestDB instance ### Configure and start Redpanda[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#configure-and-start-redpanda "Direct link to Configure and start Redpanda") The Redpanda [Quick start guide](https://docs.redpanda.com/docs/get-started/quick-start/quick-start-docker/#start-redpanda) provides a `docker-compose.yaml` file that can be used. Copy and paste the content into into a file named `docker-compose.yml` on your local filesystem: docker-compose.yml ---version: "3.7"name: redpanda-quickstartnetworks: redpanda_network: driver: bridgevolumes: redpanda-0: nullservices: redpanda-0: command: - redpanda - start - --kafka-addr - internal://0.0.0.0:9092,external://0.0.0.0:19092 # use the internal addresses to connect to the Redpanda brokers' # from inside the same Docker network. # # use the external addresses to connect to the Redpanda brokers' # from outside the Docker network. # # address the broker advertises to clients that connect to the Kafka API. - --advertise-kafka-addr - internal://redpanda-0:9092,external://localhost:19092 - --pandaproxy-addr - internal://0.0.0.0:8082,external://0.0.0.0:18082 # address the broker advertises to clients that connect to PandaProxy. - --advertise-pandaproxy-addr - internal://redpanda-0:8082,external://localhost:18082 - --schema-registry-addr - internal://0.0.0.0:8081,external://0.0.0.0:18081 # Redpanda brokers use the RPC API to communicate with eachother internally. - --rpc-addr - redpanda-0:33145 - --advertise-rpc-addr - redpanda-0:33145 # tells Seastar (the framework Redpanda uses under the hood) to use 1 core on the system. - --smp 1 # the amount of memory to make available to Redpanda. - --memory 1G # the amount of memory that's left for the Seastar subsystem. # For development purposes this is set to 0. - --reserve-memory 0M # Redpanda won't assume it has all of the provisioned CPU # (to accommodate Docker resource limitations). - --overprovisioned # enable logs for debugging. - --default-log-level=debug image: docker.redpanda.com/vectorized/redpanda:v22.3.11 container_name: redpanda-0 volumes: - redpanda-0:/var/lib/redpanda/data networks: - redpanda_network ports: - 18081:18081 - 18082:18082 - 19092:19092 - 19644:9644 console: container_name: redpanda-console image: docker.redpanda.com/vectorized/console:v2.1.1 networks: - redpanda_network entrypoint: /bin/sh command: -c 'echo "$$CONSOLE_CONFIG_FILE" > /tmp/config.yml; /app/console' environment: CONFIG_FILEPATH: /tmp/config.yml CONSOLE_CONFIG_FILE: | kafka: brokers: ["redpanda-0:9092"] schemaRegistry: enabled: true urls: ["http://redpanda-0:8081"] redpanda: adminApi: enabled: true urls: ["http://redpanda-0:9644"] ports: - 8080:8080 depends_on: - redpanda-0 Once the file is saved, run the following command to start a single Redpanda broker inside Docker and expose Redpanda to your host machine: docker compose up It also start the [Redpanda web UI](https://docs.redpanda.com/docs/get-started/quick-start/quick-start-docker/#explore-your-topic-in-redpanda-console) . ### Download Apache Kafka[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#download-apache-kafka "Direct link to Download Apache Kafka") Download [Apache Kafka](https://downloads.apache.org/kafka/3.7.0/kafka_2.12-3.7.0.tgz) and unzip the file. This step is required as Redpanda does not have its own Kafka Connect equivalent. ### Download the QuestDB Kafka connector[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#download-the-questdb-kafka-connector "Direct link to Download the QuestDB Kafka connector") Download [the QuestDB Kafka connector](https://github.com/questdb/kafka-questdb-connector/releases/latest) , under the zip archive named `kafka-questdb-connector--bin.zip`. tip You can automate downloading the latest connector package by running this command: curl -s https://api.github.com/repos/questdb/kafka-questdb-connector/releases/latest |jq -r '.assets[]|select(.content_type == "application/zip")|.browser_download_url'|wget -qi - Unzip the connector - it has a directory with 2 JARs: Copy these JARs into /path/to/kafka/lib: unzip kafka-questdb-connector-*-bin.zipcd kafka-questdb-connectorcp ./*.jar /path/to/kafka/libs There should be already a lot of other JAR files. That's how you can tell you are in the right directory. ### Configure properties[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#configure-properties "Direct link to Configure properties") Go to /path/to/kafka/config - there should be already quite a few \*.property files. Create a new file: `questdb-connector.properties` with the following lines: questdb-connector.properties name=questdb-sinkconnector.class=io.questdb.kafka.QuestDBSinkConnectorclient.conf.string=http::addr=localhost:9000;timestamp.kafka.native=truetopics=example-topictable=example_tableinclude.key=falsevalue.converter=org.apache.kafka.connect.json.JsonConvertervalue.converter.schemas.enable=falsekey.converter=org.apache.kafka.connect.storage.StringConverter In addition, pointing the open `connect-standalone.properties` and replace: bootstrap.servers=localhost:9092 with the Redpanda broker URL: bootstrap.servers=127.0.0.1:19092 ### Start Kafka Connect[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#start-kafka-connect "Direct link to Start Kafka Connect") Navigate to the Kafka Connect folder and then run: ./bin/connect-standalone.sh config/connect-standalone.properties config/questdb-connector.properties Now the Kafka Connect is initiated. ### Send a message[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#send-a-message "Direct link to Send a message") Open the Redpanda UI topic page at `http://127.0.0.1:8080/topics`. It should display `example-topic`: ![Screenshot of the Redpanda UI showing the example-topic](https://questdb.com/docs/assets/images/redpanda-topic-7095711f40c335603fcb03e3957193ac.webp) If the topic is not there then refresh a few times. Select `example-topic` to expand more details and click `Actions` --> `Publish Message`: ![Screenshot of the Redpanda UI highlighting the Actions button](https://questdb.com/docs/assets/images/redpanda-actions-a02f6ba7c99068a2192bd9d8b016638c.webp) Paste the following message into the message box: { "firstname": "Arthur", "lastname": "Dent", "age": 42 } ![Screenshot of the Redpanda UI add message page](https://questdb.com/docs/assets/images/redpanda-add-messsage-1a0ad8eb2cd0254ac6d984b048b788dd.webp) Then, click 'Publish'. ### See result from QuestDB[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#see-result-from-questdb "Direct link to See result from QuestDB") Go to QuestDB web console at `http://localhost:9000`. Run a `SELECT` query: SELECT * FROM example_table; The message is delivered to QuestDB: ![QuestDB web console result showing the Redpanda message](https://questdb.com/docs/assets/images/questdb-select-6ef0f3d71437e5e5b35aefd8dfc056b5.webp) ### Summary and next steps[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#summary-and-next-steps "Direct link to Summary and next steps") The guide demonstrates how to use Redpanda with the QuestDB Kafka connector. The connector implicitly creates a table in QuestDB with inferred schema from the Kafka message. Our connector configuration properties includes a key `timestamp.kafka.native=true` which tells the connector to use the timestamp from the Kafka message metadata. The connector can be also configured to use a custom timestamp field from the Kafka message. See the [QuestDB Kafka Connector reference manual](https://questdb.com/docs/ingestion/message-brokers/kafka/#designated-timestamps) for details. A possible improvement could be to explicitly create the target table in QuestDB instead of relying on the connector to create it implicitly. This way, you can control the schema, [partitioning](https://questdb.com/glossary/database-partitioning/) and data types of the table. It also enables QuestDB's native [deduplication feature](https://questdb.com/docs/concepts/deduplication/) . Deduplication is required for [Exactly-Once](https://questdb.com/docs/ingestion/message-brokers/kafka/#fault-tolerance) processing semantics. See also[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#see-also "Direct link to See also") ------------------------------------------------------------------------------------------------------------- * [QuestDB Kafka Connector reference manual](https://questdb.com/docs/ingestion/message-brokers/kafka/) Redpanda Connect[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#redpanda-connect "Direct link to Redpanda Connect") ------------------------------------------------------------------------------------------------------------------------------------- Redpanda Connect is a stream processing tool that can be used to build data pipelines. It's a lightweight alternative to [Apache Kafka Connect](https://questdb.com/docs/ingestion/message-brokers/kafka/#questdb-kafka-connect-connector) . This guide shows the steps to use the Redpanda Connect to write JSON data as rows into a QuestDB table. ### Prerequisites[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#prerequisites-1 "Direct link to Prerequisites") You will need the following: * [Redpanda Connect](https://docs.redpanda.com/redpanda-connect/about/) * A running QuestDB instance ### Download Redpanda Connect[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#download-redpanda-connect "Direct link to Download Redpanda Connect") The QuestDB output component was added to Redpanda Connect in version v4.37.0. To download the latest version of Redpanda Connect, follow the [installation instructions](https://docs.redpanda.com/redpanda-connect/guides/getting_started/#install) in the official documentation. ### Configure Redpanda Connect[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#configure-redpanda-connect "Direct link to Configure Redpanda Connect") One of Redpanda Connect's strengths is the ability to configure an entire data pipeline in a single yaml file. We will create a simple configuration to demonstrate the QuestDB connector's capabilities by using a straightforward input source. Create this file and name it `config.yaml` in your current directory input: stdin: {}output: questdb: address: localhost:9000 table: redpanda_connect_demo doubles: - price designated_timestamp_field: timestamp This configuration will read lines from stdin and publish them to your running QuestDB instance ### Run Redpanda Connect and publish messages[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#run-redpanda-connect-and-publish-messages "Direct link to Run Redpanda Connect and publish messages") Run the following command to send some messages to QuestDB through Redpanda Connect echo \'{"symbol": "AAPL", "price": 225.83, "timestamp": 1727294094}{"symbol": "MSFT", "price": 431.78, "timestamp": 1727294142}' \| rpk connect run config.yaml The command above sends two JSON messages to Redpanda Connect standard input, which then writes them to QuestDB. ### Verify the integration[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#verify-the-integration "Direct link to Verify the integration") Navigate to the QuestDB Web Console at `http://localhost:9000` and run the following query to see your data: SELECT *FROM redpanda_connect_demo ### Next steps[​](https://questdb.com/docs/ingestion/message-brokers/redpanda/#next-steps "Direct link to Next steps") Explore Redpanda Connect's [official documentation](https://docs.redpanda.com/redpanda-connect/about/) to learn more about its capabilities and how to use it in your projects. * [Prerequisites](https://questdb.com/docs/ingestion/message-brokers/redpanda/#prerequisites) * [Configure and start Redpanda](https://questdb.com/docs/ingestion/message-brokers/redpanda/#configure-and-start-redpanda) * [Download Apache Kafka](https://questdb.com/docs/ingestion/message-brokers/redpanda/#download-apache-kafka) * [Download the QuestDB Kafka connector](https://questdb.com/docs/ingestion/message-brokers/redpanda/#download-the-questdb-kafka-connector) * [Configure properties](https://questdb.com/docs/ingestion/message-brokers/redpanda/#configure-properties) * [Start Kafka Connect](https://questdb.com/docs/ingestion/message-brokers/redpanda/#start-kafka-connect) * [Send a message](https://questdb.com/docs/ingestion/message-brokers/redpanda/#send-a-message) * [See result from QuestDB](https://questdb.com/docs/ingestion/message-brokers/redpanda/#see-result-from-questdb) * [Summary and next steps](https://questdb.com/docs/ingestion/message-brokers/redpanda/#summary-and-next-steps) * [See also](https://questdb.com/docs/ingestion/message-brokers/redpanda/#see-also) * [Redpanda Connect](https://questdb.com/docs/ingestion/message-brokers/redpanda/#redpanda-connect) * [Prerequisites](https://questdb.com/docs/ingestion/message-brokers/redpanda/#prerequisites-1) * [Download Redpanda Connect](https://questdb.com/docs/ingestion/message-brokers/redpanda/#download-redpanda-connect) * [Configure Redpanda Connect](https://questdb.com/docs/ingestion/message-brokers/redpanda/#configure-redpanda-connect) * [Run Redpanda Connect and publish messages](https://questdb.com/docs/ingestion/message-brokers/redpanda/#run-redpanda-connect-and-publish-messages) * [Verify the integration](https://questdb.com/docs/ingestion/message-brokers/redpanda/#verify-the-integration) * [Next steps](https://questdb.com/docs/ingestion/message-brokers/redpanda/#next-steps) --- # Embeddable | QuestDB On this page Embeddable is a developer toolkit for building fast, interactive customer-facing analytics. It works well with a high performance time-series database like QuestDB. In [Embeddable](https://embeddable.com/) you define [Data Models](https://docs.embeddable.com/data-modeling/introduction) and [Components](https://docs.embeddable.com/development/introduction) in code, which are stored in your own code repository, then use the **SDK** to make these available for your team in the powerful Embeddable **no-code builder.** The end result is the ability to deliver fast, interactive **customer-facing analytics** directly into your product. Built-in **row-level security** means that every user only ever sees **exactly** the data they’re allowed to see. And two levels of fully-configurable **caching** mean you can deliver fast, realtime analytics at scale. Prerequisites[​](https://questdb.com/docs/integrations/visualization/embeddable/#prerequisites "Direct link to Prerequisites") ------------------------------------------------------------------------------------------------------------------------------- * A running QuestDB instance * Not running yet? See the [quick start](https://questdb.com/docs/getting-started/quick-start/) Getting started with Embeddable[​](https://questdb.com/docs/integrations/visualization/embeddable/#getting-started-with-embeddable "Direct link to Getting started with Embeddable") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Add a database connection using Embeddable API. This connection connects to your QuestDB instance. To add a connection, use the following API call: // for security reasons, this must *never* be called from your client-sidefetch("https://api.embeddable.com/api/v1/connections", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${apiKey}` /* keep your API Key secure */, }, body: JSON.stringify({ name: "my-questdb-db", type: "questdb", credentials: { host: "my.questdb.host", port: "8812", user: "admin", password: "quest", }, }),}) In response you will receive: Status 201 { errorMessage: null } The above represents a `CREATE` action, but all `CRUD` operations are available. The `apiKey` can be found by clicking “**Publish**” on one of your Embeddable dashboards. The `name` is a unique name to identify this connection. * By default your data models will look for a connection called “default”, but you can supply your models with different `data_source` names to support connecting different data models to different connections (simply specify the data\_source name in the model) The `type` tells Embeddable which driver to use * Here you'll want to use `questbd`, but you can connect multiple different datasources to one Embeddable workspace so you may use others such as: `postgres`, `bigquery`, `mongodb`, etc. The `credentials` is a javascript object containing the necessary credentials expected by the driver * These are securely encrypted and only used to retrieve exactly the data you have described in your data models. * Embeddable strongly encourage you to create a read-only database user for each connection (Embeddable will only ever read from your database, not write). In order to support connecting to different databases for prod, qa, test, etc (or to support different databases for different customers) you can assign each connection to an environment (see [Environments API](https://docs.embeddable.com/data/environments) ). * [Prerequisites](https://questdb.com/docs/integrations/visualization/embeddable/#prerequisites) * [Getting started with Embeddable](https://questdb.com/docs/integrations/visualization/embeddable/#getting-started-with-embeddable) --- # Superset | QuestDB On this page [Apache Superset](https://superset.apache.org/) is a popular open-source business intelligence web application that enables users to visualize and explore data through customizable dashboards and reports. QuestDB provides the [QuestDB Connect](https://pypi.org/project/questdb-connect/) python package that implements the SQLAlchemy dialect and Superset engine specification, to integrate Apache Superset with QuestDB. Installing Apache Superset via Docker (recommended)[​](https://questdb.com/docs/integrations/visualization/superset/#installing-apache-superset-via-docker-recommended "Direct link to Installing Apache Superset via Docker (recommended)") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We recommend the Docker-based Apache Superset installation. You will need to install the following requirements: * Docker, including Docker Compose * QuestDB 7.1.2 or later Then, following the steps below: 1. Clone the [Superset repo](https://github.com/apache/superset) : git clone https://github.com/apache/superset.git 2. Change your directory: cd superset 3. Create a file `docker/requirements-local.txt` with the requirement to `questdb-connect`: touch ./docker/requirements-local.txtecho "questdb-connect==1.1.3" > docker/requirements-local.txt 4. Set Superset version to 4.0.2: This step is important to ensure compatibility with QuestDB Connect. export TAG=4.0.2 5. Run Apache Superset: docker compose -f docker-compose-image-tag.yml pulldocker compose -f docker-compose-image-tag.yml up This step will initialize your Apache Superset installation, creating a default admin, users, and several other settings. The first time you start Apache Superset it can take a few minutes until it is completely initialized. Please keep an eye on the console output to see when Apache Superset is ready to be used. Installing Superset via QuestDB Connect[​](https://questdb.com/docs/integrations/visualization/superset/#installing-superset-via-questdb-connect "Direct link to Installing Superset via QuestDB Connect") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- If you have a stand-alone installation of Apache Superset and are using Apache Superset without Docker, you need to install the following requirements : * Python from 3.9 to 3.11 * [Superset](https://superset.apache.org/docs/quickstart/) 4.0.x * QuestDB 7.1.2 or later Install QuestDB Connect using `pip`: pip install 'questdb-connect==1.1.3' Connecting QuestDB to Superset[​](https://questdb.com/docs/integrations/visualization/superset/#connecting-questdb-to-superset "Direct link to Connecting QuestDB to Superset") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Once installed and initialized, Apache Superset is accessible via `http://localhost:8088`. 1. Sign in with the following details: * Username: admin * Password: admin 2. From Superset UI, select Setting > Database Connections 3. Select `+Database` to add a new QuestDB database ![QuestDB Database Selection](https://questdb.com/docs/assets/images/superset_database_selection-676d1a1831e71bf24daf9163e4b6b3eb.webp) 4. In the next step use `host.docker.internal` when running Apache Superset from Docker and `localhost` for outside of Docker. Port is `8812` by default, and the database name is `QuestDB`, default user is `admin` and password is `quest`. ![QuestDB Database Configuration](https://questdb.com/docs/assets/images/superset_database_config-b055df9584d891d130c2bb6d3eb412b6.webp) 5. Once connected, tables in QuestDB will be visible for creating Datasets in Apache Superset. ![QuestDB Tables in Superset](https://questdb.com/docs/assets/images/superset_browser-9919b9ab04fd320f316194ed025cac9d.webp) Conclusion[​](https://questdb.com/docs/integrations/visualization/superset/#conclusion "Direct link to Conclusion") -------------------------------------------------------------------------------------------------------------------- The integration of Apache Superset with QuestDB allows users to visualize and explore data through customizable dashboards and reports. This guide provides instructions for installing Apache Superset via Docker and QuestDB Connect, and connecting QuestDB to Apache Superset. If you have any questions or need help, please join our [community Slack](https://slack.questdb.com/) or open a [GitHub issue](https://github.com/questdb/questdb-connect/issues/new) . See also[​](https://questdb.com/docs/integrations/visualization/superset/#see-also "Direct link to See also") -------------------------------------------------------------------------------------------------------------- * [QuestDB Connect at GitHub](https://github.com/questdb/questdb-connect/) * [QuestDB Connect Python module](https://pypi.org/project/questdb-connect/) * [Apache Superset install](https://superset.apache.org/docs/quickstart/) * [Blog post with Superset dashboard example](https://questdb.com/blog/time-series-data-visualization-apache-superset-and-questdb/) * [Installing Apache Superset via Docker (recommended)](https://questdb.com/docs/integrations/visualization/superset/#installing-apache-superset-via-docker-recommended) * [Installing Superset via QuestDB Connect](https://questdb.com/docs/integrations/visualization/superset/#installing-superset-via-questdb-connect) * [Connecting QuestDB to Superset](https://questdb.com/docs/integrations/visualization/superset/#connecting-questdb-to-superset) * [Conclusion](https://questdb.com/docs/integrations/visualization/superset/#conclusion) * [See also](https://questdb.com/docs/integrations/visualization/superset/#see-also) --- # Run QuestDB on Kubernetes | QuestDB On this page You can deploy QuestDB in a [Kubernetes](https://kubernetes.io/) cluster using a [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) and a [persistent volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) . We distribute QuestDB via [Helm](https://helm.sh/) on [ArtifactHub](https://artifacthub.io/packages/helm/questdb/questdb) . Prerequisites[​](https://questdb.com/docs/deployment/kubernetes/#prerequisites "Direct link to Prerequisites") --------------------------------------------------------------------------------------------------------------- * [Helm](https://helm.sh/docs/intro/install/) * [Kubernetes CLI](https://kubernetes.io/docs/tasks/tools/install-kubectl/) * [minikube](https://minikube.sigs.k8s.io/docs/start/) Get the QuestDB Helm chart[​](https://questdb.com/docs/deployment/kubernetes/#get-the-questdb-helm-chart "Direct link to Get the QuestDB Helm chart") ------------------------------------------------------------------------------------------------------------------------------------------------------ Using the Helm client, add the official Helm chart repository: helm repo add questdb https://helm.questdb.io/ Update the Helm index: helm repo update Run QuestDB[​](https://questdb.com/docs/deployment/kubernetes/#run-questdb "Direct link to Run QuestDB") --------------------------------------------------------------------------------------------------------- Start a local cluster using `minikube`: minikube start Then install the chart: helm install my-questdb questdb/questdb Finally, use the Kubernetes CLI to get the pod name: kubectl get pods Result: | NAME | READY | STATUS | RESTARTS | AGE | | --- | --- | --- | --- | --- | | my-questdb-0 | 1/1 | Running | 1 | 9m59s | Querying QuestDB locally[​](https://questdb.com/docs/deployment/kubernetes/#querying-questdb-locally "Direct link to Querying QuestDB locally") ------------------------------------------------------------------------------------------------------------------------------------------------ In order to run queries against your local instance of QuestDB, you can use port forwarding: kubectl port-forward my-questdb-0 9000 The following ports may also be used: * 9000: [REST API](https://questdb.com/docs/query/rest-api/) and [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) * 8812: [Postgres](https://questdb.com/docs/query/pgwire/overview/) * 9009: [InfluxDB line protocol](https://questdb.com/docs/ingestion/ilp/overview/) Customizing the deployment[​](https://questdb.com/docs/deployment/kubernetes/#customizing-the-deployment "Direct link to Customizing the deployment") ------------------------------------------------------------------------------------------------------------------------------------------------------ The QuestDB Helm chart supports a variety of configuration options. Run the following to view all of them and any preconfigured defaults: helm show values questdb/questdb Using Kubernetes secrets[​](https://questdb.com/docs/deployment/kubernetes/#using-kubernetes-secrets "Direct link to Using Kubernetes secrets") ------------------------------------------------------------------------------------------------------------------------------------------------ QuestDB supports reading sensitive configuration values directly from mounted secret files using the `_FILE` suffix convention. This eliminates the need for shell scripts or init containers to inject secrets as environment variables. For example, to configure the PostgreSQL wire protocol password from a Kubernetes secret: apiVersion: v1kind: Secretmetadata: name: questdb-secretstype: Opaquedata: pg-password: bXktc2VjcmV0LXBhc3N3b3Jk # base64 encoded---apiVersion: apps/v1kind: StatefulSetmetadata: name: questdbspec: serviceName: questdb replicas: 1 selector: matchLabels: app: questdb template: metadata: labels: app: questdb spec: containers: - name: questdb image: questdb/questdb:latest env: - name: QDB_PG_PASSWORD_FILE value: /run/secrets/pg-password volumeMounts: - name: secrets mountPath: /run/secrets readOnly: true volumes: - name: secrets secret: secretName: questdb-secrets items: - key: pg-password path: pg-password note This example focuses on secret mounting and omits the `volumeClaimTemplates` needed for persistent storage. For production deployments, use the [QuestDB Helm chart](https://questdb.com/docs/deployment/kubernetes/#get-the-questdb-helm-chart) which handles storage configuration automatically. For the full list of supported properties, see [Secrets from files](https://questdb.com/docs/configuration/overview/#secrets-from-files) in the configuration reference. * [Prerequisites](https://questdb.com/docs/deployment/kubernetes/#prerequisites) * [Get the QuestDB Helm chart](https://questdb.com/docs/deployment/kubernetes/#get-the-questdb-helm-chart) * [Run QuestDB](https://questdb.com/docs/deployment/kubernetes/#run-questdb) * [Querying QuestDB locally](https://questdb.com/docs/deployment/kubernetes/#querying-questdb-locally) * [Customizing the deployment](https://questdb.com/docs/deployment/kubernetes/#customizing-the-deployment) * [Using Kubernetes secrets](https://questdb.com/docs/deployment/kubernetes/#using-kubernetes-secrets) --- # Implementation shortfall decomposition | QuestDB On this page Implementation Shortfall (IS) is a standard Transaction Cost Analysis framework originally developed for equities (the Perold framework), where it is widely used to evaluate broker and algo execution quality. The same decomposition applies to FX and other asset classes — the underlying idea of separating spread cost from market impact is universal. The example below uses FX trade data, but the approach works for any instrument with order book snapshots. IS decomposes total execution cost into three components: * **Effective spread** — the immediate cost of crossing the spread. Measures how far the fill price deviated from the mid at the time of execution. * **Permanent impact** — the portion of price movement that persists after the trade. This reflects the information content of the trade — if the market permanently moves against you, your trade may have been informed (or was perceived as such). * **Temporary impact** — the portion that reverts. This is the transient market impact caused by your order consuming liquidity, which fades as the book replenishes. The relationship is: **effective spread = permanent impact + temporary impact**. Problem[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#problem "Direct link to Problem") --------------------------------------------------------------------------------------------------------------------- You want to break down trading costs beyond simple slippage. For each symbol and side, you need to know how much of the execution cost was due to the spread, how much was genuine market impact, and how much was temporary dislocation that reverted. Solution[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#solution "Direct link to Solution") ------------------------------------------------------------------------------------------------------------------------ Use `HORIZON JOIN` to capture the mid-price at execution time and 30 minutes later, then `PIVOT` to reshape the offsets into columns for the decomposition: Implementation shortfall decomposition by symbol[Demo this query](https://demo.questdb.io/?query=WITH%20markouts%20AS%20(%0A%20%20%20%20SELECT%0A%20%20%20%20%20%20%20%20f.symbol%2C%0A%20%20%20%20%20%20%20%20f.price%2C%0A%20%20%20%20%20%20%20%20f.quantity%2C%0A%20%20%20%20%20%20%20%20f.side%2C%0A%20%20%20%20%20%20%20%20h.offset%2C%0A%20%20%20%20%20%20%20%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202%20AS%20mid%0A%20%20%20%20FROM%20fx_trades%20f%0A%20%20%20%20HORIZON%20JOIN%20market_data%20m%20ON%20(f.symbol%20%3D%20m.symbol)%0A%20%20%20%20%20%20%20%20LIST%20(0%2C%201800s)%20AS%20h%0A%20%20%20%20WHERE%20f.timestamp%20IN%20%27%24yesterday%27%0A)%2C%0Apivoted%20AS%20(%0A%20%20%20%20SELECT%20*%20FROM%20markouts%0A%20%20%20%20PIVOT%20(%0A%20%20%20%20%20%20%20%20avg(mid)%20AS%20mid%2C%0A%20%20%20%20%20%20%20%20avg(price)%20AS%20px%2C%0A%20%20%20%20%20%20%20%20sum(quantity)%20AS%20vol%0A%20%20%20%20%20%20%20%20FOR%20offset%20IN%20(%0A%20%20%20%20%20%20%20%20%20%20%20%200%20%20%20%20%20%20%20%20%20%20AS%20at_fill%2C%0A%20%20%20%20%20%20%20%20%20%20%20%201800000000000%20AS%20at_30m%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20GROUP%20BY%20symbol%2C%20side%0A%20%20%20%20)%0A)%0ASELECT%0A%20%20%20%20symbol%2C%0A%20%20%20%20side%2C%0A%20%20%20%20at_fill_vol%20AS%20total_volume%2C%0A%20%20%20%20CASE%20WHEN%20side%20%3D%20%27buy%27%20THEN%201%20ELSE%20-1%20END%0A%20%20%20%20%20%20%20%20*%20(at_fill_px%20-%20at_fill_mid)%20%2F%20at_fill_mid%20*%2010000%20%20%20AS%20effective_spread_bps%2C%0A%20%20%20%20CASE%20WHEN%20side%20%3D%20%27buy%27%20THEN%201%20ELSE%20-1%20END%0A%20%20%20%20%20%20%20%20*%20(at_30m_mid%20-%20at_fill_mid)%20%2F%20at_fill_mid%20*%2010000%20%20%20AS%20permanent_bps%2C%0A%20%20%20%20CASE%20WHEN%20side%20%3D%20%27buy%27%20THEN%201%20ELSE%20-1%20END%0A%20%20%20%20%20%20%20%20*%20(at_fill_px%20-%20at_30m_mid)%20%2F%20at_fill_mid%20*%2010000%20%20%20%20AS%20temporary_bps%0AFROM%20pivoted%0AORDER%20BY%20symbol%2C%20side%3B&executeQuery=true) WITH markouts AS ( SELECT f.symbol, f.price, f.quantity, f.side, h.offset, (m.best_bid + m.best_ask) / 2 AS mid FROM fx_trades f HORIZON JOIN market_data m ON (f.symbol = m.symbol) LIST (0, 1800s) AS h WHERE f.timestamp IN '$yesterday'),pivoted AS ( SELECT * FROM markouts PIVOT ( avg(mid) AS mid, avg(price) AS px, sum(quantity) AS vol FOR offset IN ( 0 AS at_fill, 1800000000000 AS at_30m ) GROUP BY symbol, side ))SELECT symbol, side, at_fill_vol AS total_volume, CASE WHEN side = 'buy' THEN 1 ELSE -1 END * (at_fill_px - at_fill_mid) / at_fill_mid * 10000 AS effective_spread_bps, CASE WHEN side = 'buy' THEN 1 ELSE -1 END * (at_30m_mid - at_fill_mid) / at_fill_mid * 10000 AS permanent_bps, CASE WHEN side = 'buy' THEN 1 ELSE -1 END * (at_fill_px - at_30m_mid) / at_fill_mid * 10000 AS temporary_bpsFROM pivotedORDER BY symbol, side; How it works[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#how-it-works "Direct link to How it works") ------------------------------------------------------------------------------------------------------------------------------------ The query has three stages: ### 1\. HORIZON JOIN — capture mid at two points in time[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#1-horizon-join--capture-mid-at-two-points-in-time "Direct link to 1. HORIZON JOIN — capture mid at two points in time") HORIZON JOIN market_data m ON (f.symbol = m.symbol) LIST (0, 1800s) AS h For each trade, this produces two rows: * **Offset 0** — the mid-price at the moment of execution (arrival price) * **Offset 1800s** — the mid-price 30 minutes later (the "settled" price) ### 2\. PIVOT — reshape offsets into columns[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#2-pivot--reshape-offsets-into-columns "Direct link to 2. PIVOT — reshape offsets into columns") PIVOT ( avg(mid) AS mid, avg(price) AS px, sum(quantity) AS vol FOR offset IN (0 AS at_fill, 1800000000000 AS at_30m) GROUP BY symbol, side) This turns the two offset rows into columns: `at_fill_mid`, `at_fill_px`, `at_fill_vol`, `at_30m_mid`, `at_30m_px`, `at_30m_vol`. The offset values in `FOR ... IN` are in nanoseconds (since `fx_trades` uses `TIMESTAMP_NS`), so 30 minutes = 1,800,000,000,000 ns. ### 3\. Decomposition — compute the three components[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#3-decomposition--compute-the-three-components "Direct link to 3. Decomposition — compute the three components") The sign convention uses `CASE WHEN side = 'buy' THEN 1 ELSE -1 END` to normalize both sides so that positive values always mean cost (worse execution): | Component | Formula | Meaning | | --- | --- | --- | | **Effective spread** | `fill_price - fill_mid` | Immediate cost of crossing the spread | | **Permanent impact** | `30m_mid - fill_mid` | How much the market permanently moved against you | | **Temporary impact** | `fill_price - 30m_mid` | How much of the initial cost reverted | Interpreting results[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#interpreting-results "Direct link to Interpreting results") ------------------------------------------------------------------------------------------------------------------------------------------------------------ * **High effective spread, low permanent**: You're paying to cross the spread but the market isn't moving against you. This is the normal cost of aggressive execution. * **High permanent impact**: Your trades carry information (or the market perceives them as informed). Consider reducing order size or using more passive execution. * **High temporary impact**: You're moving the market temporarily but it reverts. This suggests your orders are large relative to available liquidity but not information-driven. * **Negative temporary impact**: The market moved further against you after the fill. This is worse than expected — your initial impact understated the true cost. Choosing the horizon The 30-minute horizon (`1800s`) is a common choice for FX, but the right value depends on your market and trading style. For highly liquid pairs, 5–10 minutes may be sufficient for the price to settle. For less liquid instruments, you may need 1 hour or more. Adjust the `LIST` offset to match your market's typical recovery time. Related documentation * [HORIZON JOIN](https://questdb.com/docs/query/sql/horizon-join/) * [PIVOT](https://questdb.com/docs/query/sql/pivot/) * [Slippage per fill recipe](https://questdb.com/docs/cookbook/sql/finance/slippage/) * [Markout analysis recipe](https://questdb.com/docs/cookbook/sql/finance/markout/) * [Problem](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#problem) * [Solution](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#solution) * [How it works](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#how-it-works) * [1\. HORIZON JOIN — capture mid at two points in time](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#1-horizon-join--capture-mid-at-two-points-in-time) * [2\. PIVOT — reshape offsets into columns](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#2-pivot--reshape-offsets-into-columns) * [3\. Decomposition — compute the three components](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#3-decomposition--compute-the-three-components) * [Interpreting results](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/#interpreting-results) --- # Configure TLS certificate authorities | QuestDB On this page Configure TLS certificate authority (CA) validation when connecting QuestDB clients to TLS-enabled instances. Problem[​](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#problem "Direct link to Problem") ------------------------------------------------------------------------------------------------------------------ You are using a QuestDB client (Rust, Python, C++, etc.) to insert data. It works when using QuestDB without TLS, but when you enable TLS on your QuestDB instance using a self-signed certificate, you get an error of "certificate unknown". When using the PostgreSQL wire interface, you can insert data passing `sslmode=require`, and it works, so you can discard any problems with QuestDB recognizing the certificate. But you need to figure out the equivalent for your ILP client. Solution: Configure TLS CA[​](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#solution-configure-tls-ca "Direct link to Solution: Configure TLS CA") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB clients support the `tls_ca` parameter, which has multiple values to configure certificate authority validation: ### Option 1: Use WebPKI and OS certificate roots (recommended for production)[​](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#option-1-use-webpki-and-os-certificate-roots-recommended-for-production "Direct link to Option 1: Use WebPKI and OS certificate roots (recommended for production)") If you want to accept both the webpki-root certificates plus whatever you have on the OS, pass `tls_ca=webpki_and_os_roots`: https::addr=localhost:9000;username=admin;password=quest;tls_ca=webpki_and_os_roots; This will work with certificates signed by standard certificate authorities. ### Option 2: Use a custom PEM file[​](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#option-2-use-a-custom-pem-file "Direct link to Option 2: Use a custom PEM file") Point to a PEM-encoded certificate file for self-signed or custom CA certificates: https::addr=localhost:9000;username=admin;password=quest;tls_ca=pem_file;tls_roots=/path/to/cert.pem; This is useful for self-signed certificates or internal CAs. ### Option 3: Skip verification (development only)[​](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#option-3-skip-verification-development-only "Direct link to Option 3: Skip verification (development only)") For development environments with self-signed certificates, you might be tempted to disable verification by passing `tls_verify=unsafe_off`: https::addr=localhost:9000;username=admin;password=quest;tls_verify=unsafe_off; danger This is a very bad idea for production and should only be used for testing on a development environment with a self-signed certificate. It disables all certificate validation. **Note:** Some clients require enabling an optional feature (like `insecure-skip-verify` in Rust) before the `tls_verify=unsafe_off` parameter will work. Check your client's documentation for details. Available tls\_ca values[​](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#available-tls_ca-values "Direct link to Available tls_ca values") ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Value | Description | | --- | --- | | `webpki_roots` | Mozilla's WebPKI root certificates only | | `os_roots` | Operating system certificate store only | | `webpki_and_os_roots` | Both WebPKI and OS roots (recommended) | | `pem_file` | Load from a PEM file (requires `tls_roots` parameter) | Example: Rust client[​](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#example-rust-client "Direct link to Example: Rust client") -------------------------------------------------------------------------------------------------------------------------------------------------------- use questdb::ingress::{Sender, SenderBuilder};#[tokio::main]async fn main() -> Result<(), Box> { let sender = SenderBuilder::new("https", "localhost", 9000)? .username("admin")? .password("quest")? .tls_ca("webpki_and_os_roots")? // Use standard CAs .build() .await?; // Use sender... sender.close().await?; Ok(())} For self-signed certificates with a PEM file: let sender = SenderBuilder::new("https", "localhost", 9000)? .username("admin")? .password("quest")? .tls_ca("pem_file")? .tls_roots("/path/to/questdb.crt")? .build() .await?; The examples are in Rust but the concepts are similar in other languages. Check the documentation for your specific client. Related Documentation * [QuestDB Rust client](https://docs.rs/questdb/) * [QuestDB Python client](https://questdb.com/docs/ingestion/clients/python/) * [QuestDB C++ client](https://questdb.com/docs/ingestion/clients/c-and-cpp/) * [QuestDB TLS configuration](https://questdb.com/docs/security/tls/) * [Problem](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#problem) * [Solution: Configure TLS CA](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#solution-configure-tls-ca) * [Option 1: Use WebPKI and OS certificate roots (recommended for production)](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#option-1-use-webpki-and-os-certificate-roots-recommended-for-production) * [Option 2: Use a custom PEM file](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#option-2-use-a-custom-pem-file) * [Option 3: Skip verification (development only)](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#option-3-skip-verification-development-only) * [Available tls\_ca values](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#available-tls_ca-values) * [Example: Rust client](https://questdb.com/docs/cookbook/programmatic/tls-ca-configuration/#example-rust-client) --- # Grafana | QuestDB On this page [Grafana](https://grafana.com/) is a popular observability and monitoring application used to visualize data and enable [time-series data analysis](https://questdb.com/glossary/time-series-analysis/) . QuestDB is available within Grafana via the [official QuestDB plugin](https://grafana.com/grafana/plugins/questdb-questdb-datasource/) . warning QuestDB can also be used with the PostgreSQL Grafana plugin, but the configuration options are different in that case. The QuestDB official plugin is strongly recommended instead. For a walk-through style guide, see our [blog post](https://questdb.com/blog/time-series-monitoring-dashboard-grafana-questdb/) . Prerequisites[​](https://questdb.com/docs/integrations/visualization/grafana/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------------- * [Docker](https://questdb.com/docs/deployment/docker/) to run both Grafana and QuestDB * We will use the `--add-host` parameter for both Grafana and QuestDB. Start Grafana[​](https://questdb.com/docs/integrations/visualization/grafana/#start-grafana "Direct link to Start Grafana") ---------------------------------------------------------------------------------------------------------------------------- Start Grafana using `docker run`: docker run --add-host=host.docker.internal:host-gateway \-p 3000:3000 --name=grafana \-v grafana-storage:/var/lib/grafana \grafana/grafana-oss Once the Grafana server has started, you can access it via port 3000 (`http://localhost:3000`). The default login credentials are as follows: user:adminpassword:admin Start QuestDB[​](https://questdb.com/docs/integrations/visualization/grafana/#start-questdb "Direct link to Start QuestDB") ---------------------------------------------------------------------------------------------------------------------------- The Docker version runs on port `8812` for the database connection and port `9000` for the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) and REST interface: docker run --add-host=host.docker.internal:host-gateway \-p 9000:9000 -p 9009:9009 -p 8812:8812 -p 9003:9003 \-v "$(pwd):/var/lib/questdb" \-e QDB_PG_READONLY_USER_ENABLED=true \questdb/questdb:latest Add a data source[​](https://questdb.com/docs/integrations/visualization/grafana/#add-a-data-source "Direct link to Add a data source") ---------------------------------------------------------------------------------------------------------------------------------------- 1. Open Grafana's UI (by default available at `http://localhost:3000`) 2. Navigate to the bottom of the page and click **Find more data source plugins**. 3. Search for QuestDB and click **Install**. 4. Once the QuestDB data source for Grafana is finished installing, click on the blue **Add new data source** button where the **Install** button used to be. 5. Enter the connection settings. 1. Notice that `Server Address` is the host address without the port. Some common values are `host.docker.internal` when using Docker on the same host, `localhost` when running standalone Grafana on the same host, or the QuestDB instance IP address when running Grafana remotely. 2. The port, which defaults to `8812` is passed as a separate parameter. 3. For QuestDB Open Source, TLS/SSL mode should be `disable`. This can be left empty for QuestDB Enterprise. Server address: host.docker.internalServer port: 8812Username: userPassword: questTLS/SSL mode: disable 6. Toggle the **Query Builder** to **SQL Editor** by clicking the button. 7. Write SQL queries! ![Screenshot of a blank panel after being created](https://questdb.com/docs/images/blog/2023-04-12/blank-panel.webp) Real-time refresh rates[​](https://questdb.com/docs/integrations/visualization/grafana/#real-time-refresh-rates "Direct link to Real-time refresh rates") ---------------------------------------------------------------------------------------------------------------------------------------------------------- By default, Grafana limits the maximum refresh rate of your dashboards. The maximum default rate is to refresh every 5 seconds. This is to provide relief to the database under-the-hood. However, with QuestBD's significant performance optimizations, we can lower this rate for greater fluidity. To learn how, see our [blog post](https://questdb.com/blog/increase-grafana-refresh-rate-frequency/) . Global variables[​](https://questdb.com/docs/integrations/visualization/grafana/#global-variables "Direct link to Global variables") ------------------------------------------------------------------------------------------------------------------------------------- Use [global variables](https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#global-variables) to simplify queries with dynamic elements such as date range filters. ### `$__timeFilter(timestamp)`[​](https://questdb.com/docs/integrations/visualization/grafana/#__timefiltertimestamp "Direct link to __timefiltertimestamp") This variable allows filtering results by sending a start-time and end-time to QuestDB. This expression evaluates to: timestamp BETWEEN '2018-02-01T00:00:00Z' AND '2018-02-28T23:59:59Z' ### `$__interval`[​](https://questdb.com/docs/integrations/visualization/grafana/#__interval "Direct link to __interval") This variable calculates a dynamic interval based on the time range applied to the dashboard. By using this function, the sampling interval changes automatically as the user zooms in and out of the panel. An example of $\_\_interval SELECT timestamp AS time, avg(price) AS avg_priceFROM tradesWHERE $__timeFilter(timestamp)SAMPLE BY $__interval; See also[​](https://questdb.com/docs/integrations/visualization/grafana/#see-also "Direct link to See also") ------------------------------------------------------------------------------------------------------------- * [QuestDB + Grafana walkthrough](https://questdb.com/blog/time-series-monitoring-dashboard-grafana-questdb/) * [QuestDB Grafana blogs](https://questdb.com/blog/?tag=grafana) * [Official QuestDB plugin](https://grafana.com/grafana/plugins/questdb-questdb-datasource/) * [Prerequisites](https://questdb.com/docs/integrations/visualization/grafana/#prerequisites) * [Start Grafana](https://questdb.com/docs/integrations/visualization/grafana/#start-grafana) * [Start QuestDB](https://questdb.com/docs/integrations/visualization/grafana/#start-questdb) * [Add a data source](https://questdb.com/docs/integrations/visualization/grafana/#add-a-data-source) * [Real-time refresh rates](https://questdb.com/docs/integrations/visualization/grafana/#real-time-refresh-rates) * [Global variables](https://questdb.com/docs/integrations/visualization/grafana/#global-variables) * [`$__timeFilter(timestamp)`](https://questdb.com/docs/integrations/visualization/grafana/#__timefiltertimestamp) * [`$__interval`](https://questdb.com/docs/integrations/visualization/grafana/#__interval) * [See also](https://questdb.com/docs/integrations/visualization/grafana/#see-also) --- # qStudio | QuestDB On this page [qStudio](https://www.timestored.com/qstudio/) is a free SQL GUI. It allows to run SQL scripts, browse tables easily, chart and export results. qStudio includes charting functionality including time-series charting which is particularly useful with QuestDB. It works on every operating system and with every database including QuestDB via the PostgreSQL driver. Prerequisites[​](https://questdb.com/docs/integrations/visualization/qstudio/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------------- * A running QuestDB instance (See [Getting Started](https://questdb.com/docs/getting-started/quick-start/) ) Configure QuestDB connection[​](https://questdb.com/docs/integrations/visualization/qstudio/#configure-questdb-connection "Direct link to Configure QuestDB connection") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1. [Download qStudio](https://www.timestored.com/qstudio/download) for your OS 2. Launch qStudio 3. Go to `Server` -> `Add Server` 4. Click `Add data source` 5. Choose the `PostgreSQL` plugin and configure it with the following settings: host:localhostport:8812database:qdbuser:adminpassword:quest Sending Queries[​](https://questdb.com/docs/integrations/visualization/qstudio/#sending-queries "Direct link to Sending Queries") ---------------------------------------------------------------------------------------------------------------------------------- Run queries with: * Ctrl+Enter to run the current line, or * Ctrl+E to run the highlighted code. ![Screenshot of the qStudio UI running QuestDB query](https://questdb.com/docs/images/guides/qstudio/qstudio-query.webp) Screenshot of the qStudio UI running QuestDB query See also[​](https://questdb.com/docs/integrations/visualization/qstudio/#see-also "Direct link to See also") ------------------------------------------------------------------------------------------------------------- * [QuestDB Postgres wire protocol](https://questdb.com/docs/query/pgwire/overview/) * [Prerequisites](https://questdb.com/docs/integrations/visualization/qstudio/#prerequisites) * [Configure QuestDB connection](https://questdb.com/docs/integrations/visualization/qstudio/#configure-questdb-connection) * [Sending Queries](https://questdb.com/docs/integrations/visualization/qstudio/#sending-queries) * [See also](https://questdb.com/docs/integrations/visualization/qstudio/#see-also) --- # Capacity planning | QuestDB On this page This guide will help you optimize your QuestDB deployments for peak performance. We cover example scenarios across both edge cases and common setup configurations. Most configuration settings are configured in QuestDB using the `server.conf` configuration file, or as environment variables. For more information about applying configuration settings in QuestDB, see the [configuration](https://questdb.com/docs/configuration/overview/) page. To monitor the various metrics of a QuestDB instance, refer to the [Prometheus monitoring](https://questdb.com/docs/integrations/other/prometheus/) page or the [Logging & Metrics](https://questdb.com/docs/operations/logging-metrics/) page. Storage and filesystem[​](https://questdb.com/docs/getting-started/capacity-planning/#storage-and-filesystem "Direct link to Storage and filesystem") ------------------------------------------------------------------------------------------------------------------------------------------------------ Some of the aspects to consider regarding the storage of data and file systems. ### Drive selection[​](https://questdb.com/docs/getting-started/capacity-planning/#drive-selection "Direct link to Drive selection") If you're using a physically-attached drive, we strongly recommend using NVMe drives over SATA SSDs. NVMe drives offer faster read and write speeds compared to other SSDs. This translates to overall better performance. If you're using a network-attached drive, like [AWS EBS](https://aws.amazon.com/ebs/) , please refer to the next section. ### Optimizing IOPS and throughput[​](https://questdb.com/docs/getting-started/capacity-planning/#optimizing-iops-and-throughput "Direct link to Optimizing IOPS and throughput") IOPS is a measure of the number of operations per second. Throughput measures the amount of data transferred per second, e.g. in megabytes per second. Both metrics are important. However, your requirements may vary depending on the workload. For instance, large batch operations might benefit more from higher throughput, whereas real-time query performance might need higher IOPS. For typical loads, particularly when using AWS gp3 volumes, you should aim for the following baseline IOPS and throughput settings: * Minimum IOPS: 7000 * Minimum Throughput: 500 MB/s For optimum performance, utilize the maximum settings: * Maximum IOPS: 16000 * Maximum Throughput: 1 GB/s ### Supported filesystems[​](https://questdb.com/docs/getting-started/capacity-planning/#supported-filesystems "Direct link to Supported filesystems") To enable compression and to match our recommended performance profile, we recommend using [ZFS file system](https://en.wikipedia.org/wiki/ZFS) . ZFS is required for system-level compression. While ZFS is recommended, QuestDB open source supports the following filesystems: * APFS * EXT4 * NTFS * OVERLAYFS (used by Docker) * XFS (`ftype=1` only) * ZFS Other file systems supporting [mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) may work with QuestDB but they should not be used in production. QuestDB does not test on them. When you use an unsupported file system, QuestDB logs this warning: -> UNSUPPORTED (SYSTEM COULD BE UNSTABLE)" caution Users **can't use NFS or similar distributed filesystems** directly with a QuestDB database. ### Data compression[​](https://questdb.com/docs/getting-started/capacity-planning/#data-compression "Direct link to Data compression") To enable data compression, filesystem must be ZFS. For instructions on how to do so, see the [ZFS and compression](https://questdb.com/docs/deployment/compression-zfs/) guide. ### Write amplification[​](https://questdb.com/docs/getting-started/capacity-planning/#write-amplification "Direct link to Write amplification") Write amplification measures how many times data is rewritten during ingestion. A value of 1.0 means each row is written once (ideal). Higher values indicate rewrites due to out-of-order data merging into existing partitions. Calculate it using [Prometheus metrics](https://questdb.com/docs/integrations/other/prometheus/#scraping-prometheus-metrics-from-questdb) : write_amplification = questdb_physically_written_rows_total / questdb_committed_rows_total These are **cumulative lifetime counters**. To measure current write amplification, compare the delta of both values over a time window (e.g., 5 minutes). | Value | Interpretation | | --- | --- | | 1.0 – 1.5 | Excellent – minimal rewrites | | 1.5 – 3.0 | Normal for moderate out-of-order data | | 3.0 – 5.0 | Consider reducing partition size | | \> 5.0 | High – reduce partition size or investigate ingestion patterns | When ingesting out-of-order data, high write amplification combined with high disk write rate may reduce database performance. For data ingestion over PostgreSQL Wire Protocol, or as a further step for InfluxDB Line Protocol ingestion, using smaller table [partitions](https://questdb.com/docs/concepts/partitions/) can reduce write amplification. This applies in particular to tables with partition directories exceeding several hundred MBs on disk. For example, `PARTITION BY DAY` could be reduced to `PARTIION BY HOUR`, `PARTITION BY MONTH` to `PARTITION BY DAY`, and so on. #### Partition splitting[​](https://questdb.com/docs/getting-started/capacity-planning/#partition-splitting "Direct link to Partition splitting") Since QuestDB 7.2, heavily out-of-order commits may split partitions into smaller parts to reduce write amplification. When data is merged into an existing partition due to an out-of-order insert, the partition will be split into two parts: the prefix sub-partition and the suffix sub-partition. Consider the following scenario: * A partition `2023-01-01.1` with 1,000 rows every hour, and therefore 24,000 rows in total. * Inserting one row with the timestamp `2023-01-01T23:00` When the out-of-order row `2023-01-01T23:00` is inserted, the partition is split into 2 parts: * Prefix: `2023-01-01.1` with 23,000 rows * Suffix (including the merged row):`2023-01-01T75959-999999.2` with 1,001 rows See [Splitting and squashing time partitions](https://questdb.com/docs/concepts/partitions/#partition-splitting-and-squashing) for more information. CPU and RAM configuration[​](https://questdb.com/docs/getting-started/capacity-planning/#cpu-and-ram-configuration "Direct link to CPU and RAM configuration") --------------------------------------------------------------------------------------------------------------------------------------------------------------- This section describes configuration strategies based on the forecasted behavior of the database. ### RAM size[​](https://questdb.com/docs/getting-started/capacity-planning/#ram-size "Direct link to RAM size") We recommend having at least 8GB of RAM for basic workloads, and 32GB for more advanced ones. For relatively small datasets i.e 4-40GB, and a read-heavy workload, performance can be improved by maximising use of the OS page cache. Users should consider increasing available RAM to improve the speed of read operations. ### Memory page size configuration[​](https://questdb.com/docs/getting-started/capacity-planning/#memory-page-size-configuration "Direct link to Memory page size configuration") With frequent out-of-order (O3) writes over a large number of columns/tables, database performance may be impacted by large memory page sizes, as this increases the demand for RAM. The memory page, `cairo.o3.column.memory.size`, is set to 8M by default. This means that the table writer uses 16MB (2x8MB) RAM per column when it receives O3 writes. O3 write performance, and overall memory usage, may be improved by decreasing this value within the range \[128K, 8M\]. A smaller page size allows for a larger number of in-use columns, or otherwise frees up memory for other database processes to use. ### CPU cores[​](https://questdb.com/docs/getting-started/capacity-planning/#cpu-cores "Direct link to CPU cores") By default, QuestDB tries to use all available CPU cores. [The guide on shared worker configuration](https://questdb.com/docs/configuration/overview/#shared-worker) explains how to change the default settings. Assuming that the disk is not bottlenecked on IOPS, the throughput of read-only queries scales proportionally with the number of available cores. As a result, a machine with more cores will provide better query performance. ### Writer page size[​](https://questdb.com/docs/getting-started/capacity-planning/#writer-page-size "Direct link to Writer page size") The default page size for writers is 16MB. This should be adjusted according to your use case. For example, using a 16MB page-size, to write only 1MB of data is a waste of resources. To change this default value, set the `cairo.writer.data.append.page.size` option in `server.conf`: server.conf cairo.writer.data.append.page.size=1M For more horizontal use cases i.e databases with a large number of small tables, the page sizes could be reduced more dramatically. This may better distribute resources, and help to reduce write amplification. ### InfluxDB Line Protocol (ILP) over HTTP[​](https://questdb.com/docs/getting-started/capacity-planning/#influxdb-line-protocol-ilp-over-http "Direct link to InfluxDB Line Protocol (ILP) over HTTP") As of QuestDB 7.4.2, InfluxDB Line Protocol operates over HTTP instead of TCP. As such, ILP is optimal out-of-the box. See your [ILP client](https://questdb.com/docs/ingestion/overview/#first-party-clients) for language-specific configurations. ### Postgres Wire Protocol[​](https://questdb.com/docs/getting-started/capacity-planning/#postgres-wire-protocol "Direct link to Postgres Wire Protocol") For clients sending data to QuestDB using the Postgres interface, the following configuration can be applied, which sets a dedicated worker and pins it with `affinity` to a CPU by core ID: server.conf pg.worker.count=4pg.worker.affinity=1,2,3,4 Network Configuration[​](https://questdb.com/docs/getting-started/capacity-planning/#network-configuration "Direct link to Network Configuration") --------------------------------------------------------------------------------------------------------------------------------------------------- For the InfluxDB Line Protocol, PostgreSQL Wire Protocol and HTTP, there are a number of configuration settings which control: * the number of clients that may connect * the internal I/O capacities * connection timeout settings These settings are configured in the `server.conf` file, and follow the naming convention: .net.connection. Where `` is one of: * `http` - HTTP connections * `pg` - PostgreSQL Wire Protocol * `line.tcp` - InfluxDB line protocol over TCP And `` is one of the following settings: | key | description | | --- | --- | | `limit` | The number of simultaneous connections to the server. This value is intended to control server memory consumption. | | `timeout` | Connection idle timeout in milliseconds. Connections are closed by the server when this timeout lapses. | | `hint` | Applicable only for Windows, where TCP backlog limit is hit. For example Windows 10 allows max of 200 connection. Even if limit is set higher, without hint=true, it won't be possible to serve more than 200 connections. | | `sndbuf` | Maximum send buffer size on each TCP socket. If value is -1 socket send buffer remains unchanged from OS default. | | `rcvbuf` | Maximum receive buffer size on each TCP socket. If value is -1, the socket receive buffer remains unchanged from OS default. | For example, this is a configuration for Linux with a relatively low number of concurrent connections: server.conf InfluxDB Line Protocol network example configuration for a low number of concurrent connections # bind to all IP addresses on port 9009line.tcp.net.bind.to=0.0.0.0:9009# maximum of 30 concurrent connection allowedline.tcp.net.connection.limit=30# nothing to do here, connection limit is quite lowline.tcp.net.connection.hint=false# connections will time out after 60s of no activityline.tcp.net.connection.timeout=60000# receive buffer is 4MB to accomodate large messagesline.tcp.net.rcvbuf=4M This is an example for when one would like to configure InfluxDB Line Protocol for a large number of concurrent connections, on Windows: server.conf InfluxDB Line Protocol network example configuration for large number of concurrent connections on Windows # bind to specific NIC on port 9009, NIC is identified by IP addressline.tcp.net.bind.to=10.75.26.3:9009# large number of concurrent connectionsline.tcp.net.connection.limit=400# Windows will not allow 400 client to connect unless we use the "hint"line.tcp.net.connection.hint=true# connections will time out after 30s of inactivityline.tcp.net.connection.timeout=30000# receive buffer is 1MB because messages are small, smaller buffer will# reduce memory usage, 400 connections times 1MB = 400MB RAM required to handle inputline.tcp.net.rcvbuf=1M For more information on the default settings for the `http` and `pg` protocols, refer to the [server configuration page](https://questdb.com/docs/configuration/overview/) . ### Pooled connections[​](https://questdb.com/docs/getting-started/capacity-planning/#pooled-connections "Direct link to Pooled connections") Connection pooling should be used for any production-ready use of PostgreSQL Wire Protocol or InfluxDB Line Protocol over TCP. The maximum number of pooled connections is configurable, (`pg.connection.pool.capacity` for PostgreSQL Wire Protocol and (`line.tcp.connection.pool.capacity` for InfluxDB Line Protocol over TCP. The default number of connections for both interfaces is 64. Users should avoid using too many connections, as large numbers of concurrent connections will increase overall CPU usage. OS configuration[​](https://questdb.com/docs/getting-started/capacity-planning/#os-configuration "Direct link to OS configuration") ------------------------------------------------------------------------------------------------------------------------------------ Changing system settings on the host OS can improve QuestDB performance. QuestDB may reach system limits relating to maximum open files, and virtual memory areas. QuestDB writes operating system errors to its logs unchanged. We only recommend changing the following system settings in response to seeing such OS errors in the logs. ### Maximum open files[​](https://questdb.com/docs/getting-started/capacity-planning/#maximum-open-files "Direct link to Maximum open files") QuestDB uses a [columnar](https://questdb.com/glossary/columnar-database/) storage model, and therefore its core data structures relate closely to the file system. Columnar data is stored in its own `.d` file, per time partition. In edge cases with extremely large tables, frequent out-of-order ingestion, or a high number of table partitions, the number of open files may hit a user or system-wide maximum limit, causing reduced performance and other unwanted behaviours. In Linux/MacOS environments, maximum open file limits for the current user: # Soft limitulimit -Sn# Hard limitulimit -Hn #### Setting the open file limit for the current user:[​](https://questdb.com/docs/getting-started/capacity-planning/#setting-the-open-file-limit-for-the-current-user "Direct link to Setting the open file limit for the current user:") On a Linux environment, one must increase the hard limit. On MacOS, both the hard and soft limits must be set. See [Max Open Files Limit on MacOS for the JVM](https://questdb.com/blog/max-open-file-limit-macos-jvm/) for more details. Modify user limits using `ulimit`: # Hard limitulimit -H -n 1048576# Soft limitulimit -S -n 1048576 The system-wide limit should be increased correspondingly. #### Setting the system-wide open file limit on Linux:[​](https://questdb.com/docs/getting-started/capacity-planning/#setting-the-system-wide-open-file-limit-on-linux "Direct link to Setting the system-wide open file limit on Linux:") To increase this setting and persist this configuration change, the limit on the number of concurrently open files can be amended in `/etc/sysctl.conf`: /etc/sysctl.conf fs.file-max=1048576 To confirm that this value has been correctly configured, reload `sysctl` and check the current value: # reload configurationsysctl -p# query current settingssysctl fs.file-max #### Extra steps for systemd[​](https://questdb.com/docs/getting-started/capacity-planning/#extra-steps-for-systemd "Direct link to Extra steps for systemd") If you are running the QuestDB using `systemd`, you will also need to set the `LimitNOFILE` property in your service file. If you have followed the [setup guide](https://questdb.com/docs/deployment/systemd/) , then the file should be called `questdb.service` and be located at `~/.config/systemd/user/questdb.service`. Add this property to the `[Service]` section, setting it to at least `1048576`, or higher if you have set higher OS-wide limits. Then restart the service. If you have configured these settings correctly, any warnings in the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) should now be cleared. #### Setting system-wide open file limit on MacOS:[​](https://questdb.com/docs/getting-started/capacity-planning/#setting-system-wide-open-file-limit-on-macos "Direct link to Setting system-wide open file limit on MacOS:") On MacOS, the system-wide limit can be modified by using `launchctl`: sudo launchctl limit maxfiles 98304 2147483647 To confirm the change, view the current settings using `sysctl`: sysctl -a | grep kern.maxf ### Max virtual memory areas limit[​](https://questdb.com/docs/getting-started/capacity-planning/#max-virtual-memory-areas-limit "Direct link to Max virtual memory areas limit") The database relies on memory mapping to read and write data to its files. If the host machine has low limits on virtual memory mapping areas, this can cause out-of-memory exceptions ([errno=12](https://questdb.com/docs/troubleshooting/os-error-codes/) ). To increase this setting and persist this configuration change, mapped memory area limits can be amended in `/etc/sysctl.conf`: /etc/sysctl.conf vm.max_map_count=1048576 Each mapped area may consume ~128 bytes for each map count i.e 1048576 may use 1048576\*128 = 134MB of kernel memory. # reload configurationsysctl -p# query current settingscat /proc/sys/vm/max_map_count * [Storage and filesystem](https://questdb.com/docs/getting-started/capacity-planning/#storage-and-filesystem) * [Drive selection](https://questdb.com/docs/getting-started/capacity-planning/#drive-selection) * [Optimizing IOPS and throughput](https://questdb.com/docs/getting-started/capacity-planning/#optimizing-iops-and-throughput) * [Supported filesystems](https://questdb.com/docs/getting-started/capacity-planning/#supported-filesystems) * [Data compression](https://questdb.com/docs/getting-started/capacity-planning/#data-compression) * [Write amplification](https://questdb.com/docs/getting-started/capacity-planning/#write-amplification) * [CPU and RAM configuration](https://questdb.com/docs/getting-started/capacity-planning/#cpu-and-ram-configuration) * [RAM size](https://questdb.com/docs/getting-started/capacity-planning/#ram-size) * [Memory page size configuration](https://questdb.com/docs/getting-started/capacity-planning/#memory-page-size-configuration) * [CPU cores](https://questdb.com/docs/getting-started/capacity-planning/#cpu-cores) * [Writer page size](https://questdb.com/docs/getting-started/capacity-planning/#writer-page-size) * [InfluxDB Line Protocol (ILP) over HTTP](https://questdb.com/docs/getting-started/capacity-planning/#influxdb-line-protocol-ilp-over-http) * [Postgres Wire Protocol](https://questdb.com/docs/getting-started/capacity-planning/#postgres-wire-protocol) * [Network Configuration](https://questdb.com/docs/getting-started/capacity-planning/#network-configuration) * [Pooled connections](https://questdb.com/docs/getting-started/capacity-planning/#pooled-connections) * [OS configuration](https://questdb.com/docs/getting-started/capacity-planning/#os-configuration) * [Maximum open files](https://questdb.com/docs/getting-started/capacity-planning/#maximum-open-files) * [Max virtual memory areas limit](https://questdb.com/docs/getting-started/capacity-planning/#max-virtual-memory-areas-limit) --- # Data retention | QuestDB On this page Background[​](https://questdb.com/docs/operations/data-retention/#background "Direct link to Background") ---------------------------------------------------------------------------------------------------------- The nature of [time-series data](https://questdb.com/blog/what-is-time-series-data/) is that the relevance of information diminishes over time. If stale data is no longer required, users can delete old data from QuestDB to either save disk space or adhere to a data retention policy. This is achieved in QuestDB by removing data partitions from a table. QuestDB offers two approaches for data retention: * **Automatic**: Use [Time To Live (TTL)](https://questdb.com/docs/concepts/ttl/) to automatically drop partitions when data ages beyond a specified threshold. This is the simplest approach for most use cases. * **Manual**: Use `DROP PARTITION` commands as described on this page for explicit control over which partitions to remove and when. This page provides a high-level overview of partitioning with examples to drop data by date. For more details on partitioning, see the [partitioning](https://questdb.com/docs/concepts/partitions/) page. Manual partition management[​](https://questdb.com/docs/operations/data-retention/#manual-partition-management "Direct link to Manual partition management") ------------------------------------------------------------------------------------------------------------------------------------------------------------- This section covers the manual approach to removing stale data by dropping partitions. A table must have a [designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) assigned and a partitioning strategy specified during a `CREATE TABLE` operation to achieve this. note Users cannot alter the partitioning strategy after a table is created. Tables can be partitioned by one of the following: * `YEAR` * `MONTH` * `WEEK` * `DAY` * `HOUR` Creating a table and partitioning by DAY CREATE TABLE my_table(ts TIMESTAMP, symb SYMBOL, price DOUBLE) timestamp(ts)PARTITION BY DAY; ### Dropping partitions[​](https://questdb.com/docs/operations/data-retention/#dropping-partitions "Direct link to Dropping partitions") caution Use `DROP PARTITION` with care, as QuestDB **cannot recover data from dropped partitions**. To drop partitions, users can use the [ALTER TABLE DROP PARTITION](https://questdb.com/docs/query/sql/alter-table-drop-partition/) syntax. Partitions may be dropped by: * `DROP PARTITION LIST` - specifying a comma-separated list of partitions to drop --Delete a partitionALTER TABLE my_table DROP PARTITION LIST '2021-01-01';--Delete a list of two partitionsALTER TABLE my_table DROP PARTITION LIST '2021-01-01', '2021-01-02'; * `WHERE timestamp =` - exact date matching by timestamp ALTER TABLE my_table DROP PARTITIONWHERE timestamp = to_timestamp('2021-01-01', 'yyyy-MM-dd'); * `WHERE timestamp <` - using comparison operators (`<` / `>`) to delete by time range relative to a timestamp. Note that the `now()` function may be used to automate dropping of partitions relative to the current time, i.e.: --Drop partitions older than 30 daysALTER TABLE my_table DROP PARTITIONWHERE timestamp < dateadd('d', -30, now()); **Usage notes:** * The most chronologically recent partition cannot be deleted * Arbitrary partitions may be dropped, which means they may not be the oldest chronologically. Depending on the types of queries users are performing on a dataset, it may not be desirable to have gaps caused by dropped partitions. * Unlike TTL, `DROP PARTITION` commands must be triggered manually or via external scheduling (e.g., cron jobs). For fully automated retention, consider using [TTL](https://questdb.com/docs/concepts/ttl/) instead. ### Example[​](https://questdb.com/docs/operations/data-retention/#example "Direct link to Example") The following example demonstrates how to create a table with partitioning and to drop partitions based on time. This example produces 5 days' worth of data with one incrementing `LONG` value inserted per hour. Create a partitioned table and generate data CREATE TABLE my_table (timestamp TIMESTAMP, x LONG) timestamp(timestamp)PARTITION BY DAY;INSERT INTO my_tableSELECT timestamp_sequence( to_timestamp('2021-01-01T00:00:00', 'yyyy-MM-ddTHH:mm:ss'),100000L * 36000), xFROM long_sequence(120); For reference, the following functions are used to generate the example data: * [timestamp\_sequence](https://questdb.com/docs/query/functions/row-generator/#timestamp_sequence) with 1 hour stepping * [long\_sequence](https://questdb.com/docs/query/functions/row-generator/#long_sequence) which creates a `x:long` column The result of partitioning is visible when listing as directories on disk: path/to//db my_table├── 2021-01-01├── 2021-01-02├── 2021-01-03├── 2021-01-04└── 2021-01-05 Partitions can be dropped using the following query: --Delete days before 2021-01-03ALTER TABLE my_table DROP PARTITIONWHERE timestamp < to_timestamp('2021-01-03', 'yyyy-MM-dd'); * [Background](https://questdb.com/docs/operations/data-retention/#background) * [Manual partition management](https://questdb.com/docs/operations/data-retention/#manual-partition-management) * [Dropping partitions](https://questdb.com/docs/operations/data-retention/#dropping-partitions) * [Example](https://questdb.com/docs/operations/data-retention/#example) --- # Alternatives to UPDATE | QuestDB On this page QuestDB is optimized for append-only ingestion. For best performance, design your application to avoid frequently editing existing records. When you need to modify data, you have two options: 1. **[UPDATE statement](https://questdb.com/docs/query/sql/update/) ** - For correcting incorrectly inserted data. See [How UPDATE works](https://questdb.com/docs/operations/updating-data/) for implementation details. 2. **Append-oriented alternatives** (this page) - Patterns that work with QuestDB's storage model instead of against it. Alternatives to UPDATE[​](https://questdb.com/docs/operations/modifying-data/#alternatives-to-update "Direct link to Alternatives to UPDATE") ---------------------------------------------------------------------------------------------------------------------------------------------- * **[Append newest state](https://questdb.com/docs/operations/modifying-data/#append-newest-state) ** - Insert a newer state to replace an older one. This preserves history and enables [bi-temporal queries](https://martinfowler.com/articles/bitemporal-history.html) . * **[Replace table](https://questdb.com/docs/operations/modifying-data/#replace-table) ** - Create a new table with filtered data, drop the original, and rename. * **[Drop partitions](https://questdb.com/docs/operations/modifying-data/#delete-by-dropping-partitions) ** - Delete entire time-based partitions you no longer need. note Always [backup your database](https://questdb.com/docs/operations/backup/) before modifying data. Append newest state[​](https://questdb.com/docs/operations/modifying-data/#append-newest-state "Direct link to Append newest state") ------------------------------------------------------------------------------------------------------------------------------------- ### Using the timestamp field[​](https://questdb.com/docs/operations/modifying-data/#using-the-timestamp-field "Direct link to Using the timestamp field") Here's a working example using the timestamp column: CREATE TABLE takeaway_order ( ts TIMESTAMP, id SYMBOL, status SYMBOL) timestamp(ts);INSERT INTO takeaway_order VALUES (now(), 'order1', 'placed');INSERT INTO takeaway_order VALUES (now(), 'order2', 'placed');INSERT INTO takeaway_order VALUES (now(), 'order1', 'cooking');INSERT INTO takeaway_order VALUES (now(), 'order1', 'in-transit');INSERT INTO takeaway_order VALUES (now(), 'order1', 'arrived');INSERT INTO takeaway_order VALUES (now(), 'order3', 'placed');INSERT INTO takeaway_order VALUES (now(), 'order3', 'cooking');INSERT INTO takeaway_order VALUES (now(), 'order3', 'in-transit'); We join the latest timestamp of an order id against the rest of the data to obtain full details. WITH ts_takeaway_order AS ( SELECT max(ts) AS ts, id FROM takeaway_order GROUP BY id)SELECT o.*FROM ts_takeaway_order ts_o INNER JOIN 'takeaway_order' o ON ts_o.ts = o.ts This results in the latest state for each order: | _timestamp_ ts | id _symbol_ | status _symbol_ | | --- | --- | --- | | 2022-04-07T15:33:43.944922Z | order1 | arrived | | 2022-04-07T15:33:37.370694Z | order2 | placed | | 2022-04-07T15:33:50.829323Z | order3 | in-transit | ### Using dedicated fields[​](https://questdb.com/docs/operations/modifying-data/#using-dedicated-fields "Direct link to Using dedicated fields") If timestamps don't work for you here, you can also use an extra integer column called `version`, an extra boolean `deleted` column or similar. Replace Table[​](https://questdb.com/docs/operations/modifying-data/#replace-table "Direct link to Replace Table") ------------------------------------------------------------------------------------------------------------------- Another alternative is to: * Backup your database. * Select only the data you want from an existing table into a new temporary one. * Drop the original table. * Rename the temporary table to the original table's name. CREATE TABLE mytable_copy AS ( SELECT * FROM mytable WHERE column_value != 42) TIMESTAMP(ts) PARTITION BY DAY;DROP TABLE mytable;RENAME table mytable_copy TO mytable; Delete by Dropping Partitions[​](https://questdb.com/docs/operations/modifying-data/#delete-by-dropping-partitions "Direct link to Delete by Dropping Partitions") ------------------------------------------------------------------------------------------------------------------------------------------------------------------- When you create tables with a timestamp, you may organise them into [partitions](https://questdb.com/docs/concepts/partitions/) using the [`CREATE TABLE .. PARTITION BY`](https://questdb.com/docs/query/sql/create-table/#partitioning) SQL statement. But first, [backup your database](https://questdb.com/docs/operations/backup/) . You may then use the [`ALTER TABLE DROP PARTITION`](https://questdb.com/docs/query/sql/alter-table-drop-partition/) SQL statement to drop partitions you no longer need. * [Alternatives to UPDATE](https://questdb.com/docs/operations/modifying-data/#alternatives-to-update) * [Append newest state](https://questdb.com/docs/operations/modifying-data/#append-newest-state) * [Using the timestamp field](https://questdb.com/docs/operations/modifying-data/#using-the-timestamp-field) * [Using dedicated fields](https://questdb.com/docs/operations/modifying-data/#using-dedicated-fields) * [Replace Table](https://questdb.com/docs/operations/modifying-data/#replace-table) * [Delete by Dropping Partitions](https://questdb.com/docs/operations/modifying-data/#delete-by-dropping-partitions) --- # List of OS error codes | QuestDB On this page The following document contains a partial list of Operating System (OS) error codes that can be reported when running QuestDB and brief descriptions for them. If instead you come across a QuestDB error code (e.g. `ER001`), refer to the [QuestDB Error Codes](https://questdb.com/docs/troubleshooting/error-codes/) page. Where to find error codes[​](https://questdb.com/docs/troubleshooting/os-error-codes/#where-to-find-error-codes "Direct link to Where to find error codes") ------------------------------------------------------------------------------------------------------------------------------------------------------------ QuestDB includes OS error codes into the `[]` part of the exception message written to the error logs: io.questdb.cairo.CairoException: [24] could not open read-only [file=/root/.questdb/db/cpu/service.k] The above message reports error code 24 which is "Too many open files" on Linux. Some error log messages may also include `errno=` key/value pair: 2022-02-01T13:40:10.636014Z E i.q.c.l.t.LineTcpConnectionContext [8655] could not process line data [table=test_table, msg=could not mmap [size=248, offset=0, fd=1766, memUsed=314809894008, fileLen=8192], errno=12] The above message reports error code 12 which is "Out of memory" on Linux. Linux error codes[​](https://questdb.com/docs/troubleshooting/os-error-codes/#linux-error-codes "Direct link to Linux error codes") ------------------------------------------------------------------------------------------------------------------------------------ | Error number | Error name | Description | | --- | --- | --- | | 1 | EPERM | Operation not permitted. | | 2 | ENOENT | No such file or directory. | | 3 | ESRCH | No such process. | | 4 | EINTR | Interrupted system call. | | 5 | EIO | I/O error. | | 6 | ENXIO | No such device or address. | | 7 | E2BIG | Argument list too long. | | 8 | ENOEXEC | Exec format error. | | 9 | EBADF | Bad file number. | | 10 | ECHILD | No child processes. | | 11 | EAGAIN | Try again. | | 12 | ENOMEM | Out of memory. | | 13 | EACCES | Permission denied. | | 14 | EFAULT | Bad address. | | 15 | ENOTBLK | Block device required. | | 16 | EBUSY | Device or resource busy. | | 17 | EEXIST | File exists. | | 18 | EXDEV | Cross-device link. | | 19 | ENODEV | No such device. | | 20 | ENOTDIR | Not a directory. | | 21 | EISDIR | Is a directory. | | 22 | EINVAL | Invalid argument. | | 23 | ENFILE | File table overflow. | | 24 | EMFILE | Too many open files. | | 25 | ENOTTY | Not a typewriter. | | 26 | ETXTBSY | Text file busy. | | 27 | EFBIG | File too large. | | 28 | ENOSPC | No space left on device. | | 29 | ESPIPE | Illegal seek. | | 30 | EROFS | Read-only file system. | | 31 | EMLINK | Too many links. | | 32 | EPIPE | Broken pipe. | | 33 | EDOM | Math argument out of domain of func. | | 34 | ERANGE | Math result not representable. | | 35 | EDEADLK | Resource deadlock would occur. | | 36 | ENAMETOOLONG | File name too long. | | 37 | ENOLCK | No record locks available. | | 38 | ENOSYS | Function not implemented. | | 39 | ENOTEMPTY | Directory not empty. | | 40 | ELOOP | Too many symbolic links encountered. | | 42 | ENOMSG | No message of desired type. | | 43 | EIDRM | Identifier removed. | | 44 | ECHRNG | Channel number out of range. | | 45 | EL2NSYNC | Level 2 not synchronized. | | 46 | EL3HLT | Level 3 halted. | | 47 | EL3RST | Level 3 reset. | | 48 | ELNRNG | Link number out of range. | | 49 | EUNATCH | Protocol driver not attached. | | 50 | ENOCSI | No CSI structure available. | | 51 | EL2HLT | Level 2 halted. | | 52 | EBADE | Invalid exchange. | | 53 | EBADR | Invalid request descriptor. | | 54 | EXFULL | Exchange full. | | 55 | ENOANO | No anode. | | 56 | EBADRQC | Invalid request code. | | 57 | EBADSLT | Invalid slot. | | 59 | EBFONT | Bad font file format. | | 60 | ENOSTR | Device not a stream. | | 61 | ENODATA | No data available. | | 62 | ETIME | Timer expired. | | 63 | ENOSR | Out of streams resources. | | 64 | ENONET | Machine is not on the network. | | 65 | ENOPKG | Package not installed. | | 66 | EREMOTE | Object is remote. | | 67 | ENOLINK | Link has been severed. | | 68 | EADV | Advertise error. | | 69 | ESRMNT | Srmount error. | | 70 | ECOMM | Communication error on send. | | 71 | EPROTO | Protocol error. | | 72 | EMULTIHOP | Multihop attempted. | | 73 | EDOTDOT | RFS specific error. | | 74 | EBADMSG | Not a data message. | | 75 | EOVERFLOW | Value too large for defined data type. | | 76 | ENOTUNIQ | Name not unique on network. | | 77 | EBADFD | File descriptor in bad state. | | 78 | EREMCHG | Remote address changed. | | 79 | ELIBACC | Can not access a needed shared library. | | 80 | ELIBBAD | Accessing a corrupted shared library. | | 81 | ELIBSCN | .lib section in a.out corrupted. | | 82 | ELIBMAX | Attempting to link in too many shared libraries. | | 83 | ELIBEXEC | Cannot exec a shared library directly. | | 84 | EILSEQ | Illegal byte sequence. | | 85 | ERESTART | Interrupted system call should be restarted. | | 86 | ESTRPIPE | Streams pipe error. | | 87 | EUSERS | Too many users. | | 88 | ENOTSOCK | Socket operation on non-socket. | | 89 | EDESTADDRREQ | Destination address required. | | 90 | EMSGSIZE | Message too long. | | 91 | EPROTOTYPE | Protocol wrong type for socket. | | 92 | ENOPROTOOPT | Protocol not available. | | 93 | EPROTONOSUPPORT | Protocol not supported. | | 94 | ESOCKTNOSUPPORT | Socket type not supported. | | 95 | EOPNOTSUPP | Operation not supported on transport endpoint. | | 96 | EPFNOSUPPORT | Protocol family not supported. | | 97 | EAFNOSUPPORT | Address family not supported by protocol. | | 98 | EADDRINUSE | Address already in use. | | 99 | EADDRNOTAVAIL | Cannot assign requested address. | | 100 | ENETDOWN | Network is down. | | 101 | ENETUNREACH | Network is unreachable. | | 102 | ENETRESET | Network dropped connection because of reset. | | 103 | ECONNABORTED | Software caused connection abort. | | 104 | ECONNRESET | Connection reset by peer. | | 105 | ENOBUFS | No buffer space available. | | 106 | EISCONN | Transport endpoint is already connected. | | 107 | ENOTCONN | Transport endpoint is not connected. | | 108 | ESHUTDOWN | Cannot send after transport endpoint shutdown. | | 109 | ETOOMANYREFS | Too many references: cannot splice. | | 110 | ETIMEDOUT | Connection timed out. | | 111 | ECONNREFUSED | Connection refused. | | 112 | EHOSTDOWN | Host is down. | | 113 | EHOSTUNREACH | No route to host. | | 114 | EALREADY | Operation already in progress. | | 115 | EINPROGRESS | Operation now in progress. | | 116 | ESTALE | Stale NFS file handle. | | 117 | EUCLEAN | Structure needs cleaning. | | 118 | ENOTNAM | Not a XENIX named type file. | | 119 | ENAVAIL | No XENIX semaphores available. | | 120 | EISNAM | Is a named type file. | | 121 | EREMOTEIO | Remote I/O error. | | 122 | EDQUOT | Quota exceeded. | | 123 | ENOMEDIUM | No medium found. | | 124 | EMEDIUMTYPE | Wrong medium type. | | 125 | ECANCELED | Operation Canceled. | | 126 | ENOKEY | Required key not available. | | 127 | EKEYEXPIRED | Key has expired. | | 128 | EKEYREVOKED | Key has been revoked. | | 129 | EKEYREJECTED | Key was rejected by service. | | 130 | EOWNERDEAD | Owner died. | | 131 | ENOTRECOVERABLE | State not recoverable. | Windows error codes[​](https://questdb.com/docs/troubleshooting/os-error-codes/#windows-error-codes "Direct link to Windows error codes") ------------------------------------------------------------------------------------------------------------------------------------------ A complete list of Windows error codes may be found [here](https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes) . | Error number | Error name | Description | | --- | --- | --- | | 1 | ERROR\_INVALID\_FUNCTION | Incorrect function. | | 2 | ERROR\_FILE\_NOT\_FOUND | The system cannot find the file specified. | | 3 | ERROR\_PATH\_NOT\_FOUND | The system cannot find the path specified. | | 4 | ERROR\_TOO\_MANY\_OPEN\_FILES | The system cannot open the file. | | 5 | ERROR\_ACCESS\_DENIED | Access is denied. | | 6 | ERROR\_INVALID\_HANDLE | The handle is invalid. | | 7 | ERROR\_ARENA\_TRASHED | The storage control blocks were destroyed. | | 8 | ERROR\_NOT\_ENOUGH\_MEMORY | Not enough memory is available to process this command. | | 9 | ERROR\_INVALID\_BLOCK | The storage control block address is invalid. | | 10 | ERROR\_BAD\_ENVIRONMENT | The environment is incorrect. | | 11 | ERROR\_BAD\_FORMAT | An attempt was made to load a program with an incorrect format. | | 12 | ERROR\_INVALID\_ACCESS | The access code is invalid. | | 13 | ERROR\_INVALID\_DATA | The data is invalid. | | 14 | ERROR\_OUTOFMEMORY | Not enough storage is available to complete this operation. | | 15 | ERROR\_INVALID\_DRIVE | The system cannot find the drive specified. | | 16 | ERROR\_CURRENT\_DIRECTORY | The directory cannot be removed. | | 17 | ERROR\_NOT\_SAME\_DEVICE | The system cannot move the file to a different disk drive. | | 18 | ERROR\_NO\_MORE\_FILES | There are no more files. | | 19 | ERROR\_WRITE\_PROTECT | The media is write protected. | | 20 | ERROR\_BAD\_UNIT | The system cannot find the device specified. | | 21 | ERROR\_NOT\_READY | The device is not ready. | | 22 | ERROR\_BAD\_COMMAND | The device does not recognize the command. | | 23 | ERROR\_CRC | Data error (cyclic redundancy check). | | 24 | ERROR\_BAD\_LENGTH | The program issued a command but the command length is incorrect. | | 25 | ERROR\_SEEK | The drive cannot locate a specific area or track on the disk. | | 26 | ERROR\_NOT\_DOS\_DISK | The specified disk or diskette cannot be accessed. | | 27 | ERROR\_SECTOR\_NOT\_FOUND | The drive cannot find the sector requested. | | 28 | ERROR\_OUT\_OF\_PAPER | The printer is out of paper. | | 29 | ERROR\_WRITE\_FAULT | The system cannot write to the specified device. | | 30 | ERROR\_READ\_FAULT | The system cannot read from the specified device. | | 31 | ERROR\_GEN\_FAILURE | A device attached to the system is not functioning. | | 32 | ERROR\_SHARING\_VIOLATION | The process cannot access the file because it is being used by another process. | | 33 | ERROR\_LOCK\_VIOLATION | The process cannot access the file because another process has locked a portion of the file. | | 34 | ERROR\_WRONG\_DISK | The wrong diskette is in the drive. Insert %2 (Volume Serial Number: %3) into drive %1. | | 36 | ERROR\_SHARING\_BUFFER\_EXCEEDED | Too many files opened for sharing. | | 38 | ERROR\_HANDLE\_EOF | Reached the end of the file. | | 39 | ERROR\_HANDLE\_DISK\_FULL | The disk is full. | | 87 | ERROR\_INVALID\_PARAMETER | The parameter is incorrect. | | 112 | ERROR\_DISK\_FULL | The disk is full. | | 123 | ERROR\_INVALID\_NAME | The file name, directory name, or volume label syntax is incorrect. | | 1450 | ERROR\_NO\_SYSTEM\_RESOURCES | Insufficient system resources exist to complete the requested service. | MacOS error codes[​](https://questdb.com/docs/troubleshooting/os-error-codes/#macos-error-codes "Direct link to MacOS error codes") ------------------------------------------------------------------------------------------------------------------------------------ | Error number | Error name | Description | | --- | --- | --- | | 0 | Base | Undefined error: 0 | | 1 | EPERM | Operation not permitted | | 2 | ENOENT | No such file or directory | | 3 | ESRCH | No such process | | 4 | EINTR | Interrupted system call | | 5 | EIO | Input/output error | | 6 | ENXIO | Device not configured | | 7 | E2BIG | Argument list too long | | 8 | ENOEXEC | Exec format error | | 9 | EBADF | Bad file descriptor | | 10 | ECHILD | No child processes | | 11 | EDEADLK | Resource deadlock avoided | | 12 | ENOMEM | Cannot allocate memory | | 13 | EACCES | Permission denied | | 14 | EFAULT | Bad address | | 15 | ENOTBLK | Block device required | | 16 | EBUSY | Device busy | | 17 | EEXIST | File exists | | 18 | EXDEV | Cross-device link | | 19 | ENODEV | Operation not supported by device | | 20 | ENOTDIR | Not a directory | | 21 | EISDIR | Is a directory | | 22 | EINVAL | Invalid argument | | 23 | ENFILE | Too many open files in system | | 24 | EMFILE | Too many open files | | 25 | ENOTTY | Inappropriate ioctl for device | | 26 | ETXTBSY | Text file busy | | 27 | EFBIG | File too large | | 28 | ENOSPC | No space left on device | | 29 | ESPIPE | Illegal seek | | 30 | EROFS | Read-only file system | | 31 | EMLINK | Too many links | | 32 | EPIPE | Broken pipe | | 33 | EDOM | Numerical argument out of domain | | 34 | ERANGE | Result too large | | 35 | EAGAIN | Resource temporarily unavailable | | 36 | EINPROGRESS | Operation now in progress | | 37 | EALREADY | Operation already in progress | | 38 | ENOTSOCK | Socket operation on non-socket | | 39 | EDESTADDRREQ | Destination address required | | 40 | EMSGSIZE | Message too long | | 41 | EPROTOTYPE | Protocol wrong type for socket | | 42 | ENOPROTOOPT | Protocol not available | | 43 | EPROTONOSUPPORT | Protocol not supported | | 44 | ESOCKTNOSUPPORT | Socket type not supported | | 45 | ENOTSUP | Operation not supported | | 46 | EPFNOSUPPORT | Protocol family not supported | | 47 | EAFNOSUPPORT | Address family not supported by protocol family | | 48 | EADDRINUSE | Address already in use | | 49 | EADDRNOTAVAIL | Can’t assign requested address | | 50 | ENETDOWN | Network is down | | 51 | ENETUNREACH | Network is unreachable | | 52 | ENETRESET | Network dropped connection on reset | | 53 | ECONNABORTED | Software caused connection abort | | 54 | ECONNRESET | Connection reset by peer | | 55 | ENOBUFS | No buffer space available | | 56 | EISCONN | Socket is already connected | | 57 | ENOTCONN | Socket is not connected | | 58 | ESHUTDOWN | Can’t send after socket shutdown | | 59 | ETOOMANYREFS | Too many references: can’t splice | | 60 | ETIMEDOUT | Operation timed out | | 61 | ECONNREFUSED | Connection refused | | 62 | ELOOP | Too many levels of symbolic links | | 63 | ENAMETOOLONG | File name too long | | 64 | EHOSTDOWN | Host is down | | 65 | EHOSTUNREACH | No route to host | | 66 | ENOTEMPTY | Directory not empty | | 67 | EPROCLIM | Too many processes | | 68 | EUSERS | Too many users | | 69 | EDQUOT | Disc quota exceeded | | 70 | ESTALE | Stale NFS file handle | | 71 | EREMOTE | Too many levels of remote in path | | 72 | EBADRPC | RPC struct is bad | | 73 | ERPCMISMATCH | RPC version wrong | | 74 | EPROGUNAVAIL | RPC prog. not avail | | 75 | EPROGMISMATCH | Program version wrong | | 76 | EPROCUNAVAIL | Bad procedure for program | | 77 | ENOLCK | No locks available | | 78 | ENOSYS | Function not implemented | | 79 | EFTYPE | Inappropriate file type or format | | 80 | EAUTH | Authentication error | | 81 | ENEEDAUTH | Need authenticator | | 82 | EPWROFF | Device power is off | | 83 | EDEVERR | Device error | | 84 | EOVERFLOW | Value too large to be stored in data type | | 85 | EBADEXEC | Bad executable | | 86 | EBADARCH | Bad CPU type in executable | | 87 | ESHLIBVERS | Shared library version mismatch | | 88 | EBADMACHO | Malformed Macho file | | 89 | ECANCELED | Operation canceled | | 90 | EIDRM | Identifier removed | | 91 | ENOMSG | No message of desired type | | 92 | EILSEQ | Illegal byte sequence | | 93 | ENOATTR | Attribute not found | | 94 | EBADMSG | Bad message | | 95 | EMULTIHOP | EMULTIHOP (Reserved) | | 96 | ENODATA | No message available on STREAM | | 97 | ENOLINK | ENOLINK (Reserved) | | 98 | ENOSR | No STREAM resources | | 99 | ENOSTR | Not a STREAM | | 100 | EPROTO | Protocol error | | 101 | ETIME | STREAM ioctl timeout | | 102 | EOPNOTSUPP | Operation not supported on socket | | 103 | ENOPOLICY | Policy not found | | 104 | ENOTRECOVERABLE | State not recoverable | | 105 | EOWNERDEAD | Previous owner died | | 106 | EQFULL | Interface output queue is full | * [Where to find error codes](https://questdb.com/docs/troubleshooting/os-error-codes/#where-to-find-error-codes) * [Linux error codes](https://questdb.com/docs/troubleshooting/os-error-codes/#linux-error-codes) * [Windows error codes](https://questdb.com/docs/troubleshooting/os-error-codes/#windows-error-codes) * [MacOS error codes](https://questdb.com/docs/troubleshooting/os-error-codes/#macos-error-codes) --- # ECN scorecard | QuestDB On this page When evaluating execution across multiple venues, you often need several metrics side by side: spread conditions, slippage, fill sizes, and order type mix. Rather than running separate queries, this recipe produces a single **ECN scorecard** that summarizes fill quality per venue and symbol. Problem[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#problem "Direct link to Problem") ---------------------------------------------------------------------------------------------------------- You want a single dashboard-ready query that ranks venues by execution quality, combining spread at fill time, slippage against mid and top of book, average fill size, and what proportion of fills were passive. Solution[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#solution "Direct link to Solution") ------------------------------------------------------------------------------------------------------------- Use `ASOF JOIN` to pair each fill with the prevailing order book, then aggregate multiple metrics per ECN and symbol: ECN fill quality scorecard (buy side)[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20count()%20AS%20fill_count%2C%0A%20%20%20%20sum(t.quantity)%20AS%20total_volume%2C%0A%20%20%20%20avg(t.quantity)%20AS%20avg_fill_size%2C%0A%20%20%20%20avg((m.best_ask%20-%20m.best_bid)%0A%20%20%20%20%20%20%20%20%2F%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202)%20*%2010000)%20AS%20avg_spread_bps%2C%0A%20%20%20%20avg(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000)%20AS%20avg_slippage_bps%2C%0A%20%20%20%20avg((m.best_ask%20-%20t.price)%0A%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000)%20AS%20avg_slippage_vs_ask_bps%2C%0A%20%20%20%20avg(CASE%20WHEN%20t.passive%20THEN%201.0%20ELSE%200.0%20END)%20AS%20passive_ratio%0AFROM%20fx_trades%20t%0AASOF%20JOIN%20market_data%20m%20ON%20(symbol)%0AWHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24yesterday%27%0AGROUP%20BY%20t.symbol%2C%20t.ecn%0AORDER%20BY%20t.symbol%2C%20avg_slippage_bps%3B&executeQuery=true) SELECT t.symbol, t.ecn, count() AS fill_count, sum(t.quantity) AS total_volume, avg(t.quantity) AS avg_fill_size, avg((m.best_ask - m.best_bid) / ((m.best_bid + m.best_ask) / 2) * 10000) AS avg_spread_bps, avg(((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000) AS avg_slippage_bps, avg((m.best_ask - t.price) / t.price * 10000) AS avg_slippage_vs_ask_bps, avg(CASE WHEN t.passive THEN 1.0 ELSE 0.0 END) AS passive_ratioFROM fx_trades tASOF JOIN market_data m ON (symbol)WHERE t.side = 'buy' AND t.timestamp IN '$yesterday'GROUP BY t.symbol, t.ecnORDER BY t.symbol, avg_slippage_bps; How it works[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#how-it-works "Direct link to How it works") ------------------------------------------------------------------------------------------------------------------------- Each row is one symbol-ECN combination. The metrics in each row: * **`fill_count`** and **`total_volume`** — how much activity the ECN sees for this symbol. Context for statistical significance. * **`avg_fill_size`** — average quantity per fill. Venues with larger average fills may show more slippage simply due to size. * **`avg_spread_bps`** — average spread at the time of each fill. Tells you what market conditions looked like when you traded on this venue. * **`avg_slippage_bps`** — average slippage vs mid. Since this is buy-side, negative means you bought below mid (price improvement), positive means you paid above mid. * **`avg_slippage_vs_ask_bps`** — average slippage vs the best ask. Isolates how much worse than the quoted ask you actually paid. Negative means you got price improvement vs the ask. * **`passive_ratio`** — fraction of fills that were passive (limit orders). Higher passive ratio typically correlates with better slippage. Results are ordered by `avg_slippage_bps` so the best-performing ECN for each symbol appears first. Buy-side only This query filters to `side = 'buy'` because the slippage formulas are direction-specific (no `CASE` expression). For a sell-side scorecard, flip the slippage formulas: use `(t.price - mid) / t.price` for slippage vs mid, and `(t.price - m.best_bid) / t.price` for slippage vs bid. Interpreting results[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#interpreting-results "Direct link to Interpreting results") ------------------------------------------------------------------------------------------------------------------------------------------------- Compare rows for the same symbol across different ECNs: * **Low spread + low slippage**: The best combination — tight market and good fills. * **Low spread + high slippage**: Tight quotes but fills executing poorly. May indicate latency issues or thin top-of-book liquidity. * **High passive ratio + negative slippage**: Expected — passive fills provide liquidity and often get price improvement. * **Large `avg_fill_size` + high slippage**: Size-driven impact. The venue may have less depth, causing larger orders to walk the book. * **Low `fill_count`**: Treat metrics with caution — small sample sizes can be misleading. ECN markout curves[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#ecn-markout-curves "Direct link to ECN markout curves") ------------------------------------------------------------------------------------------------------------------------------------------- The scorecard above is a static snapshot. To see how fill quality evolves over time after execution, overlay markout curves per ECN. An ECN where markouts go steeply negative is delivering toxic flow — informed traders are picking you off there: ECN markout curves side by side (buy side)[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000)%20AS%20avg_markout_bps%2C%0A%20%20%20%20sum(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20*%20t.quantity)%20AS%20total_pnl%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20RANGE%20FROM%200s%20TO%205m%20STEP%205s%20AS%20h%0AWHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24yesterday%27%0AGROUP%20BY%20t.symbol%2C%20t.ecn%2C%20horizon_sec%0AORDER%20BY%20t.symbol%2C%20t.ecn%2C%20horizon_sec%3B&executeQuery=true) SELECT t.symbol, t.ecn, h.offset / 1000000000 AS horizon_sec, count() AS n, avg(((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000) AS avg_markout_bps, sum(((m.best_bid + m.best_ask) / 2 - t.price) * t.quantity) AS total_pnlFROM fx_trades tHORIZON JOIN market_data m ON (symbol) RANGE FROM 0s TO 5m STEP 5s AS hWHERE t.side = 'buy' AND t.timestamp IN '$yesterday'GROUP BY t.symbol, t.ecn, horizon_secORDER BY t.symbol, t.ecn, horizon_sec; Plot these curves overlaid per ECN for each symbol. Compare the shapes: * **Flat near zero**: Neutral flow — no systematic post-trade price movement. This is healthy. * **Rising (positive)**: Mean-reverting flow — the market comes back after the fill. You're providing liquidity at good levels on this venue. * **Falling (negative)**: Toxic flow — the market moves against you after fills on this ECN. Informed traders may be concentrated there. * **Sharp initial drop then flat**: The initial cost is the spread, and the market doesn't move further. Normal for aggressive fills on a well-functioning venue. Combine with the scorecard's `passive_ratio` and `avg_fill_size` to understand _why_ a venue shows toxicity — it may simply be where your largest aggressive orders execute, rather than a venue-specific problem. Toxicity by time of day[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#toxicity-by-time-of-day "Direct link to Toxicity by time of day") ---------------------------------------------------------------------------------------------------------------------------------------------------------- Toxicity isn't static — an ECN may show clean markouts during London hours but turn toxic during Asia when liquidity thins out. Grouping by hour reveals intraday patterns: ECN toxicity by hour (buy side)[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20hour(t.timestamp)%20AS%20hour_utc%2C%0A%20%20%20%20h.offset%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000)%20AS%20markout_5s_bps%2C%0A%20%20%20%20avg((m.best_ask%20-%20m.best_bid)%0A%20%20%20%20%20%20%20%20%2F%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202)%20*%2010000)%20AS%20avg_spread_bps%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20LIST%20(5s)%20AS%20h%0AWHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24yesterday%27%0AGROUP%20BY%20t.symbol%2C%20t.ecn%2C%20hour(t.timestamp)%2C%20h.offset%0AORDER%20BY%20t.symbol%2C%20t.ecn%2C%20hour_utc%3B&executeQuery=true) SELECT t.symbol, t.ecn, hour(t.timestamp) AS hour_utc, h.offset, count() AS n, avg(((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000) AS markout_5s_bps, avg((m.best_ask - m.best_bid) / ((m.best_bid + m.best_ask) / 2) * 10000) AS avg_spread_bpsFROM fx_trades tHORIZON JOIN market_data m ON (symbol) LIST (5s) AS hWHERE t.side = 'buy' AND t.timestamp IN '$yesterday'GROUP BY t.symbol, t.ecn, hour(t.timestamp), h.offsetORDER BY t.symbol, t.ecn, hour_utc; The 5-second markout is used as a quick toxicity signal — long enough for informed flow to show up, short enough to stay responsive. Compare `markout_5s_bps` against `avg_spread_bps` for each hour. If an ECN shows tight spreads but deeply negative markouts during certain hours, the tight spreads are bait — you're earning a small spread but losing much more to adverse selection. Consider reducing or withdrawing liquidity on that venue during those hours. Passive vs aggressive toxicity[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#passive-vs-aggressive-toxicity "Direct link to Passive vs aggressive toxicity") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The aggregate markout curves above blend passive and aggressive fills together. Splitting by `t.passive` reveals a critical distinction — toxicity on passive fills means your resting orders are being picked off, while toxicity on aggressive fills means you're crossing into a market that moves against you immediately: Passive vs aggressive toxicity per ECN (buy side)[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20t.passive%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000)%20AS%20avg_markout_bps%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20LIST%20(0%2C%201s%2C%205s%2C%2010s%2C%201m)%20AS%20h%0AWHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24yesterday%27%0AGROUP%20BY%20t.symbol%2C%20t.ecn%2C%20t.passive%2C%20horizon_sec%0AORDER%20BY%20t.symbol%2C%20t.ecn%2C%20t.passive%2C%20horizon_sec%3B&executeQuery=true) SELECT t.symbol, t.ecn, t.passive, h.offset / 1000000000 AS horizon_sec, count() AS n, avg(((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000) AS avg_markout_bpsFROM fx_trades tHORIZON JOIN market_data m ON (symbol) LIST (0, 1s, 5s, 10s, 1m) AS hWHERE t.side = 'buy' AND t.timestamp IN '$yesterday'GROUP BY t.symbol, t.ecn, t.passive, horizon_secORDER BY t.symbol, t.ecn, t.passive, horizon_sec; Compare the markout curves for `passive = true` vs `passive = false` on each ECN: * **Healthy passive fills**: Positive markout at offset 0 (you earned the spread), gradually decaying toward zero. You rested at a good level and the market didn't move against you. * **Toxic passive fills**: Markout turns negative quickly. Someone on that ECN is systematically sniping your resting orders — they trade against you just before the market moves in their direction. * **Healthy aggressive fills**: Small negative markout at offset 0 (you paid the spread), staying flat or recovering. Normal cost of crossing. * **Toxic aggressive fills**: Markout becomes increasingly negative. The market continues to move against you after you cross, suggesting you're consistently late or trading against informed flow. An ECN showing clean aggregate markouts can still have a problem if passive fills are deeply toxic while aggressive fills look fine — the two patterns cancel out in the blend. Always check both sides separately. Composite toxicity score[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#composite-toxicity-score "Direct link to Composite toxicity score") ------------------------------------------------------------------------------------------------------------------------------------------------------------- Rank ECNs by a single toxicity metric — the volume-weighted 5-second markout — alongside an `adverse_fill_ratio` that shows what fraction of fills moved against you: Composite toxicity score per ECN (buy side)[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20h.offset%2C%0A%20%20%20%20count()%20AS%20fill_count%2C%0A%20%20%20%20sum(t.quantity)%20AS%20total_volume%2C%0A%20%20%20%20sum(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%20*%20t.quantity)%0A%20%20%20%20%20%20%20%20%2F%20sum(t.quantity)%20AS%20vw_markout_5s_bps%2C%0A%20%20%20%20avg(CASE%0A%20%20%20%20%20%20%20%20WHEN%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202%20%3C%20t.price%20THEN%201.0%0A%20%20%20%20%20%20%20%20ELSE%200.0%0A%20%20%20%20END)%20AS%20adverse_fill_ratio%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20LIST%20(5s)%20AS%20h%0AWHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24yesterday%27%0AGROUP%20BY%20t.symbol%2C%20t.ecn%2C%20h.offset%0AORDER%20BY%20t.symbol%2C%20vw_markout_5s_bps%3B&executeQuery=true) SELECT t.symbol, t.ecn, h.offset, count() AS fill_count, sum(t.quantity) AS total_volume, sum(((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000 * t.quantity) / sum(t.quantity) AS vw_markout_5s_bps, avg(CASE WHEN (m.best_bid + m.best_ask) / 2 < t.price THEN 1.0 ELSE 0.0 END) AS adverse_fill_ratioFROM fx_trades tHORIZON JOIN market_data m ON (symbol) LIST (5s) AS hWHERE t.side = 'buy' AND t.timestamp IN '$yesterday'GROUP BY t.symbol, t.ecn, h.offsetORDER BY t.symbol, vw_markout_5s_bps; The two metrics complement each other: * **`vw_markout_5s_bps`** — volume-weighted 5-second markout in basis points. Negative means the market moved against you after fills on this ECN. Volume-weighting ensures large fills dominate the score. * **`adverse_fill_ratio`** — fraction of fills where the mid-price at 5 seconds was worse than the execution price. Tells you whether toxicity is driven by a few large bad fills or is systemic across the board. An ECN with a mildly negative `vw_markout_5s_bps` but 80%+ `adverse_fill_ratio` is fundamentally hostile — nearly every fill moves against you, even if the average magnitude is small. Conversely, a deeply negative `vw_markout_5s_bps` with a low `adverse_fill_ratio` suggests a few large toxic fills are dragging down the average, which may be addressable by adjusting size limits on that venue. Pivoted ECN scorecard[​](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#pivoted-ecn-scorecard "Direct link to Pivoted ECN scorecard") ---------------------------------------------------------------------------------------------------------------------------------------------------- The sections above produce one row per ECN per horizon offset. Using `PIVOT`, you can reshape the results into a wide format — one row per symbol-ECN combination with fill count, average size, volume, and markout at each horizon as separate columns: Pivoted ECN scorecard (buy side)[Demo this query](https://demo.questdb.io/?query=WITH%20markouts%20AS%20(%0A%20%20%20%20SELECT%0A%20%20%20%20%20%20%20%20t.symbol%2C%0A%20%20%20%20%20%20%20%20t.ecn%2C%0A%20%20%20%20%20%20%20%20t.price%2C%0A%20%20%20%20%20%20%20%20t.quantity%2C%0A%20%20%20%20%20%20%20%20h.offset%2C%0A%20%20%20%20%20%20%20%20m.best_bid%2C%0A%20%20%20%20%20%20%20%20m.best_ask%0A%20%20%20%20FROM%20fx_trades%20t%0A%20%20%20%20HORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20%20%20%20%20LIST%20(0%2C%205s%2C%201m)%20AS%20h%0A%20%20%20%20WHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20%20%20%20%20AND%20t.timestamp%20IN%20%27%24yesterday%27%0A)%0ASELECT%20*%20FROM%20markouts%0APIVOT%20(%0A%20%20%20%20count()%20AS%20fills%2C%0A%20%20%20%20avg(quantity)%20AS%20avg_size%2C%0A%20%20%20%20sum(quantity)%20AS%20volume%2C%0A%20%20%20%20avg(((best_bid%20%2B%20best_ask)%20%2F%202%20-%20price)%20%2F%20price%20*%2010000)%20AS%20markout_bps%0A%20%20%20%20FOR%20offset%20IN%20(0%20AS%20at_fill%2C%205000000000%20AS%20t_5s%2C%2060000000000%20AS%20t_1m)%0A%20%20%20%20GROUP%20BY%20symbol%2C%20ecn%0A)%0AORDER%20BY%20t_5s_markout_bps%3B&executeQuery=true) WITH markouts AS ( SELECT t.symbol, t.ecn, t.price, t.quantity, h.offset, m.best_bid, m.best_ask FROM fx_trades t HORIZON JOIN market_data m ON (symbol) LIST (0, 5s, 1m) AS h WHERE t.side = 'buy' AND t.timestamp IN '$yesterday')SELECT * FROM markoutsPIVOT ( count() AS fills, avg(quantity) AS avg_size, sum(quantity) AS volume, avg(((best_bid + best_ask) / 2 - price) / price * 10000) AS markout_bps FOR offset IN (0 AS at_fill, 5000000000 AS t_5s, 60000000000 AS t_1m) GROUP BY symbol, ecn)ORDER BY t_5s_markout_bps; The result has columns like `at_fill_fills`, `at_fill_markout_bps`, `t_5s_markout_bps`, `t_1m_markout_bps`, etc. — one set per horizon. This is useful for dashboard views where you want a single wide table rather than long-form output. Raw markouts can be misleading if an ECN rejects most of your flow and only fills the toxic orders. Compare `at_fill_fills` and `at_fill_avg_size` across ECNs — an ECN that fills fewer, smaller orders but shows clean markouts may simply be rejecting the hard-to-fill flow. A more complete picture requires comparing fill sizes against quoted sizes or incorporating reject rates from an orders table. Related documentation * [ASOF JOIN](https://questdb.com/docs/query/sql/asof-join/) * [HORIZON JOIN](https://questdb.com/docs/query/sql/horizon-join/) * [Slippage per fill recipe](https://questdb.com/docs/cookbook/sql/finance/slippage/) * [Markout analysis recipe](https://questdb.com/docs/cookbook/sql/finance/markout/) * [Bid-ask spread recipe](https://questdb.com/docs/cookbook/sql/finance/bid-ask-spread/) * [Problem](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#problem) * [Solution](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#solution) * [How it works](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#how-it-works) * [Interpreting results](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#interpreting-results) * [ECN markout curves](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#ecn-markout-curves) * [Toxicity by time of day](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#toxicity-by-time-of-day) * [Passive vs aggressive toxicity](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#passive-vs-aggressive-toxicity) * [Composite toxicity score](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#composite-toxicity-score) * [Pivoted ECN scorecard](https://questdb.com/docs/cookbook/sql/finance/ecn-scorecard/#pivoted-ecn-scorecard) --- # Client configuration string | QuestDB On this page You configure a QuestDB ingestion client with a configuration string. The syntax is the same in all clients, and there are a number of common options. There are also language-specific settings. This document provides a general overview and documents the common options. Configuration string breakdown[​](https://questdb.com/docs/ingestion/clients/configuration-string/#configuration-string-breakdown "Direct link to Configuration string breakdown") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- These are the common configuration options. ### Protocol Version[​](https://questdb.com/docs/ingestion/clients/configuration-string/#protocol-version "Direct link to Protocol Version") `protocol_version` — sets the line protocol version Valid options are: | Value | Behavior | QuestDB Version | | --- | --- | --- | | `1` | \- plain-text serialization
\- compatible with InfluxDB servers
\- no array type support | all | | `2` | \- binary encoding for f64
\- full support for array | \>=9.0.0 | | `auto` | \- **HTTP/HTTPS**: negotiates the best version with the server
\- **TCP/TCPS**: no negotiation, uses version 1 | | ### HTTP transport authentication[​](https://questdb.com/docs/ingestion/clients/configuration-string/#http-transport-authentication "Direct link to HTTP transport authentication") * `username` — username for HTTP basic authentication * `password` — password for HTTP basic authentication * `token` — bearer token for HTTP authentication ### TCP transport authentication[​](https://questdb.com/docs/ingestion/clients/configuration-string/#tcp-transport-authentication "Direct link to TCP transport authentication") * `username` — username for TCP authentication * `token` — token for TCP authentication ### Auto-flushing[​](https://questdb.com/docs/ingestion/clients/configuration-string/#auto-flushing "Direct link to Auto-flushing") * `auto_flush` — global switch for the auto-flushing behavior. Options are `on` or `off`. Defaults to `on` * `auto_flush_rows` — number of rows that will trigger a flush. This option is supported for HTTP transport only. Defaults to 75,000 * `auto_flush_interval` — time in milliseconds that will trigger a flush. Defaults to 1000. Used only for HTTP transport When using the TCP transport, the client automatically flushes when its buffer is full. It uses a fixed-size buffer, whose size you can set with `init_buf_size` (see below). ### Buffer[​](https://questdb.com/docs/ingestion/clients/configuration-string/#buffer "Direct link to Buffer") * `init_buf_size` — initial size of the buffer in bytes. Default: 65536 (64KiB). Also sets the fixed buffer size for TCP transport * `max_buf_size` — maximum size of the buffer in bytes. Default: 104857600 (100MiB). Used only for HTTP transport ### HTTP Transport[​](https://questdb.com/docs/ingestion/clients/configuration-string/#http-transport "Direct link to HTTP Transport") * `retry_timeout` — time in milliseconds to continue retrying after a failed HTTP request. The interval between retries is an exponential backoff starting at 10ms and doubling after each failed attempt up to a maximum of 1 second. Default: 10000 (10 seconds) * `request_timeout` — time in milliseconds to wait for a response from the server. This is in addition to the calculation derived from the `request_min_throughput` parameter. Default: 10000 (10 seconds) * `request_min_throughput` — minimum expected throughput in bytes per second for HTTP requests. If the throughput is lower than this value, the connection will time out. This is used to calculate an additional timeout on top of `request_timeout`. This is useful for large requests. You can set this value to `0` to disable this logic ### TLS encryption[​](https://questdb.com/docs/ingestion/clients/configuration-string/#tls-encryption "Direct link to TLS encryption") To enable TLS, select the `https` or `tcps` protocol. The following options are available: * `tls_roots` — path to a Java keystore file containing trusted root certificates. Defaults to the system default trust store * `tls_roots_password` — password for the keystore file. It's always required when `tls_roots` is set * `tls_verify` — whether to verify the server's certificate. This should only be used for testing as a last resort and never used in production as it makes the connection vulnerable to man-in-the-middle attacks. Options are `on` or `unsafe_off`. Defaults to `on` Other considerations[​](https://questdb.com/docs/ingestion/clients/configuration-string/#other-considerations "Direct link to Other considerations") ----------------------------------------------------------------------------------------------------------------------------------------------------- * Please refer to the [ILP overview](https://questdb.com/docs/ingestion/ilp/overview/) for details about transactions, error control, delivery guarantees, health check, or table and column auto-creation. * The method `flush()` can be called to force sending the internal buffer to a server, even when the buffer is not full yet. * [Configuration string breakdown](https://questdb.com/docs/ingestion/clients/configuration-string/#configuration-string-breakdown) * [Protocol Version](https://questdb.com/docs/ingestion/clients/configuration-string/#protocol-version) * [HTTP transport authentication](https://questdb.com/docs/ingestion/clients/configuration-string/#http-transport-authentication) * [TCP transport authentication](https://questdb.com/docs/ingestion/clients/configuration-string/#tcp-transport-authentication) * [Auto-flushing](https://questdb.com/docs/ingestion/clients/configuration-string/#auto-flushing) * [Buffer](https://questdb.com/docs/ingestion/clients/configuration-string/#buffer) * [HTTP Transport](https://questdb.com/docs/ingestion/clients/configuration-string/#http-transport) * [TLS encryption](https://questdb.com/docs/ingestion/clients/configuration-string/#tls-encryption) * [Other considerations](https://questdb.com/docs/ingestion/clients/configuration-string/#other-considerations) --- # QuestDB Enterprise quick start | QuestDB On this page QuestDB Enterprise offers the entire feature set of QuestDB open source, with premium additions. This guide will walk you through a basic Enterprise setup. Each production configuration will be unique, however these examples will help inform your own unique choices. * * * [Requirements](https://questdb.com/docs/getting-started/enterprise-quick-start/#requirements) [0\. Secure the built in admin](https://questdb.com/docs/getting-started/enterprise-quick-start/#0-secure-the-built-in-admin) [1\. Setup TLS](https://questdb.com/docs/getting-started/enterprise-quick-start/#1-setup-tls) [2\. Setup a database administrator](https://questdb.com/docs/getting-started/enterprise-quick-start/#2-setup-a-database-administrator) [3\. Create interactive user accounts](https://questdb.com/docs/getting-started/enterprise-quick-start/#3-create-interactive-user-accounts) [4\. Ingest data, InfluxDB Line Protocol](https://questdb.com/docs/getting-started/enterprise-quick-start/#4-ingest-data-influxdb-line-protocol) [5\. Ingest data, Kafka Connect (optional)](https://questdb.com/docs/getting-started/enterprise-quick-start/#5-ingest-data-kafka-connect-optional) [6\. Query data, PostgreSQL query](https://questdb.com/docs/getting-started/enterprise-quick-start/#6-query-data-postgresql-query) [7\. Setup replication](https://questdb.com/docs/getting-started/enterprise-quick-start/#7-setup-replication) [8\. Enable compression](https://questdb.com/docs/getting-started/enterprise-quick-start/#8-enable-compression) [9\. Double-check kernel limits](https://questdb.com/docs/getting-started/enterprise-quick-start/#9-double-check-kernel-limits) [Next steps](https://questdb.com/docs/getting-started/enterprise-quick-start/#next-steps) [FAQ](https://questdb.com/docs/getting-started/enterprise-quick-start/#faq) * * * Requirements[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#requirements "Direct link to Requirements") ----------------------------------------------------------------------------------------------------------------------------- The following are required prior to following this guide: * QuestDB Enterprise binary with an active license * No license? [Contact us](https://questdb.com/enterprise/contact/) for more information. * Use of a [supported file system](https://questdb.com/docs/getting-started/capacity-planning/#supported-filesystems) * A [Zettabyte File System (ZFS)](https://openzfs.org/wiki/Main_Page) is recommended to enable compression Installation guide[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#installation-guide "Direct link to Installation guide") ----------------------------------------------------------------------------------------------------------------------------------------------- Changes take place in your `conf/server.conf` file, the QuestDB [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) , your app code or third-party tool. Check the code snippet's title to see where the command is to be invoked. If you run into any trouble, please [contact us](mailto:support@questdb.io) by email or visit the [Community Forum](https://community.questdb.com/) . 0\. Secure the built in admin[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#0-secure-the-built-in-admin "Direct link to 0. Secure the built in admin") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB Enterprise provides a built-in administrator account. By default, it has the login `admin` and the password `quest`. Before you go any further, please **change the default password**! Consider changing the name, too. To change these values, swap your own in place of `myadmin` and `my_very_secure_pwd`: server.conf - Securing built-in admin account # the built-in admin's user name and passwordacl.admin.user=myadminacl.admin.password=my_very_secure_pwd Kubernetes deployments In Kubernetes, you can read the password from a mounted secret file instead of hardcoding it. Set `QDB_ACL_ADMIN_PASSWORD_FILE` to the path of the mounted secret. See [Secrets from files](https://questdb.com/docs/configuration/overview/#secrets-from-files) for details. We will optionally disable this built-in administrator account later. For more on access control, see [Role-Based Access Control](https://questdb.com/docs/security/rbac/) . 1\. Setup TLS[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#1-setup-tls "Direct link to 1. Setup TLS") ----------------------------------------------------------------------------------------------------------------------------- QuestDB supports TLS versions 1.2 and 1.3. To configure TLS on all interfaces, set the following: server.conf - Changing cert paths tls.enabled=truetls.cert.path=/path/to/certificate.pemtls.private.key.path=/path/to/private_key To hot-reload the certificate and private key and update the files on disk, login to your QuestDB [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) . This is accessible by default at `http://localhost:9000`. Login using the built-in administrator credential. Then, execute: Web Console - Reloading TLS SELECT reload_tls(); TLS is now active. For more details on TLS see the [TLS operations documentation](https://questdb.com/docs/security/tls/) . 2\. Setup a database administrator[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#2-setup-a-database-administrator "Direct link to 2. Setup a database administrator") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The built-in admin aids in the first mile, and as needed on a recovery basis. A helpful practice is to have one created admin through which to setup other accounts. Create a new database admin: Web Console - Creating an admin; use your own, secure password! CREATE USER myadmin WITH PASSWORD 'xyz';GRANT all TO myadmin WITH GRANT OPTION; For emphasis: Please choose a secure password! After admin creation, we can now disable the built-in `admin`: server.conf - Disabling service account acl.admin.user.enabled=false Can you keep it? If it's secured, it's up to you. Consider different roles. You may be setting up an Enterprise cluster as the infrastructure admin. In this case, the built-in admin is your tool to do infrastructure tasks. The admin we just created may be of a different persona, the one who sets up users, groups, dictates how data can enter and be queried. However you break it down, remember that it can always be reactivated. 3\. Create interactive user accounts[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#3-create-interactive-user-accounts "Direct link to 3. Create interactive user accounts") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Now that you have an admin account, create interactive users. Interactive users are those who will ingest into and query your database, and manipulate its data. These are different than administrators, like you, who delegate permissions. Create and govern users through **role-based access control** and the curation of your **access control list**. Interactive users may utilize the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) and/or the Postgres querying clients. It is common practice to set them up as `readonly`. But how you setup these users or groups is up to you. For ingestion, we'll cover that in the next section. Consider this first wave of users your "database consumers". For permissions, the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) requires `HTTP`, and the PostgreSQL interface requires `PGWIRE`: Web Console - Creating multiple users with differing permissions. -- Read only user, can read all tables:CREATE USER readonly WITH PASSWORD 'xyz';GRANT HTTP, PGWIRE TO readonly;GRANT SELECT ON ALL TABLES TO readonly;-- User with all permissions on a specific table:CREATE USER user1 WITH PASSWORD 'abc';GRANT HTTP, PGWIRE TO user1;GRANT ALL ON table1 TO user1;-- User who can manage access to a specific table:CREATE USER user2 WITH PASSWORD 'abc';GRANT HTTP, PGWIRE TO user2;GRANT ALL ON table2 TO user2 WITH GRANT OPTION; Permission grants can be specific and fine-tuned. List the full list of applied permissions with `all_permissions()`. * For the full role-based access control docs, including group management, see the [RBAC operations guide](https://questdb.com/docs/security/rbac/) . * For a full list of available permissions, see the [permissions sub-section in the RBAC operations guide](https://questdb.com/docs/security/rbac/#permissions) . 4\. Ingest data, InfluxDB Line Protocol[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#4-ingest-data-influxdb-line-protocol "Direct link to 4. Ingest data, InfluxDB Line Protocol") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The recommended method for high-throughput ingestion is InfluxDB Line Protocol (ILP) over HTTP. We recommend using a service account for programmatic ingestion. Service accounts apply a cleaner set of access permissions and are less likely to be affected by day-to-day user management. The process is: 1. Create a service account and grant it permissions. 2. Generate a **REST token** for the service account. 3. Use this token in your client's connection string. ### Step 1: Create the Service Account[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-1-create-the-service-account "Direct link to Step 1: Create the Service Account") First, run the following SQL in the web console. This creates a service account named `ingest_http` and grants it the necessary permissions to use HTTP endpoints and manage data. Web Console - Setup a service account CREATE SERVICE ACCOUNT ingest_ilp;-- Grant permission to create tables and use HTTP endpointsGRANT HTTP, CREATE TABLE TO ingest_ilp;-- Grant permission to add columns and insert dataGRANT ADD COLUMN, INSERT ON ALL TABLES TO ingest_ilp;-- OR, for more granular control:-- GRANT ADD COLUMN, INSERT ON table1, table2 TO ingest_ilp; ### Step 2: Generate an Authentication Token[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-2-generate-an-authentication-token "Direct link to Step 2: Generate an Authentication Token") Next, generate a REST API token for the service account. This token acts as a password, so you must store it securely. Web Console - Generate a token for the ingest client ALTER SERVICE ACCOUNT ingest_ilp CREATE TOKEN TYPE REST WITH TTL '3000d' REFRESH; This command returns a token. **Copy it immediately**, as it's shown only once. | name | token | expires\_at | refresh | | --- | --- | --- | --- | | ingest\_ilp | qt1KAsf1U9YbUVAX1H2IahXEE3-4qBcK-zx\_jsZUzV9bLY | 2033-09-19T15:32:51.628453Z | true | ### Step 3: Use the Token in Your Client[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-3-use-the-token-in-your-client "Direct link to Step 3: Use the Token in Your Client") You can now use this token to authenticate your application. The following Java example shows how to use the client library by configuring it from a connection string. This is the recommended approach. Java - Ingesting data via ILP import io.questdb.client.Sender;import java.time.temporal.ChronoUnit;public class Ingest { public static void main(String[] args) { try (Sender sender = Sender.fromConfig("https::addr=localhost:9000;token=qt1KAsf1U9YbUVAX1H2IahXEE3-4qBcK-zx_jsZUzV9bLY;")) { sender.table("ilptest"); sender.symbol("sym1", "symval1") .doubleColumn("double1", 100.0) .at(System.currentTimeMillis(), ChronoUnit.MILLIS); } }} A Note on TLS The `https::` prefix in the connection string tells the client to connect using TLS. By default, the client will verify the server's certificate. For local testing with self-signed certificates, you can disable this validation by adding `tls.verify=insecure;` to the configuration string. **This is not recommended for production.** Connecting a client to ILP is a common path. However, you may use something like [Kafka](https://questdb.com/docs/ingestion/message-brokers/kafka/) . For more on ILP ingestion, see: * [ILP Overview](https://questdb.com/docs/ingestion/ilp/overview/) — Protocol details and configuration * [Ingestion Overview](https://questdb.com/docs/ingestion/overview/) — Client libraries and ingestion methods 5\. Ingest data, Kafka Connect (optional)[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#5-ingest-data-kafka-connect-optional "Direct link to 5. Ingest data, Kafka Connect (optional)") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- _If you're not using Kafka, you can skip to section 6._ The official **QuestDB Kafka Connect sink** forwards messages from Kafka topics directly to your database using ILP protocol. The setup process is straightforward: 1. Create a dedicated service account in QuestDB. 2. Generate an authentication token for the account. 3. Configure the Kafka sink connector with your QuestDB address and the token. ### **Step 1: Create the Service Account**[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-1-create-the-service-account-1 "Direct link to step-1-create-the-service-account-1") In the QuestDB web console, create a service account named `kafka` and grant it the permissions required to connect and write data. Web Console - Create a Kafka service account CREATE SERVICE ACCOUNT kafka;-- Grant permissions to use HTTP, create tables, add new columns and insert dataGRANT HTTP, CREATE TABLE TO kafka;GRANT ADD COLUMN, INSERT ON ALL TABLES TO kafka;-- OR, for more granular control:-- GRANT ADD COLUMN, INSERT ON table1, table2 TO ingest_ilp; ### **Step 2: Generate an Authentication Token**[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-2-generate-an-authentication-token-1 "Direct link to step-2-generate-an-authentication-token-1") Next, generate a REST API token for the `kafka` service account. This token is a secret credential and should be treated like a password. Web Console - Generate a token for the service account -- Creates a token that is valid for 1 year (365 days)ALTER SERVICE ACCOUNT kafka CREATE TOKEN TYPE REST WITH TTL '365d'; The command returns a token. **Copy it immediately**, as it will not be shown again. | name | token | expires\_at | | --- | --- | --- | | kafka | `qt1KAsf1U9YbUVAX1H2IahXEE3-4qBcK-zx_jsZUzV9bLY` | `2026-07-03T18:05:00.000000Z` | Save the private key in a secure location! ### **Step 3: Configure the Kafka Connect Sink**[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-3-configure-the-kafka-connect-sink "Direct link to step-3-configure-the-kafka-connect-sink") Create a configuration file for the QuestDB sink connector. In the `client.conf.string` property, provide your QuestDB server address and paste the token you just generated. questdb-sink.properties # --- Connector Identity ---name=QuestDBSinkConnectorconnector.class=io.questdb.kafka.QuestDBSinkConnectortasks.max=1# --- Source Kafka Topic ---topics=your_kafka_topic# --- QuestDB Connection ---# Use https:: if your QuestDB server has TLS enabled.# Replace the placeholder with the token you generated.client.conf.string=https::addr=localhost:9000;token=qt1KAsf1U9YbUVAX1H2IahXEE3-4qBcK-zx_jsZUzV9bLY;# --- Optional: Data Mapping ---# Use a field from the Kafka message key or value as a QuestDB symbol.# symbol.columns=device_id Once you deploy this configuration, the connector will start sending data from your Kafka topic to QuestDB. If you encounter any issues, check the logs for both your Kafka Connect worker and your QuestDB server for more details. See the [QuestDB Kafka Connector documentation](https://questdb.com/docs/ingestion/message-brokers/kafka/#questdb-kafka-connect-connector) for more details on the configuration options and how to set up the connector. 6\. Query data, PostgreSQL query[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#6-query-data-postgresql-query "Direct link to 6. Query data, PostgreSQL query") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Now onto querying. We will demonstrate programmatic querying via the PostgreSQL interface. Again, in this case we recommend a unique user or a service account. We will create a service account named "dashboard". We'd assume that this is Grafana or a similar visual data representation application. To setup the service account: Web Console - Create a service account called 'dashboard' and grant permissions CREATE SERVICE ACCOUNT dashboard WITH password 'pwd';GRANT pgwire TO dashboard;GRANT select on all tables TO dashboard; Applying Java & jdbc, we can setup a client to query. We're providing a username and password instead of a token: Java - Querying via JDBC import java.sql.*;import java.util.Properties;public class App { public static void main(String[] args) throws SQLException { Properties properties = new Properties(); properties.setProperty("user", "dashboard"); properties.setProperty("password", "pwd"); properties.setProperty("sslmode", "require"); final Connection connection = DriverManager.getConnection( "jdbc:postgresql://localhost:8812/qdb", properties); try (PreparedStatement preparedStatement = connection.prepareStatement( "SELECT x, timestamp_sequence('2023-07-20', 1000000) FROM long_sequence(5);")) { try (ResultSet rs = preparedStatement.executeQuery()) { while (rs.next()) { System.out.println(rs.getLong(1)); System.out.println(rs.getTimestamp(2)); } } } connection.close(); }} This covers the very basics of user creation and service accounts. We have an `ingest` service account and a `dashboard` service account. For more on querying, see: * [PostgreSQL Wire Protocol](https://questdb.com/docs/query/pgwire/overview/) — Connection details and compatibility * [Query & SQL Overview](https://questdb.com/docs/query/overview/) — SQL syntax and functions > For the full role-based access control docs, including group management, see the [RBAC operations guide](https://questdb.com/docs/security/rbac/) > . Next, we will enable Enterprise-specific features. 7\. Setup replication[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#7-setup-replication "Direct link to 7. Setup replication") ----------------------------------------------------------------------------------------------------------------------------------------------------- [Replication](https://questdb.com/docs/high-availability/overview/) consists of: * a primary database instance * an object storage * any number of replica instances The primary instance uploads its Write Ahead Log (WAL) to the object storage, and the replica instances apply the same data to their tables by downloading and processing the WAL. Full instructions can be found within the [replication page](https://questdb.com/docs/high-availability/setup/) , however the key parts are: 1. _Setup the object storage_: Supported options are Azure Blob Storage, Amazon S3 or Network File Storage (NFS). 2. _Set up a primary node_: Alter the `server.conf` within the primary-to-be and create a snapshot of the database. 3. _Setting up a replica node_: Alter the `server.conf` in the replica(s)-to-be and perform "recovery" from the snapshot of the primary database. The snapshot provides a starting point for the instance, which will soon catch up with the primary node. 8\. Enable compression[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#8-enable-compression "Direct link to 8. Enable compression") -------------------------------------------------------------------------------------------------------------------------------------------------------- Compression requires the [Zettabyte File System (ZFS)](https://openzfs.org/wiki/Main_Page) . We'll assume Ubuntu, and demonstrate the basics CLI commands which you'd apply in something like an AWS EC2 to enable ZFS: Ubuntu - Install ZFS sudo apt updatesudo apt install zfsutils-linux To enable compression, create a ZPool with compression enabled: Ubuntu - Enable compression zpool create -m legacy -o feature@lz4_compress=enabled autoexpand=on -O compression=lz4 -t volume1 questdb-pool sdf The exact commands depend on which version of ZFS you are running. Use the [ZFS docs](https://openzfs.github.io/openzfs-docs/man/master/8/zpool-create.8.html) to customize your ZFS to meet your requirements. If you are running QuestDB Enterprise in Kubernetes, QuestDB offers a [Container Storage Interface](https://github.com/container-storage-interface/spec/blob/master/spec.md) (CSI) Driver to create ZFS volumes in your cluster. Please contact us for more information to see if your version and distribution of Kubernetes is supported. For more on storage and compression, see [Enable compression with ZFS](https://questdb.com/docs/deployment/compression-zfs/) . 9\. Double-check kernel limits[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#9-double-check-kernel-limits "Direct link to 9. Double-check kernel limits") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB works together with your server operating system to achieve maximum performance. Prior to putting your server under heavy loads, consider checking your [kernel-based limitations](https://questdb.com/docs/getting-started/capacity-planning/#os-configuration) . Specifically, increase the limits for how many files can be opened by your OS and its users, and the maximum amount of virtual memory allowed. This helps QuestDB operate most effectively. Next steps[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#next-steps "Direct link to Next steps") ----------------------------------------------------------------------------------------------------------------------- This guide has prepared you for reliable, production-ready usage of QuestDB Enterprise. If you're new to QuestDB, consider checking out: * [Ingestion overview](https://questdb.com/docs/ingestion/overview/) : Learn the various ingestion methods and their benefits and tradeoffs, and pick a language client. * [Query & SQL overview](https://questdb.com/docs/query/overview/) : Learn how to query QuestDB. Otherwise, enjoy! FAQ[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#faq "Direct link to FAQ") -------------------------------------------------------------------------------------------------- ### General Setup and Configuration[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#general-setup-and-configuration "Direct link to General Setup and Configuration") **Q: How do I change the default administrator password?** A: To change the default administrator password, update your `server.conf` file with the following lines, replacing `myadmin` and `my_very_secure_pwd` with your chosen administrator username and a secure password: acl.admin.user=myadminacl.admin.password=my_very_secure_pwd **Q: What should I do if I encounter errors during the TLS setup process?** A: If you encounter errors during the TLS setup, ensure that the certificate and private key paths are correctly specified in your `server.conf` file. Also, verify that your certificates are valid and not expired. For further troubleshooting, consult the [TLS operations](https://questdb.com/docs/security/tls/) documentation. ### Security and Access Control[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#security-and-access-control "Direct link to Security and Access Control") **Q: Can I recover a lost private key for a service account?** A: No, once a private key for a service account is generated, it cannot be retrieved again. It is crucial to store it securely immediately upon creation. If lost, you will need to generate a new token for the service account. **Q: How do I securely manage service account tokens?** A: Securely managing service account tokens involves storing them in a safe location, such as a secure secrets management tool. Limit the distribution of these tokens and regularly rotate them to enhance security. ### Ingestion and Querying[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#ingestion-and-querying "Direct link to Ingestion and Querying") **Q: What should I do if data ingestion via Kafka Connect fails?** A: If data ingestion via Kafka Connect fails, check your service account permissions and ensure the private key used in Kafka's configuration matches the one generated for your service account. Also, verify your network settings and ensure there are no connectivity issues between Kafka and QuestDB. **Q: How can I troubleshoot issues with querying data using the PostgreSQL interface?** A: Ensure the service account or user has the correct permissions to query the tables of interest. Verify the connection string and authentication details used in your client application. For issues related to SSL, make sure the SSL mode is appropriately configured in your client connection settings. ### Replication and Compression[​](https://questdb.com/docs/getting-started/enterprise-quick-start/#replication-and-compression "Direct link to Replication and Compression") **Q: What steps should I take if replication is not working as expected?** A: Verify that the object storage is correctly set up and accessible by the primary instance. Ensure the `server.conf` settings for replication are correctly configured on both the primary and replica nodes. Check the logs for any errors related to replication and ensure there's network connectivity between all involved parties. **Q: Compression is enabled, but I'm not observing reduced storage usage. What could be the issue?** A: Ensure that the ZFS filesystem is correctly configured with compression enabled. Note that the actual compression ratio achieved can vary significantly depending on the nature of your data. Some types of data may not compress well. Review the ZFS compression statistics to understand the compression level being achieved. If it seems out of expected range, please contact us. * [Requirements](https://questdb.com/docs/getting-started/enterprise-quick-start/#requirements) * [Installation guide](https://questdb.com/docs/getting-started/enterprise-quick-start/#installation-guide) * [0\. Secure the built in admin](https://questdb.com/docs/getting-started/enterprise-quick-start/#0-secure-the-built-in-admin) * [1\. Setup TLS](https://questdb.com/docs/getting-started/enterprise-quick-start/#1-setup-tls) * [2\. Setup a database administrator](https://questdb.com/docs/getting-started/enterprise-quick-start/#2-setup-a-database-administrator) * [3\. Create interactive user accounts](https://questdb.com/docs/getting-started/enterprise-quick-start/#3-create-interactive-user-accounts) * [4\. Ingest data, InfluxDB Line Protocol](https://questdb.com/docs/getting-started/enterprise-quick-start/#4-ingest-data-influxdb-line-protocol) * [Step 1: Create the Service Account](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-1-create-the-service-account) * [Step 2: Generate an Authentication Token](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-2-generate-an-authentication-token) * [Step 3: Use the Token in Your Client](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-3-use-the-token-in-your-client) * [5\. Ingest data, Kafka Connect (optional)](https://questdb.com/docs/getting-started/enterprise-quick-start/#5-ingest-data-kafka-connect-optional) * [**Step 1: Create the Service Account**](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-1-create-the-service-account-1) * [**Step 2: Generate an Authentication Token**](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-2-generate-an-authentication-token-1) * [**Step 3: Configure the Kafka Connect Sink**](https://questdb.com/docs/getting-started/enterprise-quick-start/#step-3-configure-the-kafka-connect-sink) * [6\. Query data, PostgreSQL query](https://questdb.com/docs/getting-started/enterprise-quick-start/#6-query-data-postgresql-query) * [7\. Setup replication](https://questdb.com/docs/getting-started/enterprise-quick-start/#7-setup-replication) * [8\. Enable compression](https://questdb.com/docs/getting-started/enterprise-quick-start/#8-enable-compression) * [9\. Double-check kernel limits](https://questdb.com/docs/getting-started/enterprise-quick-start/#9-double-check-kernel-limits) * [Next steps](https://questdb.com/docs/getting-started/enterprise-quick-start/#next-steps) * [FAQ](https://questdb.com/docs/getting-started/enterprise-quick-start/#faq) * [General Setup and Configuration](https://questdb.com/docs/getting-started/enterprise-quick-start/#general-setup-and-configuration) * [Security and Access Control](https://questdb.com/docs/getting-started/enterprise-quick-start/#security-and-access-control) * [Ingestion and Querying](https://questdb.com/docs/getting-started/enterprise-quick-start/#ingestion-and-querying) * [Replication and Compression](https://questdb.com/docs/getting-started/enterprise-quick-start/#replication-and-compression) --- # How UPDATE works | QuestDB On this page This page explains how QuestDB implements the [UPDATE statement](https://questdb.com/docs/query/sql/update/) internally. tip UPDATE uses copy-on-write which increases disk usage. For high-frequency modifications, consider [append-oriented alternatives](https://questdb.com/docs/operations/modifying-data/) that work with QuestDB's storage model. Storage model[​](https://questdb.com/docs/operations/updating-data/#storage-model "Direct link to Storage model") ------------------------------------------------------------------------------------------------------------------ To be able to understand how table rows are updated in QuestDB, first we need to have an idea of how the data is stored. The documentation contains detailed descriptions of the [storage engine](https://questdb.com/docs/architecture/storage-engine/) and the [directory layout](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#db-directory) but if we quickly want to summarize it: * Each table has its own folder in the db root, the directory is named after the table * Partitions are manifested as subdirectories under the folder which represents the table * The actual data is stored in column files inside these subdirectories * Column files store data **ordered by the designated timestamp** and they are **append-only**. This goes naturally with [time-series data](https://questdb.com/blog/what-is-time-series-data/) , just think about market data where the price of different financial instruments are tracked during the trading day, for example Column versions[​](https://questdb.com/docs/operations/updating-data/#column-versions "Direct link to Column versions") ------------------------------------------------------------------------------------------------------------------------ Since files are append-only, updating existing data is not straightforward. QuestDB's storage model assumes past data rarely changes, which optimizes read performance. However, sometimes you need to amend data that was recorded incorrectly. We could break our append-only model and modify column files in place, but this would cause inconsistent reads. Concurrent queries could see partially updated data. The solution is to make the update **transactional** and **copy-on-write**. Basically a new column file is created when processing the UPDATE statement. All readers are looking at a previous consistent view of the data from an older column file while the UPDATE is in progress. Readers can find the latest committed version of column files based on a record stored in a metadata file. When the update is completed and a new column version is available for the readers, this metadata record gets updated as part of the commit. After metadata has changed newly submitted SELECT queries will see the updated data. The copy-on-write approach gives us data consistency and good performance at a price, disk usage will increase. When sizing disk space we should account for extra storage to make sure UPDATE statements have enough headroom. Only those column files will get a new version where data is actually changing. For example, if only a single column is updated in a single partition of a table, then only a single column file will be rewritten. Vacuum updated columns[​](https://questdb.com/docs/operations/updating-data/#vacuum-updated-columns "Direct link to Vacuum updated columns") --------------------------------------------------------------------------------------------------------------------------------------------- When a column is updated, the new version of the column is written to disk and a background task starts to vacuum redundant column files. The term Vacuum originates from Postgres, it means the collection of garbage and release of disk space. The Vacuum task checks periodically if older column versions are still used by readers and deletes unused files. Vacuum runs automatically and there is also a [`VACUUM TABLE`](https://questdb.com/docs/query/sql/vacuum-table/) SQL command to trigger it. Limitations[​](https://questdb.com/docs/operations/updating-data/#limitations "Direct link to Limitations") ------------------------------------------------------------------------------------------------------------ UPDATE rewrites column files by copying records in their existing order and replacing values as needed. As a result, the **designated timestamp column cannot be updated**. Modifying the designated timestamp would require reordering records and potentially moving rows between partitions. * [Storage model](https://questdb.com/docs/operations/updating-data/#storage-model) * [Column versions](https://questdb.com/docs/operations/updating-data/#column-versions) * [Vacuum updated columns](https://questdb.com/docs/operations/updating-data/#vacuum-updated-columns) * [Limitations](https://questdb.com/docs/operations/updating-data/#limitations) --- # Automating QuestDB Tasks | QuestDB On this page QuestDB provides a simple [HTTP API](https://questdb.com/docs/query/rest-api/) that allows you to interact with the database using SQL queries. This API can be leveraged for automation using Bash scripts and scheduled execution via cron jobs. This is a lightweight approach that requires minimal dependencies. For a more robust approach, you might want to explore the integration with workflow orchestrators such as [Apache Airflow](https://questdb.com/docs/integrations/orchestration/airflow/) or [Dagster](https://questdb.com/docs/integrations/orchestration/dagster/) . Prerequisites[​](https://questdb.com/docs/operations/task-automation/#prerequisites "Direct link to Prerequisites") -------------------------------------------------------------------------------------------------------------------- * QuestDB running locally or on a server * `curl` installed (pre-installed on most Linux/macOS systems) * Basic knowledge of Bash or similar scripting language Example: Running a Scheduled Query[​](https://questdb.com/docs/operations/task-automation/#example-running-a-scheduled-query "Direct link to Example: Running a Scheduled Query") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The following example demonstrates how to execute a query using the HTTP API: drop-partitions.sh #!/bin/bash# QuestDB API URLQUESTDB_URL="http://localhost:9000/exec"# Query: Drop partitions older than 30 daysQUERY="ALTER TABLE my_table DROP PARTITION WHERE timestamp < dateadd('d', -30, now());"# Execute the querycurl -G "$QUESTDB_URL" --data-urlencode "query=$QUERY" Automating with Cron[​](https://questdb.com/docs/operations/task-automation/#automating-with-cron "Direct link to Automating with Cron") ----------------------------------------------------------------------------------------------------------------------------------------- To execute this script daily at midnight, add the following line to your crontab: 0 0 * * * /path/to/script.sh Pros & Cons[​](https://questdb.com/docs/operations/task-automation/#pros--cons "Direct link to Pros & Cons") ------------------------------------------------------------------------------------------------------------- ✅ Simple to implement ✅ No external dependencies ✅ Works on any Unix-like system \\ ❌ No monitoring or logging ❌ No built-in error handling ❌ No backfilling support Next Steps[​](https://questdb.com/docs/operations/task-automation/#next-steps "Direct link to Next Steps") ----------------------------------------------------------------------------------------------------------- For more advanced automation, consider using a workflow orchestrator like [**Dagster**](https://questdb.com/docs/integrations/orchestration/dagster/) or [**Apache Airflow**](https://questdb.com/docs/integrations/orchestration/airflow/) . * **Full Example Repository**: [https://github.com/questdb/data-orchestration-and-scheduling-samples](https://github.com/questdb/data-orchestration-and-scheduling-samples) * [Prerequisites](https://questdb.com/docs/operations/task-automation/#prerequisites) * [Example: Running a Scheduled Query](https://questdb.com/docs/operations/task-automation/#example-running-a-scheduled-query) * [Automating with Cron](https://questdb.com/docs/operations/task-automation/#automating-with-cron) * [Pros & Cons](https://questdb.com/docs/operations/task-automation/#pros--cons) * [Next Steps](https://questdb.com/docs/operations/task-automation/#next-steps) --- # Logging and metrics | QuestDB On this page This page outlines logging in QuestDB. It covers how to configure logs via `log.conf` and expose metrics via Prometheus. * [Logging](https://questdb.com/docs/operations/logging-metrics/#logging) * [Metrics](https://questdb.com/docs/operations/logging-metrics/#metrics) Log location[​](https://questdb.com/docs/operations/logging-metrics/#log-location "Direct link to Log location") ----------------------------------------------------------------------------------------------------------------- QuestDB creates the following file structure in its [root\_directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/) : questdb├── conf├── db├── log├── public└── snapshot (optional) Log files are stored in the `log` folder: ├── log│   ├── stdout-2020-04-15T11-59-59.txt│   └── stdout-2020-04-12T13-31-22.txt Understanding log levels[​](https://questdb.com/docs/operations/logging-metrics/#understanding-log-levels "Direct link to Understanding log levels") ----------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB provides the following types of log information: | Type | Marker | Details | Default | | --- | --- | --- | --- | | Advisory | A | Startup information such as hosts, listening ports, etc. Rarely used after startup | Enabled | | Critical | C | Internal database errors. Serious issues. Things that should not happen in general operation. | Enabled | | Error | E | An error, usually (but not always) caused by a user action such as inserting a `symbol` into a `timestamp` column. For context on how this error happened, check for Info-level messages logged before the error. | Enabled | | Info | I | Logs for activities. Info-level messages often provide context for an error if one is logged later. | Enabled | | Debug | D | Finer details on what is happening. Useful to debug issues. | Disabled | For more information, see the [QuestDB source code](https://github.com/questdb/questdb/blob/master/core/src/main/java/io/questdb/log/LogLevel.java) . ### Example log messages[​](https://questdb.com/docs/operations/logging-metrics/#example-log-messages "Direct link to Example log messages") Advisory: 2023-02-24T14:59:45.076113Z A server-main Config:2023-02-24T14:59:45.076130Z A server-main - http.enabled : true2023-02-24T14:59:45.076144Z A server-main - tcp.enabled : true2023-02-24T14:59:45.076159Z A server-main - pg.enabled : true Critical: 2022-08-08T11:15:13.040767Z C i.q.c.p.WriterPool could not open [table=`sys.text_import_log`, thread=1, ex=could not open read-write [file=/opt/homebrew/var/questdb/db/sys.text_import_log/_todo_], errno=13] Error: 2023-02-24T14:59:45.059012Z I i.q.c.t.t.InputFormatConfiguration loading input format config [resource=/text_loader.json]2023-03-20T08:38:17.076744Z E i.q.c.l.u.AbstractLineProtoUdpReceiver could not set receive buffer size [fd=140, size=8388608, errno=55] Info: 2020-04-15T16:42:32.879970Z I i.q.c.TableReader new transaction [txn=2, transientRowCount=1, fixedRowCount=1, maxTimestamp=1585755801000000, attempts=0]2020-04-15T16:42:32.880051Z I i.q.g.FunctionParser call to_timestamp('2020-05-01:15:43:21','yyyy-MM-dd:HH:mm:ss') -> to_timestamp(Ss) Debug: 2023-03-31T11:47:05.723715Z D i.q.g.FunctionParser call cast(investmentMill,INT) -> cast(Li)2023-03-31T11:47:05.723729Z D i.q.g.FunctionParser call rnd_symbol(4,4,4,2) -> rnd_symbol(iiii) Logging[​](https://questdb.com/docs/operations/logging-metrics/#logging "Direct link to Logging") -------------------------------------------------------------------------------------------------- The logging behavior of QuestDB may be set in dedicated configuration files or by environment variables. This section describes how to configure logging using these methods. ### Enable debug log[​](https://questdb.com/docs/operations/logging-metrics/#enable-debug-log "Direct link to Enable debug log") QuestDB `DEBUG` logging can be set globally. 1. Provide the java option `-Debug` on startup 2. Setting the `QDB_DEBUG=true` as an environment variable ### Configure log.conf[​](https://questdb.com/docs/operations/logging-metrics/#configure-logconf "Direct link to Configure log.conf") Logs may be configured via a dedicated configuration file `log.conf`. QuestDB will look for `/log.conf` first in `conf/` directory and then on the classpath, unless this name is overridden via a command line property: `-Dout=/something_else.conf`. QuestDB will create `conf/log.conf` using default values if `-Dout` is not set and file doesn't exist . On Windows log messages go to depending on run mode : * interactive session - console and `$dataDir\log\stdout-%Y-%m-%dT%H-%M-%S.txt` (default is `.\log\stdout-%Y-%m-%dT%H-%M-%S.txt` ) * service - `$dataDir\log\service-%Y-%m-%dT%H-%M-%S.txt` (default is `C:\Windows\System32\qdbroot\log\service-%Y-%m-%dT%H-%M-%S.txt` ) The possible values to enable within the `log.conf` appear as such: log.conf # list of configured writerswriters=file,stdout,http.min# rolling file writerw.file.class=io.questdb.log.LogRollingFileWriterw.file.location=${log.dir}/questdb-rolling.log.${date:yyyyMMdd}w.file.level=INFO,ERRORw.file.rollEvery=dayw.file.rollSize=1g# Optionally, use a single log# w.file.class=io.questdb.log.LogFileWriter# w.file.location=questdb-docker.log# w.file.level=INFO,ERROR,DEBUG# stdoutw.stdout.class=io.questdb.log.LogConsoleWriterw.stdout.level=INFO# min http server, used for error monitoringw.http.min.class=io.questdb.log.LogConsoleWriterw.http.min.level=ERROR## Scope provides specific context for targeted log parsingw.http.min.scope=http-min-server #### Log writer types[​](https://questdb.com/docs/operations/logging-metrics/#log-writer-types "Direct link to Log writer types") There are four types of writer. Which one you need depends on your use case. | Available writers | Description | | --- | --- | | file | Select from one of the two above patterns. Write to a single log that will grow indefinitely, or write a rolling log. Rolling logs can be split into `minute`, `hour`, `day`, `month` or `year`. | | stdout | Writes logs to standard output. | | http.min | Enabled at port `9003` by default. For more information, see the next section: [minimal HTTP server](https://questdb.com/docs/operations/logging-metrics/#minimal-http-server)
. | ### Minimal HTTP server[​](https://questdb.com/docs/operations/logging-metrics/#minimal-http-server "Direct link to Minimal HTTP server") To provide a dedicated health check feature that would have no performance knock on other system components, QuestDB decouples health checks from the REST endpoints used for querying and ingesting data. For this purpose, a `min` HTTP server runs embedded in a QuestDB instance and has a separate log and thread pool configuration. The `min` server is enabled by default and will reply to any `HTTP GET` request to port `9003`: GET health status of local instance curl -v http://127.0.0.1:9003 The server will respond with an HTTP status code of `200`, indicating that the system is operational: 200 'OK' response * Trying 127.0.0.1...* TCP_NODELAY set* Connected to 127.0.0.1 (127.0.0.1) port 9003 (#0)> GET / HTTP/1.1> Host: 127.0.0.1:9003> User-Agent: curl/7.64.1> Accept: */*>< HTTP/1.1 200 OK< Server: questDB/1.0< Date: Tue, 26 Jan 2021 12:31:03 GMT< Transfer-Encoding: chunked< Content-Type: text/plain<* Connection #0 to host 127.0.0.1 left intact Path segments are ignored which means that optional paths may be used in the URL and the server will respond with identical results, e.g.: GET health status with arbitrary path curl -v http://127.0.0.1:9003/status The following configuration options can be set in your `server.conf`: | Property | Default | Reloadable | Description | | --- | --- | --- | --- | | http.min.enabled | true | No | Enable or disable Minimal HTTP server. | | http.min.bind.to | 0.0.0.0:9003 | No | IPv4 address and port of the server. `0` means it will bind to all network interfaces, otherwise the IP address must be one of the existing network adapters. | | http.min.net.connection.limit | 4 | No | Active connection limit. | | http.min.net.connection.timeout | 300000 | No | Idle connection timeout in milliseconds. | | http.min.net.connection.hint | false | No | Windows specific flag to overcome OS limitations on TCP backlog size. | | http.min.worker.count | | No | By default, minimal HTTP server uses shared thread pool for CPU core count 16 and below. It will use dedicated thread for core count above 16. When `0`, the server will use the shared pool. Do not set pool size to more than `1`. | | http.min.worker.affinity | | No | Core number to pin thread to. | | http.min.worker.haltOnError | false | No | Flag that indicates if the worker thread must stop when an unexpected error occurs. | warning On systems with [8 Cores and less](https://questdb.com/docs/getting-started/capacity-planning/#cpu-cores) , contention for threads might increase the latency of health check service responses. If you use a load balancer, and it thinks the QuestDB service is dead with nothing apparent in the QuestDB logs, you may need to configure a dedicated thread pool for the health check service. To do so, increase `http.min.worker.count` to `1`. ### Environment variables[​](https://questdb.com/docs/operations/logging-metrics/#environment-variables "Direct link to Environment variables") Values in the log configuration file can be overridden with environment variables. All configuration keys must be formatted as described in the [environment variables](https://questdb.com/docs/operations/logging-metrics/#environment-variables) section above. For example, to set logging on `ERROR` level only: Setting log level to ERROR in log-stdout.conf w.stdout.level=ERROR This can be passed as an environment variable as follows: Setting log level to ERROR via environment variable export QDB_LOG_W_STDOUT_LEVEL=ERROR ### Docker logging[​](https://questdb.com/docs/operations/logging-metrics/#docker-logging "Direct link to Docker logging") When mounting a volume to a Docker container, a logging configuration file may be provided in the container located at `./conf/log.conf`. For example, a file with the following contents can be created: ./conf/log.conf # list of configured writerswriters=file,stdout,http.min# file writerw.file.class=io.questdb.log.LogFileWriterw.file.location=questdb-docker.logw.file.level=INFO,ERROR,DEBUG# stdoutw.stdout.class=io.questdb.log.LogConsoleWriterw.stdout.level=INFO# min http server, used for monitoringw.http.min.class=io.questdb.log.LogConsoleWriterw.http.min.level=ERROR## Scope provides specific context for targeted log parsingw.http.min.scope=http-min-server The current directory can be mounted: Mount the current directory to a QuestDB container docker run -p 9000:9000 -v "$(pwd):/var/lib/questdb/" questdb/questdb The container logs will be written to disk using the logging level and file name provided in the `./conf/log.conf` file, in this case in `./questdb-docker.log`. ### Windows log locations[​](https://questdb.com/docs/operations/logging-metrics/#windows-log-locations "Direct link to Windows log locations") When running QuestDB as Windows service you can check status in both: * Windows Event Viewer: Look for events with "QuestDB" source in `Windows Logs | Application` * The service log file: `$dataDir\log\service-%Y-%m-%dT%H-%M-%S.txt` * Default: `C:\Windows\System32\qdbroot\log\service-%Y-%m-%dT%H-%M-%S.txt` Metrics[​](https://questdb.com/docs/operations/logging-metrics/#metrics "Direct link to Metrics") -------------------------------------------------------------------------------------------------- QuestDB exposes a `/metrics` endpoint on port `9003` for internal system metrics in the Prometheus format. To use this functionality and get started with an example configuration, enable it in within your `server.conf`: | Property | Default | Description | | --- | --- | --- | | metrics.enabled | false | Enable or disable metrics endpoint. | For an example on how to setup Prometheus, see the [QuestDB and Prometheus documentation](https://questdb.com/docs/integrations/other/prometheus/) . ### Prometheus Alertmanager[​](https://questdb.com/docs/operations/logging-metrics/#prometheus-alertmanager "Direct link to Prometheus Alertmanager") QuestDB includes a log writer that sends any message logged at critical level (logger.critical("may-day")) to Prometheus Alertmanager over a TCP/IP socket. To configure this writer, add it to the `writers` config alongside other log writers: log.conf # Which writers to enablewriters=stdout,alert# stdoutw.stdout.class=io.questdb.log.LogConsoleWriterw.stdout.level=INFO# Prometheus Alertingw.alert.class=io.questdb.log.LogAlertSocketWriterw.alert.level=CRITICALw.alert.location=/alert-manager-tpt.jsonw.alert.alertTargets=localhost:9093,localhost:9096,otherhost:9093w.alert.defaultAlertHost=localhostw.alert.defaultAlertPort=9093# The `inBufferSize` and `outBufferSize` properties are the size in bytes for the# socket write buffers.w.alert.inBufferSize=2mw.alert.outBufferSize=4m# Delay in milliseconds between two consecutive attempts to alert when# there is only one target configuredw.alert.reconnectDelay=250 Of all properties, only `w.alert.class` and `w.alert.level` are required, the rest assume default values as stated above (except for `w.alert.alertTargets` which is empty by default). Alert targets are specified using `w.alert.alertTargets` as a comma-separated list of up to 12 `host:port` TCP/IP addresses. Specifying a port is optional and defaults to the value of `defaultAlertHost`. One of these alert managers is picked at random when QuestDB starts, and a connection is created. All alerts will be sent to the chosen server unless it becomes unavailable. If it is unavailable, the next server is chosen. If there is only one server configured and a fail-over cannot occur, a delay of 250 milliseconds is added between send attempts. The `w.alert.location` property refers to the path (absolute, otherwise relative to `-d database-root`) of a template file. By default, it is a resource file which contains: /alert-manager-tpt.json [ { "Status": "firing", "Labels": { "alertname": "QuestDbInstanceLogs", "service": "QuestDB", "category": "application-logs", "severity": "critical", "version": "${QDB_VERSION}", "cluster": "${CLUSTER_NAME}", "orgid": "${ORGID}", "namespace": "${NAMESPACE}", "instance": "${INSTANCE_NAME}", "alertTimestamp": "${date: yyyy/MM/ddTHH:mm:ss.SSS}" }, "Annotations": { "description": "ERROR/cl:${CLUSTER_NAME}/org:${ORGID}/ns:${NAMESPACE}/db:${INSTANCE_NAME}", "message": "${ALERT_MESSAGE}" } }] Four environment variables can be defined, and referred to with the `${VAR_NAME}` syntax: * _ORGID_ * _NAMESPACE_ * _CLUSTER\_NAME_ * _INSTANCE\_NAME_ Their default value is `GLOBAL`, they mean nothing outside a cloud environment. In addition, `ALERT_MESSAGE` is a placeholder for the actual `critical` message being sent, and `QDB_VERSION` is the runtime version of the QuestDB instance sending the alert. The `${date: }` syntax can be used to produce a timestamp at the time of sending the alert. ### Unhandled error detection[​](https://questdb.com/docs/operations/logging-metrics/#unhandled-error-detection "Direct link to Unhandled error detection") When the metrics subsystem is enabled, the health endpoint may be configured to check the occurrences of any unhandled errors since the database started. For any errors detected, it returns the HTTP 500 status code. The check is based on the `questdb_unhandled_errors_total` metric. To enable this setting, set the following in `server.conf`: server.conf to enable critical error checks in the health check endpoint metrics.enabled=truehttp.pessimistic.health.check.enabled=true When the metrics subsystem is disabled, the health check endpoint always returns the HTTP 200 status code. * [Log location](https://questdb.com/docs/operations/logging-metrics/#log-location) * [Understanding log levels](https://questdb.com/docs/operations/logging-metrics/#understanding-log-levels) * [Example log messages](https://questdb.com/docs/operations/logging-metrics/#example-log-messages) * [Logging](https://questdb.com/docs/operations/logging-metrics/#logging) * [Enable debug log](https://questdb.com/docs/operations/logging-metrics/#enable-debug-log) * [Configure log.conf](https://questdb.com/docs/operations/logging-metrics/#configure-logconf) * [Minimal HTTP server](https://questdb.com/docs/operations/logging-metrics/#minimal-http-server) * [Environment variables](https://questdb.com/docs/operations/logging-metrics/#environment-variables) * [Docker logging](https://questdb.com/docs/operations/logging-metrics/#docker-logging) * [Windows log locations](https://questdb.com/docs/operations/logging-metrics/#windows-log-locations) * [Metrics](https://questdb.com/docs/operations/logging-metrics/#metrics) * [Prometheus Alertmanager](https://questdb.com/docs/operations/logging-metrics/#prometheus-alertmanager) * [Unhandled error detection](https://questdb.com/docs/operations/logging-metrics/#unhandled-error-detection) --- # Post-trade markout analysis | QuestDB On this page Markout analysis measures how the market mid-price moves **after** a trade executes. It is the natural complement to [slippage](https://questdb.com/docs/cookbook/sql/finance/slippage/) : * **Slippage** tells you how much you paid at the moment of execution. * **Markout** tells you what happened next — did the market move in your favor (reversion) or against you (adverse selection)? A positive markout means the trade was profitable in hindsight: for buys, the mid-price rose; for sells, it fell. A negative markout means the market moved against you, which may indicate you were trading against informed flow. By computing markouts at multiple time horizons (e.g., every second for 5 minutes), you build a **markout curve** — the standard tool for evaluating execution quality over time. Problem[​](https://questdb.com/docs/cookbook/sql/finance/markout/#problem "Direct link to Problem") ---------------------------------------------------------------------------------------------------- You want to evaluate whether your fills are subject to adverse selection. For each trade, you need to know how the mid-price evolved over the seconds and minutes following execution, broken down by venue, counterparty, and passive/aggressive. Solution[​](https://questdb.com/docs/cookbook/sql/finance/markout/#solution "Direct link to Solution") ------------------------------------------------------------------------------------------------------- Use `HORIZON JOIN` to compute the mid-price at multiple time offsets after each trade, then aggregate into a markout curve: Post-trade markout curve by venue and counterparty[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20t.counterparty%2C%0A%20%20%20%20t.passive%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg(%0A%20%20%20%20%20%20%20%20CASE%20t.side%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27buy%27%20%20THEN%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27sell%27%20THEN%20(t.price%20-%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20END%0A%20%20%20%20)%20AS%20avg_markout_bps%2C%0A%20%20%20%20sum(%0A%20%20%20%20%20%20%20%20CASE%20t.side%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27buy%27%20%20THEN%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20*%20t.quantity%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27sell%27%20THEN%20(t.price%20-%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20*%20t.quantity%0A%20%20%20%20%20%20%20%20END%0A%20%20%20%20)%20AS%20total_pnl%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20RANGE%20FROM%200s%20TO%2030s%20STEP%205s%20AS%20h%0AWHERE%20t.timestamp%20IN%20%27%24now-1h..%24now%27%0AGROUP%20BY%20t.symbol%2C%20t.ecn%2C%20t.counterparty%2C%20t.passive%2C%20horizon_sec%0AORDER%20BY%20t.symbol%2C%20t.ecn%2C%20t.counterparty%2C%20t.passive%2C%20horizon_sec%3B&executeQuery=true) SELECT t.symbol, t.ecn, t.counterparty, t.passive, h.offset / 1000000000 AS horizon_sec, count() AS n, avg( CASE t.side WHEN 'buy' THEN ((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000 WHEN 'sell' THEN (t.price - (m.best_bid + m.best_ask) / 2) / t.price * 10000 END ) AS avg_markout_bps, sum( CASE t.side WHEN 'buy' THEN ((m.best_bid + m.best_ask) / 2 - t.price) * t.quantity WHEN 'sell' THEN (t.price - (m.best_bid + m.best_ask) / 2) * t.quantity END ) AS total_pnlFROM fx_trades tHORIZON JOIN market_data m ON (symbol) RANGE FROM 0s TO 30s STEP 5s AS hWHERE t.timestamp IN '$now-1h..$now'GROUP BY t.symbol, t.ecn, t.counterparty, t.passive, horizon_secORDER BY t.symbol, t.ecn, t.counterparty, t.passive, horizon_sec; How it works[​](https://questdb.com/docs/cookbook/sql/finance/markout/#how-it-works "Direct link to How it works") ------------------------------------------------------------------------------------------------------------------- [`HORIZON JOIN`](https://questdb.com/docs/query/sql/horizon-join/) is the key construct. For each trade and each time offset in the range, it performs an ASOF match against `market_data` at `trade_timestamp + offset`. The `RANGE FROM 0s TO 30s STEP 5s` generates 7 offsets (0s, 5s, 10s, ... 30s), giving you a markout reading every 5 seconds for 30 seconds after each trade. The two metrics: * **`avg_markout_bps`** — average price movement in basis points, normalized by fill price. Positive means the market moved in your favor. At offset 0, this is simply the negative of slippage-vs-mid. * **`total_pnl`** — actual P&L in currency terms (price difference × quantity). This captures the dollar impact, not just the rate — 0.1 bps on 100Mofvolumeisverydifferentfrom0.1bpson100M of volume is very different from 0.1 bps on 100Mofvolumeisverydifferentfrom0.1bpson1M. The markout formula flips the sign convention compared to slippage: * **For buys**: positive if mid rose after the fill (profit) * **For sells**: positive if mid fell after the fill (profit) As the offset increases, you see how the market evolved after each trade. Variations[​](https://questdb.com/docs/cookbook/sql/finance/markout/#variations "Direct link to Variations") ------------------------------------------------------------------------------------------------------------- ### Markout at specific horizons[​](https://questdb.com/docs/cookbook/sql/finance/markout/#markout-at-specific-horizons "Direct link to Markout at specific horizons") Use `LIST` instead of `RANGE` for non-uniform time points — useful when you care about specific benchmarks (e.g., -30s, -5s, 0, 5s, 30s): Markout at key horizons[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20t.passive%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20round(avg(%0A%20%20%20%20%20%20%20%20CASE%20t.side%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27buy%27%20%20THEN%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27sell%27%20THEN%20(t.price%20-%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20END%0A%20%20%20%20)%2C%203)%20AS%20avg_markout_bps%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20LIST%20(-30s%2C%20-5s%2C%200%2C%205s%2C%2030s)%20AS%20h%0AWHERE%20t.timestamp%20IN%20%27%24now-1h..%24now%27%0AGROUP%20BY%20t.ecn%2C%20t.passive%2C%20horizon_sec%0AORDER%20BY%20t.ecn%2C%20t.passive%2C%20horizon_sec%3B&executeQuery=true) SELECT t.ecn, t.passive, h.offset / 1000000000 AS horizon_sec, count() AS n, round(avg( CASE t.side WHEN 'buy' THEN ((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000 WHEN 'sell' THEN (t.price - (m.best_bid + m.best_ask) / 2) / t.price * 10000 END ), 3) AS avg_markout_bpsFROM fx_trades tHORIZON JOIN market_data m ON (symbol) LIST (-30s, -5s, 0, 5s, 30s) AS hWHERE t.timestamp IN '$now-1h..$now'GROUP BY t.ecn, t.passive, horizon_secORDER BY t.ecn, t.passive, horizon_sec; ### Pre- and post-trade analysis[​](https://questdb.com/docs/cookbook/sql/finance/markout/#pre--and-post-trade-analysis "Direct link to Pre- and post-trade analysis") Use negative offsets to detect information leakage — whether the market was already moving before your trade: Price movement around trade events[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20round(avg(%0A%20%20%20%20%20%20%20%20CASE%20t.side%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27buy%27%20%20THEN%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27sell%27%20THEN%20(t.price%20-%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20END%0A%20%20%20%20)%2C%203)%20AS%20avg_markout_bps%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20RANGE%20FROM%20-30s%20TO%2030s%20STEP%201s%20AS%20h%0AWHERE%20t.timestamp%20IN%20%27%24now-1h..%24now%27%0AGROUP%20BY%20horizon_sec%0AORDER%20BY%20horizon_sec%3B&executeQuery=true) SELECT h.offset / 1000000000 AS horizon_sec, count() AS n, round(avg( CASE t.side WHEN 'buy' THEN ((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000 WHEN 'sell' THEN (t.price - (m.best_bid + m.best_ask) / 2) / t.price * 10000 END ), 3) AS avg_markout_bpsFROM fx_trades tHORIZON JOIN market_data m ON (symbol) RANGE FROM -30s TO 30s STEP 1s AS hWHERE t.timestamp IN '$now-1h..$now'GROUP BY horizon_secORDER BY horizon_sec; If the markout is already trending before offset 0, it suggests the market was moving before your order — a sign of information leakage or that you are reacting to stale signals. ### Markout by side[​](https://questdb.com/docs/cookbook/sql/finance/markout/#markout-by-side "Direct link to Markout by side") Add `t.side` to the grouping to detect asymmetry between buy and sell execution. A counterparty might look fine on average but show adverse selection on one side only: Markout curve by side[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20t.side%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20round(avg(%0A%20%20%20%20%20%20%20%20CASE%20t.side%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27buy%27%20%20THEN%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20%20%20%20%20WHEN%20%27sell%27%20THEN%20(t.price%20-%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000%0A%20%20%20%20%20%20%20%20END%0A%20%20%20%20)%2C%203)%20AS%20avg_markout_bps%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20LIST%20(-30s%2C%20-5s%2C%200%2C%205s%2C%2030s)%20AS%20h%0AWHERE%20t.timestamp%20IN%20%27%24now-1h..%24now%27%0AGROUP%20BY%20t.ecn%2C%20t.side%2C%20horizon_sec%0AORDER%20BY%20t.ecn%2C%20t.side%2C%20horizon_sec%3B&executeQuery=true) SELECT t.ecn, t.side, h.offset / 1000000000 AS horizon_sec, count() AS n, round(avg( CASE t.side WHEN 'buy' THEN ((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000 WHEN 'sell' THEN (t.price - (m.best_bid + m.best_ask) / 2) / t.price * 10000 END ), 3) AS avg_markout_bpsFROM fx_trades tHORIZON JOIN market_data m ON (symbol) LIST (-30s, -5s, 0, 5s, 30s) AS hWHERE t.timestamp IN '$now-1h..$now'GROUP BY t.ecn, t.side, horizon_secORDER BY t.ecn, t.side, horizon_sec; If buy markouts diverge significantly from sell markouts at the same venue, it may indicate directional information leakage or asymmetric adverse selection. ### Single-side markout[​](https://questdb.com/docs/cookbook/sql/finance/markout/#single-side-markout "Direct link to Single-side markout") When analyzing one side at a time, you can drop the `CASE` entirely for a simpler formula: Buy-side markout — positive means price moved up after you bought[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%20%2F%20t.price%20*%2010000)%20AS%20avg_markout_bps%2C%0A%20%20%20%20sum(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%20*%20t.quantity)%20AS%20total_pnl%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20RANGE%20FROM%200s%20TO%2010m%20STEP%2010s%20AS%20h%0AWHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24now-1h..%24now%27%0AGROUP%20BY%20t.symbol%2C%20horizon_sec%0AORDER%20BY%20t.symbol%2C%20horizon_sec%3B&executeQuery=true) SELECT t.symbol, h.offset / 1000000000 AS horizon_sec, count() AS n, avg(((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000) AS avg_markout_bps, sum(((m.best_bid + m.best_ask) / 2 - t.price) * t.quantity) AS total_pnlFROM fx_trades tHORIZON JOIN market_data m ON (symbol) RANGE FROM 0s TO 10m STEP 10s AS hWHERE t.side = 'buy' AND t.timestamp IN '$now-1h..$now'GROUP BY t.symbol, horizon_secORDER BY t.symbol, horizon_sec; Sell-side markout — positive means price moved down after you sold[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg((t.price%20-%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202)%20%2F%20t.price%20*%2010000)%20AS%20avg_markout_bps%2C%0A%20%20%20%20sum((t.price%20-%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202)%20*%20t.quantity)%20AS%20total_pnl%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20RANGE%20FROM%200s%20TO%2010m%20STEP%2010s%20AS%20h%0AWHERE%20t.side%20%3D%20%27sell%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24now-1h..%24now%27%0AGROUP%20BY%20t.symbol%2C%20horizon_sec%0AORDER%20BY%20t.symbol%2C%20horizon_sec%3B&executeQuery=true) SELECT t.symbol, h.offset / 1000000000 AS horizon_sec, count() AS n, avg((t.price - (m.best_bid + m.best_ask) / 2) / t.price * 10000) AS avg_markout_bps, sum((t.price - (m.best_bid + m.best_ask) / 2) * t.quantity) AS total_pnlFROM fx_trades tHORIZON JOIN market_data m ON (symbol) RANGE FROM 0s TO 10m STEP 10s AS hWHERE t.side = 'sell' AND t.timestamp IN '$now-1h..$now'GROUP BY t.symbol, horizon_secORDER BY t.symbol, horizon_sec; This approach is useful when you want to run separate analyses per side, or when feeding results into dashboards that track buy and sell P&L independently. ### Counterparty toxicity[​](https://questdb.com/docs/cookbook/sql/finance/markout/#counterparty-toxicity "Direct link to Counterparty toxicity") Group by counterparty to identify which LPs are sending you toxic flow — trades that consistently move against you shortly after execution: Counterparty toxicity markout (buy side)[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.counterparty%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%20%2F%20t.price%20*%2010000)%20AS%20avg_markout_bps%2C%0A%20%20%20%20sum(t.quantity)%20AS%20total_volume%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20LIST%20(0%2C%201s%2C%205s%2C%2010s%2C%2030s%2C%201m%2C%205m)%20AS%20h%0AWHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24now-1h..%24now%27%0AGROUP%20BY%20t.symbol%2C%20t.counterparty%2C%20horizon_sec%0AORDER%20BY%20t.symbol%2C%20t.counterparty%2C%20horizon_sec%3B&executeQuery=true) SELECT t.symbol, t.counterparty, h.offset / 1000000000 AS horizon_sec, count() AS n, avg(((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000) AS avg_markout_bps, sum(t.quantity) AS total_volumeFROM fx_trades tHORIZON JOIN market_data m ON (symbol) LIST (0, 1s, 5s, 10s, 30s, 1m, 5m) AS hWHERE t.side = 'buy' AND t.timestamp IN '$now-1h..$now'GROUP BY t.symbol, t.counterparty, horizon_secORDER BY t.symbol, t.counterparty, horizon_sec; A counterparty whose markout is persistently negative across horizons is likely trading on information you don't have. Compare `total_volume` alongside markout — a small counterparty with terrible markout may not matter, but a large one warrants flow management. ### Passive vs aggressive with spread context[​](https://questdb.com/docs/cookbook/sql/finance/markout/#passive-vs-aggressive-with-spread-context "Direct link to Passive vs aggressive with spread context") Compare markout between passive (limit) and aggressive (market) orders, with the half-spread as a baseline. Aggressive fills should cost roughly half the spread; if the markout is worse than that, execution quality needs attention: Passive vs aggressive markout with half-spread baseline (buy side)[Demo this query](https://demo.questdb.io/?query=SELECT%0A%20%20%20%20t.symbol%2C%0A%20%20%20%20t.ecn%2C%0A%20%20%20%20t.passive%2C%0A%20%20%20%20h.offset%20%2F%201000000000%20AS%20horizon_sec%2C%0A%20%20%20%20count()%20AS%20n%2C%0A%20%20%20%20avg(((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20-%20t.price)%0A%20%20%20%20%20%20%20%20%2F%20t.price%20*%2010000)%20AS%20avg_markout_bps%2C%0A%20%20%20%20avg((m.best_ask%20-%20m.best_bid)%0A%20%20%20%20%20%20%20%20%2F%20((m.best_bid%20%2B%20m.best_ask)%20%2F%202)%20*%2010000)%20%2F%202%20AS%20avg_half_spread_bps%0AFROM%20fx_trades%20t%0AHORIZON%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20RANGE%20FROM%200s%20TO%205m%20STEP%205s%20AS%20h%0AWHERE%20t.side%20%3D%20%27buy%27%0A%20%20%20%20AND%20t.timestamp%20IN%20%27%24now-1h..%24now%27%0AGROUP%20BY%20t.symbol%2C%20t.ecn%2C%20t.passive%2C%20horizon_sec%0AORDER%20BY%20t.symbol%2C%20t.ecn%2C%20t.passive%2C%20horizon_sec%3B&executeQuery=true) SELECT t.symbol, t.ecn, t.passive, h.offset / 1000000000 AS horizon_sec, count() AS n, avg(((m.best_bid + m.best_ask) / 2 - t.price) / t.price * 10000) AS avg_markout_bps, avg((m.best_ask - m.best_bid) / ((m.best_bid + m.best_ask) / 2) * 10000) / 2 AS avg_half_spread_bpsFROM fx_trades tHORIZON JOIN market_data m ON (symbol) RANGE FROM 0s TO 5m STEP 5s AS hWHERE t.side = 'buy' AND t.timestamp IN '$now-1h..$now'GROUP BY t.symbol, t.ecn, t.passive, horizon_secORDER BY t.symbol, t.ecn, t.passive, horizon_sec; At offset 0, aggressive fills typically show `avg_markout_bps` close to negative `avg_half_spread_bps` (you crossed the spread). If markout recovers toward zero over subsequent offsets, execution is healthy — you paid the spread but the market didn't move further against you. If markout stays flat or worsens, it signals adverse selection beyond the spread cost. Interpreting the markout curve[​](https://questdb.com/docs/cookbook/sql/finance/markout/#interpreting-the-markout-curve "Direct link to Interpreting the markout curve") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- * **Flat near zero**: No significant post-trade price impact. Fills are neutral. * **Rising markout (positive trend)**: Price reverts in your favor after the fill. This is the ideal scenario — it suggests you are capturing spread or providing liquidity at good levels. * **Falling markout (negative trend)**: Adverse selection — the market moves against you after the fill. This may indicate you are being picked off by informed counterparties or reacting too slowly. * **Passive vs aggressive**: Passive fills typically show better markouts because they provide liquidity. Aggressive fills often show initial negative markout equal to the spread cost, which may or may not revert. * **Counterparty differences**: Persistent negative markout against specific counterparties is a strong signal of adverse selection and may warrant flow management. Related documentation * [HORIZON JOIN](https://questdb.com/docs/query/sql/horizon-join/) * [ASOF JOIN](https://questdb.com/docs/query/sql/asof-join/) * [Slippage per fill recipe](https://questdb.com/docs/cookbook/sql/finance/slippage/) * [Slippage (aggregated) recipe](https://questdb.com/docs/cookbook/sql/finance/slippage-aggregated/) * [Problem](https://questdb.com/docs/cookbook/sql/finance/markout/#problem) * [Solution](https://questdb.com/docs/cookbook/sql/finance/markout/#solution) * [How it works](https://questdb.com/docs/cookbook/sql/finance/markout/#how-it-works) * [Variations](https://questdb.com/docs/cookbook/sql/finance/markout/#variations) * [Markout at specific horizons](https://questdb.com/docs/cookbook/sql/finance/markout/#markout-at-specific-horizons) * [Pre- and post-trade analysis](https://questdb.com/docs/cookbook/sql/finance/markout/#pre--and-post-trade-analysis) * [Markout by side](https://questdb.com/docs/cookbook/sql/finance/markout/#markout-by-side) * [Single-side markout](https://questdb.com/docs/cookbook/sql/finance/markout/#single-side-markout) * [Counterparty toxicity](https://questdb.com/docs/cookbook/sql/finance/markout/#counterparty-toxicity) * [Passive vs aggressive with spread context](https://questdb.com/docs/cookbook/sql/finance/markout/#passive-vs-aggressive-with-spread-context) * [Interpreting the markout curve](https://questdb.com/docs/cookbook/sql/finance/markout/#interpreting-the-markout-curve) --- # SQLAlchemy | QuestDB On this page [SQLAlchemy](https://www.sqlalchemy.org/) is an open-source SQL toolkit and ORM library for Python. It provides a high-level API for communicating with [relational databases](https://questdb.com/glossary/relational-database/) , including schema creation and modification, an SQL expression language, and database connection management. The ORM layer abstracts away the complexities of the database, allowing developers to work with Python objects instead of raw SQL statements. QuestDB implements a dialect for SQLAlchemy using the [QuestDB Connect](https://github.com/questdb/questdb-connect) Python package. Please note that the SQLAlchemy ORM and metadata operations are only partially supported. Prerequisites[​](https://questdb.com/docs/integrations/other/sqlalchemy/#prerequisites "Direct link to Prerequisites") ----------------------------------------------------------------------------------------------------------------------- * Python from 3.9 to 3.11 * Psycopg2 * SQLAlchemy `<=` 1.4.47 * A QuestDB instance Installation[​](https://questdb.com/docs/integrations/other/sqlalchemy/#installation "Direct link to Installation") -------------------------------------------------------------------------------------------------------------------- You can install this package using `pip`: pip install questdb-connect Example usage[​](https://questdb.com/docs/integrations/other/sqlalchemy/#example-usage "Direct link to Example usage") ----------------------------------------------------------------------------------------------------------------------- import sqlalchemyfrom sqlalchemy import create_enginefrom sqlalchemy import textfrom sqlalchemy import MetaDatafrom sqlalchemy import Tablefrom pprint import pprintengine = create_engine("questdb://admin:quest@localhost:8812/qdb")with engine.connect() as conn: # SQL statements with no parameters conn.execute(text("CREATE TABLE IF NOT EXISTS some_table (x int, y int)")) result=conn.execute(text("SHOW TABLES")) print(result.all()) # results can be iterated in many ways. Check SQLAlchemy docs for details # passing parameters to your statements conn.execute( text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), [{"x": 11, "y": 12}, {"x": 13, "y": 14}], ) # basic select, no parameters result = conn.execute(text("select * from some_table")) print(result.all()) # select with parameters result = conn.execute(text("SELECT x, y FROM some_table WHERE y > :y"), {"y": 2}) print(result.all()) # partial support for metadata metadata_obj = MetaData() some_table = Table("some_table", metadata_obj, autoload_with=engine) pprint(some_table) # cleaning up conn.execute(text("DROP TABLE some_table")) See also[​](https://questdb.com/docs/integrations/other/sqlalchemy/#see-also "Direct link to See also") -------------------------------------------------------------------------------------------------------- * The [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/14/tutorial/index.html) * The [QuestDB Connect](https://pypi.org/project/questdb-connect/) GitHub * [Prerequisites](https://questdb.com/docs/integrations/other/sqlalchemy/#prerequisites) * [Installation](https://questdb.com/docs/integrations/other/sqlalchemy/#installation) * [Example usage](https://questdb.com/docs/integrations/other/sqlalchemy/#example-usage) * [See also](https://questdb.com/docs/integrations/other/sqlalchemy/#see-also) --- # CSV Import | QuestDB On this page tip CSV import is for bulk/batch loading. For streaming data, use [InfluxDB Line Protocol (ILP)](https://questdb.com/docs/ingestion/overview/) instead. There are three methods for CSV import: 1. [COPY SQL](https://questdb.com/docs/ingestion/import-csv/#import-csv-via-copy-sql) - Best for large files and migrations 2. [REST API](https://questdb.com/docs/ingestion/import-csv/#import-csv-via-rest) - For programmatic uploads of smaller files 3. [Web Console](https://questdb.com/docs/getting-started/web-console/import-csv/) - Interactive uploads via browser Import CSV via COPY SQL[​](https://questdb.com/docs/ingestion/import-csv/#import-csv-via-copy-sql "Direct link to Import CSV via COPY SQL") -------------------------------------------------------------------------------------------------------------------------------------------- The [COPY](https://questdb.com/docs/query/sql/copy/) SQL command is the preferred way to import large CSV files into partitioned tables. Use it for bulk data migrations from other databases. For partitioned tables, the best `COPY` performance can be achieved only on a machine with a local, physically attached SSD. It is possible to use a network block storage, such as an AWS EBS volume to perform the operation, with the following impact: * Users need to configure the maximum IOPS and throughput setting values for the volume. * The required import time is likely to be 5-10x longer. ### Prepare the import[​](https://questdb.com/docs/ingestion/import-csv/#prepare-the-import "Direct link to Prepare the import") Preparation is key. Import is a multi-step process, which consists of: * Export the existing database as CSV files * Enable and configure `COPY` command to be optimal for the system * Prepare target schema in QuestDB #### Export the existing database[​](https://questdb.com/docs/ingestion/import-csv/#export-the-existing-database "Direct link to Export the existing database") Export data using one CSV file per table. Include a column that can be used as the designated timestamp. Data in CSV is not expected to be in any particular order. If it is not possible to export the table as one CSV, export multiple files and concatenate these files before importing into QuestDB. ##### Concatenate multiple CSV files[​](https://questdb.com/docs/ingestion/import-csv/#concatenate-multiple-csv-files "Direct link to Concatenate multiple CSV files") The way to concatenate files depends on whether the CSV files have headers. For CSV files without headers, concatenation is straightforward: * Linux * macOS * Windows PowerShell ls *.csv | xargs cat > singleFile.csv ls *.csv | xargs cat > singleFile.csv $TextFiles = Get-Item C:\Users\path\to\csv\*.csv# The files are moved to the same folder.$TextFiles foreach { Add-Content -Value $(Get-Content $_) -Path C:\Users\path\to\csv\singleFile.csv} For CSV files with headers, concatenation can be tricky. You could manually remove the first line of the files before concatenating, or use some smart command line to concatenate and remove the headers. A good alternative is using the open source tool [csvstack](https://csvkit.readthedocs.io/en/latest/scripts/csvstack.html) . This is how you can concatenate multiple CSV files using _csvstack_: csvstack *.csv > singleFile.csv #### Things to know about `COPY`[​](https://questdb.com/docs/ingestion/import-csv/#things-to-know-about-copy "Direct link to things-to-know-about-copy") * `COPY` is disabled by default, as a security precaution. Configuration is required. * `COPY` is more efficient when source and target disks are different. * `COPY` is parallel when target table is partitioned. * `COPY` is _serial_ when target table is non-partitioned. Out-of-order timestamps are rejected. * `COPY` cannot import data into non-empty table. * `COPY` indexes CSV file; reading indexed CSV file benefits hugely from disk IOPS. We recommend using NVME. * `COPY` imports one file at a time; there is no internal queuing system yet. * [COPY reference](https://questdb.com/docs/query/sql/copy/) #### Configure `COPY`[​](https://questdb.com/docs/ingestion/import-csv/#configure-copy "Direct link to configure-copy") * Enable `COPY` and [configure](https://questdb.com/docs/configuration/overview/#copy-settings) the `COPY` directories to suit your server. * `cairo.sql.copy.root` must be set for `COPY` to work. ### Create the target table schema[​](https://questdb.com/docs/ingestion/import-csv/#create-the-target-table-schema "Direct link to Create the target table schema") If you know the target table schema already, you can [skip this section](https://questdb.com/docs/ingestion/import-csv/#import-csv) . QuestDB can analyze the input file and infer the schema. This happens automatically when the target table does not exist. To have QuestDB help with determining file schema, it is best to work with a sub-set of CSV. A smaller file allows us to iterate faster if iteration is required. Let's assume we have the following CSV: "locationId","timestamp","windDir","windSpeed","windGust","cloudCeiling","skyCover","visMiles","tempF","dewpF","rain1H","rain6H","rain24H","snowDepth"1,"2010-07-05T00:23:58.981263Z",3050,442,512,,"OBS",11.774906006761,-5,-31,58.228032196984,70.471606345673,77.938252342637,582,"2017-10-10T10:13:55.246046Z",900,63,428,5487,"BKN",4.958601701089,-19,-7,4.328016420894,36.020659549374,97.821114441800,413,"2010-03-12T11:17:13.727137Z",2880,299,889,371,"BKN",10.342717709226,46,81,9.149518425127,20.229637391479,20.074738007931,804,"2018-08-21T15:42:23.107543Z",930,457,695,4540,"OBS",13.359184086767,90,-47,33.346163208862,37.501996055160,58.316836760009,13... 1. Extract the first 1000 line to `test_file.csv` (assuming both files are in the `cairo.sql.copy.root` directory): head -1000 weather.csv > test_file.csv 2. Use a simple `COPY` command to import `test_file.csv` and define the table name: COPY weather from 'test_file.csv' WITH HEADER true; This creates the `weather` table and returns the ID of the background import process: | id | | --- | | 5179978a6d7a1772 | 3. In the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) right click table and select `Copy Schema to Clipboard` - this copies the schema generated by the input file analysis. 4. Paste the table schema to the code editor: CREATE TABLE 'weather' ( timestamp TIMESTAMP, windDir INT, windSpeed INT, windGust INT, cloudCeiling INT, skyCover VARCHAR, visMiles DOUBLE, tempF INT, dewpF INT, rain1H DOUBLE, rain6H DOUBLE, rain24H DOUBLE, snowDepth INT); 5. Identify the correct schema: 5.1. The generated schema may not be completely correct. Check the log table and log file to resolve common errors using the id (see also [Track import progress](https://questdb.com/docs/ingestion/import-csv/#track-import-progress) and [FAQ](https://questdb.com/docs/ingestion/import-csv/#faq) ): SELECT * FROM sys.text_import_log WHERE id = '5179978a6d7a1772' ORDER BY ts DESC; | ts | id | table | file | phase | status | message | rows\_handled | rows\_imported | errors | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 2022-08-08T16:38:06.262706Z | 5179978a6d7a1772 | weather | test\_file.csvtest\_file.csv | | finished | | 999 | 999 | 0 | | 2022-08-08T16:38:06.226162Z | 5179978a6d7a1772 | weather | test\_file.csvtest\_file.csv | | started | | | | 0 | Check `rows_handled`, `rows_imported`, and `message` for any errors and amend the schema as required. 5.2. Drop the table and re-import `test_file.csv` using the updated schema. 6. Repeat the steps to narrow down to a correct schema. The process may require either truncating: TRUNCATE TABLE table_name; or dropping the target table: DROP TABLE table_name; 7. Clean up: Once all the errors are resolved, copy the final schema, drop the small table. 8. Make sure table is correctly partitioned. The final schema in our example should look like this: CREATE TABLE 'weather' ( timestamp TIMESTAMP, windDir INT, windSpeed INT, windGust INT, cloudCeiling INT, skyCover VARCHAR, visMiles DOUBLE, tempF INT, dewpF INT, rain1H DOUBLE, rain6H DOUBLE, rain24H DOUBLE, snowDepth INT) TIMESTAMP (timestamp) partition by DAY; 9. Ready for import: Create an empty table using the final schema. ### Import CSV[​](https://questdb.com/docs/ingestion/import-csv/#import-csv "Direct link to Import CSV") Once an empty table is created in QuestDB using the correct schema, import can be initiated with: COPY weather FROM 'weather.csv' WITH HEADER true TIMESTAMP 'timestamp' FORMAT 'yyyy-MM-ddTHH:mm:ss.SSSUUUZ'; It quickly returns id of asynchronous import process running in the background: | id | | --- | | 55020329020b446a | ### Track import progress[​](https://questdb.com/docs/ingestion/import-csv/#track-import-progress "Direct link to Track import progress") `COPY` returns an id for querying the log table (`sys.text_import_log`), to monitor the progress of ongoing import: SELECT * FROM sys.text_import_log WHERE id = '55020329020b446a'; | ts | id | table | file | phase | status | message | rows\_handled | rows\_imported | errors | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 2022-08-03T14:00:40.907224Z | 55020329020b446a | weather | weather.csv | null | started | null | null | null | 0 | | 2022-08-03T14:00:40.910709Z | 55020329020b446a | weather | weather.csv | analyze\_file\_structure | started | null | null | null | 0 | | 2022-08-03T14:00:42.370563Z | 55020329020b446a | weather | weather.csv | analyze\_file\_structure | finished | null | null | null | 0 | | 2022-08-03T14:00:42.370793Z | 55020329020b446a | weather | weather.csv | boundary\_check | started | null | null | null | 0 | Looking at the log from the newest to the oldest might be more convenient: SELECT * FROM sys.text_import_log WHERE id = '55020329020b446a' ORDER BY ts DESC; Once import successfully ends the log table should contain a row with a 'null' phase and 'finished' status : | ts | id | table | file | phase | status | message | rows\_handled | rows\_imported | errors | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 2022-08-03T14:10:59.198672Z | 55020329020b446a | weather | weather.csv | null | finished | | 300000000 | 300000000 | 0 | Import into non-partitioned tables uses single-threaded implementation (serial import) that reports only start and finish records in the status table. Given an ordered CSV file `weather1mil.csv`, when importing, the log table shows: | ts | id | table | file | phase | status | message | rows\_handled | rows\_imported | errors | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 2022-08-03T15:00:40.907224Z | 42d31603842f771a | weather | weather1mil.csv | null | started | null | null | null | 0 | | 2022-08-03T15:01:20.000709Z | 42d31603842f771a | weather | weather1mil.csv | null | finished | null | 999999 | 999999 | 0 | The log table contains only coarse-grained, top-level data. Import phase run times vary a lot (e.g. `partition_import` often takes 80% of the whole import execution time), and therefore [the server log](https://questdb.com/docs/operations/logging-metrics/#logging) provides an alternative to follow more details of import: import log 2022-08-03T14:00:40.907224Z I i.q.c.t.ParallelCsvFileImporter started [importId=5502031634e923b2, phase=analyze_file_structure, file=`C:\dev\tmp\weather.csv`, workerCount=10]2022-08-03T14:00:40.917224Z I i.q.c.p.WriterPool >> [table=`weather`, thread=43]2022-08-03T14:00:41.440049Z I i.q.c.t.ParallelCsvFileImporter finished [importId=5502031634e923b2, phase=analyze_file_structure, file=`C:\dev\tmp\weather.csv`, duration=0s, errors=0]2022-08-03T14:00:41.440196Z I i.q.c.t.ParallelCsvFileImporter started [importId=5502031634e923b2, phase=boundary_check, file=`C:\dev\tmp\weather.csv`, workerCount=10]2022-08-03T14:01:18.853212Z I i.q.c.t.ParallelCsvFileImporter finished [importId=5502031634e923b2, phase=boundary_check, file=`C:\dev\tmp\weather.csv`, duration=6s, errors=0]2022-08-03T14:01:18.853303Z I i.q.c.t.ParallelCsvFileImporter started [importId=5502031634e923b2, phase=indexing, file=`C:\dev\tmp\weather.csv`, workerCount=10]2022-08-03T14:01:18.853516Z I i.q.c.t.ParallelCsvFileImporter temporary import directory [path='E:\dev\tmp\weather\]2022-08-03T14:01:42.612302Z I i.q.c.t.CsvFileIndexer finished chunk [chunkLo=23099021813, chunkHi=26948858785, lines=29999792, errors=0]2022-08-03T14:01:42.791789Z I i.q.c.t.CsvFileIndexer finished chunk [chunkLo=11549510915, chunkHi=15399347885, lines=30000011, errors=0] If the [`ON ERROR` option](https://questdb.com/docs/query/sql/copy/#options) is set to `ABORT`, import stops on the first error and the error is logged. Otherwise, all errors are listed in the log. The reference to the error varies depending on the phase of an import: * In the indexing phase, if an error occurs, the absolute input file line is referenced: 2022-08-08T11:50:24.319675Z E i.q.c.t.CsvFileIndexer could not parse timestamp [line=999986, column=1] * In the data import phase, if an error occurs, the log references the offset as related to the start of the file. 2022-08-08T12:19:56.828792Z E i.q.c.t.TextImportTask type syntax [type=INT, offset=5823, column=0, value='CMP2'] The errored rows can then be extracted for further investigation. ### FAQ[​](https://questdb.com/docs/ingestion/import-csv/#faq "Direct link to FAQ") COPY on a table with symbol columns is very slow. How can I speed it up? QuestDB uses `256` as the default symbol capacity. If the number of distinct symbol values exceeds this default significantly, the `COPY` performance will suffer. Make sure that you specify symbol capacities when creating the table before running the `COPY` command. Here is an example: CREATE TABLE table_name ( ts TIMESTAMP, sym SYMBOL CAPACITY 100000) TIMESTAMP(ts) PARTITION BY DAY; Refer to the [symbol type documentation](https://questdb.com/docs/concepts/symbol/) for more information on configuring the symbol capacity. What happens in a database crash or OS reboot? If reboot/power loss happens while partitions are being attached, the table may be left with incomplete data. Truncate the table before re-importing with: TRUNCATE TABLE table_name; If reboot/power loss happens before any partitions being attached, the import should not be affected. I'm getting "COPY is disabled \['cairo.sql.copy.root' is not set?\]" error message Please set `cairo.sql.copy.root` setting, restart the instance and try again. I'm getting "could not create temporary import work directory \[path='somepath', errno=-1\]" error message Please make sure that the `cairo.sql.copy.root` and `cairo.sql.copy.work.root` are valid paths pointing to existing directories. I'm getting "\[2\] could not open read-only \[file=somepath\]" error message Please check that import file path is valid and accessible to QuestDB instance users. If you are running QuestDB using Docker, please check if the directory mounted for storing source CSV files is identical to the one `cairo.sql.copy.root` property or `QDB_CAIRO_SQL_COPY_ROOT` environment variable points to. For example, the following command can start a QuestDB instance: docker run -p 9000:9000 \-v "/tmp/questdb:/var/lib/questdb" \-v "/tmp/questdb/my_input_root:/tmp/questdb_import" \-e QDB_CAIRO_SQL_COPY_ROOT=/tmp/questdb_wrong \questdb/questdb However, running: COPY weather from 'weather_example.csv' WITH HEADER true; Results in the "\[2\] could not open read-only \[file=/tmp/questdb\_wrong/weather\_example.csv\]" error message. I'm getting "column count mismatch \[textColumnCount=4, tableColumnCount=3, table=someTable\]" error message There are more columns in input file than in the existing target table. Please remove column(s) from input file or add them to the target table schema. I'm getting "timestamp column 'ts2' not found in file header" error message Either input file is missing header or timestamp column name given in `COPY` command is invalid. Please add file header or fix timestamp option. I'm getting "column is not a timestamp \[no=0, name='ts'\]" error message Timestamp column given by the user or (if header is missing) assumed based on target table schema is of a different type. Please check timestamp column name in input file header or make sure input file column order matches that of target table. I'm getting "target table must be empty \[table=t\]" error message `COPY` doesn't yet support importing into partitioned table with existing data. Please truncate table before re-importing with: TRUNCATE TABLE table_name; or import into another empty table and then use `INSERT INTO SELECT`: INSERT BATCH 100000 INTO table_nameSELECT * FROM other_table; to copy data into original target table. I'm getting "io\_uring error" error message It's possible that you've hit a IO\_URING-related kernel error. Please set `cairo.iouring.enabled` setting to false, restart QuestDB instance, and try again. I'm getting "name is reserved" error message The table you're trying to import into is in a bad state (incomplete metadata). Please either drop the table with: DROP TABLE table_name; and recreate the table or change the table name in the `COPY` command. I'm getting "Unable to process the import request. Another import request may be in progress." error message Only one import can be running at a time. Either cancel running import with: COPY 'paste_import_id_here' CANCEL; or wait until the current import is finished. Import finished but table is (almost) empty Please check the latest entries in log table: SELECT * FROM sys.text_import_log LIMIT -10; If "errors" column is close to number of records in the input file then it may mean: * `FORMAT` option of `COPY` command or auto-detected format doesn't match timestamp column data in file * Other column(s) can't be parsed and `ON ERROR SKIP_ROW` option was used * Input file is unordered and target table has designated timestamp but is not partitioned If none of the above causes the error, please check the log file for messages like: 2022-08-08T11:50:24.319675Z E i.q.c.t.CsvFileIndexer could not parse timestamp [line=999986, column=1] or 2022-08-08T12:19:56.828792Z E i.q.c.t.TextImportTask type syntax [type=INT, offset=5823, column=0, value='CMP2'] that should explain why rows were rejected. Note that in these examples, the former log message mentions the absolute input file line while the latter is referencing the offset as related to the start of the file. Import finished but table column names are `f0`, `f1`, ... The input file has no header and the target table does not exist, so columns received synthetic names. You can rename them with `ALTER TABLE`: ALTER TABLE table_name RENAME COLUMN f0 TO ts; Import CSV via Rest[​](https://questdb.com/docs/ingestion/import-csv/#import-csv-via-rest "Direct link to Import CSV via Rest") -------------------------------------------------------------------------------------------------------------------------------- The REST API provides an `/imp` endpoint exposed on port `9000` by default. This endpoint allows streaming tabular text data directly into a table, supporting CSV, TAB and pipe (`|`) delimited inputs with optional headers. Data types and structures are detected automatically, but additional configurations can be provided to improve automatic detection. note The REST API is better suited when the following conditions are true: * Regular uploads of small batches of data into the same table. * The file batches do not contain overlapping periods (they contain distinct days/weeks/months). Otherwise, the import performance will be impacted. For database migrations, or uploading one large CSV file into QuestDB, users may consider using the `COPY` SQL command. See [COPY command documentation](https://questdb.com/docs/query/sql/copy/) and [Guide on CSV import](https://questdb.com/docs/ingestion/import-csv/#import-csv-via-copy-sql) for more details. ### Importing compressed files[​](https://questdb.com/docs/ingestion/import-csv/#importing-compressed-files "Direct link to Importing compressed files") It is possible to upload compressed files directly without decompression: gzip -cd compressed_data.tsv.gz | curl -v -F data=@- 'http://localhost:9000/imp' The `data=@-` value instructs `curl` to read the file contents from `stdin`. ### Specifying a schema during CSV import[​](https://questdb.com/docs/ingestion/import-csv/#specifying-a-schema-during-csv-import "Direct link to Specifying a schema during CSV import") A `schema` JSON object can be provided with POST requests to `/imp` while creating tables via CSV import. This allows for more control over user-defined patterns for timestamps, or for explicitly setting types during column-creation. The following example demonstrates basic usage, in this case, that the `ticker_name` column should be parsed as `SYMBOL` type instead of `VARCHAR`: curl \ -F schema='[{"name":"ticker_name", "type": "SYMBOL"}]' \ -F data=@trades.csv 'http://localhost:9000/imp' If a timestamp column (`ts`) in this CSV file has a custom or non-standard timestamp format, this may be included with the call as follows: curl \ -F schema='[ \ {"name":"ts", "type": "TIMESTAMP", "pattern": "yyyy-MM-dd - HH:mm:ss"}, \ {"name":"ticker_name", "type": "SYMBOL"} \ ]' \ -F data=@trades.csv 'http://localhost:9000/imp' For **nanosecond-precision** timestamps such as `2021-06-22T12:08:41.077338934Z`, a pattern can be provided in the following way: curl \ -F schema='[ \ {"name":"ts", "type": "TIMESTAMP", "pattern": "yyyy-MM-ddTHH:mm:ss.SSSUUUNNNZ"} \ ]' \ -F data=@my_file.csv \ http://localhost:9000/imp More information on the patterns for timestamps can be found on the [date and time functions](https://questdb.com/docs/query/functions/date-time/#timestamp-format) page. note The `schema` object must precede the `data` object in calls to this REST endpoint. For example: # correct ordercurl -F schema='{my_schema_obj}' -F data=@my_file.csv http://localhost:9000/imp# incorrect ordercurl -F data=@my_file.csv -F schema='{my_schema_obj}' http://localhost:9000/imp ### Text loader configuration[​](https://questdb.com/docs/ingestion/import-csv/#text-loader-configuration "Direct link to Text loader configuration") QuestDB uses a `text_loader.json` configuration file which can be placed in the server's `conf` directory. This file does not exist by default, but has the following implicit settings: conf/text\_loader.json { "date": [ { "format": "dd/MM/y" }, { "format": "yyyy-MM-dd HH:mm:ss" }, { "format": "yyyy-MM-ddTHH:mm:ss.SSSz", "locale": "en-US", "utf8": false }, { "format": "MM/dd/y" } ], "timestamp": [ { "format": "yyyy-MM-ddTHH:mm:ss.SSSUUUz", "utf8": false } ]} #### Example[​](https://questdb.com/docs/ingestion/import-csv/#example "Direct link to Example") Given a CSV file which contains timestamps in the format `yyyy-MM-dd - HH:mm:ss.SSSUUU`, the following text loader configuration will provide the correct timestamp parsing: conf/text\_loader.json { "date": [ { "format": "dd/MM/y" }, { "format": "yyyy-MM-dd HH:mm:ss" }, { "format": "yyyy-MM-ddTHH:mm:ss.SSSz", "locale": "en-US", "utf8": false }, { "format": "MM/dd/y" } ], "timestamp": [ { "format": "yyyy-MM-ddTHH:mm:ss.SSSUUUz", "utf8": false }, { "format": "yyyy-MM-dd - HH:mm:ss.SSSUUU", "utf8": false } ]} The CSV data can then be loaded via POST request, for example, using cURL: curl -F data=@weather.csv 'http://localhost:9000/imp' For more information on the `/imp` entry point, refer to the [REST API documentation](https://questdb.com/docs/query/rest-api/#imp---import-data) . * [Import CSV via COPY SQL](https://questdb.com/docs/ingestion/import-csv/#import-csv-via-copy-sql) * [Prepare the import](https://questdb.com/docs/ingestion/import-csv/#prepare-the-import) * [Create the target table schema](https://questdb.com/docs/ingestion/import-csv/#create-the-target-table-schema) * [Import CSV](https://questdb.com/docs/ingestion/import-csv/#import-csv) * [Track import progress](https://questdb.com/docs/ingestion/import-csv/#track-import-progress) * [FAQ](https://questdb.com/docs/ingestion/import-csv/#faq) * [Import CSV via Rest](https://questdb.com/docs/ingestion/import-csv/#import-csv-via-rest) * [Importing compressed files](https://questdb.com/docs/ingestion/import-csv/#importing-compressed-files) * [Specifying a schema during CSV import](https://questdb.com/docs/ingestion/import-csv/#specifying-a-schema-during-csv-import) * [Text loader configuration](https://questdb.com/docs/ingestion/import-csv/#text-loader-configuration) --- # Using Docker with QuestDB | QuestDB On this page QuestDB has images for both Linux/macOS and Windows on [Docker Hub](https://hub.docker.com/r/questdb/questdb) . Install Docker[​](https://questdb.com/docs/deployment/docker/#install-docker "Direct link to Install Docker") -------------------------------------------------------------------------------------------------------------- To begin, install Docker. You can find guides for your platform on the [official documentation](https://docs.docker.com/get-docker/) . Run QuestDB image[​](https://questdb.com/docs/deployment/docker/#run-questdb-image "Direct link to Run QuestDB image") ----------------------------------------------------------------------------------------------------------------------- Once Docker is installed, you will need to pull QuestDB's image from [Docker Hub](https://hub.docker.com/r/questdb/questdb) and create a container. This can be done with a single command using: docker run \-p 9000:9000 -p 9009:9009 -p 8812:8812 -p 9003:9003 \questdb/questdb:9.3.3 This command starts a Docker container from `questdb/questdb` image. In addition, it exposes some ports, allowing you to explore QuestDB. In order to configure QuestDB, it is recommended to mount a [volume](https://questdb.com/docs/deployment/docker/#-v-parameter-to-mount-storage) to allow data persistance. This can be done by adding a `-v` flag to the above command: -v "/host/volume/location:/var/lib/questdb" Below each parameter is described in detail. ### `-p` parameter to expose ports[​](https://questdb.com/docs/deployment/docker/#-p-parameter-to-expose-ports "Direct link to -p-parameter-to-expose-ports") This parameter will expose a port to the host. You can specify: * `-p 9000:9000` - [REST API](https://questdb.com/docs/query/rest-api/) and [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) * `-p 9009:9009` - [InfluxDB line protocol](https://questdb.com/docs/ingestion/ilp/overview/) * `-p 8812:8812` - [Postgres wire protocol](https://questdb.com/docs/query/pgwire/overview/) * `-p 9003:9003` - [Min health server](https://questdb.com/docs/operations/logging-metrics/#minimal-http-server) All ports are optional, you can pick only the ones you need. For example, it is enough to expose `8812` if you only plan to use [Postgres wire protocol](https://questdb.com/docs/query/pgwire/overview/) . ### `-v` parameter to mount storage[​](https://questdb.com/docs/deployment/docker/#-v-parameter-to-mount-storage "Direct link to -v-parameter-to-mount-storage") This parameter will make a local directory available to QuestDB Docker container. It will have all data ingested to QuestDB, server logs and configuration. The QuestDB [root\_directory](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/) is located at the `/var/lib/questdb` path in the container. ### Docker image version[​](https://questdb.com/docs/deployment/docker/#docker-image-version "Direct link to Docker image version") By default, `questdb/questdb` points to the latest QuestDB version available on Docker. However, it is recommended to define the version used. questdb/questdb:9.3.3 Environment variables[​](https://questdb.com/docs/deployment/docker/#environment-variables "Direct link to Environment variables") ----------------------------------------------------------------------------------------------------------------------------------- Server configuration can be passed to QuestDB running in Docker by using the `-e` flag to pass an environment variable to a container: docker run -p 4000:4000 -e QDB_HTTP_BIND_TO=0.0.0.0:4000 questdb/questdb For a list of configuration options, see [Configuration](https://questdb.com/docs/configuration/overview/) . Container status[​](https://questdb.com/docs/deployment/docker/#container-status "Direct link to Container status") -------------------------------------------------------------------------------------------------------------------- You can check the status of your container with `docker ps`. It also lists the exposed ports, container name, uptime and more: Finding container status with docker ps docker ps Result of docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESdd363939f261 questdb/questdb "/app/bin/java -m io…" 3 seconds ago Up 2 seconds 8812/tcp, 9000/tcp frosty_gauss This container: * has an id of `dd363939f261` * uses ports `8812` & `9000`, for Postgres wire protocol and HTTP respectively * is using a `questdb/questdb` image * ran java to start the binary * is 3 seconds old * has been up for 2 seconds * has the unfortunate name of `frosty_gauss` For full container status information, see the [`docker ps` manual](https://docs.docker.com/engine/reference/commandline/ps/) . ### Debugging container logs[​](https://questdb.com/docs/deployment/docker/#debugging-container-logs "Direct link to Debugging container logs") Docker may generate a runtime error. The error may not be accurate, as the true culprit is often indicated higher up in the logs. To see the full log, retrieve the UUID - also known as the `CONTAINER ID` - using `docker ps`: Finding the CONTAINER ID CONTAINER ID IMAGE ...dd363939f261 questdb/questdb ... Now pass the `CONTAINER ID` - or `dd363939f261` - to the `docker logs` command: Generating a docker log from a CONTAINER ID $ docker logs dd363939f261No arguments found, start with default argumentsRunning as questdb userLog configuration loaded from: /var/lib/questdb/conf/log.conf...... Note that the log will pull from `/var/lib/questdb/conf/log.conf` by default. Sharing this log when seeking support for Docker deployments will help us find the root cause. Importing data and sending queries[​](https://questdb.com/docs/deployment/docker/#importing-data-and-sending-queries "Direct link to Importing data and sending queries") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- When QuestDB is running, you can start interacting with it: * Port `9000` is for REST. More info is available on the [REST documentation page](https://questdb.com/docs/query/rest-api/) . * Port `8812` is used for Postgres. Check our [Postgres reference page](https://questdb.com/docs/query/pgwire/overview/) . * Port `9009` is dedicated to InfluxDB Line Protocol. Consult our [InfluxDB protocol page](https://questdb.com/docs/ingestion/ilp/overview/) . Data persistence[​](https://questdb.com/docs/deployment/docker/#data-persistence "Direct link to Data persistence") -------------------------------------------------------------------------------------------------------------------- ### Mounting a volume[​](https://questdb.com/docs/deployment/docker/#mounting-a-volume "Direct link to Mounting a volume") Volumes can be mounted to the QuestDB Docker container so that data may be persisted or server configuration settings may be passed to an instance. The following example demonstrated how to mount the current directory to a QuestDB container using the `-v` flag in a Docker `run` command: Mounting a volume docker run -p 9000:9000 \-p 9009:9009 \-p 8812:8812 \-p 9003:9003 \-v "$(pwd):/var/lib/questdb" \questdb/questdb:9.3.3 The current directory will then have data persisted to disk for convenient migration or backups: Current directory contents ├── conf│ └── server.conf├── db├── log├── public└── snapshot (optional) A server configuration file can also be provided by mounting a local directory in a QuestDB container. Given the following configuration file which overrides the default HTTP bind property: ./server.conf http.bind.to=0.0.0.0:4000 Running the container with the `-v` flag allows for mounting the current directory to QuestDB's `conf` directory in the container. With the server configuration above, HTTP ports for the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) and REST API will be available on `http://localhost:4000`: docker run -v "$(pwd):/var/lib/questdb/conf" -p 4000:4000 questdb/questdb note If you wish to use ZFS for your QuestDB deployment, with Docker, then you will need to enable ZFS on the host volume that Docker uses. Please see the [docker documentation](https://docs.docker.com/storage/storagedriver/zfs-driver/) for more information. ### Upgrade QuestDB version[​](https://questdb.com/docs/deployment/docker/#upgrade-questdb-version "Direct link to Upgrade QuestDB version") It is possible to upgrade your QuestDB instance on Docker when a volume is mounted to maintain data persistence. note * Check the [release notes](https://github.com/questdb/questdb/releases) and ensure that necessary [backup](https://questdb.com/docs/operations/backup/) is completed. * Upgrading an instance is possible only when the original instance has a volume mounted. Without mounting a volume for the original instance, the following steps create a new instance and data in the old instance cannot be retrieved. 1. Run `docker ps` to copy the container name or ID: Container status # The existing QuestDB version is 6.5.2:CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESdd363939f261 questdb/questdb:6.5.2 "/app/bin/java -m io…" 3 seconds ago Up 2 seconds 8812/tcp, 9000/tcp frosty_gauss 2. Stop the instance and then remove the container: docker stop dd363939f261docker rm dd363939f261 3. Download the latest QuestDB image: docker pull questdb/questdb:9.3.3 4. Start a new container with the new version and the same volume mounted: docker run -p 8812:8812 -p 9000:9000 -v "$(pwd):/var/lib/questdb" questdb/questdb:9.3.3 ### Writing logs to disk[​](https://questdb.com/docs/deployment/docker/#writing-logs-to-disk "Direct link to Writing logs to disk") When mounting a volume to a Docker container, a logging configuration file may be provided in the container located at `/conf/log.conf`: Current directory contents └── conf ├── log.conf └── server.conf For example, a file with the following contents can be created: ./conf/log.conf # list of configured writerswriters=file,stdout,http.min# file writerw.file.class=io.questdb.log.LogFileWriterw.file.location=questdb-docker.logw.file.level=INFO,ERROR,DEBUG# stdoutw.stdout.class=io.questdb.log.LogConsoleWriterw.stdout.level=INFO# min http server, used monitoringw.http.min.class=io.questdb.log.LogConsoleWriterw.http.min.level=ERRORw.http.min.scope=http-min-server The current directory can be mounted: Mounting the current directory to a QuestDB container docker run -p 9000:9000 \ -p 9009:9009 \ -p 8812:8812 \ -p 9003:9003 \ -v "$(pwd):/root/.questdb/" questdb/questdb The container logs will be written to disk using the logging level and file name provided in the `conf/log.conf` file, in this case in `./questdb-docker.log`: Current directory tree ├── conf│ ├── log.conf│ └── server.conf├── db│ ├── table1│ └── table2├── public│ ├── ui / assets│ ├── ...│ └── version.txt└── questdb-docker.log For more information on logging, see the [configuration reference documentation](https://questdb.com/docs/operations/logging-metrics/#docker-logging) . ### Restart an existing container[​](https://questdb.com/docs/deployment/docker/#restart-an-existing-container "Direct link to Restart an existing container") Running the following command will create a new container for the QuestDB image: docker run -p 9000:9000 \ -p 9009:9009 \ -p 8812:8812 \ -p 9003:9003 \ questdb/questdb By giving the container a name with `--name container_name`, we have an easy way to refer to the container created by run later on: docker run -p 9000:9000 \ -p 9009:9009 \ -p 8812:8812 \ -p 9003:9003 \ --name docker_questdb \ questdb/questdb If we want to re-use this container and its data after it has been stopped, we can use the following commands: # bring the container updocker start docker_questdb# shut the container downdocker stop docker_questdb Alternatively, restart it using the `CONTAINER ID`: Starting a container by CONTAINER ID docker start dd363939f261 * [Install Docker](https://questdb.com/docs/deployment/docker/#install-docker) * [Run QuestDB image](https://questdb.com/docs/deployment/docker/#run-questdb-image) * [`-p` parameter to expose ports](https://questdb.com/docs/deployment/docker/#-p-parameter-to-expose-ports) * [`-v` parameter to mount storage](https://questdb.com/docs/deployment/docker/#-v-parameter-to-mount-storage) * [Docker image version](https://questdb.com/docs/deployment/docker/#docker-image-version) * [Environment variables](https://questdb.com/docs/deployment/docker/#environment-variables) * [Container status](https://questdb.com/docs/deployment/docker/#container-status) * [Debugging container logs](https://questdb.com/docs/deployment/docker/#debugging-container-logs) * [Importing data and sending queries](https://questdb.com/docs/deployment/docker/#importing-data-and-sending-queries) * [Data persistence](https://questdb.com/docs/deployment/docker/#data-persistence) * [Mounting a volume](https://questdb.com/docs/deployment/docker/#mounting-a-volume) * [Upgrade QuestDB version](https://questdb.com/docs/deployment/docker/#upgrade-questdb-version) * [Writing logs to disk](https://questdb.com/docs/deployment/docker/#writing-logs-to-disk) * [Restart an existing container](https://questdb.com/docs/deployment/docker/#restart-an-existing-container) --- # Create a sample database | QuestDB On this page This guide walks you through creating a sample dataset. It utilizes `rnd_` functions and basic SQL grammar to generate 'mock' data of specific types. For most applications, you will import your data using methods like the InfluxDB Line Protocol, CSV imports, or integration with third-party tools such as Telegraf, [Kafka](https://questdb.com/docs/ingestion/message-brokers/kafka/) , or Prometheus. If your interest lies in data ingestion rather than generation, refer to our [ingestion overview](https://questdb.com/docs/ingestion/overview/) . Alternatively, the [QuestDB demo instance](https://demo.questdb.io/) offers a practical way to explore data creation and manipulation without setting up your dataset. All that said, in this tutorial you will learn how to: 1. [Create tables](https://questdb.com/docs/getting-started/create-database/#creating-a-table) 2. [Populate tables with sample data](https://questdb.com/docs/getting-started/create-database/#inserting-data) 3. [Run simple and advanced queries](https://questdb.com/docs/getting-started/create-database/#running-queries) 4. [Delete tables](https://questdb.com/docs/getting-started/create-database/#deleting-tables) ### Before we begin...[​](https://questdb.com/docs/getting-started/create-database/#before-we-begin "Direct link to Before we begin...") All commands are run through the [Web Console](https://questdb.com/docs/getting-started/web-console/overview/) accessible at `http://localhost:9000`. You can also run the same SQL via the [Postgres endpoint](https://questdb.com/docs/query/pgwire/overview/) or the [REST API](https://questdb.com/docs/query/rest-api/) . If QuestDB is not running locally, checkout the [quick start](https://questdb.com/docs/getting-started/quick-start/) . ### Creating a table[​](https://questdb.com/docs/getting-started/create-database/#creating-a-table "Direct link to Creating a table") With QuestDB running, the first step is to create a table. We'll start with one representing financial market data. Then in the insert section, we'll create another pair of tables representing temperature sensors and their readings. Let's start by creating the `trades` table: CREATE TABLE trades ( timestamp TIMESTAMP, symbol SYMBOL, side SYMBOL, price DOUBLE, amount DOUBLE) TIMESTAMP(timestamp) PARTITION BY DAYDEDUP UPSERT KEYS(timestamp, symbol); This table uses QuestDB's key time-series features: * **`TIMESTAMP(timestamp)`** — Designates the time column. QuestDB physically sorts data by this column, enabling sub-millisecond time-range queries. * **`PARTITION BY DAY`** — Splits data into daily partitions for efficient queries and data lifecycle management. * **`SYMBOL`** — Optimized type for repeated strings like tickers. * **`DEDUP UPSERT KEYS`** — Prevents duplicate rows. For a deeper understanding, see [Schema design](https://questdb.com/docs/schema-design-essentials/) . We've done all of this to match the nature of how we'll query this data. We're focused on a the flow of the market, the pulse of the market's day-to-day, hence we've partitioned it as such. We're also leery of duplicates, for accuracy of data, so we'll ensure that if timestamps are identical that we do not create a duplicate. Timestamps are essential for time-series analysis. We'll proceed forward to INSERT. ### Inserting data[​](https://questdb.com/docs/getting-started/create-database/#inserting-data "Direct link to Inserting data") #### Financial market data[​](https://questdb.com/docs/getting-started/create-database/#financial-market-data "Direct link to Financial market data") Let's populate our `trades` table with procedurally-generated data: Insert as SELECT INSERT INTO trades SELECT timestamp_sequence('2024-01-01T00:00:00', 60000L * x) timestamp, -- Generate a timestamp every minute starting from Jan 1, 2024 rnd_str('ETH-USD', 'BTC-USD', 'SOL-USD', 'LTC-USD', 'UNI-USD') symbol, -- Random ticker symbols rnd_str('buy', 'sell') side, -- Random side (BUY or SELL) rnd_double() * 1000 + 100 price, -- Random price between 100.0 and 1100.0, rnd_double() * 2000 + 0.1 amount -- Random price between 0.1 and 2000.1 FROM long_sequence(10000) x; Our `trades` table now contains 10,000 randomly-generated trades. The comments indicate how we've structured our random data. We picked a few companies, BUY vs. SELL, and created a timestamp every minute. We've dictated the overall number of rows generated via `long_sequence(10000)`. We can bump that up, if we want. We've also conservatively generated a timestamp per minute, even though in reality trades against these companies are likely much more frequent. This helps keep our basic examples basic. Now let's look at the table and its data: 'trades'; It will look similar to this, albeit with alternative randomized values. | timestamp | symbol | side | price | amount | | --- | --- | --- | --- | --- | | 2024-01-01T00:00:00.000000Z | BTC-USD | sell | 483.904143675277 | 139.449481016294 | | 2024-01-01T00:00:00.060000Z | ETH-USD | sell | 920.296245196274 | 920.296245196274 | | 2024-01-01T00:00:00.180000Z | UNI-USD | sell | 643.277468441839 | 643.277468441839 | | 2024-01-01T00:00:00.360000Z | LTC-USD | buy | 218.0920768859 | 729.81119178972 | | 2024-01-01T00:00:00.600000Z | BTC-USD | sell | 157.596416931116 | 691.081778396176 | That's some fake market data. Let's create more tables to demonstrate joins. ### Quotes and instruments[​](https://questdb.com/docs/getting-started/create-database/#quotes-and-instruments "Direct link to Quotes and instruments") This next example will create and populate two more tables. One table will contain price quotes, and the other will contain instrument metadata. In both cases, we will create the table and generate the data at the same time. This combines the CREATE & SELECT operations to perform a create-and-insert: Create table as, quotes CREATE TABLE quotesAS( SELECT x ID, timestamp_sequence(to_timestamp('2019-10-17T00:00:00', 'yyyy-MM-ddTHH:mm:ss'), rnd_long(1,10,0) * 100000L) ts, rnd_double(0)*80 + 100 price, rnd_long(0, 10000, 0) instrument_id FROM long_sequence(10000000) x)TIMESTAMP(ts)PARTITION BY MONTH DEDUP UPSERT KEYS(ts, instrument_id); This table uses the same time-series features: * **`TIMESTAMP(ts)`** — Designates the time column for fast time-range queries. * **`PARTITION BY MONTH`** — Monthly partitions (use larger partitions for lower-volume data). * **`DEDUP UPSERT KEYS(ts, instrument_id)`** — One quote per timestamp per instrument. The generated data will look like the following: | ID | ts | price | instrument\_id | | --- | --- | --- | --- | | 1 | 2019-10-17T00:00:00.000000Z | 145.37 | 9160 | | 2 | 2019-10-17T00:00:00.600000Z | 162.91 | 9671 | | 3 | 2019-10-17T00:00:01.400000Z | 128.58 | 8731 | | 4 | 2019-10-17T00:00:01.500000Z | 131.69 | 3447 | | 5 | 2019-10-17T00:00:01.600000Z | 155.68 | 7985 | | ... | ... | ... | ... | Nice - and our next table, which includes the instruments themselves and their detail: Create table as, instruments CREATE TABLE instrumentsAS( SELECT x ID, -- Increasing integer rnd_str('NYSE', 'NASDAQ', 'LSE', 'TSE', 'HKEX') exchange, -- Random exchange rnd_str('Tech', 'Finance', 'Energy', 'Healthcare', 'Consumer') sector -- Random sector FROM long_sequence(10000) x) This `instruments` table has no designated timestamp — it's a static lookup table with no time dimension. This is the exception; most QuestDB tables should have a designated timestamp to enable time-series optimizations. With these two new tables, and our prior financial market data table, we've got a lot of useful queries we can test. ### Running queries[​](https://questdb.com/docs/getting-started/create-database/#running-queries "Direct link to Running queries") Our financial market data table is a great place to test various [aggregate functions](https://questdb.com/docs/query/functions/aggregation/) , to compute price over time intervals, and similar analysis. Let's expand on the `quotes` and `instruments` tables. First, let's look at `quotes`, running our shorthand for `SELECT * FROM quotes;`: quotes; Let's then select the `count` of records from `quotes`: SELECT count() FROM quotes; | count | | --- | | 10,000,000 | And then the average price: SELECT avg(price) FROM quotes; | average | | --- | | 139.99217780895 | We can now use the `instruments` table alongside the `quotes` table to get more interesting results using a `JOIN`: SELECT *FROM quotesJOIN( SELECT ID inst_id, exchange, sector FROM instruments)ON quotes.instrument_id = inst_id; The results should look like the table below: | ID | ts | price | instrument\_id | inst\_id | exchange | sector | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2019-10-17T00:00:00.000000Z | 146.47 | 3211 | 3211 | LSE | Tech | | 2 | 2019-10-17T00:00:00.100000Z | 136.59 | 2319 | 2319 | NASDAQ | Finance | | 3 | 2019-10-17T00:00:00.100000Z | 160.29 | 8723 | 8723 | NYSE | Tech | | 4 | 2019-10-17T00:00:00.100000Z | 170.94 | 885 | 885 | HKEX | Healthcare | | 5 | 2019-10-17T00:00:00.200000Z | 149.34 | 3200 | 3200 | NASDAQ | Energy | | 6 | 2019-10-17T00:00:01.100000Z | 160.95 | 4053 | 4053 | TSE | Consumer | Note the timestamps returned as we've JOIN'd the tables together. Let's try another type of aggregation: Aggregation keyed by sector SELECT sector, max(price)FROM quotesJOIN( SELECT ID inst_id, sector FROM instruments) aON quotes.instrument_id = a.inst_id; The results should look like the table below: | sector | max | | --- | --- | | Tech | 179.99998786398 | | Finance | 179.99998138348 | | Energy | 179.9999994818 | | Healthcare | 179.99991705861 | | Consumer | 179.99999233377 | Back to time, given we have one table (`quotes`) partitioned by time, let's see what we can do when we JOIN the tables together to perform an aggregation based on an hour of time: Aggregation by hourly time buckets SELECT ts, sector, exchange, avg(price)FROM quotes timestamp(ts)JOIN (SELECT ID inst_id, sector, exchange FROM instruments WHERE sector='Tech' AND exchange='NYSE') aON quotes.instrument_id = a.inst_idWHERE ts IN '2019-10-21;1d' -- this is an interval between 2019/10/21 and the next daySAMPLE BY 1h -- aggregation by hourly time bucketsALIGN TO CALENDAR; -- align the ts with the start of the hour (hh:00:00) The results should look like the table below: | ts | sector | exchange | average | | --- | --- | --- | --- | | 2019-10-21T00:00:00.000000Z | Tech | NYSE | 140.004285872 | | 2019-10-21T00:01:00.000000Z | Tech | NYSE | 136.68436714 | | 2019-10-21T00:02:00.000000Z | Tech | NYSE | 135.24368409 | | 2019-10-21T00:03:00.000000Z | Tech | NYSE | 137.19398410 | | 2019-10-21T00:04:00.000000Z | Tech | NYSE | 150.77868682 | | ... | ... | ... | ... | For more information about these statements, please refer to the [SELECT](https://questdb.com/docs/query/sql/select/) , [JOIN](https://questdb.com/docs/query/sql/join/) and [SAMPLE BY](https://questdb.com/docs/query/sql/sample-by/) pages. ### Deleting tables[​](https://questdb.com/docs/getting-started/create-database/#deleting-tables "Direct link to Deleting tables") We can now clean up the demo data by using [`DROP TABLE`](https://questdb.com/docs/query/sql/drop/) SQL. Be careful using this statement as QuestDB cannot recover data that is deleted in this way: DROP TABLE quotes;DROP TABLE instruments;DROP TABLE trades; * [Before we begin...](https://questdb.com/docs/getting-started/create-database/#before-we-begin) * [Creating a table](https://questdb.com/docs/getting-started/create-database/#creating-a-table) * [Inserting data](https://questdb.com/docs/getting-started/create-database/#inserting-data) * [Quotes and instruments](https://questdb.com/docs/getting-started/create-database/#quotes-and-instruments) * [Running queries](https://questdb.com/docs/getting-started/create-database/#running-queries) * [Deleting tables](https://questdb.com/docs/getting-started/create-database/#deleting-tables) --- # Order-level implementation shortfall | QuestDB On this page The [fill-level IS decomposition](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/) breaks down cost into spread, permanent, and temporary components per symbol. This recipe calculates **total implementation shortfall per order** — comparing the volume-weighted average execution price across all fills against the mid-price at the time the first fill arrived. This is the headline TCA metric: how much did the entire order cost relative to where the market was when you started executing? Problem[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#problem "Direct link to Problem") --------------------------------------------------------------------------------------------------------------------------- Orders in `fx_trades` are often split into multiple partial fills (rows sharing the same `order_id`). You want to compute a single cost metric per order that accounts for all fills, weighted by size, and benchmarked against the arrival price (the mid at the time of the first fill). Solution[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#solution "Direct link to Solution") ------------------------------------------------------------------------------------------------------------------------------ Use `ASOF JOIN` to capture the mid-price at each fill, then aggregate by `order_id` to get the volume-weighted average execution price and arrival mid: Total implementation shortfall per order[Demo this query](https://demo.questdb.io/?query=WITH%20fills_enriched%20AS%20(%0A%20%20%20%20SELECT%0A%20%20%20%20%20%20%20%20f.order_id%2C%0A%20%20%20%20%20%20%20%20f.symbol%2C%0A%20%20%20%20%20%20%20%20f.side%2C%0A%20%20%20%20%20%20%20%20f.price%2C%0A%20%20%20%20%20%20%20%20f.quantity%2C%0A%20%20%20%20%20%20%20%20f.timestamp%2C%0A%20%20%20%20%20%20%20%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202%20AS%20mid_at_fill%0A%20%20%20%20FROM%20fx_trades%20f%0A%20%20%20%20ASOF%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20WHERE%20f.timestamp%20IN%20%27%24yesterday%27%0A)%2C%0Aorder_summary%20AS%20(%0A%20%20%20%20SELECT%0A%20%20%20%20%20%20%20%20order_id%2C%0A%20%20%20%20%20%20%20%20symbol%2C%0A%20%20%20%20%20%20%20%20side%2C%0A%20%20%20%20%20%20%20%20first(mid_at_fill)%20AS%20arrival_mid%2C%0A%20%20%20%20%20%20%20%20sum(price%20*%20quantity)%20%2F%20sum(quantity)%20AS%20avg_exec_price%2C%0A%20%20%20%20%20%20%20%20sum(quantity)%20AS%20total_qty%2C%0A%20%20%20%20%20%20%20%20count()%20AS%20n_fills%2C%0A%20%20%20%20%20%20%20%20min(timestamp)%20AS%20first_fill_ts%2C%0A%20%20%20%20%20%20%20%20max(timestamp)%20AS%20last_fill_ts%0A%20%20%20%20FROM%20fills_enriched%0A%20%20%20%20GROUP%20BY%20order_id%2C%20symbol%2C%20side%0A)%0ASELECT%0A%20%20%20%20order_id%2C%0A%20%20%20%20symbol%2C%0A%20%20%20%20side%2C%0A%20%20%20%20n_fills%2C%0A%20%20%20%20total_qty%2C%0A%20%20%20%20CASE%20WHEN%20side%20%3D%20%27buy%27%20THEN%201%20ELSE%20-1%20END%0A%20%20%20%20%20%20%20%20*%20(avg_exec_price%20-%20arrival_mid)%0A%20%20%20%20%20%20%20%20%2F%20arrival_mid%20*%2010000%20AS%20total_is_bps%0AFROM%20order_summary%0AORDER%20BY%20total_is_bps%20DESC%3B&executeQuery=true) WITH fills_enriched AS ( SELECT f.order_id, f.symbol, f.side, f.price, f.quantity, f.timestamp, (m.best_bid + m.best_ask) / 2 AS mid_at_fill FROM fx_trades f ASOF JOIN market_data m ON (symbol) WHERE f.timestamp IN '$yesterday'),order_summary AS ( SELECT order_id, symbol, side, first(mid_at_fill) AS arrival_mid, sum(price * quantity) / sum(quantity) AS avg_exec_price, sum(quantity) AS total_qty, count() AS n_fills, min(timestamp) AS first_fill_ts, max(timestamp) AS last_fill_ts FROM fills_enriched GROUP BY order_id, symbol, side)SELECT order_id, symbol, side, n_fills, total_qty, CASE WHEN side = 'buy' THEN 1 ELSE -1 END * (avg_exec_price - arrival_mid) / arrival_mid * 10000 AS total_is_bpsFROM order_summaryORDER BY total_is_bps DESC; How it works[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#how-it-works "Direct link to How it works") ------------------------------------------------------------------------------------------------------------------------------------------ ### Step 1: Enrich fills with market state[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#step-1-enrich-fills-with-market-state "Direct link to Step 1: Enrich fills with market state") The `ASOF JOIN` pairs each fill with the most recent order book snapshot to compute the mid-price at execution time. ### Step 2: Aggregate to order level[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#step-2-aggregate-to-order-level "Direct link to Step 2: Aggregate to order level") The `order_summary` CTE groups fills by `order_id` and computes: * **`arrival_mid`** — `first(mid_at_fill)` gives the mid at the time of the earliest fill, which serves as the arrival price benchmark * **`avg_exec_price`** — volume-weighted average price across all fills: `sum(price * quantity) / sum(quantity)` * **`n_fills`** and **`total_qty`** — order size context ### Step 3: Compute IS[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#step-3-compute-is "Direct link to Step 3: Compute IS") The final SELECT calculates the shortfall in basis points: IS = direction * (avg_exec_price - arrival_mid) / arrival_mid * 10000 Where `direction` is +1 for buys, -1 for sells — so positive IS always means you paid more than the arrival benchmark. Results are ordered worst-first (`DESC`) so the most expensive orders appear at the top. Interpreting results[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#interpreting-results "Direct link to Interpreting results") ------------------------------------------------------------------------------------------------------------------------------------------------------------------ * **Near-zero IS**: The order executed close to the arrival price. Good execution for the order size. * **Positive IS (cost)**: The order executed worse than the arrival mid. For multi-fill orders, later fills may have walked the book or the market moved during execution. * **Negative IS (savings)**: The order beat the arrival benchmark. Can happen with patient limit orders or favorable market movement during execution. * **High `n_fills`**: Orders with many partial fills are more likely to show IS due to market movement between fills. Compare IS against `n_fills` and `last_fill_ts - first_fill_ts` to understand whether cost came from market impact or execution duration. Execution drift (delay cost)[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#execution-drift-delay-cost "Direct link to Execution drift (delay cost)") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Total IS tells you _how much_ an order cost, but not _when_ that cost accrued. Execution drift measures how much the mid-price moved against you between the first and last fill — isolating the cost of taking time to complete the order: Mid-price drift during order execution[Demo this query](https://demo.questdb.io/?query=WITH%20fills_enriched%20AS%20(%0A%20%20%20%20SELECT%0A%20%20%20%20%20%20%20%20f.order_id%2C%0A%20%20%20%20%20%20%20%20f.symbol%2C%0A%20%20%20%20%20%20%20%20f.side%2C%0A%20%20%20%20%20%20%20%20f.price%2C%0A%20%20%20%20%20%20%20%20f.quantity%2C%0A%20%20%20%20%20%20%20%20f.timestamp%2C%0A%20%20%20%20%20%20%20%20(m.best_bid%20%2B%20m.best_ask)%20%2F%202%20AS%20mid_at_fill%0A%20%20%20%20FROM%20fx_trades%20f%0A%20%20%20%20ASOF%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20WHERE%20f.timestamp%20IN%20%27%24yesterday%27%0A)%2C%0Aorder_bounds%20AS%20(%0A%20%20%20%20SELECT%0A%20%20%20%20%20%20%20%20order_id%2C%0A%20%20%20%20%20%20%20%20symbol%2C%0A%20%20%20%20%20%20%20%20side%2C%0A%20%20%20%20%20%20%20%20first(mid_at_fill)%20AS%20arrival_mid%2C%0A%20%20%20%20%20%20%20%20last(mid_at_fill)%20AS%20mid_at_last_fill%2C%0A%20%20%20%20%20%20%20%20min(timestamp)%20AS%20first_fill_ts%2C%0A%20%20%20%20%20%20%20%20max(timestamp)%20AS%20last_fill_ts%0A%20%20%20%20FROM%20fills_enriched%0A%20%20%20%20GROUP%20BY%20order_id%2C%20symbol%2C%20side%0A)%0ASELECT%0A%20%20%20%20order_id%2C%0A%20%20%20%20symbol%2C%0A%20%20%20%20side%2C%0A%20%20%20%20CASE%20WHEN%20side%20%3D%20%27buy%27%20THEN%201%20ELSE%20-1%20END%0A%20%20%20%20%20%20%20%20*%20(mid_at_last_fill%20-%20arrival_mid)%0A%20%20%20%20%20%20%20%20%2F%20arrival_mid%20*%2010000%20AS%20execution_drift_bps%2C%0A%20%20%20%20last_fill_ts%20-%20first_fill_ts%20AS%20execution_duration%0AFROM%20order_bounds%0AORDER%20BY%20execution_drift_bps%20DESC%3B&executeQuery=true) WITH fills_enriched AS ( SELECT f.order_id, f.symbol, f.side, f.price, f.quantity, f.timestamp, (m.best_bid + m.best_ask) / 2 AS mid_at_fill FROM fx_trades f ASOF JOIN market_data m ON (symbol) WHERE f.timestamp IN '$yesterday'),order_bounds AS ( SELECT order_id, symbol, side, first(mid_at_fill) AS arrival_mid, last(mid_at_fill) AS mid_at_last_fill, min(timestamp) AS first_fill_ts, max(timestamp) AS last_fill_ts FROM fills_enriched GROUP BY order_id, symbol, side)SELECT order_id, symbol, side, CASE WHEN side = 'buy' THEN 1 ELSE -1 END * (mid_at_last_fill - arrival_mid) / arrival_mid * 10000 AS execution_drift_bps, last_fill_ts - first_fill_ts AS execution_durationFROM order_boundsORDER BY execution_drift_bps DESC; `execution_drift_bps` measures how much the mid moved against you from first fill to last fill. `execution_duration` shows how long the order took to complete. Arrival price vs first fill In this dataset, the arrival price and first fill are effectively the same moment. In a real trading system, the arrival price would be the mid at decision time (before the order was sent), and **delay cost** would be the drift from decision to first fill. With `fx_trades`, the best available proxy is drift during execution — from first fill to last fill. High drift on long-duration orders suggests the market is moving against you while you execute. This can indicate that order sizes are too large for the available liquidity, or that execution is too slow. Compare with total IS — if drift accounts for most of the IS, faster execution would reduce costs. Spread cost per order[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#spread-cost-per-order "Direct link to Spread cost per order") --------------------------------------------------------------------------------------------------------------------------------------------------------------------- Isolate the spread component of execution cost — the quantity-weighted half-spread paid across all fills in an order: Spread cost per order[Demo this query](https://demo.questdb.io/?query=WITH%20fills_enriched%20AS%20(%0A%20%20%20%20SELECT%0A%20%20%20%20%20%20%20%20f.order_id%2C%0A%20%20%20%20%20%20%20%20f.symbol%2C%0A%20%20%20%20%20%20%20%20f.side%2C%0A%20%20%20%20%20%20%20%20f.price%2C%0A%20%20%20%20%20%20%20%20f.quantity%2C%0A%20%20%20%20%20%20%20%20m.best_ask%20-%20m.best_bid%20AS%20spread_at_fill%0A%20%20%20%20FROM%20fx_trades%20f%0A%20%20%20%20ASOF%20JOIN%20market_data%20m%20ON%20(symbol)%0A%20%20%20%20WHERE%20f.timestamp%20IN%20%27%24yesterday%27%0A)%0ASELECT%0A%20%20%20%20order_id%2C%0A%20%20%20%20symbol%2C%0A%20%20%20%20sum(0.5%20*%20spread_at_fill%20*%20quantity)%0A%20%20%20%20%20%20%20%20%2F%20sum(quantity)%20AS%20avg_halfspread%2C%0A%20%20%20%20sum(0.5%20*%20spread_at_fill%20%2F%20price%20*%2010000%20*%20quantity)%0A%20%20%20%20%20%20%20%20%2F%20sum(quantity)%20AS%20spread_cost_bps%2C%0A%20%20%20%20sum(quantity)%20AS%20total_qty%0AFROM%20fills_enriched%0AGROUP%20BY%20order_id%2C%20symbol%0AORDER%20BY%20spread_cost_bps%20DESC%3B&executeQuery=true) WITH fills_enriched AS ( SELECT f.order_id, f.symbol, f.side, f.price, f.quantity, m.best_ask - m.best_bid AS spread_at_fill FROM fx_trades f ASOF JOIN market_data m ON (symbol) WHERE f.timestamp IN '$yesterday')SELECT order_id, symbol, sum(0.5 * spread_at_fill * quantity) / sum(quantity) AS avg_halfspread, sum(0.5 * spread_at_fill / price * 10000 * quantity) / sum(quantity) AS spread_cost_bps, sum(quantity) AS total_qtyFROM fills_enrichedGROUP BY order_id, symbolORDER BY spread_cost_bps DESC; Two spread metrics per order: * **`avg_halfspread`** — quantity-weighted average half-spread in price terms. This is the baseline cost of crossing the spread, weighted by how much volume went through at each spread level. * **`spread_cost_bps`** — the same in basis points, normalized by fill price. Compare `spread_cost_bps` against total IS to understand how much of the execution cost was simply the spread vs. market impact. If spread cost accounts for most of the IS, execution quality is reasonable — you're paying the market price for immediacy. If total IS significantly exceeds spread cost, the excess is market impact or adverse drift. Permanent vs temporary impact per order[​](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#permanent-vs-temporary-impact-per-order "Direct link to Permanent vs temporary impact per order") --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Decompose each order's total IS into permanent impact (information content) and temporary impact (transient dislocation that reverts). This uses `HORIZON JOIN` to capture the mid at fill time and 30 minutes later, then `PIVOT` to reshape into columns: Order-level IS decomposition into permanent and temporary impact[Demo this query](https://demo.questdb.io/?query=WITH%20order_markouts%20AS%20(%0A%20%20%20%20SELECT%0A%20%20%20%20%20%20%20%20f.order_id%2C%0A%20%20%20%20%20%20%20%20f.symbol%2C%0A%20%20%20%20%20%20%20%20f.side%2C%0A%20%20%20%20%20%20%20%20h.offset%2C%0A%20%20%20%20%20%20%20%20sum((m.best_bid%20%2B%20m.best_ask)%20%2F%202%20*%20f.quantity)%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%20sum(f.quantity)%20AS%20weighted_mid%2C%0A%20%20%20%20%20%20%20%20sum(f.price%20*%20f.quantity)%20%2F%20sum(f.quantity)%20AS%20avg_exec_price%2C%0A%20%20%20%20%20%20%20%20sum(f.quantity)%20AS%20total_qty%0A%20%20%20%20FROM%20fx_trades%20f%0A%20%20%20%20HORIZON%20JOIN%20market_data%20m%20ON%20(f.symbol%20%3D%20m.symbol)%0A%20%20%20%20%20%20%20%20LIST%20(0s%2C%2030m)%20AS%20h%0A%20%20%20%20WHERE%20f.timestamp%20IN%20%27%24yesterday%27%0A)%2C%0Apivoted%20AS%20(%0A%20%20%20%20SELECT%20*%20FROM%20order_markouts%0A%20%20%20%20PIVOT%20(%0A%20%20%20%20%20%20%20%20first(weighted_mid)%20AS%20mid%0A%20%20%20%20%20%20%20%20FOR%20offset%20IN%20(%0A%20%20%20%20%20%20%20%20%20%20%20%200%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20AS%20at_fill%2C%0A%20%20%20%20%20%20%20%20%20%20%20%201800000000000%20%20%20AS%20at_30m%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20GROUP%20BY%20order_id%2C%20symbol%2C%20side%2C%20avg_exec_price%2C%20total_qty%0A%20%20%20%20)%0A)%0ASELECT%0A%20%20%20%20order_id%2C%0A%20%20%20%20symbol%2C%0A%20%20%20%20side%2C%0A%20%20%20%20total_qty%2C%0A%20%20%20%20CASE%20WHEN%20side%20%3D%20%27buy%27%20THEN%201%20ELSE%20-1%20END%0A%20%20%20%20%20%20%20%20*%20(avg_exec_price%20-%20at_fill_mid)%0A%20%20%20%20%20%20%20%20%2F%20at_fill_mid%20*%2010000%20AS%20total_is_bps%2C%0A%20%20%20%20CASE%20WHEN%20side%20%3D%20%27buy%27%20THEN%201%20ELSE%20-1%20END%0A%20%20%20%20%20%20%20%20*%20(at_30m_mid%20-%20at_fill_mid)%0A%20%20%20%20%20%20%20%20%2F%20at_fill_mid%20*%2010000%20AS%20permanent_bps%2C%0A%20%20%20%20CASE%20WHEN%20side%20%3D%20%27buy%27%20THEN%201%20ELSE%20-1%20END%0A%20%20%20%20%20%20%20%20*%20(avg_exec_price%20-%20at_30m_mid)%0A%20%20%20%20%20%20%20%20%2F%20at_fill_mid%20*%2010000%20AS%20temporary_bps%0AFROM%20pivoted%0AORDER%20BY%20total_is_bps%20DESC%3B&executeQuery=true) WITH order_markouts AS ( SELECT f.order_id, f.symbol, f.side, h.offset, sum((m.best_bid + m.best_ask) / 2 * f.quantity) / sum(f.quantity) AS weighted_mid, sum(f.price * f.quantity) / sum(f.quantity) AS avg_exec_price, sum(f.quantity) AS total_qty FROM fx_trades f HORIZON JOIN market_data m ON (f.symbol = m.symbol) LIST (0s, 30m) AS h WHERE f.timestamp IN '$yesterday'),pivoted AS ( SELECT * FROM order_markouts PIVOT ( first(weighted_mid) AS mid FOR offset IN ( 0 AS at_fill, 1800000000000 AS at_30m ) GROUP BY order_id, symbol, side, avg_exec_price, total_qty ))SELECT order_id, symbol, side, total_qty, CASE WHEN side = 'buy' THEN 1 ELSE -1 END * (avg_exec_price - at_fill_mid) / at_fill_mid * 10000 AS total_is_bps, CASE WHEN side = 'buy' THEN 1 ELSE -1 END * (at_30m_mid - at_fill_mid) / at_fill_mid * 10000 AS permanent_bps, CASE WHEN side = 'buy' THEN 1 ELSE -1 END * (avg_exec_price - at_30m_mid) / at_fill_mid * 10000 AS temporary_bpsFROM pivotedORDER BY total_is_bps DESC; The first CTE does the heavy lifting — it computes the quantity-weighted mid and quantity-weighted average execution price per order _at each horizon offset_, so the aggregation happens before the PIVOT. The PIVOT then simply reshapes the two offsets (0s and 30m) into columns. This gives you three metrics per order: * **`total_is_bps`** — same as the headline IS above, for reference * **`permanent_bps`** — how much the mid moved permanently (arrival mid vs mid 30 minutes after execution). High permanent impact suggests your order carried information or was perceived as informed. * **`temporary_bps`** — how much of the cost reverted (fill price vs post-execution mid). High temporary impact means you moved the market but it bounced back — you paid for liquidity consumption, not information. The identity holds: **total IS = permanent + temporary**. An order with mostly permanent impact is genuinely moving the market. An order with mostly temporary impact is just paying for immediacy. Related documentation * [ASOF JOIN](https://questdb.com/docs/query/sql/asof-join/) * [HORIZON JOIN](https://questdb.com/docs/query/sql/horizon-join/) * [PIVOT](https://questdb.com/docs/query/sql/pivot/) * [GROUP BY](https://questdb.com/docs/query/sql/group-by/) * [Implementation shortfall decomposition recipe](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall/) * [Slippage per fill recipe](https://questdb.com/docs/cookbook/sql/finance/slippage/) * [Problem](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#problem) * [Solution](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#solution) * [How it works](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#how-it-works) * [Step 1: Enrich fills with market state](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#step-1-enrich-fills-with-market-state) * [Step 2: Aggregate to order level](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#step-2-aggregate-to-order-level) * [Step 3: Compute IS](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#step-3-compute-is) * [Interpreting results](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#interpreting-results) * [Execution drift (delay cost)](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#execution-drift-delay-cost) * [Spread cost per order](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#spread-cost-per-order) * [Permanent vs temporary impact per order](https://questdb.com/docs/cookbook/sql/finance/implementation-shortfall-order/#permanent-vs-temporary-impact-per-order) --- # Import CSV Using Web Console | QuestDB On this page The **Import CSV** functionality in the Web Console provides a user-friendly interface to upload and import CSV files into QuestDB. You can create new tables or append data to existing tables with automatic schema detection and flexible configuration options. ![Import CSV interface in the Web Console](https://questdb.com/docs/images/docs/console/import-csv.webp) Accessing the Import Interface[​](https://questdb.com/docs/getting-started/web-console/import-csv/#accessing-the-import-interface "Direct link to Accessing the Import Interface") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- You can access the import tab by clicking the import icon in the left-side navigation menu of the Web Console. ![Screenshot of the Web Console showing the location of the Import tab](https://questdb.com/docs/images/docs/console/import-button.webp) Import Process[​](https://questdb.com/docs/getting-started/web-console/import-csv/#import-process "Direct link to Import Process") ----------------------------------------------------------------------------------------------------------------------------------- ### Upload Queue[​](https://questdb.com/docs/getting-started/web-console/import-csv/#upload-queue "Direct link to Upload Queue") Once a file is added to the upload queue, the following configurations will be displayed: ![Screenshot of the Web Console showing the file ready to be uploaded](https://questdb.com/docs/images/docs/console/ready-to-upload.webp) ### Configuration Options[​](https://questdb.com/docs/getting-started/web-console/import-csv/#configuration-options "Direct link to Configuration Options") * **File**: The file name, size, and import status * **Table name**: The name of the table to be created or updated. By default, this is the name of the imported file * **Schema**: The column name and data type. The schema is automatically detected but can be set manually * **Write mode**: * `Append`: Uploaded data will be appended to the end of the table * `Overwrite`: Uploaded data will override existing data in the table * **Actions**: * `Settings`: Additional configuration for the import * `Upload`: Start the upload * `X`: Delete the file from the upload queue Table Schema Configuration[​](https://questdb.com/docs/getting-started/web-console/import-csv/#table-schema-configuration "Direct link to Table Schema Configuration") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- ### For Existing Tables[​](https://questdb.com/docs/getting-started/web-console/import-csv/#for-existing-tables "Direct link to For Existing Tables") To update the schema of an existing table, select `Overwrite` write mode to replace the existing rows and partition unit with data from the CSV file. For an existing table, changing the table name allows you to import the data as a new separate table. ### For New Tables[​](https://questdb.com/docs/getting-started/web-console/import-csv/#for-new-tables "Direct link to For New Tables") The following settings are available for configuration: | Setting | Description | | --- | --- | | Partition | Change the partition setting of the table | | Designated timestamp | Selecting a designated timestamp. This is mandatory if the partition unit is not `NONE` | | Data type | Define the data type. For timestamp, the timestamp format is mandatory and there is the option to select the column as the designated timestamp | | Delete column | Click `x` to delete the column from the table | | Add column | At the end of the column list, select "Add column" to insert a new column into the table | The following table schema details are imported based on the CSV file: * The column order * The column name Import Settings[​](https://questdb.com/docs/getting-started/web-console/import-csv/#import-settings "Direct link to Import Settings") -------------------------------------------------------------------------------------------------------------------------------------- The Settings panel displays the following configurations: | Setting | Description | Default value | | --- | --- | --- | | Maximum number of uncommitted rows | The size of the commit batch. A commit will be issued when this number is reached in the buffer. This setting is the same as `cairo.max.uncommitted.rows`. To avoid running out of memory during an import, set this value based on the RAM size of the machine | 500000 | | Delimiter | The delimiter character to parse the CSV file | Automatic | | Atomicity | Error behavior. Rejected rows or columns will be reported in the Details panel after the import is completed | Skip column | | Force header | Whether to interpret the first line as the header. The result will be reported in the Details panel after the import is completed | FALSE | | Skip line extra values | Whether the parser should ignore extra values by skipping the entire line. An extra value is something in addition to what is defined by the header | FALSE | Import Results and Details[​](https://questdb.com/docs/getting-started/web-console/import-csv/#import-results-and-details "Direct link to Import Results and Details") ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- ### Status Display[​](https://questdb.com/docs/getting-started/web-console/import-csv/#status-display "Direct link to Status Display") The import status is displayed in the file column. Once the action is completed, the number of rows inserted is displayed alongside the `Details` tab: ![Screenshot of the Web Console showing number of row imported and the Details tab](https://questdb.com/docs/images/docs/console/import-complete.webp) ### Details Panel[​](https://questdb.com/docs/getting-started/web-console/import-csv/#details-panel "Direct link to Details Panel") The `Details` panel lists rejected rows and import errors for each column: ![Screenshot of the Web Console showing the import details](https://questdb.com/docs/images/docs/console/import-details.webp) The details such as forced header, table name, and rejected rows are related to the import settings you defined. For example, setting Atomicity in Settings to "Skip row" will result in skipped rows being reported under Rejected rows after the import. * [Accessing the Import Interface](https://questdb.com/docs/getting-started/web-console/import-csv/#accessing-the-import-interface) * [Import Process](https://questdb.com/docs/getting-started/web-console/import-csv/#import-process) * [Upload Queue](https://questdb.com/docs/getting-started/web-console/import-csv/#upload-queue) * [Configuration Options](https://questdb.com/docs/getting-started/web-console/import-csv/#configuration-options) * [Table Schema Configuration](https://questdb.com/docs/getting-started/web-console/import-csv/#table-schema-configuration) * [For Existing Tables](https://questdb.com/docs/getting-started/web-console/import-csv/#for-existing-tables) * [For New Tables](https://questdb.com/docs/getting-started/web-console/import-csv/#for-new-tables) * [Import Settings](https://questdb.com/docs/getting-started/web-console/import-csv/#import-settings) * [Import Results and Details](https://questdb.com/docs/getting-started/web-console/import-csv/#import-results-and-details) * [Status Display](https://questdb.com/docs/getting-started/web-console/import-csv/#status-display) * [Details Panel](https://questdb.com/docs/getting-started/web-console/import-csv/#details-panel) --- # Databento | QuestDB On this page [Databento](https://questdb.com/docs/integrations/other/databento/) is a market data aggregator that provides a single, normalized feed covering multiple venues, simplifying the process of ingesting live market data. It interfaces well with QuestDB for real-time data analysis and visualization in Grafana. This guide will show how to ingest live market data from [Databento](https://questdb.com/docs/integrations/other/databento/) into QuestDB and visualize it using Grafana. For a deeper dive, see our [Databento & QuestDB blog](https://questdb.com/blog/ingesting-live-market-data-data-bento/) . Prerequisites[​](https://questdb.com/docs/integrations/other/databento/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------- * [QuestDB](https://questdb.com/download/) * [Databento Python client](https://pypi.org/project/databento/) * [QuestDB Python client](https://questdb.com/docs/ingestion/clients/python/) * [Grafana](https://questdb.com/docs/integrations/visualization/grafana/) (Optional) Install the required Python libraries: pip3 install questdbpip3 install databento Ingest Data from Databento into QuestDB[​](https://questdb.com/docs/integrations/other/databento/#ingest-data-from-databento-into-questdb "Direct link to Ingest Data from Databento into QuestDB") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ### Create Databento Client[​](https://questdb.com/docs/integrations/other/databento/#create-databento-client "Direct link to Create Databento Client") Set up a Databento client with your API key: import databento as dbdb_client = db.Live(key="YOUR_API_KEY") ### Subscribe to Market Data[​](https://questdb.com/docs/integrations/other/databento/#subscribe-to-market-data "Direct link to Subscribe to Market Data") Subscribe to a data feed, such as the CME S&P 500 E-Mini futures: db_client.subscribe(dataset="GLBX.MDP3",schema="mbp-1",stype_in="raw_symbol",symbols="ESM4") ### Ingest Data into QuestDB[​](https://questdb.com/docs/integrations/other/databento/#ingest-data-into-questdb "Direct link to Ingest Data into QuestDB") Ingest the data into QuestDB using the Sender class: from questdb.ingress import Senderimport numpy as npquestdb_conf = "http::addr=localhost:9000;username=admin;password=quest;"with Sender.from_conf(questdb_conf) as sender:sender.row('top_of_book',symbols={'instrument': 'ESM4'},columns={'bid_size': record.levels[0].bid_sz,'bid': record.levels[0].bid_px*0.000000001,'ask': record.levels[0].ask_px*0.000000001,'ask_size': record.levels[0].ask_sz},at=np.datetime64(record.ts_event, 'ns').astype('datetime64[ms]').astype(object))sender.flush() Query QuestDB[​](https://questdb.com/docs/integrations/other/databento/#query-questdb "Direct link to Query QuestDB") ---------------------------------------------------------------------------------------------------------------------- Now that data is flowing, you can visit QuestDB at `http://localhost:9000` to try some queries. Read our [SQL Overview](https://questdb.com/docs/query/overview/) to learn more about the power and depth of querying. Visualize in Grafana[​](https://questdb.com/docs/integrations/other/databento/#visualize-in-grafana "Direct link to Visualize in Grafana") ------------------------------------------------------------------------------------------------------------------------------------------- After ingesting the data, you can visualize it in Grafana by creating a dashboard with SQL queries such as: SELECT timestamp, instrument, bid, askFROM top_of_bookWHERE $\_\_timeFilter(timestamp) AND instrument = $symbol For more detailed analysis, create multiple charts using Grafana's variable and repeat options. To learn the basics of QuestDB and Grafana, see [our blog](https://questdb.com/blog/time-series-monitoring-dashboard-grafana-questdb/) . You can substitute the demonstration queries with your own! Summary[​](https://questdb.com/docs/integrations/other/databento/#summary "Direct link to Summary") ---------------------------------------------------------------------------------------------------- In this guide, we set up a pipeline to ingest live market data from Databento into QuestDB and optionally created a visualization using Grafana. This setup allows you to build powerful dashboards and analyze market data efficiently. For more information, check out [Databento’s documentation](https://databento.com/docs/) . * [Prerequisites](https://questdb.com/docs/integrations/other/databento/#prerequisites) * [Ingest Data from Databento into QuestDB](https://questdb.com/docs/integrations/other/databento/#ingest-data-from-databento-into-questdb) * [Create Databento Client](https://questdb.com/docs/integrations/other/databento/#create-databento-client) * [Subscribe to Market Data](https://questdb.com/docs/integrations/other/databento/#subscribe-to-market-data) * [Ingest Data into QuestDB](https://questdb.com/docs/integrations/other/databento/#ingest-data-into-questdb) * [Query QuestDB](https://questdb.com/docs/integrations/other/databento/#query-questdb) * [Visualize in Grafana](https://questdb.com/docs/integrations/other/databento/#visualize-in-grafana) * [Summary](https://questdb.com/docs/integrations/other/databento/#summary) --- # Web Console Overview | QuestDB On this page Web Console is a client that allows you to interact with QuestDB. It provides UI tools to query and explore the data, visualize the results in a table or plot. ![Screenshot of the Web Console](https://questdb.com/docs/images/docs/console/overview.webp) ### Accessing the Web Console[​](https://questdb.com/docs/getting-started/web-console/overview/#accessing-the-web-console "Direct link to Accessing the Web Console") Web Console will be available at `http://[server-address]:9000`. When running locally, this will be `http://localhost:9000`. ### Layout[​](https://questdb.com/docs/getting-started/web-console/overview/#layout "Direct link to Layout") ![Preview of the different sections in the Web Console](https://questdb.com/docs/images/docs/console/layout.webp) The Web Console is organized into the following main sections that work together to provide a complete workflow: ### Code Editor[​](https://questdb.com/docs/getting-started/web-console/overview/#code-editor "Direct link to Code Editor") The **Code Editor** is where you write and execute SQL queries with features like syntax highlighting, auto-completion, and error tracing. It supports executing queries by selection, multiple query execution, and query planning. [Learn more about Code Editor →](https://questdb.com/docs/getting-started/web-console/code-editor/) ### AI Assistant[​](https://questdb.com/docs/getting-started/web-console/overview/#ai-assistant "Direct link to AI Assistant") The **AI Assistant** provides intelligent query assistance directly in the Web Console using AI-powered explanations and suggestions. It helps you write, understand, and fix SQL queries while maintaining complete control over your data and API keys through a Bring Your Own Key (BYOK) model. [Learn more about AI Assistant →](https://questdb.com/docs/getting-started/web-console/questdb-ai/) ### Metrics View[​](https://questdb.com/docs/getting-started/web-console/overview/#metrics-view "Direct link to Metrics View") The **Metrics View** provides real-time monitoring and telemetry capabilities for your QuestDB instance. It displays interactive charts and widgets to track database performance, WAL operations, and table-specific metrics. [Learn more about Metrics View →](https://questdb.com/docs/getting-started/web-console/metrics-view/) ### Schema Explorer[​](https://questdb.com/docs/getting-started/web-console/overview/#schema-explorer "Direct link to Schema Explorer") The **Schema Explorer** is the navigation hub for exploring tables and materialized views. It provides detailed information about each database object including columns with data types, storage configuration (partitioning and WAL status), and for materialized views, their base tables. [Learn more about Schema Explorer →](https://questdb.com/docs/getting-started/web-console/schema-explorer/) ### Result Grid[​](https://questdb.com/docs/getting-started/web-console/overview/#result-grid "Direct link to Result Grid") The **Result Grid** displays your query results in an interactive table format with features for data navigation, export, and visualization. [Learn more about Result Grid →](https://questdb.com/docs/getting-started/web-console/result-grid/) ### Query Log[​](https://questdb.com/docs/getting-started/web-console/overview/#query-log "Direct link to Query Log") The **Query Log** monitors query execution status and performance metrics, providing real-time feedback and maintaining a history of recent operations. It shows execution times, row counts, and detailed error information to help optimize your queries. [Learn more about Query Log →](https://questdb.com/docs/getting-started/web-console/query-log/) ### Import CSV[​](https://questdb.com/docs/getting-started/web-console/overview/#import-csv "Direct link to Import CSV") The **Import CSV** interface allows you to upload and import CSV files into QuestDB with automatic schema detection, flexible configuration options, and detailed progress tracking. You can create new tables or append to existing ones with full control over the import process. [Learn more about Import CSV →](https://questdb.com/docs/getting-started/web-console/import-csv/) ### Right Sidebar[​](https://questdb.com/docs/getting-started/web-console/overview/#right-sidebar "Direct link to Right Sidebar") The **Right Sidebar** provides quick access to essential tools and information: * **Help**: Access quick links and contact options through a convenient help menu * **QuestDB News**: Stay up-to-date with the latest QuestDB announcements and updates * **Create Table**: Build new tables visually using an intuitive interface. Define table structure, configure partitioning, enable WAL, and add columns with their data types—all without writing SQL code. [Learn more about Create Table →](https://questdb.com/docs/getting-started/web-console/create-table/) ### Instance Naming[​](https://questdb.com/docs/getting-started/web-console/overview/#instance-naming "Direct link to Instance Naming") Web Console allows you to set the instance name, type, and color. This functionality is particularly useful for production users who manage multiple deployments and frequently navigate between them. This feature makes it easier to keep track of instance information and label instances with meaningful names for their users. The instance name, instance type, and description are displayed when hovering over the icon in the instance information badge. Instance information can be modified through the dialog that opens when clicking the edit icon: ![Instance information edit popper in Web Console](https://questdb.com/docs/images/docs/console/instance-naming.webp) info If `http.settings.readonly` configuration is set to true, instance information is not editable. info When using QuestDB Enterprise with Role-Based Access Control (RBAC), only the users with `SETTINGS` or `DATABASE ADMIN` permission can edit the instance information. See [Database Permissions](https://questdb.com/docs/security/rbac/#database-permissions) for more details. * [Accessing the Web Console](https://questdb.com/docs/getting-started/web-console/overview/#accessing-the-web-console) * [Layout](https://questdb.com/docs/getting-started/web-console/overview/#layout) * [Code Editor](https://questdb.com/docs/getting-started/web-console/overview/#code-editor) * [AI Assistant](https://questdb.com/docs/getting-started/web-console/overview/#ai-assistant) * [Metrics View](https://questdb.com/docs/getting-started/web-console/overview/#metrics-view) * [Schema Explorer](https://questdb.com/docs/getting-started/web-console/overview/#schema-explorer) * [Result Grid](https://questdb.com/docs/getting-started/web-console/overview/#result-grid) * [Query Log](https://questdb.com/docs/getting-started/web-console/overview/#query-log) * [Import CSV](https://questdb.com/docs/getting-started/web-console/overview/#import-csv) * [Right Sidebar](https://questdb.com/docs/getting-started/web-console/overview/#right-sidebar) * [Instance Naming](https://questdb.com/docs/getting-started/web-console/overview/#instance-naming) --- # Query Log | QuestDB On this page The **Query Log** displays execution status, performance metrics, and detailed information about your query operations in the bottom panel of the Web Console. It provides real-time feedback on query execution and maintains a history of operations for each tab. ![Query Log in the Web Console](https://questdb.com/docs/images/docs/console/query-log-expanded.webp) Expansion and Collapse[​](https://questdb.com/docs/getting-started/web-console/query-log/#expansion-and-collapse "Direct link to Expansion and Collapse") ---------------------------------------------------------------------------------------------------------------------------------------------------------- The Query Log can be toggled between two display modes: ### Collapsed Mode (Default)[​](https://questdb.com/docs/getting-started/web-console/query-log/#collapsed-mode-default "Direct link to Collapsed Mode (Default)") * Shows the status for the [query in cursor](https://questdb.com/docs/getting-started/web-console/query-log/#active-item-and-cursor-position) * Displays as a compact single-line summary ### Expanded Mode[​](https://questdb.com/docs/getting-started/web-console/query-log/#expanded-mode "Direct link to Expanded Mode") * Shows the complete history of executed queries for the current tab * Displays detailed execution information for each query * Provides access to the "Clear query log" button Click the button in the top-right corner to switch between modes. Active Item and Cursor Position[​](https://questdb.com/docs/getting-started/web-console/query-log/#active-item-and-cursor-position "Direct link to Active Item and Cursor Position") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The Query Log is dynamically connected to your editor cursor position: * **Active Query**: The highlighted query in the log changes based on where your cursor is positioned in the editor * **Error Markers**: Error indicators in the editor are synchronized with the query log entries * **Status Updates**: Moving your cursor between different queries automatically updates the active notification This integration ensures that the Query Log always shows relevant information for the query you're currently working on, making debugging and performance analysis more efficient. Execution Details[​](https://questdb.com/docs/getting-started/web-console/query-log/#execution-details "Direct link to Execution Details") ------------------------------------------------------------------------------------------------------------------------------------------- The Query Log provides comprehensive performance metrics for each executed query: * **Row Count**: Number of rows returned by SELECT queries * **Execution Time**: Time spent by QuestDB processing your query * **Network Time**: Time spent transferring data between client and server * **Total Time**: Complete end-to-end time from query submission to result display **Example**: `9,735,994 rows in 304ms Execute: 73.66ms Network: 230.34ms Total: 304ms` Additional timing details include: * **Count**: Time spent counting rows * **Authentication**: Time spent on authentication * **Compile**: Time spent compiling the query Copy Query Text[​](https://questdb.com/docs/getting-started/web-console/query-log/#copy-query-text "Direct link to Copy Query Text") ------------------------------------------------------------------------------------------------------------------------------------- Each query log entry includes a copy button that allows you to copy the executed SQL query text to your clipboard. Clear Query Log[​](https://questdb.com/docs/getting-started/web-console/query-log/#clear-query-log "Direct link to Clear Query Log") ------------------------------------------------------------------------------------------------------------------------------------- The "Clear query log" button removes all query execution history for the current tab. This action: * Removes all notifications and execution history * Clears error markers from the editor * Operates **per tab** - each tab maintains its own independent query log * [Expansion and Collapse](https://questdb.com/docs/getting-started/web-console/query-log/#expansion-and-collapse) * [Collapsed Mode (Default)](https://questdb.com/docs/getting-started/web-console/query-log/#collapsed-mode-default) * [Expanded Mode](https://questdb.com/docs/getting-started/web-console/query-log/#expanded-mode) * [Active Item and Cursor Position](https://questdb.com/docs/getting-started/web-console/query-log/#active-item-and-cursor-position) * [Execution Details](https://questdb.com/docs/getting-started/web-console/query-log/#execution-details) * [Copy Query Text](https://questdb.com/docs/getting-started/web-console/query-log/#copy-query-text) * [Clear Query Log](https://questdb.com/docs/getting-started/web-console/query-log/#clear-query-log) --- # Schema Explorer | QuestDB On this page The **Schema Explorer** is the navigation panel on the left side of the Web Console that helps you browse and understand your database structure. It provides a hierarchical view of all tables and materialized views with detailed information about their columns, data types, storage configuration, and relationships. You can toggle the Schema Explorer by using the database icon on the left. ![Schema Explorer in the Web Console](https://questdb.com/docs/images/docs/console/schema-explorer.webp) Tree View[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#tree-view "Direct link to Tree View") ------------------------------------------------------------------------------------------------------------------------- The Schema Explorer displays database objects in an expandable tree structure. When you expand a table or materialized view, the following information is available: ### Folders[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#folders "Direct link to Folders") #### Columns[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#columns "Direct link to Columns") All table columns are displayed with their names and data types, each represented by type-specific icons: * **Designated Timestamp**: The designated timestamp column is highlighted with a distinctive green-colored icon * **Symbol Columns**: Distinguished by tag icons, these can be further expanded to reveal: * **Indexed**: Indicates whether the symbol column has an index for faster filtering * **Symbol Capacity**: The maximum number of distinct symbols that can be stored (e.g., 256) * **Cached**: Shows whether symbol values are cached in memory for improved performance #### Storage Details[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#storage-details "Direct link to Storage Details") * **Partitioning**: Displays the table's partitioning approach (e.g., "By day", "By week", "None") * **WAL**: Indicates whether Write-Ahead Log is enabled or disabled for the table tip Table and materialized view icons visually indicate key storage details such as partitioning and WAL status. Hover over these icons to see detailed information including partitioning strategy, ordering configuration, and WAL status, allowing you to quickly assess critical storage details without expanding the full table structure. #### Base Tables[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#base-tables "Direct link to Base Tables") For materialized views, shows the underlying source tables ### Context Menu[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#context-menu "Direct link to Context Menu") Right-clicking on any table or materialized view opens a context menu with the following actions: ![Table context menu for quick actions](https://questdb.com/docs/images/docs/console/table-context-menu.webp) * **Copy schema**: Copies the schema of the table to the clipboard * **Resume WAL**: If WAL is suspended for a table, a warning icon is shown to the right of the table name. You can resume WAL from a specific transaction number by clicking on the context menu item. info When a materialized view is invalid, a warning icon is shown to the right of the materialized view name. You can see the invalidation reason by hovering over the icon. ### Keyboard Navigation[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#keyboard-navigation "Direct link to Keyboard Navigation") You can navigate in the tree view using arrow keys, Home, End, Page Up, and Page Down. Toolbar[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#toolbar "Direct link to Toolbar") ------------------------------------------------------------------------------------------------------------------- The toolbar provides essential actions for filtering, managing, and interacting with your database objects. ![Schema Explorer Toolbar](https://questdb.com/docs/images/docs/console/schema-toolbar.webp) ### Filter[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#filter "Direct link to Filter") Type to filter tables and materialized views by name. ### Suspended Tables[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#suspended-tables "Direct link to Suspended Tables") When tables have suspended WAL operations, an error icon with a count of suspended tables appears. Click to filter and show only suspended tables. ### Table Management Actions[​](https://questdb.com/docs/getting-started/web-console/schema-explorer/#table-management-actions "Direct link to Table Management Actions") * **Add Metrics**: Chart icon button to add metrics for monitoring database performance. See [Metrics View](https://questdb.com/docs/getting-started/web-console/metrics-view/) for details. * **Select Mode**: Checkbox circle icon to enter table selection mode for copying multiple schemas to the clipboard. * **Auto Refresh**: Refresh icon to toggle automatic updates of the schema explorer when database structure changes. Disabling auto refresh is recommended only for development purposes. * [Tree View](https://questdb.com/docs/getting-started/web-console/schema-explorer/#tree-view) * [Folders](https://questdb.com/docs/getting-started/web-console/schema-explorer/#folders) * [Context Menu](https://questdb.com/docs/getting-started/web-console/schema-explorer/#context-menu) * [Keyboard Navigation](https://questdb.com/docs/getting-started/web-console/schema-explorer/#keyboard-navigation) * [Toolbar](https://questdb.com/docs/getting-started/web-console/schema-explorer/#toolbar) * [Filter](https://questdb.com/docs/getting-started/web-console/schema-explorer/#filter) * [Suspended Tables](https://questdb.com/docs/getting-started/web-console/schema-explorer/#suspended-tables) * [Table Management Actions](https://questdb.com/docs/getting-started/web-console/schema-explorer/#table-management-actions) --- # QuestDB AI | QuestDB On this page The **QuestDB AI Assistant** provides intelligent query assistance directly within the Web Console. You can generate, explain, and fix SQL queries, and ask questions about your schema and QuestDB using models from OpenAI and Anthropic, all while maintaining complete control over your data and API keys. ![AI Assistant chat window in Web Console](https://questdb.com/docs/images/docs/console/ai-assistant-hero.webp) Configuration[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#configuration "Direct link to Configuration") -------------------------------------------------------------------------------------------------------------------------------- Before using the AI Assistant, you need to configure at least one AI provider with your own API key. Additional providers will be available in future releases. ### Adding a model provider[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#adding-a-model-provider "Direct link to Adding a model provider") The AI Assistant follows a Bring Your Own Key (BYOK) model for security and privacy. Currently, **OpenAI** and **Anthropic** models are available: ![Configuration modal first step](https://questdb.com/docs/images/docs/console/configure.webp) To add a model provider: 1. Click the **Configure** button in the top bar 2. Select your preferred AI provider 3. Enter your API key from the provider's platform: * [OpenAI Platform](https://platform.openai.com/api-keys) * [Anthropic Console](https://console.anthropic.com/settings/keys) 4. Click **Next** to validate your key info Your API keys are stored only in your browser's local storage and are never transmitted to QuestDB servers. They are sent directly to your chosen AI provider when making requests. ### Setting up model preferences[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#setting-up-model-preferences "Direct link to Setting up model preferences") After validating your API key, you can configure the provider settings: ![Model selection interface with toggle switches](https://questdb.com/docs/images/docs/console/configure-step-2.webp) * Enable individual models based on your needs. You can switch between enabled models at any time after setup. * Grant or revoke schema access to the AI Assistant. info Schema access only provides table structure information to the AI. Your actual data records are never sent to AI providers. Granting schema access helps the AI Assistant generate more accurate queries. ### Settings[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#settings "Direct link to Settings") After initial setup, you can modify settings or remove API keys using the **Settings** button in the top bar. ![Settings modal for configuring the providers after initial setup](https://questdb.com/docs/images/docs/console/settings.webp) Chat Window[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#chat-window "Direct link to Chat Window") -------------------------------------------------------------------------------------------------------------------------- The Chat Window is the primary interface for interacting with the AI Assistant. ### Opening the Chat[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#opening-the-chat "Direct link to Opening the Chat") Access the AI Assistant through multiple methods: * Clicking the AI icon in the right sidebar opens the latest chat ![Sidebar item for opening AI chat window](https://questdb.com/docs/images/docs/console/sidebar-ai.webp) * Clicking the AI icon next to a query in the Code Editor opens a chat for that query. **An icon with a border indicates an existing chat for the query.** ![AI Icons in editor](https://questdb.com/docs/images/docs/console/ai-gutter-icons.webp) * Clicking **Explain schema with AI** in the table context menu opens a chat with a schema explanation for the selected table, materialized view, or view. ![Explain schema with AI](https://questdb.com/docs/images/docs/console/explain-schema.png) ### Chat Interface[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#chat-interface "Direct link to Chat Interface") The chat window provides a complete conversation interface: * **Header**: Shows the conversation name with action buttons * **Messages**: Displays the conversation between you and the AI * **Input Area**: Text area for submitting your questions, with a context badge showing the connected entity info Chats are connected to a single query to improve response accuracy. The context badge in the input area shows which query or table the conversation is focused on. You can click on the context badge to see the related query in the editor. ### Managing Conversations[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#managing-conversations "Direct link to Managing Conversations") * **Create a new chat**: Click the **+** button in the chat header * **View chat history**: Click the history icon in the chat header to see all past chats ![Chat history view](https://questdb.com/docs/images/docs/console/chat-history.webp) Chats are displayed in a timeline. You can: * **Rename a chat**: Click the edit icon next to a conversation name * **Delete a chat**: Click the delete icon next to a conversation * **Search chats**: Use the text input to search conversations by name ### Quick Actions[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#quick-actions "Direct link to Quick Actions") When opening a chat for a query with no conversation history, quick actions are available: ![Empty chat window showing Explain Query button](https://questdb.com/docs/images/docs/console/quick-actions.webp) * **Explain Query**: Provides an explanation of the query logic * **Fix Query**: Appears when a query has an execution error. The AI Assistant analyzes the error and suggests a corrected version. ### SQL Suggestions[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#sql-suggestions "Direct link to SQL Suggestions") The AI Assistant can provide query suggestions when you prompt it to generate, refine, or fix a query. A diff editor is shown when a query is suggested: ![AI suggestion showing diff view with original and modified SQL](https://questdb.com/docs/images/docs/console/ai-query-suggestion.webp) The diff editor provides several actions: * **Run**: Execute the suggested query using the Run icon in the header * **Accept**: Apply the suggestion and mark it as accepted. The AI Assistant uses accepted queries as the basis for future suggestions. * **Reject**: Reject the suggestion and notify the model * **Apply to Editor**: Insert the suggestion into your editor. Available for all queries in the history. * **Open in editor**: Expand the diff view to a full editor tab where you can accept or reject the suggestion ### Status Indicators[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#status-indicators "Direct link to Status Indicators") The AI Assistant shows its reasoning process in expandable sections. You can investigate the reviewed documentation and tables by expanding individual status indicators. ### Aborting Generation[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#aborting-generation "Direct link to Aborting Generation") Click the red stop button during AI operations to cancel the current response. The conversation and message history are preserved, and you can continue the conversation or start a new operation. Tips for using the AI Assistant[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#tips-for-using-the-ai-assistant "Direct link to Tips for using the AI Assistant") -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- * Keep conversations focused on a single query or table for better contextual accuracy * Use the Explain feature to understand complex SQL patterns and QuestDB-specific syntax * Use the Fix feature when queries fail to get immediate troubleshooting assistance * Enable schema access for more accurate suggestions about your specific tables * Rename conversations with descriptive titles for easier navigation in history * Review AI suggestions carefully before accepting them into your editor Privacy & Data Security[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#privacy--data-security "Direct link to Privacy & Data Security") ------------------------------------------------------------------------------------------------------------------------------------------------------------- ### Data Flow[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#data-flow "Direct link to Data Flow") Queries and conversation context are sent directly from your browser to your chosen AI provider. QuestDB does not receive, store, or process your conversations. info Web Console does not send any data to a model provider unless a provider is configured explicitly by the user. ### Bring Your Own Key (BYOK)[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#bring-your-own-key-byok "Direct link to Bring Your Own Key (BYOK)") Your API keys and conversations are stored in your browser. They are never transmitted to QuestDB servers and remain under your complete control. You can edit or remove your API keys at any time through the Settings modal. Keys are sent only to your chosen AI provider when you make requests. ### Schema vs Data[​](https://questdb.com/docs/getting-started/web-console/questdb-ai/#schema-vs-data "Direct link to Schema vs Data") Schema access grants the AI visibility to your database structure (table names, column names, data types) but never includes actual data records or values from your tables. You control schema access independently for each provider. Even with schema access enabled, the AI only sees metadata about your database structure, not the data itself. Different AI providers have different data handling practices. Consult your provider's documentation to understand their data retention, usage, and privacy policies. * [Configuration](https://questdb.com/docs/getting-started/web-console/questdb-ai/#configuration) * [Adding a model provider](https://questdb.com/docs/getting-started/web-console/questdb-ai/#adding-a-model-provider) * [Setting up model preferences](https://questdb.com/docs/getting-started/web-console/questdb-ai/#setting-up-model-preferences) * [Settings](https://questdb.com/docs/getting-started/web-console/questdb-ai/#settings) * [Chat Window](https://questdb.com/docs/getting-started/web-console/questdb-ai/#chat-window) * [Opening the Chat](https://questdb.com/docs/getting-started/web-console/questdb-ai/#opening-the-chat) * [Chat Interface](https://questdb.com/docs/getting-started/web-console/questdb-ai/#chat-interface) * [Managing Conversations](https://questdb.com/docs/getting-started/web-console/questdb-ai/#managing-conversations) * [Quick Actions](https://questdb.com/docs/getting-started/web-console/questdb-ai/#quick-actions) * [SQL Suggestions](https://questdb.com/docs/getting-started/web-console/questdb-ai/#sql-suggestions) * [Status Indicators](https://questdb.com/docs/getting-started/web-console/questdb-ai/#status-indicators) * [Aborting Generation](https://questdb.com/docs/getting-started/web-console/questdb-ai/#aborting-generation) * [Tips for using the AI Assistant](https://questdb.com/docs/getting-started/web-console/questdb-ai/#tips-for-using-the-ai-assistant) * [Privacy & Data Security](https://questdb.com/docs/getting-started/web-console/questdb-ai/#privacy--data-security) * [Data Flow](https://questdb.com/docs/getting-started/web-console/questdb-ai/#data-flow) * [Bring Your Own Key (BYOK)](https://questdb.com/docs/getting-started/web-console/questdb-ai/#bring-your-own-key-byok) * [Schema vs Data](https://questdb.com/docs/getting-started/web-console/questdb-ai/#schema-vs-data) --- # Go Client Documentation | QuestDB On this page QuestDB supports the Go ecosystem, offering a Go client designed for high-performance data ingestion, tailored specifically for insert-only operations. This combination of QuestDB and its Go client provides exceptional time series data ingestion and analytical capabilities. The Go client introduces several advantages: * **Automatic table creation**: No need to define your schema upfront. * **Concurrent schema changes**: Seamlessly handle multiple data streams with on-the-fly schema modifications * **Optimized batching**: Use strong defaults or curate the size of your batches * **Health checks and feedback**: Ensure your system's integrity with built-in health monitoring * **Automatic write retries**: Reuse connections and retry after interruptions This quick start guide will help you get up and running with the basic functionalities of the Go client, covering connection setup, authentication, and some common insert patterns. ![Golang](https://questdb.com/docs/images/logos/go.svg) [![Documentation icon](https://questdb.com/docs/images/icons/open-book.svg "Documentation")View full docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/) [![Github icon](https://questdb.com/docs/images/github.svg "Source")View source code](https://github.com/questdb/go-questdb-client/) info This page focuses on our high-performance ingestion client, which is optimized for **writing** data to QuestDB. For retrieving data, we recommend using a [PostgreSQL-compatible Go library](https://questdb.com/docs/query/pgwire/go/) or our [HTTP query endpoint](https://questdb.com/docs/query/overview/#rest-http-api) . Requirements[​](https://questdb.com/docs/ingestion/clients/go/#requirements "Direct link to Requirements") ----------------------------------------------------------------------------------------------------------- * Requires Go 1.19 or later. * Assumes QuestDB is running. If it's not, refer to [the general quick start](https://questdb.com/docs/getting-started/quick-start/) . Client Installation[​](https://questdb.com/docs/ingestion/clients/go/#client-installation "Direct link to Client Installation") -------------------------------------------------------------------------------------------------------------------------------- To add the QuestDB client to your Go project: go get github.com/questdb/go-questdb-client/ Authentication[​](https://questdb.com/docs/ingestion/clients/go/#authentication "Direct link to Authentication") ----------------------------------------------------------------------------------------------------------------- Passing in a configuration string with HTTP basic authentication: package mainimport ( "context" "github.com/questdb/go-questdb-client/v4")func main() { ctx := context.TODO() client, err := questdb.LineSenderFromConf(ctx, "http::addr=localhost:9000;username=admin;password=quest;") if err != nil { panic("Failed to create client") } // Utilize the client for your operations...} Or, set the QDB\_CLIENT\_CONF environment variable and call `questdb.LineSenderFromEnv()`. 1. Export the configuration string as an environment variable: export QDB_CLIENT_CONF="http::addr=localhost:9000;username=admin;password=quest;" 2. Then in your Go code: client, err := questdb.LineSenderFromEnv(context.TODO()) Alternatively, you can use the built-in Go API to specify the connection options. package mainimport ( "context" qdb "github.com/questdb/go-questdb-client/v4")func main() { ctx := context.TODO() client, err := qdb.NewLineSender(context.TODO(), qdb.WithHttp(), qdb.WithAddress("localhost:9000"), qdb.WithBasicAuth("admin", "quest")) When using QuestDB Enterprise, authentication can also be done via REST token. Please check the [RBAC docs](https://questdb.com/docs/security/rbac/#authentication) for more info. Basic Insert[​](https://questdb.com/docs/ingestion/clients/go/#basic-insert "Direct link to Basic Insert") ----------------------------------------------------------------------------------------------------------- Example: inserting executed trades for cryptocurrencies. Without authentication and using the current timestamp: package mainimport ( "context" "github.com/questdb/go-questdb-client/v4")func main() { ctx := context.TODO() client, err := questdb.LineSenderFromConf(ctx, "http::addr=localhost:9000;") if err != nil { panic("Failed to create client") } err = client.Table("trades"). Symbol("symbol", "ETH-USD"). Symbol("side", "sell"). Float64Column("price", 2615.54). Float64Column("amount", 0.00044). AtNow(ctx) if err != nil { panic("Failed to insert data") } err = client.Flush(ctx) if err != nil { panic("Failed to flush data") }} In this case, the designated timestamp will be the one at execution time. Let's see now an example with an explicit timestamp, custom auto-flushing, and basic auth. package mainimport ( "context" "github.com/questdb/go-questdb-client/v4" "time")func main() { ctx := context.TODO() client, err := questdb.LineSenderFromConf(ctx, "http::addr=localhost:9000;username=admin;password=quest;auto_flush_rows=100;auto_flush_interval=1000;") if err != nil { panic("Failed to create client") } timestamp := time.Now() err = client.Table("trades"). Symbol("symbol", "ETH-USD"). Symbol("side", "sell"). Float64Column("price", 2615.54). Float64Column("amount", 0.00044). At(ctx, timestamp) if err != nil { panic("Failed to insert data") } err = client.Flush(ctx) // You can flush manually at any point. // If you don't flush manually, the client will flush automatically // when a row is added and either: // * The buffer contains 75000 rows (if HTTP) or 600 rows (if TCP) // * The last flush was more than 1000ms ago. // Auto-flushing can be customized via the `auto_flush_..` params. if err != nil { panic("Failed to flush data") }} We recommended to use User-assigned timestamps when ingesting data into QuestDB. Using the current timestamp hinder the ability to deduplicate rows which is [important for exactly-once processing](https://questdb.com/docs/ingestion/ilp/overview/#exactly-once-delivery-vs-at-least-once-delivery) . Configuration options[​](https://questdb.com/docs/ingestion/clients/go/#configuration-options "Direct link to Configuration options") -------------------------------------------------------------------------------------------------------------------------------------- The minimal configuration string needs to have the protocol, host, and port, as in: http::addr=localhost:9000; In the Go client, you can set the configuration options via the standard config string, which is the same across all clients, or using [the built-in API](https://pkg.go.dev/github.com/questdb/go-questdb-client/#LineSenderOption) . For all the extra options you can use, please check [the client docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/#LineSenderFromConf) Alternatively, for a breakdown of Configuration string options available across all clients, see the [Configuration string](https://questdb.com/docs/ingestion/clients/configuration-string/) page. Next Steps[​](https://questdb.com/docs/ingestion/clients/go/#next-steps "Direct link to Next Steps") ----------------------------------------------------------------------------------------------------- Please refer to the [ILP overview](https://questdb.com/docs/ingestion/ilp/overview/) for details about transactions, error control, delivery guarantees, health check, or table and column auto-creation. Explore the full capabilities of the Go client via [Go.dev](https://pkg.go.dev/github.com/questdb/go-questdb-client/) . With data flowing into QuestDB, now it's time to for analysis. To learn _The Way_ of QuestDB SQL, see the [Query & SQL Overview](https://questdb.com/docs/query/overview/) . Alone? Stuck? Want help? Visit us in our [Community Forum](https://community.questdb.com/) . * [Requirements](https://questdb.com/docs/ingestion/clients/go/#requirements) * [Client Installation](https://questdb.com/docs/ingestion/clients/go/#client-installation) * [Authentication](https://questdb.com/docs/ingestion/clients/go/#authentication) * [Basic Insert](https://questdb.com/docs/ingestion/clients/go/#basic-insert) * [Configuration options](https://questdb.com/docs/ingestion/clients/go/#configuration-options) * [Next Steps](https://questdb.com/docs/ingestion/clients/go/#next-steps) --- # Advanced InfluxDB Line Protocol settings | QuestDB On this page This documentation provides aid for those venturing outside of the path laid down by their language clients. For the introductory InfluxDB Line Protocol materials, including authentication, see the [ILP overview](https://questdb.com/docs/ingestion/ilp/overview/) . For the the basics of ingestion, instead consult the [Ingestion overview](https://questdb.com/docs/ingestion/overview/) . Syntax[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#syntax "Direct link to Syntax") ---------------------------------------------------------------------------------------------------- Each InfluxDB Line Protocol message has to end with a new line `\n` character. table_name,symbolset columnset timestamp\n | Element | Definition | | --- | --- | | `table_name` | Name of the table where QuestDB will write data. | | `symbolset` | A set of comma-separated `name=value` pairs that will be parsed as symbol columns. | | `columnset` | A set of comma-separated `name=value` pairs that will be parsed as non-symbol columns. | | `timestamp` | UNIX timestamp. The default unit is nanosecond and is configurable via `line.tcp.timestamp`. The value will be truncated to microsecond resolution when parsed by QuestDB. | `name` in the `name=value` pair always corresponds to `column name` in the table. Behavior[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#behavior "Direct link to Behavior") ---------------------------------------------------------------------------------------------------------- * When the `table_name` does not correspond to an existing table, QuestDB will create the table on the fly using the name provided. Column types will be automatically recognized and assigned based on the data. * The `timestamp` column is automatically created as [designated timestamp](https://questdb.com/docs/concepts/designated-timestamp/) with the [partition strategy](https://questdb.com/docs/concepts/partitions/) set to `DAY`. Alternatively, use [CREATE TABLE](https://questdb.com/docs/query/sql/create-table/) to create the table with a different partition strategy before ingestion. * When the timestamp is empty, QuestDB will use the server timestamp. Generic example[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#generic-example "Direct link to Generic example") ------------------------------------------------------------------------------------------------------------------------------- Let's assume the following data: | timestamp | symbol | price | amount | side | | --- | --- | --- | --- | --- | | 1465839830100400000 | BTC-USD | 61432 | 0.5 | buy | | 1465839830100600000 | ETH-USD | 3421 | 2.1 | sell | | 1465839830100700000 | BTC-USD | 61435 | 1.2 | buy | The line protocol syntax for that table is: trades,symbol=BTC-USD,side=buy price=61432,amount=0.5 1465839830100400000\ntrades,symbol=ETH-USD,side=sell price=3421,amount=2.1 1465839830100600000\ntrades,symbol=BTC-USD,side=buy price=61435,amount=1.2 1465839830100700000\n This would create table similar to this SQL statement and populate it. CREATE TABLE trades ( timestamp TIMESTAMP, symbol SYMBOL, price DOUBLE, amount DOUBLE, side SYMBOL) TIMESTAMP(timestamp) PARTITION BY DAY; Designated timestamp[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#designated-timestamp "Direct link to Designated timestamp") ---------------------------------------------------------------------------------------------------------------------------------------------- ### Timestamps[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#timestamps "Direct link to Timestamps") Designated timestamp is the trailing value of an InfluxDB Line Protocol message. It is optional, and when present, is a timestamp in Epoch nanoseconds. When the timestamp is omitted, the server will insert each message using the system clock as the row timestamp. See `cairo.timestamp.locale` and `line.tcp.timestamp` [configuration options](https://questdb.com/docs/configuration/overview/) . caution * While [`columnset` timestamp type units](https://questdb.com/docs/ingestion/ilp/columnset-types/#timestamp) are microseconds, the designated timestamp units are nanoseconds by default, and can be overridden via the `line.tcp.timestamp` configuration property. * The native timestamp format used by QuestDB is a Unix timestamp in microsecond resolution; timestamps in nanoseconds will be parsed and truncated to microseconds. When the `timestamp_ns` type is used for the designated column, the timestamp will retain the nanosecond precision. * For HTTP, precision parameters can added to a request. These include `n` or `ns` for nanoseconds, `u` or `us` formicroseconds, `ms` for milliseconds, `s` for seconds, `m` for minutes and `h` for hours. Otherwise, it will default to nanoseconds. curl -i -XPOST 'http://localhost:9000/write?db=mydb&precision=s' \--data-binary 'trades,symbol=BTC-USD price=61432 1465839830100400200' Example of InfluxDB Line Protocol message with desginated timestamp value tracking,loc=north val=200i 1000000000\n Example of InfluxDB Line Protocol message sans timestamp tracking,loc=north val=200i\n note We recommend populating designated timestamp via trailing value syntax above. It is also possible to populate designated timestamp via `columnset`. Please see [mixed timestamp](https://questdb.com/docs/ingestion/ilp/columnset-types/#timestamp) reference. Irregularly-structured data[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#irregularly-structured-data "Direct link to Irregularly-structured data") ------------------------------------------------------------------------------------------------------------------------------------------------------------------- InfluxDB line protocol makes it possible to send data under different shapes. Each new entry may contain certain tags or fields, and others not. QuestDB supports on-the-fly data structure changes with minimal overhead. Whilst the example just above highlights structured data, it is possible for InfluxDB line protocol users to send data as follows: trades,symbol=BTC-USD price=61432 1465839830100400000\ntrades,symbol=BTC-USD price=61435 1465839830100700000\ntrades,symbol=ETH-USD price=3421,amount=2.1 1465839830100800000\n This would result in the following table: | timestamp | symbol | price | amount | | --- | --- | --- | --- | | 1465839830100400000 | BTC-USD | 61432 | NULL | | 1465839830100700000 | BTC-USD | 61435 | NULL | | 1465839830100800000 | ETH-USD | 3421 | 2.1 | tip Whilst we offer this function for flexibility, we recommend that users try to minimize structural changes to maintain operational simplicity. Duplicate column names[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#duplicate-column-names "Direct link to Duplicate column names") ---------------------------------------------------------------------------------------------------------------------------------------------------- If line contains duplicate column names, the value stored in the table will be that from the first `name=value` pair on each line. For example: trade,ticker=USD price=30,price=60 1638202821000000000\n Price `30` is stored, `60` is ignored. Name restrictions[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#name-restrictions "Direct link to Name restrictions") ------------------------------------------------------------------------------------------------------------------------------------- Table name cannot contain any of the following characters: `\n`, `\r`, `?`, `,`, `”`, `"`, `\`, `/`, `:`, `)`, `(`, `+`, `*`, `%`, `~`, starting `.`, trailing `.`, or a non-printable char. Column name cannot contain any of the following characters: `\n`, `\r`, `?`, `.`, `,`, `”`, `"`, `\\`, `/`, `:`, `)`, `(`, `+`, `-`, `\*` `%%`, `~`, or a non-printable char. Both table name and column names are allowed to have spaces . These spaces have to be escaped with `\`. For example both of these are valid lines. trade\ table,ticker=USD price=30,details="Latest price" 1638202821000000000\n trade,symbol\ ticker=USD price=30,details="Latest price" 1638202821000000000\n Symbolset[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#symbolset "Direct link to Symbolset") ------------------------------------------------------------------------------------------------------------- Area of the message that contains comma-separated set of `name=value` pairs for symbol columns. For example in a message like this: trade,ticker=BTCUSD,venue=coinbase price=30,price=60 1638202821000000000\n `symbolset` is `ticker=BTCUSD,venue=coinbase`. Please note the mandatory space between `symbolset` and `columnset`. Naming rules for columns are subject to [duplicate rules](https://questdb.com/docs/ingestion/ilp/advanced-settings/#duplicate-column-names) and [name restrictions](https://questdb.com/docs/ingestion/ilp/advanced-settings/#name-restrictions) . ### Symbolset values[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#symbolset-values "Direct link to Symbolset values") `symbolset` values are always interpreted as [SYMBOL](https://questdb.com/docs/concepts/symbol/) . Parser takes values literally so please beware of accidentally using high cardinality types such as `9092i` or `1.245667`. This will result in a significant performance loss due to large mapping tables. `symbolset` values are not quoted. They are allowed to have special characters, such as (space), `=`, `,`, `\n`, `\r` and `\`, which must be escaped with a `\`. Example: trade,ticker=BTC\\USD\,All,venue=coin\ base price=30 1638202821000000000\n Whenever `symbolset` column does not exist, it will be added on-the-fly with type `SYMBOL`. On other hand when the column does exist, it is expected to be of `SYMBOL` type, otherwise the line is rejected. Columnset[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#columnset "Direct link to Columnset") ------------------------------------------------------------------------------------------------------------- Area of the message that contains comma-separated set of `name=value` pairs for non-symbol columns. For example in a message like this: trade,ticker=BTCUSD priceLow=30,priceHigh=60 1638202821000000000\n `columnset` is `priceLow=30,priceHigh=60`. Naming rules for columns are subject to [duplicate rules](https://questdb.com/docs/ingestion/ilp/advanced-settings/#duplicate-column-names) and [name restrictions](https://questdb.com/docs/ingestion/ilp/advanced-settings/#name-restrictions) . ### Columnset values[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#columnset-values "Direct link to Columnset values") `columnset` supports several values types, which are used to either derive type of new column or mapping strategy when column already exists. These types are limited by existing InfluxDB Line Protocol specification. Wider QuestDB type system is available by creating table via SQL upfront. The following are supported value types: [Integer](https://questdb.com/docs/ingestion/ilp/columnset-types/#integer) , [Long256](https://questdb.com/docs/ingestion/ilp/columnset-types/#long256) , [Float](https://questdb.com/docs/ingestion/ilp/columnset-types/#float) , [String](https://questdb.com/docs/ingestion/ilp/columnset-types/#string) and [Timestamp](https://questdb.com/docs/ingestion/ilp/columnset-types/#timestamp) Inserting NULL values[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#inserting-null-values "Direct link to Inserting NULL values") ------------------------------------------------------------------------------------------------------------------------------------------------- To insert a NULL value, skip the column (or symbol) for that row. For example: table1 a=10.5 1647357688714369403table1 b=1.25 1647357698714369403 Will insert as: | a | b | timestamp | | --- | --- | --- | | 10.5 | _NULL_ | 2022-03-15T15:21:28.714369Z | | _NULL_ | 1.25 | 2022-03-15T15:21:38.714369Z | InfluxDB Line Protocol Datatypes and Casts[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#influxdb-line-protocol-datatypes-and-casts "Direct link to InfluxDB Line Protocol Datatypes and Casts") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ### Varchar vs Symbols[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#varchar-vs-symbols "Direct link to Varchar vs Symbols") Strings may be recorded as either the `VARCHAR` type or the `SYMBOL` type. Inspecting a sample message we can see how a space `' '` separator splits `SYMBOL` columns to the left from the rest of the columns. table_name,col1=symbol_val1,col2=symbol_val2 col3="varchar val",col4=10.5 ┬ ╰───────── separator In this example, columns `col1` and `col2` are strings written to the database as `SYMBOL`s, whilst `col3` is written out as a `VARCHAR`. `SYMBOL`s are strings which are automatically [interned](https://en.wikipedia.org/wiki/String_interning) by the database on a per-column basis. You should use this type if you expect the string to be re-used over and over, such as is common with identifiers. For one-off strings use `VARCHAR` columns which aren't interned. ### Casts[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#casts "Direct link to Casts") QuestDB types are a superset of those supported by InfluxDB Line Protocol. This means that when sending data you should be aware of the performed conversions. See: * [QuestDB Types in SQL](https://questdb.com/docs/query/datatypes/overview/) * [InfluxDB Line Protocol types and cast conversion tables](https://questdb.com/docs/ingestion/ilp/columnset-types/) Constructing well-formed messages[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#constructing-well-formed-messages "Direct link to Constructing well-formed messages") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Different library implementations will perform different degrees of content validation upfront before sending messages out. To avoid encountering issues, follow these guidelines: * **All strings must be UTF-8 encoded.** * **Each column should only be specified once per row..** * **Symbol columns must be written out before other columns.** * **Table and column names can't have invalid characters.** These should not contain `?`, `.`,`,`, `'`, `"`, `\`, `/`, `:`, `(`, `)`, `+`, `-`, `*`, `%`, `~`,`' '` (space), `\0` (nul terminator), [ZERO WIDTH NO-BREAK SPACE](https://unicode-explorer.com/c/FEFF) . * **Write timestamp column via designated API**, or at the end of the message if you are using raw sockets. If you have multiple timestamp columns write additional ones as column values. * **Don't change column type between rows.** Error handling[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#error-handling "Direct link to Error handling") ---------------------------------------------------------------------------------------------------------------------------- QuestDB will always log any InfluxDB Line Protocol errors in its [server logs](https://questdb.com/docs/concepts/deep-dive/root-directory-structure/#log-directory) . It is recommended that sending applications reuse TCP connections. If QuestDB receives an invalid message, it will discard invalid lines, produce an error message in the logs and forcibly _disconnect_ the sender to prevent further data loss. Data may be discarded because of: * missing new line characters at the end of messages * an invalid data format such as unescaped special characters * invalid column / table name characters * schema mismatch with existing tables * message size overflows on the input buffer * system errors such as no space left on the disk Detecting malformed input can be achieved through QuestDB logs by searching for `LineTcpMeasurementScheduler` and `LineTcpConnectionContext`, for example: 2022-02-03T11:01:51.007235Z I i.q.c.l.t.LineTcpMeasurementScheduler could not create table [tableName=trades, ex=`column name contains invalid characters [colName=trade_%]`, errno=0] The following input is tolerated by QuestDB: * a column is specified twice or more on the same line, QuestDB will pick the first occurrence and ignore the rest * missing columns, their value will be defaulted to `null`/`0.0`/`false` depending on the type of the column * missing designated timestamp, the current server time will be used to generate the timestamp * the timestamp is specified as a column instead of appending it to the end of the line * timestamp appears as a column and is also present at the end of the line, the value sent as a field will be used With sufficient client-side validation, the lack of errors to the client and confirmation isn't necessarily a concern: QuestDB will log out any issues and disconnect on error. The database will process any valid lines up to that point and insert rows. To resume WAL table ingestion after recovery from errors, see [ALTER TABLE RESUME WAL](https://questdb.com/docs/query/sql/alter-table-resume-wal/) for more information. ### If you don't immediately see data[​](https://questdb.com/docs/ingestion/ilp/advanced-settings/#if-you-dont-immediately-see-data "Direct link to If you don't immediately see data") If you don't see your inserted data, this is usually a result of one of two things: * You prepared the messages, but forgot to call `.flush()` or similar in your client library, so no data was sent. * The internal timers and buffers within QuestDB did not commit the data yet. For development (and development only), you may want to tweak configuration settings to commit data more frequently. cairo.max.uncommitted.rows=1 Refer to [InfluxDB Line Protocol's configuration](https://questdb.com/docs/configuration/overview/#influxdb-line-protocol-ilp) documentation for more on these configuration settings. * [Syntax](https://questdb.com/docs/ingestion/ilp/advanced-settings/#syntax) * [Behavior](https://questdb.com/docs/ingestion/ilp/advanced-settings/#behavior) * [Generic example](https://questdb.com/docs/ingestion/ilp/advanced-settings/#generic-example) * [Designated timestamp](https://questdb.com/docs/ingestion/ilp/advanced-settings/#designated-timestamp) * [Timestamps](https://questdb.com/docs/ingestion/ilp/advanced-settings/#timestamps) * [Irregularly-structured data](https://questdb.com/docs/ingestion/ilp/advanced-settings/#irregularly-structured-data) * [Duplicate column names](https://questdb.com/docs/ingestion/ilp/advanced-settings/#duplicate-column-names) * [Name restrictions](https://questdb.com/docs/ingestion/ilp/advanced-settings/#name-restrictions) * [Symbolset](https://questdb.com/docs/ingestion/ilp/advanced-settings/#symbolset) * [Symbolset values](https://questdb.com/docs/ingestion/ilp/advanced-settings/#symbolset-values) * [Columnset](https://questdb.com/docs/ingestion/ilp/advanced-settings/#columnset) * [Columnset values](https://questdb.com/docs/ingestion/ilp/advanced-settings/#columnset-values) * [Inserting NULL values](https://questdb.com/docs/ingestion/ilp/advanced-settings/#inserting-null-values) * [InfluxDB Line Protocol Datatypes and Casts](https://questdb.com/docs/ingestion/ilp/advanced-settings/#influxdb-line-protocol-datatypes-and-casts) * [Varchar vs Symbols](https://questdb.com/docs/ingestion/ilp/advanced-settings/#varchar-vs-symbols) * [Casts](https://questdb.com/docs/ingestion/ilp/advanced-settings/#casts) * [Constructing well-formed messages](https://questdb.com/docs/ingestion/ilp/advanced-settings/#constructing-well-formed-messages) * [Error handling](https://questdb.com/docs/ingestion/ilp/advanced-settings/#error-handling) * [If you don't immediately see data](https://questdb.com/docs/ingestion/ilp/advanced-settings/#if-you-dont-immediately-see-data) --- # Code Editor | QuestDB On this page The **Code Editor** is the main workspace where you write and execute SQL queries in the QuestDB Web Console. It provides a modern, feature-rich editing experience with syntax highlighting, auto-completion, and multiple query execution mechanisms. ![Code Editor in the Web Console](https://questdb.com/docs/images/docs/console/code-editor.webp) Editor[​](https://questdb.com/docs/getting-started/web-console/code-editor/#editor "Direct link to Editor") ------------------------------------------------------------------------------------------------------------ The Monaco-based editor provides a powerful development environment for writing SQL queries with professional IDE features. It offers syntax highlighting, intelligent auto-completion for database objects, and multiple execution modes to suit different query workflows. ### Key Features[​](https://questdb.com/docs/getting-started/web-console/code-editor/#key-features "Direct link to Key Features") * **Syntax Highlighting**: Color-coded SQL keywords, strings, comments, and functions specific to QuestDB SQL * **Auto-Completion**: Intelligent suggestions for table names, columns, and SQL functions as you type * **Visual Query Status**: Glyph icons in the editor margin show query execution status (success, error, running) * **Error Markers**: Underlined error positions based on query results * **Multiple Execution Modes**: Support for single query execution, selection-based execution, and batch execution * **Query Planning**: Analyze query execution plans with EXPLAIN functionality info Error markers and the query log are dynamically updated based on cursor position. When you place your cursor within a query, the query log will display the status of that specific query, and error markers will appear if the query execution was previously unsuccessful. ### Running a Query[​](https://questdb.com/docs/getting-started/web-console/code-editor/#running-a-query "Direct link to Running a Query") Individual query execution offers flexible options for running specific SQL statements within your editor content. #### Running a query from the icon[​](https://questdb.com/docs/getting-started/web-console/code-editor/#running-a-query-from-the-icon "Direct link to Running a query from the icon") Click the icon in the left margin next to any SQL query to execute it. ![Run icon variants in the editor](https://questdb.com/docs/images/docs/console/editor-glyphs.webp) The icon provides visual feedback: * **Hollow play icon**: Ready to execute * **Success icon**: Query executed successfully * **Error icon**: Query failed with errors * **Cancel icon**: Currently running, click to cancel When multiple queries exist on the same line, a dropdown menu appears with execution options for each query. #### Running a query with selection[​](https://questdb.com/docs/getting-started/web-console/code-editor/#running-a-query-with-selection "Direct link to Running a query with selection") Select a portion of the query in the editor and press `Ctrl/Cmd + Enter`, or click on the run icon to execute only the selected portion. This allows you to run specific parts of larger queries or test query fragments independently. info When a query is executed with a selection, the selected portion of text is highlighted with a green or red background to indicate the status. You can also track the status from the run icon of the parent query. #### Getting query plan[​](https://questdb.com/docs/getting-started/web-console/code-editor/#getting-query-plan "Direct link to Getting query plan") Right-click on a run icon to access the context menu and select "Get query plan" to see how QuestDB will execute your query. This runs an `EXPLAIN` command and displays the execution plan in the result grid. See [EXPLAIN](https://questdb.com/docs/query/sql/explain/) for details. ### Running Multiple Queries[​](https://questdb.com/docs/getting-started/web-console/code-editor/#running-multiple-queries "Direct link to Running Multiple Queries") The Code Editor supports executing multiple queries in sequence through batch execution. This feature provides two distinct approaches for running multiple queries efficiently. The editor provides dedicated buttons on the top right for multiple query execution: ![Run query dropdown](https://questdb.com/docs/images/docs/console/editor-run-query.webp) **Run Query Button**: * Dynamically adapts based on your current selection and context * For single query: Shows "Run query" or "Run selected query" * For multiple selected queries: Shows "Run N selected queries" * **Keyboard shortcut**: `Ctrl/Cmd + Enter` **Run All Queries Button**: * Executes every query in the current tab sequentially * **Keyboard shortcut**: `Ctrl/Cmd + Shift + Enter` #### Execution Modes[​](https://questdb.com/docs/getting-started/web-console/code-editor/#execution-modes "Direct link to Execution Modes") **Selected Queries Mode**: When you have multiple queries selected (partially or fully), the system runs only the selected portions of each query in sequence. This allows you to: * Run specific parts of larger queries * Execute a subset of queries from your tab * Test query fragments before running the complete set **All Queries Mode**: When you choose "Run all queries", the system executes every query in the tab from top to bottom. This mode includes: * **Confirmation dialog**: Prevents accidental execution of all queries * **Stop after failure option**: Checkbox to halt execution when a query fails (enabled by default) * **Progress tracking**: Real-time feedback showing successful and failed query counts * **Execution summary**: Shows the summary in the query log, including timing and the number of failed/successful queries tip Running multiple queries is ideal for data migration, bulk operations, or running complex multi-step procedures. The "Stop after failure" option helps prevent cascading errors in critical operations. Tabs[​](https://questdb.com/docs/getting-started/web-console/code-editor/#tabs "Direct link to Tabs") ------------------------------------------------------------------------------------------------------ The Code Editor supports multiple tabs to help you organize and manage different SQL queries simultaneously. Each tab represents a separate query buffer with its own content and execution state. ### Adding a New Tab[​](https://questdb.com/docs/getting-started/web-console/code-editor/#adding-a-new-tab "Direct link to Adding a New Tab") Click the `+` button to create a new tab for writing additional queries ### Renaming a Tab[​](https://questdb.com/docs/getting-started/web-console/code-editor/#renaming-a-tab "Direct link to Renaming a Tab") Double-click on a tab name to rename it for better organization ### Tab History[​](https://questdb.com/docs/getting-started/web-console/code-editor/#tab-history "Direct link to Tab History") Access previously closed tabs and manage your query history ![Tab history in the Web Console](https://questdb.com/docs/images/docs/console/tab-history.webp) * **Restore Tab**: Click on an item to restore a previously closed tab from the history * **Clear History**: Remove all stored tab history to start fresh info Web Console maintains a separate query log for each tab. See [Query Log](https://questdb.com/docs/getting-started/web-console/query-log/) for details. * [Editor](https://questdb.com/docs/getting-started/web-console/code-editor/#editor) * [Key Features](https://questdb.com/docs/getting-started/web-console/code-editor/#key-features) * [Running a Query](https://questdb.com/docs/getting-started/web-console/code-editor/#running-a-query) * [Running Multiple Queries](https://questdb.com/docs/getting-started/web-console/code-editor/#running-multiple-queries) * [Tabs](https://questdb.com/docs/getting-started/web-console/code-editor/#tabs) * [Adding a New Tab](https://questdb.com/docs/getting-started/web-console/code-editor/#adding-a-new-tab) * [Renaming a Tab](https://questdb.com/docs/getting-started/web-console/code-editor/#renaming-a-tab) * [Tab History](https://questdb.com/docs/getting-started/web-console/code-editor/#tab-history) --- # Monitoring and alerting | QuestDB On this page There are many variables to consider when monitoring an active production database. This document is designed to be a helpful starting point. We plan to expand this guide to be more helpful. If you have any recommendations, feel free to [create an issue](https://github.com/questdb/documentation/issues) or a PR on GitHub. For detailed instructions on setting up Prometheus to scrape QuestDB metrics, see the [Prometheus integration guide](https://questdb.com/docs/integrations/other/prometheus/) . Basic health check[​](https://questdb.com/docs/operations/monitoring-alerting/#basic-health-check "Direct link to Basic health check") --------------------------------------------------------------------------------------------------------------------------------------- QuestDB comes with an out-of-the-box health check HTTP endpoint: GET health status of local instance curl -v http://127.0.0.1:9003 Getting an OK response means the QuestDB process is up and running. This method provides no further information. If you allocate 8 vCPUs/cores or less to QuestDB, the HTTP server thread may not be able to get enough CPU time to respond in a timely manner. Your load balancer may flag the instance as dead. In such a case, create an isolated thread pool just for the health check service (the `min` HTTP server), by setting this configuration option: http.min.worker.count=1 Alert on critical errors[​](https://questdb.com/docs/operations/monitoring-alerting/#alert-on-critical-errors "Direct link to Alert on critical errors") --------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB includes a log writer that sends any message logged at critical level to Prometheus Alertmanager over a TCP/IP socket. To configure this writer, add it to the `writers` config alongside other log writers. This is the basic setup: log.conf writers=stdout,alertw.alert.class=io.questdb.log.LogAlertSocketWriterw.alert.level=CRITICAL For more details, see the [Logging and metrics page](https://questdb.com/docs/operations/logging-metrics/#prometheus-alertmanager) . Detect table health issues[​](https://questdb.com/docs/operations/monitoring-alerting/#detect-table-health-issues "Direct link to Detect table health issues") --------------------------------------------------------------------------------------------------------------------------------------------------------------- This section covers monitoring and troubleshooting table health issues. For detailed per-table monitoring, use the [`tables()`](https://questdb.com/docs/query/functions/meta/#tables) function which returns real-time statistics including WAL status, memory pressure, and performance histograms. The function is lightweight and fully in-memory, suitable for frequent polling. ### Health dashboard query[​](https://questdb.com/docs/operations/monitoring-alerting/#health-dashboard-query "Direct link to Health dashboard query") SELECT table_name, table_row_count, wal_pending_row_count, CASE WHEN table_suspended THEN 'SUSPENDED' WHEN table_memory_pressure_level = 2 THEN 'BACKOFF' WHEN table_memory_pressure_level = 1 THEN 'PRESSURE' ELSE 'OK' END AS status, wal_txn - table_txn AS lag_txns, table_write_amp_p50 AS write_amp, table_merge_rate_p99 AS slowest_mergeFROM tables()WHERE walEnabledORDER BY table_suspended DESC, table_memory_pressure_level DESC, wal_pending_row_count DESC; ### Detect suspended tables[​](https://questdb.com/docs/operations/monitoring-alerting/#detect-suspended-tables "Direct link to Detect suspended tables") A WAL table becomes suspended when an error occurs during WAL apply, such as disk full, corrupted WAL segment, or kernel limits reached. While suspended, new data continues to be written to WAL but is not applied to the table. **Detection:** SELECT table_name FROM tables() WHERE table_suspended; **Resolution:** Resume from the failed transaction: ALTER TABLE my_table RESUME WAL; If the transaction is corrupted, skip it by specifying the next transaction: -- Find the last applied transactionSELECT writerTxn FROM wal_tables() WHERE name = 'my_table';-- Resume from the next transactionALTER TABLE my_table RESUME WAL FROM TXN ; For corrupted WAL segments (common after disk full errors), you may need to skip multiple transactions. Query `wal_transactions()` to find all transactions in the corrupted segment, then resume from the first transaction after that segment. See [ALTER TABLE RESUME WAL](https://questdb.com/docs/query/sql/alter-table-resume-wal/) for detailed recovery procedures including corrupted segment handling. ### Detect invalid materialized views[​](https://questdb.com/docs/operations/monitoring-alerting/#detect-invalid-materialized-views "Direct link to Detect invalid materialized views") Materialized views become invalid when their base table is modified in incompatible ways: dropping referenced columns, dropping partitions, renaming the table, or running TRUNCATE/UPDATE operations. **Detection:** SELECT view_name, invalidation_reasonFROM materialized_views()WHERE view_status = 'invalid'; **Resolution:** Perform a full refresh to rebuild the view: REFRESH MATERIALIZED VIEW my_view FULL; This deletes existing data and rebuilds from the base table. For large tables, this may take significant time. See [Materialized view invalidation](https://questdb.com/docs/concepts/materialized-views/#view-invalidation) for more details on causes and prevention. ### Detect memory pressure[​](https://questdb.com/docs/operations/monitoring-alerting/#detect-memory-pressure "Direct link to Detect memory pressure") Memory pressure indicates the system is running low on memory for out-of-order (O3) operations. Level 1 reduces parallelism to conserve memory. Level 2 enters backoff mode, which can significantly impact throughput. **Detection:** SELECT table_name, CASE table_memory_pressure_level WHEN 1 THEN 'PRESSURE' WHEN 2 THEN 'BACKOFF' END AS statusFROM tables()WHERE table_memory_pressure_level > 0; **Resolution:** Reduce O3 memory allocation per column. The default of 256K actually uses 512K (2x the configured size). Reducing this frees memory for other operations: server.conf cairo.o3.column.memory.size=128K Other options: * Add more RAM to the server * Reduce concurrent ingestion load * Reduce the number of tables with active O3 writes See [Capacity planning](https://questdb.com/docs/getting-started/capacity-planning/#memory-page-size-configuration) and [Optimize for many tables](https://questdb.com/docs/cookbook/operations/optimize-many-tables/) for detailed configuration guidance. ### Detect small transactions[​](https://questdb.com/docs/operations/monitoring-alerting/#detect-small-transactions "Direct link to Detect small transactions") Small transaction sizes may indicate that the client is sending individual rows instead of batching. Larger batch sizes reduce transaction overhead and improve ingestion throughput. **Detection:** SELECT table_name, wal_tx_size_p50, wal_tx_size_p90, wal_tx_size_maxFROM tables()WHERE walEnabled AND wal_tx_size_p90 > 0 AND wal_tx_size_p90 < 100; **Resolution:** * Use the [official client libraries](https://questdb.com/docs/ingestion/overview/#first-party-clients) which handle batching automatically * For custom ILP clients, configure auto-flush by row count or time interval rather than flushing after each row * For HTTP/PostgreSQL ingestion, send multiple rows per request ### Detect high write amplification[​](https://questdb.com/docs/operations/monitoring-alerting/#detect-high-write-amplification "Direct link to Detect high write amplification") Write amplification measures how many times data is rewritten during ingestion. A value of 1.0 is ideal, meaning each row is written exactly once. Higher values indicate O3 merge overhead from out-of-order data being merged into existing partitions. | Value | Interpretation | | --- | --- | | 1.0 – 1.5 | Excellent – minimal rewrites | | 1.5 – 3.0 | Normal for moderate out-of-order data | | 3.0 – 5.0 | Consider reducing partition size | | \> 5.0 | High – reduce partition size or investigate ingestion patterns | **Detection:** SELECT table_name, table_write_amp_p50, table_write_amp_p99, table_merge_rate_p99 AS slowest_mergeFROM tables()WHERE walEnabled AND table_write_amp_p50 > 3.0ORDER BY table_write_amp_p99 DESC; **Resolution:** Reduce partition size to limit the scope of O3 merges. For example, a table with `PARTITION BY DAY` experiencing high amplification may benefit from `PARTITION BY HOUR`: -- Recreate with smaller partitionsCREATE TABLE trades_new ( ...) TIMESTAMP(ts) PARTITION BY HOUR; Other options: * Reduce `cairo.writer.data.append.page.size` in server.conf * Enable [deduplication](https://questdb.com/docs/concepts/deduplication/) if data can be replayed * Investigate client-side to reduce out-of-order data at the source See [Write amplification](https://questdb.com/docs/getting-started/capacity-planning/#write-amplification) for detailed guidance. ### Detect transaction lag and pending rows[​](https://questdb.com/docs/operations/monitoring-alerting/#detect-transaction-lag-and-pending-rows "Direct link to Detect transaction lag and pending rows") When `wal_txn - table_txn` (pending transactions) or `wal_pending_row_count` (pending rows) continuously grows, the WAL apply process cannot keep up with ingestion. The data is safely stored in WAL but not yet visible to queries. A continuously rising difference indicates that either a table has become suspended and WAL can't be applied to it, or QuestDB is not able to keep up with the ingestion rate. **Detection:** SELECT table_name, wal_txn - table_txn AS pending_txns, wal_pending_row_countFROM tables()WHERE walEnabled AND (wal_txn - table_txn > 10 OR wal_pending_row_count > 1000000)ORDER BY wal_pending_row_count DESC; **Resolution:** * Check if the table is suspended and resume it. See [Detect suspended tables](https://questdb.com/docs/operations/monitoring-alerting/#detect-suspended-tables) . * Check for memory pressure which limits parallelism. See [Detect memory pressure](https://questdb.com/docs/operations/monitoring-alerting/#detect-memory-pressure) . * Check for high write amplification which slows merges. See [Detect high write amplification](https://questdb.com/docs/operations/monitoring-alerting/#detect-high-write-amplification) . * Temporarily reduce ingestion rate to allow the backlog to clear. See the [`tables()` reference](https://questdb.com/docs/query/functions/meta/#tables) for the complete list of columns and additional example queries. Detect slow queries[​](https://questdb.com/docs/operations/monitoring-alerting/#detect-slow-queries "Direct link to Detect slow queries") ------------------------------------------------------------------------------------------------------------------------------------------ QuestDB maintains a table called `_query_trace`, which records each executed query and the time it took. You can query this table to find slow queries. Read more on query tracing on the [Concepts page](https://questdb.com/docs/concepts/deep-dive/query-tracing/) . * [Basic health check](https://questdb.com/docs/operations/monitoring-alerting/#basic-health-check) * [Alert on critical errors](https://questdb.com/docs/operations/monitoring-alerting/#alert-on-critical-errors) * [Detect table health issues](https://questdb.com/docs/operations/monitoring-alerting/#detect-table-health-issues) * [Health dashboard query](https://questdb.com/docs/operations/monitoring-alerting/#health-dashboard-query) * [Detect suspended tables](https://questdb.com/docs/operations/monitoring-alerting/#detect-suspended-tables) * [Detect invalid materialized views](https://questdb.com/docs/operations/monitoring-alerting/#detect-invalid-materialized-views) * [Detect memory pressure](https://questdb.com/docs/operations/monitoring-alerting/#detect-memory-pressure) * [Detect small transactions](https://questdb.com/docs/operations/monitoring-alerting/#detect-small-transactions) * [Detect high write amplification](https://questdb.com/docs/operations/monitoring-alerting/#detect-high-write-amplification) * [Detect transaction lag and pending rows](https://questdb.com/docs/operations/monitoring-alerting/#detect-transaction-lag-and-pending-rows) * [Detect slow queries](https://questdb.com/docs/operations/monitoring-alerting/#detect-slow-queries) --- # Backup and restore | QuestDB On this page You should back up QuestDB to be prepared for the case where your original database or data is lost, or if your database or table is corrupted. Backups are also required to create [replica instances](https://questdb.com/docs/high-availability/setup/) in QuestDB Enterprise. Overview[​](https://questdb.com/docs/operations/backup/#overview "Direct link to Overview") -------------------------------------------------------------------------------------------- QuestDB supports two backup methods: * **Built-in incremental backup** (Enterprise only): Fully automated—configure once, set a schedule, and backups run automatically. Supports point-in-time recovery to any backup timestamp. * **[Manual checkpoint backup](https://questdb.com/docs/operations/backup/#questdb-oss-manual-backups-with-checkpoints) ** (OSS and Enterprise): Relies on external tools to copy data. Requires manual coordination: `CHECKPOINT CREATE` → copy data with external tools → `CHECKPOINT RELEASE`. Works well with cloud disk snapshots (AWS EBS, Azure disks, etc.) where you simply trigger a snapshot. For on-premises environments without snapshot capabilities, you'll need external tools or custom scripts (e.g., rsync), which do not provide point-in-time recovery. QuestDB Enterprise: built-in backup and restore[​](https://questdb.com/docs/operations/backup/#questdb-enterprise-built-in-backup-and-restore "Direct link to QuestDB Enterprise: built-in backup and restore") ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- QuestDB Enterprise provides an incremental backup system that stores your data in object storage. Backups are incremental—only changed data is uploaded—making them fast and bandwidth-efficient. You can monitor progress and check for errors while backups run. ### Prerequisites[​](https://questdb.com/docs/operations/backup/#prerequisites "Direct link to Prerequisites") #### License[​](https://questdb.com/docs/operations/backup/#license "Direct link to License") Built-in backup requires QuestDB Enterprise. This feature is not available in the open source version. See [QuestDB Enterprise](https://questdb.com/enterprise/) for licensing information. #### Supported storage backends[​](https://questdb.com/docs/operations/backup/#supported-storage-backends "Direct link to Supported storage backends") Backup supports the following storage backends: * **Amazon S3** and S3-compatible storage (MinIO, etc.) * **Azure Blob Storage** * **Google Cloud Storage (GCS)** * **Filesystem** - Local or network-attached storage (NFS, etc.). Backup is not sensitive to the underlying filesystem type. See [Configure object storage](https://questdb.com/docs/high-availability/setup/#1-configure-object-storage) for connection string formats. #### Permissions[​](https://questdb.com/docs/operations/backup/#permissions "Direct link to Permissions") The backup process requires write access to the target storage. Authentication is optional—you can use instance credentials (IAM roles, managed identities) or provide explicit credentials in the connection string. #### Network[​](https://questdb.com/docs/operations/backup/#network "Direct link to Network") Network requirements depend on your chosen storage backend. Ensure QuestDB can reach the storage endpoint on the appropriate port (typically HTTPS/443 for cloud storage). #### Storage capacity[​](https://questdb.com/docs/operations/backup/#storage-capacity "Direct link to Storage capacity") Plan your backup storage before starting. A safe estimate is **2× your uncompressed database size**. See [Estimate backup storage](https://questdb.com/docs/operations/backup/#estimate-backup-storage) for detailed calculations. ### Quick start[​](https://questdb.com/docs/operations/backup/#quick-start "Direct link to Quick start") Minimal configuration to enable backups: backup.enabled=truebackup.object.store=s3::bucket=my-bucket;region=eu-west-1;access_key_id=...;secret_access_key=...; Then run `BACKUP DATABASE;` in SQL. See [Run a backup](https://questdb.com/docs/operations/backup/#run-a-backup) for details. ### Configure[​](https://questdb.com/docs/operations/backup/#configure "Direct link to Configure") #### Backup retention[​](https://questdb.com/docs/operations/backup/#backup-retention "Direct link to Backup retention") Control how many backups to keep before automatic cleanup removes older ones: backup.cleanup.keep.latest.n=7 #### Filesystem backups[​](https://questdb.com/docs/operations/backup/#filesystem-backups "Direct link to Filesystem backups") For local testing or air-gapped environments, you can back up to a local filesystem path instead of cloud object storage: backup.object.store=fs::root=/mnt/backups;atomic_write_dir=/mnt/backups/atomic; The `atomic_write_dir` parameter is required for filesystem backends and specifies a directory for atomic write operations during backup. #### Configuration reference[​](https://questdb.com/docs/operations/backup/#configuration-reference "Direct link to Configuration reference") | Property | Description | Default | | --- | --- | --- | | `backup.enabled` | Enable backup functionality | `false` | | `backup.object.store` | Object store connection string | None (required) | | `backup.schedule.cron` | Cron expression for [scheduled backups](https://questdb.com/docs/operations/backup/#scheduled-backups) | None (manual only) | | `backup.schedule.tz` | [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
for cron [schedule](https://questdb.com/docs/operations/backup/#scheduled-backups) | `UTC` | | `backup.cleanup.keep.latest.n` | Number of backups to retain | `5` | | `backup.compression.level` | Compression level (1-22) | `5` | | `backup.compression.threads` | Threads for compression | CPU count | | `backup.enable.partition.hashes` | Compute BLAKE3 hashes during backup | `false` | | `backup.verify.partition.hashes` | Verify hashes during restore | `false` | ### Run a backup[​](https://questdb.com/docs/operations/backup/#run-a-backup "Direct link to Run a backup") Once configured, you can run a backup at any time using the following command: Backup database BACKUP DATABASE; Example output: | backup\_timestamp | | --- | | 2024-08-24T12:34:56.789123Z | The backup captures the committed database state at the moment the command executes. In-flight transactions are not included. ### Monitor and abort[​](https://questdb.com/docs/operations/backup/#monitor-and-abort "Direct link to Monitor and abort") You can monitor backup progress and history using the `backups()` table function: Backup history SELECT * FROM backups(); Example output: | status | progress\_percent | start\_ts | end\_ts | backup\_error | cleanup\_error | | --- | --- | --- | --- | --- | --- | | backup complete | 100 | 2025-07-30T12:49:30.554262Z | 2025-07-30T16:19:48.554262Z | | | | backup complete | 100 | 2025-08-06T14:15:22.882130Z | 2025-08-06T17:09:57.882130Z | | | | backup failed | 35 | 2025-08-20T11:58:03.675219Z | 2025-08-20T12:14:07.675219Z | connection error | | | backup in progress | 10 | 2025-08-27T15:42:18.281907Z | | | | | cleanup in progress | 100 | 2025-08-13T13:37:41.103729Z | 2025-08-13T16:44:25.103729Z | | | Status values: | Status | Meaning | Action | | --- | --- | --- | | `backup in progress` | Backup is currently running | Wait or run `BACKUP ABORT` | | `backup complete` | Backup finished successfully | None required | | `backup failed` | Backup encountered an error | Check `backup_error` column | | `cleanup in progress` | Old backup data is being removed | Wait for completion | | `cleanup complete` | Cleanup finished successfully | None required | | `cleanup failed` | Cleanup encountered an error | Check `cleanup_error` column | To abort a running backup: Abort backup BACKUP ABORT; ### Scheduled backups[​](https://questdb.com/docs/operations/backup/#scheduled-backups "Direct link to Scheduled backups") You can configure automatic scheduled backups using cron syntax. The example below runs a backup every day at midnight UTC. backup.schedule.cron=0 0 * * *backup.schedule.tz=UTC #### Cron format[​](https://questdb.com/docs/operations/backup/#cron-format "Direct link to Cron format") QuestDB uses the standard **5-field cron format**: FIELD VALUES SPECIAL CHARS┌──────────── minute ───────── 0-59 ───────────── * , - /│ ┌────────── hour ─────────── 0-23 ───────────── * , - /│ │ ┌──────── day of month ─── 1-31 ───────────── * , - / L W│ │ │ ┌────── month ────────── 1-12 or JAN-DEC ── * , - /│ │ │ │ ┌──── day of week ──── 0-7 or SUN-SAT ─── * , - / L #│ │ │ │ │* * * * * Special character meanings: * `*` — matches any value * `,` — separates multiple values (e.g., `1,15` for 1st and 15th) * `-` — defines a range (e.g., `1-5` for Monday through Friday) * `/` — specifies intervals (e.g., `*/15` for every 15 units) * `L` — last day of the month, or last specific weekday (e.g., `5L` = last Friday) * `W` — nearest weekday to the given day (e.g., `15W` = nearest weekday to the 15th) * `#` — nth weekday of the month (e.g., `5#3` = third Friday) For day-of-week, 0 and 7 both represent Sunday; 1-6 represent Monday through Saturday. tip Use [crontab.guru](https://crontab.guru/) to build and validate your cron expressions. #### Timezone[​](https://questdb.com/docs/operations/backup/#timezone "Direct link to Timezone") The `backup.schedule.tz` property accepts any valid [IANA timezone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g., `America/New_York`, `Europe/London`) or `UTC`. If `backup.schedule.tz` not specified, the default is `UTC`. #### Resetting schedule without restart[​](https://questdb.com/docs/operations/backup/#resetting-schedule-without-restart "Direct link to Resetting schedule without restart") The `backup.schedule.cron` and `backup.schedule.tz` settings can be modified in `server.conf` and hot-reloaded without restarting the server: SELECT reload_config(); You can also use this to enable and disable the schedule by adding or commenting out the `backup.schedule.cron` config setting. ### Backup instance name[​](https://questdb.com/docs/operations/backup/#backup-instance-name "Direct link to Backup instance name") Each QuestDB instance has a backup instance name (three random words like `gentle-forest-orchid`). This name is generated on the first backup and organizes backups in the object store under `backup//`. To find your instance name, run: SELECT backup_instance_name; Returns `null` if no backup has been run yet. ### Replication WAL cleanup integration[​](https://questdb.com/docs/operations/backup/#replication-wal-cleanup-integration "Direct link to Replication WAL cleanup integration") When replication is enabled, the [WAL cleaner](https://questdb.com/docs/high-availability/wal-cleanup/) uses backup manifests to determine which replicated WAL data in object storage can be safely deleted. By default, the cleaner retains replication data for as many backups as your [`backup.cleanup.keep.latest.n`](https://questdb.com/docs/operations/backup/#backup-retention) setting (default 5) and deletes everything older. No additional configuration is required — enabling backups on a replicated instance is sufficient. ### Performance characteristics[​](https://questdb.com/docs/operations/backup/#performance-characteristics "Direct link to Performance characteristics") Backup is designed to prioritize database availability over backup speed. Key characteristics: * **Pressure-sensitive**: Backup automatically throttles itself to avoid overwhelming the database instance during normal operations * **Batch uploads**: Data uploads in batches rather than continuously - you may see surges of activity followed by quieter periods in logs * **Compressed**: Data is compressed before upload to reduce transfer time and storage costs * **Multi-threaded**: Backup uses multiple threads but is deliberately throttled to maintain instance reliability Backup duration depends on data size. Large databases (1TB+) may take several hours for a full initial backup. Subsequent incremental backups are faster as only changed data is uploaded. ### Estimate backup storage[​](https://questdb.com/docs/operations/backup/#estimate-backup-storage "Direct link to Estimate backup storage") A safe estimate for total backup storage is **2× your uncompressed database size on disk**. This provides headroom for the full backup plus incremental history and edge cases. #### How storage accumulates[​](https://questdb.com/docs/operations/backup/#how-storage-accumulates "Direct link to How storage accumulates") | Backup type | What's uploaded | Estimated size | | --- | --- | --- | | Initial (full) | Entire database | DB size ÷ 4 (default compression) | | Incremental | Changed partitions only | Changed data ÷ 4 | Total storage = full backup + (average incremental × retention count) The default compression level (5) achieves approximately 4× reduction. Higher `backup.compression.level` values (up to 22) improve compression at the cost of CPU time. #### Partition-level granularity[​](https://questdb.com/docs/operations/backup/#partition-level-granularity "Direct link to Partition-level granularity") Partitions are the smallest backup unit. Any modification to a partition—even a single row or column update—causes the entire partition to be re-uploaded in the next incremental backup. This means: * **Append-only workloads** (typical time-series): Very efficient. Only the latest partition changes between backups. * **Cross-partition updates**: Less efficient. An `UPDATE` without a constraining `WHERE` clause touches all partitions, causing them all to be re-uploaded. * **Schema changes**: Column type changes cause affected partitions to be re-uploaded. #### Example calculation[​](https://questdb.com/docs/operations/backup/#example-calculation "Direct link to Example calculation") A 500 GB database with daily backups, 7-day retention, and ~5% daily change: | Component | Calculation | Size | | --- | --- | --- | | Full backup | 500 GB ÷ 4 | 125 GB | | Daily incremental | 25 GB ÷ 4 | ~6 GB | | 7 incrementals | 6 GB × 7 | ~42 GB | | **Total** | | **~170 GB** | In this example, actual usage (~170 GB) is well under the 2× planning estimate (1 TB). The 2× rule is intentionally conservative—use it for initial capacity planning before you know your actual change patterns, then refine based on observed usage. #### Check actual usage[​](https://questdb.com/docs/operations/backup/#check-actual-usage "Direct link to Check actual usage") To verify your estimates against actual storage, browse your backup data in the object store. Backups are stored under `backup//`. To find your instance name, see [Backup instance name](https://questdb.com/docs/operations/backup/#backup-instance-name) . ### Limitations[​](https://questdb.com/docs/operations/backup/#limitations "Direct link to Limitations") * **Database-wide only**: Backup captures the entire database. You cannot exclude tables or backup selected tables individually. Every backup includes all user tables, materialized views, and metadata. * **One backup at a time**: Only one backup can run at any given time. Starting a new backup while one is running will return an error. * **Primary and replica backups are separate**: Each QuestDB instance has its own [`backup_instance_name`](https://questdb.com/docs/operations/backup/#backup-instance-name) , so backing up both a primary and its replica creates two separate backup sets in the object store. Typically, backing up the primary is sufficient since replicas sync from the same data. * **Same backup object store for all nodes**: When using replication, all nodes in the cluster should use the same `backup.object.store` connection string. The [WAL cleaner](https://questdb.com/docs/high-availability/wal-cleanup/) reads backup manifests from every node to determine what replication data can be safely deleted. If nodes back up to different object stores, the cleaner cannot see all manifests and will not trigger correctly. ### Backup validation[​](https://questdb.com/docs/operations/backup/#backup-validation "Direct link to Backup validation") Backup integrity is verified during restore, not as a standalone operation. #### Verification during restore[​](https://questdb.com/docs/operations/backup/#verification-during-restore "Direct link to Verification during restore") QuestDB performs the following checks when restoring: * **Transaction log verification**: Header, hash, and size validation of transaction log entries (always enabled) * **Partition hash verification**: Optional BLAKE3 hash comparison for each file in every partition * **Manifest validation**: Version compatibility and path safety checks To enable partition hash verification, set these properties in `server.conf`: backup.enable.partition.hashes=true # Compute hashes during backupbackup.verify.partition.hashes=true # Verify hashes during restore If verification fails, restore stops immediately with an error such as: `hash mismatch [path=col1.d, expected=..., actual=...]` #### What's not available[​](https://questdb.com/docs/operations/backup/#whats-not-available "Direct link to What's not available") * No standalone `VALIDATE BACKUP` command * No dry-run restore option * Object store integrity relies on the storage provider (e.g., S3's built-in checksums) ### Restore[​](https://questdb.com/docs/operations/backup/#restore "Direct link to Restore") Restore is fast—approximately 1.8 TiB can be restored in under 20 minutes, depending on network bandwidth and storage performance. caution Enterprise backup restore uses a different trigger file (`_backup_restore`) than OSS checkpoint restore (`_restore`). Do not confuse these two mechanisms. To restore from an object store backup, create a `_backup_restore` file in the QuestDB install root. This is a properties file with the object store configuration and optional selector fields. On startup, QuestDB reads this file, selects the requested backup timestamp (or the latest available), downloads the backup data, and reconstructs the local database state. backup.object.store=s3::bucket=my-bucket;region=eu-west-1;access_key_id=...;secret_access_key=...;backup.instance.name=gentle-forest-orchidbackup.restore.timestamp=2024-08-24T12:34:56.789123Z Parameters: | Parameter | Required | Description | | --- | --- | --- | | `backup.object.store` | Sometimes | Object store connection string; required unless already specified in `server.conf` | | `backup.instance.name` | Sometimes | Required when multiple instance names exist in the bucket; see [Backup instance name](https://questdb.com/docs/operations/backup/#backup-instance-name) | | `backup.restore.timestamp` | No | Timestamp for point-in-time recovery; omit for latest backup | #### Point-in-time recovery[​](https://questdb.com/docs/operations/backup/#point-in-time-recovery "Direct link to Point-in-time recovery") Use `backup.restore.timestamp` to restore to a specific point in time. QuestDB finds the most recent successful backup at or before the specified timestamp. To find available backup timestamps, query the source instance: SELECT start_ts FROM backups() WHERE status = 'backup complete'; You can also specify an arbitrary timestamp (e.g., just before an accidental deletion). QuestDB restores from the nearest available backup before that time. If no backup exists at or before the specified timestamp, QuestDB fails to start with the error: `backup restore error: No backup timestamp found that is <=`. warning Restore requires an empty database directory. If the target database already has data (indicated by the presence of `db/.data_id`), restore fails with: "The local database is not empty." Use a fresh installation directory for restore operations. The QuestDB version performing the restore must have the same major version as the version that created the backup (e.g., 8.1.0 and 8.1.1 are compatible). Restart QuestDB. If restore succeeds, `_backup_restore` is removed automatically. #### Restore failure recovery[​](https://questdb.com/docs/operations/backup/#restore-failure-recovery "Direct link to Restore failure recovery") If restore fails, QuestDB creates artifacts to help diagnose and recover: | Artifact | Purpose | | --- | --- | | `.restore_failed/` | Directory containing tables that failed to restore | | `_restore_failed` | File listing the names of failed tables | To recover from a failed restore: 1. Check the `.restore_failed/` directory and `_restore_failed` file for details 2. Investigate and fix the underlying issue (connectivity, permissions, etc.) 3. Remove both `.restore_failed/` directory and `_restore_failed` file 4. Restart QuestDB to retry the restore If you see the error "Failed restore directory found", a previous restore attempt failed. Remove the artifacts listed above before restarting. ### Create a replica from a backup[​](https://questdb.com/docs/operations/backup/#create-a-replica-from-a-backup "Direct link to Create a replica from a backup") You can use a backup to bootstrap a new replica instance instead of relying solely on WAL replay from the object store. This is faster when the backup is more recent than the oldest available WAL data. 1. **Ensure the primary is running and has replication configured** The primary must have `replication.role=primary` and a configured `replication.object.store`. 2. **Create a `_backup_restore` file on the new replica machine** Point it to the same backup location used by the primary: backup.object.store=s3::bucket=my-bucket;region=eu-west-1;access_key_id=...;secret_access_key=...;backup.instance.name=gentle-forest-orchid 3. **Configure the replica** Set `replication.role=replica` and ensure `replication.object.store` points to the same object store as the primary. 4. **Start the replica** QuestDB restores from the backup first, then switches to WAL replay to catch up with the primary. For more details on replication setup, see the [replication guide](https://questdb.com/docs/high-availability/setup/) . ### Troubleshooting[​](https://questdb.com/docs/operations/backup/#troubleshooting "Direct link to Troubleshooting") If you encounter errors during backup or restore: * **ER007 - Data ID mismatch**: The local database and object store have different Data IDs. See [error code ER007](https://questdb.com/docs/troubleshooting/error-codes/#er007) for resolution steps. * **Backup stuck at 0%**: Check network connectivity to the object store and verify credentials are correct. * **"Failed restore directory found"**: A previous restore attempt failed. Remove the `.restore_failed/` directory and `_restore_failed` file, then restart. See [Restore failure recovery](https://questdb.com/docs/operations/backup/#restore-failure-recovery) . * **"The local database is not empty"**: Restore requires an empty database directory. Use a fresh installation or remove the existing `db/` directory. QuestDB OSS: manual backups with checkpoints[​](https://questdb.com/docs/operations/backup/#questdb-oss-manual-backups-with-checkpoints "Direct link to QuestDB OSS: manual backups with checkpoints") ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The OSS workflow relies on the `CHECKPOINT` mode and external snapshot or file copy tools. When in `CHECKPOINT` mode, QuestDB remains available for reads and writes, but some housekeeping tasks are paused. This is safe in principle, but database writes may consume more space than normal. When the database exits `CHECKPOINT` mode, it resumes the housekeeping tasks and reclaims disk space. You must create a copy of the database using a tool of your choice. These are some suggestions: * Cloud snapshot, e.g. EBS volume snapshot on AWS, Premium SSD Disk snapshot on Azure etc * On-prem backup tools and software you typically use * Basic command line tools, such as `cp` or `rsync` ### Data backup checklist[​](https://questdb.com/docs/operations/backup/#data-backup-checklist "Direct link to Data backup checklist") Before backing up QuestDB, consider these items: #### Pick a good time[​](https://questdb.com/docs/operations/backup/#pick-a-good-time "Direct link to Pick a good time") We recommend that teams take a database backup when the database write load is at its lowest. If the database is under constant write load, a helpful workaround is to ensure that the disk has at least 50% free space. The more free space, the safer it is to enter the checkpoint mode. #### Determine backup frequency[​](https://questdb.com/docs/operations/backup/#determine-backup-frequency "Direct link to Determine backup frequency") We recommend daily backups for disaster recovery purposes. #### Choose your data copy method[​](https://questdb.com/docs/operations/backup/#choose-your-data-copy-method "Direct link to Choose your data copy method") When choosing the right copy method, consider the following goals: * Minimize the time QuestDB spends in checkpoint mode * Ensure that the copy time remains sustainable as the database grows QuestDB backup lends itself relatively well to all types of differential data copying. Due to time partitioning, older data is often unmodified, at both block and file levels. ##### Cloud snapshots[​](https://questdb.com/docs/operations/backup/#cloud-snapshots "Direct link to Cloud snapshots") If you're using cloud disks, such as EBS on AWS, SSD on Azure, or similar, we strongly recommend using their existing cloud _snapshot_ infrastructure. The advantages of this approach are that: * Cloud snapshots minimize the time QuestDB spends in checkpoint mode * Cloud snapshots are differential and can be restored cleanly See the following guides for volume snapshot creation on the following cloud platforms: * [AWS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-creating-snapshot.html) - creating EBS snapshots * [Azure](https://docs.microsoft.com/en-us/azure/virtual-machines/snapshot-copy-managed-disk?tabs=portal) - creating snapshots of a virtual hard disk * [GCP](https://cloud.google.com/compute/docs/disks/create-snapshots) - working with persistent disk snapshots Cloud snapshot-based systems usually break down their backup process into two steps: 1. Take a snapshot 2. Back up the snapshot **Exit the `CHECKPOINT` mode as soon as the snapshotting stage is complete.** Specifically, exit checkpoint mode at the following snapshot stage: | Cloud Provider | State | Exit checkpoint mode | | --- | --- | --- | | **Google Cloud** (GCP) | RUNNING (UPLOADING) | When RUNNING substate changes from CREATING to UPLOADING | | **Amazon Web Services** (AWS) | PENDING | When status is PENDING | | **Microsoft Azure** | PENDING | Before the longer running "CREATING" stage | ##### Volume snapshots[​](https://questdb.com/docs/operations/backup/#volume-snapshots "Direct link to Volume snapshots") When the database is on-prem, we recommend using existing file system backup tools. For example, volume snapshots can be taken via [LVM](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/logical_volume_manager_administration/lvm_overview) . ##### File copy[​](https://questdb.com/docs/operations/backup/#file-copy "Direct link to File copy") If filesystem or volume snapshots are not available, consider using a file copy method to back up the QuestDB server root directory. We recommend using a copy tool that can skip copying files based on the modification date. One such popular tool to accomplish this is [rsync](https://linux.die.net/man/1/rsync) . ### Steps in the backup procedure[​](https://questdb.com/docs/operations/backup/#steps-in-the-backup-procedure "Direct link to Steps in the backup procedure") While explaining the steps, we'll assume the database root directory is `/var/lib/questdb`. #### Enter checkpoint mode[​](https://questdb.com/docs/operations/backup/#enter-checkpoint-mode "Direct link to Enter checkpoint mode") To enter the checkpoint mode: Creating a Checkpoint CHECKPOINT CREATE You can create only one checkpoint. Attempting to create a second checkpoint will fail. #### Check checkpoint status[​](https://questdb.com/docs/operations/backup/#check-checkpoint-status "Direct link to Check checkpoint status") You can double-check at any time that the database is in the checkpoint mode: Checking Checkpoint Status SELECT * FROM checkpoint_status(); Having confirmed that QuestDB has entered the checkpoint mode, we now create the backup. #### Take a snapshot or begin file copy[​](https://questdb.com/docs/operations/backup/#take-a-snapshot-or-begin-file-copy "Direct link to Take a snapshot or begin file copy") After a checkpoint is created and before it is released, you may safely access the file system using tools external to the database instance. In other words, you're now OK to begin your backup. If your data copy method is a volume snapshot, you can exit the checkpoint mode as soon as the snapshot is taken (which takes a minute or two). **Make sure to back up the entire server root directory, including the `db`, `snapshot`, and all other directories.** File copy may take longer to back up files compared to snapshot. You will have to wait until the data transfer is fully complete before exiting checkpoint mode. **It is very important to exit the checkpoint mode regardless of whether the copy operation succeeded or failed!** #### Exit checkpoint mode[​](https://questdb.com/docs/operations/backup/#exit-checkpoint-mode "Direct link to Exit checkpoint mode") With your backup complete, exit checkpoint mode: Releasing a Checkpoint CHECKPOINT RELEASE This concludes the backup process. Now, with our additional copy, we're ready to restore QuestDB. ### Restore to a saved checkpoint[​](https://questdb.com/docs/operations/backup/#restore-to-a-saved-checkpoint "Direct link to Restore to a saved checkpoint") Restoring from a local checkpoint will restore the entire database. caution OSS checkpoint restore uses the `_restore` trigger file. This is different from Enterprise backup restore which uses `_backup_restore`. Follow these steps: * Ensure your QuestDB version matches the one that did the backup * Restore QuestDB root directory contents (`/var/lib/questdb/`) from the backup * Touch the `_restore` file * Start the database using the restored root directory #### Database versions[​](https://questdb.com/docs/operations/backup/#database-versions "Direct link to Database versions") Restoring data is only possible if the backup and restore QuestDB versions have the same major version number, for example: `8.1.0` and `8.1.1` are compatible. `8.1.0` and `7.5.1` are not compatible. #### Restore the root directory[​](https://questdb.com/docs/operations/backup/#restore-the-root-directory "Direct link to Restore the root directory") When using cloud tools, create a new disk from the snapshot. The entire disk contents of the original database will be available when the compute instance starts. warning **AWS EBS lazy loading**: By default, EBS volumes created from snapshots load data lazily (on first access), which can cause slow reads after restore. To mitigate this: * **Option 1**: Enable [Fast Snapshot Restore (FSR)](https://docs.aws.amazon.com/ebs/latest/userguide/ebs-fast-snapshot-restore.html) on the snapshot before creating the volume * **Option 2**: Pre-warm the volume by reading all blocks after restore: sudo fio --filename=/dev/nvme1n1 --rw=read --bs=1M --iodepth=32 \ --ioengine=libaio --direct=1 --name=volume-initialize This issue may also affect other cloud providers with similar snapshot behavior. If you are not using cloud tools, you have to make sure that you restore the root from the backup using your own tools of choice! #### The trigger file[​](https://questdb.com/docs/operations/backup/#the-trigger-file "Direct link to The trigger file") When you are starting the database from the backup for the first time, the database must perform a restore procedure. This ensures the data is consistent and can be read and written. It only takes place on startup, and requires a specific blank file to exist as the indication of user intent. Touch the `_restore` file in the root directory. The following command will do the trick: touch /var/lib/questdb/_restore #### Start the database[​](https://questdb.com/docs/operations/backup/#start-the-database "Direct link to Start the database") Start the database using the root directory as usual. When the `_restore` file is present, the database will perform the restore procedure. There are two possible outcomes: * Restore is successful: the database continues to run normally and is ready to use; the `_restore` file is removed to prevent the same procedure running twice * Restore fails: the database exits and the `_restore` file remains in place. An error message appears in `stderr`. If it can be resolved, starting the database again will retry the restore procedure Further reading[​](https://questdb.com/docs/operations/backup/#further-reading "Direct link to Further reading") ----------------------------------------------------------------------------------------------------------------- * [`BACKUP` SQL reference](https://questdb.com/docs/query/sql/backup/) - Enterprise backup command syntax * [`CHECKPOINT` SQL reference](https://questdb.com/docs/query/sql/checkpoint/) - OSS checkpoint command syntax * [Overview](https://questdb.com/docs/operations/backup/#overview) * [QuestDB Enterprise: built-in backup and restore](https://questdb.com/docs/operations/backup/#questdb-enterprise-built-in-backup-and-restore) * [Prerequisites](https://questdb.com/docs/operations/backup/#prerequisites) * [Quick start](https://questdb.com/docs/operations/backup/#quick-start) * [Configure](https://questdb.com/docs/operations/backup/#configure) * [Run a backup](https://questdb.com/docs/operations/backup/#run-a-backup) * [Monitor and abort](https://questdb.com/docs/operations/backup/#monitor-and-abort) * [Scheduled backups](https://questdb.com/docs/operations/backup/#scheduled-backups) * [Backup instance name](https://questdb.com/docs/operations/backup/#backup-instance-name) * [Replication WAL cleanup integration](https://questdb.com/docs/operations/backup/#replication-wal-cleanup-integration) * [Performance characteristics](https://questdb.com/docs/operations/backup/#performance-characteristics) * [Estimate backup storage](https://questdb.com/docs/operations/backup/#estimate-backup-storage) * [Limitations](https://questdb.com/docs/operations/backup/#limitations) * [Backup validation](https://questdb.com/docs/operations/backup/#backup-validation) * [Restore](https://questdb.com/docs/operations/backup/#restore) * [Create a replica from a backup](https://questdb.com/docs/operations/backup/#create-a-replica-from-a-backup) * [Troubleshooting](https://questdb.com/docs/operations/backup/#troubleshooting) * [QuestDB OSS: manual backups with checkpoints](https://questdb.com/docs/operations/backup/#questdb-oss-manual-backups-with-checkpoints) * [Data backup checklist](https://questdb.com/docs/operations/backup/#data-backup-checklist) * [Steps in the backup procedure](https://questdb.com/docs/operations/backup/#steps-in-the-backup-procedure) * [Restore to a saved checkpoint](https://questdb.com/docs/operations/backup/#restore-to-a-saved-checkpoint) * [Further reading](https://questdb.com/docs/operations/backup/#further-reading) --- # Metrics View | QuestDB On this page The **Metrics View** provides real-time monitoring capabilities for your QuestDB instance. It displays interactive charts and widgets that help you track database performance, WAL operations, and table-specific metrics. ![Metrics View in the Web Console](https://questdb.com/docs/images/docs/console/metrics-view.webp) Prerequisites[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------------------- To use the Metrics View, you must enable telemetry on your QuestDB server: ### Server Configuration[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#server-configuration "Direct link to Server Configuration") Set the following in your `server.conf` file: telemetry.enabled=true ### Environment Variable[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#environment-variable "Direct link to Environment Variable") Alternatively, set the environment variable: QDB_TELEMETRY_ENABLED=true After making these changes, restart your QuestDB server to enable telemetry collection. Adding a Metrics Tab[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#adding-a-metrics-tab "Direct link to Adding a Metrics Tab") ------------------------------------------------------------------------------------------------------------------------------------------------------- Click the **"Add metrics"** button (chart icon) in the [Schema Explorer toolbar](https://questdb.com/docs/getting-started/web-console/schema-explorer/#toolbar) . A new metrics tab will automatically open with the default interface. info Metrics tabs are visually distinguished by their chart icon and blue color scheme in the tab bar. The "+" button in the tab bar creates new SQL editor tabs, not metrics tabs. Toolbar[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#toolbar "Direct link to Toolbar") ---------------------------------------------------------------------------------------------------------------- The Metrics View toolbar provides comprehensive controls for managing your monitoring experience. ![Metrics toolbar in the Web Console](https://questdb.com/docs/images/docs/console/metrics-toolbar.webp) * **Add Widget**: Opens a modal to select the type of metric for the widget * **Refresh All Widgets**: Manually refreshes all widgets to get the latest data * **Refresh Rate**: Choose automatic refresh intervals: * **Off**: No automatic refresh * **1s, 5s, 10s, 30s, 1m**: Fixed refresh intervals * **Auto**: Intelligent refresh rate based on selected time range * **Date/Time Picker**: Select custom time ranges for data analysis: * **Predefined ranges**: Last 5m, 15m, 1h, 3h, 6h, 12h, 24h, 3 days, 7 days * **Custom ranges**: Select specific start and end times * **View Mode Toggle**: Switch between Grid and List layouts Each widget in the Metrics View provides comprehensive customization options. ![Metrics widget in the Web Console](https://questdb.com/docs/images/docs/console/metric-widget.webp) * **Table Name**: Input field for selecting which table to monitor * **Color Customization**: Changes chart line colors for better visualization * **Interactive Charts**: Allows you to explore data by moving the mouse over the chart * **Remove Widget**: Deletes widgets that are no longer needed Available Metrics[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#available-metrics "Direct link to Available Metrics") ---------------------------------------------------------------------------------------------------------------------------------------------- The Metrics View supports several types of widgets, each providing specific insights: ### WAL Transaction Throughput (txn/s)[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#wal-transaction-throughput-txns "Direct link to WAL Transaction Throughput (txn/s)") This metric monitors the rate at which transactions are applied to tables. Performance is influenced by: * Batch merging efficiency (multiple transactions processed together) * Data ingestion rate from source * Storage performance and contention * Concurrent writes across multiple tables sharing resources Compare against data source metrics to distinguish between ingestion bottlenecks and system performance limitations. ### WAL Row Throughput (rows/s)[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#wal-row-throughput-rowss "Direct link to WAL Row Throughput (rows/s)") This metric displays rows processed per second during transaction merges. While similar to transaction throughput, this metric helps identify: * Data density variations within transactions * Processing overhead for row-heavy transactions * Resource utilization from row-level operations * Impact of row complexity on merge performance Use alongside transaction throughput to understand the relationship between transaction size and processing efficiency. ### WAL Transaction Latency (90th percentile)[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#wal-transaction-latency-90th-percentile "Direct link to WAL Transaction Latency (90th percentile)") This metric indicates the time required for data to become readable after being written. Higher latency may stem from: * Large transaction sizes (refer to Avg Transaction Size metric if elevated) * Unordered data requiring additional processing * Write amplification (see dedicated metric if batch size is optimal) * Storage I/O limitations or contention Monitor this metric alongside related charts to identify the root cause of performance variations and optimize accordingly. ### Table Write Amplification[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#table-write-amplification "Direct link to Table Write Amplification") This metric tracks the data write overhead during merge operations. Write amplification occurs when: * Copy-on-write operations affect large data blocks * Datasets are re-ingested for deduplication * Data requires extensive rewriting during merges Scale ranges from optimal (1x) to problematic (1000x+). High amplification typically indicates duplicate data ingestion or suboptimal data ordering patterns. ### Table Average Transaction Size (rows/txn)[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#table-average-transaction-size-rowstxn "Direct link to Table Average Transaction Size (rows/txn)") This metric tracks the mean size of transactions processed through the database API. While the database is optimized for both small and large transactions, larger batch sizes generally lead to better database performance. Monitor this metric to understand your API's transaction patterns and identify opportunities for batch size optimization. Key aspects to observe: * Transaction size trends and variations * Any unusually small transactions that could be batched * Consistency of batch sizes across time periods info Metrics View displays key metrics for quick monitoring in the Web Console. For comprehensive metrics and advanced monitoring capabilities, see [Prometheus monitoring and alerting](https://questdb.com/docs/integrations/other/prometheus/) . Best Practices[​](https://questdb.com/docs/getting-started/web-console/metrics-view/#best-practices "Direct link to Best Practices") ------------------------------------------------------------------------------------------------------------------------------------- * Limit the number of active widgets to maintain performance * Use appropriate time ranges (shorter ranges for real-time monitoring) * Remove unused widgets to reduce resource consumption * Historical data queries may transfer more data for longer time ranges This comprehensive monitoring capability helps you maintain optimal database performance and identify issues before they impact your applications. * [Prerequisites](https://questdb.com/docs/getting-started/web-console/metrics-view/#prerequisites) * [Server Configuration](https://questdb.com/docs/getting-started/web-console/metrics-view/#server-configuration) * [Environment Variable](https://questdb.com/docs/getting-started/web-console/metrics-view/#environment-variable) * [Adding a Metrics Tab](https://questdb.com/docs/getting-started/web-console/metrics-view/#adding-a-metrics-tab) * [Toolbar](https://questdb.com/docs/getting-started/web-console/metrics-view/#toolbar) * [Widget](https://questdb.com/docs/getting-started/web-console/metrics-view/#widget) * [Available Metrics](https://questdb.com/docs/getting-started/web-console/metrics-view/#available-metrics) * [WAL Transaction Throughput (txn/s)](https://questdb.com/docs/getting-started/web-console/metrics-view/#wal-transaction-throughput-txns) * [WAL Row Throughput (rows/s)](https://questdb.com/docs/getting-started/web-console/metrics-view/#wal-row-throughput-rowss) * [WAL Transaction Latency (90th percentile)](https://questdb.com/docs/getting-started/web-console/metrics-view/#wal-transaction-latency-90th-percentile) * [Table Write Amplification](https://questdb.com/docs/getting-started/web-console/metrics-view/#table-write-amplification) * [Table Average Transaction Size (rows/txn)](https://questdb.com/docs/getting-started/web-console/metrics-view/#table-average-transaction-size-rowstxn) * [Best Practices](https://questdb.com/docs/getting-started/web-console/metrics-view/#best-practices) --- # Node.js Client Documentation | QuestDB On this page QuestDB offers Node.js developers a dedicated client designed for efficient and high-performance data ingestion. The Node.js client has solid benefits: * **Automatic table creation**: No need to define your schema upfront. * **Concurrent schema changes**: Seamlessly handle multiple data streams with on-the-fly schema modifications * **Optimized batching**: Use strong defaults or curate the size of your batches * **Health checks and feedback**: Ensure your system's integrity with built-in health monitoring * **Automatic write retries**: Reuse connections and retry after interruptions This quick start guide introduces the basic functionalities of the Node.js client, including setting up a connection, inserting data, and flushing data to QuestDB. ![NodeJS](https://questdb.com/docs/images/logos/jsIconGreen.svg) [![Documentation icon](https://questdb.com/docs/images/icons/open-book.svg "Documentation")View full docs](https://questdb.github.io/nodejs-questdb-client) [![Github icon](https://questdb.com/docs/images/github.svg "Source")View source code](https://github.com/questdb/nodejs-questdb-client) info This page focuses on our high-performance ingestion client, which is optimized for **writing** data to QuestDB. For retrieving data, we recommend using a [PostgreSQL-compatible Node.js library](https://questdb.com/docs/query/pgwire/nodejs/) or our [HTTP query endpoint](https://questdb.com/docs/query/overview/#rest-http-api) . Requirements[​](https://questdb.com/docs/ingestion/clients/nodejs/#requirements "Direct link to Requirements") --------------------------------------------------------------------------------------------------------------- * Node.js v16 or newer. * Assumes QuestDB is running. If it's not, refer to [the general quick start](https://questdb.com/docs/getting-started/quick-start/) . Client installation[​](https://questdb.com/docs/ingestion/clients/nodejs/#client-installation "Direct link to Client installation") ------------------------------------------------------------------------------------------------------------------------------------ Install the QuestDB Node.js client via npm: npm i -s @questdb/nodejs-client Authentication[​](https://questdb.com/docs/ingestion/clients/nodejs/#authentication "Direct link to Authentication") --------------------------------------------------------------------------------------------------------------------- Passing in a configuration string with basic auth: const { Sender } = require("@questdb/nodejs-client");const conf = "http::addr=localhost:9000;username=admin;password=quest;"const sender = Sender.fromConfig(conf); ... Passing via the `QDB_CLIENT_CONF` env var: export QDB_CLIENT_CONF="http::addr=localhost:9000;username=admin;password=quest;" const { Sender } = require("@questdb/nodejs-client");const sender = Sender.fromEnv(); ... When using QuestDB Enterprise, authentication can also be done via REST token. Please check the [RBAC docs](https://questdb.com/docs/security/rbac/#authentication) for more info. Basic insert[​](https://questdb.com/docs/ingestion/clients/nodejs/#basic-insert "Direct link to Basic insert") --------------------------------------------------------------------------------------------------------------- Example: inserting executed trades for cryptocurrencies. Without authentication and using the current timestamp. const { Sender } = require("@questdb/nodejs-client")async function run() { // create a sender using HTTP protocol const sender = Sender.fromConfig("http::addr=localhost:9000") // add rows to the buffer of the sender await sender .table("trades") .symbol("symbol", "ETH-USD") .symbol("side", "sell") .floatColumn("price", 2615.54) .floatColumn("amount", 0.00044) .atNow() // flush the buffer of the sender, sending the data to QuestDB // the buffer is cleared after the data is sent, and the sender is ready to accept new data await sender.flush() // close the connection after all rows ingested // unflushed data will be lost await sender.close()}run().then(console.log).catch(console.error) In this case, the designated timestamp will be the one at execution time. Let's see now an example with an explicit timestamp, custom auto-flushing, and basic auth. const { Sender } = require("@questdb/nodejs-client")async function run() { // create a sender using HTTP protocol const sender = Sender.fromConfig( "http::addr=localhost:9000;username=admin;password=quest;auto_flush_rows=100;auto_flush_interval=1000;", ) // Calculate the current timestamp. You could also parse a date from your source data. const timestamp = Date.now() // add rows to the buffer of the sender await sender .table("trades") .symbol("symbol", "ETH-USD") .symbol("side", "sell") .floatColumn("price", 2615.54) .floatColumn("amount", 0.00044) .at(timestamp, "ms") // add rows to the buffer of the sender await sender .table("trades") .symbol("symbol", "BTC-USD") .symbol("side", "sell") .floatColumn("price", 39269.98) .floatColumn("amount", 0.001) .at(timestamp, "ms") // flush the buffer of the sender, sending the data to QuestDB // the buffer is cleared after the data is sent, and the sender is ready to accept new data await sender.flush() // close the connection after all rows ingested // unflushed data will be lost await sender.close()}run().then(console.log).catch(console.error) As you can see, both events now are using the same timestamp. We recommended to use the original event timestamps when ingesting data into QuestDB. Using the current timestamp hinder the ability to deduplicate rows which is [important for exactly-once processing](https://questdb.com/docs/ingestion/ilp/overview/#exactly-once-delivery-vs-at-least-once-delivery) . Decimal insertion[​](https://questdb.com/docs/ingestion/clients/nodejs/#decimal-insertion "Direct link to Decimal insertion") ------------------------------------------------------------------------------------------------------------------------------ note Decimal columns are available with ILP protocol version 3 (QuestDB v9.2.0+ and NodeJS client v4.2.0+). HTTP/HTTPS connections negotiate this automatically (`protocol_version=auto`), while TCP/TCPS connections must opt in explicitly (for example `tcp::...;protocol_version=3`). Once on v3, you can choose between the textual helper and the binary helper. caution QuestDB does not auto-create decimal columns. Define them ahead of ingestion with `DECIMAL(precision, scale)` so the server knows how many digits to store, as explained in the [decimal data type](https://questdb.com/docs/query/datatypes/decimal/#creating-tables-with-decimals) guide. ### Text literal (easy to use)[​](https://questdb.com/docs/ingestion/clients/nodejs/#text-literal-easy-to-use "Direct link to Text literal (easy to use)") import { Sender } from "@questdb/nodejs-client";async function runDecimalsText() { const sender = await Sender.fromConfig( "tcp::addr=localhost:9009;protocol_version=3", ); await sender .table("fx") .symbol("pair", "EURUSD") .decimalColumnText("mid", "1.234500") // keeps trailing zeros .atNow(); await sender.flush(); await sender.close();} `decimalColumnText` accepts strings or numbers. String literals go through `validateDecimalText` and are written verbatim with the `d` suffix, so every digit (including trailing zeros or exponent form) is preserved. Passing a number is convenient, but JavaScript’s default formatting will drop insignificant zeros. ### Binary form (high throughput)[​](https://questdb.com/docs/ingestion/clients/nodejs/#binary-form-high-throughput "Direct link to Binary form (high throughput)") const sender = await Sender.fromConfig( "tcp::addr=localhost:9009;protocol_version=3",);const scale = 4;const notional = 12345678901234567890n; // represents 1_234_567_890_123_456.7890await sender .table("positions") .symbol("desk", "ny") .decimalColumnUnscaled("notional", notional, scale) .atNow();await sender.flush();await sender.close(); `decimalColumnUnscaled` converts `BigInt` inputs into the ILP v3 binary payload. You can also pass an `Int8Array` if you already have a two’s-complement, big-endian byte array. The scale must stay between 0 and 76, and payloads wider than 32 bytes are rejected up front. This binary path keeps rows compact, making it the preferred option for high-performance feeds. Configuration options[​](https://questdb.com/docs/ingestion/clients/nodejs/#configuration-options "Direct link to Configuration options") ------------------------------------------------------------------------------------------------------------------------------------------ The minimal configuration string needs to have the protocol, host, and port, as in: http::addr=localhost:9000; For all the extra options you can use, please check [the client docs](https://questdb.github.io/nodejs-questdb-client/classes/SenderOptions.html) Alternatively, for a breakdown of Configuration string options available across all clients, see the [Configuration string](https://questdb.com/docs/ingestion/clients/configuration-string/) page. Next Steps[​](https://questdb.com/docs/ingestion/clients/nodejs/#next-steps "Direct link to Next Steps") --------------------------------------------------------------------------------------------------------- Please refer to the [ILP overview](https://questdb.com/docs/ingestion/ilp/overview/) for details about transactions, error control, delivery guarantees, health check, or table and column auto-creation. Dive deeper into the Node.js client capabilities, including TypeScript and Worker Threads examples, by exploring the [GitHub repository](https://github.com/questdb/nodejs-questdb-client) . To learn _The Way_ of QuestDB SQL, see the [Query & SQL Overview](https://questdb.com/docs/query/overview/) . Should you encounter any issues or have questions, the [Community Forum](https://community.questdb.com/) is a vibrant platform for discussions. * [Requirements](https://questdb.com/docs/ingestion/clients/nodejs/#requirements) * [Client installation](https://questdb.com/docs/ingestion/clients/nodejs/#client-installation) * [Authentication](https://questdb.com/docs/ingestion/clients/nodejs/#authentication) * [Basic insert](https://questdb.com/docs/ingestion/clients/nodejs/#basic-insert) * [Decimal insertion](https://questdb.com/docs/ingestion/clients/nodejs/#decimal-insertion) * [Text literal (easy to use)](https://questdb.com/docs/ingestion/clients/nodejs/#text-literal-easy-to-use) * [Binary form (high throughput)](https://questdb.com/docs/ingestion/clients/nodejs/#binary-form-high-throughput) * [Configuration options](https://questdb.com/docs/ingestion/clients/nodejs/#configuration-options) * [Next Steps](https://questdb.com/docs/ingestion/clients/nodejs/#next-steps) --- # PowerBI | QuestDB On this page This guide demonstrates how to connect QuestDB with Microsoft PowerBI to create interactive data visualizations and dashboards. Prerequisites[​](https://questdb.com/docs/integrations/visualization/powerbi/#prerequisites "Direct link to Prerequisites") ---------------------------------------------------------------------------------------------------------------------------- * [QuestDB](https://questdb.com/docs/getting-started/quick-start/) running locally or remotely * [PowerBI Desktop](https://powerbi.microsoft.com/) installed Connection Setup[​](https://questdb.com/docs/integrations/visualization/powerbi/#connection-setup "Direct link to Connection Setup") ------------------------------------------------------------------------------------------------------------------------------------- QuestDB utilizes a fully featured PostgreSQL Wire Protocol (PGWire). As such, setup for PowerBI mirrors the standard PostgreSQL connection setup. The benefit is the performance profile of QuestDB, and its powerful time-series SQL extensions, with the simplicity of the PGWire protocol. 1. Open PowerBI Desktop 2. Click "Get Data" in the Home tab ![Select Get Data](https://questdb.com/docs/images/docs/powerbi/powerbi-1.webp) 3. Select "Database" → "PostgreSQL" ![Select PostgreSQL](https://questdb.com/docs/images/docs/powerbi/powerbi-2.webp) 4. Enter your QuestDB connection details: * Server: `localhost` (or your server address) * Database: `qdb` * Data Connectivity mode: `Import` * Advanced options (optional): * Port: `8812` (default QuestDB PGWire port) * Command timeout: Adjust based on your query complexity 5. Select: * Database authentication: * User: `admin` * Password: `quest` 6. Click "Connect" Working with Data[​](https://questdb.com/docs/integrations/visualization/powerbi/#working-with-data "Direct link to Working with Data") ---------------------------------------------------------------------------------------------------------------------------------------- 1. In the Navigator window, select the tables you want to analyze 2. Click "Transform Data" to modify the data or "Load" to import it directly 3. Create visualizations by dragging fields onto the report canvas 4. Save your report and publish it to PowerBI Service if needed Using Custom SQL[​](https://questdb.com/docs/integrations/visualization/powerbi/#using-custom-sql "Direct link to Using Custom SQL") ------------------------------------------------------------------------------------------------------------------------------------- To leverage QuestDB-specific features like `SAMPLE BY` and `LATEST ON`, you can use custom SQL: 1. In the "Get Data" dialog, click "Advanced options" 2. Enter your SQL query in the "SQL statement" field 3. Click "OK" to execute > Remember, you must include a timestamp column when using functions like `SAMPLE BY`. Here are some useful query examples: -- Get 1-hour samples of trade pricesSELECT timestamp, avg(price) as avg_price, sum(amount) as volumeFROM tradesWHERE timestamp >= dateadd('d', -7, now())SAMPLE BY 1h;-- Get latest trade for each symbolSELECT * FROM tradesLATEST ON timestamp PARTITION BY symbol;-- Combine SAMPLE BY with multiple aggregationsSELECT timestamp, symbol, max(price) max_price, min(price) min_price, avg(price) avg_priceFROM tradesWHERE timestamp >= dateadd('M', -1, now())SAMPLE BY 1dALIGN TO CALENDAR; Best Practices[​](https://questdb.com/docs/integrations/visualization/powerbi/#best-practices "Direct link to Best Practices") ------------------------------------------------------------------------------------------------------------------------------- * Leverage [timestamps](https://questdb.com/docs/concepts/timestamps-timezones/) functions for time-series analysis * Explore various [aggregation functions](https://questdb.com/docs/query/functions/aggregation/) to suit your data needs * Consider using powerful [window functions](https://questdb.com/docs/query/functions/window-functions/overview/) to perform complex calculations * For large datasets, use incremental refresh in PowerBI Caveats[​](https://questdb.com/docs/integrations/visualization/powerbi/#caveats "Direct link to Caveats") ---------------------------------------------------------------------------------------------------------- ### Date Table Limitations[​](https://questdb.com/docs/integrations/visualization/powerbi/#date-table-limitations "Direct link to Date Table Limitations") QuestDB currently cannot be used as a source for PowerBI's "Mark as Date Table" feature. This means: * You cannot mark QuestDB tables as date tables in PowerBI * Some time intelligence functions in PowerBI may not be available * If you need date table functionality, consider creating it in PowerBI or using another data source tip If you'd like QuestDB to support this feature, please add a 👍 to [this GitHub issue](https://github.com/questdb/questdb/issues/5208) . Troubleshooting[​](https://questdb.com/docs/integrations/visualization/powerbi/#troubleshooting "Direct link to Troubleshooting") ---------------------------------------------------------------------------------------------------------------------------------- * If connection fails, verify your QuestDB instance is running and accessible * Ensure PGWire is enabled in your QuestDB configuration * `pg.enabled=true` - see [configuration](https://questdb.com/docs/configuration/overview/) for more details * Check that the port `8812` is open and not blocked by firewalls * For timeout errors, adjust the command timeout in advanced options Further Reading[​](https://questdb.com/docs/integrations/visualization/powerbi/#further-reading "Direct link to Further Reading") ---------------------------------------------------------------------------------------------------------------------------------- * [QuestDB PGWire](https://questdb.com/docs/query/pgwire/overview/) * [PowerBI Documentation](https://docs.microsoft.com/en-us/power-bi/) * [Prerequisites](https://questdb.com/docs/integrations/visualization/powerbi/#prerequisites) * [Connection Setup](https://questdb.com/docs/integrations/visualization/powerbi/#connection-setup) * [Working with Data](https://questdb.com/docs/integrations/visualization/powerbi/#working-with-data) * [Using Custom SQL](https://questdb.com/docs/integrations/visualization/powerbi/#using-custom-sql) * [Best Practices](https://questdb.com/docs/integrations/visualization/powerbi/#best-practices) * [Caveats](https://questdb.com/docs/integrations/visualization/powerbi/#caveats) * [Date Table Limitations](https://questdb.com/docs/integrations/visualization/powerbi/#date-table-limitations) * [Troubleshooting](https://questdb.com/docs/integrations/visualization/powerbi/#troubleshooting) * [Further Reading](https://questdb.com/docs/integrations/visualization/powerbi/#further-reading) --- # Profiling | QuestDB On this page Profiling lets you see what's happening inside the database at the code level - which functions are consuming CPU time, where memory is being allocated, and what's blocking threads. This is an advanced diagnostic technique. Most users will never need it; query plans, metrics, and logs are usually sufficient for understanding performance. However, when you're facing issues that can't be explained by the usual tools - unexplained CPU spikes, mysterious latency, or behavior that doesn't match what metrics suggest - profiling reveals the internal picture. QuestDB embeds [async-profiler](https://github.com/async-profiler/async-profiler) in the Linux x86\_64 distribution, with convenience commands built into `questdb.sh`. There are two profiling approaches: * **Ad-hoc profiling** produces a single flame graph for a specific time window. Use this when you can reproduce an issue on demand - start profiling, trigger the problem, stop profiling, and analyze the result. * **Continuous profiling** records to JFR files in the background, which can later be converted to heatmaps. Heatmaps show activity over time, letting you spot anomalies and zoom into specific moments to generate flame graphs. Use this when problems occur unpredictably - the profiler is always running, so you can investigate after the fact. This page covers: * [Ad-hoc profiling](https://questdb.com/docs/troubleshooting/profiling/#attach-to-a-running-instance) - Attach to a running instance and capture a flame graph * [Continuous profiling](https://questdb.com/docs/troubleshooting/profiling/#continuous-profiling) - Run the profiler in the background for later analysis ![Architecture of the file storing a column](https://questdb.com/docs/images/docs/concepts/heatmap.webp) Heatmap showing CPU usage over time with flame graph Prerequisites[​](https://questdb.com/docs/troubleshooting/profiling/#prerequisites "Direct link to Prerequisites") ------------------------------------------------------------------------------------------------------------------- Profiling requires [async-profiler](https://github.com/async-profiler/async-profiler) . QuestDB ships with async-profiler and the `jfrconv` converter bundled in the **Linux x86\_64** distribution only. For other platforms, you must install async-profiler separately. ### Linux kernel settings[​](https://questdb.com/docs/troubleshooting/profiling/#linux-kernel-settings "Direct link to Linux kernel settings") Profiling works without any kernel configuration changes, but for best accuracy on Linux, configure the following kernel parameters: # Allow unrestricted access to perf eventssudo sysctl kernel.perf_event_paranoid=-1# Expose kernel symbols for complete stack tracessudo sysctl kernel.kptr_restrict=0 To make these settings permanent, add them to `/etc/sysctl.conf` or create a file in `/etc/sysctl.d/`: # /etc/sysctl.d/99-profiling.confkernel.perf_event_paranoid=-1kernel.kptr_restrict=0 | Setting | Recommended Value | Description | | --- | --- | --- | | `perf_event_paranoid` | `-1` | Controls access to performance events. Value `-1` allows unrestricted access to perf events, providing the most accurate profiling results. | | `kptr_restrict` | `0` | Controls kernel pointer visibility. Value `0` exposes kernel symbols, enabling complete stack traces including kernel frames. | Without these settings, profiling still works but may have reduced accuracy. warning These settings have security implications as they expose performance counters and kernel addresses. On production systems, consider enabling them only during profiling sessions, or use more restrictive values based on your security requirements. See the [Linux kernel perf security documentation](https://www.kernel.org/doc/html/v6.0/admin-guide/perf-security.html) for details. Attach to a running instance[​](https://questdb.com/docs/troubleshooting/profiling/#attach-to-a-running-instance "Direct link to Attach to a running instance") ---------------------------------------------------------------------------------------------------------------------------------------------------------------- Use the `profile` command to attach async-profiler to an already running QuestDB instance. This mode is useful for ad-hoc profiling of production systems without requiring a restart. ### Syntax[​](https://questdb.com/docs/troubleshooting/profiling/#syntax "Direct link to Syntax") ./questdb.sh profile [-t tag] -- [profiler-args] | Option | Description | | --- | --- | | `-t` | Process tag to identify which QuestDB instance to profile. Required only when profiling an instance started with a custom `-t` tag. | | `--` | Separator between questdb.sh options and async-profiler arguments. | All arguments after `--` are passed directly to the `asprof` command-line tool. ### Examples[​](https://questdb.com/docs/troubleshooting/profiling/#examples "Direct link to Examples") Profile CPU usage for 30 seconds and generate an HTML flame graph: ./questdb.sh profile -- -e cpu -d 30 -f /tmp/cpu-profile.html Profile memory allocations: ./questdb.sh profile -- -e alloc -d 60 -f /tmp/alloc-profile.html Profile lock contention: ./questdb.sh profile -- -e lock -d 30 -f /tmp/lock-profile.html Generate a JFR (Java Flight Recorder) file instead of HTML: ./questdb.sh profile -- -e cpu -d 60 -f /tmp/profile.jfr Profile a specific tagged instance: ./questdb.sh profile -t mydb -- -e cpu -d 30 -f /tmp/profile.html ### Common profiler arguments[​](https://questdb.com/docs/troubleshooting/profiling/#common-profiler-arguments "Direct link to Common profiler arguments") | Argument | Description | | --- | --- | | `-e ` | Event to profile: `cpu`, `alloc`, `lock`, `wall`, `itimer`, etc. | | `-d ` | Duration of profiling in seconds. | | `-f ` | Output file. Extension determines format: `.html` for flame graph, `.jfr` for JFR, `.svg` for SVG. | | `-i ` | Sampling interval (e.g., `10ms`, `1us`). | | `-t` | Profile threads separately. Each stack trace will end with a frame that denotes a single thread. (Note: this is asprof's `-t`, distinct from the questdb.sh `-t` tag option used before `--`.) | | `--all-user` | Include only user-mode events. | For a complete list of options, see the [async-profiler documentation](https://github.com/async-profiler/async-profiler) . Continuous profiling[​](https://questdb.com/docs/troubleshooting/profiling/#continuous-profiling "Direct link to Continuous profiling") ---------------------------------------------------------------------------------------------------------------------------------------- ### Overview[​](https://questdb.com/docs/troubleshooting/profiling/#overview "Direct link to Overview") Use the `-p` flag with the `start` command to run the profiler continuously in the background. This is valuable when you don't know when a problem will occur - the profiler is always recording, so you can analyze what happened after the fact. Continuous profiling helps catch rare events that are difficult to reproduce and reveals patterns and trends over time. Profile data is written to JFR files in the `/profiles` directory (e.g., `~/.questdb/profiles/`). These can later be converted to heatmaps. Heatmaps show samples over time, letting you spot anomalies and then zoom into a specific time window to generate a flame graph for just that period. ### Default configuration[​](https://questdb.com/docs/troubleshooting/profiling/#default-configuration "Direct link to Default configuration") When you run `./questdb.sh start -p` without additional parameters, the profiler uses these defaults: | Setting | Default | Description | | --- | --- | --- | | Events | `cpu,wall` | Profiles both CPU time and wall-clock time simultaneously | | Interval | `5ms` | Sampling interval for CPU and wall-clock profiling | | Allocation interval | `512k` | Sample every 512 KB of allocations (when `alloc` event is enabled) | | Lock threshold | `10ms` | Sample locks held longer than 10ms (when `lock` event is enabled) | | Loop duration | `30m` | Start a new JFR file every 30 minutes | | Output directory | `/profiles` | Profile files are written here | | File name pattern | `profile-%n{48}.jfr` | Sequence number up to 48, then wraps around | With the default 30-minute loop and sequence limit of 48, the profiler keeps up to 24 hours of data before overwriting. JFR file sizes depend on workload activity - expect roughly 10-50 MB per 30-minute file under typical load. Monitor disk usage if running continuously in production. Override defaults via environment variables before starting QuestDB: export PROFILER_EVENT="cpu" # Profile CPU onlyexport PROFILER_INTERVAL="10ms" # Less frequent samplingexport PROFILER_LOOP="1h" # New file every hour./questdb.sh start -p If you pass custom agent parameters after `--`, they replace the environment variable defaults entirely. ### Syntax[​](https://questdb.com/docs/troubleshooting/profiling/#syntax-1 "Direct link to Syntax") ./questdb.sh start -p [-d dir] [-f] [-n] [-t tag] [-- agent-params] | Option | Description | | --- | --- | | `-p` | Enable async-profiler agent at startup. | | `-d` | QuestDB root directory. | | `-f` | Force overwrite of the public (Web Console) directory. | | `-n` | Disable HUP signal handler (keeps QuestDB running after terminal closes). | | `-t` | Process tag for identification. | | `--` | Separator between questdb.sh options and JVM agent parameters. | Arguments after `--` are passed as JVM agent parameters to async-profiler. ### Examples[​](https://questdb.com/docs/troubleshooting/profiling/#examples-1 "Direct link to Examples") Start with default settings (profiles `cpu,wall` events, writes to `/profiles/`): ./questdb.sh start -p Start with custom parameters (overrides all defaults): ./questdb.sh start -p -- start,event=cpu,file=/tmp/profile.jfr,interval=10ms Start with wall-clock profiling at a custom interval: ./questdb.sh start -p -- start,event=wall,file=/tmp/wall.jfr,interval=20ms ### Agent parameters[​](https://questdb.com/docs/troubleshooting/profiling/#agent-parameters "Direct link to Agent parameters") When using continuous profiling, parameters are passed in a comma-separated format: | Parameter | Description | | --- | --- | | `start` | Begin profiling immediately on JVM startup. | | `event=` | Event type to profile: `cpu`, `alloc`, `lock`, `wall`, etc. | | `file=` | Output file path. | | `interval=