The Relentless Searchers

A medium SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.

Domain
SQL
Difficulty
medium
Seniority
L4
Asked in
Google

Problem

Every row in search_queries is one search, and activity is lopsided enough that many users end up on the same total. Return each user with their query total and a whole-number position starting at 1 for the most active, listed in position order. No two users share a position: equal totals are settled by user ID read as text, alphabetically.

Most users look once and leave. A few never stop looking.

Tables

search_queries

Columns: query_id INTEGER, user_id INTEGER, search_term TEXT, results_count INTEGER, clicked_result INTEGER, query_time TEXT

search_queries: 10 of 200 rows
query_iduser_idsearch_termresults_countclicked_resultquery_time
2071100laptop deals1722026-02-02 01:11:00
2142197phone case iphone 153432026-03-03 02:22:00
2213294usb-c cable 6ft51NULL2026-04-04 03:33:00
2284NULLgaming mouse6852026-05-05 04:44:00
2355488mechanical keyboard8562026-06-06 05:55:00
24265854k monitor102NULL2026-07-07 06:06:00
2497682standing desk11982026-08-08 07:17:00
2568NULLwebcam 1080p13692026-09-09 08:28:00
2639876bluetooth speaker153NULL2026-01-10 09:39:00
2710973smart watch17012026-02-11 10:50:00

Expected output

First 5 of 76 rows
user_idquery_countrnk
NULL501
10022
107023
126424
136125

Worked solution and explanation

What this really is

This is a strict total ordering dressed up as a search leaderboard. Anyone can write GROUP BY user_id with COUNT(*). What separates candidates is seeing that the count is not an ordering on its own: search_queries is long-tailed, so whole crowds of users share a total. Number on the count alone and those users either share a slot or get shuffled into whatever order the engine felt like. Settle the tie on user_id as a number instead of as text and they land in the wrong slots. The second key is the whole problem, and the grader checks every position.


Build it in one pass

Step 1: Count searches per user

GROUP BY user_id with COUNT(*) gives each user's total. Rows with a NULL user_id collapse into one group of their own, and that group stays: no WHERE user_id IS NOT NULL, no inner join to a users table.

Step 2: Order the window on the aggregate itself

Window functions run after GROUP BY, so the window can sort on COUNT(*) directly in the same SELECT: no staging step needed. It orders by COUNT(*) DESC first, then CAST(user_id AS TEXT) ASC. The count settles almost every position; the text key only speaks inside a tie, and it has to be text because the statement asks for alphabetical order of the ID.

Step 3: Number by position, then list in that order

ROW_NUMBER() hands out 1, 2, 3 down that order. The final ORDER BY rnk lists the rows in position order, and since rnk is unique there are no ties left to argue about.


The solution

Count once, number over a total order
SELECT
    user_id,
    COUNT(*) AS query_count,
    ROW_NUMBER() OVER (
        ORDER BY COUNT(*) DESC, CAST(user_id AS TEXT) ASC
    ) AS rnk
FROM search_queries
GROUP BY user_id
ORDER BY rnk
Ordering on the count alone
RANK() OVER (ORDER BY COUNT(*) DESC) gives the four users tied at 2 the same rnk and then skips three numbers. ROW_NUMBER() over the same one-key order gives them different numbers, but which user gets which is up to the engine.
Count, then text ID
Adding CAST(user_id AS TEXT) as the second key gives every user their own number, none skipped, and the same assignment on every run.

Common follow-up questions