在VARCHAR
utf8mb4_unicode_ci
归类中包含包含ngrams的列:
+---------------------------+
| ngram |
+---------------------------+
| stack overflow |
| stack |
| overflow |
| stack overflow protection |
| overflow protection |
| protection |
+---------------------------+
一个查询:
SELECT * FROM ngrams WHERE ngram IN ('stack', 'stack overflow', 'protection', 'overflow')
考虑到此查询返回的行,如何仅保留返回行中具有最长ngrams 的行?
在此示例中,我得到3行:stack
,stack overflow
和protection
。
然后,我需要像这样过滤行:
stack
,因为返回的行中存在stack overflow
stack overflow
,因为没有其他返回的行是包含stack overflow
的ngram(表中有stack overflow protection
,但它不在返回的行中)protection
overflow
,因为返回的行中存在stack overflow
由于排序规则,必须在MySQL中完成(MySQL之外的比较不会产生与MySQL相同的结果)。 (除非我不知道某些MySQL函数允许公开字符串的整理版本。)
我可以想到以下解决方案:(sql fiddle)
SELECT ngram
FROM ngrams n1
WHERE n1.ngram IN ('stack', 'stack overflow', 'protection')
AND NOT EXISTS (
SELECT 1
FROM ngrams n2
WHERE n2.ngram IN ('stack', 'stack overflow', 'protection')
AND LENGTH(n2.ngram) > LENGTH(n1.ngram)
AND CONCAT(' ', n2.ngram, ' ') LIKE CONCAT('% ', n1.ngram, ' %')
)
但效率很低,因为子查询将针对每个匹配的ngram执行。
所以我正在寻找
答案 0 :(得分:15)
如果我理解你的逻辑,这个查询应该给你正确的结果:
SELECT n1.ngram
FROM
ngrams n1 LEFT JOIN ngrams n2
ON
n2.ngram IN ('stack', 'stack overflow', 'protection')
AND n2.ngram LIKE CONCAT('%', n1.ngram, '%')
AND CHAR_LENGTH(n1.ngram) < CHAR_LENGTH(n2.ngram)
WHERE
n1.ngram IN ('stack', 'stack overflow', 'protection')
AND n2.ngram IS NULL;
请参阅小提琴here。但是因为我希望你的表可以有很多记录,而你的单词列表非常有限,为什么不在执行实际查询之前从这个列表中删除最短的ngrams?我的想法是减少列表
('stack', 'stack overflow', 'protection')
到
('stack overflow', 'protection')
这个查询应该可以解决问题:
SELECT *
FROM
ngrams
WHERE
ngram IN (
SELECT s1.ngram
FROM (
SELECT DISTINCT ngram
FROM ngrams
WHERE ngram IN ('stack','stack overflow','protection')
) s1 LEFT JOIN (
SELECT DISTINCT ngram
FROM ngrams
WHERE ngram IN ('stack','stack overflow','protection')
) s2
ON s2.ngram LIKE CONCAT('%', s1.ngram, '%')
AND CHAR_LENGTH(s1.ngram) < CHAR_LENGTH(s2.ngram)
WHERE
s2.ngram IS NULL
);
是的我在再次将结果返回ngrams
之前两次查询表ngrams
,因为我们必须确保表中实际存在最长的值,但是如果你有一个ngram列上的正确索引使用DISTINCT的两个派生查询应该非常有效:
ALTER TABLE ngrams ADD INDEX idx_ngram (ngram);
小提琴是here。
修改强>
正如samuil正确指出的那样,如果你只需要找到最短的ngram而不是与之关联的整行,那么你就不需要外部查询,只需要执行内部查询即可。使用正确的索引,两个SELECT DISTINCT查询将非常高效,即使无法优化JOIN(n2.ngram LIKE CONCAT('%', n1.ngram, '%')
也无法利用索引),它将仅在少数已经过滤的记录上执行,并且应该非常快。
答案 1 :(得分:5)
您正在尝试过滤查询本身中的ngrams。 分两步完成它可能更有效。 从包含所有可能的ngrams的表开始:
CREATE TABLE original (ngram varchar(100) NOT NULL)
GO
CREATE TABLE refined (ngram varchar(100) NOT NULL PRIMARY KEY)
GO
INSERT INTO original (ngram)
SELECT DISTINCT ngram
FROM ngrams
WHERE ngram IN ('stack', 'stack overflow', 'protection')
GO
INSERT INTO refined (ngram)
SELECT ngram
FROM original
然后删除你不想要的那些。 对于每个ngram,生成所有可能的子串。对于每个子字符串,从列表中删除该条目(如果有)。 它需要几个嵌套循环,但除非你的ngram包含非常多的单词,否则它不会花费太多时间。
CREATE PROCEDURE refine()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE words varchar(100);
DECLARE posFrom, posTo int;
DECLARE cur CURSOR FOR SELECT ngram FROM original;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO words;
IF done THEN
LEAVE read_loop;
END IF;
SET posFrom = 1;
REPEAT
SET posTo = LOCATE(' ', words, posFrom);
WHILE posTo > 0 DO
DELETE FROM refined WHERE ngram = SUBSTRING(words, posFrom, posTo - posFrom);
SET posTo = LOCATE(' ', words, posTo + 1);
END WHILE;
IF posFrom > 1 THEN
DELETE FROM refined WHERE ngram = SUBSTRING(words, posFrom);
END IF;
SET posFrom = LOCATE(' ', words, posFrom) + 1;
UNTIL posFrom = 1 END REPEAT;
END LOOP;
CLOSE cur;
END
剩下的是一张只有最长ngrams的表格:
CALL refine;
SELECT ngram FROM refined;
SQL小提琴:http://sqlfiddle.com/#!2/029dc/1/1
编辑:我在表格refined
上添加了一个索引;现在它应该在 O(n)时间运行。
答案 2 :(得分:5)
在没有先查看其他解决方案的情况下执行此操作后,我发现它与您现有的最佳解决方案类似,但读取稍微简单,可能更高效;
SELECT n1.ngram
FROM ngrams n1
LEFT JOIN ngrams n2
ON n2.ngram IN ('stack', 'stack overflow', 'protection', 'overflow')
AND n1.ngram <> n2.ngram
AND INSTR(n2.ngram, n1.ngram) > 0
WHERE n1.ngram IN ('stack', 'stack overflow', 'protection', 'overflow')
AND n2.ngram IS NULL;
由于AND n1.ngram <> n2.ngram
行没有计算,查询应该能够更有效地使用索引。
答案 3 :(得分:4)
我认为你可以在LIKE %original string%
上使用自内连接,只选择那些ngram长度等于最长连接ngram长度的行。
SELECT n1.* FROM ngrams n1
INNER JOIN ngrams n2 ON
n2.ngram LIKE CONCAT('%', `n1`.`ngram`, '%')
AND n2.ngram IN ('stack overflow', 'stack')
WHERE n1.ngram IN ('stack overflow', 'stack')
GROUP BY n1.ngram
HAVING MAX(CHAR_LENGTH(n2.ngram)) = CHAR_LENGTH(n1.ngram);
此解决方案的缺点是您需要提供两次字符串列表。
事实证明,您不需要提供两次列表:
SELECT n1.*
FROM ngrams n1
INNER JOIN ngrams n2 ON
n2.ngram LIKE CONCAT('%', `n1`.`ngram`, '%')
AND n2.ngram IN ('stack overflow', 'stack')
GROUP BY n1.ngram
HAVING MAX(CHAR_LENGTH(n2.ngram)) = CHAR_LENGTH(n1.ngram);
答案 4 :(得分:4)
对您的查询稍作修改:
SELECT ngram
FROM ngrams n1
WHERE n1.ngram IN ('stack', 'stack overflow', 'protection') AND
NOT EXISTS (SELECT 1
FROM ngrams n2
WHERE n2.ngram IN ('stack', 'stack overflow', 'protection') AND
n2.ngram <> n1.ngram AND
n2.ngram LIKE CONCAT('% ', n1.ngram, ' %')
);
ngrams(ngram)
上的索引应该非常快。请注意,这简化了like
条件。我认为没有理由为什么你应该担心单词边界。不会“堆叠”成为“堆栈”的更长版本吗? (虽然n-gram所指的项目可以是单词,但除非另有说明,否则我将它们与字母相关联。)
使用索引,这应该与使用join
的其他解决方案的性能相当。
如果我必须做这个数十亿次并且ngram表不是太大,我会预处理它以获得所有对“概括” - ngram_pairs
。这会将上述内容更改为
SELECT ngram
FROM ngrams n1
WHERE n1.ngram IN ('stack', 'stack overflow', 'protection') AND
NOT EXISTS (SELECT 1
FROM ngram_pairs np
WHERE np.ngram1 = n1.ngram and
np.ngram2 in ('stack', 'stack overflow', 'protection')
)
这应该比like
上的索引要好ngram_pairs(ngram1, ngram2)
。以下是生成ngram_pairs
的代码:
create table ngram_pairs as
select n1.ngram as ngram1, n2.ngram as ngram2
from ngrams n1 join
ngrams n2
on length(n1.ngram) < length(n2.ngram) and
n2.ngram like concat('%', n1.ngram, '%');
create index ngram_pairs_ngram1_ngram2 on ngram_pairs(ngram1, ngram2);
答案 5 :(得分:3)
使用用户变量
尝试此查询select
ngram
from
(select
ngram,
@t:=if(@prev=rank, @t+1, 1) as num,
@prev:=rank
from
(select
ngram,
@rank:=if(@prev like concat(ngram,'%'), @rank, @rank+1) as rank,
CHAR_LENGTH(ngram) as size,
@prev:=ngram
from
tbl
join
(select
@prev:='',
@rank:=1) t
where
ngram in ('stack overflow', 'stack', 'protection')
order by
rank, size desc
)t
join
(select
@t:=0,
@prev:=0) t1
) t
where
num =1
| NGRAM |
|----------------|
| stack overflow |
| protection |
答案 6 :(得分:3)
以下查询仅扫描数据一次,并提供正确的结果(fiddle):
SELECT my_ngrams.ngram
FROM (SELECT CASE WHEN @v LIKE CONCAT('%',n1.ngram,'%') THEN 1 ELSE 0 END AS ngram_match
, @v:=concat(@v,',',n1.ngram) AS ngram_concat
, n1.ngram
FROM ngrams n1, (SELECT @v := '') r
WHERE n1.ngram IN ('stack', 'stack overflow', 'overflow', 'protection', 'overflow protection')
ORDER BY length(n1.ngram) DESC) my_ngrams
WHERE my_ngrams.ngram_match <> 1
;
但是,它依赖于MySQL中用户定义变量的行为(http://dev.mysql.com/doc/refman/5.5/en/user-variables.html),因此应该谨慎使用。
“order by”对于解决方案非常重要,因为它会影响用户定义的变量在逐行的基础上进行评估,从而影响哪些行与案例匹配并稍后进行过滤。
它还将所有结果连接在一起,以便在过滤之前搜索ngram匹配,因此您应该知道最终可能会出现一个比MySQL允许的最大值(http://dev.mysql.com/doc/refman/5.5/en/char.html)更宽的连接字符串。
即使对于大型表,只要列被正确索引,这应该非常有效。
答案 7 :(得分:2)
以下是使用LEFT JOIN的替代方法。
该表是自连接的,条件是不存在包含在另一个ngram中的ngram,并且它不等于自连接表中的ngram。避免了子查询,记住了性能。
修改强>:
添加了过滤条件。
SELECT n1.ngram
FROM ngrams n1
LEFT JOIN
(
SELECT ngram
FROM ngrams
WHERE ngram IN ('stack', 'stack overflow', 'protection')) n2
ON n2.ngram like Concat('%', n1.ngram, '%') and n1.ngram <> n2.ngram
WHERE n2.ngram IS NULL
AND n1.ngram IN ('stack', 'stack overflow', 'protection');
如果您正在检查是否只有ngram的开头包含在另一个ngram中,您可以将JOIN条件替换为
ON n2.ngram like Concat(n1.ngram, '%') and n1.ngram <> n2.ngram
。
我在SQL Fiddle中添加了更多值:
<强>参考强>:
答案 8 :(得分:0)
试试这个:Fiddle
SELECT *
FROM tab
WHERE ngram NOT IN (SELECT DISTINCT b.ngram
FROM tab a,
tab b
WHERE a.ngram != b.ngram
AND a.ngram LIKE Concat('%', b.ngram, '%'));
如果您只想包含表格中存在的列表,请尝试以下查询: -
SELECT b.ngram ab
FROM (SELECT *
FROM tab
WHERE ngram IN ( 'stack', 'stack overflow', 'protection' )) a,
(SELECT *
FROM tab
WHERE ngram IN ( 'stack', 'stack overflow', 'protection' )) b
WHERE a.ngram LIKE Concat('%', b.ngram, '%')
GROUP BY b.ngram
HAVING Count(*) = 1
答案 9 :(得分:0)
SELECT * FROM ngrams a WHERE a.n NOT IN (SELECT DISTINCT a.n
FROM ngrams b
WHERE b.n != a.n
AND b.n LIKE CONCAT('%', a.n, '%'));
答案 10 :(得分:0)
SELECT a.ngram FROM ngram a CROSS JOIN (SELECT ngram AS ngram1 FROM ngram) b
ON b.ngram1 LIKE CONCAT('%', a.ngram, '%')
WHERE length(a.ngram) <= length(b.ngram1)
GROUP BY a.ngram HAVING COUNT(a.ngram) = 1 ORDER BY LENGTH(b.ngram1) DESC
答案 11 :(得分:-3)
尝试
ORDER BY LENGTH(ngram) DESC and use LIMIT 1
编辑:
试试:
SELECT n1.ngram
FROM ngrams n1
INNER JOIN ngrams n2
ON LENGTH(n2.ngram) < LENGTH(n1.ngram)
WHERE n2.ngram IN ('stack', 'stack overflow', 'protection')
GROUP BY n1.ngram