In a Union-Find (Disjoint-Set) implementation, using only union by rank (attaching the shorter tree under the taller tree's root) without path compression gives each find() operation a worst-case cost of O(log n). If path compression (making every visited node point directly to the root during find()) is added on top of union by rank, why does the amortized cost per operation drop all the way to O(alpha(n)), where alpha is the inverse Ackermann function, instead of staying at O(log n)?
Path compression flattens every tree touched during a find() call, and this flattening compounds across the whole sequence of operations; combined with the balanced trees union by rank already guarantees, the amortized cost per operation is bounded by inverse Ackermann alpha(n), a function that stays at 4 or less for any n smaller than a number vastly larger than the atoms in the observable universe
Union by rank alone already produces the alpha(n) bound, so adding path compression only shaves a constant factor off the O(log n) worst case that union by rank guarantees on its own
A single find() call becomes strictly O(1) once path compression is applied, so every individual operation runs in constant time rather than just the amortized average across a sequence of operations
Adding path compression removes the need for union by rank altogether, since flattened trees no longer require rank tracking to stay balanced during future union() calls
Answer: A. Path compression flattens every tree touched during a find() call, and this flattening compounds across the whole sequence of operations; combined with the balanced trees union by rank already guarantees, the amortized cost per operation is bounded by inverse Ackermann alpha(n), a function that stays at 4 or less for any n smaller than a number vastly larger than the atoms in the observable universe
ExplanationTarjan's classic result on Union-Find shows the two optimizations must work together: union by rank keeps tree height at O(log n) so no single union or find degrades badly, while path compression rewires every node touched during a find() so it points directly at the root, which pays off on every subsequent find() involving those nodes. The combined effect, analyzed across a whole sequence of m operations on n elements, is an amortized bound of O(alpha(n)) per operation, where alpha is the inverse Ackermann function. Because alpha(n) grows so slowly, it stays at 4 or below for every n up to a number far larger than the number of atoms in the observable universe (roughly 10^80), so in every practical setting the amortized cost per operation is effectively constant. This is an amortized guarantee over the full operation sequence, not a per-call worst case: an individual find() call early in the sequence, before any compression has happened, can still walk a chain of length up to O(log n); it is only the average cost over many calls that is bounded by alpha(n). The two techniques are also not interchangeable substitutes — union by rank on its own bounds tree height at O(log n) but does not independently deliver the alpha(n) bound, and dropping rank tracking in favor of compression alone loses the guarantee that trees stay balanced between compressions.
Question 22 · Express.js · hard
Infer this code and predict what happens when a client sends GET /users/profile:
'''javascript
const express = require('express');
const app = express();
app.get('/users/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
app.get('/users/profile', (req, res) => {
res.send('User profile page');
});
app.listen(3000);
'''
What response does the client receive, and why?
'User ID: profile' — Express matches routes in registration order, so the earlier '/users/:id' route captures the request with req.params.id set to 'profile', and the second route never runs
'User profile page' — Express automatically checks for an exact literal match like '/users/profile' before falling back to a parameterized route like '/users/:id', regardless of which was registered first
The server crashes at startup with a routing conflict error, because '/users/:id' and '/users/profile' both match the same URL pattern and Express refuses to register two overlapping routes
Both handlers run for the same request, so the client receives 'User ID: profile' followed by 'User profile page' as two separate parts of one response
Answer: A. 'User ID: profile' — Express matches routes in registration order, so the earlier '/users/:id' route captures the request with req.params.id set to 'profile', and the second route never runs
ExplanationExpress matches routes strictly in the order they are registered, not by specificity. Since app.get('/users/:id', ...) is defined first, a request to /users/profile matches it immediately — ':id' is a wildcard for any single path segment, so it captures 'profile' as req.params.id. The handler calls res.send('User ID: profile'), which sends the response and ends the request-response cycle right there. The second route, app.get('/users/profile', ...), is never reached — it becomes dead code unless the more specific literal route is moved above the parameterized one. Option B describes 'longest/most-specific match wins' routing, which some frameworks use but Express does not — Express is purely order-based. Option C is wrong because Express performs no route-conflict validation at startup; overlapping routes are legal (and a common source of real bugs). Option D is wrong because res.send() finalizes the response — a second handler cannot also write to the same response without an explicit next() call, and attempting to would throw 'Cannot set headers after they are sent'.
Question 23 · DOM Manipulation & MutationObserver Batching · hard
Consider the following JavaScript code:
```javascript
const observer = new MutationObserver((mutationsList) => {
console.log("Callback ran, batch size:", mutationsList.length);
});
observer.observe(document.body, { childList: true });
for (let i = 0; i < 5; i++) {
const li = document.createElement("li");
document.body.appendChild(li);
}
```
How many times does the callback run, and what value does mutationsList.length have when it runs?
The callback fires once, after the loop finishes, with mutationsList.length equal to 5, because MutationObserver batches every mutation from a single synchronous block of code into one callback call.
The callback fires five separate times, once right after each appendChild() call, because DOM observers respond the instant a node is inserted into the page.
The callback never fires at all, because the childList option only reports nodes being removed from an element, not nodes being added to it.
Calling observer.observe() itself triggers the callback right away, so mutationsList.length equals 0 since no mutations have actually happened yet at that point.
Answer: A. The callback fires once, after the loop finishes, with mutationsList.length equal to 5, because MutationObserver batches every mutation from a single synchronous block of code into one callback call.
ExplanationMutationObserver does not call its callback the instant each DOM change happens. Instead, every change is recorded as a separate MutationRecord, and the browser waits until the current synchronous block of JavaScript finishes running before delivering all the records collected so far as one array in a single callback call. In this code, the for loop runs from start to finish without ever handing control back to the browser, so all five appendChild() calls happen back to back, each producing its own MutationRecord for the added node. Because the loop never pauses, those five records are grouped together and handed to the callback in one delivery once the loop completes. So the callback runs exactly once, and mutationsList.length is 5. It would only fire five separate times if each appendChild() were somehow separated by a point where the browser could hand control back to the event loop (for example, if each call were wrapped in its own setTimeout). The childList option reports both additions and removals of child nodes, not removals only, so the observer does fire here. And observe() itself never triggers the callback — the callback only runs in response to actual mutations, so a length of 0 immediately after calling observe() would mean nothing had happened yet, which is not the case once the loop runs.
Question 24 · HTML5 semantic elements & DOM manipulation · hard
Five `<li class="item">` elements sit inside a `<ul id="list">` with no other classes anywhere on the page. This script runs after the page loads:
```js
const items = document.getElementsByClassName("item");
for (let i = 0; i < items.length; i++) {
items[i].classList.remove("item");
}
```
After this loop finishes, how many of the five `<li>` elements still have the class "item"?
None of the five elements still have the class "item", because getElementsByClassName returns a static NodeList that is fully evaluated before the loop starts, so all five original elements are visited and each has its class removed exactly once
Two elements still have the class "item", because getElementsByClassName returns a live HTMLCollection that shrinks immediately each time a matching class is removed, so the loop's index skips over elements that get shifted into already-visited positions
All five elements still have the class "item", because classList.remove() only schedules the change for the next repaint, so the collection's length and contents remain unchanged for the entire duration of the loop
Three elements still have the class "item", because the loop condition i < items.length is evaluated once when the loop begins and then cached, so the loop always runs for exactly the original five iterations regardless of what happens inside the body
Answer: B. Two elements still have the class "item", because getElementsByClassName returns a live HTMLCollection that shrinks immediately each time a matching class is removed, so the loop's index skips over elements that get shifted into already-visited positions
ExplanationgetElementsByClassName() returns a live HTMLCollection — a view that automatically re-evaluates which elements currently match "item," not a frozen snapshot. Label the five items by their original order: e0, e1, e2, e3, e4; all start in the collection, so items.length is 5.
i=0: items[0] is e0. Removing its class means e0 no longer matches "item," so the live collection instantly re-computes to [e1, e2, e3, e4] and length drops to 4.
i=1: items[1] is now e2 (the collection shifted left after e0 left). Removing its class shrinks the collection to [e1, e3, e4], length 3.
i=2: items[2] is now e4. Removing its class shrinks the collection to [e1, e3], length 2.
i=3: the loop checks 3 < items.length, i.e. 3 < 2, which is false, so the loop stops.
Three elements (e0, e2, e4) had their class stripped, while e1 and e3 were never visited because the shrinking, re-indexing collection kept sliding later elements into positions the loop had already passed. That leaves exactly two elements — e1 and e3 — still carrying the class "item." The fix is to iterate over a static copy, e.g. Array.from(items) or a reverse loop, so removals don't reshuffle the indices you're about to read. This liveness behavior is also why querySelectorAll (which returns a static NodeList) does not have this bug: it fixes its matched set at call time instead of tracking the DOM continuously.
Given the CSS: display: flex; flex-direction: row; flex-wrap: wrap; gap: 20px; on a container that is exactly 600px wide, with 6 child items each set to flex: 0 0 120px (flex-grow: 0, flex-shrink: 0, flex-basis: 120px), how are the items distributed across lines and what happens to any leftover space on each line?
Line 1 holds items 1-4 (4 × 120px + 3 × 20px gaps = 540px used, 60px left empty), then item 5 starts line 2 with item 6 joining it (2 × 120px + 1 × 20px gap = 260px used, 340px left empty); every item stays exactly 120px wide because flex-grow: 0 blocks any item from expanding into the unused space
After line 1 packs items 1-4 at 540px used, flex-grow then distributes the remaining 60px evenly across those 4 items, growing each one to 135px wide before line 2 receives items 5 and 6
5 items fit on line 1 because 120px × 5 = 600px matches the container width exactly, with the four 20px gaps absorbed inside the items' own box width; item 6 then wraps alone onto line 2
flex-wrap: wrap overrides the flex-shrink: 0 declaration, so all 6 items compress to fit on a single 600px line, each shrinking to 80px wide with 20px gaps between them
Answer: A. Line 1 holds items 1-4 (4 × 120px + 3 × 20px gaps = 540px used, 60px left empty), then item 5 starts line 2 with item 6 joining it (2 × 120px + 1 × 20px gap = 260px used, 340px left empty); every item stays exactly 120px wide because flex-grow: 0 blocks any item from expanding into the unused space
ExplanationWork through the line-packing algorithm item by item, adding each item's 120px basis plus a 20px gap before it (gaps sit only between items, never at the container's edges): item1 = 120px; +gap+item2 = 120+20+120 = 260px; +gap+item3 = 260+20+120 = 400px; +gap+item4 = 400+20+120 = 540px — all still ≤ 600px, so items 1-4 stay on line 1. Testing item5 next: 540+20+120 = 680px > 600px, so item5 cannot join line 1 and starts line 2 instead. Line 1's actual used width is therefore 4 × 120px + 3 × 20px = 480 + 60 = 540px, leaving 600 − 540 = 60px empty at the end of the line. Line 2 then takes item5 and item6: 120+20+120 = 260px used, leaving 600 − 260 = 340px empty. Because flex-grow is explicitly set to 0 on every item, none of that leftover space is distributed to the items — flex-grow only kicks in when it is a positive number, so each item remains fixed at exactly 120px wide on both lines, and the unused space simply sits after the last item on each line (the default justify-content: flex-start behavior).
Examine this CSS Grid layout with a fixed-width container:
```css
.container {
display: grid;
grid-template-columns: 100px 2fr 1fr;
width: 700px;
}
```
Ignoring gaps and borders, what are the computed widths of the three columns, in order, and why does their total equal exactly 700px?
The three columns compute to 100px, 400px, and 200px — the browser reserves 100px for the fixed track first, then splits the remaining 600px into 3 fr-units of 200px each, so 2fr = 400px and 1fr = 200px.
Because fr units are calculated from the container's full width before fixed tracks are subtracted, the columns compute to 100px, 466.67px, and 233.33px — which actually overflows the 700px container by 100px.
Since grid-template-columns splits total width evenly across all declared tracks regardless of unit type, all three columns compute to 233.33px, ignoring the explicit 100px and fr ratios entirely.
Reading the fr values in ascending order rather than track order gives 100px, 200px, and 400px — assigning the smaller 1fr share to the second column and the larger 2fr share to the third.
Answer: A. The three columns compute to 100px, 400px, and 200px — the browser reserves 100px for the fixed track first, then splits the remaining 600px into 3 fr-units of 200px each, so 2fr = 400px and 1fr = 200px.
Explanationgrid-template-columns: 100px 2fr 1fr defines three tracks: one fixed-width track and two flexible fr tracks. The grid algorithm always lays out non-flexible tracks first, so the browser reserves exactly 100px for the first column before anything else is computed. What's left over is the container width minus that reserved amount: 700px − 100px = 600px. This leftover 600px is then divided among the fr tracks in proportion to their fr values. The two fr values, 2fr and 1fr, sum to 3 fr total, so each fr unit is worth 600px ÷ 3 = 200px. The second column, sized at 2fr, receives 2 × 200px = 400px, and the third column, sized at 1fr, receives 1 × 200px = 200px. Adding all three together — 100px + 400px + 200px = 700px — matches the container's declared width exactly, with no leftover and no overflow. This is the defining behavior of the fr unit: it distributes only the space remaining after every fixed-size (or content-sized) track has already claimed its share, never the container's full width.
Question 27 · Node.js HTTP server & Express routing · hard
Study this Express.js route setup carefully:
```js
const express = require("express");
const app = express();
app.get("/product/:id", (req, res, next) => {
console.log("Middleware A");
next();
});
app.get("/product/:id", (req, res) => {
console.log("Middleware B");
res.send("Found");
});
app.get("/product/:id", (req, res) => {
console.log("Middleware C");
res.send("Also found");
});
```
When a GET request arrives for `/product/55`, which console messages get printed and what response body does the client actually receive?
"Middleware A" and "Middleware B" are logged, and the client receives "Found"; Express advances from the first handler to the second because next() was called, but the third handler never runs since the second handler ends the response without calling next()
"Middleware A", "Middleware B", and "Middleware C" are all logged, since Express runs every handler whose path matches the incoming request regardless of whether next() was called, and the client receives "Also found" because each res.send() call overwrites the previous response before it reaches the client
Only "Middleware A" is logged and the client never receives any response, because calling next() with no arguments forwards the request straight to Express's error-handling middleware, and since this app defines none, the request simply stalls with no reply
The server throws a TypeError before printing anything, because next() is only a valid parameter inside app.use() middleware functions, so referencing it in an app.get() route handler causes Express to crash immediately on startup
Answer: A. "Middleware A" and "Middleware B" are logged, and the client receives "Found"; Express advances from the first handler to the second because next() was called, but the third handler never runs since the second handler ends the response without calling next()
ExplanationExpress keeps matching route handlers in a stack, ordered by registration, and only moves from one to the next when the current handler explicitly calls next(). Tracing this request to /product/55: the first handler matches, prints "Middleware A", and calls next() with no work done on the response — so Express advances to the second matching handler. That handler prints "Middleware B" and calls res.send("Found"), which writes the response body, sets the headers, and calls the underlying res.end() to close out the HTTP response. Because this handler never calls next(), Express has no instruction to continue down the route stack, so the third handler — the one that would log "Middleware C" and send "Also found" — is never invoked at all, not logged and not executed. The client therefore receives exactly "Found" as the response body, and the terminal shows only two lines: "Middleware A" then "Middleware B". This is precisely why Express middleware chains rely on next() as an explicit hand-off signal rather than auto-advancing through every matching route, and why calling res.send() is treated as the end of a handler's involvement in that request-response cycle.
Question 28 · SQL queries & database operations · hard
Consider the SQL transaction: BEGIN; UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; UPDATE accounts SET balance = balance + 100 WHERE account_id = 2; COMMIT; If the second UPDATE fails (e.g., account_id = 2 does not exist), predict what happens to the first UPDATE and explain why transactions ensure ACID atomicity, therefore preventing partial updates that would cause data loss?
If the second UPDATE fails, the transaction is rolled back: account_id = 1 balance change is UNDONE because transactions are atomic (all-or-nothing); ACID guarantees: Atomicity (all-or-nothing), Consistency (valid state), Isolation (concurrent txn isolated), Durability (committed data persists); therefore money is not lost between accounts because either both transfers complete or both are undone
The first UPDATE commits successfully; the second UPDATE fails silently; balance changes persist partially, risking 100 units vanishing from the system: account 1 is left debited while account 2 never receives the credit, an inconsistency ACID transactions exist specifically to prevent
Both UPDATEs succeed regardless of errors; transactions do not guarantee atomicity, because the database engine treats each UPDATE statement as independently durable the moment it executes, regardless of the surrounding BEGIN/COMMIT block
The transaction fails at BEGIN and both UPDATEs are silently skipped, since ROLLBACK only applies when COMMIT is explicitly called after a statement has already succeeded
Answer: A. If the second UPDATE fails, the transaction is rolled back: account_id = 1 balance change is UNDONE because transactions are atomic (all-or-nothing); ACID guarantees: Atomicity (all-or-nothing), Consistency (valid state), Isolation (concurrent txn isolated), Durability (committed data persists); therefore money is not lost between accounts because either both transfers complete or both are undone
ExplanationA transaction wraps multiple statements so that COMMIT applies all of them together and ROLLBACK undoes all of them together — this is atomicity, one of the four ACID properties (Atomicity, Consistency, Isolation, Durability). Suppose account 1 starts with 1000 and account 2 with 500 (total 1500): a fully successful transaction would leave account 1 at 900 and account 2 at 600, total still 1500. But here the second UPDATE fails because account_id = 2 does not exist, so the database rolls back the entire transaction — account 1's balance reverts to 1000, exactly as if neither statement had run. Atomicity is what stops the 100 units from being deducted from account 1 without ever reaching account 2; without it, the total across accounts would drop to 1400 and the missing 100 units would be unaccounted for.
Question 29 · WebSocket & real-time communication · hard
In the WebSocket server code below, every incoming message is broadcast to all connected clients via wss.clients.forEach(): const wss = new WebSocketServer({port: 8080}); wss.on("connection", (ws) => { ws.on("message", (msg) => { wss.clients.forEach(client => { if(client.readyState === WebSocket.OPEN) client.send("Broadcast: " + msg); }); }); }); If 5 clients are connected and client 1 sends a message, how many broadcast messages does the server send?
The forEach loop iterates over all 5 connected clients, and each one whose readyState is OPEN receives the broadcast — so the server sends 5 messages in total, including one back to client 1 itself.
The server sends only 4 messages, because the code excludes the original sender from the broadcast list.
The server sends 25 messages, because each of the 5 clients broadcasts the message to all 5 clients in turn.
WebSocket does not support broadcasting to multiple clients — only one-to-one messaging is possible, so this code could not send more than 1 message.
Answer: A. The forEach loop iterates over all 5 connected clients, and each one whose readyState is OPEN receives the broadcast — so the server sends 5 messages in total, including one back to client 1 itself.
ExplanationWhen client 1 sends a message, the server's message handler fires once, and inside it wss.clients.forEach() iterates over the full client set — client1 through client5. For each client, the code checks client.readyState === WebSocket.OPEN; since all 5 are open, each one receives a call to client.send("Broadcast: " + msg). That produces exactly 5 send calls, hence 5 broadcast frames, and because the sender is never excluded from wss.clients, client 1 also receives its own message echoed back — a common pattern in chat applications. Each frame carries a small WebSocket header plus the "Broadcast: ..." payload, roughly 25 bytes per frame, so the 5 frames together total about 125 bytes. Sending only 4 messages would require the loop to explicitly skip the sender (for example with an if (client !== ws) check), which this code does not do. Sending 25 messages would require every client to independently re-broadcast to all other clients, which nothing in this handler causes. And broadcasting to multiple clients is a standard WebSocket pattern, not a limitation of the protocol.
Question 30 · REST API design & HTTP methods · hard
Examine the HTTP methods: POST /api/users returns 201 Created with Location: /api/users/42; GET /api/users/42 returns 200 OK with user JSON; PATCH /api/users/42 updates and returns 200 OK; DELETE /api/users/42 returns 204 No Content. If client sends DELETE then GET, predict the status codes and explain why 204 indicates successful deletion without response body, proving delete operation persisted?
DELETE succeeds with 204 No Content and no body; GET then returns 404 Not Found because the deleted user record no longer exists, which is what confirms the deletion actually persisted.
DELETE returns 200 OK carrying an empty JSON body rather than 204, and GET also returns 200 OK with an empty body — but a 204 status is never valid for a DELETE response, which is incorrect.
GET incorrectly returns 204 No Content after the DELETE's own 204, as if the deleted resource were still being served from a server-side cache at the same URL — but a deleted resource cannot return 204 on GET.
Both calls return 304 Not Modified — DELETE returns 304 because the URL is unchanged, and GET returns 304 because the client's cached copy of the user is treated as still valid — but 304 requires a conditional GET, not a DELETE.
Answer: A. DELETE succeeds with 204 No Content and no body; GET then returns 404 Not Found because the deleted user record no longer exists, which is what confirms the deletion actually persisted.
ExplanationREST APIs use HTTP status codes to communicate the outcome of each request. POST returns 201 Created because a new resource now exists at the given Location. GET returns 200 OK because it returns the user's JSON body. DELETE returns 204 No Content because the deletion succeeded but there is nothing left to send back — the resource is gone, so the response body is intentionally empty. When the client then sends GET /api/users/42, the server looks up user 42 in the database, finds no matching record because it was just deleted, and returns 404 Not Found. This second status code is the proof that the deletion was real and persisted: if DELETE had merely acknowledged the request without actually removing the record, the follow-up GET would still return 200 OK with the user's data. Instead it returns 404, showing the record is genuinely gone from storage, not just missing from the DELETE response body itself.
Question 31 · SQL JOIN queries · hard
Given the SQL query: SELECT users.name, SUM(orders.amount) AS agg_val FROM users INNER JOIN orders ON users.user_id = orders.user_id GROUP BY users.name HAVING SUM(orders.amount) > 500; — with users having 10000 rows and orders having 50000 rows, analyze the execution plan and predict the output row count?
The query performs an INNER JOIN producing 50000 row combinations (10000 users times 5 average matching orders per user, under the stated uniform distribution), GROUP BY then reduces this to 10000 groups, and HAVING filters groups where SUM(amount) > 500, therefore the result contains only groups meeting the threshold condition
The query returns all 10000 rows from users without filtering because HAVING is evaluated before GROUP BY in the SQL execution order
The INNER JOIN operation returns exactly 50000 rows because the join always preserves the larger table regardless of matching conditions
The GROUP BY clause is redundant when SUM is used because aggregate functions automatically group by the first column in the SELECT list
Answer: A. The query performs an INNER JOIN producing 50000 row combinations (10000 users times 5 average matching orders per user, under the stated uniform distribution), GROUP BY then reduces this to 10000 groups, and HAVING filters groups where SUM(amount) > 500, therefore the result contains only groups meeting the threshold condition
ExplanationStep-by-step SQL execution: (1) FROM clause: scan users (10000 rows) and orders (50000 rows). (2) INNER JOIN: match on user_id; with orders distributed uniformly across users, 50000 orders divided by 10000 users gives 5 matching orders per user on average, so the join produces 50000 row combinations. (3) GROUP BY users.name: collapses these 50000 joined rows back into 10000 groups, one per user. (4) HAVING SUM(amount) > 500: filters those 10000 groups, keeping only the ones whose order total exceeds 500. The final output therefore contains only that filtered subset of groups — not the raw 50000 join rows, not the unfiltered 10000 groups, and not all 10000 users as would happen if HAVING were skipped.
Question 32 · SQL aggregation · hard
Given the SQL query: SELECT products.name, AVG(reviews.rating) AS agg_val FROM products LEFT JOIN reviews ON products.product_id = reviews.product_id GROUP BY products.name HAVING AVG(reviews.rating) > 3.5; — with products having 5000 rows and reviews having 25000 rows, analyze the execution plan and predict the output row count?
At most 5000 rows appear in the result, because GROUP BY products.name collapses the joined output into one row per distinct product, and HAVING can only remove groups — never add them — so the exact count lands somewhere between 0 and 5000 depending on the rating data
Exactly 5000 rows appear in the result, because the LEFT JOIN guarantees every product survives into the final output regardless of what the HAVING clause filters afterward
Exactly 25000 rows appear in the result, because the output row count always matches the larger of the two joined tables in a LEFT JOIN
Exactly 125000000 rows appear in the result, because that raw cross-product of products and reviews is what GROUP BY and HAVING operate on without reducing it
Answer: A. At most 5000 rows appear in the result, because GROUP BY products.name collapses the joined output into one row per distinct product, and HAVING can only remove groups — never add them — so the exact count lands somewhere between 0 and 5000 depending on the rating data
ExplanationThe FROM clause scans products (5000 rows) and reviews (25000 rows), and the LEFT JOIN matches them on product_id, producing at most 5000 × 25000 combinations in the worst case, though in practice each product only combines with its own matching reviews. GROUP BY products.name then collapses every matched row down to one row per distinct product name, so at most 5000 groups exist after this step — products with no matching reviews still form their own group, but AVG(reviews.rating) evaluates to NULL for them. HAVING AVG(rating) > 3.5 then filters these groups: since NULL > 3.5 is never true, reviewless products are dropped, and only groups whose average rating genuinely exceeds 3.5 remain. Because HAVING only ever removes groups and never creates new ones, the final row count is bounded between 0 and 5000 — the precise number cannot be pinned down from the two table sizes alone, since it depends on the actual distribution of rating values.
Question 33 · SQL subqueries · hard
Consider the table Students(student_id, name, marks) with these rows: (1, Aarav, 78), (2, Bhavya, 92), (3, Chirag, 65), (4, Divya, 88), (5, Esha, 74). Given the following query:
```sql
SELECT name FROM Students WHERE marks > (SELECT AVG(marks) FROM Students);
```
What does this query return?
Bhavya and Divya, because the subquery first computes the class average as 79.4, and the outer query returns only the students whose marks exceed that value
All five students, because comparing a column to a scalar subquery's result without wrapping it in ANY or ALL causes SQL to skip the filter and return every row unchanged
Aarav, Chirag, and Esha, because these are the three students whose marks are below the class average of 79.4, and the query filters out the higher scorers
The query fails with an error, because a subquery cannot select from the same table that the outer query is already querying
Answer: A. Bhavya and Divya, because the subquery first computes the class average as 79.4, and the outer query returns only the students whose marks exceed that value
ExplanationThe subquery (SELECT AVG(marks) FROM Students) runs first and independently of the outer query, computing the average of all five marks: (78 + 92 + 65 + 88 + 74) / 5 = 397 / 5 = 79.4. Because this is an uncorrelated subquery, it is evaluated once and its scalar result (79.4) is substituted directly into the WHERE clause, giving WHERE marks > 79.4. The outer query then checks each row: Aarav (78) fails since 78 < 79.4; Bhavya (92) passes; Chirag (65) fails; Divya (88) passes; Esha (74) fails. So the result contains exactly two names: Bhavya and Divya. A scalar subquery (one that returns a single value) never needs ANY or ALL — those keywords are only required when a subquery can return multiple rows — and a subquery is free to select from the same table the outer query is querying, since it is evaluated as a completely separate, independent step before the outer WHERE clause runs.
Question 34 · SQL joins, GROUP BY, and HAVING execution order · hard
Given the SQL query: SELECT students.name, AVG(grades.score) AS agg_val FROM students LEFT JOIN grades ON students.student_id = grades.student_id GROUP BY students.name HAVING AVG(grades.score) > 85; — with students having 3000 rows, grades having 15000 rows, and every grades.student_id referencing an existing student, which statement correctly analyzes the execution plan and the resulting row count?
The LEFT JOIN produces at least 15000 joined rows (exactly 15000 only if every student has at least one grade record; each student with zero matching grades adds one additional row with NULL values), GROUP BY then consolidates these into at most 3000 groups, and HAVING filters those groups to only the ones where AVG(score) > 85, so the final result contains at most 3000 rows, each satisfying the threshold condition
The query returns all 3000 rows from students without filtering because HAVING is evaluated before GROUP BY in the SQL execution order
The LEFT JOIN operation returns exactly 15000 rows because the join always preserves the larger table regardless of matching conditions
The GROUP BY clause is redundant when AVG is used because aggregate functions automatically group by the first column in the SELECT list
Answer: A. The LEFT JOIN produces at least 15000 joined rows (exactly 15000 only if every student has at least one grade record; each student with zero matching grades adds one additional row with NULL values), GROUP BY then consolidates these into at most 3000 groups, and HAVING filters those groups to only the ones where AVG(score) > 85, so the final result contains at most 3000 rows, each satisfying the threshold condition
ExplanationSQL executes in this logical order: FROM/JOIN first, then GROUP BY, then HAVING. In the FROM/JOIN step, because grades.student_id is a foreign key referencing students, each of the 15000 grades rows matches exactly one student row, contributing exactly 15000 joined rows. Because this is a LEFT JOIN, any student row with zero matching grades rows is still kept, with NULLs in place of the grade columns — and each such unmatched student contributes one extra row that is not part of the 15000 count. So the total joined row count is at least 15000, and strictly greater than 15000 whenever any of the 3000 students has no grades at all; it is never capped at 15000. Next, GROUP BY students.name consolidates the joined rows into at most 3000 groups, one per distinct student. Finally, HAVING AVG(score) > 85 discards any group whose average score does not exceed 85, so the query's output is a subset of at most 3000 rows — only the students whose average grade clears the threshold.
Question 35 · SQL GROUP BY · hard
Given the SQL query: SELECT customers.name, COUNT(transactions.total) AS agg_val FROM customers INNER JOIN transactions ON customers.customer_id = transactions.customer_id GROUP BY customers.name HAVING COUNT(transactions.total) > 10; — with customers having 8000 rows and transactions having 100000 rows, analyze the execution plan and predict the output row count?
The query performs an INNER JOIN producing up to 100000 row combinations (bounded by the transactions table, since each transaction matches at most one customer via customer_id), then GROUP BY reduces this to at most 8000 groups, and HAVING filters groups where COUNT(total) > 10, therefore the result contains only groups meeting the threshold condition
The query returns all 8000 rows from customers without filtering because HAVING is evaluated before GROUP BY in the SQL execution order
The INNER JOIN operation returns exactly 100000 rows because the join always preserves the larger table regardless of matching conditions
The GROUP BY clause is redundant when COUNT is used because aggregate functions automatically group by the first column in the SELECT list
Answer: A. The query performs an INNER JOIN producing up to 100000 row combinations (bounded by the transactions table, since each transaction matches at most one customer via customer_id), then GROUP BY reduces this to at most 8000 groups, and HAVING filters groups where COUNT(total) > 10, therefore the result contains only groups meeting the threshold condition
ExplanationStep-by-step SQL execution: (1) FROM clause: scan customers (8000 rows) and transactions (100000 rows). (2) INNER JOIN on customer_id: since customer_id identifies a single customer, each of the 100000 transaction rows matches at most one customer row, so the join produces up to 100000 rows — bounded by the transactions table, not by the product of both table sizes. (3) GROUP BY customers.name: collapses the joined rows into at most 8000 groups, one per customer. (4) HAVING COUNT(total) > 10: keeps only the groups whose transaction count exceeds 10, discarding customers with 10 or fewer transactions. On average each customer has 100000/8000 = 12.5 transactions, so a meaningful share of groups will satisfy the HAVING condition, but the exact result size depends on how unevenly transactions are distributed across customers.
Question 36 · SQL HAVING clause · hard
Given the SQL query: SELECT articles.name, SUM(comments.likes) AS agg_val FROM articles LEFT JOIN comments ON articles.article_id = comments.article_id GROUP BY articles.name HAVING SUM(comments.likes) > 100; — with articles having 1000 rows and comments having 50000 rows, analyze the execution plan and predict the output row count?
The LEFT JOIN produces up to 50000 row combinations (bounded by the comments table, the many side of the relationship), GROUP BY articles.name then collapses these into at most 1000 groups, and HAVING SUM(likes) > 100 keeps only the groups whose total exceeds 100, so the final result contains at most 1000 rows
The query returns all 1000 rows from articles without filtering because HAVING is evaluated before GROUP BY in the SQL execution order
The LEFT JOIN operation returns exactly 50000 rows because the join always preserves the larger table regardless of matching conditions
The GROUP BY clause is redundant when SUM is used because aggregate functions automatically group by the first column in the SELECT list
Answer: A. The LEFT JOIN produces up to 50000 row combinations (bounded by the comments table, the many side of the relationship), GROUP BY articles.name then collapses these into at most 1000 groups, and HAVING SUM(likes) > 100 keeps only the groups whose total exceeds 100, so the final result contains at most 1000 rows
ExplanationStep-by-step SQL execution: (1) FROM clause: scan articles (1000 rows) and comments (50000 rows). (2) LEFT JOIN on article_id: comments is the many side of the one-to-many relationship, so the join is bounded by its row count — at most 50000 combined rows. (3) GROUP BY articles.name collapses these joined rows into at most 1000 groups, one per distinct article name. (4) HAVING SUM(likes) > 100 discards any group whose total likes do not exceed 100. Therefore the final output contains at most 1000 rows, and could be as few as 0 if no article's comments total more than 100 likes.
Question 37 · SQL optimization · hard
Given the SQL query: SELECT warehouses.name, SUM(inventory.quantity) AS agg_val FROM warehouses RIGHT JOIN inventory ON warehouses.warehouse_id = inventory.warehouse_id GROUP BY warehouses.name HAVING SUM(inventory.quantity) > 5000; — with warehouses having 200 rows and inventory having 40000 rows, analyze the execution plan and predict the output row count?
The query performs a RIGHT JOIN producing up to 40000 row combinations, then GROUP BY reduces to 200 groups, and HAVING filters groups where SUM(quantity) > 5000, therefore the result contains only groups meeting the threshold condition
The query returns all 200 rows from warehouses without filtering because HAVING is evaluated before GROUP BY in the SQL execution order
The RIGHT JOIN operation returns exactly 40000 rows because the join always preserves the larger table regardless of matching conditions
The GROUP BY clause is redundant when SUM is used because aggregate functions automatically group by the first column in the SELECT list
Answer: A. The query performs a RIGHT JOIN producing up to 40000 row combinations, then GROUP BY reduces to 200 groups, and HAVING filters groups where SUM(quantity) > 5000, therefore the result contains only groups meeting the threshold condition
ExplanationStep-by-step SQL execution: (1) FROM clause: scan warehouses (200 rows) and inventory (40000 rows). (2) RIGHT JOIN on warehouse_id: since inventory is the right table, all 40000 inventory rows are preserved in the join output, each matched to its warehouse — with a uniform distribution that is 40000/200 = 200 matching rows per warehouse, so the join produces up to 40000 row combinations. (3) GROUP BY warehouses.name: collapses those 40000 joined rows into 200 groups, one per warehouse. (4) HAVING SUM(quantity) > 5000: filters those 200 groups down to only the ones whose total quantity exceeds 5000. The final result therefore contains only the groups meeting that threshold — not all 200 warehouses, and not the full 40000-row join output.
Question 38 · SQL query execution order: JOIN, GROUP BY, and HAVING · hard
Given the SQL query: SELECT flights.name, MIN(bookings.price) AS agg_val FROM flights INNER JOIN bookings ON flights.flight_id = bookings.flight_id GROUP BY flights.name HAVING MIN(bookings.price) > 299; — with flights having 5000 rows and bookings having 80000 rows, analyze the execution plan and predict the output row count?
The query performs a INNER JOIN producing up to 400000000 row combinations, then GROUP BY reduces to 5000 groups, and HAVING filters groups where MIN(price) > 299, therefore the result contains only groups meeting the threshold condition
The query returns all 5000 rows from flights without filtering because HAVING is evaluated before GROUP BY in the SQL execution order
The INNER JOIN operation returns exactly 80000 rows because the join always preserves the larger table regardless of matching conditions
The GROUP BY clause is redundant when MIN is used because aggregate functions automatically group by the first column in the SELECT list
Answer: A. The query performs a INNER JOIN producing up to 400000000 row combinations, then GROUP BY reduces to 5000 groups, and HAVING filters groups where MIN(price) > 299, therefore the result contains only groups meeting the threshold condition
ExplanationStep-by-step SQL execution: (1) FROM clause: scan flights (5000 rows) and bookings (80000 rows). (2) INNER JOIN: match rows on flight_id, producing up to 5000 x 80000 = 400000000 row combinations in the worst case before any grouping happens. (3) GROUP BY flights.name: collapses the joined rows into at most 5000 groups, one per distinct flight. (4) HAVING MIN(bookings.price) > 299: this runs after grouping and keeps only those groups whose minimum price exceeds 299, discarding the rest. Therefore the final output is a subset of the 5000 groups — specifically, only the groups that satisfy the HAVING condition — not the full join output and not all 5000 flights.
Question 39 · SQL Joins, GROUP BY and HAVING · hard
Given the SQL query: SELECT hospitals.name, COUNT(patients.visits) AS agg_val FROM hospitals LEFT JOIN patients ON hospitals.hospital_id = patients.hospital_id GROUP BY hospitals.name HAVING COUNT(patients.visits) > 500; — with hospitals having 300 rows and patients having 200000 rows, analyze the execution plan and predict the output row count?
The query performs a LEFT JOIN producing up to 60000000 row combinations, then GROUP BY reduces to 300 groups, and HAVING filters groups where COUNT(visits) > 500, therefore the result contains only groups meeting the threshold condition
The query returns all 300 rows from hospitals without filtering because HAVING is evaluated before GROUP BY in the SQL execution order
The LEFT JOIN operation returns exactly 200000 rows because the join always preserves the larger table regardless of matching conditions
The GROUP BY clause is redundant when COUNT is used because aggregate functions automatically group by the first column in the SELECT list
Answer: A. The query performs a LEFT JOIN producing up to 60000000 row combinations, then GROUP BY reduces to 300 groups, and HAVING filters groups where COUNT(visits) > 500, therefore the result contains only groups meeting the threshold condition
ExplanationStep-by-step SQL execution: (1) FROM clause reads hospitals (300 rows) and patients (200000 rows). (2) LEFT JOIN matches hospitals.hospital_id to patients.hospital_id; in the theoretical worst case, if every patients row matched every hospitals row, the join could produce up to 300 x 200000 = 60000000 row combinations (in practice far fewer, since only matching rows are joined). (3) GROUP BY hospitals.name then collapses the joined rows into at most 300 groups, one per hospital, since hospitals.name is the grouping key and hospitals has only 300 rows. (4) HAVING COUNT(patients.visits) > 500 is applied after grouping (HAVING always runs after GROUP BY, never before), keeping only the groups whose aggregated visit count exceeds 500. So the final result is a subset of those 300 groups: exactly the hospitals whose total visit count passes the threshold.
Question 40 · SQL JOIN with GROUP BY and HAVING · hard
Given the SQL query: SELECT courses.name, AVG(enrollments.grade) AS agg_val FROM courses INNER JOIN enrollments ON courses.course_id = enrollments.course_id GROUP BY courses.name HAVING AVG(enrollments.grade) > 78; — with courses having 500 rows and enrollments having 30000 rows, analyze the execution plan and predict the output row count?
The query performs an INNER JOIN producing exactly 30000 row combinations (since each enrollment row matches exactly one course via the foreign key), GROUP BY then reduces this to at most 500 groups, and HAVING filters those groups where AVG(grade) > 78, therefore the result contains only the groups meeting the threshold condition
The query returns all 500 rows from courses without filtering because HAVING is evaluated before GROUP BY in the SQL execution order
The INNER JOIN operation returns exactly 30000 rows because the join always preserves the larger table regardless of matching conditions
The GROUP BY clause is redundant when AVG is used because aggregate functions automatically group by the first column in the SELECT list
Answer: A. The query performs an INNER JOIN producing exactly 30000 row combinations (since each enrollment row matches exactly one course via the foreign key), GROUP BY then reduces this to at most 500 groups, and HAVING filters those groups where AVG(grade) > 78, therefore the result contains only the groups meeting the threshold condition
ExplanationStep-by-step SQL execution: (1) FROM clause: scan courses (500 rows) and enrollments (30000 rows). (2) INNER JOIN: each enrollments row has one matching courses row via the course_id foreign key, so the join produces exactly 30000 row combinations, not a cross product of 500 × 30000. (3) GROUP BY courses.name: collapses the 30000 joined rows into at most 500 groups, one per course that has at least one enrollment. (4) HAVING AVG(grade) > 78: keeps only the groups whose average grade exceeds 78, dropping the rest. Therefore the final result contains only the groups meeting the threshold condition, not all 500 courses.