2017-10-22 10 views
5

varchar - MySQLデータベース内で指定された単語の最も類似した開始点を見つける方法?例えばMySQL - 最も類似した開始点を持つ単語を見つける方法

+-------------------+ 
| word_column | 
+-------------------+ 
| StackOferflow  | 
| StackExchange  | 
| MetaStackExchange | 
|  ....  | 
+-------------------+ 

クエリ:call get_with_similar_begin('StackExch_bla_bla_bla');
出力:'StackExchange'

クエリ:call get_with_similar_begin('StackO_bla_bla_bla');
出力:'StackOferflow'


UPDATE:

Select * from words where word_column like 'StackExch_bla_bla_bla'は、'StackExchange'がこのフィルタと一致しないため、正しい結果を得られません。

追加情報:私はword_columnBTREE-indexを持って、私たちは、あなたが望むものを達成するために、クエリ以下のようなCTEを使用することができますたび

+0

'like'と' regexp'は適していませんなぜなら、私は入力単語と同じ方法で始まる単語を取得したいからです。 – Palindromer

答えて

2

可能SQL Serverでそれを使用したいと思います:

declare @search nvarchar(255) = 'StackExch_bla_bla_bla'; 

-- A cte that contains `StackExch_bla_bla_bla` sub-strings: {`StackExch_bla_bla_bla`, `StackExch_bla_bla_bl`, ..., `S`} 
with cte(part, lvl) as ( 
    select @search, 1 
    union all 
    select substring(@search, 1, len(@search) - lvl), lvl + 1 
    from cte 
    where lvl < len(@search) 
), t as ( -- Now below cte will find match level of each word_column 
    select t.word_column, min(cte.lvl) matchLvl 
    from yourTable t 
    left join cte 
     on t.word_column like cte.part+'%' 
    group by t.word_column 
) 
select top(1) word_column 
from t 
where matchLvl is not null -- remove non-matched rows 
order by matchLvl; 

SQL Server Fiddle Demo

私はそれのための方法を見つけるために多くの時間を必要とします。 MySQLで

私の最高の試みはこれです:;)

select tt.word_column 
from (
    select t.word_column, min(lvl) matchLvl 
    from yourTable t 
    join (
    select 'StackExch_bla_bla_bla' part, 1 lvl 
    union all select 'StackExch_bla_bla_bl', 2 
    union all select 'StackExch_bla_bla_b', 3 
    union all select 'StackExch_bla_bla_', 4 
    union all select 'StackExch_bla_bla', 5 
    union all select 'StackExch_bla_bl', 6 
    union all select 'StackExch_bla_b', 7 
    union all select 'StackExch_bla_', 8 
    union all select 'StackExch_bla', 9 
    union all select 'StackExch_bl', 10 
    union all select 'StackExch_b', 11 
    union all select 'StackExch_', 12 
    union all select 'StackExch', 13 
    union all select 'StackExc', 14 
    union all select 'StackEx', 15 
    union all select 'StackE', 16 
    union all select 'Stack', 17 
    union all select 'Stac', 18 
    union all select 'Sta', 19 
    union all select 'St', 20 
    union all select 'S', 21 
) p on t.word_column like concat(p.part, '%') 
    group by t.word_column 
) tt 
order by matchLvl 
limit 1; 

私は、ストアドプロシージャを作成し、pサブ選択あなたが-HTHを望むものを達成することができますに値を格納するために一時テーブルを使用してだと思います。

MySQL Fiddle Demo

+0

それは働いている解決策ですが、それは良くありません。私の列はbtree-indexを持っているので、データベースでは索引を1回だけ使用できます(多数の単語始まりを作成することなく)。 – Palindromer

+0

これはあまり良くないと知っています。アルゴリズムをより明確にするために追加します。 –

-1

あなたは、これは、@ shA.tの答えに若干のバリエーションです

select * from words where 'your_input_value' like concat(word_column,'%') 
2

入力値に演算子 'のような' 割り当てることができます。集約は必要ありません。

select t.*, p.lvl 
from yourTable t join 
    (select 'StackExch_bla_bla_bla' as part, 1 as lvl union all 
     select 'StackExch_bla_bla_bl', 2 union all 
     select 'StackExch_bla_bla_b', 3 union all 
     select 'StackExch_bla_bla_', 4 union all 
     select 'StackExch_bla_bla', 5 union all 
     select 'StackExch_bla_bl', 6 union all 
     select 'StackExch_bla_b', 7 union all 
     select 'StackExch_bla_', 8 union all 
     select 'StackExch_bla', 9 union all 
     select 'StackExch_bl', 10 union all 
     select 'StackExch_b', 11 union all 
     select 'StackExch_', 12 union all 
     select 'StackExch', 13 union all 
     select 'StackExc', 14 union all 
     select 'StackEx', 15 union all 
     select 'StackE', 16 union all 
     select 'Stack', 17 union all 
     select 'Stac', 18 union all 
     select 'Sta', 19 union all 
     select 'St', 20 union all 
     select 'S', 21 
    ) p 
    on t.word_column like concat(p.part, '%') 
order by matchLvl 
limit 1; 

より高速な方法は、caseを使用することです:

select t.*, 
     (case when t.word_column like concat('StackExch_bla_bla_bla', '%') then 'StackExch_bla_bla_bla' 
      when t.word_column like concat('StackExch_bla_bla_bl', '%') then 'StackExch_bla_bla_bl' 
      when t.word_column like concat('StackExch_bla_bla_b', '%') then 'StackExch_bla_bla_b' 
      . . . 
      when t.word_column like concat('S', '%') then 'S' 
      else '' 
     end) as longest_match 
from t 
order by length(longest_match) desc 
limit 1; 

これらのどちらがインデックスの有効活用を行います。あなたが最初のマッチを打ったときに停止

select t.* 
from t 
where t.word_column like 'StackExch_bla_bla_bla%' 
limit 1; 

あなたは、インデックスを使用するバージョンをしたい場合は、としてクエリをアプリケーション層でのループを行い、繰り返し実行されます。 MySQLはlike比較のためのインデックスを使用する必要があります。

あなたはunion allを使用して、これにかなり近づくことができます。

(select t.*, 'StackExch_bla_bla_bla' as matching 
from t 
where t.word_column like 'StackExch_bla_bla_bla%' 
limit 1 
) union all 
(select t.*, 'StackExch_bla_bla_bl' 
from t 
where t.word_column like 'StackExch_bla_bla_bl%' 
limit 1 
) union all 
(select t.*, 'StackExch_bla_bla_b' 
from t 
where t.word_column like 'StackExch_bla_bla_b%' 
limit 1 
) union al 
. . . 
(select t.*, 'S' 
from t 
where t.word_column like 'S%' 
limit 1 
) 
order by length(matching) desc 
limit 1; 
+1

興味深いことに、CASEクエリは、テーブル/インデックスの完全スキャンと魅惑的なソートが必要な場合でも、結合を使用する単一クエリよりも少なくとも10倍高速です。しかし、 - UNIONソリューションは瞬時です。 –

2

は、テーブル/挿入データを作成します。

CREATE DATABASE IF NOT EXISTS stackoverflow; 
USE stackoverflow; 

DROP TABLE IF EXISTS word; 
CREATE TABLE IF NOT EXISTS word(
     word_column VARCHAR(255) 
    , KEY(word_column) 
) 
; 

INSERT INTO word 
    (`word_column`) 
VALUES 
    ('StackOverflow'), 
    ('StackExchange'), 
    ('MetaStackExchange') 
; 

この解決策は、多数のリストを生成するかどうかによって異なります。 このクエリでこれを行うことができます。 このクエリは1から1000までの数値を生成します。 このクエリは1000文字までの検索をサポートします。

クエリ

SELECT 
@row := @row + 1 AS ROW 
FROM (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) 
row1 
CROSS JOIN (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) row2 
CROSS JOIN (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) row3 
CROSS JOIN (
    SELECT @row := 0 
) AS init_user_param 

結果

row 
-------- 
     1 
     2 
     3 
     4 
     5 
     6 
     7 
     8 
     9 
     10 
    ... 
    ... 
    990 
    991 
    992 
    993 
    994 
    995 
    996 
    997 
    998 
    999 
    1000 

は、今、私たちは、ユニークワードのリストを見つけることがDISTINCT SUBSTRING('StackExch_bla_bla_bla', 1, [number])と組み合わせて送達テーブルとして最後のクエリを使用します。

クエリ

SELECT 
DISTINCT 
    SUBSTRING('StackExch_bla_bla_bla', 1, rows.row) AS word 
FROM (

    SELECT 
    @row := @row + 1 AS ROW 
    FROM (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) 
    row1 
    CROSS JOIN (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) row2 
    CROSS JOIN (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) row3 
    CROSS JOIN (
    SELECT @row := 0 
) AS init_user_param 
) ROWS 

結果

word     
----------------------- 
S      
St      
Sta      
Stac     
Stack     
StackE     
StackEx     
StackExc    
StackExch    
StackExch_    
StackExch_b    
StackExch_bl   
StackExch_bla   
StackExch_bla_   
StackExch_bla_b   
StackExch_bla_bl  
StackExch_bla_bla  
StackExch_bla_bla_  
StackExch_bla_bla_b  
StackExch_bla_bla_bl 
StackExch_bla_bla_bla 

は今すぐ参加して、リストを生成するためにREPLACE(word_column, word, '')CHAR_LENGTH(REPLACE(word_column, word, ''))を使用することができますしたいです。

クエリ

SELECT 
* 
, REPLACE(word_column, word, '') AS replaced 
, CHAR_LENGTH(REPLACE(word_column, word, '')) chars_afterreplace 
FROM (
SELECT 
    DISTINCT 
    SUBSTRING('StackExch_bla_bla_bla', 1, rows.row_number) AS word 
    FROM (

    SELECT 
    @row := @row + 1 AS row_number 
    FROM (
     SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
    ) 
    row1 
    CROSS JOIN (
     SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
    ) row2 
    CROSS JOIN (
     SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
    ) row3 
    CROSS JOIN (
     SELECT @row := 0 
    ) AS init_user_param 
) ROWS 
) words 
INNER JOIN 
    word 
ON 
word.word_column LIKE CONCAT(words.word, '%') 

結果

word  word_column replaced  chars_afterreplace 
---------- ------------- ------------- -------------------- 
S   StackExchange tackExchange      12 
S   StackOverflow tackOverflow      12 
St   StackExchange ackExchange      11 
St   StackOverflow ackOverflow      11 
Sta   StackExchange ckExchange      10 
Sta   StackOverflow ckOverflow      10 
Stac  StackExchange kExchange       9 
Stac  StackOverflow kOverflow       9 
Stack  StackExchange Exchange       8 
Stack  StackOverflow Overflow       8 
StackE  StackExchange xchange       7 
StackEx  StackExchange change       6 
StackExc StackExchange hange        5 
StackExch StackExchange ange        4 
StackExch_ StackExchange StackExchange     13 

は今、我々は最低chars_afterreplaceと言葉を望んではっきりと見ることができます。 だから我々はORDER BY CHAR_LENGTH(REPLACE(word_column, word, '')) ASC LIMIT 1

クエリ

SELECT 
word.word_column 
FROM (
SELECT 
    DISTINCT 
    SUBSTRING('StackExch_bla_bla_bla', 1, rows.row_number) AS word 
FROM (

    SELECT 
    @row := @row + 1 AS row_number 
    FROM (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) 
    row1 
    CROSS JOIN (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) row2 
    CROSS JOIN (
    SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 
) row3 
    CROSS JOIN (
    SELECT @row := 0 
) AS init_user_param 
) ROWS 

) words 
INNER JOIN word 
ON word.word_column LIKE CONCAT(words.word, '%') 
ORDER BY CHAR_LENGTH(REPLACE(word_column, word, '')) ASC 
LIMIT 1 

結果

word_column  
--------------- 
StackExchange 
0

次の解決策は、1からの長さ(少なくとも)にシーケンス番号を含むテーブルを必要とするをしたいですあなたのword_columnword_columnVARCHAR(190)であると仮定すると、1から190までの番号の表が必要です。シーケンスプラグインでMariaDBを使用する場合は、表seq_1_to_190を使用できます。あなたがそれを持っていない場合、それを作成する多くの方法があります。一つの簡単な方法は、information_schema.columnsテーブルを使用することです:

create table if not exists seq_1_to_190 (seq tinyint unsigned auto_increment primary key) 
    select null as seq from information_schema.columns limit 190; 

ます。また、サブクエリでオンザフライでそれを作成することができますが、それはあなたのクエリが複雑になります。

セッション変数@wordを使用して検索文字列を保存します。

set @word = 'StackExch_bla_bla_bla'; 

しかし、すべての出現を定数検索文字列で置き換えることができます。

今、私たちは

select seq as l, left(@word, seq) as substr 
from seq_1_to_190 s 
where s.seq <= char_length(@word) 

http://rextester.com/BWU18001

と、あなたのwordsテーブルとそれに参加するときLIKE条件のためにそれを使用して、すべての接頭サブストリングを作成するために、シーケンステーブルを使用することができます。

select w.word_column 
from (
    select seq as l, left(@word, seq) as substr 
    from seq_1_to_190 s 
    where s.seq <= char_length(@word) 
) s 
join words w on w.word_column like concat(replace(s.substr, '_', '\_'), '%') 
order by s.l desc 
limit 1 

http://rextester.com/STQP82942

_はプレースホルダで、\_で検索文字列にエスケープする必要があることに注意してください。あなたの文字列に含めることができる場合は%のためにそれを行う必要がありますが、私は私の答えでこの部分をスキップします。

クエリはまた、サブクエリなしで書くことができます。

select w.word_column 
from seq_1_to_190 s 
join words w on w.word_column like concat(replace(left(@word, seq), '_', '\_'), '%') 
where s.seq <= char_length(@word) 
order by s.seq desc 
limit 1 

http://rextester.com/QVZI59071

これらのクエリは、仕事をしてtheorieに、彼らはまた、高速である必要があります。しかし、MySQL(私のケースではMariaDB 10.0.19)は、悪い実行計画を作成し、ORDER BY節のインデックスを使用しません。両方のクエリは、100K行のデータセットで約1.8秒で実行されます。

私は、単一のクエリを使用してパフォーマンスを向上させるために何ができるベストは

select (
    select word_column 
    from words w 
    where w.word_column like concat(replace(left(@word, s.seq), '_', '\_'), '%') 
    limit 1 
) as word_column 
from seq_1_to_190 s 
where s.seq <= char_length(@word) 
having word_column is not null 
order by s.seq desc 
limit 1 

http://rextester.com/APZHA8471

あるこの1つは高速ですが、それでも670ミリ秒のような必要があります。 Gordons CASEクエリは125ミリ秒で実行されますが、フルテーブル/インデックススキャンとファイルセットが必要です。

私はインデックス付きの一時テーブルとORDER BY句のインデックスを使用するためにエンジンを強制的に管理しかし:

drop temporary table if exists tmp; 
create temporary table tmp(
    id tinyint unsigned auto_increment primary key, 
    pattern varchar(190) 
) engine=memory 
    select null as id, left(@word, seq) as pattern 
    from seq_1_to_190 s 
    where s.seq <= char_length(@word) 
    order by s.seq desc; 

select w.word_column 
from tmp force index for order by (primary) 
join words w 
    on w.word_column >= tmp.pattern 
    and w.word_column < concat(tmp.pattern, char(127)) 
order by tmp.id asc 
limit 1 

http://rextester.com/OOE82089

このクエリは、上の「インスタント」(1ミリ秒未満)であります私の100K行のテストテーブル。 FORCE INDEXを削除するか、LIKE条件を使用すると、再び低速になります。

char(127)がASCII文字列で機能するように見えることに注意してください。キャラクタセットに応じて別のキャラクタを探す必要があるかもしれません。

結局のところ、最初の考えはUNION ALLというクエリを使用することでしたが、これはGordon Linoffによって提案されました。また、 "インスタント" である

set @subquery = '(
    select word_column 
    from words 
    where word_column like {pattern} 
    limit 1 
)'; 

set session group_concat_max_len = 1000000; 
set @sql = (
    select group_concat(
     replace(
      @subquery, 
      '{pattern}', 
      replace(quote(concat(left(@word, seq), '%')), '_', '\_') 
     ) 
     order by s.seq desc 
     separator ' union all ' 
    ) 
    from seq_1_to_190 s 
    where s.seq <= char_length(@word) 
); 
set @sql = concat(@sql, ' limit 1'); 

prepare stmt from @sql; 
execute stmt; 

http://rextester.com/OPTJ37873

: - しかし、ここでSQL唯一のソリューションです。

あなたはstrored手続き/関数のような場合は、 - ここで、関数の:それはおそらく最速の方法です

select get_with_similar_begin('StackExch_bla_bla_bla'); 
select get_with_similar_begin('StackO_bla_bla_bla'); 

http://rextester.com/CJTU4629

として

create function get_with_similar_begin(search_str text) returns text 
begin 
    declare l integer; 
    declare res text; 
    declare pattern text; 

    set l = char_length(search_str); 
    while l > 0 and res is null do 
     set pattern = left(search_str, l); 
     set pattern = replace(pattern, '_', '\_'); 
     set pattern = replace(pattern, '%', '\%'); 
     set pattern = concat(pattern, '%'); 
     set res = (select word_column from words where word_column like pattern); 
     set l = l - 1; 
    end while; 
    return res; 
end 

はそれを使用してください。長い文字列の場合、の種類を分けて征服すると、平均ルックアップ数が減少することがあります()。しかし、ちょっと残忍かもしれません。

あなたは大きなテーブルの上にあなたのクエリをテストしたい場合は - 私は(シーケンスプラグインとMariaDBのための)私のテストテーブルを作成するには、次のコードを使用:

drop table if exists words; 
create table words(
    id mediumint auto_increment primary key, 
    word_column varchar(190), 
    index(word_column) 
); 

insert into words(word_column) 
    select concat('Stack', rand(1)) as word_column 
    from seq_1_to_100000; 

insert into words(word_column)values('StackOferflow'),('StackExchange'),('MetaStackExchange'); 
関連する問題