Why remote Parquet queries are fast
Most engines that query Iceberg/Parquet directly (e.g. Trino, Athena) read at the row-group / column-chunk level. Pinot reads at the Parquet page level — a finer granularity — and uses its own indexes (inverted, range) to skip straight to the pages that matter instead of scanning a whole column chunk. This page-level skip is what makes an indexed External Table competitive with a native Pinot segment despite the data living remotely. External Tables also federate cleanly with regular Pinot tables. A common pattern keeps recent data (e.g. the last 24h) in a REALTIME table fed from Kafka, while the same events land in Iceberg for long-term retention — a single query can span both, giving one query surface across the full time range without copying the archive into Pinot.Capacity guardrails
These aren’t hard limits, but the confident, well-tested operating range today:
Millions of files isn’t viable today. If a source has millions of small files, either compact upstream to raise average file size and cut file count below the guardrail, or onboard a subset first (e.g. the most recent partitions) and expand later rather than pointing at the full dataset on day one.
Full-refresh Iceberg tables (where a snapshot replaces all existing files rather than appending) are supported, but a full replace can spike the segment count for that cycle. Before onboarding one, check how many files the refreshed snapshot contains, whether old segments need cleanup after each refresh, and whether the file count stays inside the guardrails above.
Deployment
- Use a dedicated tenant for External Table workloads rather than an existing production tenant — it isolates ingestion and query resource usage from other traffic on the cluster.
- Default replication factor is 1, chosen to maximize ingestion throughput; pseudo-segments are small (single-digit KB to a few MB before indexes are attached), so the cost of raising it later — e.g. to 2 once onboarding is stable — is low.
- Both SSE and MSE query execution modes are supported against External Tables.
- For AWS Glue sources specifically, the cluster’s IAM role/credentials also need Glue API access, in addition to the S3 permissions in Troubleshooting → Access Denied:
glue:GetTable,glue:GetTables,glue:GetCatalog,glue:GetDatabase,glue:GetDatabases. Scope these to the specific Glue databases/tables the cluster needs, and prefer an assumed IAM role over static access keys.
Ingestion mechanics
Each sync run has two phases, which explains a couple of behaviors that are otherwise easy to mistake for a bug:- Segment generation (controller). Source files are processed in batches (
taskMaxNumFiles, default100). The controller reads Parquet footer statistics (row count, min/max) and creates pseudo-segments — metadata and index pointers, not materialized data. - Download and index (servers). Servers download the segment artifacts and build indexes. Queries can return partial results while this phase is in flight — for example, a response noting some segments are unavailable — until all servers finish loading.
enabled=false on ExternalTableSyncTask). Leaving it on means every scheduled tick re-scans and bootstraps newly-discovered files, so the segment count keeps growing even when there’s no new data to justify it. For catalog-backed incremental sources, rely on checkpointing (see Best Practices & Configs — Checkpointing & schema evolution) instead of re-bootstrapping.
Null handling — why it matters
nullHandlingEnabled defaults to false, and the onboarding/preview flow never sets it automatically. Without it, Pinot doesn’t build null bitmap vectors during segment generation, so columns containing nulls in the source Parquet aren’t tracked — IS NULL, IS NOT NULL, and aggregations that depend on the distinction (COUNT(col) vs. COUNT(*)) will be wrong on those columns.
Turn it on explicitly if the source has nullable columns you query on:
Additional troubleshooting
ZooKeeper packet-size overflow as segment count grows
Symptom:- Cluster config:
"zk.serializer.znrecord.write.size.limit.bytes": "62914560"(60 MB — raise further if the error persists as more segments are added). - JVM flag on both Controller and Server:
-Djute.maxbuffer=62914560. - Restart Controller and Server.
403 on segment upload with 1970-era timestamps
Symptom:ingestionConfig (at creation, or via a config update followed by re-triggering ingestion):

