title: 'Performance Tuning' sidebar_label: 'Performance Tuning' sidebar_position: 33 description: 'Comprehensive guide to optimizing query performance, acceleration, storage, spill-to-disk, and resource utilization in Spice deployments.' keywords:
This guide provides recommendations for optimizing Spice performance across data acceleration, storage, query execution, caching, and resource allocation.
The settings that matter most, in the order they are usually worth checking:
| Concern | Recommendation | Details |
|---|---|---|
| Accelerator choice | cayenne for datasets of 10 GB and above; duckdb (file mode) for 1–10 GB with complex SQL; arrow for small, latency-critical data | Accelerator Selection (opens in a new tab) |
| Where acceleration data lives | Local NVMe or SSD, for its per-I/O latency above all. Network block storage (EBS and equivalents) is a durable fallback. NAS, NFS, and SMB shares are not recommended. | Storage (opens in a new tab) |
| Where queries spill | Set runtime.query.temp_directory to a directory on local NVMe/SSD with ample free space, never the root volume or a RAM-backed mount | Spill-to-Disk and the Temporary Directory (opens in a new tab) |
| Memory | Leave runtime.query.memory_limit unset and size the container; bound bursts with runtime.query.max_concurrent_queries | Query Memory Management (opens in a new tab) |
| CPU | Set a CPU request and no CPU limit; use runtime.cpu.cores to bound thread pools without throttling | Resource Allocation (opens in a new tab) |
| Repeated queries | Enable the SQL results cache with tiny_lfu eviction and a stale_while_revalidate_ttl | Caching Configuration (opens in a new tab) |
| Data layout | Sort accelerated data by the columns queries filter on so zone maps and segment statistics prune scans | File Format Filtering and Optimization (opens in a new tab) |
| Refresh cost | Prefer append or changes over full for large datasets; set time_column and time_partition_column | Data Refresh Optimization (opens in a new tab) |
| Observability | Capture plans for slow queries with task history; watch the query pool, resident memory, storage, and cache metrics | Monitoring and Profiling (opens in a new tab) |
Choose the appropriate Data Accelerator (opens in a new tab) based on dataset characteristics and query patterns.
| Scenario | Recommended Accelerator | Key Configuration |
|---|---|---|
| Small datasets (under 1 GB), low latency | arrow | Default in-memory |
| Small datasets (1-10 GB), complex SQL | duckdb with mode: file | Set duckdb_memory_limit; place duckdb_file on local NVMe/SSD |
| Datasets 10 GB and above (up to 1+ TB) | cayenne | Place data files on local NVMe/SSD; tune cache parameters; needs 1/3 to 1/2 the memory of duckdb |
| Write-heavy workloads | cayenne with zstd | Set cayenne_compression_strategy: zstd |
| Point lookups, large datasets | cayenne | Vortex provides 100x faster random access (opens in a new tab) |
| Point lookups, small-medium datasets | arrow with hash index | Set a primary_key to auto-enable the hash index (experimental, v1.11.0-rc.2+) |
| Point lookups with explicit indexes | duckdb or sqlite | Configure indexes |
Every file-mode engine — Cayenne, DuckDB, SQLite, and Turso — is only as fast as the storage its files sit on. Decide where those files and the query spill directory live before tuning anything else; see Storage (opens in a new tab).
Spice is an in-process engine: accelerated data, the metadata that indexes it, and the intermediate results of queries that outgrow memory all live on whatever storage the process can reach. That storage is on the critical path of every query that is not served from RAM, and of every refresh, compaction, and spill.
Recommendation: store acceleration data and temporary/spill files on local NVMe or SSD. Network-attached block storage (Amazon EBS, Azure Managed Disks, GCP Persistent Disk and Hyperdisk) works as a durable fallback with tuning; network file systems (NAS, NFS, SMB, EFS, Azure Files) are not recommended for either.
Each engine has a different I/O profile, and none of them is friendly to high-latency storage:
| Component | I/O pattern | Sensitive to |
|---|---|---|
| Spice Cayenne (Vortex data files) | Random reads of individual segments — Vortex is built for random access, so a selective query issues many small reads rather than one sequential scan. Compaction rewrites files in the background. | Read latency, IOPS, write bandwidth |
| Cayenne metastore (SQLite) | Many small synchronous writes with fsync on every commit; read on every plan. | Write latency |
DuckDB (mode: file) | Buffer-managed block reads and writes; checkpoints and index serialization; its own spill files for sorts, joins, and aggregates. | Latency and throughput |
SQLite and Turso (mode: file) | Page-cache misses become synchronous reads; WAL writes with fsync; a single writer per file. | Latency, locking semantics |
| DataFusion spill (all engines) | Large sequential writes and reads of Arrow IPC batches while a sort, aggregate, or sort-merge join runs beyond runtime.query.memory_limit. | Throughput, free space |
| Snapshots | Whole-file copies to and from object storage on bootstrap and after refreshes. | Throughput |
Latency matters more than IOPS. A query's critical path is a chain of dependent I/Os — read a footer, then the segments it points to, then the next batch; write a spill batch, wait for it, write the next — and each one waits for the device before the operator can continue. Per-I/O latency therefore sets how long a single query, refresh, or spill takes; IOPS and bandwidth only set how many can proceed at once before they start queueing behind each other. A volume with generous provisioned IOPS but millisecond latency still runs a dependent read chain an order of magnitude slower than NVMe, and no amount of additional IOPS closes that gap.
| Medium | Typical per-I/O latency |
|---|---|
| Local NVMe / SSD | ~100 µs |
io2 Block Express, Premium SSD v2, Hyperdisk Extreme | Under 1 ms |
gp3 / gp2, standard managed disks | Single-digit milliseconds |
| S3 Express One Zone | Single-digit milliseconds |
| NFS, SMB, EFS, Azure Files | Milliseconds plus protocol overhead, highly variable |
Segment and page caches absorb some of the difference for hot data, but cold scans, refreshes, and spills run at the latency of the medium.
Put every locally stored artifact on one local NVMe/SSD file system, in separate directories, and point the runtime at them explicitly rather than relying on the defaults (the working directory's .spice/data for acceleration files, and the operating system's temporary directory for spill):
Keep spill and data on the same fast device rather than moving spill to a slower one to "protect" the data volume — a spill that runs slowly holds the query's memory reservation for longer and delays every query behind it. When a host has two NVMe devices, put spill on one and data on the other so a large spill does not compete with scans for the device's queue.
| Tier | Typical latency | Durability | Acceleration data | Spill / temp |
|---|---|---|---|---|
Local NVMe instance store (AWS i/d families, Azure Lsv3, GCP Local SSD) | ~100 µs | Lost when the instance stops, is replaced, or its disk fails | Recommended | Recommended |
| Local SSD, bare metal | ~100 µs | Survives restarts; single host | Recommended | Recommended |
RAM-backed (tmpfs) | Memory speed | Lost on restart | Only when data is small and RAM abundant | Not recommended — defeats the point of spilling |
| Network block storage (EBS, Azure Managed Disks, GCP PD/Hyperdisk) | Sub-millisecond (io2, Premium SSD v2, Hyperdisk Extreme) to single-digit ms (gp3) | Survives instance replacement | Acceptable with provisioned IOPS; Spice adapts | Acceptable; never the small root volume |
| Network file systems (NFS, SMB/CIFS, EFS, Azure Files, Filestore) | Milliseconds, variable | Shared and durable | Not recommended | Not recommended |
| Object storage | Single-digit ms (S3 Express One Zone) to tens of ms (S3 Standard) | Durable, shared | Cayenne data files on S3 Express only; cold tier on standard S3; snapshot copies on standard S3, GCS, or ADLS | Not supported |
| HDD | ~10 ms seeks | Survives restarts | Not recommended | Not recommended |
File-mode accelerators detect the class of storage behind their file path at registration and tune themselves for it. The result is the acceleration's storage_profile (opens in a new tab), which defaults to auto:
auto reads /proc/self/mountinfo on Linux, finds the mount that holds the (nearest existing ancestor of the) file path, and classifies it: tmpfs/ramfs mounts resolve to tmpfs; nfs, nfs4, cifs, smbfs, and smb3 mounts resolve to ebs (the network-attached tier); block devices that identify as Amazon Elastic Block Store or as Microsoft/Azure virtual disks resolve to ebs; devices that identify as Amazon EC2 NVMe instance storage, NVMe devices in general, and any non-rotational device resolve to local_ssd. Device-mapper, LVM, and software-RAID volumes are resolved through their underlying devices. Anything else — including rotating disks and every platform other than Linux — resolves to unknown, which applies the engine defaults.local_ssd, ebs, and tmpfs override detection. Set one when detection cannot see the device: a network block device that is not EBS or an Azure disk (GCP Persistent Disk and Hyperdisk, Ceph RBD, SAN LUNs, OpenStack Cinder, and similar volumes present as ordinary non-rotational disks and resolve to local_ssd), a container runtime that hides /sys block metadata, or a non-Linux host.Each engine applies its own adjustments to the resolved profile:
| Engine | ebs (network-attached block storage, NFS, SMB) | tmpfs | local_ssd / unknown |
|---|---|---|---|
| Cayenne | Target file size pinned at 256 MB; the tuner treats the medium as the slow tier — it flushes larger inline batches to amortize metastore commits, classifies the table as I/O-bound at half the latency, drains memory buffers earlier, and withholds write shards sooner; compaction output is written with O_DIRECT, preallocation, and a final fsync, bypassing the page cache; the aggregate encode concurrency across tables is capped from the volume's write bandwidth (the instance's EBS baseline on EC2, otherwise the measured throughput) | Target file size 64 MB; no slow-tier bias | Engine defaults; no bias |
| DuckDB | Connection pool floor of 4 (instead of 10); checkpoint_threshold raised to 256 MiB so each checkpoint amortizes more I/O | checkpoint_threshold raised to 1 GiB | Engine defaults |
| SQLite | busy_timeout 15 s (instead of 5 s); page cache ~200 MB (instead of ~20 MB); 256 MiB of memory-mapped I/O | Engine defaults | Engine defaults |
| Turso | Page cache ~200 MB | Engine defaults | Engine defaults |
Within the network-attached tier, Cayenne also measures the medium rather than treating every volume alike. At registration it writes an 8 MiB probe file to the data directory and to the metastore directory, syncs it, and records the sequential write throughput, once per distinct volume. A volume that measures at or below roughly 125 MiB/s (a baseline gp3 volume) receives the full slow-tier bias; one that measures at or above roughly 1 GiB/s (local NVMe, or a heavily provisioned io2 volume) receives none, with a linear scale in between. On AWS EC2 it also queries the instance metadata service once to learn whether the instance is a burstable T-family type and what its baseline EBS bandwidth is, and caps the process-wide encode concurrency so parallel writers cannot oversubscribe the instance's EBS pipe. See Storage (opens in a new tab) in the Cayenne performance guide.
The detected tier is observable. Cayenne reports cayenne_data_storage_class and cayenne_metastore_storage_class per table (0 local SSD, 1 network-attached, 2 tmpfs, 3 unknown) and the probe results as cayenne_data_storage_write_mibps and cayenne_metastore_storage_write_mibps. A table that reports 1 on a host you believe has local NVMe — or 0 on one you know is on Persistent Disk — is the signal to set storage_profile explicitly.
Local NVMe is the recommended medium for both acceleration data and spill. It delivers the lowest latency and highest IOPS available on any platform, it is included in the instance price on the cloud families that offer it, and it is the only tier on which Cayenne's random-access reads and DuckDB's buffer manager run at their design speed.
It is ephemeral. Cloud instance storage is physically attached to the host and lives only as long as the instance does:
d-suffixed families (opens in a new tab) are lost on stop-deallocate, resize, and host redeployment.Plan for the loss rather than trying to prevent it:
| Strategy | When to use |
|---|---|
| Re-materialize from the source on restart | Refresh from the source is fast enough for the recovery objective. The default behavior; nothing to configure. |
| Acceleration snapshots (opens in a new tab) (Spice.ai Enterprise) | Restarts must be fast and the source is slow or expensive to re-read. A new pod bootstraps from the latest snapshot in object storage instead of refreshing. Designed for exactly this pairing. |
| Read/write separation (opens in a new tab) | Many read replicas on ephemeral storage bootstrapped from snapshots written by one ingesting cluster. |
| Cayenne data on S3 Express One Zone (opens in a new tab) | Cayenne accelerations that must survive instance replacement without a snapshot round trip. Adds single-digit-millisecond latency per cold read; metadata stays local. |
| Cayenne cold object-store tier (opens in a new tab) | Tables larger than local NVMe capacity: recent data stays on NVMe, older data graduates to standard S3. |
Capacity planning. Size the volume for the data, its transient copies, and spill together:
| Item | Planning figure |
|---|---|
| Cayenne data files | 0.25–0.5× raw data size (Vortex with btrblocks), plus room for a compaction pass to hold the old and new copies of a file set at once |
| Cayenne metastore | ~10 MB per 1,000 files, plus its WAL |
| DuckDB file | Roughly the compressed data size, plus the previous copy of the data on every refresh_mode: full until on_full_refresh (opens in a new tab) reclaims it |
| SQLite / Turso file | 1.2–1.5× raw data size (row-oriented, little compression) |
Query spill (runtime.query.temp_directory) | 2–4× the largest join or sort input that can exceed memory; DataFusion stops spilling at 100 GB in total, see Spill-to-Disk (opens in a new tab) |
| DuckDB spill | DuckDB caps its own temporary files at 90% of the volume's free space; budget for the largest out-of-core sort or join |
| Headroom | Keep at least 20% free. Cayenne warns at startup when a data or metastore volume has under 10% or under 2 GiB free, because a full volume fails CDC ingestion. |
File system and mount guidance. Use ext4 or xfs (DuckDB performs best on XFS (opens in a new tab)). Mount with noatime. Mount the device at its own path rather than sharing the root file system: the root volume on a cloud instance is usually a small network-attached disk, and a spill directory that silently lands there is the most common cause of a large query failing with ResourcesExhausted while memory was still available. When an instance has several NVMe devices, either dedicate one to spill or stripe them (RAID 0) for capacity and throughput — the array is no less durable than its members, which are ephemeral anyway.
Amazon EBS, Azure Managed Disks, and GCP Persistent Disk and Hyperdisk are block devices reached over the network. They survive instance replacement, can be snapshotted, and can be detached and reattached elsewhere, which makes them the right choice when an acceleration must outlive its host and a snapshot-based bootstrap is not available. The cost is latency first — every cache miss and every synchronous write is a network round trip of a millisecond or more, against tens of microseconds on NVMe — and, second, the throughput ceilings that both the volume and the instance impose.
Characteristics that affect Spice, using EBS as the reference:
gp3 (opens in a new tab) volume includes a baseline of 3,000 IOPS and 125 MiB/s regardless of size, and can be provisioned up to 80,000 IOPS and 2,000 MiB/s for an additional cost. A gp2 volume earns 3 IOPS per GiB and bursts to 3,000 IOPS on I/O credits — a 100 GiB gp2 volume runs at 300 IOPS once its credits are spent. gp3 does not burst, so it sustains its provisioned figures indefinitely.gp3 and gp2 deliver single-digit millisecond latency. io2 Block Express (opens in a new tab) is designed for an average under 500 µs for 16 KiB I/O with far fewer outliers; Azure Premium SSD v2 and GCP Hyperdisk Extreme are the equivalents.What Spice does on the ebs profile is summarized in How Spice adapts to the storage medium (opens in a new tab): fewer pooled connections and larger checkpoints for DuckDB, larger page caches and longer lock waits for SQLite and Turso, and for Cayenne a slow-tier tuning bias, an O_DIRECT compaction writer that keeps rewritten files out of the page cache, and an encode-concurrency cap derived from the instance's EBS bandwidth.
Guidance:
io2 Block Express, Premium SSD v2, and Hyperdisk Extreme are the sub-millisecond tiers and the right default for accelerations; gp3 with provisioned IOPS and throughput is the economical fallback for data that is mostly served from cache. Provisioned IOPS stop queueing from adding to the latency; they do not reduce it. Do not run accelerations on gp2, whose burst credits are exhausted by exactly the sustained scans and refreshes an accelerator performs.Capping global encode-concurrency budget to the instance EBS write-bandwidth ceiling) when the instance bandwidth binds.runtime.query.temp_directory to a directory on the data volume or on a dedicated volume. When Cayenne is active and the setting is unset, the runtime logs a reminder at startup.d-suffixed types); a spill that runs at NVMe speed holds memory reservations for a fraction of the time, and spill has no durability requirement.storage_profile: ebs explicitly on GCP Persistent Disk and Hyperdisk, on Ceph, SAN, and other network block devices that auto-detection resolves to local_ssd, and on any platform where detection returns unknown but the storage is network-attached.io2 volumes provide no file-system coordination and will corrupt these files.VolumeQueueLength, VolumeReadOps/VolumeWriteOps against the provisioned IOPS, BurstBalance on gp2, and the instance-level EBSIOBalance% and EBSByteBalance% — see Amazon EBS CloudWatch metrics (opens in a new tab). A queue length that grows while IOPS plateau is a saturated volume; a balance that trends to zero is an instance about to be throttled to its baseline.On Azure, prefer Premium SSD v2 or Ultra Disk (opens in a new tab) with explicit IOPS and throughput; on GCP prefer Hyperdisk Balanced or Extreme (opens in a new tab) over pd-ssd. The platform-specific StorageClass recommendations are in the AWS (opens in a new tab), Azure (opens in a new tab), GCP (opens in a new tab), and Helm (opens in a new tab) deployment guides.
Network file systems — on-premises NAS appliances exported over NFS or SMB, Amazon EFS, Azure Files, Google Cloud Filestore, and similar — are not recommended for acceleration files or for the spill directory, and the limitation is not only speed:
fsync, and DuckDB's checkpoints all issue these operations in volume. Throughput on managed offerings is also metered per file system or per share, and shared with every other client.nfs, nfs4, cifs, smbfs, and smb3 mounts as the network-attached (ebs) tier, so the adjustments above apply, including Cayenne's O_DIRECT compaction writer. Those adjustments amortize the latency; they do not remove it, and they cannot make a file-sharing protocol's locks trustworthy.If a network file system is unavoidable — a regulated environment that provides nothing else, for example — limit the exposure:
cayenne_metadata_dir on a local volume) even when data files are on the share, and keep spill local (runtime.query.temp_directory).storage_profile: ebs explicitly if the share is mounted through a user-space client that does not appear in /proc/self/mountinfo with one of the file-system types above.hard, a generous timeo, and nconnect where the client supports it; mount SMB with the 3.1.1 dialect and the largest rsize/wsize the server allows.Using NAS as a data source is a different matter. The NFS (opens in a new tab) and SMB (opens in a new tab) data connectors read Parquet, CSV, and JSON from shares as a federated source; accelerating that data onto local NVMe is the recommended pattern, and it moves the share off the query path entirely.
A tmpfs or ramfs mount — including a Kubernetes emptyDir with medium: Memory — is the fastest medium available, and the runtime recognizes it (Cayenne switches to 64 MB files, DuckDB raises its checkpoint threshold to 1 GiB). It is rarely the right choice:
tmpfs are charged to the process's cgroup. On Kubernetes, "files you write count against the memory limit of the container that wrote them" (Kubernetes volumes (opens in a new tab)). A spill directory on tmpfs therefore cannot relieve memory pressure: a query that spills to it consumes the same memory it was trying to release, and the container is OOM-killed instead of the query slowing down.Use it only when the dataset is small relative to RAM and the workload genuinely needs the last few microseconds of read latency — and never for runtime.query.temp_directory.
Object storage is not a file system and is used in three specific ways:
snapshots.location on that standard bucket. S3 Express One Zone (cayenne_file_path, cayenne_s3_*) is the Cayenne data tier and does not substitute for the snapshot bucket.Spill cannot be directed at object storage, and acceleration files other than Cayenne's cannot be placed on it.
Kubernetes adds a layer between the pod and the disk, and the defaults are wrong for Spice in two ways: the pod's writable layer and a plain emptyDir live on the node's ephemeral storage, which is usually the root disk; and the default StorageClass on every managed cloud is a network block volume.
Follow the step-by-step guide: Local NVMe Storage on Kubernetes (opens in a new tab). It covers choosing NVMe node types on EKS, GKE, AKS, and self-hosted clusters, mounting the disks, publishing them as PersistentVolumes with the Local Volume Static Provisioner, deploying the Helm chart with stateful.enabled: true on a local-storage class, and verifying that Spice detected local SSD. The short version:
io2/gp3 via the EBS CSI driver, Premium SSD v2 via the Azure Disk CSI driver, Hyperdisk via the GCE PD CSI driver) is the fallback when the volume must follow the pod between nodes; set storage_profile: ebs on GCP, where auto-detection cannot identify the device. Never use a ReadWriteMany file-system volume (EFS, Azure Files, Filestore, an NFS provisioner) — see Network file systems (opens in a new tab).runtime.query.temp_directory) goes in a subdirectory of the same volume, such as /data/tmp. A plain emptyDir is acceptable only when the node's ephemeral storage is itself on NVMe (Karpenter's instanceStorePolicy: RAID0, GKE's --ephemeral-storage-local-ssd), with a sizeLimit. Never emptyDir with medium: Memory — see RAM-backed storage (opens in a new tab).The Helm chart's storage class recommendations (opens in a new tab) list the per-cloud StorageClasses in order of preference.
Bind-mount a directory on local NVMe into the container and point the accelerators and runtime.query.temp_directory at it — see Docker persistence (opens in a new tab). Without a mount, acceleration files land in the container's writable layer (on the host's root disk, through the storage driver) and spill lands in the image's temporary directory, which is the same place — from v2.3.1 that is /app/tmp, because the published images are built FROM scratch, carry no /tmp, and set TMPDIR=/app/tmp (see Temporary files (opens in a new tab)). Avoid Docker's --tmpfs for either, for the reasons in RAM-backed storage (opens in a new tab).
On a bare-metal or VM host, set the same two things — acceleration paths and runtime.query.temp_directory — to the NVMe mount. The runtime does not read TMPDIR for anything other than the spill fallback, and it does not relocate the default .spice/data directory, so the paths must be explicit.
Spice Cayenne (opens in a new tab) uses the Vortex (opens in a new tab) columnar format for high-performance analytics on large datasets. Cayenne is self-tuning by default and adapts to the detected storage medium; the sections below cover the knobs that are still worth setting by hand. For the full reference — self-tuning modes, goal-driven SLOs, the storage tier, write-path and compaction tuning — see the Cayenne performance guide (opens in a new tab).
Store Cayenne data files and the metastore on local NVMe. Vortex's random-access encodings turn a selective query into many small reads, and the metastore commits with fsync on every write; both run at the latency of the medium. On network block storage Cayenne biases its tuner, caps its write concurrency, and bypasses the page cache for compaction output automatically — see How Spice adapts to the storage medium (opens in a new tab) — but every cache miss still pays the volume's per-read latency, and provisioned IOPS only keep queueing from adding to it. Keep the metastore (cayenne_metadata_dir) on local disk in every configuration, including S3 Express and network file systems, and keep it outside the data directory (opens in a new tab).
Vortex provides 100x faster random access (opens in a new tab) compared to Apache Parquet through:
is_sorted, is_constant, min, max for query optimizationFor point lookups on large datasets, Spice Cayenne often matches or exceeds the performance of traditional B-tree indexes while consuming no additional memory for index structures. Where a query pins an exact key the data is not clustered on, statistics alone cannot narrow it — declare indexes (opens in a new tab) to have Cayenne build a secondary index (opens in a new tab) for that key set.
Spice Cayenne maintains two in-memory caches that significantly impact query performance. Both are engine-global and set under runtime.params — one segment cache serves every Cayenne table in the process, so a cayenne_segment_cache_mb set under a dataset's acceleration.params is reported at startup and otherwise ignored:
Footer Cache Sizing:
The footer cache stores file metadata. Size based on file count:
Segment Cache Sizing:
The segment cache stores decompressed data. Size based on working set:
0 disables itrate(cayenne_segment_cache_hits[5m]) / rate(cayenne_segment_cache_accesses[5m]) — see segment cache metrics (opens in a new tab)The slower the storage, the more a larger segment cache pays back: on network block storage or S3 Express, size it to the hot working set rather than leaving the derived default.
| Strategy | Read Performance | Write Performance | Compression Ratio |
|---|---|---|---|
btrblocks (default) | Fastest | Moderate | Higher |
zstd | Moderate | Faster | High |
Choose btrblocks for read-heavy analytics workloads. Use zstd only when size on disk is the primary concern—setting zstd trades query performance for reduced storage size.
Set sort_columns to the columns queries filter on most. Sorted files have tight per-segment min/max ranges, so segment pruning skips most of the table for a selective predicate, and is_sorted enables binary search for point lookups. For CDC datasets the adaptive layout also clusters compacted files by the filter columns it observes on scans; an explicit sort_columns outranks that observation. See Sorted data and zone maps (opens in a new tab) in the Cayenne guide.
DuckDB (opens in a new tab) provides mature SQL support with sophisticated query optimization.
:::tip[Datasets 10 GB or larger]
For any dataset of 10 GB or larger, Spice Cayenne (opens in a new tab) is recommended over DuckDB, because of DuckDB's memory requirements. Cayenne typically needs one-third to one-half the memory of the DuckDB accelerator for the same dataset. See Spice Cayenne Performance Optimization (opens in a new tab).
:::
Put the duckdb_file on local NVMe or SSD. DuckDB's own guidance is that its disk-based mode "is designed to work best with SSD and NVMe disks" and that HDDs give low performance, especially for writes; it also recommends against its native format in read-write mode on network-attached file systems (NAS, NFS, SMB) while confirming that network block disks such as EBS work — see DuckDB's environment guide (opens in a new tab) and Storage (opens in a new tab) above.
DuckDB spills its own sorts, joins, and aggregates to a temporary directory. The Spice runtime passes runtime.query.temp_directory to every DuckDB instance it opens, so the one setting covers both engines; when it is unset, DuckDB uses a .tmp directory next to the database file (and .tmp under the working directory for the in-memory instance). DuckDB stops spilling when its temporary files reach 90% of the volume's free space. See Temporary Directory (opens in a new tab).
On the ebs storage profile the runtime raises DuckDB's checkpoint_threshold to 256 MiB and lowers the connection pool floor to 4; on tmpfs the threshold rises to 1 GiB. Set storage_profile (opens in a new tab) explicitly when the device cannot be auto-detected.
Guidelines:
duckdb_memory_limit to control memory per instanceduckdb_file share a connection poolIncrease connection_pool_size for high-concurrency workloads. Each connection consumes memory.
DuckDB supports ART (Adaptive Radix Tree) indexes (opens in a new tab) for faster point lookups:
Indexes consume memory and do not spill to disk (opens in a new tab). Creating an index requires the entire dataset to be loaded into memory. Monitor memory usage when adding indexes. For more details on ART index performance, see the ART paper (opens in a new tab).
DuckDB automatically creates zone-maps (opens in a new tab) (min/max statistics) for each row group, enabling efficient predicate pushdown. In practice, zone-maps on sorted data often outperform ART indexes for range and equality queries while consuming no additional memory.
Why Zone-Maps Outperform Indexes:
Optimization Pattern: Sorted Views
Accelerate a view with ORDER BY to create sorted physical data, then set duckdb_preserve_insertion_order: true to maintain sort order:
Key Configuration:
| Parameter | Value | Purpose |
|---|---|---|
duckdb_preserve_insertion_order | true on sorted view | Maintains physical sort order from ORDER BY |
duckdb_preserve_insertion_order | false on source table | Faster writes without order guarantees |
Separate duckdb_file | Per view | Isolates sorted data from source tables |
Queries filtering on account_id or (account_id, pool_id) benefit from zone-map pruning, skipping entire row groups that don't match the filter predicates.
For a table rather than a view, on_refresh_sort_columns sorts the data after each refresh for the same effect; note that it currently drops indexes and constraints on that table.
Enable aggregate pushdown for improved performance on supported aggregate queries:
Requires query_federation to be disabled. Supports count, sum, avg, min, and max functions.
A repeatedly full-refreshed DuckDB file grows without bound because bulk loads bypass the WAL-triggered checkpoint that would reclaim the previous copy of the data. Set on_full_refresh (opens in a new tab) to replace_file (readers never interrupted, file can shrink) or checkpoint_file (lighter, file plateaus) so the acceleration volume is sized for the data rather than for its history.
SQLite (opens in a new tab) and Turso (opens in a new tab) are row-oriented engines suited to small and medium datasets with simple queries and point lookups. Their performance characteristics are dominated by two things:
fsync, and both are the engines most exposed to the locking problems of network file systems (opens in a new tab). Keep their files on local NVMe/SSD. On the ebs profile the runtime raises SQLite's page cache to ~200 MB and enables 256 MiB of memory-mapped I/O, and raises Turso's page cache to ~200 MB, to absorb per-I/O latency.busy_timeout (SQLite; 5 s by default, 15 s on ebs) if database is locked appears, or move the workload to DuckDB or Cayenne.Neither engine spills intermediate results to disk; SQLite keeps temporary tables and indices in memory (temp_store = memory). Queries that join or aggregate beyond RAM belong on DuckDB or Cayenne, where DataFusion and DuckDB spill.
Spice uses Apache DataFusion (opens in a new tab) as its query execution engine for Arrow and Spice Cayenne accelerators. DataFusion provides vectorized, multi-threaded query execution with automatic memory management and spilling.
DataFusion automatically parallelizes queries across available CPU cores. By default, the number of partitions equals the runtime's CPU entitlement (opens in a new tab) in whole cores, providing maximum parallelism. Override it with runtime.query.target_partitions, or state the entitlement itself with runtime.cpu.cores to size partitions and every other CPU-derived pool together.
DataFusion's GreedyMemoryPool (opens in a new tab) allows memory reservations on a first-come, first-served basis up to the configured memory_limit. This approach improves throughput for high-concurrency queries with many partitions compared to dividing memory evenly.
DataFusion supports multiple join algorithms and automatically selects the best one based on query statistics:
| Algorithm | Memory Usage | Best For |
|---|---|---|
| Hash Join | Higher | Fast execution with sufficient memory (default) |
| Sort-Merge Join | Lower | Memory-constrained environments, pre-sorted data |
| Nested Loop Join | Variable | Cross joins, non-equi joins |
DataFusion prefers hash joins by default for equi-joins. Hash joins do not currently support spilling, so memory-constrained environments may benefit from sort-merge joins for large datasets.
DataFusion pushes filters from operators (TopK, Join, Aggregate) into file scans to prune data early. This optimization is enabled by default and can skip entire row groups or files based on statistics. For example, a SELECT * FROM t ORDER BY timestamp DESC LIMIT 10 query pushes timestamp filters down to file scans, pruning files that cannot contain top-10 candidates.
Spice uses DataFusion's Parquet reader, which applies several optimizations automatically when reading Parquet files from S3, file, Iceberg, and Delta Lake connectors:
These optimizations are applied automatically and require no configuration. The effectiveness depends on data layout—sorting data by frequently filtered columns maximizes row group pruning.
Spice connects to various file formats (Parquet, Iceberg, Delta Lake) and uses DataFusion's query execution engine to push down predicates and prune data at the file, row group, and page level. Understanding these optimizations helps when designing data layouts for optimal query performance.
Apache Parquet stores data in row groups with per-column statistics. DataFusion uses these statistics to skip row groups that cannot contain matching rows.
Row Group Pruning:
Each Parquet row group contains min/max statistics for each column. When a query includes a WHERE clause, DataFusion evaluates whether each row group could contain matching rows based on these statistics. Row groups that cannot match are skipped entirely.
For example, with a predicate WHERE timestamp > '2024-01-01', DataFusion skips row groups where the maximum timestamp is before 2024-01-01.
Page Index:
Parquet's Page Index (opens in a new tab) provides finer-grained statistics at the page level within row groups. DataFusion uses the Page Index to skip individual pages, reducing I/O for selective queries.
Bloom Filters:
Parquet files can include bloom filters for membership testing. For equality predicates like WHERE user_id = 'abc123', DataFusion checks the bloom filter before reading column data. If the bloom filter indicates the value is not present, the row group is skipped.
Late Materialization (Filter Pushdown):
DataFusion supports applying filters during Parquet decoding rather than after. This optimization, called late materialization, filters rows before materializing all columns, reducing memory usage for selective queries.
Apache Iceberg (opens in a new tab) provides hidden partitioning and multi-level metadata filtering that simplifies query optimization.
Hidden Partitioning:
Iceberg automatically derives partition values from source columns using transforms like day(timestamp), month(timestamp), or bucket(user_id, 16). Queries filter on the source column directly (e.g., WHERE timestamp > '2024-01-01'), and Iceberg automatically prunes partitions without requiring users to specify partition columns in predicates.
Two-Level Metadata Filtering:
Iceberg uses a hierarchical metadata structure that enables filtering at multiple levels:
This two-level approach can eliminate entire groups of files before reading any data, providing significant performance benefits for large tables.
Column-Level Statistics:
Iceberg manifests store column-level statistics including:
lower_bound and upper_bound for min/max filteringnull_count for null handling optimizationvalue_count for cardinality estimationPartition Evolution:
Iceberg supports changing partition schemes without rewriting existing data. Historical data retains its original partitioning while new data uses the updated scheme. Queries automatically account for both partition layouts.
Delta Lake (opens in a new tab) provides data skipping and Z-ordering for query optimization.
Data Skipping:
Delta Lake collects column statistics (min, max, null counts) during writes. The delta.dataSkippingNumIndexedCols table property controls how many columns have statistics collected (counted from the first column in the schema). Queries filter using these statistics to skip files that cannot contain matching rows.
Generated Columns:
Delta Lake supports generated columns that derive values from other columns. When partitioned by a generated column (e.g., eventDate generated from CAST(eventTime AS DATE)), queries filtering on the source column automatically benefit from partition pruning.
Z-Ordering:
Z-ordering (opens in a new tab) colocates related data in the same files by clustering on specified columns. After running OPTIMIZE ... ZORDER BY (column), queries filtering on the Z-ordered columns benefit from improved data skipping.
Compaction:
Delta Lake's OPTIMIZE command compacts small files into larger ones, reducing the number of files to scan and improving query performance through better I/O patterns.
Spice Cayenne uses Vortex (opens in a new tab), which provides segment-level statistics and compute push-down on compressed data.
Segment Statistics:
Vortex's ChunkedLayout maintains per-segment statistics including min, max, null_count, is_sorted, and is_constant for each column. These statistics function similarly to DuckDB's zone-maps, enabling segment pruning during query execution.
Compute Push-Down:
Vortex supports executing filter operations directly on compressed data. For encodings like FSST (strings), FastLanes (integers), and ALP (floats), predicates can be evaluated without full decompression, reducing CPU and memory usage.
Encoding-Aware Optimization:
Vortex tracks encoding metadata that enables additional optimizations:
is_sorted: Enables binary search for point lookupsis_constant: Returns values immediately without scanningSee Spice Cayenne Performance Optimization (opens in a new tab) for cache tuning and other Cayenne-specific settings.
| Optimization | Parquet | Iceberg | Delta Lake | Vortex |
|---|---|---|---|---|
| Row group/file pruning | ✅ | ✅ | ✅ | ✅ |
| Page-level filtering | ✅ | ✅ (via Parquet) | ✅ (via Parquet) | ✅ (segment-level) |
| Bloom filters | ✅ | ✅ (via Parquet) | ❌ | ❌ |
| Hidden partitioning | ❌ | ✅ | ❌ | ❌ |
| Manifest-level filtering | ❌ | ✅ | ❌ | ❌ |
| Compute on compressed data | ❌ | ❌ | ❌ | ✅ |
| Z-ordering | ❌ | ✅ | ✅ | ❌ |
Optimization Recommendations:
delta.dataSkippingNumIndexedCols)Configure DataFusion query memory limits to prevent out-of-memory errors:
If not specified, memory_limit defaults to 90% of the memory the process may use — its cgroup memory limit when one binds (a container, a systemd unit's MemoryMax=, a capped parent slice, or a Kubernetes pod cgroup), otherwise total system memory; when a Cayenne acceleration can reach the in-memory CDC tier (refresh_mode: changes or caching, or append with refresh_check_interval ≤ 5m), the default is 70% instead, reserving headroom for Cayenne's compaction memory pool and in-memory CDC tier. A pod whose Cayenne accelerations only bulk-write — refresh_mode: full, or append on a slower cadence — cannot fill that tier and keeps the 90% base, reduced by the off-pool per-table scan caches Cayenne holds for each accelerated table. Datasets, views and catalogs (opens in a new tab) are all classified this way; because changes is the only refresh mode catalog acceleration accepts, an accelerated catalog always reaches the tier, and it contributes one table's worth of off-pool reservation however many tables it goes on to discover (see Accelerated catalogs (opens in a new tab)). Either way the derived limit is floored at 50% of the same memory. For deployments with co-located accelerators, set an explicit limit based on available memory:
Accelerator memory in that expression is per dataset, not per deployment: each accelerated dataset holds its own caches, sized as a fraction of total memory but clamped to a floor that does not shrink with the container. The limit therefore has to be derived for the dataset count, and the ratio that works in a small environment does not carry to a large one. See How Spice Uses Memory (opens in a new tab) and Sizing a Non-Production Environment (opens in a new tab).
Note also that this limit bounds the query execution pool, not the process. Serialization buffers, accelerator caches, and allocator retention sit outside it, which is why lowering it does not always reduce resident memory — see What the Memory Limit Does Not Cover (opens in a new tab).
When the goal is to reduce peak memory, prefer bounding concurrency over lowering this limit:
| Intent | Setting |
|---|---|
| Reduce peak memory under burst | runtime.query.max_concurrent_queries (defaults to 4× the CPU entitlement's cores, not from memory) |
| Reduce per-plan fan-out and its reservations | runtime.query.target_partitions (defaults to the CPU entitlement's cores) |
| Change how much memory a single query may reserve | runtime.query.memory_limit |
Lowering max_concurrent_queries reduces peak memory usage directly, by bounding how many plans hold reservations at once. The trade-off is latency: excess queries wait for admission rather than being refused, so peak end-to-end query time rises with the time spent queueing. Tune it against the workload's tolerance for wait, and watch total query duration — not just execution time — when you change it.
Lowering memory_limit shrinks the pool each query draws from without reducing how many run concurrently, and on a Cayenne CDC deployment it can leave resident memory unchanged because the in-memory CDC tier expands into the freed budget — see Tuning the Memory Limit Safely (opens in a new tab).
When a sort, aggregation, or sort-merge join cannot reserve more memory from the pool, DataFusion writes its intermediate batches to disk as Arrow IPC Stream (opens in a new tab) files and reads them back in a merge pass. Spilling turns an out-of-memory refusal into a slower query — but only if the spill directory is fast enough and large enough to take it.
Where spill goes. runtime.query.temp_directory names the directory; DataFusion creates a uniquely named subdirectory beneath it for each runtime and removes the files when the query completes. The one setting covers every consumer of temporary space:
| Consumer | Uses runtime.query.temp_directory |
|---|---|
| DataFusion query execution (Arrow and Cayenne accelerators, federated and file sources) | Yes — spill for sort, aggregate, and sort-merge join |
| Cayenne background compaction | Yes — its dedicated runtime environment spills to the same directory |
| DuckDB accelerator instances | Yes — passed through as DuckDB's temp_directory; without it DuckDB uses a .tmp directory beside the database file |
| Cluster-mode executors | Yes — the default local working directory for shuffle data |
| Cayenne in-memory CDC tier under memory pressure | No — checkpoints to the table's data directory, which is why that volume needs free space too |
| SQLite and Turso | No — they do not spill; temporary tables are held in memory |
The default is the operating system's temporary directory ($TMPDIR, otherwise /tmp). On most hosts that is the root volume: on a cloud instance a small network-attached disk, in a container the writable layer. Both are the wrong place — slow, small, and shared with logs and images — and a spill that fails for lack of space does not fall back to memory; the query fails with a ResourcesExhausted error, indistinguishable at first glance from an ordinary out-of-memory refusal. When Cayenne acceleration is active and the setting is unset, the runtime logs this at startup:
Set it explicitly, on local NVMe or SSD:
Guidance for the directory:
gp3 volume each of those waits is a millisecond or more, and the spill is additionally bounded by the volume's throughput and competes with data reads for the instance's EBS bandwidth; on a network file system it is slower still. See Storage (opens in a new tab).tmpfs spill directory charges the spilled bytes to the same memory the query was trying to free — see RAM-backed storage (opens in a new tab).The used disk space during the spilling process has exceeded the allowable limit. The cap is not configurable in the current release: there is no runtime.query setting for it, and SET datafusion.runtime.max_temp_directory_size is rejected because Spice's query APIs do not accept SET statements. Queries and compaction have separate environments, so each has its own 100 GB. A workload that approaches the cap needs more memory, fewer concurrent queries, or a smaller working set (more selective predicates, sorted data, sort_columns), not a larger volume.Not every operator spills. Hash joins, the external sort's final merge, and repartition merges cannot spill; a query that exceeds memory in one of those fails regardless of the temporary directory. See Spill Limitations (opens in a new tab).
| Compression | Disk Usage | CPU Overhead | Best on |
|---|---|---|---|
zstd (default) | Lowest | Moderate | Network block storage, bandwidth-limited volumes, small volumes |
lz4_frame | Medium | Lowest | Local NVMe, CPU-constrained hosts |
uncompressed | Highest | None | Local NVMe when CPU is the bottleneck and space is abundant; debugging |
Compression trades CPU for bytes. On a volume whose throughput is the constraint (EBS at its provisioned MiB/s, an instance near its EBS bandwidth cap), zstd writes fewer bytes and usually finishes sooner. On local NVMe, which can absorb several GB/s, the compression itself can become the bottleneck, and lz4_frame or uncompressed is often faster end-to-end. Measure with the actual spill-heavy query rather than assuming; the difference shows in total query duration.
DataFusion processes data in batches of 8192 rows by default. This batch size balances memory usage with vectorized execution efficiency. Larger batches improve CPU cache utilization and SIMD operations but consume more memory per partition.
Spice supports multiple caching layers for query acceleration.
Cache query results for repeated queries:
Cache Key Types:
| Type | Behavior | Use Case |
|---|---|---|
plan (default) | Uses query logical plan | Varied query formatting |
sql | Uses exact SQL string | Identical repeated queries |
Use sql for lowest latency with identical queries. Use plan for semantic query matching.
Serve stale cached results while refreshing in the background:
This pattern reduces query latency spikes during cache refresh.
| Mode | Memory Impact | Use Case |
|---|---|---|
full | 2.5x dataset | Small-medium datasets, complete updates |
append | Minimal | Time-series, logs, immutable data |
changes | Minimal | CDC-enabled sources |
caching | Minimal | Dynamic content, API data |
A full refresh also rewrites the acceleration file, so its cost scales with the storage medium: on network block storage a full refresh of a large DuckDB or Cayenne table is bounded by the volume's write throughput, and a DuckDB file grows by the whole table on every cycle until on_full_refresh (opens in a new tab) reclaims the space. Prefer append or changes for anything large.
Use time_column for efficient incremental updates:
For data that has a granular time column and a separate column for partitioning (i.e. day buckets), set time_partition_column to the partitioning column based on time:
In this scenario, event_date is the day bucket used for physical partitioning (e.g., s3://bucket/events/event_date=2025-10-01/), while event_time provides the granular timestamp for precise filtering.
| event_id | event_time (time_column) | event_date (time_partition_column) | event_type | user_id |
|---|---|---|---|---|
| 8f2a-1 | 2025-10-01 08:14:22.123 | 2025-10-01 | page_view | u_442 |
| 8f2a-2 | 2025-10-01 22:01:05.884 | 2025-10-01 | click | u_901 |
| 9c11-a | 2025-10-02 01:12:44.001 | 2025-10-02 | purchase | u_442 |
| 9c11-b | 2025-10-02 14:30:12.550 | 2025-10-02 | page_view | u_118 |
For object storage with append-only files, use last_modified to skip unchanged files:
On ephemeral storage every restart is a cold start. Acceleration snapshots (opens in a new tab) (Spice.ai Enterprise) upload the acceleration file after each refresh and download the newest one on boot when the local file is missing, so a replacement pod on fresh NVMe serves queries in the time it takes to copy the file rather than the time it takes to re-read the source. Enable snapshots_compaction for DuckDB so the uploaded file is compact.
Configure resource requests and limits based on workload:
For the volume itself — node types with NVMe, mounting the disks, and publishing them as PersistentVolumes — follow Local NVMe Storage on Kubernetes (opens in a new tab).
:::tip[CPU Limits]
Avoid setting CPU limits. CPU limits can cause throttling (opens in a new tab) even when CPU is available, degrading query performance. Set CPU requests to guarantee scheduling.
To bound how wide Spice builds its thread pools without imposing a CFS quota, use runtime.cpu.cores (opens in a new tab) rather than resources.limits.cpu. It caps how much machine the runtime organizes itself around; it does not cap how much CPU the process may consume, so there is no throttling.
:::
Do not back the data or spill volume with emptyDir: { medium: Memory } — the files count against the container's memory limit, so a spill cannot relieve memory pressure and a large acceleration file becomes an OOM kill. See Storage on Kubernetes (opens in a new tab) for the volume options.
The runtime sizes its thread pools, query fan-out, and accelerator concurrency from one CPU entitlement. Every CPU-derived pool scales with that number, and so does memory: worker threads, partitions, and per-plan operator reservations all grow with it, roughly linearly. A value far above the cores actually available buys nothing and costs both scheduling overhead and memory.
A pod that follows the advice above — requests, no limits — is sized for a bounded multiple of its CPU request, not for the whole node. A requests.cpu: 4 pod on a 64-core node sizes for 8 cores. This is the default and needs no configuration: the Spice Helm chart and the Spice Kubernetes Operator both pass the pod's CPU request through automatically whenever one is set.
That default suits the common case — a pod scheduled against a request, sharing a node. Two deployments want something else:
| Intent | Configuration |
|---|---|
| Pack many mostly-idle instances on a node, each free to burst across the whole machine | runtime.cpu.cores: all, small request |
| Size for a specific number regardless of what the pod requests | runtime.cpu.cores: 6 |
| Hard-cap CPU consumption because cluster policy requires it | resources.limits.cpu (accepts throttling) |
A hand-written pod spec that sets a CPU request but does not pass it through gets neither behavior — it falls back to sizing for the machine, and the runtime warns at startup naming the variable to set. See Sizing from a CPU request (opens in a new tab).
Compare spiced_cpu_budget_cores against spiced_cpu_request_millicores and spiced_cpu_limit_millicores to see what a pod sized for and what it was chosen against; the source label says which rung produced it. See runtime.cpu (opens in a new tab).
| Storage Type | Acceleration data files | Query spill files (runtime.query.temp_directory) |
|---|---|---|
| Local NVMe / SSD (instance store, bare metal) | Recommended | Recommended |
| Network block storage (EBS, Azure Disk, PD/Hyperdisk) | Durable fallback; provision IOPS; storage_profile: ebs where undetected | Acceptable; not the root volume |
RAM-backed (tmpfs, emptyDir medium: Memory) | Small datasets only | Never |
| Network file systems (NFS, SMB, EFS, Azure Files) | Not recommended | Not recommended |
| Object storage | Cayenne data files on S3 Express One Zone; the Cayenne cold tier on standard S3; snapshot copies on standard S3, GCS, or ADLS | Not supported |
| HDD | Cold archives only | Not recommended |
See Storage (opens in a new tab) for the reasoning, the per-engine adjustments, and the platform specifics.
Enable query plan capture for slow query analysis:
Monitor key performance metrics:
query_duration_ms - Query execution timequery_executions - Query throughputquery_failures with err_code="ResourcesExhausted" - Memory refusals and failed spillsquery_memory_pool_used_bytes against process_resident_anon_bytes - Pool usage versus off-pool memorydataset_load_state - Acceleration statuscayenne_segment_cache_hits / cayenne_segment_cache_accesses for CayenneStorage-related metrics:
cayenne_data_storage_class and cayenne_metastore_storage_class — the detected storage tier per table (0 local SSD, 1 network-attached, 2 tmpfs, 3 unknown); cayenne_data_storage_write_mibps and cayenne_metastore_storage_write_mibps — the measured write throughputcayenne_storage_bytes, cayenne_data_dir_bytes, and cayenne_metastore_db_bytes / cayenne_metastore_wal_bytes — the on-disk footprint, see storage footprint metrics (opens in a new tab)cayenne_write_phase_duration_ms by phase and cayenne_compaction_duration_ms — where write and compaction time goes; a rising vortex_write or publish_commit phase on an unchanged workload is the storage slowing downawait and queue depth from iostat, free space on the data and spill volumes, and on AWS the EBS VolumeQueueLength, BurstBalance, EBSIOBalance%, and EBSByteBalance% metricsSee Observability (opens in a new tab) for metric configuration.
Use this checklist when optimizing Spice deployments:
duckdb_file, sqlite_file, turso_file, cayenne_file_path, cayenne_metadata_dir) on local NVMe/SSD, not the root volume, a network file system, or a RAM-backed mountruntime.query.temp_directory to a directory on local NVMe/SSD with 2–4× the largest spillable input freestorage_profile explicitly where auto-detection cannot see the device (GCP Persistent Disk, SAN, non-Linux hosts)io2 Block Express, Premium SSD v2, Hyperdisk Extreme), provision IOPS and throughput, and choose an instance whose EBS bandwidth sustains them; plan for instance-store data loss with snapshots or re-materializationruntime.query.max_concurrent_queries before lowering runtime.query.memory_limitrefresh_mode: append or changes for large and time-series data; set on_full_refresh on full-refreshed DuckDB filessort_columns, on_refresh_sort_columns, sorted views)runtime.cpu.cores to bound thread poolsSpice Documentation:
acceleration.storage_profile (opens in a new tab) - Storage profile referenceruntime.query.temp_directory (opens in a new tab) - Temporary directory referenceExternal References:
emptyDir, local, and memory-backed volumes