X(Twitter) Zenn GitHub RSS 共有

(SQL)カラムの内容にキーワードが前方一致するレコードを抽出する

作成日時:2024-03-30
更新日時:2024-03-30

概要

カラムの内容にキーワードが前方一致するレコードを抽出するSQLについて考える。

「カラムの内容 キーワード 前方一致」ではない。
「カラムの内容 キーワード 前方一致」である。

自分で言っていてよく分からなくなってきた。

前者をSQLで表すと下記のとおりである。

SELECT * FROM tbl WHERE col LIKE CONCAT(input, '%');

後者をSQLで表すと下記のとおりである。

SELECT * FROM tbl WHERE input LIKE CONCAT(col , '%');

後者において、下記のテーブルの場合、inputが「123ABC」ならば、No.1からNo.3までのレコードが抽出される。

noidsomething
11a
212b
3123c
41234d

表題に関し、どういったクエリを投げるべきか、実際に動かして考えてみる。

環境と前提

環境

前提

CREATE TABLE tbl (
    no INT,
    id VARCHAR,
    something INT,
    PRIMARY KEY(no)
);

CREATE INDEX ON tbl(id);

INSERT INTO tbl (no,id,something) 
 SELECT 
     i, i, i
 FROM
     generate_series(1,100000) as i
;

方法1

単純にカラムに’%‘を付与してLIKE検索。

SELECT * FROM tbl WHERE '123ABC' LIKE CONCAT(id , '%');
Seq Scan on tbl b  (cost=0.00..2041.00 rows=500 width=13)  Filter: ('123ABC'::text ~~ concat(id, '%'))

フルスキャン。

方法2

キーワードを先頭N文字ごとに切り取った列を作成。
その後、JOIN。

WITH keywords(keyword) AS (
VALUES 
-- substringでも可
 ('1'),
 ('12'),
 ('123'),
 ('123A'),
 ('123AB'),
 ('123ABC')
) 
SELECT
    tbl.* 
FROM
    keywords 
    INNER JOIN tbl 
        ON keywords.keyword = tbl.id
Nested Loop  (cost=0.42..50.74 rows=6 width=13)
 +-Values Scan on "*VALUES*"  (cost=0.00..0.08 rows=6 width=32)
 +-Index Scan using tbl_id_idx on tbl b  (cost=0.42..8.44 rows=1 width=13)  Index Cond: ((id)::text = "*VALUES*".column1)

インデックスが使われる。

雑多な感想

全文インデックスを貼る手もあるが、この為だけにインデックスの容量を増大させたり、対象テーブルの登録/更新/削除のコストを高めるというのか。

しかし、こういう使い方をするテーブルはおそらくマスタ系で更新も少ないだろうし、件数も少ないことが予想される。
なので方法1でもコスト的に問題ないと思われる。件数が少ないならばB木インデックスも不要(使用されない)。

状況と場合によっては方法1でもインデックスが使用されるし、方法2でもインデックスが使われない場合もある。実運用での想定データを登録したうえで、実行計画を取ることが大事。

そもそも「先頭N文字がXだったら~」という判断をすることを強制するテーブル設計がおかしい。
1つのカラムに複数の意味を持たせてはいけない。

個人的な結論

レコード数が少ないならば方法1。インデックスも不要。
レコード数が多いならば方法2。対象カラムにB木インデックスを貼る。