<!-- BusyBot node reference — https://busybot.net/tools/questdb/ -->

> Node: QuestDB (`questdb`) · Action · v1
> Category: Data & Storage · Credentials: QuestDB (`questDbApi`)
> Updated: 2026-08-16

# QuestDB

> Execute SQL queries and insert data into QuestDB time-series databases.

## Overview

QuestDB time-series database tool. Supports two operations: Execute Query (run arbitrary SQL with parameterized `$1`, `$2` placeholders and optional query parameters mapped from input item properties) and Insert (add rows from input item properties into a table). QuestDB is reached over its PostgreSQL wire protocol (PGWire), which listens on port 8812 by default. QuestDB is append-only — it does **not** support UPDATE or DELETE operations, and it does not support the `RETURNING` clause, so a post-insert SELECT is used to return table data after inserting. Values are always sent as bound parameters. QuestDB has specialized time-series SQL extensions including `SAMPLE BY`, `LATEST ON`, and designated timestamp columns.

**Category:** Data & Storage  
**Tool Name:** `questdb`  
**Version:** 1

**Appearance:** Icon: `lucide-Database` | Color: `#DA4855`

## Node Type

**Action** — processes input items and produces output

## Input / Output

| Direction | Port(s) |
|-----------|--------|
| Input | `Input` |
| Output | `Output`, `Error` |

## Credentials

This tool requires **QuestDB** credentials.
See the [Credentials Guide](https://busybot.net/credentials/quest-db-api/) for setup instructions.

### Operations

| Operation | Value | Description |
|-----------|-------|-------------|
| Execute Query | `executeQuery` | Execute a SQL query |
| Insert | `insert` | Insert rows in database |

### Parameters

#### Execute Query (`executeQuery`)

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| Query | `string` | Yes | — | The SQL query to execute. Use $1, $2, etc. for parameterized values mapped via Query Parameters. QuestDB supports time-series extensions like SAMPLE BY, LATEST ON, and designated timestamp filtering. |
| Additional Fields | `collection` | No | `{}` | Additional configuration for query execution. |
| — Mode | `options` | No | `independently` | The way queries should be sent to the database. Note: QuestDB has limited transaction support; "independently" is recommended. |
| | | | | Options: `independently` (execute each query independently), `transaction` (execute all queries in a single transaction — limited QuestDB support) |
| — Query Parameters | `string` | No | — | Comma-separated list of property names from the input item to use as positional query parameters ($1, $2, etc.). |

#### Insert (`insert`)

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| Table | `string` | Yes | — | Name of the table to insert data into. |
| Columns | `string` | No | — | Comma-separated list of column names from the input item to use for insertion. Supports type casting syntax (e.g., "count:int,name:text"). Leave empty to use all input item properties. Supports expressions like {{ $json.columnList }}. |
| Return Fields | `string` | No | `*` | Comma-separated list of columns to return after insertion via a post-insert SELECT. Use "*" for all columns. Note: QuestDB does not support RETURNING, so all rows from the table are returned. |

#### All Operations

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| Max Concurrency | `number` | No | `10` | Maximum number of items to process concurrently. |

## Output Data

Returned rows replace the item. Each row a statement returns becomes its own output item, and the row's columns *are* that item's JSON — the input item's JSON does not pass through. Binary data on the input item is forwarded onto every output item.

The item count is the thing to plan for: a query that returns 200 rows turns one input item into 200 output items.

| Operation | Output items |
|-----------|--------------|
| `executeQuery` | One item per returned row, holding that row's columns. A statement that returns no rows (a DDL statement, or a SELECT with no matches) produces one item carrying the input item's JSON plus `_queryResult: []` |
| `insert` | One item per row returned by the post-insert SELECT — that read is not filtered to the row you just inserted, so a table with 10,000 rows produces 10,000 output items for every input item |

## Usage Examples

- Execute a SQL query against QuestDB with parameterized placeholders
- Insert time-series data rows into a QuestDB table from input item properties
- Query sensor data with SAMPLE BY to aggregate by time interval
- Use LATEST ON to get the most recent value per sensor
- Select data from QuestDB with time range filters

## Example Configuration

Run a query whose two placeholders are filled from properties on the input item:

```json
{
  "type": "questdb",
  "parameters": {
    "operation": "executeQuery",
    "query": "SELECT * FROM sensor_data WHERE timestamp > $1 AND device_id = $2",
    "additionalFields": {
      "mode": "independently",
      "queryParams": "since,deviceId"
    },
    "maxConcurrency": 5
  }
}
```

Insert a row built from named properties of each input item:

```json
{
  "type": "questdb",
  "parameters": {
    "operation": "insert",
    "table": "sensor_readings",
    "columns": "device_id,temperature,timestamp",
    "maxConcurrency": 10
  }
}
```

Insert rows and read back only the columns you care about:

```json
{
  "type": "questdb",
  "parameters": {
    "operation": "insert",
    "table": "measurements",
    "columns": "sensor_id,value,recorded_at",
    "returnFields": "id,sensor_id,value,recorded_at",
    "maxConcurrency": 15
  }
}
```

Aggregate a time series into hourly buckets with `SAMPLE BY`:

```json
{
  "type": "questdb",
  "parameters": {
    "operation": "executeQuery",
    "query": "SELECT timestamp, avg(temperature) FROM weather_data WHERE timestamp > $1 SAMPLE BY 1h",
    "additionalFields": {
      "mode": "independently",
      "queryParams": "since"
    }
  }
}
```

Bulk-load events and return the whole row for each:

```json
{
  "type": "questdb",
  "parameters": {
    "operation": "insert",
    "table": "iot_events",
    "columns": "device_id,event_type,value,created_at",
    "returnFields": "*"
  }
}
```

Fetch the most recent row per symbol with `LATEST ON`:

```json
{
  "type": "questdb",
  "parameters": {
    "operation": "executeQuery",
    "query": "SELECT * FROM stock_prices LATEST ON timestamp PARTITION BY symbol WHERE symbol = $1",
    "additionalFields": {
      "mode": "independently",
      "queryParams": "symbol"
    },
    "maxConcurrency": 1
  }
}
```

### Error Handling

| Mode | Behavior |
|------|----------|
| **stop** | Halts workflow on first error |
| **continue** | Skips failed items, passes successful ones through |
| **errorPort** | Routes failed items to Error output port |

## Tips

Execute SQL queries or insert data into a QuestDB time-series database using PostgreSQL wire protocol.

### Behavior notes

- **Append-only database.** QuestDB has no UPDATE or DELETE, which is why this node offers only Execute Query and Insert. Correct data by writing new rows rather than editing old ones.
- **Insert reads the table back.** Because QuestDB cannot return the inserted row, the node follows every insert with a `SELECT` of Return Fields from the whole table, unfiltered. That read decides how many items leave the node — narrow Return Fields to the columns you need and expect the fan-out, or use Execute Query when you do not need a read-back at all.
- **Query Parameters are property names, not values.** `quantity,price` binds `$1` to the input item's `quantity` and `$2` to its `price`. A property that is missing from the item binds as null, so a query that silently returns nothing is usually a misspelled property name.
- **Values are bound; names are not.** The `$1`, `$2` values and the column values taken from the input item are sent as bind parameters. Table and column names are written into the statement as quoted identifiers, so do not build them out of untrusted input. Everything you type in Query runs exactly as written — nothing is substituted into it.
- **Column selection.** Only properties that actually exist on the input item are inserted; if none of the named columns are present, the item fails. Leave Columns empty to insert every property on the item except those whose names begin with `_`.