When would you write an INNER JOIN whose ON predicate is a range instead of an equality?
answer
- The matching rule is containment, not equal keys
- Think price bands and validity windows
- ON accepts any boolean expression
- Overlapping bands emit the row twice
- Half-open beats an inclusive upper bound
basics
~20 sWhen the match is defined by containment rather than equal keys: assigning an amount to a price tier, a timestamp to a validity window, or a value to a bucket. The ON clause accepts any boolean predicate, not only column equality.
solid answer
~50 s`ON` takes an arbitrary boolean expression, so `ON s.amount >= t.min_amount AND s.amount < t.max_amount` is a perfectly ordinary inner join — usually called a non-equi or range join. The classic uses are lookups whose key is an interval: banding a value into tiers, resolving a rate or price that was valid at a given moment, mapping a measurement to a bucket. The join rule does not change; only the predicate does. Two things need care. First, the ranges must be **non-overlapping**, because a row that satisfies two ranges is emitted twice — the join does not pick a best match. Second, the boundaries must tile without gaps and without double-counting, which is why half-open intervals (`>= lower AND < upper`) are safer than `BETWEEN`, whose endpoints are both inclusive and so make adjacent bands touch. Equality is just the special case where the predicate happens to be `=`.
code
sql · 6 lines-- Assign each sale to its price tier by containment
SELECT s.id, s.amount, t.tier_name
FROM sales s
INNER JOIN tiers t
ON s.amount >= t.min_amount
AND s.amount < t.max_amount; -- half-open: no boundary double-matchgo deeper
Know that an ON clause is not restricted to = and can compare a value against a range; recognise a tier or validity-window lookup when you see one.
Explain why overlapping ranges duplicate rows and why half-open intervals are the right way to write contiguous bands, including the timestamp case.
Show that you validate the assumptions the join depends on — bands that neither overlap nor leave gaps — and that you check whether rows disappeared for lack of a matching interval.
Own the modelling decision: whether banding rules live as interval rows joined at query time or as a denormalised assignment, and what the team gives up when the rules change.
## ON is a predicate, not a key comparison The join rule is "emit the pairs where the ON predicate is TRUE". Nothing in that rule mentions equality. `ON` accepts any boolean expression over columns of the two sides: inequalities, ranges, several conditions combined with `AND` or `OR`, expressions over columns. A join whose predicate is not a plain equality is conventionally called a **non-equi join**; when the predicate is an interval containment, a **range join**. ## The canonical use: an interval-keyed lookup ```sql -- tiers: (min_amount, max_amount, tier_name), non-overlapping bands SELECT s.id, s.amount, t.tier_name FROM sales s INNER JOIN tiers t ON s.amount >= t.min_amount AND s.amount < t.max_amount; ``` The match is defined by containment: this sale's amount falls inside this tier's band. There is no shared key column to equi-join on, and inventing one (storing a `tier_id` on every sale) would duplicate the banding rule in the data and go stale the moment the bands change. The same shape covers temporal lookups — the rate, price or exchange rate that was in force at a given instant: ```sql SELECT o.id, o.amount * r.rate AS amount_usd FROM orders o INNER JOIN fx_rates r ON r.currency = o.currency AND o.placed_at >= r.valid_from AND o.placed_at < r.valid_to; ``` Note the mix: one equality (`currency`) plus a range. That is common and entirely normal — an ON clause combines conditions freely. ## Overlapping ranges silently duplicate This is the failure mode interviewers probe. A join does not choose a best or first match; it emits **every** qualifying pair. If two tiers overlap and a sale falls in both, that sale appears twice, once per tier, and every downstream consumer sees two rows where the business means one. The join is behaving exactly as specified; the *data* violated the assumption that the bands partition the space. The defensive habits are: enforce non-overlap where you can, and verify it when you cannot — ```sql -- does any amount fall into more than one tier? SELECT a.tier_name, b.tier_name FROM tiers a INNER JOIN tiers b ON a.tier_name <> b.tier_name AND a.min_amount < b.max_amount AND b.min_amount < a.max_amount; ``` An empty result means the bands do not overlap. ## Half-open intervals beat BETWEEN `BETWEEN x AND y` is shorthand for `>= x AND <= y` — **both** endpoints inclusive. For contiguous bands that is the wrong shape: if one tier ends at 100 and the next begins at 100, a value of exactly 100 matches both and is emitted twice. Writing the band as `>= min AND < max` (half-open) makes each boundary belong to exactly one band, with no gap and no overlap. The same argument applies with extra force to timestamps, where an inclusive upper bound forces you to guess the last representable instant before the next period begins. Also watch the argument order: `BETWEEN` requires the lower bound first. `x BETWEEN hi AND lo` with the bounds swapped is not an error — it is simply never TRUE, so the join silently returns nothing. ## Missing rows are still dropped A range join is an inner join, so a row that falls into **no** band disappears entirely. An amount below the lowest tier's floor, or a timestamp before the first rate's start, is not an error — the row is just absent from the result. If the bands are supposed to cover everything, a shrinking row count is your signal that they do not. ## Other non-equi shapes Inequality predicates appear beyond banding: pairing each row with rows that came before it, joining on a tolerance (`ABS(a.v - b.v) < 0.01`), or on containment in some other sense. The mechanics are always the same — every TRUE pair is emitted — and the same duplication caution applies, more strongly, because an open-ended inequality can match a great many partners per row. ## The interview answer "When the relationship is containment rather than equal keys — tiers, validity windows, buckets. ON takes any boolean predicate. I write the bands half-open rather than with BETWEEN so the boundaries do not double-match, and I make sure the ranges do not overlap, because the join emits every match rather than picking one."
- What happens if two tiers overlap and a sale falls into both?The sale is emitted twice, once per matching tier. A join has no notion of a best or first match — it returns every pair whose predicate is TRUE. The duplication is a data problem: the bands were assumed to partition the space and do not. Verify non-overlap, or make the bands half-open.
- Why prefer `>= min AND < max` over `BETWEEN min AND max` for contiguous bands?`BETWEEN` includes both endpoints, so when one band ends where the next begins, a value exactly on the boundary matches both and is duplicated. A half-open interval assigns each boundary to exactly one band, leaving no gap and no overlap — and it avoids having to guess the last representable instant for timestamp ranges.
- What happens to a row whose value falls into no band at all?It is dropped, because this is still an inner join: no qualifying pair, no output row. Nothing warns you. If the bands are meant to be exhaustive, compare the row count before and after the join, or add a catch-all band with open-ended bounds.
saying these in an interview costs you the question
- Believes ON can only compare columns for equality
- Thinks the join picks the closest or first matching range
- Uses BETWEEN for contiguous bands and double-counts boundaries
- Assumes rows matching no range are returned with NULLs
- Writes BETWEEN with the bounds reversed and expects matches