# 60 phút luyện vòng technical screen FDE: SQL, JSON và kiểm tra dữ liệu bằng Python

> Cú pháp thì tra được. Cái bẫy thật sự là dữ liệu sai mà không hề báo lỗi, và sáu bài tập sau giúp bạn tập thói quen bắt được nó.

Bản gốc: https://fdetimes.net/vi/bach-khoa/fde-technical-screen-sql-python-json-practice/

Ở Databricks, vòng technical screen cho vị trí forward deployed engineer chỉ kéo dài 45 đến 60 phút. Chừng đó thời gian được chia cho nhiều câu hỏi ngắn, và cả buổi diễn ra từ xa qua video. Với nhịp như vậy, bạn gần như không có thời gian tra cú pháp.

Hướng dẫn phỏng vấn của Aced đưa ra một câu SQL mẫu cho vòng này: viết truy vấn trả về những người dùng có nhiều hơn một giao dịch. Câu này dễ. Nhưng chính vì dễ, nó cho bạn cơ hội hỏi thêm về NULL, về bản ghi trùng và về định dạng dữ liệu đầu vào, thay vì gõ xong đáp án rồi dừng.

Bài này là một buổi luyện 60 phút gồm sáu bước. Bạn sẽ viết bốn mẫu SQL và một đoạn Python đọc JSON bẩn. Trên đường đi, bạn sẽ gặp hai lỗi mà module json của Python không hề báo.

## Cần chuẩn bị gì trước khi bấm giờ?

Bạn cần Python 3 và một database hỗ trợ window function, chẳng hạn PostgreSQL. Tạo một bảng mẫu tên `transactions` với các cột `user_id`, `txn_id`, `email`, `amount`, `created_at`, rồi chèn khoảng hai chục dòng. Nhớ cố ý thêm vài dòng trùng `txn_id` và vài dòng có `email` là NULL.

Dữ liệu là tự bịa, nhưng phải bẩn có chủ đích. Bảng quá sạch thì chẳng dạy được gì cho vòng screen này, và cũng không giống dữ liệu bạn sẽ gặp khi đi làm.

## Bước 1–2: đếm cho đúng rồi mới đếm cho nhanh

**Bước 1 (10 phút).** Giải câu hỏi mẫu của Databricks:

```sql
SELECT user_id, COUNT(*) AS n_txn
FROM transactions
GROUP BY user_id
HAVING COUNT(*) > 1;
```

Kiểm tra: đếm tay xem kết quả có khớp với số người dùng bạn cố ý cho nhiều giao dịch không. Sau đó tự hỏi: nếu cùng một `txn_id` bị ghi hai lần, người dùng đó có bị tính nhầm là "nhiều hơn một giao dịch" không? Có. Bước 3 sẽ xử lý chuyện này.

**Bước 2 (10 phút).** Đo tỷ lệ NULL. Tài liệu SQLite định nghĩa rõ: `COUNT(X)` chỉ đếm những dòng mà X khác NULL, còn `COUNT(*)` đếm mọi dòng trong nhóm. Lấy số thứ hai trừ số thứ nhất là ra số dòng thiếu dữ liệu.

```sql
SELECT COUNT(*)                AS total_rows,
       COUNT(email)            AS has_email,
       COUNT(*) - COUNT(email) AS missing_email
FROM transactions;
```

Thử hình dung bảng có 1.000 dòng và 940 dòng có email. Vậy 60 dòng thiếu email, tức 6%. Đừng dừng ở con số đó. Hãy nói thêm xem 6% này có làm hỏng phép tính nào ở các bước sau không.

## Bước 3–4: khi nào cần đến window function?

**Bước 3 (10 phút).** Khử trùng lặp. Theo tài liệu PostgreSQL, `ROW_NUMBER` đánh số các dòng trong mỗi partition, bắt đầu từ 1. Bạn chia partition theo khóa nghiệp vụ, sắp xếp theo thời gian giảm dần, rồi giữ dòng số 1, tức bản ghi mới nhất.

```sql
WITH ranked AS (
  SELECT *,
         ROW_NUMBER() OVER (
           PARTITION BY txn_id
           ORDER BY created_at DESC
         ) AS rn
  FROM transactions
)
SELECT * FROM ranked WHERE rn = 1;
```

Kiểm tra: số dòng trả về phải bằng `COUNT(DISTINCT txn_id)` của bảng gốc. Sau đó chạy lại Bước 1 trên CTE này và đặt hai kết quả cạnh nhau. Người phỏng vấn sẽ thấy bạn tự kiểm chứng câu trả lời của chính mình.

**Bước 4 (10 phút).** Top-N theo nhóm. Điểm cần nắm là cách xử lý khi có đồng hạng. `RANK` để lại khoảng trống sau nhóm đồng hạng. `DENSE_RANK` thì không, vì nó đếm theo từng nhóm đồng hạng.

```sql
WITH ranked AS (
  SELECT user_id, txn_id, amount,
         RANK() OVER (
           PARTITION BY user_id ORDER BY amount DESC
         ) AS rnk,
         DENSE_RANK() OVER (
           PARTITION BY user_id ORDER BY amount DESC
         ) AS drnk
  FROM transactions
)
SELECT * FROM ranked WHERE rnk <= 2;
```

Với một người dùng có ba giao dịch, hai cột này cho kết quả như sau:

| amount | RANK | DENSE_RANK |
|---|---|---|
| 100 | 1 | 1 |
| 100 | 1 | 1 |
| 90 | 3 | 2 |

Giả sử câu hỏi là "lấy top 2 giao dịch lớn nhất của mỗi người dùng". Lọc `rnk <= 2` sẽ trả về hai dòng 100. Đổi thành `drnk <= 2` thì trả về cả ba dòng. Không cách nào đúng tuyệt đối, nên câu trả lời tốt là hỏi lại người phỏng vấn muốn xử lý đồng hạng thế nào.

## Bước 5: JSON bẩn không được làm sập vòng lặp

Theo tài liệu Python, `json.loads` nhận một `str`, `bytes` hoặc `bytearray` chứa tài liệu JSON và trả về đối tượng Python. Nếu dữ liệu không phải JSON hợp lệ, hàm ném `JSONDecodeError`, một lớp con của `ValueError`. Vì vậy, lần nào đọc dữ liệu từ bên ngoài cũng nên bọc trong try/except.

```python
import json

lines = [
    '{"user_id": 1, "amount": 20.5}',
    '{"user_id": 2, "amount": NaN}',
    '{"user_id": 3, "user_id": 4, "amount": 5}',
    '{"user_id": 5, "amount": ',
]

good, bad = [], []
for i, line in enumerate(lines):
    try:
        good.append(json.loads(line))
    except json.JSONDecodeError as e:
        bad.append((i, str(e)))

print(len(good), len(bad))
```

Kiểm tra: bạn sẽ thấy 3 dòng "tốt" và 1 dòng lỗi, chính là dòng bị cắt cụt ở cuối. Hãy giữ lại số dòng và thông báo lỗi, đừng `pass` cho qua. Khách hàng sẽ muốn biết chính xác dòng nào hỏng.

## Bước 6: vì sao "3 dòng tốt" là con số sai?

In `good` ra xem. Dòng thứ hai có `amount` là `nan`. Lý do: theo tài liệu Python, module json mặc định chấp nhận `Infinity`, `-Infinity` và `NaN` như số JSON hợp lệ, dù chuẩn JSON không cho phép. Dòng thứ ba có `user_id` bằng 4, vì gặp khóa trùng thì module này không ném lỗi mà chỉ giữ cặp tên–giá trị cuối cùng.

**Điểm mấu chốt:** Parse thành công chưa có nghĩa là dữ liệu đúng.

Bắt NaN không cần thư viện nào thêm, vì NaN là giá trị duy nhất không bằng chính nó:

```python
for rec in good:
    amt = rec.get("amount")
    if isinstance(amt, float) and amt != amt:
        print("NaN:", rec)
```

Khóa trùng khó bắt hơn, vì khi đã thành dict thì giá trị cũ mất rồi.   Phiên bản dưới đây đã rút gọn, đủ để bạn dùng khi luyện tập:

```python
def no_dup_keys(pairs):
    keys = [k for k, _ in pairs]
    if len(keys) != len(set(keys)):
        raise ValueError(f"khóa trùng: {keys}")
    return dict(pairs)

json.loads(line, object_pairs_hook=no_dup_keys)
```

Nếu đưa hook này vào vòng lặp ở Bước 5, hãy đổi `except json.JSONDecodeError` thành `except ValueError`. Vì `JSONDecodeError` là lớp con của `ValueError`, một khối except đó bắt được cả dòng bị cắt cụt lẫn dòng có khóa trùng. Chạy lại, bạn sẽ thấy 2 dòng tốt và 2 dòng lỗi, còn dòng NaN thì được bắt riêng bằng đoạn kiểm tra ở trên.

## Những chỗ dễ trượt dù bạn biết làm

Một lỗi hay gặp là dùng `COUNT(email)` khi định đếm số dòng, hoặc ngược lại, rồi báo một con số sai mà không hề biết. Một lỗi khác là khử trùng lặp bằng `DISTINCT` trên mọi cột: hai bản ghi trùng `txn_id` thường lệch nhau ở `created_at`, nên `DISTINCT` không gộp được. Lỗi thứ ba là nuốt `JSONDecodeError` bằng một khối `except` trống.

Còn một rủi ro không nằm trong code: im lặng. Buổi phỏng vấn diễn ra qua video, nên người phỏng vấn chỉ biết bạn đang nghĩ gì khi bạn nói ra. Hãy nói giả định của mình trước khi gõ.

## Ở chỗ khách hàng, đây là việc của tuần đầu

Sáu bước trên không chỉ để đi phỏng vấn. Khi khách hàng gửi file export đầu tiên, hãy làm ba việc trước khi dựng pipeline hay agent nào: chạy tỷ lệ NULL, đếm bản ghi trùng theo khóa nghiệp vụ, và đọc thử file JSON để lấy danh sách dòng lỗi.

Có trong tay ba con số đó, buổi họp đầu tiên với đội dữ liệu của khách sẽ xoay quanh dữ liệu thật thay vì phỏng đoán.

Khi viết CV, đừng ghi "thành thạo SQL". Hãy viết một dòng cụ thể, chẳng hạn "phát hiện và loại bản ghi trùng theo mã giao dịch trước khi đưa dữ liệu vào báo cáo", kèm con số thật từ dự án của bạn.

Cú pháp `ROW_NUMBER` tra một phút là ra. Thói quen nghi ngờ dữ liệu trước khi khách hàng nghi ngờ bạn thì phải luyện, và 60 phút này là để luyện nó.

**Thử ngay tuần này:**

- Hẹn giờ 60 phút rồi làm hết sáu bước trong bài trên một database có window function. Vừa làm vừa nói to cách nghĩ của mình, như khi đang gọi video với người phỏng vấn.
- Viết một hàm Python đọc file JSON từng dòng và trả về danh sách dòng lỗi kèm số dòng. Đưa nó lên GitHub cùng một README giải thích hai cái bẫy khóa trùng và NaN.
- Lấy một bảng dữ liệu ở chỗ làm hoặc trong dự án cá nhân, chạy truy vấn tỷ lệ NULL cho mọi cột quan trọng, rồi viết kết quả thành ba câu.

## Nguồn

- [Databricks Forward Deployed Engineer Interview Guide | Sample Questions (2026) - Aced (formerly Exponent)](https://www.aced.io/guides/databricks-forward-deployed-engineer-interview)

- [json — JSON encoder and decoder — Python 3.15.2 documentation](https://docs.python.org/3/library/json.html)

- [Built-in Aggregate Functions](https://www.sqlite.org/lang_aggfunc.html)

- [PostgreSQL: Documentation: 18: 9.22. Window Functions](https://www.postgresql.org/docs/current/functions-window.html)
