Skip to content
All writing Part 09 of 09 · [object Object]
Engineering · 4 min read

Cross-Engine Query Exercises (With Answers)

Test yourself: identify the operation first, then write it in each engine. Worked ClickHouse, Spark, and MongoDB exercises with answers — counting NULLs, array membership, segmentation, and more.

Test yourself: read the scenario, figure out “which operation is this” first, then “how does this engine write it,” and only then reveal the answer. Example schema is analytics.events throughout; fields vary by scenario.

ClickHouse

1. Count only the sign-ups that filled in an email (excluding NULL)

Scenario: events has a signup_email field, some of it NULL. Get both the total row count and the count with email.

Show answer
SELECT count(*) AS total, count(signup_email) AS with_email
FROM analytics.events;

count(*) includes NULL, count(col) only counts non-NULL; the difference is the number of missing emails. ↳ count(*) vs count(col)

2. Find campaigns whose name contains “summer” (case-insensitive)

Scenario: search for the substring summer inside field_name.

Show answer
SELECT * FROM analytics.events
WHERE field_name ILIKE '%summer%';

ILIKE = case-insensitive LIKE; % matches any characters. ↳ LIKE / ILIKE substring search

3. Filter rows where the tags array contains ‘vip’

Scenario: tags is Array(String), and you want to know whether vip is one of the elements.

Show answer
SELECT * FROM analytics.events
WHERE has(tags, 'vip');

has(arr, val) is ClickHouse’s array membership check. ↳ ClickHouse array functions

4. Find requests where at least one latency exceeds 500ms

Scenario: latencies is a numeric array; you want it if any element is > 500.

Show answer
SELECT request_id FROM analytics.events
WHERE arrayExists(x -> x > 500, latencies);

arrayExists is equivalent to Python’s any(). ↳ ClickHouse array functions

5. Segment users by spend and activity

Scenario: classify users into segments using layered conditions.

Filter fields: total_spent and days_active

  1. VIP_Active: total_spent >= 5000 & days_active <= 14
  2. VIP_At_Risk: total_spent >= 5000
  3. Loyal_Regular: total_spent >= 1000
  4. everything else falls through to Standard
Show answer
SELECT user_id,
    multiIf(
        total_spent >= 5000 AND days_active <= 14, 'VIP_Active',
        total_spent >= 5000, 'VIP_At_Risk',
        total_spent >= 1000, 'Loyal_Regular',
        'Standard'
    ) AS segment
FROM analytics.events;

multiIf is a compact version of CASE WHEN. ↳ ClickHouse array functions

6. Default to unknown when email is NULL

Scenario: signup_email is partly NULL; replace NULL with the string unknown on output.

Show answer
SELECT ifNull(signup_email, 'unknown') AS email
FROM analytics.events;

ClickHouse uses ifNull to default a null (Spark uses NVL). ↳ ClickHouse array functions

Spark

1. Per-customer list of items bought (with duplicates vs. deduplicated)

Scenario: roll multiple rows of item_bought into an array → one column keeps duplicates, the other doesn’t. So you get three columns: id, all_items, unique_items.

Show answer
SELECT customer_id,
    collect_list(item_bought) AS all_items,
    collect_set(item_bought)  AS unique_items
FROM store_sales
GROUP BY customer_id;

collect_list keeps duplicates, collect_set de-dupes. ↳ Spark array functions and collect

2. Filter rows where tags contains ‘vip’

Show answer
SELECT * FROM events
WHERE array_contains(tags, 'vip');

Spark’s array membership check is array_contains. ↳ Spark array functions and collect

3. Multiply local prices by an exchange rate

Scenario: prices is a numeric array, multiply each element by rate.

Show answer
SELECT transform(prices, x -> x * rate) AS usd_prices
FROM orders;

transform(arr, x -> ...) applies an expression to every element. ↳ Spark array functions and collect

4. Keep only latencies over 500ms

Show answer
SELECT filter(latencies, x -> x > 500) AS slow
FROM events;

WHERE filters whole rows, filter filters elements inside a single row’s array — different levels: to keep “rows that have any latency > 500” use WHERE exists(latencies, x -> x > 500) (but the array stays intact); to drop the ≤ 500 elements inside the array, you need filter.

filter(arr, x -> ...) keeps elements matching a predicate. ↳ Spark array functions and collect

5. Default to ‘unknown’ when email is null

Show answer
SELECT NVL(signup_email, 'unknown') AS email FROM events;

Spark uses NVL to default a null (ClickHouse uses ifNull). ↳ Spark array functions and collect

MongoDB

1. Age between 20 and 40 (exclusive)

Show answer
db.users.find({ age: { $gt: 20, $lt: 40 } });

You can stack multiple comparison operators on the same field. ↳ MongoDB operators and vocabulary

2. name contains “abc” (case-insensitive) — the LIKE equivalent

Show answer
db.users.find({ name: { $regex: "abc", $options: "i" } });

$regex ≈ SQL LIKE, $options: "i"ILIKE. ↳ MongoDB operators and vocabulary

3. Documents where email is present

Show answer
db.users.find({ email: { $exists: true } });

In Mongo, Absent ≠ null: $exists asks “does this key exist at all.” ↳ MongoDB operators and vocabulary

4. type is one of a / b / c — the IN equivalent

Show answer
db.users.find({ type: { $in: ["a", "b", "c"] } });

$in ≈ SQL IN. ↳ MongoDB operators and vocabulary

5. Minors or VIPs

Show answer
db.users.find({ $or: [{ age: { $lt: 18 } }, { vip: true }] });

$or / $and / $nor take an array. ↳ MongoDB operators and vocabulary


References

Related: back to the Cross-Engine DB Query overview — the deep dives behind these answers: count(*) vs count(col), LIKE / ILIKE, ClickHouse array functions, Spark array functions, and MongoDB operators.

Tags #query-engines #sql #databases
// connect

Be brave | Be wise | Be grateful

21 BreakinCode

// elsewhere
LinkedInMedium (lang: en)Life RecordYoutube
wh:~$William Hung· © 2026 Taipei · GMT+8 · Available for collaboration