Truy vấn dữ liệu

← Bắt đầu với PostgreSQL | → Lọc dữ liệu - WHERE, LIMIT, IN, BETWEEN, LIKE, IS NULL

Bạn sẽ học được

Lấy dữ liệu từ một bảng bằng SELECT, đặt tên tạm cho cột bằng bí danh, sắp xếp kết quả bằng ORDER BY, và loại bỏ dòng trùng bằng DISTINCT.


1. SELECT

SELECT là câu lệnh dùng nhiều nhất trong SQL. Dạng đơn giản nhất:

SELECT cot1, cot2, ...
FROM ten_bang;

1.1. Chọn một cột

SELECT first_name
FROM customer;
 first_name
-------------
 Jared
 Mary
 Patricia
 Linda
 Barbara
...
(599 rows)

1.2. Chọn nhiều cột

SELECT first_name, last_name, email
FROM customer;
 first_name | last_name |                  email
------------+-----------+------------------------------------------
 Jared      | Ely       | jared.ely@sakilacustomer.org
 Mary       | Smith     | mary.smith@sakilacustomer.org
 Patricia   | Johnson   | patricia.johnson@sakilacustomer.org
...
(599 rows)

1.3. Chọn tất cả các cột với *

SELECT * FROM customer;

Đừng dùng SELECT * trong code ứng dụng

SELECT * tiện khi khám phá dữ liệu bằng tay, nhưng có hại trong code chạy thật:

Hãy liệt kê rõ tên cột bạn cần.

1.4. Biểu thức trong SELECT

SELECT không chỉ lấy cột — nó tính toán được:

SELECT
   first_name || ' ' || last_name AS ho_ten,
   length(email) AS do_dai_email
FROM customer
LIMIT 3;
   ho_ten    | do_dai_email
-------------+--------------
 Jared Ely   |           28
 Mary Smith  |           29
 Patricia... |           35

Toán tử || nối chuỗi trong PostgreSQL (không phải + như SQL Server, không phải CONCAT() bắt buộc như MySQL).

|| và NULL

'abc' || NULL cho kết quả NULL, không phải 'abc'. Nếu last_name có thể NULL, cả họ tên biến mất. Dùng concat_ws() để an toàn:

SELECT concat_ws(' ', first_name, last_name) AS ho_ten FROM customer;

concat_ws bỏ qua giá trị NULL. → Biểu thức điều kiện - CASE, COALESCE, NULLIF, CAST

1.5. SELECT không cần FROM

PostgreSQL cho phép dùng SELECT như một máy tính:

SELECT 2 + 3;             -- 5
SELECT now();             -- thời điểm hiện tại
SELECT version();         -- phiên bản PostgreSQL
SELECT upper('postgres'); -- POSTGRES

Rất hữu ích khi muốn thử nhanh một hàm.


2. Bí danh cột (column alias)

Bí danh đặt tên tạm cho cột hoặc biểu thức chỉ trong phạm vi câu truy vấn đó.

SELECT bieu_thuc AS ten_bi_danh
FROM ten_bang;

Từ khóa AS có thể lược bỏ:

SELECT first_name ten, last_name ho
FROM customer;

2.1. Bí danh có khoảng trắng hoặc chữ hoa

Phải bọc trong dấu nháy kép ":

SELECT
   first_name || ' ' || last_name AS "Họ và tên",
   email AS "Địa chỉ Email"
FROM customer
LIMIT 3;
      Họ và tên      |             Địa chỉ Email
---------------------+---------------------------------------
 Jared Ely           | jared.ely@sakilacustomer.org
 Mary Smith          | mary.smith@sakilacustomer.org
 Patricia Johnson    | patricia.johnson@sakilacustomer.org

Nháy đơn vs nháy kép — lỗi kinh điển

Trong PostgreSQL hai loại nháy có ý nghĩa hoàn toàn khác nhau:

Ký hiệuÝ nghĩaVí dụ
'...'Chuỗi ký tự (giá trị dữ liệu)WHERE first_name = 'Mary'
"..."Định danh (tên bảng/cột/bí danh)AS "Họ và tên"

Viết SELECT "Mary" sẽ báo lỗi column "Mary" does not exist vì Postgres hiểu đó là tên cột.
Viết AS 'Ho ten' cũng lỗi cú pháp trong PostgreSQL hiện đại.

2.2. Nơi được và không được dùng bí danh

Đây là hệ quả trực tiếp của thứ tự thực thi (xem Tổng quan về PostgreSQL):

-- ❌ LỖI: WHERE chạy trước SELECT nên chưa biết bí danh
SELECT length AS thoi_luong
FROM film
WHERE thoi_luong > 120;
-- ERROR: column "thoi_luong" does not exist
 
-- ✅ ĐÚNG: lặp lại biểu thức
SELECT length AS thoi_luong
FROM film
WHERE length > 120;
 
-- ✅ ĐÚNG: ORDER BY dùng được bí danh
SELECT length AS thoi_luong
FROM film
ORDER BY thoi_luong DESC;
 
-- ✅ ĐÚNG: GROUP BY dùng được bí danh (đặc cách của PostgreSQL)
SELECT rating AS xep_hang, count(*)
FROM film
GROUP BY xep_hang;

Cách tránh lặp biểu thức phức tạp

Nếu biểu thức dài, dùng CTE hoặc subquery thay vì chép lại:

WITH t AS (
  SELECT title, length / 60.0 AS gio FROM film
)
SELECT * FROM t WHERE gio > 2;

→ CTE và truy vấn đệ quy


3. ORDER BY

ORDER BY sắp xếp tập kết quả.

SELECT cot1, cot2
FROM ten_bang
ORDER BY
   cot1 [ASC | DESC] [NULLS FIRST | NULLS LAST],
   cot2 [ASC | DESC];
  • ASC — tăng dần (mặc định, không cần viết)
  • DESC — giảm dần

3.1. Sắp xếp theo một cột

SELECT first_name, last_name
FROM customer
ORDER BY first_name ASC
LIMIT 5;
 first_name | last_name
------------+-----------
 Aaron      | Selby
 Adam       | Gooch
 Adrian     | Clary
 Agnes      | Bishop
 Alan       | Kahn

3.2. Sắp xếp theo nhiều cột

Postgres sắp theo cột đầu trước; chỉ khi bằng nhau mới xét cột sau:

SELECT first_name, last_name
FROM customer
ORDER BY first_name ASC, last_name DESC
LIMIT 5;

3.3. Sắp xếp theo biểu thức

SELECT first_name, last_name, length(first_name) AS do_dai
FROM customer
ORDER BY length(first_name) DESC
LIMIT 5;

3.4. Sắp xếp theo số thứ tự cột

SELECT first_name, last_name
FROM customer
ORDER BY 2, 1;   -- sắp theo last_name rồi first_name

Tránh dùng số thứ tự trong code thật

ORDER BY 2 trở nên sai lặng lẽ khi ai đó chèn thêm cột vào SELECT. Chỉ dùng khi gõ nhanh trong psql.

3.5. NULL được sắp ở đâu?

Trong PostgreSQL, mặc định:

  • ASC → NULLS LAST (NULL nằm cuối)
  • DESC → NULLS FIRST (NULL nằm đầu)

Postgres coi NULL là lớn hơn mọi giá trị khi sắp xếp. Bạn có thể ghi đè:

-- Ví dụ tự chứa để thấy rõ
CREATE TABLE demo_null (num INT);
INSERT INTO demo_null VALUES (1), (2), (3), (NULL);
 
SELECT num FROM demo_null ORDER BY num;
--  1, 2, 3, NULL      (ASC → NULLS LAST)
 
SELECT num FROM demo_null ORDER BY num NULLS FIRST;
--  NULL, 1, 2, 3
 
SELECT num FROM demo_null ORDER BY num DESC;
--  NULL, 3, 2, 1      (DESC → NULLS FIRST)
 
SELECT num FROM demo_null ORDER BY num DESC NULLS LAST;
--  3, 2, 1, NULL

Khác biệt với MySQL

MySQL luôn xếp NULL đầu tiên khi ASC. Postgres làm ngược lại. Nếu bạn di chuyển truy vấn giữa hai hệ, hãy viết NULLS FIRST/NULLS LAST tường minh.

3.6. Cạm bẫy ORDER BY không xác định

-- ⚠️ Kết quả KHÔNG ổn định giữa các lần chạy
SELECT title, rental_rate
FROM film
ORDER BY rental_rate DESC
LIMIT 3;

Có 336 phim cùng rental_rate = 4.99. Postgres không hứa hẹn trả về 3 phim nào trong số đó — thứ tự phụ thuộc kế hoạch thực thi, có thể đổi sau khi VACUUM hoặc khi thêm index.

Cách sửa: luôn thêm một cột phân biệt duy nhất làm tiêu chí cuối:

SELECT title, rental_rate
FROM film
ORDER BY rental_rate DESC, film_id
LIMIT 3;

Quy tắc này đặc biệt quan trọng khi phân trang. → Lọc dữ liệu - WHERE, LIMIT, IN, BETWEEN, LIKE, IS NULL

3.7. Sắp xếp tiếng Việt

Sắp xếp phụ thuộc collation của database:

-- Xem collation hiện tại
SHOW lc_collate;
 
-- Sắp theo quy tắc tiếng Việt cho một truy vấn cụ thể
SELECT ten FROM nhan_vien
ORDER BY ten COLLATE "vi-VN-x-icu";

Collation ICU vi-VN-x-icu xử lý đúng thứ tự Ă, Â, Đ, Ê, Ô, Ơ, Ư và dấu thanh — điều mà collation C hay en_US.UTF-8 không làm được.


4. SELECT DISTINCT

DISTINCT loại bỏ các dòng trùng lặp khỏi kết quả.

SELECT DISTINCT cot1, cot2
FROM ten_bang;

DISTINCT áp dụng cho cả dòng, không phải từng cột

SELECT DISTINCT a, b loại bỏ những dòng mà cặp (a, b) trùng nhau, chứ không phải khử trùng riêng từng cột. Đây là hiểu nhầm phổ biến nhất về DISTINCT.

4.1. Ví dụ tự chứa

DROP TABLE IF EXISTS mau_sac;
 
CREATE TABLE mau_sac (
   id SERIAL PRIMARY KEY,
   mau_nen VARCHAR,
   mau_chu VARCHAR
);
 
INSERT INTO mau_sac (mau_nen, mau_chu)
VALUES
   ('red',   'red'),
   ('red',   'red'),
   ('red',   NULL),
   (NULL,    'red'),
   ('red',   'green'),
   ('red',   'blue'),
   ('green', 'red'),
   ('green', 'blue'),
   ('green', 'green'),
   ('blue',  'red'),
   ('blue',  'green'),
   ('blue',  'blue');

Một cột:

SELECT DISTINCT mau_nen
FROM mau_sac
ORDER BY mau_nen;
 mau_nen
---------
 blue
 green
 red
 [NULL]
(4 rows)

DISTINCT coi các NULL là bằng nhau

Đây là ngoại lệ so với toán tử = (trong đó NULL = NULL cho ra NULL, không phải true). DISTINCT, GROUP BY và UNION đều gom tất cả NULL vào một nhóm duy nhất. → Lọc dữ liệu - WHERE, LIMIT, IN, BETWEEN, LIKE, IS NULL

Hai cột:

SELECT DISTINCT mau_nen, mau_chu
FROM mau_sac
ORDER BY mau_nen, mau_chu;

Cặp ('red','red') xuất hiện 2 lần trong bảng nhưng chỉ trả về 1 lần. Các cặp khác đều duy nhất nên giữ nguyên.

4.2. DISTINCT ON — đặc sản của PostgreSQL

DISTINCT ON (bieu_thuc) giữ lại dòng đầu tiên của mỗi nhóm giá trị, theo thứ tự ORDER BY.

SELECT DISTINCT ON (mau_nen) mau_nen, mau_chu
FROM mau_sac
ORDER BY mau_nen, mau_chu;
 mau_nen | mau_chu
---------+---------
 blue    | blue
 green   | blue
 red     | blue
 [NULL]  | red
(4 rows)

Ứng dụng thực tế — lấy lần thuê gần nhất của mỗi khách:

SELECT DISTINCT ON (customer_id)
       customer_id,
       rental_id,
       rental_date
FROM rental
ORDER BY customer_id, rental_date DESC;

Đây là bài toán “greatest-n-per-group”. Ở các hệ CSDL khác bạn phải viết window function hoặc subquery tương quan; PostgreSQL cho phép giải trong 4 dòng.

Quy tắc bắt buộc của DISTINCT ON

Biểu thức trong DISTINCT ON (...) phải là các cột đầu tiên của ORDER BY. Nếu không, PostgreSQL báo lỗi:

ERROR: SELECT DISTINCT ON expressions must match initial ORDER BY expressions

Trong ví dụ trên, ORDER BY customer_id, rental_date DESC — customer_id đứng đầu, đúng yêu cầu.

4.3. DISTINCT vs GROUP BY

Hai câu sau cho kết quả giống hệt nhau:

SELECT DISTINCT rating FROM film;
SELECT rating FROM film GROUP BY rating;

Trình tối ưu hóa của PostgreSQL thường tạo cùng một kế hoạch thực thi. Chọn DISTINCT khi chỉ muốn khử trùng, chọn GROUP BY khi cần hàm tổng hợp đi kèm. → Nhóm dữ liệu - GROUP BY, HAVING, hàm tổng hợp


5. Bài tập

Dùng CSDL dvdrental:

  1. Liệt kê title và length của 10 phim ngắn nhất, kết quả phải ổn định giữa các lần chạy.
  2. Hiển thị họ tên đầy đủ của khách hàng dưới dạng một cột tên "Khách hàng", viết hoa toàn bộ.
  3. Có bao nhiêu mức rating khác nhau trong bảng film? Liệt kê chúng.
  4. Với mỗi rating, tìm phim có thời lượng dài nhất — chỉ dùng DISTINCT ON.
  5. Sắp xếp bảng film theo rental_rate giảm dần nhưng đưa các giá trị NULL (nếu có) lên đầu.
  6. Giải thích vì sao câu sau bị lỗi và sửa lại:
    SELECT rental_rate * 2 AS gia_gap_doi
    FROM film
    WHERE gia_gap_doi > 5;

6. Tóm tắt

  • SELECT lấy cột và tính biểu thức; nối chuỗi bằng || (cẩn thận với NULL → dùng concat_ws).
  • Bí danh dùng AS; bọc "..." nếu có khoảng trắng/chữ hoa. Nháy đơn = chuỗi, nháy kép = định danh.
  • Bí danh không dùng được trong WHERE, dùng được trong ORDER BY và GROUP BY.
  • ORDER BY mặc định ASC; NULL xếp cuối khi ASC, xếp đầu khi DESC.
  • Luôn thêm cột duy nhất vào cuối ORDER BY để kết quả ổn định.
  • DISTINCT khử trùng theo toàn bộ dòng; DISTINCT ON giữ dòng đầu mỗi nhóm — công cụ mạnh riêng của PostgreSQL.

← Bắt đầu với PostgreSQL | → Lọc dữ liệu - WHERE, LIMIT, IN, BETWEEN, LIKE, IS NULL