Người dùng nhập tên rate_100%, nhưng kết quả còn có rateX100Y. Query đã dùng parameter binding, vậy vì sao vẫn khớp nhiều hơn mong muốn? Dữ liệu đang được nhận như pattern của LIKE, trong đó phần trăm và gạch dưới có ý nghĩa riêng.
Bài đối chiếu PostgreSQL 17, SQLite và Python sqlite3 ngày 04/11/2026. Fixture chạy bằng Python 3.9.6 với SQLite 3.54.0 trong database bộ nhớ. Chưa chạy PostgreSQL server, chưa kiểm tra collation tiếng Việt hoặc hiệu năng tìm kiếm.
Phân biệt cú pháp query và cú pháp pattern
Trong LIKE, % khớp một chuỗi bất kỳ, còn _ khớp một ký tự. Parameter binding đưa giá trị vào query theo hợp đồng của driver; nó không tự biến các wildcard bên trong giá trị thành ký tự literal.
Ví dụ này chọn dấu ! làm escape character và ghi rõ ESCAPE '!' trong query. Khi người dùng nhập chính dấu chấm than, cần escape nó trước, rồi mới escape % và _. Nếu làm thứ tự ngược, các dấu escape vừa thêm có thể bị xử lý lần nữa.
Một fixture có hai tên dễ khớp nhầm
Lưu thành like_demo.py, chạy python3 like_demo.py:
import sqlite3
def contains_pattern(text):
if not text:
raise ValueError("Can tu khoa khong rong")
literal = text.replace("!", "!!").replace("%", "!%").replace("_", "!_")
return "%" + literal + "%"
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE demo_names (id INTEGER PRIMARY KEY, name TEXT)")
db.executemany("INSERT INTO demo_names VALUES (?, ?)", [
(1, "rate_100%"),
(2, "rateX100Y"),
(3, "wow!x"),
(4, "wowx"),
])
term = "rate_100%"
naive = db.execute(
"SELECT id FROM demo_names WHERE name LIKE ? ORDER BY id",
("%" + term + "%",),
).fetchall()
literal = db.execute(
"SELECT id FROM demo_names WHERE name LIKE ? ESCAPE '!' ORDER BY id",
(contains_pattern(term),),
).fetchall()
assert naive == [(1,), (2,)]
assert literal == [(1,)]
print(naive, literal)
Hai kết quả đã được kiểm tra. Query đầu không vi phạm cách bind của sqlite3, nhưng diễn đạt tìm theo wildcard. Query sau diễn đạt tìm chuỗi chứa đúng các ký tự mà term cung cấp, trong phạm vi semantics LIKE của database đang dùng.
Hai dấu % bọc ngoài do ứng dụng chủ ý thêm để tìm chuỗi con. Phần nội dung người dùng nhập bên trong đã được escape. Không ghép term trực tiếp thành một đoạn SQL; placeholder vẫn giữ vai trò riêng của nó.
Kiểm tra cả ký tự escape
Thêm vào cùng file trước khi đóng connection:
found = db.execute(
"SELECT id FROM demo_names WHERE name LIKE ? ESCAPE '!' ORDER BY id",
(contains_pattern("wow!x"),),
).fetchall()
assert found == [(3,)]
assert contains_pattern("!%_") == "%!!!%!_%"
db.close()
Các assertion đã chạy qua. Một hàm chỉ thay % và _ mà bỏ quên chính escape character sẽ không bảo toàn ý nghĩa mọi đầu vào. Giữ case này trong kiểm tra của helper để tránh sửa một ký tự rồi làm hỏng ký tự khác.
Demo từ chối chuỗi rỗng để tránh vô tình biến thao tác ấy thành tìm tất cả. Đây là chính sách của ví dụ; sản phẩm có thể chọn cách khác, nhưng nên quyết định rõ ở lớp nhận request.
Giới hạn khi chuyển sang database thật
Placeholder ? thuộc cách gọi sqlite3 ở đây. Driver PostgreSQL có cú pháp binding riêng, nên chuyển cả query lẫn cách truyền arguments theo driver. Cơ chế phân biệt hoa thường, dấu và locale cũng cần kiểm tra trên database thật; fixture ASCII không chứng minh một bộ tìm kiếm tiếng Việt đã hoàn chỉnh.
Pattern bắt đầu bằng wildcard có thể không phù hợp một chiến lược index đang dùng. Bài không đo execution plan và không kết luận LIKE là lựa chọn tốt nhất cho mọi ô tìm kiếm. Nếu cần tìm từ, xếp hạng hoặc truy vấn lớn, hãy đánh giá công cụ tìm kiếm theo yêu cầu đó.
Chốt trước người dùng đang nhập một pattern hay một chuỗi cần tìm nguyên nghĩa. Sau đó giữ parameter binding và escape pattern như hai bước có mục đích rõ ràng.


