What the Capital One CodeSignal Data Analyst Assessment Actually Asks (Candidate Reports, 2021–2026)

Published September 2026 · 13 min read · 🎯 Interview Prep

Capital One does not publish the format of its data analyst screen. What exists is a trail of candidate reports on Blind from 2021 to 2025 and a handful of prep guides dated 2025 and 2026, and they mostly agree: about 70 minutes, around 14–15 questions, provided CSV or Excel datasets, mostly multiple choice, plus written SQL. This post lays out what those sources say and where they disagree, then works each named SQL pattern on a card-transactions schema — the shape the datasets are described as having — and ends with the two mistakes that cost points and a 2-week plan.

In this article

  1. The format, as reported (and where reports disagree)
  2. The schema used in every example below
  3. INNER vs LEFT JOIN — and when a join duplicates rows
  4. GROUP BY with COUNT, SUM, AVG, MIN, MAX
  5. CASE WHEN — conditional metrics and buckets
  6. CTEs and subqueries
  7. Window functions — ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM OVER
  8. Date filtering — day, week, month, quarter, rolling window
  9. The two mistakes that cost points on dataset questions
  10. A 2-week prep plan
  11. FAQ
  12. Sources

The format, as reported (and where reports disagree)

Everything in this section is attributed. None of it comes from Capital One, and one reply on the most recent Blind thread — from someone posting as a Capital One employee in June 2025 — warns that the format "has changed in last 4 years". Read the dates as part of the claim.

Source (dated)What it says
Blind, Dec 2021 (Senior Data Analyst)"70 minutes code signal assessment"
Blind, Feb 2022"14 questions instead of just 4" (vs the engineering test); "you can use a scripting language of choice (Python, R) or Excel"; "one question is a database question you have to use SQL for"
Blind, Nov 2022"14 questions and all of them were pretty lengthy"; "csv data to answer multiple choice questions"; "few sql questions"
Blind, Jun 2025"around 14–15 questions in 70 minutes"; "CSV files involved in some of the questions"
linkjob guide, Aug 2025"about 70 to 80 minutes"; "CSV data files"; joins, aggregations, GROUP BY, subqueries, CTEs, ROW_NUMBER / RANK
extrabrain guide, Feb 2026"roughly 70 to 80 minutes"; datasets that "resemble real customer, transaction, account, product, or campaign datasets"; the full SQL list worked below
finalroundai guide (analyst track)"around 70 minutes"; "multiple MCQ questions and 4 to 6 general questions"; SQL joins, aggregations, window functions

Where they agree: roughly 70 minutes, provided datasets (CSV or Excel), mostly multiple-choice questions answered from those datasets, and written SQL. Where they disagree:

What this means for prep: the multiple-choice questions are dataset questions, so the answer is a number, and a query that runs but double-counts is simply wrong. The SQL you need is not exotic — it is the six patterns below, executed without fan-out and at the right grain, under a clock.

The schema used in every example below

The February 2026 guide describes the datasets as resembling "customer, transaction, account, product, or campaign" tables. Capital One is a card issuer, so every example here runs on a card-transactions schema — the synthetic fraud ledger inside SQL Quest's Banking track, which you can query in the browser. Column names are real:

TableColumnsGrain
accountsaccount_id, email, signup_at, country, device_fingerprint, ip_block, statusone row per account
merchantsmerchant_id, name, category, country, risk_tierone row per merchant
transactionstxn_id, account_id, amount, txn_at, merchant_id, lat, lng, statusone row per card transaction
chargebackschargeback_id, txn_id, account_id, merchant_id, reason_code, opened_at, resolved_at, status, related_chargeback_idone row per dispute

The last column is the one to internalise. Before any query, say the grain out loud: one row per what? Accounts have many transactions; transactions have zero or more chargebacks. Every mistake in the "two mistakes" section comes from forgetting one of those arrows.

INNER vs LEFT JOIN — and when a join duplicates rows

Both guides open with joins, and the February 2026 one adds the phrase that matters: "understanding when joins create duplicate rows". The classic dataset question is accounts that have never transacted. INNER JOIN cannot answer it — it drops exactly the rows you want.

— LEFT JOIN + IS NULL: accounts with no transactions at all
SELECT a.account_id, a.country, a.signup_at
FROM accounts a
LEFT JOIN transactions t ON t.account_id = a.account_id
WHERE t.txn_id IS NULL
ORDER BY a.signup_at;

The mirror question — how many transactions per account, including zero — is where the duplicate-row trap lives. LEFT JOIN keeps every account, but each account now appears once per transaction. That is correct here, because you are about to count them; it is wrong the moment you SUM something from the accounts side.

— Transactions per account, keeping the zeros (COUNT(t.txn_id), not COUNT(*))
SELECT a.account_id,
       COUNT(t.txn_id) AS txn_count      -- COUNT(*) would return 1 for a no-transaction account
FROM accounts a
LEFT JOIN transactions t ON t.account_id = a.account_id
GROUP BY a.account_id
ORDER BY txn_count DESC;
Practice joins on real data →

GROUP BY with COUNT, SUM, AVG, MIN, MAX

The guides list all five aggregates by name. A dataset question will ask for one number per category — which merchant category has the highest average transaction? — and the multiple-choice options will include the answers you get from the common mistakes (forgetting to filter refunds, averaging the wrong thing).

— One row per merchant category: the five aggregates on completed transactions
SELECT m.category,
       COUNT(*)        AS txn_count,
       SUM(t.amount)   AS total_spend,
       ROUND(AVG(t.amount), 2) AS avg_amount,
       MIN(t.amount)   AS smallest,
       MAX(t.amount)   AS largest
FROM transactions t
JOIN merchants m ON m.merchant_id = t.merchant_id
WHERE t.status = 'completed'          -- filter rows BEFORE grouping
GROUP BY m.category
HAVING COUNT(*) >= 20                 -- filter GROUPS after
ORDER BY avg_amount DESC;

Two things to have cold: the WHERE / HAVING split (row filter before grouping, group filter after), and that this join is safe — merchants is one row per merchant, so joining it onto transactions cannot multiply anything. The unsafe direction is the next section but one.

▶ Write this one yourself: Transaction Share by Merchant Category

CASE WHEN — conditional metrics and buckets

The February 2026 guide names "CASE WHEN for conditional metrics and buckets". Both shapes are one query each. Buckets:

— Bucket transactions by size, then count each bucket
SELECT
  CASE
    WHEN amount < 25   THEN 'small'
    WHEN amount < 250  THEN 'medium'
    ELSE 'large'
  END AS size_bucket,
  COUNT(*) AS txn_count
FROM transactions
GROUP BY size_bucket
ORDER BY MIN(amount);

Conditional metrics — a rate computed inside one aggregate — is the pattern that turns up as "what share of high-risk merchants' transactions were declined?":

— Decline rate per merchant risk tier: CASE inside AVG
SELECT m.risk_tier,
       COUNT(*) AS txn_count,
       ROUND(100.0 * AVG(CASE WHEN t.status = 'declined' THEN 1 ELSE 0 END), 2) AS decline_rate_pct
FROM transactions t
JOIN merchants m ON m.merchant_id = t.merchant_id
GROUP BY m.risk_tier
ORDER BY decline_rate_pct DESC;
Practice CASE WHEN patterns →

CTEs and subqueries

Both guides name CTEs; the February 2026 one adds "for breaking a problem into readable steps", which is the point. A typical two-step question: which accounts spend more than the average account? The average account's spend is itself an aggregate of an aggregate, so it needs two levels.

— Accounts whose total spend beats the average account (two CTEs, each a sentence)
WITH per_account AS (
  SELECT account_id, SUM(amount) AS total_spend
  FROM transactions
  WHERE status = 'completed'
  GROUP BY account_id
),
benchmark AS (
  SELECT AVG(total_spend) AS avg_spend FROM per_account
)
SELECT p.account_id, p.total_spend
FROM per_account p
CROSS JOIN benchmark b
WHERE p.total_spend > b.avg_spend
ORDER BY p.total_spend DESC;

The same query as a subquery is shorter and harder to read under a clock. When a question has two steps, write two CTEs and name them after the step. If the written SQL question is graded by a person, the names are free marks; if it is graded by output, they are free debugging.

Window functions — ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM OVER

The February 2026 guide lists exactly six: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD and SUM OVER. They cover three question shapes.

Top-N per group (ROW_NUMBER / RANK / DENSE_RANK)

— Top 3 merchants by spend in each country
WITH merchant_spend AS (
  SELECT m.country, m.name, SUM(t.amount) AS total_spend
  FROM transactions t
  JOIN merchants m ON m.merchant_id = t.merchant_id
  WHERE t.status = 'completed'
  GROUP BY m.country, m.name
),
ranked AS (
  SELECT *,
         RANK() OVER (PARTITION BY country ORDER BY total_spend DESC) AS rank_in_country
  FROM merchant_spend
)
SELECT country, name, total_spend, rank_in_country
FROM ranked
WHERE rank_in_country <= 3
ORDER BY country, rank_in_country;

The three ranking functions differ only on ties: ROW_NUMBER breaks them arbitrarily (1, 2, 3), RANK leaves a gap (1, 1, 3), DENSE_RANK does not (1, 1, 2). A multiple-choice question that asks "how many merchants are in the top 3" is really asking which one you used.

Previous / next row (LAG / LEAD)

On a card ledger the LAG question is velocity: how many minutes since this account's previous transaction? Two charges seconds apart is what a fraud rule looks for, and it is the same shape as month-over-month change on any other table.

— Minutes since the same account's previous transaction (SQLite date math)
WITH ordered AS (
  SELECT account_id, txn_id, amount, txn_at,
         LAG(txn_at) OVER (PARTITION BY account_id ORDER BY txn_at) AS prev_txn_at
  FROM transactions
)
SELECT account_id, txn_id, amount, txn_at,
       ROUND((julianday(txn_at) - julianday(prev_txn_at)) * 1440, 1) AS minutes_since_prev
FROM ordered
WHERE prev_txn_at IS NOT NULL
ORDER BY minutes_since_prev
LIMIT 20;

Running totals (SUM OVER)

— Running spend per account, in transaction order
SELECT account_id, txn_at, amount,
       SUM(amount) OVER (
         PARTITION BY account_id
         ORDER BY txn_at
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS running_spend
FROM transactions
WHERE status = 'completed'
ORDER BY account_id, txn_at;
Practice window functions →

Date filtering — day, week, month, quarter, rolling window

The February 2026 guide's last item is "date filtering by day, week, month, quarter, and rolling window". The syntax is dialect-specific — the examples below are SQLite, which is what runs in the browser on SQL Quest; the ideas move unchanged to PostgreSQL's date_trunc and INTERVAL.

— Monthly spend, then the last-7-day and quarter filters
-- by month
SELECT strftime('%Y-%m', txn_at) AS month,
       COUNT(*) AS txn_count, SUM(amount) AS spend
FROM transactions
WHERE status = 'completed'
GROUP BY month
ORDER BY month;

-- rolling window: the 7 days ending on the ledger's last day
SELECT COUNT(*) AS txns_last_7_days
FROM transactions
WHERE txn_at >= datetime((SELECT MAX(txn_at) FROM transactions), '-7 days');

-- quarter: derive it from the month number
SELECT strftime('%Y', txn_at) || '-Q' || ((CAST(strftime('%m', txn_at) AS INTEGER) + 2) / 3) AS quarter,
       SUM(amount) AS spend
FROM transactions
GROUP BY quarter;

The date trap on a dataset question: a filter like txn_at BETWEEN '2026-03-01' AND '2026-03-31' silently drops all of March 31 when txn_at carries a time — '2026-03-31 14:00' is greater than '2026-03-31'. Use >= '2026-03-01' AND < '2026-04-01'. On a multiple-choice question, the off-by-one-day answer is usually one of the options.

The two mistakes that cost points on dataset questions

On a written SQL question a reviewer may give partial credit for a query that is nearly right. On a multiple-choice dataset question there is no such thing: your number either matches an option or it does not, and the wrong options are the numbers you get from these two mistakes.

Mistake 1 — join fan-out

Joining a one-to-many table and then summing from the "one" side. Every transaction that has two chargebacks appears twice, and SUM(amount) is inflated by exactly the rows you duplicated.

— WRONG: spend per account is inflated for any account with multiple disputes on a transaction
SELECT t.account_id, SUM(t.amount) AS total_spend, COUNT(c.chargeback_id) AS disputes
FROM transactions t
LEFT JOIN chargebacks c ON c.txn_id = t.txn_id
GROUP BY t.account_id;
— RIGHT: bring chargebacks to one row per transaction first, then join
WITH disputes_per_txn AS (
  SELECT txn_id, COUNT(*) AS disputes
  FROM chargebacks
  GROUP BY txn_id
)
SELECT t.account_id,
       SUM(t.amount) AS total_spend,
       SUM(COALESCE(d.disputes, 0)) AS disputes
FROM transactions t
LEFT JOIN disputes_per_txn d ON d.txn_id = t.txn_id
GROUP BY t.account_id;

The test for fan-out is one sentence: does this join multiply the thing I am summing? If the joined table can have more than one row per key on my side, yes. Pre-aggregate it, or count with COUNT(DISTINCT t.txn_id) and sum from a CTE that never saw the join.

Mistake 2 — the wrong grain

Answering "average transaction amount per country" by averaging each account's average. The numbers differ because an account with one transaction and an account with a thousand get equal weight. Neither query is "wrong SQL"; one of them answers a different question.

— Same words, two grains, two numbers
-- grain: transaction (what "average transaction amount" means)
SELECT a.country, ROUND(AVG(t.amount), 2) AS avg_txn_amount
FROM transactions t
JOIN accounts a ON a.account_id = t.account_id
GROUP BY a.country;

-- grain: account (what "average account's average transaction" means)
WITH per_account AS (
  SELECT account_id, AVG(amount) AS avg_amount
  FROM transactions
  GROUP BY account_id
)
SELECT a.country, ROUND(AVG(p.avg_amount), 2) AS avg_of_account_avgs
FROM per_account p
JOIN accounts a ON a.account_id = p.account_id
GROUP BY a.country;

Read the question for its noun. "Per transaction", "per account", "per merchant" is the grain; the GROUP BY must produce one row per that noun and nothing else. If a question is ambiguous, the option list usually contains both numbers — which is itself the hint that grain is what is being tested.

A 2-week prep plan

Built for someone who already writes SELECT-FROM-WHERE and has an evening a day. Every item maps to a section above; the timed runs at the end are there because every candidate report mentions the clock.

Week 1 — the patterns, one per evening

  1. Day 1 — grain. Take the four-table schema above and, for ten plausible questions, write only the GROUP BY line and the expected row count. No SELECT list. This is the skill the dataset questions test.
  2. Day 2 — joins. INNER vs LEFT, the IS NULL anti-join, and COUNT(col) vs COUNT(*) after a LEFT JOIN. Then deliberately fan out a join and watch the SUM change. Joins practice set.
  3. Day 3 — aggregates. All five aggregates, WHERE before, HAVING after. Write each query twice: once with the filter in the right place, once wrong, and see which answer a multiple-choice question would list.
  4. Day 4 — CASE WHEN. Buckets and conditional rates. CASE WHEN practice set.
  5. Day 5 — CTEs. Rewrite every day-3 query that needed a subquery as named CTEs. Say each CTE's grain out loud.
  6. Day 6 — windows. Top-N per group with all three ranking functions on the same data; LAG for gaps; SUM OVER for running totals. Window functions practice set.
  7. Day 7 — dates. Month, quarter, rolling 7-day, and the end-of-range trap. In the tool you plan to use on the day, not just SQL.

Week 2 — speed, tools, and the clock

  1. Day 8 — pick your dataset tool. Candidates report a free choice of Excel, Python, R or SQL for the multiple-choice items. Load a CSV into each and time a GROUP BY and a two-table join. Keep the fastest one; it will not be the one you like most.
  2. Day 9 — fan-out drills. Ten questions on the card ledger where the wrong answer comes from a duplicated join. The Capital One practice cut is built around exactly this.
  3. Day 10 — grain drills. Ten questions where two grains give two numbers; write both, pick the one the wording asks for.
  4. Day 11 — window and date mix. Period-over-period change with LAG, then top-N within a date range.
  5. Day 12 — timed run 1. 70 minutes, 14 questions of your own, CSV in your chosen tool, one written SQL question at the end. Note where the time went.
  6. Day 13 — fix the slow spots. Whatever ate the time on day 12 — usually loading and cleaning the CSV, not the SQL.
  7. Day 14 — timed run 2, then stop. A second 70-minute run. The January 2023 Blind advice, from one candidate and worth exactly that, was to leave the written SQL for the end and move quickly through the dataset items; decide in this run whether that order suits you.

Practice the Capital One cut on real bank data

Joins at the right grain, GROUP BY, CTEs and window functions on FDIC bank data, plus the card-transactions ledger every example above ran on. Runs in the browser, no signup; the AI tutor explains why a wrong answer is wrong instead of just marking it.

Start with one question: Transaction Share by Merchant Category →

or open the full Capital One practice page →

FAQ

How long is the Capital One CodeSignal data analyst assessment and how many questions does it have?

Candidate reports on Blind describe a 70-minute assessment (a December 2021 report for a Senior Data Analyst role, and a June 2025 report of "around 14–15 questions in 70 minutes"). Two earlier reports (February and November 2022) say 14 questions. Prep guides dated August 2025 and February 2026 say roughly 70 to 80 minutes; those guides count "four to six" questions, apparently counting the dataset sections rather than the individual multiple-choice items. Capital One does not publish the format, and a Capital One employee replied on the June 2025 thread that the format had changed over the previous four years — so treat every number as candidate-reported and dated.

Is the Capital One data analyst CodeSignal test multiple choice or written SQL?

Mostly multiple choice over provided datasets, plus written SQL. A November 2022 Blind report describes "data analysis questions where you had to use the csv data to answer multiple choice questions" and "few sql questions"; a February 2022 reply says "one question is a database question you have to use SQL for" and that the rest can be answered with "a scripting language of choice (Python, R) or Excel". The reports differ on whether the written SQL is one question or a few, so prepare for more than one.

What SQL topics does the Capital One data analyst assessment cover?

The prep guides name: INNER JOIN and LEFT JOIN, including when a join creates duplicate rows; GROUP BY with COUNT, SUM, AVG, MIN and MAX; CASE WHEN for conditional metrics and buckets; CTEs and subqueries; window functions ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD and SUM OVER; and date filtering by day, week, month, quarter and rolling window (extrabrain, February 2026; linkjob, August 2025). Candidate reports add that the multiple-choice questions are dataset questions — the answer is a number — so join fan-out and aggregating at the wrong grain are the mistakes that cost points.

Can I use Excel or Python instead of SQL on the Capital One CodeSignal assessment?

For the dataset multiple-choice questions, candidate reports say yes: a February 2022 Blind reply says you can use "a scripting language of choice (Python, R) or Excel", and only the database question requires SQL. Guides dated 2025 and 2026 describe the same choice. Pick the tool you are fastest in for the dataset questions — several reports say time is the constraint — and keep SQL for the written question.

Where can I practice SQL for the Capital One data analyst assessment?

SQL Quest's Capital One page is a practice cut built from the same public descriptions: joins at the right grain, GROUP BY, CTEs and window functions on real FDIC bank data, plus a synthetic card-transactions ledger for the fan-out and velocity patterns. It runs in the browser with no signup, and free challenges include the Easy and Medium ones; Hard challenges are on Pro ($29/mo, $99/yr or $199 lifetime). The topic pages for joins, window functions and CASE WHEN cover the individual patterns.

Sources

Read on 7 September 2026. Nothing on this page comes from Capital One; every format claim is one of these, dated.