Lọc dữ liệu: WHERE, LIMIT, IN, BETWEEN, LIKE, IS NULL
Properties3
description
Chọn đúng những dòng cần thiết bằng WHERE và các toán tử AND, OR, IN, BETWEEN, LIKE, IS NULL; giới hạn kết quả bằng LIMIT/FETCH; và hiểu logic ba trạng thái của NULL — nguồn gốc…
Chọn đúng những dòng cần thiết bằng WHERE và các toán tử AND, OR, IN, BETWEEN, LIKE, IS NULL; giới hạn kết quả bằng LIMIT/FETCH; và hiểu logic ba trạng thái của NULL — nguồn gốc của phần lớn lỗi truy vấn.
1. WHERE
WHERE lọc dòng trước khi nhóm và trước khi SELECT được tính.
SELECT danh_sach_cotFROM ten_bangWHERE dieu_kien;
dieu_kien là một biểu thức boolean — nó cho ra true, false, hoặc NULL. Chỉ những dòng cho true mới được giữ lại.
SELECT title, length, ratingFROM filmWHERE length > 180 AND rating = 'R'ORDER BY length DESC, film_idLIMIT 5;
AND có độ ưu tiên cao hơn OR — luôn dùng ngoặc
-- ⚠️ Không như bạn tưởng!WHERE rating = 'R' OR rating = 'PG' AND length > 120-- Postgres hiểu thành:WHERE rating = 'R' OR (rating = 'PG' AND length > 120)-- → trả về TẤT CẢ phim R, bất kể độ dài-- ✅ Ý bạn muốn:WHERE (rating = 'R' OR rating = 'PG') AND length > 120
Đây là lỗi logic phổ biến nhất trong SQL và nó không báo lỗi cú pháp — chỉ trả về kết quả sai lặng lẽ.
1.4. Bảng chân trị với NULL
PostgreSQL dùng logic ba trạng thái: TRUE, FALSE, NULL (unknown).
AND:
AND
TRUE
FALSE
NULL
TRUE
TRUE
FALSE
NULL
FALSE
FALSE
FALSE
FALSE
NULL
NULL
FALSE
NULL
OR:
OR
TRUE
FALSE
NULL
TRUE
TRUE
TRUE
TRUE
FALSE
TRUE
FALSE
NULL
NULL
TRUE
NULL
NULL
Điểm mấu chốt: FALSE AND NULL = FALSE (biết chắc sai), nhưng TRUE AND NULL = NULL (không biết).
2. LIMIT và OFFSET
SELECT ...FROM ...ORDER BY ...LIMIT so_dong OFFSET so_dong_bo_qua;
SELECT film_id, title, lengthFROM filmORDER BY film_idLIMIT 5;
SELECT film_id, titleFROM filmORDER BY film_idLIMIT 5 OFFSET 3;
LIMIT mà không có ORDER BY là vô nghĩa
Không có ORDER BY, PostgreSQL trả về dòng theo thứ tự vật lý — có thể đổi sau mỗi lần UPDATE hay VACUUM. LIMIT 10 sẽ cho 10 dòng khác nhau giữa các lần chạy.
2.1. OFFSET chậm dần theo trang
OFFSET 100000 buộc PostgreSQL đọc và bỏ đi 100.000 dòng trước. Phân trang sâu sẽ ngày càng chậm.
Giải pháp — keyset pagination (con trỏ):
-- Trang 1SELECT film_id, titleFROM filmORDER BY film_idLIMIT 20;-- giả sử film_id cuối cùng là 20-- Trang 2: dùng giá trị cuối làm mốc, KHÔNG dùng OFFSETSELECT film_id, titleFROM filmWHERE film_id > 20ORDER BY film_idLIMIT 20;
Cách này giữ tốc độ không đổi dù ở trang thứ mấy, vì index đưa thẳng tới vị trí cần.
2.2. FETCH — cú pháp chuẩn SQL
FETCH làm y hệt LIMIT nhưng theo chuẩn SQL:
OFFSET so_dong_bo_qua ROWSFETCH FIRST so_dong ROWS ONLY;
SELECT film_id, titleFROM filmORDER BY titleOFFSET 5 ROWSFETCH FIRST 5 ROWS ONLY;
LIMIT ngắn hơn và phổ biến hơn trong hệ sinh thái PostgreSQL; FETCH có tính di động cao hơn giữa các hệ CSDL.
WITH TIES — chỉ FETCH mới có:
SELECT title, lengthFROM filmORDER BY length DESCFETCH FIRST 3 ROWS WITH TIES;
Nếu có 5 phim cùng độ dài 185 phút, câu này trả về cả 5 thay vì cắt cụt ở 3. LIMIT không làm được điều này.
3. IN
IN kiểm tra giá trị có thuộc một danh sách hay không — là cách viết gọn của nhiều OR.
gia_tri IN (v1, v2, v3)-- tương đươnggia_tri = v1 OR gia_tri = v2 OR gia_tri = v3
SELECT customer_id, rental_id, return_dateFROM rentalWHERE customer_id IN (1, 2)ORDER BY return_date DESCLIMIT 5;
3.1. IN với subquery
SELECT customer_id, first_name, last_nameFROM customerWHERE customer_id IN ( SELECT customer_id FROM rental WHERE CAST(return_date AS DATE) = '2005-05-27');
-- Nếu subquery trả về BẤT KỲ giá trị NULL nào,-- câu này trả về 0 dòng — luôn luôn!SELECT * FROM filmWHERE film_id NOT IN (SELECT film_id FROM inventory);
Vì sao?x NOT IN (1, 2, NULL) được khai triển thành:
x <> 1 AND x <> 2 AND x <> NULL
Mà x <> NULL luôn cho NULL, nên toàn bộ AND không bao giờ thành true.
Cách sửa — dùng NOT EXISTS (an toàn với NULL):
SELECT f.film_id, f.titleFROM film fWHERE NOT EXISTS ( SELECT 1 FROM inventory i WHERE i.film_id = f.film_id);
Hoặc lọc NULL ra khỏi subquery:
WHERE film_id NOT IN ( SELECT film_id FROM inventory WHERE film_id IS NOT NULL)
gia_tri BETWEEN thap AND cao-- tương đươnggia_tri >= thap AND gia_tri <= cao
BETWEENbao gồm cả hai đầu mút.
SELECT customer_id, amount, payment_dateFROM paymentWHERE amount BETWEEN 8 AND 9ORDER BY payment_idLIMIT 5;
Phủ định:
WHERE amount NOT BETWEEN 8 AND 9
4.1. BETWEEN với ngày — cạm bẫy nghiêm trọng
-- ⚠️ SAI nếu payment_date là TIMESTAMPSELECT * FROM paymentWHERE payment_date BETWEEN '2007-02-01' AND '2007-02-28';
'2007-02-28' được hiểu là 2007-02-28 00:00:00. Mọi giao dịch trong ngày 28/02 sau nửa đêm — tức gần như cả ngày — đều bị bỏ sót.
Cách đúng — dùng nửa khoảng mở:
SELECT * FROM paymentWHERE payment_date >= '2007-02-01' AND payment_date < '2007-03-01';
Mẫu >= đầu AND < đầu_kỳ_sau luôn đúng, không phụ thuộc độ chính xác của timestamp (giây, mili giây, micro giây) và không cần lo năm nhuận. → Kiểu dữ liệu chuyên sâu
BETWEEN chỉ an toàn với kiểu rời rạc
Với INTEGER hay DATE (không có phần giờ), BETWEEN hoàn toàn ổn. Chỉ với TIMESTAMP và số thực nó mới nguy hiểm.
5. LIKE — khớp mẫu
chuoi LIKE mauchuoi NOT LIKE mau
Hai ký tự đại diện:
Ký tự
Ý nghĩa
%
Không hoặc nhiều ký tự bất kỳ
_
Đúng một ký tự bất kỳ
5.1. Ví dụ
SELECT first_name, last_nameFROM customerWHERE first_name LIKE 'Jen%';
first_name | last_name------------+----------- Jennifer | Davis Jennie | Terry Jenny | Castro(3 rows)
Các mẫu thường dùng:
first_name LIKE 'Jen%' -- bắt đầu bằng Jenfirst_name LIKE '%er' -- kết thúc bằng erfirst_name LIKE '%en%' -- chứa en ở bất kỳ đâufirst_name LIKE '_her%' -- ký tự bất kỳ + "her" + phần còn lại (Cheryl, Sherry, Theresa)first_name LIKE 'A___' -- đúng 4 ký tự, bắt đầu bằng A
ILIKE là mở rộng riêng của PostgreSQL, không có trong chuẩn SQL.
Các toán tử viết tắt:
Toán tử
Tương đương
~~
LIKE
~~*
ILIKE
!~~
NOT LIKE
!~~*
NOT ILIKE
5.3. Escape ký tự đặc biệt
Muốn tìm ký tự % hay _ theo nghĩa đen:
-- Tìm chuỗi chứa dấu gạch dưới thật sựSELECT * FROM san_phamWHERE ma LIKE '%\_%';-- Hoặc chỉ định ký tự escape riêngSELECT * FROM san_phamWHERE ma LIKE '%!_%' ESCAPE '!';
5.4. Hiệu năng của LIKE
Mẫu
Dùng được index B-tree?
'abc%'
✅ Có (nếu index tạo với text_pattern_ops hoặc collation C)
'%abc'
❌ Không — buộc quét toàn bảng
'%abc%'
❌ Không
ILIKE bất kỳ
❌ Không với B-tree thường
Với tìm kiếm dạng '%abc%' trên bảng lớn, dùng index GIN + pg_trgm:
CREATE EXTENSION IF NOT EXISTS pg_trgm;CREATE INDEX idx_customer_name_trgmON customer USING GIN (first_name gin_trgm_ops);-- Bây giờ câu này dùng được indexSELECT * FROM customer WHERE first_name ILIKE '%enn%';
5.5. Biểu thức chính quy
Khi LIKE không đủ mạnh:
SELECT first_name FROM customerWHERE first_name ~ '^J.*r$'; -- regex, phân biệt hoa thườngSELECT first_name FROM customerWHERE first_name ~* '^j'; -- regex, không phân biệt hoa thường
Toán tử
Ý nghĩa
~
Khớp regex, phân biệt hoa thường
~*
Khớp regex, không phân biệt hoa thường
!~
Không khớp regex
!~*
Không khớp regex, không phân biệt hoa thường
6. IS NULL
NULL nghĩa là “không biết”, không phải “rỗng” hay “bằng 0”.
Vì NULL là “không biết”, so sánh hai thứ không biết cũng cho ra “không biết”. Và WHERE chỉ giữ dòng cho true — nên WHERE cot = NULL không bao giờ trả về dòng nào.
6.2. Cú pháp đúng
gia_tri IS NULLgia_tri IS NOT NULL
-- Tìm các lượt thuê chưa trả đĩaSELECT rental_id, customer_id, rental_dateFROM rentalWHERE return_date IS NULLORDER BY rental_date;
DROP TABLE IF EXISTS lien_he;CREATE TABLE lien_he ( id SERIAL PRIMARY KEY, ho_ten VARCHAR NOT NULL, email VARCHAR NOT NULL, dien_thoai VARCHAR);INSERT INTO lien_he (ho_ten, email, dien_thoai)VALUES ('An', 'an@example.com', '0901234567'), ('Bình','binh@example.com', NULL), ('Cường','cuong@example.com', ''), ('Dung','dung@example.com', '0987654321');
Truy vấn
Kết quả
Giải thích
WHERE dien_thoai IS NULL
Bình
Đúng cách
WHERE dien_thoai = NULL
(không dòng nào)
Sai — luôn cho NULL
WHERE dien_thoai = ''
Cường
Chuỗi rỗng khácNULL
WHERE dien_thoai IS NOT NULL
An, Cường, Dung
Cường có chuỗi rỗng, vẫn tính là NOT NULL
WHERE dien_thoai <> '0901234567'
Cường, Dung
⚠️ Thiếu Bình!
Dòng cuối là bẫy: bạn hỏi “ai không có số 0901234567” nhưng Bình bị loại vì NULL <> '0901...' cho ra NULL.
Cách sửa:
WHERE dien_thoai IS DISTINCT FROM '0901234567';
6.4. IS DISTINCT FROM — so sánh an toàn với NULL
Biểu thức
Kết quả
NULL = NULL
NULL
NULL IS NOT DISTINCT FROM NULL
true ✅
1 <> NULL
NULL
1 IS DISTINCT FROM NULL
true ✅
IS DISTINCT FROM coi NULL như một giá trị bình thường. Cực kỳ hữu ích khi so sánh cột có thể NULL, ví dụ khi phát hiện thay đổi dữ liệu:
UPDATE muc_tieu tSET gia_tri = s.gia_triFROM nguon sWHERE t.id = s.id AND t.gia_tri IS DISTINCT FROM s.gia_tri; -- chỉ update khi thật sự khác
6.5. Xử lý NULL trong kết quả
-- Thay NULL bằng giá trị mặc địnhSELECT ho_ten, COALESCE(dien_thoai, 'Chưa có') AS sdtFROM lien_he;-- Coi chuỗi rỗng như NULLSELECT ho_ten, COALESCE(NULLIF(dien_thoai, ''), 'Chưa có') AS sdtFROM lien_he;
SELECT count(*) AS tong_dong, -- 4 count(dien_thoai) AS co_sdt, -- 3 (bỏ qua NULL của Bình) avg(length(dien_thoai)) AS do_dai_tb -- tính trên 3 giá trị, không phải 4FROM lien_he;
Không dùng BETWEEN '2007-03-01' AND '2007-03-31' — sẽ mất gần trọn ngày 31.
5.
SELECT title, lengthFROM filmORDER BY length DESCFETCH FIRST 10 ROWS WITH TIES;
Trả về nhiều hơn 10 dòng vì có 5 phim cùng 185 phút.
6. Bảng payment không có customer_id nào là NULL, nên câu này thực ra chạy đúng. Nhưng nó mong manh: chỉ cần một NULL xuất hiện là kết quả về 0. Viết an toàn:
SELECT c.* FROM customer cWHERE NOT EXISTS ( SELECT 1 FROM payment p WHERE p.customer_id = c.customer_id AND p.amount > 100);
7.
SELECT f.film_id, f.titleFROM film fWHERE NOT EXISTS ( SELECT 1 FROM inventory i WHERE i.film_id = f.film_id)ORDER BY f.title;