Bảng, dòng và cột
Bạn sẽ học được
- Một bảng trong cơ sở dữ liệu quan hệ gồm những gì: dòng, cột, kiểu dữ liệu.
- Khoá chính (primary key) và khoá ngoại (foreign key) — hai khái niệm quyết định cách dữ liệu liên kết.
NULLnghĩa là gì và vì sao nó khác chuỗi rỗng hay số0.- Cách tự khám phá cấu trúc một bảng khi chưa có tài liệu.
- Bộ dữ liệu bán lẻ của DataHọc gồm những bảng nào và chúng nối với nhau ra sao.
Khái niệm cốt lõi
Hãy hình dung một bảng như một trang tính Excel. Phần trên cùng là tên các cột, mỗi hàng bên dưới là một bản ghi. Cơ sở dữ liệu quan hệ dùng đúng mô hình đó, chỉ khác ở ba điểm quan trọng: mỗi cột có kiểu dữ liệu cố định, mỗi dòng thường có khoá chính, và các bảng có thể trỏ vào nhau bằng khoá ngoại.
Cột là một thuộc tính, ví dụ city trong bảng khách hàng. Mỗi cột có kiểu: integer cho số nguyên, text cho chuỗi, numeric(12, 2) cho số tiền, date cho ngày. Kiểu dữ liệu không phải thủ tục giấy tờ — nó quyết định bạn so sánh được gì, tính toán được gì và dữ liệu rác có bị chặn lại hay không. Nhét "mười" vào cột price sẽ bị từ chối ngay.
Dòng là một bản ghi cụ thể: khách hàng An Nguyễn, sống ở Hà Nội, đăng ký ngày 12/03/2023. Trong một bảng, mỗi dòng mô tả đúng một đối tượng.
Khoá chính là một cột (hoặc nhóm cột) định danh duy nhất mỗi dòng. Bảng customers có id là khoá chính: không có hai khách hàng cùng id, và id không được để trống. Nhờ khoá chính, bạn có thể nói "khách hàng 7" mà không sợ nhầm với ai khác, kể cả khi có người trùng tên.
Khoá ngoại là cột trỏ sang khoá chính của bảng khác. orders.customer_id là khoá ngoại trỏ về customers.id. Nó cho biết đơn hàng này của khách nào, đồng thời ngăn tạo đơn hàng cho một khách không tồn tại. Đây chính là "quan hệ" trong "cơ sở dữ liệu quan hệ" — và là lý do bạn cần học JOIN ở chương 5 để ghép thông tin từ nhiều bảng.
NULL là giá trị "không biết" hoặc "không áp dụng". Khách hàng Dũng Phạm có email là NULL, nghĩa là chưa có hoặc không cung cấp — khác hẳn với chuỗi rỗng ''. Đây là nguồn gốc của rất nhiều bug phân tích, vì NULL không bằng chính nó và các phép so sánh thông thường sẽ bỏ sót nó. Khi cần tìm dòng thiếu dữ liệu, bạn phải dùng IS NULL, không dùng = NULL.
Cuối cùng, từ schema trong ngữ cảnh này có hai nghĩa. Nghĩa hẹp: mô tả cấu trúc — bảng nào, cột nào, kiểu gì. Nghĩa rộng hơn trong các công cụ như dbt: tầng tổ chức dữ liệu. Bài này dùng nghĩa hẹp.
Ví dụ chạy được
Bộ dữ liệu bán lẻ của DataHọc có bốn bảng, nối với nhau thành một chuỗi:
customers ──< orders ──< order_items >── productsDấu ──< đọc là "một khách có nhiều đơn". Cụ thể:
| Bảng | Vai trò | Khoá chính | Khoá ngoại |
|---|---|---|---|
customers | Khách hàng | id | — |
products | Sản phẩm | id | — |
orders | Đơn hàng | id | customer_id → customers.id |
order_items | Dòng hàng trong đơn | id | order_id → orders.id, product_id → products.id |
Xem ba dòng đầu của products:
SELECT *
FROM products
LIMIT 3;| id | name | category | price | stock |
|---|---|---|---|---|
| 1 | Tai nghe Bluetooth | Điện tử | 890000 | 25 |
| 2 | Bàn phím cơ | Điện tử | 1250000 | 10 |
| 3 | Nồi chiên không dầu | Gia dụng | 1690000 | 5 |
Đọc bảng này: id là khoá chính, price lưu số tiền chưa có dấu phân cách, stock là số lượng tồn kho — sản phẩm id 4 (Bình giữ nhiệt) có stock = 0, nghĩa là hết hàng.
Giờ xem orders, chú ý cột customer_id:
SELECT id, customer_id, order_date, status
FROM orders
LIMIT 3;| id | customer_id | order_date | status |
|---|---|---|---|
| 1 | 1 | 2025-01-05 | completed |
| 2 | 2 | 2025-01-18 | completed |
| 3 | 3 | 2025-02-02 | completed |
Đơn 1 thuộc khách 1 (An Nguyễn), đơn 2 thuộc khách 2 (Bình Trần). Nếu bạn muốn biết tên khách của đơn 1, bảng orders một mình không trả lời được — nó chỉ có số 1. Bạn cần ghép sang customers. Chính xác đó là việc JOIN sẽ làm.
Còn order_items lưu từng món trong đơn. Một đơn có thể có nhiều dòng hàng, mỗi dòng ghi sản phẩm, số lượng và đơn giá tại thời điểm mua:
SELECT order_id, product_id, quantity, unit_price
FROM order_items
WHERE order_id = 1;| order_id | product_id | quantity | unit_price |
|---|---|---|---|
| 1 | 1 | 2 | 890000 |
| 1 | 4 | 1 | 320000 |
Đơn 1 mua 2 tai nghe và 1 bình giữ nhiệt. Tổng tiền của đơn là 2 × 890000 + 1 × 320000 = 2.100.000. Bạn vừa đọc được một phép tính kinh doanh chỉ từ cấu trúc bảng.
Khi chưa có tài liệu, cách nhanh nhất để xem cấu trúc là lấy một dòng và đọc tên cột:
SELECT * FROM customers LIMIT 1;Kết quả cho thấy customers có các cột id, name, email, city, signup_date, segment. Nếu đang dùng psql, lệnh \d customers in ra kiểu dữ liệu và ràng buộc đầy đủ; trong trình duyệt của DataHọc, SELECT * là đủ để bắt đầu.
Thực hành
- Chạy
SELECT * FROM products;và đếm xem có bao nhiêu sản phẩm. Sản phẩm nào hết hàng? - Chạy
SELECT * FROM orders WHERE customer_id = 2;để xem khách Bình Trần đã đặt những đơn nào. Đối chiếustatuscủa từng đơn. - Chạy
SELECT * FROM order_items LIMIT 5;rồi tự tính tổng tiền của đơn 2 bằng tay: cộngquantity × unit_pricecủa các dòng cóorder_id = 2.
Bài học này chưa yêu cầu viết truy vấn phức tạp. Mục tiêu là bạn hình dung được dữ liệu nằm ở đâu, cột nào nối với cột nào — kỹ năng này sẽ tiết kiệm cho bạn hàng giờ khi làm việc với một database lạ.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
column "customers" does not exist khi viết SELECT customers | Nhầm tên bảng thành tên cột | customers phải đứng sau FROM, không đứng sau SELECT |
Đếm được ít dòng hơn mong đợi khi lọc email != 'x' | Các dòng NULL bị loại khỏi mọi so sánh | Dùng email IS NOT NULL AND email != 'x' khi cần tính cả trường hợp thiếu |
invalid input syntax for type integer | Nhét chữ vào cột số, thường do nhầm cột | Kiểm tra kiểu dữ liệu bằng SELECT * ... LIMIT 1 hoặc \d |
Tưởng id tự tăng nhưng giá trị nhảy cóc | id là khoá chính, không hứa liên tục | Không bao giờ dựa vào tính liên tục của id để đếm |
Tóm tắt
- Bảng gồm dòng (bản ghi) và cột (thuộc tính có kiểu dữ liệu cố định).
- Khoá chính định danh duy nhất một dòng; khoá ngoại trỏ sang bảng khác và tạo nên quan hệ.
NULLlà "không biết", khác chuỗi rỗng và số0; so sánh vớiNULLcầnIS NULL/IS NOT NULL.- Bộ dữ liệu bán lẻ:
customers→orders→order_items←products. - Muốn hiểu một bảng lạ, lấy một dòng bằng
SELECT * ... LIMIT 1và đọc tên cột trước khi viết truy vấn.
Dùng ← → để chuyển bài