Examples¶
Getting started¶
connect() quick start¶
The following example leases a pooled sender from a
QuestDB handle, writes a row over QWP/WebSocket,
waits for the server acknowledgement, then runs a bind-parameter query into
pandas.
import sys
import questdb
from questdb import QuestDBError, TimestampNanos
def example(host: str = 'localhost', port: int = 9000):
try:
conf = f'ws::addr={host}:{port};'
with questdb.connect(conf) as db:
# Lease a pooled sender for row-by-row ingestion.
# Take one lease per thread; leaving the `with` block
# returns it to the pool.
with db.sender() as sender:
sender.row(
'trades',
symbols={
'symbol': 'ETH-USD',
'side': 'sell'},
columns={
'price': 2615.54,
'amount': 0.00044},
at=TimestampNanos.now())
# Pooled rows auto-publish at the configured thresholds.
# `wait=True` also publishes any partial batch, then blocks
# until the server acknowledges everything on this lease.
sender.flush(wait=True)
# Query with positional bind parameters ($1..$N) and read the
# result straight into pandas. Fully draining the result
# returns its connection to the pool.
frame = db.query(
'SELECT symbol, price, amount FROM trades '
'WHERE symbol = $1 LIMIT 5',
['ETH-USD'],
).to_pandas()
print(frame)
except QuestDBError as e:
sys.stderr.write(
f'Got error: {e} (code={e.code}, in_doubt={e.in_doubt})\n')
if __name__ == '__main__':
example()
Queries¶
The following example runs DDL through execute(), binds positional
parameters, streams a large result batch by batch, and leases one reader
connection for a run of queries.
import sys
import questdb
from questdb import QuestDBError
def example(host: str = 'localhost', port: int = 9000):
try:
with questdb.connect(f'ws::addr={host}:{port};') as db:
# execute() runs DDL and DML statements: it drains the
# (empty) result and returns the pooled connection in one
# call.
db.execute(
'CREATE TABLE IF NOT EXISTS trades ('
' timestamp TIMESTAMP, symbol SYMBOL,'
' price DOUBLE, amount DOUBLE'
') TIMESTAMP(timestamp) PARTITION BY DAY WAL')
# Bind positional parameters to $1..$N placeholders instead of
# interpolating values into the SQL.
frame = db.query(
'SELECT timestamp, price, amount FROM trades '
'WHERE symbol = $1 AND price > $2 LIMIT 10',
['ETH-USD', 1000.0],
).to_pandas()
print(frame)
# Stream a large result batch by batch instead of
# materializing all of it; fully draining the iterator
# returns the connection to the pool.
total = 0
with db.query('SELECT * FROM trades') as result:
for batch in result.iter_pandas():
total += len(batch)
print('rows streamed:', total)
# Lease one reader connection for a run of queries.
with db.reader() as r:
latest = r.query(
'SELECT * FROM trades LIMIT -5').to_pandas()
per_symbol = r.query(
'SELECT symbol, count() FROM trades').to_pandas()
print(latest)
print(per_symbol)
except QuestDBError as e:
sys.stderr.write(
f'Got error: {e} (code={e.code}, in_doubt={e.in_doubt})\n')
if __name__ == '__main__':
example()
DataFrame bulk load and reader leases¶
The following example bulk-loads a pandas DataFrame through
QuestDB.dataframe, then leases one reader
connection with db.reader() and runs two queries on it.
import sys
import pandas as pd
import questdb
from questdb import QuestDBError
def example(host: str = 'localhost', port: int = 9000):
df = pd.DataFrame({
'symbol': pd.Categorical(['ETH-USD', 'BTC-USD']),
'side': pd.Categorical(['sell', 'sell']),
'price': [2615.54, 39269.98],
'amount': [0.00044, 0.001],
'timestamp': pd.to_datetime(
['2021-01-01T00:00:00Z', '2021-01-01T00:00:01Z'])})
try:
conf = f'ws::addr={host}:{port};'
with questdb.connect(conf) as db:
# Bulk-load the whole frame over the direct columnar path.
# Categorical columns become SYMBOL automatically
# (symbols='auto'); the call blocks until the server
# acknowledges the final batch.
db.dataframe(df, table_name='trades', at='timestamp')
# Lease one reader connection and run queries on it
# sequentially.
with db.reader() as q:
recent = q.query('SELECT * FROM trades LIMIT 5').to_pandas()
print(recent)
by_symbol = q.query(
'SELECT symbol, count() FROM trades'
).to_pandas()
print(by_symbol)
except QuestDBError as e:
sys.stderr.write(
f'Got error: {e} (code={e.code}, in_doubt={e.in_doubt})\n')
if __name__ == '__main__':
example()
Ticking data and flush cadence¶
The following example mimics an application loop producing rows at random intervals. It disables pooled auto-flush and publishes on an explicit row-count / elapsed-time cadence.
import random
import sys
import time
import uuid
import questdb
from questdb import QuestDBError, TimestampNanos
FLUSH_ROWS = 100 # publish once this many rows are buffered ...
FLUSH_INTERVAL = 5.0 # ... or once this many seconds have passed
def example(host: str = 'localhost', port: int = 9000, total_rows=None):
table_name = str(uuid.uuid1())
try:
with questdb.connect(
f'ws::addr={host}:{port};auto_flush=off;') as db:
with db.sender() as sender:
# Auto-flush is disabled above so this example can implement
# its own row-count / elapsed-time cadence.
sent = 0
last_flush = time.monotonic()
print('Ctrl^C to terminate...')
while total_rows is None or sent < total_rows:
time.sleep(random.randint(0, 750) / 1000)
sender.row(
table_name,
symbols={
'src': random.choice(('ALPHA', 'BETA', 'OMEGA')),
'dst': random.choice(('ALPHA', 'BETA', 'OMEGA'))},
columns={
'price': random.randint(200, 500),
'qty': random.randint(1, 5)},
at=TimestampNanos.now())
sent += 1
if (len(sender) >= FLUSH_ROWS or
time.monotonic() - last_flush >= FLUSH_INTERVAL):
print(f'Flushing {len(sender)} rows...')
sender.flush()
last_flush = time.monotonic()
# Closing the lease publishes any remaining rows.
print(f'table: {table_name}, total rows sent: {sent}')
except KeyboardInterrupt:
print('bye!')
except QuestDBError as e:
sys.stderr.write(
f'Got error: {e} (code={e.code}, in_doubt={e.in_doubt})\n')
if __name__ == '__main__':
# Bounded run so the example terminates; pass total_rows=None to run
# until Ctrl^C, as a real ticking-data loop would.
example(total_rows=25)
Data Frames¶
Pandas Basics¶
The following example shows how to insert data from a Pandas DataFrame to the
'trades' table and run a generated query into Pandas.
import questdb
from questdb import QuestDBError
import sys
import pandas as pd
def example(host: str = 'localhost', port: int = 9000):
df = pd.DataFrame({
'symbol': pd.Categorical(['ETH-USD', 'BTC-USD']),
'side': pd.Categorical(['sell', 'sell']),
'price': [2615.54, 39269.98],
'amount': [0.00044, 0.001],
'timestamp': pd.to_datetime(['2021-01-01', '2021-01-02'])})
try:
with questdb.connect(f"ws::addr={host}:{port};") as db:
# Ingress: publish a Pandas DataFrame into QuestDB.
db.dataframe(
df,
table_name='trades', # Table name to insert into.
symbols=['symbol', 'side'], # Columns to be inserted as SYMBOL types.
at='timestamp') # Column containing the designated timestamps.
# Egress: query QuestDB and materialise the result as Pandas.
with db.query(
"SELECT x AS trade_id, "
"x * 10.0 AS price "
"FROM long_sequence(3)") as result:
queried = result.to_pandas()
print(queried)
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
if __name__ == '__main__':
example()
For details on all options, see the
QuestDB.dataframe method.
pd.Categorical and multiple tables¶
The next example shows some more advanced features inserting data from Pandas and running a generated query into Pandas.
The data is sent to multiple tables: the columnar path loads one table per call, so the frame is split by its table-naming
pd.Categoricalcolumn and each group is bulk-loaded with its ownQuestDB.dataframecall.Columns of type
pd.Categoricalare sent asSYMBOLtypes.
import questdb
from questdb import QuestDBError
import sys
import pandas as pd
def example(host: str = 'localhost', port: int = 9000):
df = pd.DataFrame({
'metric': pd.Categorical(
['humidity', 'temp_c', 'voc_index', 'temp_c']),
'sensor': pd.Categorical(
['paris-01', 'london-02', 'london-01', 'paris-01']),
'value': [
0.83, 22.62, 100.0, 23.62],
'ts': [
pd.Timestamp('2022-08-06 07:35:23.189062'),
pd.Timestamp('2022-08-06 07:35:23.189062'),
pd.Timestamp('2022-08-06 07:35:23.189062'),
pd.Timestamp('2022-08-06 07:35:23.189062')]})
try:
with questdb.connect(f"ws::addr={host}:{port};") as db:
# Ingress: one DataFrame destined for multiple tables.
# The columnar path loads one table per call, so split by the
# table-naming column and bulk-load each group.
for metric, group in df.groupby('metric', observed=True):
db.dataframe(
group.drop(columns=['metric']),
table_name=str(metric),
symbols='auto', # Category columns as SYMBOL. (Default)
at='ts')
# Egress: query QuestDB and materialise the result as Pandas.
with db.query(
"SELECT x AS sample_id, "
"x / 10.0 AS value "
"FROM long_sequence(3)") as result:
queried = result.to_pandas()
print(queried)
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
if __name__ == '__main__':
example()
After running this example, the rows will be split across the 'humidity',
'temp_c' and 'voc_index' tables.
For details on all options, see the
QuestDB.dataframe method.
Loading Pandas from a Parquet File¶
The following example shows how to load a Pandas DataFrame from a Parquet file and run a generated query into Pandas.
The example also relies on the dataframe’s index name to determine the table name.
import questdb
import pandas as pd
def write_parquet_file():
df = pd.DataFrame({
'location': pd.Categorical(
['BP-5541', 'UB-3355', 'SL-0995', 'BP-6653']),
'provider': pd.Categorical(
['BP Pulse', 'Ubitricity', 'Source London', 'BP Pulse']),
'speed_kwh': pd.Categorical(
[50, 7, 7, 120]),
'connector_type': pd.Categorical(
['Type 2 & 2+CCS', 'Type 1 & 2', 'Type 1 & 2', 'Type 2 & 2+CCS']),
'current_type': pd.Categorical(
['dc', 'ac', 'ac', 'dc']),
'price_pence':
[54, 34, 32, 59],
'in_use':
[True, False, False, True],
'ts': [
pd.Timestamp('2022-12-30 12:15:00'),
pd.Timestamp('2022-12-30 12:16:00'),
pd.Timestamp('2022-12-30 12:18:00'),
pd.Timestamp('2022-12-30 12:19:00')]})
name = 'ev_chargers'
df.index.name = name # We set the dataframe's index name here!
filename = f'{name}.parquet'
df.to_parquet(filename)
return filename
def example(host: str = 'localhost', port: int = 9000):
filename = write_parquet_file()
df = pd.read_parquet(filename)
with questdb.connect(f"ws::addr={host}:{port};") as db:
# Ingress: bulk-load a Pandas DataFrame into QuestDB.
# Note: Table name is looked up from the dataframe's index name.
db.dataframe(df, at='ts')
# Egress: query QuestDB and materialise the result as Pandas.
with db.query(
"SELECT x AS charger_id, "
"x * 25 AS speed_kwh "
"FROM long_sequence(3)") as result:
queried = result.to_pandas()
print(queried)
if __name__ == '__main__':
example()
For details on all options, see the
QuestDB.dataframe method.
Polars¶
The following example ingests a Polars DataFrame over QWP/WebSocket via
QuestDB.dataframe, runs a query into
Polars, and includes a schema_overrides variant.
"""Polars DataFrame ingest and query example.
`QuestDB.dataframe()` accepts polars `DataFrame` and `LazyFrame` directly,
riding the Arrow PyCapsule Interface (`__arrow_c_stream__`) straight into
`column_sender_flush_arrow_batch`. `QuestDB.query()` can materialise query
results as a polars `DataFrame` with `QueryResult.to_polars()`.
"""
import questdb
from questdb import QuestDBError
import datetime
import sys
def example(host: str = 'localhost', port: int = 9000):
import polars as pl
df = pl.DataFrame({
'symbol': ['ETH-USD', 'BTC-USD', 'ETH-USD'],
'side': ['sell', 'buy', 'buy'],
'price': [2615.54, 67234.12, 2620.88],
'amount': [0.00044, 0.0012, 0.00033],
'ts': [
datetime.datetime(2025, 1, 1, 12, 0, 0),
datetime.datetime(2025, 1, 1, 12, 0, 1),
datetime.datetime(2025, 1, 1, 12, 0, 2),
],
})
try:
conf = f'ws::addr={host}:{port};'
with questdb.connect(conf) as db:
# Ingress: publish a Polars DataFrame into QuestDB.
db.dataframe(df, table_name='trades', at='ts')
db.dataframe(
df,
table_name='trades_chunked',
at='ts',
max_rows_per_batch=2)
# Egress: query QuestDB and materialise the result as Polars.
with db.query(
"SELECT x AS trade_id, "
"x * 10.0 AS price, "
"timestamp_sequence("
"'2025-01-01T12:00:00.000000Z', 1000000) AS ts "
"FROM long_sequence(3)") as result:
queried = result.to_polars()
print(queried)
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
def schema_overrides_example(host: str = 'localhost', port: int = 9000):
"""B-class wire types (IPv4 / Geohash / etc.) need an explicit hint
because `arr.dtype` alone cannot disambiguate them from plain
integer columns. `schema_overrides` injects the corresponding
`questdb.*` Arrow Field metadata. Requires pyarrow.
"""
import polars as pl
df = pl.DataFrame({
'addr': [0x0A000001, 0xC0A80101, 0x7F000001],
'price': [100, 200, 300],
'ts': [
datetime.datetime(2025, 1, 1, 12, 0, 0),
datetime.datetime(2025, 1, 1, 12, 0, 1),
datetime.datetime(2025, 1, 1, 12, 0, 2),
],
}, schema={'addr': pl.UInt32, 'price': pl.Int64, 'ts': pl.Datetime('us')})
try:
conf = f'ws::addr={host}:{port};'
with questdb.connect(conf) as db:
db.dataframe(
df,
table_name='ipv4_log',
at='ts',
schema_overrides={'addr': 'ipv4'})
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
if __name__ == '__main__':
example()
schema_overrides_example()
PyArrow¶
The following example ingests a PyArrow table over QWP/WebSocket via
QuestDB.dataframe and runs a query into
PyArrow.
"""PyArrow Table / RecordBatch ingest and query example.
`QuestDB.dataframe()` accepts any object exposing the Arrow PyCapsule
Interface (`__arrow_c_stream__`) — pyarrow Table, RecordBatch, DuckDB
relations, cudf, etc. — and routes through
`column_sender_flush_arrow_batch` one-shot. No per-column Cython
dispatch, no chunk lifecycle. `QuestDB.query()` can materialise query
results as a pyarrow `Table` with `QueryResult.to_arrow()`.
"""
import questdb
from questdb import QuestDBError
import sys
def example(host: str = 'localhost', port: int = 9000):
import pyarrow as pa
schema = pa.schema([
pa.field('symbol', pa.string()),
pa.field('side', pa.string()),
pa.field('price', pa.float64()),
pa.field('amount', pa.float64()),
pa.field('ts', pa.timestamp('us')),
])
table = pa.Table.from_pydict({
'symbol': ['ETH-USD', 'BTC-USD'],
'side': ['sell', 'buy'],
'price': [2615.54, 67234.12],
'amount': [0.00044, 0.0012],
'ts': [1735732800_000_000, 1735732801_000_000],
}, schema=schema)
try:
conf = f'ws::addr={host}:{port};'
with questdb.connect(conf) as db:
# Ingress: publish a PyArrow Table into QuestDB.
db.dataframe(table, table_name='trades', at='ts')
# Egress: query QuestDB and materialise the result as PyArrow.
with db.query(
"SELECT x AS trade_id, "
"x * 10.0 AS price, "
"timestamp_sequence("
"'2025-01-01T12:00:00.000000Z', 1000000) AS ts "
"FROM long_sequence(3)") as result:
queried = result.to_arrow()
print(queried)
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
def schema_metadata_example(host: str = 'localhost', port: int = 9000):
"""B-class wire types can be selected either via `schema_overrides`
(wrapper injects metadata for you) or by attaching the metadata
directly on the pyarrow Field. Both are equivalent; this example
shows the direct-attach form.
"""
import pyarrow as pa
schema = pa.schema([
pa.field('addr', pa.uint32(),
metadata={b'questdb.column_type': b'ipv4'}),
pa.field('loc', pa.int32(),
metadata={b'questdb.geohash_bits': b'20'}),
pa.field('ts', pa.timestamp('us')),
])
table = pa.Table.from_pydict({
'addr': [0x0A000001, 0xC0A80101],
'loc': [0x12345, 0x67890],
'ts': [1735732800_000_000, 1735732801_000_000],
}, schema=schema)
try:
conf = f'ws::addr={host}:{port};'
with questdb.connect(conf) as db:
db.dataframe(table, table_name='locations', at='ts')
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
if __name__ == '__main__':
example()
schema_metadata_example()
Decimal Types (QuestDB 9.2.0+)¶
The following example shows how to insert data with decimal precision using
Python’s decimal.Decimal type, both row by row and as an
object-dtype DataFrame column.
Requires QuestDB server 9.2.0+. DECIMAL columns must be
pre-created (auto-creation not supported); the
examples create them through query().
import sys
from decimal import Decimal
import pandas as pd
import questdb
from questdb import QuestDBError, TimestampNanos
def example(host: str = 'localhost', port: int = 9000):
try:
with questdb.connect(f'ws::addr={host}:{port};') as db:
# DECIMAL columns must be created ahead of time; the server
# does not auto-create them.
db.execute(
'CREATE TABLE IF NOT EXISTS financial_data ('
' symbol SYMBOL,'
' price DECIMAL(18, 6),'
' quantity DECIMAL(12, 4),'
' timestamp TIMESTAMP_NS'
') TIMESTAMP(timestamp) PARTITION BY DAY WAL')
# Row-by-row ingestion with Python Decimal values.
with db.sender() as sender:
sender.row(
'financial_data',
symbols={'symbol': 'BTC-USD'},
columns={
'price': Decimal('50123.456789'),
'quantity': Decimal('1.2345')},
at=TimestampNanos.now())
sender.flush(wait=True)
# Bulk load with object-dtype Decimal columns.
df = pd.DataFrame({
'symbol': pd.Categorical(['BTC-USD', 'ETH-USD']),
'price': [Decimal('50123.456789'), Decimal('2615.123456')],
'quantity': [Decimal('1.2345'), Decimal('10.5678')],
})
db.dataframe(
df, table_name='financial_data', symbols=['symbol'],
at=TimestampNanos.now())
frame = db.query(
'SELECT symbol, price, quantity FROM financial_data '
'LIMIT -3').to_pandas()
print(frame)
except QuestDBError as e:
sys.stderr.write(
f'Got error: {e} (code={e.code}, in_doubt={e.in_doubt})\n')
if __name__ == '__main__':
example()
For better performance with DataFrames, use PyArrow decimal types, which carry width and scale in the column type:
import sys
import pandas as pd
import pyarrow as pa
import questdb
from questdb import QuestDBError, TimestampNanos
def example(host: str = 'localhost', port: int = 9000):
# Arrow decimal columns carry their width and scale in the type,
# so they map onto DECIMAL columns without per-cell inspection.
df = pd.DataFrame({
'symbol': pd.Categorical(['BTC-USD', 'ETH-USD']),
'price': pd.Series(
[50123.456789, 2615.123456],
dtype=pd.ArrowDtype(pa.decimal128(18, 6))),
'quantity': pd.Series(
[1.2345, 10.5678],
dtype=pd.ArrowDtype(pa.decimal128(12, 4))),
})
try:
with questdb.connect(f'ws::addr={host}:{port};') as db:
db.execute(
'CREATE TABLE IF NOT EXISTS financial_data ('
' symbol SYMBOL,'
' price DECIMAL(18, 6),'
' quantity DECIMAL(12, 4),'
' timestamp TIMESTAMP_NS'
') TIMESTAMP(timestamp) PARTITION BY DAY WAL')
db.dataframe(
df, table_name='financial_data', symbols=['symbol'],
at=TimestampNanos.now())
except QuestDBError as e:
sys.stderr.write(
f'Got error: {e} (code={e.code}, in_doubt={e.in_doubt})\n')
if __name__ == '__main__':
example()
Note
Over ws:: / wss:: decimal support is negotiated automatically.
On the legacy ILP transports, HTTP/HTTPS auto-negotiates
protocol version 3 while TCP/TCPS
must configure it explicitly:
tcp::addr=localhost:9009;protocol_version=3;.
For more details, see the QuestDB DECIMAL documentation.
Production¶
TLS, token auth, multi-host¶
The following production-shaped example combines TLS with OS trust roots, a
bearer token, a multi-node addr list, a disk-backed store-and-forward
spool, and a connection-event listener.
import sys
import questdb
from questdb import QuestDBError, TimestampNanos
def on_connection_event(event):
print(event.kind, event.host, event.port, event.cause_msg,
file=sys.stderr)
def example():
# A production-shaped configuration: TLS with OS trust roots, a
# bearer token, every node of the deployment in one addr list, and
# a disk-backed store-and-forward spool that replays
# unacknowledged frames after a process restart with the same
# sender_id. For HTTP basic auth, replace the token with
# 'username=...;password=...;'.
conf = (
'wss::addr=db-primary.example.com:9000,db-replica.example.com:9000;'
'token=YOUR_BEARER_TOKEN;'
'tls_ca=os_roots;'
'sf_dir=/var/spool/questdb;'
'sender_id=ingest-01;'
)
try:
with questdb.connect(
conf, connection_listener=on_connection_event) as db:
info = db.server_info()
print('connected to', info.role)
with db.sender() as sender:
sender.row(
'trades',
symbols={'symbol': 'ETH-USD'},
columns={'price': 2615.54, 'amount': 0.00044},
at=TimestampNanos.now())
# Block until the server acknowledges everything
# published on this lease.
sender.flush(wait=True)
print('events delivered:', db.connection_events_delivered)
except QuestDBError as e:
sys.stderr.write(
f'Got error: {e} (code={e.code}, in_doubt={e.in_doubt})\n')
sys.exit(1)
if __name__ == '__main__':
example()
Connection-level transports¶
The examples below use the standalone Sender —
the connection-level API, where one sender drives exactly one connection.
This layer carries the point-to-point capabilities the deployment-level
handle does not: QWP/UDP datagrams, the legacy ILP transports (HTTP/TCP),
and HTTP transactions. See Two API layers.
QWP over UDP¶
The following example sends a row using QuestWire Protocol over UDP.
Requires a QuestDB instance with QWP/UDP receiver support enabled. The
default listener port is 9007.
from questdb import Sender, Protocol, QuestDBError, TimestampNanos
import sys
def example(
host: str = 'localhost',
port: int = 9007,
table_name: str = 'trades'):
try:
with Sender(
Protocol.Udp,
host,
port,
max_datagram_size=1400) as sender:
sender.row(
table_name,
symbols={
'symbol': 'ETH-USD',
'side': 'sell'},
columns={
'price': 2615.54,
'amount': 0.00044,
},
at=TimestampNanos.now())
# QWP/UDP defaults `auto_flush_bytes` to the datagram size.
# Flush manually here to send the row immediately.
sender.flush()
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
if __name__ == '__main__':
example()
Legacy ILP (HTTP/TCP)¶
InfluxDB Line Protocol ingestion is fully supported through the standalone
Sender — including HTTP transactions, which are
unique to this layer.
HTTP with Token Auth¶
The following example connects to the database and sends two rows (lines).
The connection is made via HTTPS and uses token based authentication.
The data is sent at the end of the with block.
from questdb import Sender, QuestDBError, TimestampNanos
import sys
def example(host: str = 'localhost', port: int = 9000, token: str = None):
try:
if token is not None:
# Bearer-token auth runs over TLS.
conf = f'https::addr={host}:{port};token={token};'
else:
conf = f'http::addr={host}:{port};'
with Sender.from_conf(conf) as sender:
# Record with provided designated timestamp (using the 'at' param)
# Notice the designated timestamp is expected in Nanoseconds,
# but timestamps in other columns are expected in Microseconds.
# The API provides convenient functions
sender.row(
'trades',
symbols={
'symbol': 'ETH-USD',
'side': 'sell'},
columns={
'price': 2615.54,
'amount': 0.00044,
},
at=TimestampNanos.now())
# You can call `sender.row` multiple times inside the same `with`
# block. The client will buffer the rows and send them in batches.
# You can flush manually at any point.
sender.flush()
# 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.
# Any remaining pending rows will be sent when the `with` block ends.
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
if __name__ == '__main__':
example()
HTTP Transactions¶
The following example sends rows atomically inside an ILP/HTTP transaction, a capability unique to the HTTP transport.
from questdb import Sender, TimestampNanos
import pandas as pd
def example():
conf = 'http::addr=localhost:9000;'
with Sender.from_conf(conf) as sender:
# Force a whole dataframe to be written in a single transaction.
# This temporarily suspends auto-flushing:
# The dataframe-generated buffer must thus fit in memory.
with sender.transaction('trades') as txn:
df = pd.DataFrame({
'symbol': pd.Categorical(['ETH-USD', 'BTC-USD']),
'side': pd.Categorical(['sell', 'sell']),
'price': [2615.54, 39269.98],
'amount': [0.00044, 0.001],
'timestamp': pd.to_datetime(['2021-01-01', '2021-01-02'])})
txn.dataframe(
df,
symbols=['symbol', 'side'],
at='timestamp')
# You can write additional dataframes or rows,
# but they must all be for the same table.
txn.row(
symbols={'symbol': 'ETH-USD', 'side': 'sell'},
columns={'price': 2615.54, 'amount': 0.00044},
at=TimestampNanos.now())
# The transaction is flushed when the `with` block ends.
if __name__ == '__main__':
example()
TCP Basics¶
The following example writes rows over ILP/TCP with auto-flush.
from questdb import Sender, QuestDBError, TimestampNanos
import sys
def example(host: str = 'localhost', port: int = 9009):
try:
conf = f'tcp::addr={host}:{port};'
with Sender.from_conf(conf) as sender:
# Record with provided designated timestamp (using the 'at' param)
# Notice the designated timestamp is expected in Nanoseconds,
# but timestamps in other columns are expected in Microseconds.
# The API provides convenient functions
sender.row(
'trades',
symbols={
'symbol': 'ETH-USD',
'side': 'sell'},
columns={
'price': 2615.54,
'amount': 0.00044,
},
at=TimestampNanos.now())
# You can call `sender.row` multiple times inside the same `with`
# block. The client will buffer the rows and send them in batches.
# You can flush manually at any point.
sender.flush()
# 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.
# Any remaining pending rows will be sent when the `with` block ends.
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
if __name__ == '__main__':
example()
TCP Authentication and TLS¶
Continuing from the previous example, the connection is authenticated and also uses TLS.
from questdb import Sender, QuestDBError, TimestampNanos
import sys
def example(host: str = 'localhost', port: int = 9009):
try:
conf = (
f"tcps::addr={host}:{port};" +
"username=testUser1;" +
"token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;" +
"token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;" +
"token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;")
with Sender.from_conf(conf) as sender:
# Record with provided designated timestamp (using the 'at' param)
# Notice the designated timestamp is expected in Nanoseconds,
# but timestamps in other columns are expected in Microseconds.
# The API provides convenient functions
sender.row(
'trades',
symbols={
'symbol': 'ETH-USD',
'side': 'sell'},
columns={
'price': 2615.54,
'amount': 0.00044,
},
at=TimestampNanos.now())
# You can call `sender.row` multiple times inside the same `with`
# block. The client will buffer the rows and send them in batches.
# You can flush manually at any point.
sender.flush()
# 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.
# Any remaining pending rows will be sent when the `with` block ends.
except QuestDBError as e:
sys.stderr.write(f'Got error: {e}\n')
if __name__ == '__main__':
example()
Further legacy ILP examples — ECDSA auth (tcp_auth.py) and
configuration loading (http_from_conf.py) — live in the repository’s
examples directory.