N023-H1 Tier 2 · Core SQL · hard ecommerce · Brightlane

Return one row per unique identifier

Part of UNION, UNION ALL, INTERSECT, EXCEPT in SQL

The problem

Brightlane's compliance team needs a single deduplicated column listing every way a customer can be identified in the system — both their numeric ID and their email address — in one combined list.

Write a query to return one row per unique identifier.

Assumptions:

  • The customers table contains every customer Brightlane has on file.
  • The id column is integer; the email column is text. Both must appear in the same output column.
  • The result is deduplicated — if any ID happens to also appear as an email value, it appears once.

Output:

  • One row per distinct identifier, with a single column identifier.
Schema · ecommerce 5 tables
categories
id integer
name text
parent_id? integer
products
id integer
name text
category_id integer
price numeric
stock_qty integer
attributes? jsonb
order_items
id integer
order_id integer
product_id integer
quantity integer
unit_price numeric
customers
id integer
name text
email text
city? text
country text
created_at timestamptz
is_active boolean
orders
id integer
customer_id integer
ordered_at timestamptz
status text
total_amount numeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution Try it yourself first
Solution query
SELECT
  id::TEXT AS identifier
FROM
  customers
UNION
SELECT
  email
FROM
  customers

The shape

The id::text cast is doing all the work. Without it, the left side projects an integer and the right side projects text, and PostgreSQL refuses to stack incompatible types across a set operation. Casting the integer ID to text makes both columns text, the two sides reconcile, and the compliance team gets a single deduplicated identifier column holding both numeric IDs and email addresses.

Clause by clause

  • SELECT id::text AS identifier FROM customers is the left input. The ::text cast converts the integer id into its text representation — 1 becomes '1', 42 becomes '42'. The AS identifier alias names the output column; the column name comes from the left query, so this is the alias that survives.
  • UNION is the set operator. PostgreSQL evaluates both queries, then sorts or hashes the combined result to deduplicate. UNION (not UNION ALL) is what gives the compliance team "one row per unique identifier."
  • SELECT email FROM customers is the right input. The email column is already text, so no cast is needed. The two sides now agree on column count (one) and column type (text), which is what lets the set operation run.

Why this and not the uncast version

The uncast version (SELECT id ... UNION SELECT email ...) fails outright. PostgreSQL refuses to stack an integer column with a text column across UNION — the error is UNION types integer and text cannot be matched. There is no implicit coercion between numeric and text types for set operations; the engine raises and the query never produces rows.

The cast resolves it because text is the only type that can hold every value the prompt cares about. An email can't be written as an integer, but an integer can be written as text. So the cast goes on the integer side, promoting it to the wider type the text side already speaks.

The trap

The trap is reading the type-mismatch error as a syntax issue and trying to fix it by reordering the queries or removing the alias. Neither helps. The constraint is type compatibility across corresponding column positions, and the fix is an explicit cast on whichever side has the narrower type. The wider type is the destination; the cast is the bridge.

You practiced reconciling type mismatches across a UNION. The recurring rule: corresponding columns from both sides must resolve to a compatible type — when they don't (integer vs text here), UNION raises a type error that an explicit CAST resolves.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.

Practice, feedback, mastery, review. That's the loop that turns reading into real skill.

Start free

No account, no credit card. Start solving in under a minute.