Skip to content
Talk to our solutions team

Worksheet profiling and analytics

Four routes read a worksheet table without you writing SQL: enriched schema, sampled rows, a per-column profile, and distinct values with counts. All four are GET, all four operate on a table that already exists in the worksheet, and none of them go through the entity engine — there is no access rule, no row-level security and no field protection on a worksheet table. The only boundary is the tenant that created the worksheet.

MethodPathReturns
GET/data/worksheets/{worksheet_id}/tables/{table_name}/schemaPer-column type, nullability, null count, distinct count, sample values, min/max
GET/data/worksheets/{worksheet_id}/tables/{table_name}/sampleRows, with a selection strategy
GET/data/worksheets/{worksheet_id}/tables/{table_name}/analytics/{column_name}One column’s statistics, dispatched by inferred kind
GET/data/worksheets/{worksheet_id}/tables/{table_name}/columns/{column_name}/distinctDistinct values with occurrence counts

data.svc registers these at /worksheets/…; the paths above are the public form, with the /data/ prefix the gateway strips before forwarding.

  • Authentication is a JWT, and the tenant key is enforced on every call. A worksheet belonging to another tenant answers 403 worksheet.cpet_violation, not 404.
  • Nothing here is rate limited. The per-tenant limiter covers worksheet creation, file upload and POST .../query; the four introspection routes are not counted.
  • {column_name} matches case-insensitively on the analytics and distinct routes, and the response echoes the table’s own casing. Table names in the path match exactly.
  • type is a raw DuckDB type string (BIGINT, VARCHAR, TIMESTAMP, DECIMAL(10,2)), not a field type from the entity model. The two vocabularies are unrelated.
  • Statistics are computed on every request against live table data. Nothing is cached, and a wide table costs several queries per column.

Distinct counts: exact below the threshold, estimated above it

Section titled “Distinct counts: exact below the threshold, estimated above it”

Three of the four routes report a distinct count, and all three compute it the same way:

  1. Run DuckDB’s APPROX_COUNT_DISTINCT on the column.
  2. If the estimate is greater than 10,000, keep it and set the approximate flag to true.
  3. If it is between 1 and 10,000, re-run an exact COUNT(DISTINCT …), replace the value, and set the flag to false.
  4. If it is 0, the count is 0 and the flag is false.

NULL is excluded from both the estimate and the exact count.

RouteCount fieldFlag
…/schemacolumns[].distinct_countcolumns[].distinct_count_is_approx
…/analytics/{column}distinct_countdistinct_count_is_approx
…/columns/{column}/distincttotal_distincttotal_distinct_is_approx

GET /data/worksheets/{worksheet_id}/tables/{table_name}/schema

No parameters.

{
"table": "orders",
"row_count": 48213,
"columns": [
{
"name": "order_id",
"type": "BIGINT",
"nullable": true,
"null_count": 0,
"distinct_count": 48213,
"distinct_count_is_approx": true,
"sample_values": ["1001", "1002", "1003", "1004", "1005"],
"min": "1001",
"max": "49213"
},
{
"name": "region",
"type": "VARCHAR",
"nullable": true,
"null_count": 12,
"distinct_count": 4,
"distinct_count_is_approx": false,
"sample_values": ["EMEA", "APAC", "AMER", "LATAM"]
}
]
}
FieldTypeNotes
tablestringEchoes the path parameter.
row_countintExact COUNT(*). -1 if the count query fails.
columns[].namestringOrdinal order, as the table declares it.
columns[].typestringDuckDB type name.
columns[].nullableboolDeclared nullability from the information schema.
columns[].null_countintExact count of NULL values.
columns[].distinct_countintSee the threshold rule above. Excludes NULL.
columns[].distinct_count_is_approxbooltrue only above 10,000.
columns[].sample_valuesarrayUp to 5 distinct non-null values, unordered. Omitted when empty.
columns[].min / maxanyNumeric and temporal columns only. Omitted otherwise.

GET /data/worksheets/{worksheet_id}/tables/{table_name}/sample

ParameterValuesDefaultEffect
ninteger50Rows to return. Clamped to 10,000. A value ≤ 0, or one that does not parse, falls back to 50.
strategyhead, random, tailheadRow selection.
seedinteger0Seeds random.
columnscomma-separated namesall columnsProjection.
{
"table": "orders",
"strategy": "random",
"rows": [
{ "order_id": "1042", "region": "EMEA", "amount": 199.5 },
{ "order_id": "2298", "region": "APAC", "amount": 84.0 }
],
"schema": [
{ "name": "order_id", "type": "BIGINT" },
{ "name": "region", "type": "VARCHAR" },
{ "name": "amount", "type": "DOUBLE" }
],
"row_count_returned": 2
}

The schema block here carries only name and type — it is the result-set schema of the sample query, not the enriched schema above.

Strategy semantics:

  • head takes the first n rows in storage order.
  • tail takes the last n rows and returns them in storage order, not reversed.
  • random orders by a hash of the row id combined with seed. It is deterministic: the same seed returns the same rows, and because seed defaults to 0, repeated calls without a seed return an identical sample. Vary seed to get a different draw.

A columns value naming a column that does not exist fails the underlying query and returns 400 query.execution_error, not column.not_found.

GET /data/worksheets/{worksheet_id}/tables/{table_name}/analytics/{column_name}

ParameterValuesDefaultEffect
histogram_bucketsinteger20Requested histogram buckets. Numeric columns only. A negative value suppresses the histogram.
top_kinteger20Number of most-frequent values. Text columns only. A negative value suppresses the top-values query.
include_quantilesfalsequantiles includedOnly the exact string false disables quantiles. Numeric columns only.

The column’s DuckDB type name decides which statistics block you get. Matching is a case-insensitive substring test in this order:

KindMatches when the type nameExamples
booleancontains BOOLBOOLEAN
numericcontains INT, FLOAT, DOUBLE, DECIMAL, NUMERIC or REALBIGINT, DECIMAL(10,2), DOUBLE
temporalcontains DATE, TIME or TIMESTAMPDATE, TIMESTAMP, TIME
textanything elseVARCHAR, UUID, BLOB, JSON, LIST, STRUCT

Every response carries the base block; exactly one of numeric, text, temporal, boolean is present, matching kind.

FieldTypeNotes
columnstringThe table’s exact casing.
typestringDuckDB type name.
kindstringnumeric · text · temporal · boolean.
countintExact COUNT(*) over the table, including nulls.
null_countintExact.
null_percentagefloatnull_count / count, a fraction in [0,1] — not a percentage — rounded to 4 decimal places. 0 when the table is empty.
distinct_countintSee the threshold rule.
distinct_count_is_approxbool
{
"column": "amount",
"type": "DOUBLE",
"kind": "numeric",
"count": 48213,
"null_count": 31,
"null_percentage": 0.0006,
"distinct_count": 9840,
"distinct_count_is_approx": false,
"numeric": {
"min": 0.5,
"max": 10000.5,
"mean": 213.4471,
"median": 149.0,
"stddev": 402.118,
"quantiles": { "p25": 62.0, "p50": 149.0, "p75": 268.5, "p95": 812.0, "p99": 2140.0 },
"histogram": {
"bucket_count": 18,
"buckets": [
{ "range_min": "0.5", "range_max": "500.5", "count": 44120 },
{ "range_min": "500.5", "range_max": "1000.5", "count": 2981 }
]
}
}
}
  • quantiles keys are exactly p25, p50, p75, p95, p99. The block is omitted when include_quantiles=false.
  • min and max carry the column’s own storage type. On BIGINT, HUGEINT and DECIMAL columns they come back as JSON strings, not numbers, to avoid precision loss. On DOUBLE, FLOAT and the smaller integer types they are JSON numbers.
  • mean, median, stddev and every quantile are DOUBLE aggregates, so they are JSON numbers — including on a BIGINT or HUGEINT column, where min and max beside them are strings. Only a DECIMAL column renders them as strings.
  • histogram.buckets[].range_min and range_max are always JSON strings, whatever the column type.
  • Every bucket is (max - min) / histogram_buckets wide — the width comes from the requested bucket count, not from bucket_count.
  • Empty buckets are absent. bucket_count is the number of buckets that contain at least one row, so it is usually smaller than histogram_buckets and the array is not a contiguous series.
  • histogram is omitted entirely when the column has a single distinct value (min == max), and when histogram_buckets is negative.
{
"column": "region",
"type": "VARCHAR",
"kind": "text",
"count": 48213,
"null_count": 12,
"null_percentage": 0.0002,
"distinct_count": 4,
"distinct_count_is_approx": false,
"text": {
"top_values": [
{ "value": "EMEA", "count": 21044 },
{ "value": "AMER", "count": 18110 }
],
"avg_length": 4.0,
"min_length": 4,
"max_length": 5
}
}

top_values is ordered by count descending, limited by top_k, and excludes NULL. Length statistics are computed over non-null values only; avg_length is rounded to 2 decimal places.

{
"column": "ordered_on",
"type": "DATE",
"kind": "temporal",
"count": 48213,
"null_count": 0,
"null_percentage": 0,
"distinct_count": 731,
"distinct_count_is_approx": false,
"temporal": {
"min": "2024-01-01",
"max": "2025-12-31",
"distribution": {
"granularity": "month",
"buckets": [
{ "bucket": "2024-01-01", "count": 1980 },
{ "bucket": "2024-02-01", "count": 2104 }
]
}
}
}

granularity is always month and is not a parameter. There is one bucket per month that contains at least one row — months with no rows are absent, so the series is not contiguous and a two-year column returns at most 24 buckets. distribution is omitted when the column is entirely null. Bucket labels and min/max are formatted from the column type: DATE as 2006-01-02, TIME as 15:04:05, and TIMESTAMP as RFC 3339 in UTC.

{
"column": "is_paid",
"type": "BOOLEAN",
"kind": "boolean",
"count": 48213,
"null_count": 40,
"null_percentage": 0.0008,
"distinct_count": 2,
"distinct_count_is_approx": false,
"boolean": { "true_count": 31900, "false_count": 16273 }
}

Both counts are exact and neither includes NULL; true_count + false_count + null_count == count. There is no histogram or top-k for a boolean column.

GET /data/worksheets/{worksheet_id}/tables/{table_name}/columns/{column_name}/distinct

ParameterValuesDefaultEffect
limitinteger100Values returned. Clamped to 10,000. A value ≤ 0, or one that does not parse, falls back to 100.
orderfrequency, alpha, valuefrequencyfrequency is count descending; alpha casts the value to text and sorts lexically; value sorts by the column’s native type. An unrecognised value falls back to frequency.
with_countsfalsecounts includedOnly the exact string false drops the counts.
searchsubstringnoneCase-insensitive ILIKE filter on the value cast to text.
{
"column": "region",
"total_distinct": 4,
"total_distinct_is_approx": false,
"returned": 4,
"truncated": false,
"values": [
{ "value": "EMEA", "count": 21044 },
{ "value": "AMER", "count": 18110 },
{ "value": "APAC", "count": 6203 },
{ "value": "LATAM", "count": 844 }
]
}
FieldTypeNotes
columnstringThe table’s exact casing.
total_distinctintDistinct values in the whole column. See the threshold rule.
total_distinct_is_approxbool
returnedintLength of values.
truncatedbooltrue when more values existed than limit allowed.
values[].valueanyNULL is excluded.
values[].countintExact occurrence count, or -1 when with_counts=false.

All four routes return the standard worksheet error body: {"error": {"code", "message", "details", "request_id"}}.

CodeStatusRaised when
worksheet.not_found404Unknown worksheet id, or one more than 5 minutes past expiry — the lookup evicts it and reports it missing.
worksheet.expired410The worksheet’s TTL has passed but it is still inside the 5-minute grace window.
worksheet.cpet_violation403The worksheet belongs to a different tenant.
table.not_found404No such table in this worksheet.
column.not_found404No such column, on the analytics and distinct routes.
query.execution_error400The underlying statistics or sample query failed — including a bad columns projection.

Worksheets live in the memory of one process. A profiling call that returns 404 after previously succeeding means the worksheet’s process restarted or the request reached a different replica — not that the table was dropped.

  • Worksheets — the create, ingest, define-table and query lifecycle these routes read from.
  • Worksheet limits and quotas — the rate limits these routes are exempt from, and every other bound.
  • Data API HTTP reference — every route, header and status code in one place.
  • Query DSL — profiling and filtering entity data, which is a separate surface with access control applied.