Create an in-memory database that offers a small set of SQL-inspired capabilities. Build it in stages: first support tables and inserts, then add projected queries, filtering through WHERE-style conditions, and ordering through ORDER BY.
Important Notes:
Build a Database class with the operations below.
class Database:
def __init__(self):
"""Initialize the database"""
pass
def create_table(self, table_name: str, columns: List[str]):
"""Create a new table with specified columns"""
pass
def insert(self, table_name: str, row: Dict[str, Any]):
"""Insert a row into the specified table"""
pass
def query(self, table_name: str, columns: List[str]) -> List[Dict[str, Any]]:
"""
Query specific columns from a table (projection).
Returns all rows but only with the specified columns.
"""
pass
db = Database()
db.create_table("users", ["id", "name", "birthday"])
db.insert("users", {"id": "1", "name": "Alice", "birthday": "1990-05-15"})
db.insert("users", {"id": "2", "name": "Bob", "birthday": "1985-08-20"})
db.insert("users", {"id": "3", "name": "Charlie", "birthday": "1992-03-10"})
# Query all users, returning only id and name
result = db.query("users", ["id", "name"])
# Expected: [
# {"id": "1", "name": "Alice"},
# {"id": "2", "name": "Bob"},
# {"id": "3", "name": "Charlie"}
# ]
# Query all users, returning only name
result = db.query("users", ["name"])
# Expected: [
# {"name": "Alice"},
# {"name": "Bob"},
# {"name": "Charlie"}
# ]
The first query retains only each user's id and name, while the second projects the table down to name alone.
Enhance query so callers may optionally supply one predicate to decide which rows are included.
def query(self,
table_name: str,
columns: List[str],
where: Optional[Callable[[Dict], bool]] = None) -> List[Dict[str, Any]]:
"""
Query with optional WHERE condition.
Args:
table_name: Name of the table to query
columns: List of column names to return
where: Optional filter function that takes a row dict and returns bool
"""
pass
db = Database()
db.create_table("users", ["id", "name", "birthday"])
db.insert("users", {"id": "1", "name": "Alice", "birthday": "1990-05-15"})
db.insert("users", {"id": "2", "name": "Bob", "birthday": "1985-08-20"})
db.insert("users", {"id": "3", "name": "Charlie", "birthday": "1992-03-10"})
# Query users born after 1990-01-01
result = db.query(
"users",
["name", "birthday"],
where=lambda row: row["birthday"] > "1990-01-01"
)
# Expected: [
# {"name": "Alice", "birthday": "1990-05-15"},
# {"name": "Charlie", "birthday": "1992-03-10"}
# ]
# Query users with id greater than 1
result = db.query(
"users",
["id", "name"],
where=lambda row: int(row["id"]) > 1
)
# Expected: [
# {"id": "2", "name": "Bob"},
# {"id": "3", "name": "Charlie"}
# ]
The first result includes the two birthdays later than 1990-01-01; the second includes the rows whose numeric IDs exceed 1.
Expand filtering so several conditions can be evaluated together with AND semantics.
# Query users with id > 1 AND name starting with 'C'
result = db.query(
"users",
["id", "name"],
where=lambda row: int(row["id"]) > 1 and row["name"].startswith("C")
)
# Expected: [{"id": "3", "name": "Charlie"}]
Only Charlie satisfies both the ID condition and the name-prefix condition.
You may instead define the interface to receive a collection of condition tuples:
def query(self,
table_name: str,
columns: List[str],
where: Optional[List[Tuple[str, str, Any]]] = None) -> List[Dict[str, Any]]:
"""
Args:
where: List of conditions [(column, operator, value),...]
All conditions are combined with AND
Operators: "=", ">", "<", ">=", "<=", "!="
"""
pass
# Query users with id > 1 AND name = "Charlie"
result = db.query(
"users",
["id", "name"],
where=[("id", ">", "1"), ("name", "=", "Charlie")]
)
# Expected: [{"id": "3", "name": "Charlie"}]
This tuple-list representation is an optional Part 3 API rather than a required replacement. The reference implementation described later accepts both a callable filter such as where=lambda row:... and a tuple-list filter such as where=[("col", "op", value),...].
Add optional sorting to query.
The interface should leave room for the multi-column ordering introduced in Part 5. You may begin with order_by: Optional[str] and revise it later, which is simpler but breaks backward compatibility, or adopt order_by: Optional[Union[str, Tuple[List[str], bool]]] immediately, which is more extensible but more involved.
For the basic form, the signature can be:
def query(self,
table_name: str,
columns: List[str],
where: Optional[Callable[[Dict], bool]] = None,
order_by: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Query with optional WHERE and ORDER BY.
Args:
order_by: Column name to sort by (ascending order)
"""
pass
The reference implementation uses the extensible Part 5 representation from the beginning, so it does not need to change the interface later.
db = Database()
db.create_table("users", ["id", "name", "age"])
db.insert("users", {"id": "1", "name": "Alice", "age": "30"})
db.insert("users", {"id": "2", "name": "Bob", "age": "25"})
db.insert("users", {"id": "3", "name": "Charlie", "age": "35"})
# Query users ordered by name (using simple string)
result = db.query("users", ["name", "age"], order_by="name")
# Expected: [
# {"name": "Alice", "age": "30"},
# {"name": "Bob", "age": "25"},
# {"name": "Charlie", "age": "35"}
# ]
# Alternatively, if using Part 5's format from the start:
result = db.query("users", ["name", "age"], order_by=(["name"], True))
# Query users with WHERE and ORDER BY
result = db.query(
"users",
["name", "age"],
where=lambda row: int(row["age"]) > 28,
order_by="name" # or order_by=(["name"], True)
)
# Expected: [
# {"name": "Alice", "age": "30"},
# {"name": "Charlie", "age": "35"}
# ]
The first ordered result is alphabetical by name. In the final query, Bob is removed by the predicate, and the remaining names are returned alphabetically.
Extend ordering to accept multiple sort keys and one common direction for all of them.
def query(self,
table_name: str,
columns: List[str],
where: Optional[Callable[[Dict], bool]] = None,
order_by: Optional[Tuple[List[str], bool]] = None) -> List[Dict[str, Any]]:
"""
Query with optional WHERE and ORDER BY.
Args:
order_by: Tuple of (column_list, is_ascending)
Example: (["age", "name"], True) sorts by age then name, both ascending
Example: (["age", "name"], False) sorts by age then name, both descending
Note: All columns use the same sort direction in this simplified version
"""
pass
db = Database()
db.create_table("users", ["id", "name", "birthday"])
db.insert("users", {"id": "1", "name": "Ada", "birthday": "1815-12-10"})
db.insert("users", {"id": "2", "name": "Charles", "birthday": "1791-12-26"})
db.insert("users", {"id": "3", "name": "Charles", "birthday": "1903-06-23"})
# Sort by name descending, then birthday descending
result = db.query(
"users",
["id", "name", "birthday"],
order_by=(["name", "birthday"], False)
)
# Expected: [
# {"id": "3", "name": "Charles", "birthday": "1903-06-23"},
# {"id": "2", "name": "Charles", "birthday": "1791-12-26"},
# {"id": "1", "name": "Ada", "birthday": "1815-12-10"}
# ]
# With WHERE clause
result = db.query(
"users",
["id"],
where=lambda row: row["name"] == "Charles",
order_by=(["birthday"], False)
)
# Expected: [{"id": "3"}, {"id": "2"}]
The first query places the two Charles rows before Ada and uses birthday to break their tie, all in descending order. The second query keeps only Charles and orders those two rows by descending birthday.
A more advanced API could assign a direction independently to each key:
order_by: Optional[List[Tuple[str, str]]] = None
# Example: [("name", "ASC"), ("birthday", "DESC")]
That design supports mixed directions, but it makes the implementation more complicated. The simpler all-columns-share-one-direction model above is sufficient for this interview exercise.
This part is generally a design discussion rather than an implementation task.
The interviewer may ask: “How would you improve query performance by adding indexes?”
Inverted Index for WHERE Clauses
{column_name: {value: [row_indices]}}.{"name": {"Alice": [0], "Bob": [1, 3]}} maps each value to the rows containing it.B-Tree Index for Range Queries
>, <, >=, and <= predicates.Composite Index
(age, name) for queries that filter on both fields.Index Maintenance
Query Optimization Strategy
class Database:
def __init__(self):
self.tables = {}
self.schemas = {}
self.indexes = {} # {table_name: {column_name: {value: [row_indices]}}}
def create_index(self, table_name: str, column_name: str):
"""Build inverted index on column"""
if table_name not in self.indexes:
self.indexes[table_name] = {}
index = {}
for i, row in enumerate(self.tables[table_name]):
value = row[column_name]
if value not in index:
index[value] = []
index[value].append(i)
self.indexes[table_name][column_name] = index
As each capability is added, retain compatibility with the earlier calls whenever possible:
# Part 1: query(table, columns)
# Part 2: query(table, columns, where=None)
# Part 3: Same signature, enhanced where logic
# Part 4: query(table, columns, where=None, order_by=None)
# Part 5: Same signature, enhanced order_by logic
class Database:
def __init__(self):
self.tables = {} # {table_name: [row_dicts]}
self.schemas = {} # {table_name: [column_names]}