Performance
What Quarry does and does not do for query performance.
Quarry is a query builder. It does not add its own performance layer on top of ClickHouse.
That means Quarry does not:
- optimize your schema
- rewrite queries for speed
- infer
PREWHERE - decide when
FINALis appropriate - choose insert batch sizes for you
- tune server settings automatically
Quarry mostly gives you typed access to the SQL features ClickHouse already has.
What this means in practice
For performance work, use the same judgment you would use with handwritten ClickHouse SQL.
Typical ClickHouse performance concerns still apply:
- schema design
- partitioning and sort keys
- filtering strategy, including
PREWHEREvsWHERE - whether
FINALis necessary - aggregation memory use, including functions such as
groupArray(...) - insert batch sizing
- query settings such as
max_threads
Quarry's role
Quarry can help you inspect the final SQL with toSQL(), but it does not try
to be an optimizer.
const compiled = db
.selectFrom("event_logs as e")
.select("e.user_id")
.where("e.event_type", "=", "signup")
.toSQL();If performance matters, inspect the SQL Quarry produced and then apply normal ClickHouse performance analysis to that query.
TypeScript and large schemas
Compiler performance is separate from ClickHouse execution performance. Quarry validates a selected source directly and avoids enumerating every possible table alias for large schemas. The improvement is part of the exported declarations, so downstream applications benefit on supported TypeScript versions without upgrading their compiler.
Repository maintainers can reproduce the consumer checks with:
pnpm test:performance
node scripts/check-type-performance.mjs --nativeThe fixtures import built declarations and cover 710 database objects, 1,000 distinct wide tables, service-specific schemas, joined selections, CTE chains, and array operations. CI records timings and checks an instantiation budget under pinned TypeScript 6.0.2. Wall time is not a CI gate because shared runners vary. Counter values are only comparable under the same compiler version.
The standalone core/CLI checker also runs under TypeScript 7. Core declarations
use tsdown’s Oxc emit with isolatedDeclarations; the classic TypeScript 6
dependency remains for validation and tooling. Docs retain TypeScript 5.9.
Consumers can use TS 5.9, 6, or 7 independently of the compiler used to build Quarry.
For application code, select only needed columns, keep exported row/query types
understandable, and profile a representative consumer with --extendedDiagnostics
before changing types. Deep generic wrappers and unusually wide schemas can have
different costs from these fixtures. Do not disable strictness to hide a slowdown.
Give each service the schema it needs
Keep one generated schema and derive a smaller type for query modules that use only a few tables:
import { createClickHouseDB } from "quarry";
import type { DB } from "./generated-schema";
type AnalyticsDB = Pick<DB, "events" | "users">;
const analytics = createClickHouseDB<AnalyticsDB>({ client });
const query = analytics.selectFrom("events as e")
.innerJoin("users as u", "e.user_id", "u.id")
.select("u.id", "e.event_type");This reuses your client and preserves the generated column types. TypeScript
validates the selected table names, so schema regeneration still catches a removed
or renamed table. Other services can keep the full DB or choose a different
subset. The full schema remains available for introspection and other modules.
The performance fixture includes both full and scoped builders over the same 1,000-table schema. Narrowing to the eleven tables used by its queries reduced check time in local experiments; actual gains depend on your schema and queries. There is no benefit from narrowing if the module needs every table.
A Pick type limits the builder's compile-time API. It does not restrict the
ClickHouse connection's permissions or authorize tenant access.