商品名のA_1を探したのに、AB1まで見つかることがあります。SQLiteのLIKEでは、_が任意の一文字を表す記号になるためです。%も、長さが決まっていない文字の並びを表します。
ユーザーが入力した%や_を、その文字として探すなら、LIKEのESCAPEを使います。SQLへ安全に値を渡すことと、検索記号を文字として扱うことは別の処理です。
検索語の記号を文字として渡す
次は架空の商品名だけを持つメモリーDBです。エスケープ文字に!を選び、検索語内の!、%、_を順に処理します。外側の%は文字どおりの検索語を含む部分一致にするための指定です。
import sqlite3
def literal_pattern(value):
escaped = value.replace("!", "!!").replace("%", "!%").replace("_", "!_")
return "%" + escaped + "%"
db = sqlite3.connect(":memory:")
try:
db.execute("CREATE TABLE products(name TEXT NOT NULL)")
db.executemany("INSERT INTO products VALUES (?)", [
("A_1",), ("AB1",), ("割引10%",), ("割引100",),
("bang!one",), ("bangXone",),
])
expected = {
"A_1": ["A_1"],
"10%": ["割引10%"],
"bang!": ["bang!one"],
}
for query, wanted in expected.items():
rows = db.execute(
"SELECT name FROM products WHERE name LIKE ? ESCAPE '!' ORDER BY name",
(literal_pattern(query),),
).fetchall()
result = [row[0] for row in rows]
print(query, "->", result)
assert result == wanted
finally:
db.close()
A_1ではA_1だけ、10%では割引10%だけが見つかります。!を含む語も文字として検索できます。Windows・Python 3.12.10・SQLite 3.49.1で、この三つの結果を確認しました。
プレースホルダーだけでは記号が残る
?を使って値を渡すことは、SQLの構文と入力値を分けるために必要です。ただし、渡した値がLIKEのパターンとして解釈されることまでは止めません。検索語をそのまま%で囲めば、入力内の%や_もワイルドカードとして働きます。
例ではSQLの文を固定し、処理した検索パターンを別の引数で渡しています。SQL文字列へ検索語を直接つないで、エスケープの代わりにする方法ではありません。
!自身を最初に二重化する順序にも意味があります。%や_へ!を足した後で!を全部二重化すると、後から付けた記号まで変えてしまいます。
完全一致と部分一致を分ける
コードやIDを一つ特定するなら、LIKEで部分一致にせず、=による完全一致が目的に合う場合があります。商品名の一部を探すのか、識別子が同じ一件を探すのかを先に決めます。
大文字小文字や、全角と半角、Unicodeの表記の違いは、この処理ではそろえていません。%と_を文字として扱えたことを、あらゆる文字の照合規則が一致したことと混同しないようにします。
空の検索語を許すと、外側の%だけで広い範囲が見つかります。実際の検索画面では空欄や短すぎる語をどう扱うか、結果の上限やページ分割も決めてください。
SQLの値の渡し方はSQLite入門、文字列の置換操作はPythonの文字列操作で確認できます。
参考資料
SQLiteの式とLIKEとPython sqlite3の公式資料を2026年11月1日に確認しました。
