--- title: "Performance" slug: "dev-database-operations-performance" description: "Optimize your queries and loads with effective table structures, batching, and parallelism. Learn what you control and what to ask your platform team." updated: 2026-10-30T18:19:06Z published: 2026-10-30T18:19:06Z canonical: "docs.vastdata.com/dev-database-operations-performance" --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.vastdata.com/llms.txt > Use this file to discover all available pages before exploring further. # Performance *Applies to VAST Cluster 5.5. Examples use the VAST DataBase Python SDK 2.1.0.* Some of what decides how fast your queries and loads run is under your control: how you lay out tables, what you ask for in a query, how you batch writes, and how much parallelism the client uses. The rest is cluster configuration, which your platform team owns. This page covers both sides, so you know what to change yourself and what to ask for. ## What you control and what the cluster controls | Area | Who controls it | Where to read more | | --- | --- | --- | | Table structure: sorted, partitioned, semi-sorted projections | You, when you create the table | [Data modeling](/develop/docs/dev-database-guides-data-modeling) | | Which columns and rows a query asks for | You | [Ask for less](/develop/docs/dev-database-operations-performance#ask-for-less) | | Write batch sizes and load method | You | [Write in large batches](/develop/docs/dev-database-operations-performance#write-in-large-batches) | | Client parallelism: splits, endpoints, import workers | You | [Spread work across endpoints](/develop/docs/dev-database-operations-performance#spread-work-across-endpoints) | | Which CNodes serve VAST DataBase (virtual IP pools) | Platform team | [Cluster settings](/develop/docs/dev-database-operations-performance#cluster-settings-to-ask-about) | | QoS limits on views and users | Platform team | [Cluster settings](/develop/docs/dev-database-operations-performance#cluster-settings-to-ask-about) | | Where Trino or Spark runs | Platform team and you | [Running Trino or Spark on the cluster](/develop/docs/dev-database-operations-performance#running-trino-or-spark-on-the-cluster) | ## Choose the table structure for your queries Table structure has the biggest effect, and most of it can't be changed after you create the table. [Data modeling](/develop/docs/dev-database-guides-data-modeling) explains how to choose. In short: - **Sorted tables** improve queries that filter on the sort columns ([Sorted tables](/docs-5-5/docs/sorted-tables)). - **Partitioned tables** let a query skip partitions that can't match, and deleting a whole partition is much faster than deleting rows ([Database partitioning](/docs-5-5/docs/database-partitioning)). - **Semi-sorted projections** are used by queries automatically. `QueryConfig.use_semi_sorted_projections=False` turns that off, for example to compare timings. Why data order decides how much of a table a query reads is explained in [read path and pruning](/develop/docs/dev-database-concepts-architecture-read-path-and-pruning). ## Ask for less Both of these are applied on the cluster, so less data travels to your client: - **Columns.** `select(columns=[...])` reads only the columns you list. With `columns=None` you get every column. In Trino or Spark, avoid `SELECT *` for the same reason. - **Filters.** A predicate passed to `select()` is evaluated on the cluster. Write filters the cluster can evaluate; [querying](/develop/docs/dev-database-guides-querying) and [pushdowns](/develop/docs/dev-database-reference-pushdowns) list what pushes down. How the SDK uses your filter depends on the table: - On a **partitioned** table, the SDK first picks the partitions that can match, using only the parts of the filter on partition source columns, and then queries those partitions. A filter that doesn't mention a partition column scans every partition. - On a **sorted** table, the SDK asks the cluster how many rows the filter is likely to match, and uses that to decide how many splits to run. `limit_rows` stops a query once that many rows have come back, which saves work when you only need a sample. Set up an example table in a schema called `ops_performance`, using the environment variables from the [quickstart](/develop/docs/dev-database-quickstart#2-connect): ``` import os import pyarrow as pa import vastdb from vastdb.config import QueryConfig session = vastdb.connect() BUCKET = os.environ["VASTDB_BUCKET"] columns = pa.schema([ ("id", pa.int64()), ("sensor", pa.utf8()), ("reading", pa.float64()), ("note", pa.utf8()), ]) rows = pa.table({ "id": list(range(10_000)), "sensor": [f"s{i % 10}" for i in range(10_000)], "reading": [float(i % 100) for i in range(10_000)], "note": ["x" * 100] * 10_000, }, schema=columns) with session.transaction() as tx: schema = tx.bucket(BUCKET).create_schema("ops_performance", fail_if_exists=False) table = schema.create_table("readings", columns, fail_if_exists=False) table.insert(rows) print("Inserted", rows.num_rows, "rows") ``` ``` Inserted 10000 rows ``` Ask only for the two columns and the rows you need: ``` with session.transaction() as tx: table = tx.bucket(BUCKET).schema("ops_performance").table("readings") result = table.select( columns=["id", "reading"], predicate=(table["sensor"] == "s3") & (table["reading"] > 90), ).read_all() print(result.schema.names) print(result.num_rows, "rows") ``` ``` ['id', 'reading'] 100 rows ``` ## Write in large batches Each `insert()` call is at least one request, so many small inserts cost more than a few large ones. Insert into sorted tables in batches as large as possible: it improves both insert and query performance (see [Sorted tables](/docs-5-5/docs/sorted-tables)). What the SDK does with one large batch (client behaviour in SDK 2.1.0): - A request can carry up to 5 MiB. If the whole batch fits, with a small safety margin, it goes in one request. - If it doesn't fit, the SDK inserts as many columns as fit, splitting the rows across requests if it must, to get row IDs. It then writes the remaining columns with update requests. So a very wide batch turns into several requests. Logging `vastdb._by_column_inserter` at DEBUG shows which path a batch took. - A single row that can't fit in a request raises [TooWideRow](/develop/docs/dev-database-troubleshooting-errors#vastdberrorstoowiderow-pyarrowrecordbatch). For **partitioned** tables, inserts benefit when rows are grouped per partition before they're written (see [Database partitioning](/docs-5-5/docs/database-partitioning)). The SDK's [buffered inserter](/develop/docs/dev-database-reference-sdk#buffered-inserts-partitioned-tables) does that grouping for you; `BufferedInsertConfig` sets the buffer size (10 MiB by default) and how many rows a partition collects before it's flushed (65,536 by default). For files already in a VAST bucket, **import** them instead of reading them into the client and inserting. With `import_files()` the cluster reads the Parquet files itself, so the data never passes through your client. See [ingest](/develop/docs/dev-database-guides-ingest). There's one known slow case in 5.5: importing into a table that is both sorted and partitioned "takes considerably longer" than importing into a table that's neither. ## Spread work across endpoints A VAST DataBase endpoint is a virtual IP (VIP) on a CNode. The SDK spreads queries and imports over several of them. **How the SDK finds endpoints.** When you open a table, the cluster returns the VIPs of the pool that serves it, and the SDK uses those as its data endpoints. If the cluster returns none, the SDK logs a warning and uses only the endpoint you connected to. You can see what was discovered in [get_stats().endpoints](/develop/docs/dev-database-operations-monitoring-your-workload#table-statistics). To choose the endpoints yourself, set `SessionConfig(data_endpoints=[...])` for the whole session, or `data_endpoints` in `QueryConfig` or `ImportConfig` for one call. **Queries.** The SDK divides a query into *splits*, disjoint subsets of the table's rows, and runs them in parallel ([read path](/develop/docs/dev-database-concepts-architecture-read-path-and-pruning#how-a-query-is-divided)): | Setting (`QueryConfig`) | Default | Effect | | --- | --- | --- | | `num_splits` | Estimated as `num_rows // rows_per_split`, at least 1 | How many splits, each its own stream of requests. | | `rows_per_split` | 4,000,000 | Used for the estimate above. | | `num_sub_splits` | 4 | Concurrent reads inside each request, on the cluster. | | `data_endpoints` | The discovered endpoints | One client worker thread per entry. You can list an endpoint more than once to get more threads. | | `queue_priority` | Unset | A non-negative integer. Requests with lower values are served before those with higher values. | So a small table runs as a single split by default. For a large table, more endpoints and more splits mean more parallel work, until the client or network is the bottleneck. Measure with your own data; there are no published figures for 5.5. To process splits in separate workers yourself, for example one per process, use `select_splits()`. It returns one reader per split. It doesn't work on partitioned tables. ``` config = QueryConfig(num_splits=4) with session.transaction() as tx: table = tx.bucket(BUCKET).schema("ops_performance").table("readings") readers = table.select_splits(columns=["id"], config=config) total = sum(reader.read_all().num_rows for reader in readers) print(len(readers), "readers,", total, "rows") ``` ``` 4 readers, 10000 rows ``` To pin a query to particular endpoints, pass them in `QueryConfig`. The addresses come from your platform team: ``` config = QueryConfig( data_endpoints=["http://vip1.example.com", "http://vip2.example.com", "http://vip3.example.com"], num_splits=12, ) with session.transaction() as tx: table = tx.bucket(BUCKET).schema("ops_performance").table("readings") result = table.select(columns=["id"], config=config).read_all() ``` **Trino splits.** The VAST connector for Trino also divides each scan into splits, set by `num_of_splits` in the catalog or the `vast.num_of_splits` session property ([Trino](/develop/docs/dev-database-integrations-trino#session-properties)). It uses fewer splits than that for a small table only when it knows the table is small, from row statistics that `ANALYZE` collects: - A large table that had never been analyzed used the full `num_of_splits` for every scan. After `ANALYZE`, the same scan used fewer splits, and a table of a few rows used one. - A selective filter on an analyzed table, a filter on a sorted table's sort columns, or a filter on a partitioned table's partition column also used fewer splits. - The `vast.query_data_rows_per_split` session property made no visible difference to the split count. In the same tests, one split made a full scan two to three times slower than eight, while 8, 64 and 256 splits were within run-to-run noise. Those are single-endpoint loopback results on one Trino worker; measure on your own cluster. `ANALYZE vast."<database>/<schema>".<table>` stores the statistics as an object in the table's database (bucket), so they survive a Trino restart. Re-run it after large loads. `SHOW STATS FOR` shows what Trino knows: before `ANALYZE`, only the row count. **Imports.** `import_files()` runs `ImportConfig.import_concurrency` workers. The default is twice the number of CNodes behind the endpoints. Each request carries up to 10 files, and workers take endpoints round-robin across CNodes. The SDK hands out whole files, so each file is imported by one worker. A load of a few very large files can't use more workers than it has files. **Throttling.** If the cluster is busy, it can answer with `Slowdown` (HTTP 503). The SDK retries those automatically with backoff. See [errors](/develop/docs/dev-database-troubleshooting-errors#vastdberrorsslowdown-serviceunavailable-http-503) and [Retries](/develop/docs/dev-database-reference-sdk#retries). ## Clean up ``` with session.transaction() as tx: schema = tx.bucket(BUCKET).schema("ops_performance") schema.table("readings").drop() schema.drop() print("Dropped ops_performance") ``` ``` Dropped ops_performance ``` ## Cluster settings to ask about These are set by your platform team. If your client side looks fine and things are still slow, ask about them. **A virtual IP pool dedicated to VAST DataBase.** Use one, containing only the CNodes that serve database work, so that database workloads and other cluster services don't affect each other's performance (see [Configuring the VAST cluster for database access](/docs-5-5/docs/configuring-the-vast-cluster-for-database-access)). Ask how many CNodes and VIPs the pool has: the SDK spreads work over those VIPs. **A Query Engine pool.** The VAST Query Engine needs its own virtual IP pool with the Query Engine role ([VAST Query Engine](/docs-5-5/docs/vast-query-engine#accessing-vast-databases-using-the-query-engine)). A Query Engine pool can't be modified after creation; it has to be deleted and recreated. **QoS policies.** Platform teams can cap bandwidth and IOPS per view, and bandwidth, IOPS and the number of S3 connections per user ([Creating a QoS policy](/docs-5-5/docs/creating-a-qos-policy)). If a limit applies to your user or database view, your throughput stops at that limit however you tune the client. **Admin task: send to your platform team.** See [Configuring the VAST cluster for database access](/docs-5-5/docs/configuring-the-vast-cluster-for-database-access) and [Creating a QoS policy](/docs-5-5/docs/creating-a-qos-policy). ## Running Trino or Spark on the cluster Trino and Spark can run as managed applications on CNodes of the VAST cluster, sharing those CNodes' compute with VAST storage ([Managed applications on clusters](/docs-5-5/docs/managed-applications-on-clusters)). Setting this up is part of VAST DataEngine, which has its own documentation. For what changes for you as a user, see [Trino](/develop/docs/dev-database-integrations-trino#trino-as-a-managed-application-on-the-cluster) and [Spark](/develop/docs/dev-database-integrations-spark#spark-as-a-managed-application-on-the-cluster). **Admin task: send to your platform team.** See [Managed applications on clusters](/docs-5-5/docs/managed-applications-on-clusters). ## See also - [Monitoring your workload](/develop/docs/dev-database-operations-monitoring-your-workload): logging and statistics to find out where time goes. - [Data modeling](/develop/docs/dev-database-guides-data-modeling): choosing the table structure. - [Python SDK reference](/develop/docs/dev-database-reference-sdk#queryconfig): every `QueryConfig` and `ImportConfig` field. - See also: [Optimize queries](/develop/docs/dev-database-guides-optimize-queries), [Profile a query](/develop/docs/dev-database-guides-profile-a-query) and [Compression and data reduction](/develop/docs/dev-database-concepts-architecture-compression).