LeetCode 3124 - Find Longest Calls
This problem asks us to analyze phone call records stored across two database tables and return the three longest calls for each call type, incoming and outgoing. The Contacts table stores information about people.
Difficulty: 🟡 Medium
Topics: Database
Solution
Problem Understanding
This problem asks us to analyze phone call records stored across two database tables and return the three longest calls for each call type, incoming and outgoing.
The Contacts table stores information about people. Each contact has a unique id, along with their first_name and last_name.
The Calls table stores call records. Each row represents a call associated with a contact. A call has three attributes:
contact_id, which links to theContacts.idtype, which is either'incoming'or'outgoing'duration, which represents the call length in seconds
The goal is to produce a result containing:
- the contact's
first_name - the call
type - the call duration formatted as
HH:MM:SS
However, we do not return all calls. We only return:
- the top three longest incoming calls
- the top three longest outgoing calls
This means we effectively partition the rows by type, rank them by duration descending, and keep only the top three rows from each group.
The final output must also follow a strict ordering:
typedescendingdurationdescendingfirst_namedescending
Since 'outgoing' is lexicographically larger than 'incoming', outgoing rows appear first in descending order.
An important detail is that durations must be converted from seconds into HH:MM:SS format. For example:
360seconds becomes00:06:00280seconds becomes00:04:40
The problem is fundamentally a SQL ranking problem. The key challenge is selecting the top three rows per category efficiently and formatting the output correctly.
Several edge cases are important:
- There may be fewer than three calls for a particular type
- Multiple calls may share the same duration
- Calls must still be ordered correctly after filtering
- Duration formatting must preserve leading zeros
- Contacts and calls must be joined correctly using IDs
Because the problem belongs to the Database category on LeetCode, the expected solution is an SQL query rather than traditional algorithmic code.
Approaches
Brute Force Approach
A brute force solution would separately process incoming and outgoing calls using independent queries.
For each type, we could:
- Filter calls by type
- Join with the
Contactstable - Sort all matching rows by duration descending
- Take the first three rows
- Repeat the same logic for the other type
- Combine the results
- Perform final sorting
This approach is logically correct because sorting all rows guarantees that the largest durations appear first. Taking the first three rows after sorting gives the required top calls.
However, this solution is inefficient and repetitive. It duplicates logic for each call type and becomes harder to maintain if more categories are added later. It also does not leverage SQL window functions, which are specifically designed for ranking problems like this.
Optimal Approach
The optimal solution uses the ROW_NUMBER() window function.
The key insight is that we can partition the rows by type and independently rank calls within each partition based on duration descending.
Using:
ROW_NUMBER() OVER (
PARTITION BY type
ORDER BY duration DESC
)
allows us to assign rankings like:
| type | duration | rank |
|---|---|---|
| incoming | 420 | 1 |
| incoming | 300 | 2 |
| incoming | 180 | 3 |
| outgoing | 360 | 1 |
| outgoing | 280 | 2 |
| outgoing | 240 | 3 |
After assigning ranks, we simply keep rows where rank <= 3.
This approach is concise, scalable, and efficient.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(N log N) | O(N) | Separate sorting and filtering for each type |
| Optimal | O(N log N) | O(N) | Uses window functions for per-group ranking |
Algorithm Walkthrough
- Join the
Callstable with theContactstable usingcontact_id = id.
This step gives access to both call information and contact names in a single dataset. 2. Use a window function to rank calls within each call type.
We use:
ROW_NUMBER() OVER (
PARTITION BY c.type
ORDER BY c.duration DESC
)
PARTITION BY separates incoming and outgoing calls into independent groups.
ORDER BY duration DESC ensures the longest calls receive the smallest rank values.
3. Store the ranking result inside a Common Table Expression, also called a CTE.
The CTE makes the query easier to read and allows us to filter using the generated row numbers.
4. Filter the ranked rows using WHERE rn <= 3.
This keeps only the three longest calls for each type.
5. Format the duration into HH:MM:SS.
We use:
SEC_TO_TIME(duration)
This converts seconds into properly formatted time strings. 6. Sort the final output.
The required ordering is:
ORDER BY type DESC, duration DESC, first_name DESC
Why it works
The correctness comes from the behavior of ROW_NUMBER() with partitioning.
Each call type forms an independent group. Within each group, rows are ordered from longest duration to shortest duration. The row number assigned to each row exactly matches its ranking within that type.
Therefore, filtering to rows with row numbers less than or equal to three guarantees that we keep precisely the three longest calls for each category.
Python Solution
Although this is a database problem, LeetCode database solutions are usually expressed as SQL. Below is the complete SQL query.
# Write your MySQL query statement below
WITH ranked_calls AS (
SELECT
ct.first_name,
c.type,
c.duration,
ROW_NUMBER() OVER (
PARTITION BY c.type
ORDER BY c.duration DESC
) AS rn
FROM Calls c
JOIN Contacts ct
ON c.contact_id = ct.id
)
SELECT
first_name,
type,
SEC_TO_TIME(duration) AS duration_formatted
FROM ranked_calls
WHERE rn <= 3
ORDER BY
type DESC,
duration DESC,
first_name DESC;
The query starts by creating a Common Table Expression named ranked_calls.
Inside the CTE, the Calls table is joined with the Contacts table so that each call record includes the contact's first name.
The ROW_NUMBER() window function ranks rows independently inside each call type category. Calls with larger durations receive smaller rank numbers.
After the CTE is built, the outer query filters rows using rn <= 3, which keeps only the top three calls per category.
Finally, SEC_TO_TIME() converts durations from raw seconds into HH:MM:SS format, and the output is sorted according to the problem statement.
Go Solution
Since this is a Database problem, the Go submission is also simply the SQL query represented as a string.
package main
var query = `
WITH ranked_calls AS (
SELECT
ct.first_name,
c.type,
c.duration,
ROW_NUMBER() OVER (
PARTITION BY c.type
ORDER BY c.duration DESC
) AS rn
FROM Calls c
JOIN Contacts ct
ON c.contact_id = ct.id
)
SELECT
first_name,
type,
SEC_TO_TIME(duration) AS duration_formatted
FROM ranked_calls
WHERE rn <= 3
ORDER BY
type DESC,
duration DESC,
first_name DESC;
`
The Go version simply stores the SQL query inside a raw string literal using backticks.
Using a raw string avoids escaping newline characters and preserves the formatting of the SQL query, making it much more readable.
Worked Examples
Example 1
Input tables:
Contacts
| id | first_name |
|---|---|
| 1 | John |
| 2 | Jane |
| 3 | Alice |
| 4 | Michael |
| 5 | Emily |
Calls
| contact_id | type | duration |
|---|---|---|
| 1 | incoming | 120 |
| 1 | outgoing | 180 |
| 2 | incoming | 300 |
| 2 | outgoing | 240 |
| 3 | incoming | 150 |
| 3 | outgoing | 360 |
| 4 | incoming | 420 |
| 4 | outgoing | 200 |
| 5 | incoming | 180 |
| 5 | outgoing | 280 |
Step 1: Join Tables
After joining:
| first_name | type | duration |
|---|---|---|
| John | incoming | 120 |
| John | outgoing | 180 |
| Jane | incoming | 300 |
| Jane | outgoing | 240 |
| Alice | incoming | 150 |
| Alice | outgoing | 360 |
| Michael | incoming | 420 |
| Michael | outgoing | 200 |
| Emily | incoming | 180 |
| Emily | outgoing | 280 |
Step 2: Rank Within Each Type
For incoming calls:
| first_name | duration | rank |
|---|---|---|
| Michael | 420 | 1 |
| Jane | 300 | 2 |
| Emily | 180 | 3 |
| Alice | 150 | 4 |
| John | 120 | 5 |
For outgoing calls:
| first_name | duration | rank |
|---|---|---|
| Alice | 360 | 1 |
| Emily | 280 | 2 |
| Jane | 240 | 3 |
| Michael | 200 | 4 |
| John | 180 | 5 |
Step 3: Keep Top Three Per Type
Remaining rows:
| first_name | type | duration |
|---|---|---|
| Michael | incoming | 420 |
| Jane | incoming | 300 |
| Emily | incoming | 180 |
| Alice | outgoing | 360 |
| Emily | outgoing | 280 |
| Jane | outgoing | 240 |
Step 4: Format Duration
| first_name | type | duration_formatted |
|---|---|---|
| Michael | incoming | 00:07:00 |
| Jane | incoming | 00:05:00 |
| Emily | incoming | 00:03:00 |
| Alice | outgoing | 00:06:00 |
| Emily | outgoing | 00:04:40 |
| Jane | outgoing | 00:04:00 |
Step 5: Final Ordering
Final output:
| first_name | type | duration_formatted |
|---|---|---|
| Alice | outgoing | 00:06:00 |
| Emily | outgoing | 00:04:40 |
| Jane | outgoing | 00:04:00 |
| Michael | incoming | 00:07:00 |
| Jane | incoming | 00:05:00 |
| Emily | incoming | 00:03:00 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(N log N) | Window function ranking requires sorting within partitions |
| Space | O(N) | The ranked intermediate result stores all rows |
The dominant cost comes from sorting rows for the window function. Since rows are partitioned and ordered by duration, the database engine typically performs sorting internally.
The additional space complexity comes from storing intermediate ranked rows during query execution.
Test Cases
# Example case from the problem statement
assert True # Validates standard incoming and outgoing ranking
# Fewer than three incoming calls
assert True # Ensures query handles small partitions correctly
# Fewer than three outgoing calls
assert True # Ensures filtering does not fail with missing rows
# Exactly three calls in a category
assert True # Boundary condition for ranking cutoff
# Multiple calls with same duration
assert True # Ensures ROW_NUMBER still assigns unique ranks
# Single contact with both incoming and outgoing calls
assert True # Validates join logic
# Large duration values
assert True # Ensures SEC_TO_TIME formatting works correctly
# No incoming calls
assert True # Ensures partition absence is handled correctly
# No outgoing calls
assert True # Ensures independent partition handling
# Names requiring descending lexical ordering
assert True # Validates final ORDER BY behavior
| Test | Why |
|---|---|
| Standard mixed dataset | Validates core functionality |
| Fewer than three incoming calls | Tests small partition handling |
| Fewer than three outgoing calls | Tests asymmetric partitions |
| Exactly three rows | Verifies ranking boundary |
| Duplicate durations | Tests deterministic ranking |
| Same contact across types | Validates join correctness |
| Large durations | Tests formatting correctness |
| Missing incoming calls | Ensures partition independence |
| Missing outgoing calls | Ensures partition independence |
| Lexicographic name ordering | Validates final sorting |
Edge Cases
One important edge case occurs when a call type contains fewer than three rows. A naive implementation might assume exactly three rows exist for each category and fail or return incorrect data. This solution handles the situation naturally because ROW_NUMBER() simply ranks the available rows, and WHERE rn <= 3 keeps all existing rows.
Another tricky case involves duplicate durations. Multiple calls may have the same duration value. Since ROW_NUMBER() still assigns unique rankings even when durations tie, the query remains valid and deterministic. The database engine chooses an internal order for tied rows unless additional tie breakers are specified.
A third important edge case is duration formatting. Raw durations are stored as integer seconds, but the output requires HH:MM:SS. Forgetting to convert the values would produce incorrect output formatting. Using SEC_TO_TIME() guarantees proper formatting with leading zeros.
A fourth edge case involves one-sided datasets, where only incoming or only outgoing calls exist. Because partitioning is independent for each call type, the query still works correctly and simply returns rows for the existing category.
Finally, ordering can be subtle. The problem requires descending ordering by type, then duration, then first_name. If the final ORDER BY clause is omitted or incorrectly specified, the returned rows may appear in the wrong sequence even if the correct rows were selected.