Compare
hypequery vs Raw SQL Strings in TypeScript
Raw SQL strings are the right call for one-off scripts and gnarly analytical queries. For the queries embedded in your app, hand-written SQL and interfaces drift from your schema. Here is where the line sits — and why hypequery keeps the raw-SQL escape hatch.
Decision type
Architecture and workflow fit
Audience
TypeScript teams building on ClickHouse
Outcome
Choose a path and move into implementation
Best for
Reused, app-embedded queries that must stay in sync with the schema
Schema drift
Rename a column and every affected query fails to compile
Refactoring
Find-references and rename work across the codebase
Injection safety
Parameterized by construction; values never interpolated
Escape hatch
selectExpr / rawAs / withCTE for the hard parts
What this page is for
Use this page when the real question is how one tool changes the shape of your ClickHouse application code, not just which syntax looks nicer.
What this page is not
It is not a broad ecosystem roundup. It stays narrow on the tradeoff a ClickHouse-heavy TypeScript team is actually deciding.
Recommended next move
If the tradeoff already looks clear, stop reading comparisons and test the fit against one real query in your own schema.
Short answer
Do you even need a query builder? Sometimes not. If you're writing a one-off backfill script, a migration, or one genuinely gnarly analytical query — window functions, ASOF joins, three nested CTEs — a raw SQL string is the clearest thing you can write, and reaching for an abstraction just adds noise. hypequery agrees with this. It ships selectExpr, rawAs, and withCTE precisely so you can drop to raw SQL whenever the builder gets in the way. You never have to fight it.
The case for the builder isn't "raw SQL is bad." It's about the other SQL — the queries that live inside your application, get reused across endpoints, filtered by user input, and refactored as the schema changes. That's where raw SQL strings plus hand-written result interfaces quietly rot: rename a column and nothing fails until runtime. This page is about that practice — SQL strings wherever they live, in template literals, .sql files, or any client — not about any one library. (For the narrower comparison with the official transport client, see hypequery vs @clickhouse/client, which hypequery is actually built on.)
If you'd rather just see it run, the quick start goes from a live ClickHouse schema to a typed query in a few minutes.
Where raw SQL is genuinely the right call
Be honest about this up front, because it's where a lot of "just use a library" advice goes wrong:
- One-off scripts and backfills. A script you run once and delete doesn't benefit from types. Write the SQL, run it, move on.
- Migrations and DDL.
CREATE TABLE,ALTER, engine and partition tuning — this is raw SQL territory and always will be. - Genuinely gnarly analytics. A query with several window functions, an ASOF JOIN, and correlated subqueries is often clearer as SQL than as any fluent chain. hypequery doesn't have an
asofJoinmethod, and it's honest about that — ASOF JOIN is raw-SQL territory.
If that's the shape of your work, you may not need a builder at all. The builder earns its place when queries are repeated and embedded, not throwaway.
Where hand-written SQL strings start to hurt
The problems don't show up on day one. They show up three months later, in an app with fifty queries scattered across route handlers.
Schema drift is silent. You rename total to amount in ClickHouse. Your hand-written string SELECT sum(total) ... still compiles, still type-checks against your hand-written interface OrderRow, and still passes review. It fails at runtime, in production, when ClickHouse rejects the unknown column. Nothing in your toolchain warned you.
There's no refactoring support. Rename a column and you're doing a project-wide string search, hoping you catch every .sql file and every template literal. "Find all references" doesn't work on strings. Neither does rename-symbol.
Reuse becomes copy-paste. The same 20-line revenue query gets pasted into three endpoints with slightly different WHERE clauses. Now a fix has to land in three places, and they drift apart over time. (This exact pattern is the subject of stop writing the same query three times.)
Interpolation is an injection risk. The moment a raw string stops being static and starts including a runtime value, you're one careless template literal away from an injection bug:
Parameterization fixes this — but it's opt-in, and "remember to parameterize every value, forever, across the whole team" is not a safety model. It's a hope.
Before and after
Here's a realistic analytics query — daily revenue by region for one tenant — written both ways. First, the raw-string version with a hand-written interface:
That interface is a promise nobody enforces. revenue is typed as number, but sum() of a numeric column comes back from ClickHouse as a string in JS. Rename total and the string keeps compiling. The as RevenueRow[] cast papers over all of it.
Now the same query with hypequery:
No hand-written interface. The result type is inferred from the generated schema — including that revenue is a string, because that's what ClickHouse actually returns. tenant_id is parameterized by construction; there's no template literal to get wrong. Rename total in ClickHouse, re-run hypequery generate, and .sum('total', ...) stops compiling — in your editor, before the code ships.
The builder has edges, and it tells you where they are
This is the part that makes the tradeoff fair: hypequery doesn't pretend to cover every ClickHouse feature. Window functions aren't first-class builder methods. You express them with selectExpr, which drops a raw SQL expression into a typed .select():
And .withCTE() takes a raw SQL string when a subquery is easier written by hand than built. The point of these escape hatches is that adopting the builder is not an all-or-nothing bet. You get compile-time safety on the 90% of queries the builder covers cleanly, and raw SQL for the 10% where SQL is genuinely the better tool — in the same query, side by side. You never have to abandon the typed layer just because one clause is exotic.
The honest comparison
| Raw SQL strings | hypequery | |
|---|---|---|
| Best for | One-off scripts, migrations, gnarly analytics | Reused, app-embedded queries |
| Schema drift | Silent until runtime | Fails to compile |
| Refactoring | String search only | Find-references, rename-symbol |
| Result types | Hand-written, unenforced | Generated from live schema |
| Runtime type mapping | You remember it (UInt64 → string) | Encoded in generated types |
| Injection | Safe only if you never interpolate | Parameterized by construction |
| Reuse | Copy-paste | Composable query definitions |
| Exotic SQL | Native | selectExpr / rawAs / withCTE |
Getting started
If your queries are one-offs, keep writing SQL — that's the right tool, and hypequery would just be in the way. If they live in your app and keep changing, the quick start takes you from schema introspection to a typed query and a served endpoint in a few minutes. From there, the ClickHouse query builder guide covers filters, joins, and aggregations against generated types, and migrating raw SQL to hypequery walks through converting an existing string-based codebase one query at a time — keeping the raw-SQL escape hatch wherever it still makes sense.
Decision checkpoint
If the tradeoff is already clear, move into implementation
Most teams do not need another round of comparison content after this point. The better test is whether the workflow holds up on your own schema and your own query complexity.
Related content
Continue into implementation
FAQ
Is raw SQL ever the right choice over hypequery?
Yes. For a one-off backfill script, a migration, or a genuinely complex analytical query with window functions and ASOF joins, raw SQL is clearer than any builder. hypequery is aimed at the queries that live in your application and get reused, filtered, and refactored over time — not throwaway SQL.
Do I have to give up raw SQL entirely to use hypequery?
No. hypequery has escape hatches precisely so you never fight the builder: selectExpr and rawAs drop raw SQL expressions into a typed .select(), and .withCTE() takes a raw SQL subquery. You get compile-time safety on the parts the builder covers and raw SQL for the parts it doesn't.
How is this different from comparing hypequery to @clickhouse/client?
This page is about the practice of writing raw SQL strings and hand-written result interfaces wherever they live — template literals, .sql files, any client. The comparison with the official @clickhouse/client is about that specific transport library, which hypequery is actually built on top of.
Does the builder cover window functions and FINAL?
Partly. FINAL, LIMIT BY, CTEs, and array joins are first-class builder methods. Window functions are not — you express them with selectExpr('... OVER (...)', 'alias'). The builder is honest about its edges and hands you raw SQL where it stops.
Next step
Move from evaluation into a typed ClickHouse workflow
Generate schema types, define your first reusable query, and decide whether it should run locally or over HTTP.