Skip to content

Query Data with the Data Out API

Run SQL queries (including joins and tables[rollups]) against Druid using the Data Out API

The Data Out API runs a SQL query against Obsrv’s realtime store (Druid) and returns the result over HTTP.

You write the query using placeholder table names (table1, table2, …) and map each placeholder to an actual datasource using query parameters on the request URL:

POST /v2/data/query?table1=<value>&table2=<value>...
  • The parameter key must exactly match the table name used in the SQL (table1, table2, …).
  • The parameter value is the datasource to query — passed either as an alias (default) or as a raw datasource_ref (alias=false).

The examples below hit localhost:3000 with no auth header — fine for local/unsecured setups. On a secured deployment with RBAC enabled, the API requires a bearer token:

Authorization: Bearer <user_access_token>

The caller’s token must grant the api.data.out permission. Missing/invalid tokens return 401; a token without the required permission returns 403. If RBAC is disabled, no token is required.

To obtain a token, follow How-Tos → Troubleshoot → Obsrv Troubleshooting Guide → Dataset APIs access issue (Unauthorised Access error), which has the exact token-fetch curl and how to pass the token in the request header.

FieldRequiredDescription
idyesFixed API id: api.data.out.
veryesAPI version, e.g. v2.
tsyesRequest timestamp (ISO-8601).
params.msgidyesClient-supplied message id (echoed back in the response).
queryyesThe SQL query string. Table names in it are the placeholders mapped by the ?table1=... query params.

The datasource mapping is supplied via the URL query params (?table1=...), not in the body.

{
"id": "api.data.out",
"ver": "v2",
"ts": "2026-07-15T12:00:00+05:30",
"params": { "status": "SUCCESS", "msgid": "single", "resmsgid": "<generated>" },
"responseCode": "OK",
"result": [
{ "count": 1234 }
]
}

result holds the Druid query output (array of rows). Error responses use the same envelope with params.status = "FAILED" and an error code/message.

The optional alias query parameter decides how each value is interpreted.

Default — alias=true (or omitted): pass the alias name

Section titled “Default — alias=true (or omitted): pass the alias name”

Each parameter value is the datasource alias (the dataset alias, or a tables[rollups] alias).

  • To query a dataset’s main data, pass the dataset alias.
  • To query a tables[rollups], pass that tables[rollups] alias.

Alias vs datasource_ref — how to tell them apart. The alias is the stable, human-facing name returned by the read APIs (e.g. <dataset>_druid, or <dataset>_druid_master for a master dataset). The datasource_ref is the physical Druid datasource name, which may carry a date suffix (e.g. <dataset>_druid-20260715). In the default mode you pass the alias; with alias=false you pass the exact datasource_ref. Always take both values from the read APIs rather than constructing them by hand — the suffix scheme is an internal detail.

POST /v2/data/query?table1=<dataset-or-tables[rollups]-alias>

Where to find the alias:

  • Dataset alias — from the dataset read API: GET /v2/datasets/read/:dataset_id returns the alias field for the dataset’s primary datasource.
  • tables[rollups] alias — from the tables read APIs (management API): POST /dataset/table/list (all tables of a dataset) or GET /dataset/table/read/:id (a single table) return the datasource (alias) and datasource_ref for each table.

Use the alias value returned by these APIs as the parameter value.

This is the recommended mode. Aliases are stable, human-friendly names and keep pointing to the correct underlying datasource even when the physical datasource_ref changes.

Each parameter value must be the exact datasource_ref of the datasource you want — the primary datasource’s ref, or a specific tables[rollups] datasource’s ref (both are returned by the read APIs above).

POST /v2/data/query?alias=false&table1=<primary-or-tables[rollups]-datasource_ref>

Use this only when you need to pin an exact datasource_ref (for example a specific tables[rollups] or a dated snapshot).

A dataset can expose more than one queryable table in the realtime store:

  • the primary datasource (raw/base granularity), and
  • one or more tables[rollups] datasources (pre-aggregated at a given granularity, e.g. day / week / month).

Each tables[rollups] is an independent datasource with its own alias and its own datasource_ref (fetch them from the tables read APIs above). You query a tables[rollups] exactly like any other table — just pass its alias (default) or its datasource_ref (alias=false) as the parameter value.

Query a tables[rollups] by alias (default):

Terminal window
curl --location 'localhost:3000/v2/data/query?table1=<tables[rollups]-alias>' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"rollup-by-alias"},
"query":"SELECT COUNT(*) FROM \"table1\""}'

Query a tables[rollups] by its datasource_ref:

Terminal window
curl --location 'localhost:3000/v2/data/query?alias=false&table1=<tables[rollups]-datasource_ref>' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"rollup-by-ref"},
"query":"SELECT COUNT(*) FROM \"table1\""}'

Join a tables[rollups] with another datasource (mix as needed — each tableN param is resolved independently):

Terminal window
curl --location 'localhost:3000/v2/data/query?table1=<tables[rollups]-alias>&table2=<dataset-alias>' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"rollup-join"},
"query":"SELECT table1.user_id, table2.root_org FROM \"table1\" INNER JOIN \"table2\" ON table1.user_id = table2.user_id LIMIT 10"}'

Replace the example values with real aliases / refs from your deployment.

Terminal window
curl --location 'localhost:3000/v2/data/query?table1=progress-details_druid' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"single"},
"query":"SELECT COUNT(*) FROM \"table1\""}'
Terminal window
curl --location 'localhost:3000/v2/data/query?table1=progress-details_druid&table2=user-details_druid_master' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"join"},
"query":"SELECT table1.user_id, table2.root_org FROM \"table1\" INNER JOIN \"table2\" ON table1.user_id = table2.user_id LIMIT 10"}'
Terminal window
curl --location 'localhost:3000/v2/data/query?table1=progress-details_druid&table2=user-details_druid_master' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"subquery-join"},
"query":"SELECT COUNT(*) FROM \"table1\" INNER JOIN (SELECT * FROM \"table2\" WHERE LOWER(\"root_org\") = LOWER('\''Infosys'\'')) AS \"table2\" ON \"table1\".\"user_id\" = \"table2\".\"user_id\""}'

Column filters must reference columns that actually exist on that datasource. Here root_org is filtered on table2 only; filtering a column on a datasource that does not have it will error. Adjust column names to match your datasources.

Terminal window
curl --location 'localhost:3000/v2/data/query?table1=progress-details_druid' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"deep-nested"},
"query":"SELECT * FROM (SELECT * FROM (SELECT * FROM \"table1\") a) b LIMIT 5"}'

The CTE name (c) is a local alias, not a datasource — it needs no parameter. Only table1 does.

Terminal window
curl --location 'localhost:3000/v2/data/query?table1=progress-details_druid' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"cte"},
"query":"WITH c AS (SELECT * FROM \"table1\") SELECT COUNT(*) FROM c"}'
Terminal window
curl --location 'localhost:3000/v2/data/query?table1=progress-details_druid&table2=user-details_druid_master' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"union"},
"query":"SELECT user_id FROM \"table1\" UNION SELECT user_id FROM \"table2\""}'
Terminal window
curl --location 'localhost:3000/v2/data/query?table1=progress-details_druid&table2=user-details_druid_master' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"where-subquery"},
"query":"SELECT * FROM \"table1\" WHERE user_id IN (SELECT user_id FROM \"table2\") LIMIT 10"}'
Terminal window
curl --location 'localhost:3000/v2/data/query?table1=progress-details_druid&table2=user-details_druid_master' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"scalar-subquery"},
"query":"SELECT (SELECT COUNT(*) FROM \"table2\") AS user_count FROM \"table1\" LIMIT 1"}'
Terminal window
curl --location 'localhost:3000/v2/data/query?alias=false&table1=progress-details_druid-2026-07-14' \
--header 'Content-Type: application/json' \
--data '{"id":"api.data.out","ver":"v2","ts":"2026-07-14T00:00:00+05:30","params":{"msgid":"direct-ref"},
"query":"SELECT COUNT(*) FROM \"table1\""}'

All of the above return 200 with the query result in the response body.


ScenarioStatusError code
Query executed200
A referenced table has no matching parameter400DATA_OUT_MISSING_TABLE_PARAM
SQL fails to parse / more than one statement400DATA_OUT_INVALID_QUERY
Request body fails schema validation400DATA_OUT_INVALID_INPUT
A mapped datasource does not exist404DATASOURCE_NOT_FOUND
Datasource exists but segments not yet published in Druid404DATASOURCE_NOT_AVAILABLE
Datasource still loading (load status < 100%)416DATASOURCE_NOT_FULLY_AVAILABLE
Missing/invalid bearer token (RBAC enabled)401
Token lacks api.data.out permission403

The 416 for “still loading” is what the API currently returns (mapped from RANGE_NOT_SATISFIABLE). It signals “data not ready yet, retry later” rather than a standard range error — treat it as a transient/retryable condition.

  • Parameter keys must match the table identifiers in the SQL exactly (table1, table2, …).
  • Default mode expects aliases (dataset alias or tables[rollups] alias); alias=false expects exact datasource_ref values (primary or tables[rollups]).
  • The same table name cannot map to two different datasources within one query (one parameter per name).