DHDataHọc
Lý thuyếtBài 2/306 phút đọc+10 XP

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.
  • NULL nghĩ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:

code
customers ──< orders ──< order_items >── products

Dấu ──< đọc là "một khách có nhiều đơn". Cụ thể:

BảngVai tròKhoá chínhKhoá ngoại
customersKhách hàngid—
productsSản phẩmid—
ordersĐơn hàngidcustomer_id → customers.id
order_itemsDòng hàng trong đơnidorder_id → orders.id, product_id → products.id

Xem ba dòng đầu của products:

code
SELECT *
FROM products
LIMIT 3;
idnamecategorypricestock
1Tai nghe BluetoothĐiện tử89000025
2Bàn phím cơĐiện tử125000010
3Nồi chiên không dầuGia dụng16900005

Đọ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:

code
SELECT id, customer_id, order_date, status
FROM orders
LIMIT 3;
idcustomer_idorder_datestatus
112025-01-05completed
222025-01-18completed
332025-02-02completed

Đơ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:

code
SELECT order_id, product_id, quantity, unit_price
FROM order_items
WHERE order_id = 1;
order_idproduct_idquantityunit_price
112890000
141320000

Đơ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:

code
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

  1. 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?
  2. Chạy SELECT * FROM orders WHERE customer_id = 2; để xem khách Bình Trần đã đặt những đơn nào. Đối chiếu status của từng đơn.
  3. 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ộng quantity × unit_price củ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ứngNguyên nhânCách sửa
column "customers" does not exist khi viết SELECT customersNhầm tên bảng thành tên cộtcustomers 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ánhDù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 integerNhét chữ vào cột số, thường do nhầm cộtKiể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ócid là khoá chính, không hứa liên tụcKhô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ệ.
  • NULL là "không biết", khác chuỗi rỗng và số 0; so sánh với NULL cần IS 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 1 và đọc tên cột trước khi viết truy vấn.

Dùng ← → để chuyển bài