SQL interviews for 10+ years of experience skip defining a join and go after what only experience teaches: why the optimizer guesses wrong, what really happens when you commit, how two correct transactions still break a business rule, how to get one dropped table back without losing everyone else's writes, and how you set standards, hire and defend decisions across teams. It is written for engineers with around eight to fifteen years of SQL behind them, interviewing for senior, staff or lead roles. Each question shows what the interviewer is really checking, a shape for your answer and a sample you can adapt. Say your own version out loud, with a story from your own work.
Search all questions by round, difficulty and level, or save the ones you want to practice.
Cause: the plan is compiled for the first value's estimated row count and reused for every later value.
Why it flips: a restart, a stats update or plan eviction recompiles it with whatever value arrives first.
Fixes: recompile that one query, split the code path for the big customers, or pin a plan that is safe for both.
“This is a cached plan meeting skewed data. When the query first compiles, the optimizer looks at the parameter value it was given and estimates rows from the statistics. If that first value was a small customer, it picks an index seek with lookups, which is perfect for fifty rows and terrible for five million. The plan then gets reused for everyone. After a restart or a stats update, if the big customer happens to arrive first, you get the reverse: a scan that's fine for them and wasteful for everyone else. To prove it, I compare estimated and actual rows in the plan and check the compiled parameter value. For the fix, I'd first try recompiling just that statement if it doesn't run thousands of times a second. Otherwise I split the path so the few huge customers get their own query, or force a plan that's acceptable for both shapes.”
Blaming the network or adding an index without comparing estimated rows against actual rows.
Independence: by default the optimizer roughly multiplies each filter's selectivity as if the columns were unrelated.
Correlation: every running shoe is already footwear, so the second filter removes almost nothing, and the multiplied estimate is far too low.
Knock-on: a tiny estimate picks nested loops and small memory budgets, both wrong for the real row count.
Fix: multi-column statistics, or a query and model that don't ask the optimizer to guess.
“By default most optimizers estimate each filter on its own and then combine them as if the columns were unrelated. Say footwear is a tenth of the table and running shoes a hundredth. Multiplied, that's a thousandth. But every running shoe is already footwear, so the real answer is a hundredth, ten times more rows than estimated, and each extra correlated filter makes the gap bigger. With a low estimate, the optimizer picks a nested loop and a small memory budget, which suits a few dozen rows and falls over at tens of thousands. I confirm it by comparing estimated and actual rows at each step of the plan. The fix is to tell the optimizer about the relationship. In PostgreSQL that's extended statistics on the column pair, and SQL Server can use statistics on a multi-column index or ones you create by hand. Sometimes the better fix is the query: if subcategory already implies category, filter on subcategory alone.”
-- PostgreSQL
CREATE STATISTICS products_cat_subcat (dependencies)
ON category, subcategory FROM products;
ANALYZE products;
Updating statistics again and again without noticing that the two columns are correlated.
Budget: each sort or hash gets working memory; when the rows do not fit, it writes batches to disk and reads them back.
Why too small: an underestimated row count, wider rows than needed, or a low per-operation setting.
Fixes: correct the estimate, let an index return rows already in order, or carry fewer columns through the sort.
Careful with settings: the limit applies per operation, so raising it for everyone can run the server out of memory at peak.
“Sorts and hash joins work in memory, and the engine decides up front how much each one may use. In SQL Server that's a memory grant sized from the estimated rows and row width. In PostgreSQL it's work_mem, which applies to each sort or hash step separately. When the real data doesn't fit, the step writes batches to disk and reads them back, and that's where the time goes. So I ask why it didn't fit. Usually it's a bad estimate, so the budget was sized for a fraction of the rows, and I fix that first. Sometimes the query drags wide text columns through the sort that it only needs at the end. Or an index could hand the rows over already sorted, so there's no sort at all. I'm wary of raising the setting for everyone, because one query can have several sorts and hashes, each taking that much, on every connection. Raising it for one session or one report is safer.”
Raising the server-wide memory setting for sorts without working out why this query didn't fit.
What the engine must check: that no child row still points at the parent, or cascade to the ones that do.
Missing index: many engines index the parent key but not the child's foreign key column.
Fix: index the child's foreign key columns, leading with them, and check the rest of the schema for the same gap.
“My first guess is an unindexed foreign key on the child. When you delete a parent row, or change its key, the engine has to confirm no child row still points at it, or find the rows to cascade to. The parent's key is indexed because it's a primary key, but many engines don't create an index on the child's referencing column for you. So a single-row delete turns into a scan of a huge child table, and it holds locks while it scans, which is why other work queues up behind it. In some engines it can even take a wider lock on the child than you'd expect. I'd confirm it from the plan of the delete, then add an index that leads with the foreign key columns. Then I'd run a query over the catalog to list every foreign key without a matching index, because if one was missed, others usually were too.”
Assuming a foreign key always comes with an index on both sides.
Visibility: an index entry does not say whether its row is visible to this transaction; that lives with the row.
Visibility map: pages flagged all-visible can skip the table; any other page means a heap fetch.
Cause: a big load or many updates, and vacuum hasn't been through since to set the flags.
Fix: vacuum the table, then tune autovacuum so it keeps up with that table's writes.
“In PostgreSQL an index entry doesn't say whether the row it points to is visible to my transaction. That information lives with the row in the table. So an index-only scan checks the visibility map first, which keeps a flag per table page saying every row on it is visible to everyone. If the page is flagged, it can trust the index. If not, it has to visit the table for that row, and that's a heap fetch. Millions of heap fetches mean most pages aren't flagged, usually because the table just had a big load or lots of updates and vacuum hasn't run since. Vacuum is what sets those flags. So I'd vacuum the table and check that heap fetches drop. Then I'd tune autovacuum for that table so it runs often enough for its write pattern. For a table that's rewritten constantly, I'd accept that index-only scans won't help much.”
Assuming an index-only scan never touches the table, and adding more columns to the index to fix it.
Why: each transaction reads two on call from its snapshot, then updates a different row, so no write conflict is seen.
Name it: write skew, allowed by snapshot isolation, which some engines label repeatable read or even serializable.
Fix: true serializable with retries, lock the rows you read, or make both writers update one shared row.
“Each transaction takes its own snapshot and sees two doctors on call, so each one decides it's safe to go off. They then update different rows, so there's no write-write conflict for snapshot isolation to catch, and both commit. Now nobody is on call. That's write skew. It's worth knowing that some engines call snapshot isolation repeatable read, and at least one calls it serializable, so the label alone doesn't protect you. I have three fixes. Use true serializable isolation where the engine supports it, which aborts one of them, and have the code retry. Or lock the rows the decision depends on, by selecting the shift's on-call rows for update before checking the count. Or turn it into a real conflict by having both transactions update a single shift row. For a small rule like this I'd lock the rows I read, because it's explicit and easy to review.”
-- PostgreSQL
BEGIN;
SELECT doctor_id FROM on_call
WHERE shift_id = 42 AND on_duty = true
FOR UPDATE;
-- the app continues only if two or more rows came back
UPDATE on_call SET on_duty = false
WHERE shift_id = 42 AND doctor_id = 7;
COMMIT;
Assuming any isolation level called serializable stops every anomaly, or blaming it on a missing unique constraint.
Dirty data: it reads changes that may still roll back, so a report can show things that never happened.
Wrong row counts: if pages split or rows move during the scan, it can miss rows or read the same row twice.
Instead: read committed snapshot, so readers see the last committed version without blocking writers.
Cost: a version store to size and watch, and code that relied on a read waiting for a writer needs checking.
“NOLOCK means read uncommitted, and it's worse than most people think. The obvious problem is dirty reads: the report can include an order that gets rolled back a second later. The less obvious one is that the scan doesn't hold its place safely. If a page splits or rows move while it reads, it can skip rows or read the same row twice, so even totals over long-committed data can be wrong. It can also fail outright with a data movement error. What I'd do instead is turn on read committed snapshot for the database. Readers then see the last committed version of each row, so readers don't block writers and writers don't block readers. The cost is a version store in tempdb that needs sizing and monitoring, some extra work on every update, and a review of any code that relied on a read waiting for another transaction, because now it won't wait. Then I'd remove the hints in batches, finance reports first.”
Calling NOLOCK a safe speed-up as long as you don't mind slightly stale data.
The counter: transaction IDs are 32-bit and compared in a circle, so old rows must be frozen by vacuum before the counter comes round.
Why urgent: if freezing falls too far behind, the database stops handing out new transaction IDs to protect the data, which means no writes.
Root cause: find what holds vacuum back: a long open transaction, an abandoned replication slot, a forgotten prepared transaction, or autovacuum that can't keep up.
Fix: clear the blocker, vacuum the oldest tables first, then alert on transaction ID age.
“PostgreSQL tags every row version with the ID of the transaction that wrote it, and those IDs are 32-bit numbers compared in a circle. A row that's old enough would suddenly look like it came from the future and vanish from queries. Vacuum prevents that by freezing old rows, marking them visible to everyone. The warning means freezing has fallen dangerously behind, and if it keeps going, PostgreSQL stops assigning new transaction IDs to protect the data, which in practice means no more writes. So I treat it as an outage in waiting. First I find out why vacuum couldn't do its job: usually a transaction open for days, a replication slot nobody reads from, or an old prepared transaction. Freezing can't get past any of those. I clear that, then vacuum the tables with the oldest transaction IDs first. Afterwards I add an alert on transaction ID age, well before the warnings start.”
-- PostgreSQL: how close each database is
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
-- slots that can hold vacuum back
SELECT slot_name, active, xmin, catalog_xmin
FROM pg_replication_slots;
Treating it as a log warning to look at next week, or restarting the server and hoping it clears.
Sorted by rules: a text index is ordered by the collation rules in force when each entry was written.
Rules changed: some engines take those rules from the operating system, and the upgrade changed how some strings sort.
Effect: searches take the wrong branch and miss rows, and uniqueness checks can look in the wrong place.
Fix: check the indexes, clean up duplicates, rebuild them, and treat sorting-library changes as database upgrades.
“A B-tree on a text column is sorted by the collation rules, and some engines take those rules from the operating system's library. If an OS upgrade changes how certain strings sort, say ones with punctuation or accented letters, the index on disk is still in the old order while every new search compares with the new rules. The search walks down the tree, turns the wrong way, and misses a row that's there. A uniqueness check can do the same, look in the wrong spot and let a duplicate in. Nothing raises an error, which is what makes it nasty. I'd confirm it with an index consistency check, list every index on text columns using that collation, clean up duplicates, and rebuild them. To prevent it, I'd pin the collation library version, plan index rebuilds into any OS upgrade, and keep primaries and replicas on the same version, because a physical replica reads the same index files with its own rules.”
Blaming caching in the application, or restoring from backup, without asking what changed on the servers.
Log first: every change is written to the write-ahead log, and COMMIT returns once the log up to that commit is flushed to durable storage.
Pages later: changed data pages stay in memory and are written out later, in the background and at checkpoints.
Recovery: on restart the engine replays the log from the last checkpoint, so committed work comes back and uncommitted work does not count.
Trade-offs: relaxed commit settings trade the last few transactions for speed.
“When I commit, the database doesn't write my changed rows into the data files straight away. Every change has already been described in the write-ahead log, and COMMIT only returns once the log up to my commit has been flushed to durable storage. The changed pages stay in memory and are written later, by background writers and at checkpoints. If the power goes a moment after my commit, the data file might not have my change, but the log does. On restart, recovery starts from the last checkpoint and replays the log, so committed work comes back and anything uncommitted is rolled back or simply never becomes visible. That's why log write speed drives commit latency, and why checkpoint spacing affects how long recovery takes. Some engines let you relax this, with asynchronous commit or delayed durability. Commits get faster, but a crash can lose the last few transactions, though nothing is corrupted. I'd only allow that for data we can afford to lose.”
Saying COMMIT writes every changed row into the data files before it returns.
Contain: stop the script or job, check nothing else ran with it, and tell the affected teams.
Restore to the side: restore the last full backup to a separate server and replay logs to just before the drop.
Bring it back: copy the table, with its indexes, constraints, triggers and permissions, into production.
Afterwards: take drop rights away from people and jobs that do not need them, and run restore drills.
“First I'd make sure nothing else is about to run, like a script with more drops in it, and tell the teams whose features are down. I would not restore the whole production database to before 14:05, because that throws away every write since then across all the other tables. Instead I restore the latest full backup onto a separate server and replay the logs up to just before the drop. I find the exact point from the log or the audit trail rather than guessing. Then I copy the table back into production along with its indexes, constraints, triggers and grants, which people often forget. If it was dropped with cascade, I also check for views and foreign keys that went with it. The whole thing only works if we've practiced it, so the lasting fix is regular restore drills with timings, plus removing drop rights from people and application accounts that don't need them.”
Restoring the whole database over production and wiping out every other write since the backup.
Key: tenant id, so a tenant's data and joins stay on one shard and most requests touch only one.
Routing: a lookup from tenant to shard, not a formula baked into code, so tenants can move.
Uneven tenants: the biggest tenants may need a shard of their own, so build the tool to move one early.
What gets harder: cross-tenant reports, unique ids, migrations on every shard, and transactions across shards.
“First I'd check we've really used up the simpler options, because sharding is very hard to undo. If we go ahead, for a multi-tenant product the natural key is tenant id. Almost every request is about one tenant, so it hits one shard, and joins and transactions stay local. I'd route through a small directory that maps each tenant to a shard, rather than a formula in the application, because that lets me move a tenant when a shard gets hot. Tenants are never equal in size, so the largest few may need a shard to themselves, and I'd build the tool to move a tenant early: copy, catch up, then a short cutover. Then I'd be clear with the team about what gets harder. Reports across all tenants move to a separate analytics store, ids must be unique without one shared sequence, every migration runs on every shard, and anything spanning tenants loses a simple transaction.”
Choosing a shard key without checking which queries must stay on one shard, or leaving no way to move data between shards.
The problem: the database commit and the message publish are two systems, so one can succeed without the other.
Outbox: write the event into an outbox table in the same transaction as the order.
Relay: a separate process, or change data capture reading the log, publishes outbox rows and marks them sent.
Delivery: at least once, so consumers must ignore duplicates by event id.
“That's a dual write. Saving the order and publishing the message are two separate systems with no shared transaction. If the service publishes before commit and then rolls back, you get an event for an order that doesn't exist. If it commits and then crashes before publishing, the event is lost. The fix I use is an outbox. In the same transaction that inserts the order, I insert a row into an outbox table with the event payload, so either both commit or neither does. A relay then reads unsent outbox rows in order, publishes them and marks them sent, or a change data capture tool picks up the outbox inserts from the database log. The relay can crash after publishing but before marking a row sent, so sometimes it publishes twice. That means consumers must be idempotent, keyed on the event id. I also clear old outbox rows regularly so the table stays small.”
BEGIN;
INSERT INTO orders (id, customer_id, total_cents)
VALUES (:order_id, :customer_id, :total_cents);
INSERT INTO outbox (event_id, topic, payload)
VALUES (:event_id, 'order_placed', :payload);
COMMIT;
Adding a retry loop around the publish, as if that closes the gap between the commit and the message.
Coupling: your table layout becomes their dependency, so a rename or a split can break a team you never hear from.
Load and privacy: their queries share your resources, and they see columns they have no reason to see.
Offer: an API, published events, or a reporting copy with views you keep stable and versioned.
Match the need: choose by how fresh the data must be and how they will query it.
“I'd start by asking what they need, because the answer changes the offer. My worry with raw table access is that my schema quietly becomes their contract. The next time I split a table or rename a column, their feature breaks, and I may not even know they depend on it. Their queries also compete with my service for the same resources, and they'd see columns like personal data they have no reason to see. So I'd offer something built to be depended on. If they need a few records in real time, an API. If they need to react to changes, events we publish. If they need reports, a replica or a warehouse feed with a set of views I promise to keep stable and version, with read-only rights on just those views. If they're under deadline pressure, views on a replica can be ready in days, so they're not blocked, and it's still a contract I can keep.”
Granting read access to everything because it's quicker, or saying no without offering anything.
Events: each call becomes plus one at its start and minus one at its end.
Running total: add the events up in time order; the running value is how many calls are live.
Ties: at the same instant, count ends before starts so back-to-back calls don't overlap.
Peak: take the highest running value per day.
“I'd turn each call into two events: plus one when it starts and minus one when it ends. Put all the events in time order and keep a running total, and at any point that total is the number of calls live right then. The detail people miss is ties. If one call ends at exactly the moment another starts, they didn't overlap, so at the same timestamp I sort the minus ones before the plus ones. I use a ROWS frame so events are added one at a time, instead of the default range frame, which lumps together every event with the same timestamp. Then I take the highest running value per day. This avoids joining every call to every other call, which gets slow fast. One edge I'd mention: if a day's peak is right at midnight from calls carried over, I'd add a zero event at each midnight so that moment is measured too.”
WITH events AS (
SELECT started_at AS at_time, 1 AS delta FROM calls
UNION ALL
SELECT ended_at, -1 FROM calls
),
running AS (
SELECT at_time,
SUM(delta) OVER (ORDER BY at_time, delta
ROWS UNBOUNDED PRECEDING) AS live_calls
FROM events
)
SELECT CAST(at_time AS date) AS call_day,
MAX(live_calls) AS peak_calls
FROM running
GROUP BY CAST(at_time AS date)
ORDER BY call_day;
Joining every call to every other call, or forgetting the tie between one call ending and another starting.
Join: a full outer join on the key keeps rows that exist on only one side.
Classify: missing on the old side means added, missing on the new side means removed.
Compare safely: IS DISTINCT FROM counts NULL to a value as a change, which <> does not.
“I'd full outer join yesterday to today on the customer id. If the old side is missing, the row was added. If the new side is missing, it was removed. If both exist, I compare the columns. The trap is NULL. If the phone number went from NULL to a real number, old phone not-equal new phone is unknown, not true, so a plain not-equals silently skips that change. IS DISTINCT FROM treats NULL as a comparable value, so NULL to a number counts as a change and NULL to NULL doesn't. Where an engine doesn't support it, one trick is NOT EXISTS over the old values INTERSECT the new values, because set operators treat NULLs as equal. On very large tables I'd compare a hash of each row first to narrow things down, then check the real columns for the rows that differ.”
SELECT COALESCE(n.customer_id, o.customer_id) AS customer_id,
CASE
WHEN o.customer_id IS NULL THEN 'added'
WHEN n.customer_id IS NULL THEN 'removed'
ELSE 'changed'
END AS change_type
FROM customers_yesterday o
FULL OUTER JOIN customers_today n
ON n.customer_id = o.customer_id
WHERE o.customer_id IS NULL
OR n.customer_id IS NULL
OR n.email IS DISTINCT FROM o.email
OR n.phone IS DISTINCT FROM o.phone
OR n.city IS DISTINCT FROM o.city;
Comparing columns with a plain not-equals and never noticing that changes to or from NULL disappear.
Why gaps: sequence values aren't returned on rollback, and engines cache blocks of values that a restart can skip.
Separate concerns: keep the identity as the technical key and add a business number.
Gap-free counter: one counter row updated in the same transaction as the insert, which serialises invoice creation.
“Identity columns and sequences are built for speed under concurrency, not for gap-free numbers. A value handed out to a transaction that rolls back is never returned, and engines keep a block of values in memory, so a restart or failover can skip the rest of that block. That's almost certainly the overnight jump. So I'd leave the identity as the technical key and add a separate invoice number. To make it gap-free, I keep a counter table with one row per series, and in the same transaction that creates the invoice, I increment that row and use the new value. If the transaction rolls back, the increment rolls back too. The cost is that the row is locked until commit, so invoices are created one at a time per series. That's fine for invoices, but I'd say it out loud, and keep that transaction very short.”
-- PostgreSQL
BEGIN;
UPDATE invoice_counter
SET last_number = last_number + 1
WHERE series = 'INV'
RETURNING last_number;
-- insert the invoice using that number, then commit
COMMIT;
Using MAX(invoice_no) + 1 without locking, which hands the same number to two concurrent invoices.
Rounding: floating-point addition isn't associative, so the order of adding changes the last digits.
Why it varies: parallel or hash plans add rows in a different order on each run.
Fix: exact DECIMAL or NUMERIC, or whole units, for anything that must add up exactly.
“FLOAT stores binary approximations, so a value like 0.1 isn't held exactly, and every addition rounds a little. Floating-point addition also isn't associative: adding A then B then C can give a slightly different result from C then B then A. On a big table the engine may run the SUM in parallel, splitting rows across threads and combining partial sums, and the split changes from run to run. Same data, different order, different last digits. For anything that has to reconcile, like amounts or quantities, the column should be DECIMAL or NUMERIC with a fixed scale, or stored as whole units. If I can't change the column soon, casting each value to DECIMAL inside the SUM makes the total stable, because decimal addition is exact. I'd also stop anyone comparing float totals for equality in checks and use a tolerance instead.”
Blaming a bug in the database or rounding the total for display and calling it fixed.
Then: the decision, and why it was reasonable with what you knew.
What went wrong: the warning signs, and what it cost the team and the product.
The way out: how you moved off it step by step while the system kept running.
Lesson: what you now check before making a similar call.
“Early on at my last company, I pushed for putting business rules in database triggers. When an order line changed, triggers recalculated totals, stock and loyalty points. It kept the data consistent with very little app code, and at our size it worked. Four years later it was our biggest problem. Nobody could tell from the code what an update would really do, one small change set off a chain of triggers across five tables, and those chains were behind most of our deadlocks. Testing needed a full database. I owned that, and wrote a plan to move the logic into the order service one rule at a time. For each rule we added the service version, ran both and compared results for a couple of weeks, then dropped the trigger. It took two quarters. We kept constraints and simple audit triggers, because they protect data without hiding business logic. Now I ask one question before any trigger: will the next engineer know it exists?”
Picking a decision someone else made, or telling a story with no cost and no lesson.
Write it down: a short checklist for safe changes, with the reason behind each rule.
Automate: checks in the pipeline catch the common mistakes before a human looks.
Spread the skill: train a reviewer in each team, and keep your own review for the risky changes.
“When I took that on at my last company, every migration came to me, and I was the queue. So I wrote a one-page checklist: changes must be backward compatible so old and new code can run together, new foreign keys get an index, big backfills run in batches, and every migration sets a lock timeout so it fails instead of freezing a table. Each rule had a line on why, with the incident that taught us. Then we put checks in the pipeline that flagged the common problems, like adding a column with a slow default on a big table. I trained one person in each team to review against the checklist, and I only reviewed changes that touched the largest tables or broke a rule on purpose. I held a short weekly slot for questions. Within a couple of months my reviews dropped to a handful a week, and migration incidents went down too.”
Insisting on personally approving every change, or having no way for teams to learn the reasons behind the rules.
Decide first: write down what the person will actually do in their first months, and what good looks like.
Realistic exercise: a real slow query with its plan, or a risky migration to review, talked through out loud.
Past work: dig into one incident or design they owned until you know which decisions were theirs.
Fair and consistent: the same exercises and scoring for everyone, with notes written before the panel talks.
“First I write down what this person will really do: review migrations, tune the worst queries, get paged when the database misbehaves. Then I build the loop around that. I skip trivia like reciting isolation levels. Instead I hand them an anonymised slow query with its plan and ask them to think out loud: what they'd check first, what they'd change, what else they'd want to know. Then I give them a migration that would lock a big table and see whether they spot it and how they'd make it safe. The other session goes on one incident or design they owned, and I keep asking until I know which parts were their decisions. Everyone gets the same exercises and scoring guide, and each interviewer writes notes before we discuss, so the loudest voice doesn't decide. At my last company that loop found two strong hires who'd been turned away elsewhere for not knowing trivia.”
Relying on trivia or gut feel, with no clear picture of what the job actually needs.
Find the goal: ask what problem they want solved, like cost, speed or growth, before debating technology.
Bring evidence: current load, headroom and where the time really goes, plus what the orders data needs.
Offer options: cheaper fixes for today, a fair test for the new store, and a written trade-off with a decision date.
“I wouldn't argue about technology in the first meeting. I'd ask what's driving it: slow pages, fear of growth, cost, or something a customer said. Then I'd come back with numbers: how close we are to our limits, which queries take the time, and what we've tried. Orders need transactions across several tables and flexible reporting, which is what relational databases are good at, and a rewrite would freeze features for months. So I'd lay out options. Replicas, caching, archiving old orders and partitioning might give years of headroom for weeks of work. If there are parts that really fit a document or key-value store, like session data or event logs, I'd suggest starting there. And I'd offer a small, time-boxed test against the real bottleneck, with success measures agreed up front. Then leadership decides with the trade-offs in writing, and I back the decision fully once it's made.”
Dismissing the idea as uninformed, or agreeing to the rewrite without checking what problem it solves.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.