577. Employee Bonus
On LeetCode ->Problem¶
Return each employee's name and bonus when their bonus is below 1000, or when they have no bonus row.
Input:
# Employee
------+--------+------------+-------
empId | name | supervisor | salary
------+--------+------------+-------
3 | Brad | null | 4000
1 | John | 3 | 1000
2 | Dan | 3 | 2000
4 | Thomas | 3 | 4000
# Bonus
------+------
empId | bonus
------+------
2 | 500
4 | 2000
Output:
Key trick¶
Use a LEFT JOIN from Employee to Bonus, then keep rows where bonus < 1000 or bonus IS NULL.
Trap¶
- Using an
INNER JOINremoves employees with no bonus. - Writing only
bonus < 1000missesNULLbonuses because comparisons withNULLare unknown. - Using
<= 1000is wrong because the condition is strictly less than1000. - Putting
bonus < 1000only in theJOINcondition can incorrectly keep employees with bonuses>= 1000as if they had no bonus.
Why is it interesting?¶
This is a small but classic test of LEFT JOIN semantics and NULL handling.
SQL solution¶
SELECT
e.name,
b.bonus
FROM Employee AS e
LEFT JOIN Bonus AS b
ON e.empId = b.empId
-- Keep real low bonuses and employees without a matching bonus row.
WHERE b.bonus < 1000
OR b.bonus IS NULL;
Pandas solution¶
import pandas as pd
def employee_bonus(employee: pd.DataFrame, bonus: pd.DataFrame) -> pd.DataFrame:
return (
employee
# LEFT JOIN so employees without bonus are preserved.
.merge(bonus, on="empId", how="left")
# Keep bonus < 1000 or missing bonus.
.loc[lambda df: (df["bonus"] < 1000) | df["bonus"].isna()]
[["name", "bonus"]]
)
Pytest test¶
import sqlite3
import pandas as pd
import pytest
SQLITE_QUERY = """
SELECT
e.name,
b.bonus
FROM Employee AS e
LEFT JOIN Bonus AS b
ON e.empId = b.empId
WHERE b.bonus < 1000
OR b.bonus IS NULL;
"""
def employee_bonus(employee: pd.DataFrame, bonus: pd.DataFrame) -> pd.DataFrame:
return (
employee
.merge(bonus, on="empId", how="left")
.loc[lambda df: (df["bonus"] < 1000) | df["bonus"].isna()]
[["name", "bonus"]]
)
def run_sqlite(employee_rows, bonus_rows):
conn = sqlite3.connect(":memory:")
conn.execute(
"""
CREATE TABLE Employee (
empId INTEGER,
name TEXT,
supervisor INTEGER,
salary INTEGER
)
"""
)
conn.execute(
"""
CREATE TABLE Bonus (
empId INTEGER,
bonus INTEGER
)
"""
)
conn.executemany(
"""
INSERT INTO Employee (empId, name, supervisor, salary)
VALUES (?, ?, ?, ?)
""",
employee_rows,
)
conn.executemany(
"""
INSERT INTO Bonus (empId, bonus)
VALUES (?, ?)
""",
bonus_rows,
)
return conn.execute(SQLITE_QUERY).fetchall()
def make_employee_df(rows):
return pd.DataFrame(
rows,
columns=["empId", "name", "supervisor", "salary"],
)
def make_bonus_df(rows):
return pd.DataFrame(
rows,
columns=["empId", "bonus"],
)
def normalize_rows(rows):
return sorted(rows, key=lambda row: row[0])
def normalize_pandas_result(df):
normalized = df.copy()
normalized["bonus"] = normalized["bonus"].astype("object")
normalized["bonus"] = normalized["bonus"].where(normalized["bonus"].notna(), None)
return normalize_rows([tuple(row) for row in normalized[["name", "bonus"]].to_numpy()])
@pytest.mark.parametrize(
"employee_rows, bonus_rows, expected",
[
(
[
(3, "Brad", None, 4000),
(1, "John", 3, 1000),
(2, "Dan", 3, 2000),
(4, "Thomas", 3, 4000),
],
[
(2, 500),
(4, 2000),
],
[
("Brad", None),
("John", None),
("Dan", 500),
],
),
(
[
(1, "A", None, 100),
(2, "B", 1, 200),
],
[],
[
("A", None),
("B", None),
],
),
(
[
(1, "Low", None, 100),
(2, "Boundary", None, 100),
(3, "High", None, 100),
],
[
(1, 999),
(2, 1000),
(3, 1001),
],
[
("Low", 999),
],
),
(
[],
[],
[],
),
],
)
def test_employee_bonus_sqlite_and_pandas(employee_rows, bonus_rows, expected):
sql_result = run_sqlite(employee_rows, bonus_rows)
employee = make_employee_df(employee_rows)
bonus = make_bonus_df(bonus_rows)
pandas_result = employee_bonus(employee, bonus)
assert normalize_rows(sql_result) == normalize_rows(expected)
assert normalize_pandas_result(pandas_result) == normalize_rows(expected)
Comment on my solution¶
Your SQL solution is correct: it uses the required LEFT JOIN and handles missing bonuses with IS NULL.
Your Pandas solution is also correct: how="left" preserves employees without bonuses, and isna() correctly mirrors the SQL IS NULL condition.