SET options.
Two 0.16.0 features have their own dedicated config reference: Large External Tables (
segmentGroupConfig) and Deletion Vectors (enableDeletionVectors and snapshot readiness).Best practices
Onboarding- Enter the bucket name only (no
s3://) and set the AWS region in the connection config — a region in config means you don’t depend on theAWS_REGIONenvironment variable. - Grant the cluster access to the source bucket with an assumed IAM role (
roleArn+ optionalexternalId) or the cluster’s node role; verify withaws s3 ls s3://<bucket>/<prefix>/before onboarding. - Keep the source bucket in the same AWS region as your cluster. Cross-region reads without an S3 interface endpoint go through the NAT Gateway and can add large AWS charges; see Networking and data transfer costs.
- For very large sources, start with a smaller prefix subset and scale the cluster before onboarding the full dataset.
- Set
continueOnFileError=trueif a sync run should skip a file that fails to process — unreadable, corrupt, or any other per-file failure, not just unreadable ones — instead of failing the whole run (default is to fail). Because it skips on any failure, it can silently drop more data than “unreadable file” suggests, so review sync logs after enabling it. This only affects files during a sync — it does not protect an already-ingested segment whose file is later deleted or compacted away (see the note below). - If your source compacts or rewrites files, onboard it through an Iceberg REST catalog, not the raw S3/GCS Data Lake source — a REST catalog reconciles compaction via snapshot metadata, while raw sources pin segments to physical file paths that break when files are rewritten. See Segments fail to load after upstream compaction.
- Keep the data types the preview step infers; overriding them can break segment generation.
- Set a time column where the data has one — it enables time-based segment pruning and retention. Recommended but not mandatory; tables without a time column still work.
- Add indexes for the columns you filter and group by — without them, queries scan remote data.
- For dictionary-backed indexes (inverted, FST, IFST), keep the forward index
RAWand add an explicitdictionaryblock. - Avoid grouping by derived/computed columns — it defeats segment pruning.
-
Enable the page cache (
enable.prefetch.page.cache, a per-table tier setting) so index data is served locally. See Caching and index pinning. -
Pin small, hot indexes (dictionary, and anything besides a bloom filter) with
preload.index.keys.override; let the index cache serve the larger/colder ones. An enabled bloom filter is pinned for you automatically — see the note below. -
Turn on index consolidation to collapse a segment’s per-column index files into one mmap, avoiding the OS
max_map_countlimit on wide tables — see Caching and index pinning. Operations - Let the controller watcher schedule syncs; monitor health with the observability endpoints.
Networking and data transfer costs
External Tables and batch ingestion read data directly from S3, often in large volumes. Where the bucket sits relative to your StarTree cluster decides the network path those reads take, and that path decides what you pay AWS. This matters most for BYOC deployments, where the network charges land on your own AWS account.
Recommendations
- Co-locate the data and the cluster. Keep source buckets in the same region as your StarTree cluster whenever you can. It is the cheapest and fastest setup.
- Weigh how often the data is read. For a one-time read, such as a single backfill, moving the data next to the cluster may not save much. Repeated reads of a large dataset are different: External Tables query S3 again and again, and the per-GB charges add up quickly.
- Check the bucket region before onboarding. Compare it with your cluster’s region (
aws s3api get-bucket-location --bucket <bucket>). A gateway endpoint only covers buckets in its own region. - For cross-region buckets, add an S3 interface endpoint before you onboard. Create a cross-region interface endpoint for S3 in your cluster’s VPC so reads skip the NAT Gateway. See the AWS guides for gateway endpoints for S3 and cross-region PrivateLink support. Contact StarTree support if you need help with the setup.
- Watch NAT Gateway traffic during the first sync. A spike in the NAT Gateway’s CloudWatch byte counters during an initial onboarding usually means S3 reads are leaving the private network.
Caching and index pinning
External Table queries read indexes from remote storage, so keeping index data local is what makes them fast. There are two ways to do that — pinning and the index cache — and they’re used together.
Pin the small, always-needed indexes; let the cache handle the rest. Pinning is resident memory, so reserve it for compact indexes you hit on most queries — bloom filters, dictionaries, and small range/inverted indexes. Everything larger or colder (JSON, text, wide columns) should go through the index cache, which fetches on demand and evicts what isn’t used.
<column>.<indexKey> (or * for all columns), where indexKey is the index’s file key — e.g. inverted_index, range_index, bloom_filter, or dictionary. Examples: payment_type.inverted_index, *.bloom_filter, *.dictionary.
Bloom filters are pinned for you. Creating or updating an external table’s config — through the Data Portal or the preview/create/update API — adds
*.bloom_filter to preload.index.keys.override on every S3/GCS remote tier automatically, whenever the table has an enabled bloom filter on any column. This runs on every create and update, not only at onboarding, and it only ever adds that one entry: it never removes or replaces keys you’ve set yourself. Contact StarTree support to turn it off cluster-wide.Starting with StarTree Cloud 0.18.0, saving an External Table’s config through any path, including SQL DDL and a plain POST /tables, also sets preload.index.keys.additional to *.bloom_filter on every tier that doesn’t already have that key. It applies whether or not the table has a bloom filter. Keys in preload.index.keys.additional are preloaded on top of whatever preload.index.keys.override selects. To turn this default off for one table, set preload.index.keys.additional to an empty string; a value you set, including an empty one, is kept. The preload.index.keys.override entry described above is separate and is still added while the table has a bloom filter.max_map_count) — surfacing as native-memory / mmap allocation failures and server restarts. Set preload.enable.index.consolidation=true to pack a segment’s index regions into a single file per segment, drastically cutting the mmap count. Onboarding enables this by default.
Where configs live
Table config — ExternalTableSyncTask
Catalog & mode
External Tables read Parquet files only. There is no
inputFormat config — the sync engine doesn’t read one, so setting any such key has no effect.Connection keys
The catalog connection keys are prefixedcatalog.s3.* or catalog.iceberg-rest.* (REST URI, service type — glue, s3Tables, unity, or rest (Nessie and other generic Iceberg REST catalogs) — warehouse/prefix, namespace/table, and auth.rest.* / auth.storage.* credentials). The full list with examples is in AWS Glue: Onboarding via API, Unity Catalog: Onboarding via API for Unity-specific auth, or Nessie: Onboarding via API for the generic rest adapter.
Two further top-level keys select source storage independently of catalogType: sourceStorageProvider (azure, for an Azure Blob/ADLS source instead of S3) and externalTableAccessPolicy (unity for Unity-vended Azure credentials, or azure-sas for a configured SAS token via catalog.{catalogType}.auth.storage.sasToken / .credentialPrefix). Hive Metastore and Unity Catalog / generic REST catalogs accept both; AWS Glue and S3 Tables don’t. Onboarded today only through SQL DDL — not the Data Portal or REST onboarding flow.
Scheduling & task sizing
continueOnFileError vs. continueOnError — two different configs. continueOnFileError (this ExternalTableSyncTask config) skips an entire file that fails to process, for any reason — not just unreadable ones — so one bad file doesn’t fail the run. continueOnError (an OSS field in the table’s ingestionConfig, not here) skips individual bad rows while reading a file or stream. Both apply only during ingestion — neither one protects a segment that is already ingested. In particular, if a source file is deleted or compacted away upstream after its segment exists, the server fails to serve that segment regardless of either flag; the durable fix for compacting sources is an Iceberg REST catalog.Snapshot processing (Iceberg)
Checkpointing & schema evolution
How schema evolution resolves changes (0.16.0+). When
schemaEvolution.enabled=true on a catalogType=iceberg-rest source, each sync unions the current schema against the source’s Iceberg field-id/alias history instead of matching by column name alone. This means a source column rename is tracked as an alias rather than dropping and re-adding a column — the existing Pinot column name is preserved. Resolution only covers top-level fields (nested-field renames aren’t resolved). A detected type widening is rejected by default, unless schemaEvolution.typeWidening.enabled=true opts the table into applying it instead — set schemaEvolution.failFast=true if you want a rejection (widening or otherwise) to abort the run instead of being logged and skipped. When fail-fast aborts a run, the status API reports failurePhase: UPDATING_SCHEMA.Type widening (opt-in, StarTree Cloud 0.18.0+). schemaEvolution.typeWidening.enabled=true lets a table adopt a source-side widening instead of rejecting it. INT→LONG is the only supported widening; FLOAT→DOUBLE and anything else are still rejected. It applies only on the periodic-watcher path (an executor=controller, enabled=true table) — snapshot-sync ingestion skips schema evolution entirely and keeps the registered schema unchanged. Widening is safe for External Tables specifically: a segment’s forward index is the backing Parquet file, so adopting the wider type is a metadata-only edit, not a physical rewrite. It defaults to off because applying it opens a transient window, until every segment has reloaded onto the new type, where a query spanning both generations of segments on the widened column can fail (ORDER BY / GROUP BY / DISTINCT) or drop mismatching rows (a plain SELECT) — aggregations like SUM / COUNT are unaffected. Enabling it is a deliberate choice to accept that window during rollout.Decimal precision changes aren’t supported. BIG_DECIMAL (Pinot’s only decimal type) carries no precision or scale of its own, so an Iceberg decimal precision increase produces an identical Pinot schema and the sync doesn’t notice it. The physical Parquet encoding still varies by precision (decimal(9,2) in an INT32, decimal(18,2) in an INT64, wider in a FIXED_LEN_BYTE_ARRAY), and reading across files written before and after the change isn’t validated.For the full list of supported changes, an end-to-end widening example, and common errors, see Schema Evolution.Nested logical names under column mapping (
storeNestedSchema.enabled). Delta UniForm tables created with delta.columnMapping.mode (name or id) don’t store your column names in the Parquet files — each physical column is an opaque col-<uuid>, and the logical name plus a stable field-id live only in table metadata. Top-level column names are always resolved back to logical names, with or without this flag. Names inside a complex value are only resolved when storeNestedSchema.enabled is on (the default) for the table’s ExternalTableSyncTask config; set it to false to opt out, and nested keys are then stored under their physical names, so JSON_EXTRACT_SCALAR(customer, '$.name', ...) returns NULL even though the data is there. Only an explicit, case-insensitive false opts out — an unset, blank, or unrecognized value keeps the default on.The flag applies to every schema-writing path (onboarding, preview, and the schema-evolution union during sync) and is independent of schemaEvolution.enabled. It defaults to on because leaving it off silently materializes the wrong JSON keys for complex columns on column-mapped tables; the only cost of stamping is a larger ZK-persisted schema on tables that don’t need it, and it does nothing for native Iceberg or add_files tables where physical names already equal logical names. Changing it on an already-onboarded table takes effect only on the next schema write (re-preview/re-register, or the next sync if schema evolution is on), followed by a segment reload — resolution happens at read time, so segments don’t need regenerating. A table onboarded before this default flipped gets that one-time schema update and segment reload automatically on its first sync after upgrade, if it has complex columns and no explicit storeNestedSchema.enabled setting. See Unity Catalog — schema options for column mapping.Tier backend properties (caching & preload)
Set on the S3 tier intierConfigs[].tierBackendProperties. The onboarding/preview flow sets sensible defaults.
Two-tier scaffold for
skipSegmentPreprocess=true (create-time only). If the table config already sets indexingConfig.skipSegmentPreprocess=true when it’s created — through the preview API or CREATE TABLE ... WITH (type='iceberg', ...) — the enrichment scaffolds two tiers instead of the usual single fixed-selector tier: <tableName>_preprocessed_tier (segmentAge=2h) and <tableName>_s3_tier (segmentAge=0s), both keyed on creationTime. They share the same bucket and region but get distinct pathPrefix values (the preprocessed tier appends /preprocessed), so a segment aging from one tier to the other is a real physical move, and matching indexingConfig.tierOverwrites entries are written so segment preprocessing runs on the preprocessed tier and stays skipped on the s3 tier. New segments land on the s3 tier and relocate to the preprocessed tier once they pass the 2-hour threshold. This applies only at create time: adding skipSegmentPreprocess=true to an existing table via PUT /tables/<name>/config does not scaffold the second tier, and turning the flag back off does not remove one already created.Common
¹ The onboarding/preview flow sets these to
true on newly scaffolded tables (preload.enable.index.consolidation, enable.prefetch.page.cache). Existing tables are never re-enriched — if a table predates these defaults, add the keys manually.
² pathPrefix is not validated. Nothing errors if it’s missing or wrong — files simply land at the bucket root instead of under a per-table prefix. Always set it explicitly so each table’s files get their own folder.
Preload tuning
The S3 tier also exposes advanced on-demand buffer, mmap, and read-ahead sub-properties (
ondemand.*, mmap.*, readAhead.*). These are internal tuning knobs — leave them at defaults unless StarTree support advises otherwise.Query options
Set per query withSET "key" = 'value'. Where an option is also a tier setting, the query option overrides it for that one query.
Example — bypass the cache for one query:
Time travel (query-time snapshot pinning)
By default, a query against an Iceberg external table reads the newest fully-synced snapshot, resolved per query at the broker. ThesnapshotVersionByTable query option pins the query to a specific snapshot instead — the broker routes it to exactly that snapshot’s file set, so you get the data (and row-level deletes) as of that snapshot:
nyc_taxi_trips, nyc_taxi_trips_OFFLINE, and mydb.nyc_taxi_trips all resolve to the same entry. The option works on both the single-stage and multi-stage query engines.
Caveats
- Snapshot IDs only, no SQL clause. There is no
FOR VERSION AS OF/FOR TIMESTAMP AS OFsyntax and no timestamp-based variant. Pass the numeric Iceberg snapshot ID (as a JSON number or numeric string) via the query option. - Bounded time-travel window. Only snapshots still in the table’s active snapshot list can be pinned. Snapshot retention keeps the newest
iceberg.snapshotProcessing.retention.maxActiveVersionsversions (default 2). A configured value below 2 is ignored outright and the default of 2 — also the floor — is used instead. This is not arbitrary Iceberg-history time travel — it is “pick one of the last N snapshots StarTree has synced and still retains.” - Iceberg catalog sources only. Supported for Iceberg REST catalog sources (AWS Glue, S3 Tables, Unity Catalog, generic REST). On raw S3/GCS Data Lake tables the option is silently ignored — file listings have no snapshot concept.
- Expired or unknown snapshots fail the query. Pinning a snapshot that is not (or no longer) active returns an error (
Iceberg snapshot <id> is not active for table <table>) rather than silently falling back to the latest snapshot. Malformed JSON in the option also fails the query. - No pin = latest ready snapshot. Without the option, each query reads the newest snapshot that is fully synced and cache-ready at that moment. Two queries issued at different times may therefore read different snapshots; pin a snapshot when you need repeatable reads across queries.
- On some tables the broker sets it automatically. Every query on a deletion-vector-enabled table runs against a pinned snapshot automatically — and so does every query on a table with snapshot consistency enabled, or one using segment groups. You only set the option yourself to override the default (latest) choice.
- Not the same as
initialSnapshotId. The table-config keysiceberg.snapshotProcessing.initialVersionSelector/initialSnapshotId(and the DDLsnapshot_mode/snapshot_id) choose where ingestion starts when the table is created. They do not affect which snapshot a query reads. - Finding snapshot IDs.
GET …/externalTable/status?includeLag=truereturns the newest synced snapshot (lag.synced.snapshotId) and the upstream head (lag.upstream.snapshotId) — see Observability. For older retained snapshots, get IDs from the source catalog (e.g. the Iceberg table’ssnapshotsmetadata); there is currently no API that lists all active snapshot IDs.

