What does WHERE (dept_id, grade) IN ((1,'A'),(2,'B')) match, and how does it differ from two IN lists?
answer
- Look at what the parentheses on the left group
- Each list element is a tuple, not a value
- Compare it with dept_id IN (1,2) AND grade IN ('A','B')
- Expands to an OR of ANDs, not a cross product
basics
~20 sThat is a row-value IN predicate: it matches only the exact pairs (1,'A') and (2,'B'). Two separate IN lists on dept_id and grade would form the cross product and also match (1,'B') and (2,'A'). Engine support varies.
solid answer
~40 sThe parenthesised column list is a **row value constructor**, so the predicate compares whole tuples: a row qualifies only if `(dept_id, grade)` equals `(1,'A')` or equals `(2,'B')`. Written as separate predicates — `dept_id IN (1,2) AND grade IN ('A','B')` — you get the cross product of the two lists, four combinations, so a grade-B row in department 1 also passes. The row-value form is the compact way to say "any of these specific combinations", and it expands to `(dept_id = 1 AND grade = 'A') OR (dept_id = 2 AND grade = 'B')`. Comparison is positional and the tuples must have the same degree and comparable types. Support is not universal: PostgreSQL and MySQL accept it, some engines do not, so check yours before relying on it in portable code.
code
sql · 12 lines-- exact pairs only
SELECT * FROM employees
WHERE (dept_id, grade) IN ((1, 'A'), (2, 'B'));
-- portable equivalent
SELECT * FROM employees
WHERE (dept_id = 1 AND grade = 'A')
OR (dept_id = 2 AND grade = 'B');
-- NOT the same: cross product, also matches (1,'B') and (2,'A')
SELECT * FROM employees
WHERE dept_id IN (1, 2) AND grade IN ('A', 'B');go deeper
Recognise the syntax and know that each parenthesised item is a whole pair, so only the listed combinations match. You are not expected to write it unprompted.
Expand it to the OR-of-ANDs and articulate why two separate IN lists are a cross product and therefore a different filter. Mention that support varies by engine.
Judge when it earns its place — composite-key lookups and exact-combination filters — versus when the pair set should live in a table, and note the portable fallback for engines that lack it.
Weigh a non-universal construct against the codebase's portability commitments: a clearer predicate is worth little if it blocks a future engine move or forces a dialect-specific code path.
## Row values Standard SQL lets you build a **row value constructor** — an ordered tuple of expressions written in parentheses — and compare it against another row value. `IN` accepts a list of them: ```sql SELECT * FROM employees WHERE (dept_id, grade) IN ((1, 'A'), (2, 'B')); ``` The comparison is positional and element-wise: the first element of the left tuple is compared with the first element of each right tuple, and so on. Two row values are equal when every corresponding pair is equal. So the predicate above is exactly: ```sql WHERE (dept_id = 1 AND grade = 'A') OR (dept_id = 2 AND grade = 'B') ``` The two tuples must have the same **degree** (number of elements) and pairwise comparable types; a mismatch is an error, not a silent coercion of the whole tuple. ## Why it is not the same as separate IN lists The naive rewrite people reach for is: ```sql WHERE dept_id IN (1, 2) AND grade IN ('A', 'B') ``` That is a different question. It accepts **any combination** of a listed department with a listed grade — the cross product, four pairs here: (1,'A'), (1,'B'), (2,'A'), (2,'B'). If your intent was "grade A in department 1 and grade B in department 2", this quietly returns extra rows. The bigger the two lists, the worse the leak: two lists of 10 values express 100 combinations, while a row-value list of 10 tuples expresses exactly 10. This is the reason the construct exists. Without it you write the OR-of-ANDs by hand, which is verbose and easy to get wrong with mismatched parentheses, or you build a small derived table of the allowed pairs and join to it — which works but changes the shape of the query and reintroduces the row-multiplicity question if the pair list has duplicates. ## Where else row values show up Row value constructors are not specific to `IN`. They also work with the comparison operators, where the comparison is **lexicographic**: ```sql WHERE (created_at, id) > (TIMESTAMP '2024-05-01 10:00:00', 12345) ``` meaning "later than that timestamp, or the same timestamp with a larger id". Recognising the tuple syntax in one place helps you read it in the other. ## NULLs Row comparison inherits SQL's three-valued logic. If an element on either side is NULL, the equality of that pair is UNKNOWN, and the tuple comparison can be UNKNOWN rather than TRUE or FALSE — so a row with a NULL in one of the compared columns will not be returned by the predicate. Treat tuple membership tests the same way you treat any equality on a nullable column: if NULL should participate, handle it explicitly. ## Portability Row value constructors are in the SQL standard, but engine support is uneven, and it is uneven per-context: an engine may accept row values in a comparison but not in an `IN` list, or accept them only in certain positions. PostgreSQL and MySQL both accept the `IN`-list form shown here. Before you use it in code that has to run on more than one engine, verify on each of them — and if it is not available, fall back to the explicit OR-of-ANDs, which is portable everywhere and semantically identical. ## When to reach for it Good fits: filtering on a composite key (`(order_id, line_no) IN (…)`), fetching a specific set of (tenant, entity) pairs, or any "these exact combinations" filter assembled by an application. Poor fits: long generated lists, where the same arguments apply as to any oversized literal list, and cases where the pairs really live in a table already, in which case join to that table instead of inlining the data. ## What to say Name it as a row value constructor, give the OR-of-ANDs expansion, and contrast it with the cross-product behaviour of two independent `IN` lists. That contrast is the whole point of the question, and it is where a candidate either sees the difference in intent or does not.
- How would you write the same filter without row value constructors?As an explicit disjunction of conjunctions: `(dept_id = 1 AND grade = 'A') OR (dept_id = 2 AND grade = 'B')`. That is portable everywhere and semantically identical. The alternative is to put the allowed pairs in a derived table or temporary table and join, which also works but changes the query shape and can duplicate rows if the pair set has duplicates.
- Row value constructors also appear with < and >. What does (a, b) > (1, 5) mean?It is a lexicographic comparison: true when a > 1, or when a = 1 and b > 5. It is not `a > 1 AND b > 5`. This is the reason the syntax turns up in ordered-scan predicates over a composite sort key, where you want "everything after this exact position".
- What happens if one of the compared columns is NULL?Equality on that element is UNKNOWN, so the tuple comparison is UNKNOWN rather than TRUE, and WHERE drops the row. Row-value membership tests give nullable columns the same treatment as any other equality test — if NULL should count as a match you have to say so explicitly.
saying these in an interview costs you the question
- Says it is equivalent to two independent IN lists
- Thinks the tuples are matched by column name rather than position
- Assumes every engine supports row values in an IN list
- Expects (a,b) > (1,5) to mean a > 1 AND b > 5
- Believes tuple lists of different degrees are padded automatically