> hypequery

ClickHouse Behavior

The ClickHouse-specific behavior that most often affects how you use hypequery.

hypequery is built for ClickHouse, so some of the behavior you see is really ClickHouse behavior showing through.

This page covers the main things worth knowing so the API feels less surprising.

What hypequery exposes directly

These are the ClickHouse features that show up clearly in the builder:

FeatureNotes
PREWHERESeparate from WHERE, with prewhere(...) and orPrewhere(...)
FINALExposed directly as .final() because it is a table-read concern
Query SETTINGSPassed through to ClickHouse for that execution
GLOBAL IN / GLOBAL NOT INExposed through globalIn and globalNotIn operators
IN subqueriesExposed through inSubquery and globalInSubquery
IN table referencesExposed through inTable and globalInTable
Tuple membershipExposed through inTuple and globalInTuple
WITH clause CTEsExposed through withCTE(...)
WITH scalar expressionsExposed through withScalar(...)
ARRAY JOIN / LEFT ARRAY JOINExposed through arrayJoin(...) and leftArrayJoin(...)
LIMIT ... BY ...Exposed through limitBy(...)
GROUP BY ... WITH TOTALSExposed through withTotals()
ClickHouse time bucketingExposed through groupByTimeInterval(...) and related helpers
Explicit null helpersPrefer whereNull(...) and related helpers over bare null equality
LEFT JOIN null semantics controlUse .settings({ join_use_nulls: 1 }) when SQL-style nulls matter

The point is not to hide ClickHouse behind a generic SQL layer. These features are exposed because they matter in real queries.

PREWHERE is different from WHERE

PREWHERE is its own ClickHouse clause. It is not just another spelling of WHERE.

Use:

  • prewhere(...) for highly selective predicates that should run earlier
  • where(...) for the rest of your filtering logic

hypequery keeps them separate and does not try to rewrite one into the other for you.

FINAL belongs to the table read

FINAL is about how ClickHouse reads a table. It is not a general-purpose query modifier.

In hypequery you can use:

  • .final() for simple single-table reads
  • source-level FINAL behavior when the query shape gets more advanced

Use it when merge-time correctness matters for the table you are reading.

Array joins are their own ClickHouse feature

ARRAY JOIN and LEFT ARRAY JOIN are not ordinary relational joins.

Use:

  • arrayJoin(...) when array elements should expand into separate rows
  • leftArrayJoin(...) when you want that expansion but still need rows with empty arrays preserved

These map directly to ClickHouse array-join behavior.

LIMIT BY is different from LIMIT

LIMIT BY limits rows per grouping key. It does not just cap the final result set.

Use:

  • limitBy(count, by) when you want a per-group cap
  • limit(count) when you want a cap on the final result

You can use both in the same query when needed.

Query settings pass through to ClickHouse

.settings({...}) becomes ClickHouse query settings for that execution.

This is the right place for settings like:

  • max_execution_time
  • join_use_nulls
  • max_threads
  • final

The SQL text and the settings payload stay separate. Settings affect execution, not the SQL string itself.

LEFT JOIN defaults are a ClickHouse behavior

ClickHouse does not default unmatched LEFT JOIN columns to null unless join_use_nulls = 1 is enabled.

That means the right-hand side of a LEFT JOIN may come back with type defaults instead of null unless you opt into null semantics.

If you need SQL-style null behavior, set:

  • .settings({ join_use_nulls: 1 })

That is a ClickHouse rule, not a hypequery quirk.

Null handling is explicit

Plain null equality is not a great fit for how ClickHouse works.

Prefer:

  • whereNull(...)
  • whereNotNull(...)
  • prewhereNull(...)
  • prewhereNotNull(...)

These helpers make the intent obvious and avoid pretending ClickHouse behaves like Postgres.

Runtime values come from JSONEachRow

The values you get back should match what the ClickHouse JavaScript client returns from JSONEachRow.

That means some values may not come back in the JavaScript shape people expect at first glance.

Common examples:

ClickHouse valueTypical runtime value
UInt64 / Int64string
count() and other 64-bit integer aggregatesstring
DateTime / DateTime64string
Decimal(p, s)number

One important caveat: JavaScript numbers can lose precision for larger decimal values even when ClickHouse stored them exactly.

Practical guidance

If you are deciding where something belongs, the usual rules are:

  • use prewhere(...) for highly selective early filters
  • use where(...) for the rest of your filtering logic
  • use final() when merge-time correctness matters for the table read
  • use .settings({...}) for ClickHouse runtime behavior like join_use_nulls or max_threads
  • use explicit null helpers when you mean null checks

On this page