咱们常常在数据库中使用 LIKE 操做符来完成对数据的模糊搜索,LIKE 操做符用于在 WHERE 子句中搜索列中的指定模式。html
若是须要查找客户表中全部姓氏是“张”的数据,可使用下面的 SQL 语句:程序员
SELECT * FROM Customer WHERE Name LIKE '张%'
若是须要查找客户表中全部手机尾号是“1234”的数据,可使用下面的 SQL 语句:数据库
SELECT * FROM Customer WHERE Phone LIKE '%123456'
若是须要查找客户表中全部名字中包含“秀”的数据,可使用下面的 SQL 语句:数组
SELECT * FROM Customer WHERE Name LIKE '%秀%'
以上三种分别对应了:左前缀匹配、右后缀匹配和模糊查询,而且对应了不一样的查询优化方式。ide
如今有一张名为 tbl_like 的数据表,表中包含了四大名著中的所有语句,数据条数上千万:函数
若是要查询全部以“孙悟空”开头的句子,可使用下面的 SQL 语句:工具
SELECT * FROM tbl_like WHERE txt LIKE '孙悟空%'
SQL Server 数据库比较强大,耗时八百多毫秒,并不算快:测试
咱们能够在 txt 列上创建索引,用于优化该查询:优化
CREATE INDEX tbl_like_txt_idx ON [tbl_like] ( [txt] )
应用索引后,查询速度大大加快,仅需 5 毫秒:this
由此可知:对于左前缀匹配,咱们能够经过增长索引的方式来加快查询速度。
在右后缀匹配查询中,上述索引对右后缀匹配并不生效。使用如下 SQL 语句查询全部以“孙悟空”结尾的数据:
SELECT * FROM tbl_like WHERE txt LIKE '%孙悟空'
效率十分低下,耗时达到了 2.5秒:
咱们能够采用“以空间换时间”的方式来解决右后缀匹配查询时效率低下的问题。
简单来讲,咱们能够将字符串倒过来,让右后缀匹配变成左前缀匹配。以“防着古海回来再抓孙悟空”为例,将其倒置以后的字符串是“空悟孙抓再来回海古着防”。当须要查找结尾为“孙悟空”的数据时,去查找以“空悟孙”开头的数据便可。
具体作法是:在该表中增长“txt_back”列,将“txt”列的值倒置后,填入“txt_back”列中,最后为 “txt_back”列增长索引。
ALTER TABLE tbl_like ADD txt_back nvarchar(1000);-- 增长数据列 UPDATE tbl_like SET txt_back = reverse(txt); -- 填充 txt_back 的值 CREATE INDEX tbl_like_txt_back_idx ON [tbl_like] ( [txt_back] );-- 为 txt_back 列增长索引
数据表调整以后,咱们的 SQL 语句也须要调整:
SELECT * FROM tbl_like WHERE txt_back LIKE '空悟孙%'
此番操做下来,执行速度就很是迅速了:
由此可知:对于右后缀匹配,咱们能够创建倒序字段将右后缀匹配变成左前缀匹配来加快查询速度。
在查询全部包含“悟空”的语句时,咱们使用如下的 SQL 语句:
SELECT * FROM tbl_like WHERE txt LIKE '%悟空%'
该语句没法利用到索引,因此查询很是慢,须要 2.7 秒:
遗憾的是,咱们并无一个简单的办法能够优化这个查询。但没有简单的办法,并不表明没有办法。解决办法之一就是:分词+倒排索引。
分词就是将连续的字序列按照必定的规范从新组合成词序列的过程。咱们知道,在英文的行文中,单词之间是以空格做为天然分界符的,而中文只是字、句和段能经过明显的分界符来简单划界,惟独词没有一个形式上的分界符,虽然英文也一样存在短语的划分问题,不过在词这一层上,中文比之英文要复杂得多、困可贵多。
倒排索引源于实际应用中须要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具备该属性值的各记录的地址。因为不是由记录来肯定属性值,而是由属性值来肯定记录的位置,于是称为倒排索引(inverted index)。带有倒排索引的文件咱们称为倒排索引文件,简称倒排文件(inverted file)。
以上两段让人摸不着头脑的文字来自百度百科,你能够和我同样选择忽略他。
咱们不须要特别高超的分词技巧,由于汉语的特性,咱们只需“二元”分词便可。
所谓二元分词,即将一段话中的文字每两个字符做为一个词来分词。仍是以“防着古海回来再抓孙悟空”这句话为例,进行二元分词以后,获得的结果是:防着、着古、古海,海回,回来,来再,再抓,抓孙,孙悟,悟空。使用 C# 简单实现一下:
public static List<String> Cut(String str) { var list = new List<String>(); var buffer = new Char[2]; for (int i = 0; i < str.Length - 1; i++) { buffer[0] = str[i]; buffer[1] = str[i + 1]; list.Add(new String(buffer)); } return list; }
测试一下结果:
咱们须要一张数据表,把分词后的词条和原始数据对应起来,为了得到更好的效率,咱们还用到了覆盖索引:
CREATE TABLE tbl_like_word ( [id] int identity, [rid] int NOT NULL, [word] nchar(2) NOT NULL, PRIMARY KEY CLUSTERED ([id]) ); CREATE INDEX tbl_like_word_word_idx ON tbl_like_word(word,rid);-- 覆盖索引(Covering index)
以上 SQL 语句建立了一张名为 ”tbl_like_word“的数据表,并为其 ”word“和“rid”列增长了联合索引。这就是咱们的倒排表,接下来就是为其填充数据。
为了便于演示,笔者使用了 LINQPad 来作数据处理,对该工具感兴趣的朋友,能够参看笔者以前的文章:《.NET 程序员的 Playground :LINQPad》,文章中对 LINQPad 作了一个简要的介绍,连接地址是:https://www.coderbusy.com/archives/432.html 。
咱们须要先用 LINQPad 自带的数据库连接功能连接至数据库,以后就能够在 LINQPad 中与数据库交互了。首先按 Id 顺序每 3000 条一批读取 tbl_like 表中的数据,对 txt 字段的值分词后生成 tbl_like_word 所需的数据,以后将数据批量入库。完整的 LINQPad 代码以下:
void Main() { var maxId = 0; const int limit = 3000; var wordList = new List<Tbl_like_word>(); while (true) { $"开始处理:{maxId} 以后 {limit} 条".Dump("Log"); //分批次读取 var items = Tbl_likes .Where(i => i.Id > maxId) .OrderBy(i => i.Id) .Select(i => new { i.Id, i.Txt }) .Take(limit) .ToList(); if (items.Count == 0) { break; } //逐条生产 foreach (var item in items) { maxId = item.Id; //单个字的数据跳过 if (item.Txt.Length < 2) { continue; } var words = Cut(item.Txt); wordList.AddRange(words.Select(str => new Tbl_like_word { Rid = item.Id, Word = str })); } } "处理完毕,开始入库。".Dump("Log"); this.BulkInsert(wordList); SaveChanges(); "入库完成".Dump("Log"); } // Define other methods, classes and namespaces here public static List<