最近、SQLiteで大文字と小文字を区別しない検索を行って、同じ名前のアイテムがプロジェクトの1つであるlistOKにすでに存在するかどうかを確認する必要がありました。最初は簡単な作業のように見えましたが、深く掘り下げると、簡単であることがわかりましたが、まったく単純ではなく、多くの紆余曲折がありました。
組み込みのSQLite機能とその欠点
SQLiteでは、次の3つの方法で大文字と小文字を区別しない検索を取得できます。
-- 1. Use a NOCASE collation
-- (we will look at other ways for applying collations later):
SELECT *
FROM items
WHERE text = "String in AnY case" COLLATE NOCASE;
-- 2. Normalize all strings to the same case,
-- does not matter lower or upper:
SELECT *
FROM items
WHERE LOWER(text) = "string in lower case";
-- 3. Use LIKE operator which is case insensitive by default:
SELECT *
FROM items
WHERE text LIKE "String in AnY case";
SQLAlchemyとそのORMを使用する場合、これらのアプローチは次のようになります。
from sqlalchemy import func
from sqlalchemy.orm.query import Query
from package.models import YourModel
text_to_find = "Text in AnY case"
# NOCASE collation
Query(YourModel)
.filter(
YourModel.field_name.collate("NOCASE") == text_to_find
)
# Normalizing text to the same case
Query(YourModel)
.filter(
func.lower(YourModel.field_name) == text_to_find.lower()
).all()
# LIKE operator. No need to use SQLAlchemy's ilike
# since SQLite LIKE is already case-insensitive.
Query(YourModel)
.filter(YourModel.field_name.like(text_to_find))
これらすべてのアプローチは理想的ではありません。 最初 、特別な考慮なしに、LIKE
を使用して、作業中のフィールドのインデックスを使用しません。 最悪の犯罪者である:ほとんどの場合、インデックスを使用することはできません。大文字と小文字を区別しないクエリでのインデックスの使用の詳細については、以下をご覧ください。
2番目 、さらに重要なことに、大文字と小文字を区別しないという意味についての理解はかなり限られています。
SQLiteは、デフォルトではASCII文字の大文字/小文字のみを理解します。 LIKE演算子は、大文字と小文字を区別します デフォルトでは、ASCII範囲を超えるUnicode文字の場合。たとえば、式'a' LIKE'A'はTRUEですが、'æ'LIKE'Æ'はFALSEです。
英語のアルファベットや数字などのみを含む文字列を使用する場合は問題ありません。完全なUnicodeスペクトルが必要だったため、より適切な解決策が必要でした。
以下に、すべてのUnicodeシンボルについてSQLiteで大文字と小文字を区別しない検索/比較を実現する5つの方法を要約します。これらのソリューションの一部は、他のデータベースに適合させたり、Unicode対応のLIKE
を実装したりすることができます。 、REGEXP
、MATCH
、およびその他の機能。ただし、これらのトピックはこの投稿の範囲外です。
各アプローチの長所と短所、実装の詳細、そして最後に、インデックスとパフォーマンスの考慮事項について見ていきます。
ソリューション
1.ICU拡張
SQLiteの公式ドキュメントには、SQLiteでUnicodeの完全なサポートを追加する方法としてICU拡張機能が記載されています。 ICUは、International ComponentsforUnicodeの略です。
ICUは、大文字と小文字を区別しないLIKE
の両方の問題を解決します 比較/検索に加えて、さまざまな照合のサポートを追加して、適切な測定を行います。 Cで記述されており、SQLiteとより緊密に統合されているため、後のソリューションよりも高速になる可能性があります。
ただし、課題があります:
-
新しいタイプです 依存関係:Pythonライブラリではなく、アプリケーションと一緒に配布する必要がある拡張機能。
-
ICUは、使用する前にコンパイルする必要があります。OSやプラットフォームが異なる可能性があります(テストされていません)。
-
ICU自体はUnicode変換を実装していませんが、下線付きのオペレーティングシステムに依存しています。特にWindowsとmacOSで、OS固有の問題について何度も言及されています。
他のすべてのソリューションは、比較を実行するためにPythonコードに依存するため、文字列を変換および比較するための適切なアプローチを選択することが重要です。
大文字と小文字を区別しない比較のために適切なPython関数を選択する
大文字と小文字を区別しない比較と検索を実行するには、文字列を1つの大文字と小文字に正規化する必要があります。私の最初の本能は、str.lower()
を使用することでした。 このため。ほとんどの状況で機能しますが、適切な方法ではありません。 str.casefold()
を使用することをお勧めします (ドキュメント):
文字列の大文字と小文字を区別したコピーを返します。大文字と小文字を区別しないマッチングには、大文字と小文字を区別する文字列を使用できます。
大文字小文字の区別は小文字に似ていますが、文字列内のすべての大文字小文字の区別を削除することを目的としているため、より積極的です。たとえば、ドイツ語の小文字の「ß」は「ss」と同等です。すでに小文字なので、
lower()
'ß'には何もしません。casefold()
それを「ss」に変換します。
したがって、以下ではstr.casefold()
を使用します。 すべての変換と比較のための関数。
2.アプリケーション定義の照合
すべてのUnicodeシンボルに対して大文字と小文字を区別しない検索を実行するには、データベース(ドキュメント)に接続した後、アプリケーションで新しい照合を定義する必要があります。ここで選択肢があります–組み込みのNOCASE
をオーバーロードします または独自に作成する–以下で長所と短所について説明します。例として、新しい名前を使用します。
import sqlite3
# Custom collation, maybe it is more efficient
# to store strings
def unicode_nocase_collation(a: str, b: str):
if a.casefold() == b.casefold():
return 0
if a.casefold() < b.casefold():
return -1
return 1
connection.create_collation(
"UNICODE_NOCASE", unicode_nocase_collation
)
# Connect to the DB and register the function
connection = sqlite3.connect("your_db_path")
connection.create_collation(
"UNICODE_NOCASE", unicode_nocase_collation
)
# Or, if you use SQLAlchemy you need to register
# the collation via an event
@sa.event.listens_for(sa.engine.Engine, 'connect')
def sqlite_engine_connect(connection, _):
connection.create_collation(
"UNICODE_NOCASE", unicode_nocase_collation
)
照合には、次のソリューションと比較していくつかの利点があります。
-
それらは使いやすいです。テーブルスキーマで照合を指定できます。特に指定しない限り、このフィールドのすべてのクエリとインデックスに自動的に適用されます。
CREATE TABLE test (text VARCHAR COLLATE UNICODE_NOCASE);
完全を期すために、照合を使用するさらに2つの方法を見てみましょう。
-- In a particular query: SELECT * FROM items WHERE text = "Text in AnY case" COLLATE UNICODE_NOCASE; -- In an index: CREATE INDEX IF NOT EXISTS idx1 ON test (text COLLATE UNICODE_NOCASE); -- Word of caution: your query and index -- must match exactly,including collation, -- otherwise, SQLite will perform a full table scan. -- More on indexes below. EXPLAIN QUERY PLAN SELECT * FROM test WHERE text = 'something'; -- Output: SCAN TABLE test EXPLAIN QUERY PLAN SELECT * FROM test WHERE text = 'something' COLLATE NOCASE; -- Output: SEARCH TABLE test USING COVERING INDEX idx1 (text=?)
-
照合は、
ORDER BY
で大文字と小文字を区別しない並べ替えを提供します 箱から出して。テーブルスキーマで照合を定義すると、特に簡単に取得できます。
パフォーマンス面での照合にはいくつかの特殊性があり、これについてはさらに説明します。
3.アプリケーション定義のSQL関数
大文字と小文字を区別しない検索を実現する別の方法は、アプリケーション定義のSQL関数(ドキュメント)を作成することです。
import sqlite3
# Custom function
def casefold(s: str):
return s.casefold()
# Connect to the DB and register the function
connection = sqlite3.connect("your_db_path")
connection.create_function("CASEFOLD", 1, casefold)
# Or, if you use SQLAlchemy you need to register
# the function via an event
@sa.event.listens_for(sa.engine.Engine, 'connect')
def sqlite_engine_connect(connection, _):
connection.create_function("CASEFOLD", 1, casefold)
どちらの場合もcreate_function
最大4つの引数を受け入れます:
- SQLクエリで使用される関数の名前
- 関数が受け入れる引数の数
- 関数自体
- オプションのbool
deterministic
、デフォルトのFalse
(Python 3.8で追加)–インデックスにとって重要です。これについては、以下で説明します。
照合と同様に、組み込み関数をオーバーロードするという選択肢があります(たとえば、LOWER
)または新規作成します。これについては後で詳しく説明します。
4.アプリケーションで比較します
大文字と小文字を区別しない検索のもう1つの方法は、アプリ自体で比較することです。特に、他のフィールドのインデックスを使用して検索を絞り込むことができる場合はそうです。たとえば、listOKでは、特定のリスト内のアイテムに対して大文字と小文字を区別しない比較が必要です。したがって、リスト内のすべてのアイテムを選択し、それらを1つのケースに正規化して、正規化された新しいアイテムと比較することができます。
状況によっては、特に比較するサブセットが小さい場合は、悪い解決策ではありません。ただし、テキストでデータベースインデックスを利用することはできず、範囲を絞り込むために使用する他のパラメータでのみ利用できます。
このアプローチの利点は柔軟性です。アプリケーションでは、同等性をチェックできるだけでなく、たとえば、「ファジー」比較を実装して、起こりうるミスプリント、単数形/複数形などを考慮に入れることができます。これは、listOKに選択したルートです。ボットは「スマート」なアイテムを作成するためにあいまいな比較を必要としていたためです。
さらに、データベースとの結合を排除します。これは、データについて何も知らない単純なストレージです。
5.正規化されたフィールドを個別に保存します
もう1つの解決策があります。データベースに別の列を作成し、検索する正規化されたテキストをそこに保持します。たとえば、テーブルは次の構造を持つ場合があります(関連するフィールドのみ):
id | 名前 | name_normalized |
---|---|---|
1 | 文の大文字化 | 文の大文字化 |
2 | 大文字 | 大文字 |
3 | 非ASCII記号:НайдиМеня | 非ASCII記号:найдименя |
これは最初は過度に見えるかもしれません。正規化されたバージョンを常に更新し、name
のサイズを効果的に2倍にする必要があります。 分野。ただし、ORMを使用する場合、または手動で実行する場合でも簡単に実行でき、ディスク容量とRAMの相対性理論は安価です。
このアプローチの利点:
-
アプリケーションとデータベースを完全に切り離し、簡単に切り替えることができます。
-
クエリで必要な場合は、正規化されたファイルを前処理できます(トリミング、句読点やスペースの削除など)。
組み込み関数と照合をオーバーロードする必要がありますか?
アプリケーション定義のSQL関数と照合を使用する場合、多くの場合、一意の名前を使用するか、組み込み機能をオーバーロードするかを選択できます。どちらのアプローチにも、2つの主要な側面で長所と短所があります。
まず、信頼性/予測可能性 何らかの理由(1回限りの間違い、バグ、または意図的)で、これらの関数または照合を登録しない場合:
-
オーバーロード:データベースは引き続き機能しますが、結果が正しくない可能性があります:
- 組み込みの関数/照合は、カスタムの対応する関数とは異なる動作をします。
- インデックスに照合機能がない場合は機能しているように見えますが、読んでも結果が間違っている可能性があります。
- インデックスとカスタム関数/照合を使用するインデックスを持つテーブルが更新されると、インデックスが破損する可能性があります(組み込みの実装を使用して更新されます)が、何も起こらなかったかのように機能し続けます。
-
オーバーロードしない:データベースは、存在しない関数または照合が使用されるすべての点で機能しません:
- 存在しない関数でインデックスを使用すると、読み取りには使用できますが、更新には使用できません。
- アプリケーション定義の照合を使用するインデックスは、インデックスの検索中に照合を使用するため、まったく機能しません。
次に、アクセシビリティ メインアプリケーションの外部:移行、分析など:
-
オーバーロード:インデックスが破損するリスクを念頭に置いて、問題なくデータベースを変更できます。
-
オーバーロードしない:多くの場合、これらの関数または照合を登録するか、それに依存するデータベースの部分を回避するために追加の手順を実行する必要があります。
オーバーロードすることにした場合、カスタム関数または照合に基づいてインデックスを再構築することをお勧めします。たとえば、次のように、間違ったデータが記録された場合に備えてください。
-- Rebuild all indexes using this collation
REINDEX YOUR_COLLATION_NAME;
-- Rebuild particular index
REINDEX index_name;
-- Rebuild all indexes
REINDEX;
アプリケーション定義の関数と照合のパフォーマンス
カスタム関数または照合は、組み込み関数よりもはるかに低速です。SQLiteは、関数を呼び出すたびにアプリケーションに「戻ります」。関数にグローバルカウンターを追加することで、簡単に確認できます。
counter = 0
def casefold(a: str):
global counter
counter += 1
return a.casefold()
# Work with the database
print(counter)
# Number of times the function has been called
クエリをほとんど実行しない場合、またはデータベースが小さい場合は、意味のある違いは見られません。ただし、この関数/照合でインデックスを使用しない場合、データベースは各行に関数/照合を適用して全表スキャンを実行する場合があります。テーブルのサイズ、ハードウェア、および要求の数によっては、パフォーマンスの低下が驚くべき場合があります。後で、アプリケーション定義の関数と照合のパフォーマンスのレビューを公開します。
厳密に言えば、照合は、比較ごとに1つではなく2つの文字列を大文字と小文字を区別する必要があるため、SQL関数よりも少し遅くなります。この違いは非常に小さいですが、私のテストでは、ケースフォールド関数は同様の照合よりも約25%速く、1億回の反復後に10秒の違いになりました。
インデックスと大文字と小文字を区別しない検索
インデックスと関数
基本から始めましょう。任意のフィールドにインデックスを定義すると、このフィールドに適用される関数のクエリでは使用されません。
CREATE TABLE table_name (id INTEGER, name VARCHAR);
CREATE INDEX idx1 ON table_name (name);
EXPLAIN QUERY PLAN
SELECT id, name FROM table_name WHERE LOWER(name) = 'test';
-- Output: SCAN TABLE table_name
このようなクエリには、関数自体を含む個別のインデックスが必要です。
CREATE INDEX idx1 ON table_name (LOWER(name));
EXPLAIN QUERY PLAN
SELECT id, name
FROM table_name WHERE LOWER(name) = 'test';
-- Output: SEARCH TABLE table_name USING INDEX idx1 (<expr>=?)
SQLiteでは、カスタム関数でも実行できますが、決定論的としてマークする必要があります(つまり、同じ入力で同じ結果が返されます)。
connection.create_function(
"CASEFOLD", 1, casefold, deterministic=True
)
その後、カスタムSQL関数にインデックスを作成できます。
CREATE INDEX idx1
ON table_name (CASEFOLD(name));
EXPLAIN QUERY PLAN
SELECT id, name
FROM table_name WHERE CASEFOLD(name) = 'test';
-- Output: SEARCH TABLE table_name USING INDEX idx1 (<expr>=?)
インデックスと照合
照合とインデックスの状況は似ています。クエリでインデックスを利用するには、同じ照合(暗黙的または明示的に提供)を使用する必要があります。そうしないと、機能しません。
-- Table without specified collation will use BINARY
CREATE TABLE test (id INTEGER, text VARCHAR);
-- Create an index with a different collation
CREATE INDEX IF NOT EXISTS idx1 ON test (text COLLATE NOCASE);
-- Query will use default column collation -- BINARY
-- and the index will not be used
EXPLAIN QUERY PLAN
SELECT * FROM test WHERE text = 'test';
-- Output: SCAN TABLE test
-- Now collations match and index is used
EXPLAIN QUERY PLAN
SELECT * FROM test WHERE text = 'test' COLLATE NOCASE;
-- Output: SEARCH TABLE test USING INDEX idx1 (text=?)
上記のように、テーブルスキーマの列に照合を指定できます。これは最も便利な方法です。特に指定しない限り、それぞれのフィールドのすべてのクエリとインデックスに自動的に適用されます。
-- Using application defined collation UNICODE_NOCASE from above
CREATE TABLE test (text VARCHAR COLLATE UNICODE_NOCASE);
-- Index will be built using the collation
CREATE INDEX idx1 ON test (text);
-- Query will utilize index and collation automatically
EXPLAIN QUERY PLAN
SELECT * FROM test WHERE text = 'something';
-- Output: SEARCH TABLE test USING COVERING INDEX idx1 (text=?)
どのソリューションを選択しますか?
ソリューションを選択するには、比較のためのいくつかの基準が必要です。
-
シンプルさ –実装と保守がどれほど難しいか
-
パフォーマンス –クエリの速度
-
余分なスペース –ソリューションに必要な追加のデータベーススペースの量
-
カップリング –ソリューションがコードとストレージをどの程度絡み合わせているか
ソリューション | シンプルさ | パフォーマンス(相対、インデックスなし) | エクストラスペース | カップリング |
---|---|---|---|---|
ICU拡張機能 | 難しい:新しいタイプの依存関係とコンパイルが必要です | 中から高 | いいえ | はい |
カスタム照合 | シンプル:テーブルスキーマに照合を設定し、フィールド上のすべてのクエリに自動的に適用できます | 低 | いいえ | はい |
カスタムSQL関数 | 中:それに基づいてインデックスを作成するか、関連するすべてのクエリで使用する必要があります | 低 | いいえ | はい |
アプリでの比較 | シンプル | ユースケースによって異なります | いいえ | いいえ |
正規化された文字列の保存 | 中:正規化された文字列を最新の状態に保つ必要があります | 低から中 | x2 | いいえ |
いつものように、ソリューションの選択は、ユースケースとパフォーマンスの要求によって異なります。個人的には、カスタム照合、アプリでの比較、または正規化された文字列の保存のいずれかを使用します。たとえば、listOKでは、最初に照合を使用し、あいまい検索を追加するとアプリでの比較に移行しました。