CMD Guide
HomeDatabasesSQL Practice Problems

Unused Accounts

Problem

Table: Accounts

+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| account_id    | int     |
| account_name  | varchar |
+---------------+---------+
account_id is the primary key (column with unique values) for this table.
Each row of this table contains the ID and the name of an account in the bank.

Table: Transactions

+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| transaction_id| int     |
| account_id    | int     |
| transaction_date | date |
+---------------+---------+
transaction_id is the primary key (column with unique values) for this table.
Each row of this table contains the ID of a transaction, the ID of the account that initiated the transaction, and the date when the transaction was made.

Problem Definition

Write a solution to find all accounts that did not make any transactions in 2020.

Return the result table ordered by account_name in ascending order.

Example

Input: 
Accounts table:
+------------+--------------+
| account_id | account_name |
+------------+--------------+
| 1          | Alice        |
| 2          | Bob          |
| 3          | Charlie      |
+------------+--------------+
Transactions table:
+----------------+------------+-----------------+
| transaction_id | account_id | transaction_date|
+----------------+------------+-----------------+
| 1              | 1          | 2020-09-01      |
| 2              | 2          | 2020-09-02      |
| 3              | 1          | 2020-09-03      |
| 4              | 3          | 2019-08-21      |
| 5              | 2          | 2021-07-03      |
+----------------+------------+-----------------+
Output: 
+--------------+
| account_name |
+--------------+
| Charlie      |
+--------------+

Try It Yourself

sql
-- TODO: Write your user queries here

Solution

To identify all accounts that did not make any transactions in the year 2020, we can leverage SQL's LEFT JOIN along with conditional filtering. This approach allows us to include all accounts and exclude those that have associated transactions in the specified year.

SQL Query

SELECT A.account_name FROM Accounts AS A LEFT JOIN Transactions AS T ON A.account_id = T.account_id AND YEAR(transaction_date) = '2020' WHERE T.account_id IS NULL ORDER BY account_name ASC;

Step-by-Step Approach

Step 1: Perform a Left Join Between Accounts and Transactions for the Year 2020

Combine the Accounts and Transactions tables to associate each account with its transactions in the year 2020. The LEFT JOIN ensures that all accounts are included, even if they have no transactions in 2020.

SQL Query:

SELECT A.account_name, T.account_id FROM Accounts AS A LEFT JOIN Transactions AS T ON A.account_id = T.account_id AND YEAR(transaction_date) = '2020';

Explanation:

Output After Step 1:

Assuming the example input provided, the intermediate result after the LEFT JOIN would be:

+--------------+------------+ | account_name | account_id | +--------------+------------+ | Alice | 1 | | Alice | 1 | | Bob | 2 | | Charlie | NULL | +--------------+------------+

Step 2: Filter Accounts Without Transactions in 2020

Identify accounts that did not make any transactions in 2020 by selecting records where the joined Transactions data is NULL.

SQL Query:

SELECT A.account_name FROM Accounts AS A LEFT JOIN Transactions AS T ON A.account_id = T.account_id AND YEAR(transaction_date) = '2020' WHERE T.account_id IS NULL;

Explanation:

Output After Step 2:

Based on the intermediate result, the filtered output would be:

+--------------+ | account_name | +--------------+ | Charlie | +--------------+

Step 3: Order the Results by Account Name in Ascending Order

Sort the final list of accounts alphabetically by account_name to present the data in an organized and readable manner.

SQL Query:

ORDER BY account_name ASC;

Explanation:

Final Output:

+--------------+ | account_name | +--------------+ | Charlie | +--------------+

Pattern: anti-join (set difference) with predicate in ON

Name: anti-join — "rows in A with no matching row in B under condition C." Here: accounts with no login in year 2024.

-- LEFT JOIN … IS NULL (anti-join); year filter MUST stay in ON
SELECT a.account_name
FROM Accounts a
LEFT JOIN Logins l
  ON l.account_id = a.account_id AND YEAR(l.login_date) = 2024
WHERE l.account_id IS NULL;

-- Preferred portable form: NOT EXISTS
SELECT a.account_name
FROM Accounts a
WHERE NOT EXISTS (
  SELECT 1 FROM Logins l
  WHERE l.account_id = a.account_id
    AND l.login_date >= '2024-01-01' AND l.login_date < '2025-01-01'
);

Critical ON vs WHERE trap: if you move YEAR(login_date)=2024 into the outer WHERE, unmatched accounts still have NULL login_date and fail the year predicate — or matched non-2024 logins behave differently. Putting the year filter in WHERE on the right table columns effectively destroys anti-join semantics for "no 2024 login." Keep selective match conditions in ON (or inside NOT EXISTS).

Sargability: YEAR(login_date) = 2024 is non-sargable on login_date. Prefer half-open range login_date >= '2024-01-01' AND login_date < '2025-01-01' so a btree on (account_id, login_date) can seek.

The NOT IN + NULL poison (why not to write it as NOT IN): the tempting form WHERE account_id NOT IN (SELECT account_id FROM Transactions WHERE YEAR(transaction_date)=2020) silently breaks if the subquery yields any NULL. x NOT IN (a, b, NULL) expands to x<>a AND x<>b AND x<>NULL; the last term is UNKNOWN, so the whole predicate can never be TRUE and the query returns zero rows — a purge job that deletes nothing. NOT EXISTS and LEFT JOIN … IS NULL do not have this 3-valued-logic trap and are the correct anti-join encodings.

When-NOT: EXCEPT on account ids works if you only care about id sets and no extra columns; NOT EXISTS scales better with indexes for correlated anti-semijoins.

Drill: Explain why Alice can appear twice in an intermediate LEFT JOIN before the IS NULL filter (multiple non-matching or matching login rows fan out) and why the final anti-join still returns each unused account once when filtered correctly.

🤖 Don't fully get this? Learn it with Claude

Stuck on Unused Accounts? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Unused Accounts** (Databases) and want to truly understand it. Explain Unused Accounts from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Unused Accounts** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Unused Accounts** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Unused Accounts** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes