我们经常在数据库中使用 LIKE 操作符来完成对数据的模糊搜索,LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式。

如果需要查找客户表中所有姓氏是“张”的数据,可以使用下面的 SQL 语句:

SELECT * FROM Customer WHERE Name LIKE '张%'

如果需要查找客户表中所有手机尾号是“1234”的数据,可以使用下面的 SQL 语句:

SELECT * FROM Customer WHERE Phone LIKE '%123456'

如果需要查找客户表中所有名字中包含“秀”的数据,可以使用下面的 SQL 语句:

SELECT * FROM Customer WHERE Name LIKE '%秀%'

以上三种分别对应了:左前缀匹配、右后缀匹配和模糊查询,并且对应了不同的查询优化方式。

数据概览

现在有一张名为 tbl_like 的数据表,表中包含了四大名著中的全部语句,数据条数上千万:

左前缀匹配查询优化

如果要查询所有以“孙悟空”开头的句子,可以使用下面的 SQL 语句:

SELECT * FROM tbl_like WHERE txt LIKE '孙悟空%'

SQL Server 数据库比较强大,耗时八百多毫秒,并不算快:

我们可以在 txt 列上建立索引,用于优化该查询:

CREATE INDEX tbl_like_txt_idx ON [tbl_like] ( [txt] )

应用索引后,查询速度大大加快,仅需 5 毫秒:

由此可知:对于左前缀匹配,我们可以通过增加索引的方式来加快查询速度。

右后缀匹配查询优化

在右后缀匹配查询中,上述索引对右后缀匹配并不生效。使用以下 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 Cut(String str){       var list = new List();       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 自带的数据库链接功能链接至数据库,之后就可以在 LINQPad 中与数据库交互了。首先按 Id 顺序每 3000 条一批读取 tbl_like 表中的数据,对 txt 字段的值分词后生成 tbl_like_word 所需的数据,之后将数据批量入库。完整的 LINQPad 代码如下:

void Main(){       var maxId = 0;       const int limit = 3000;       var wordList = new List();       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 herepublic static List Cut(String str){       var list = new List();       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;}

以上 LINQPad 脚本使用 Entity Framework Core 连接到了数据库,并引用了 NuGet 包“EFCore.BulkExtensions”来做数据批量插入。

之后,就可以把查询安排上,先查询倒排索引,然后关联到主表:

SELECT TOP 10 * FROM tbl_like WHERE id IN (SELECT rid FROM tbl_like_word WHERE word IN ('悟空'))

查询速度很快,仅需十几毫秒:

因为我们将所有的语句分成了二字符词组,所以当需要对单个字符模糊查询时,直接使用 LIKE 是一个更加经济的方案。如果需要查询的字符多于两个时,就需要对查询词进行分词。如需查询“东土大唐”一词,构造出的查询语句可能会是这样:

SELECT TOP 10*FROM tbl_like WHERE id IN (SELECT rid FROM tbl_like_word WHERE word IN ('东土','土大','大唐'))

但是,该查询并不符合我们的预期,因为其将只包含“土大”的语句也筛选了出来:

我们可以采取一些技巧来解决这个问题,比如先 GROUP 一下:

SELECT TOP    10 *FROM    tbl_likeWHERE    id IN (    SELECT        rid    FROM        tbl_like_word    WHERE        word IN ( '东土', '土大', '大唐' )    GROUP BY        rid    HAVING    COUNT ( DISTINCT ( word ) ) = 3    )

在上述 SQL 语句中,我们对 rid 进行了分组,并筛选出了不重复的词组数量是三个(即我们的查询词数量)的。于是,我们可以得到正确的结果:

由此可知:对于模糊查询,我们可以通过分词+倒排索引的方式优化查询速度。

后记

虽然在讲述时使用的是 SQL Server 数据库,但是以上优化经验对大部分关系型数据库来说是通用的,比如 MySQL、Oracle 等。

如果你和笔者一样在实际工作中使用 PostgreSQL 数据库,那么在做倒排索引时可以直接使用数组类型并配置 GiN 索引,以获得更好的开发和使用体验。需要注意的是,虽然 PostgreSQL 支持函数索引,但是如果对函数结果进行 LIKE 筛选时,索引并不会命中。

对于 SQLite 这种小型数据库,模糊搜索并不能使用到索引,所以左前缀搜索和右后缀搜索的优化方式对其不生效。不过,一般我们不会使用 SQLite 去存储大量的数据,尽管分词+倒排索引的优化方式也可以在 SQLite 中实现。

android 数据库模糊查询语句_单表千万行数据库:LIKE 搜索优化手记相关推荐

  1. mysql 查询表后三行数据库_单表千万行数据库 LIKE 搜索优化手记

    我们经常在数据库中使用 LIKE 操作符来完成对数据的模糊搜索,LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式. 如果需要查找客户表中所有姓氏是"张"的数据,可以使用 ...

  2. 数据库单表千万行 LIKE 搜索优化手记

    我们经常在数据库中使用 LIKE 操作符来完成对数据的模糊搜索,LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式. 如果需要查找客户表中所有姓氏是"张"的数据,可以使用 ...

  3. oracle 查询天,Oracle查询_ 单表查询

    前面我们详解了关于Oracle的增删改,今天让我们接着来学习Oracle的查询吧, Oracle中查询可是重头戏噢!!!跟着煌sir的步伐,走位,走位~~~ 小知识锦囊 在此前,先讲解一个小知识点 O ...

  4. Mycat高可用架构原理_Mycat集群搭建_HA高可用集群_高可用_单表存储千万级_海量存储_分表扩展---MyCat分布式数据库集群架构工作笔记0027

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 前面我们已经讲了,对于数据库来说,mycat可以,我们通过搭建一主一从,双主双从,来实现数据库集群 ...

  5. MyCat分布式数据库集群架构工作笔记0021---高可用_单表存储千万级_海量存储_水平分表全局表

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 然后咱们说现在咱们已经实现了水平分表,ER表,但是现在还有一个问题 下面这个 字典表,因为字典表放 ...

  6. 【2020-2021春学期】数据库作业5:单表查询例题练习

    文章目录 单表查询 1.选择表中若干列 [例3.19]查询全体学生姓名和出生年份 [例3.20]查询全体学生姓名.出生年份.院系(用小写) 2.选择表中的若干元组 [例3.21]查询选修了课程的学生学 ...

  7. 数据库:MySQL(单表的表记录的操作)(二)

    一.表记录的增删改查 1.增加表记录 <1>插入一条记录:insert [into] tab_name (field1,filed2,.......) values (value1,val ...

  8. server多笔记录拼接字符串 sql_第四章、SQL Server数据库查询大全(单表查询、多表连接查询、嵌套查询、关联子查询、拼sql字符串的查询、交叉查询)...

    4.1.查询的类型 declare @value as int set @value = 50 select  'age:'as age,2008 years,@valueas va --这种查询时跟 ...

  9. mysql的表面sno大全_学生表学号sno数据库

    Microsoft SQL Server 2005习题汇总小结 先建student ,course,sc表: CREATE TABLE Student ( Sno     char(7)   PRIM ...

最新文章

  1. Objective-C:MRC(引用计数器)在OC内部的可变对象是适用的,不可变对象是不适用的(例如 NSString、NSArray等)...
  2. mysql嵌入式语句_MySQL/MariaDB 语句速查笔记
  3. sjms-3 结构型模式
  4. JBoss BRMS 5.3 –添加了业务活动监视(BAM)报告
  5. lua虚拟机字节码修改_LUA虚拟机的字节码怎么看?
  6. VC++2010配置使用MySQL5.6
  7. 卸载mySQL数据库
  8. react 小书学习笔记-state/props
  9. 计算机指纹驱动程序,计算机指纹失灵,提示找不到支持Windows Hello指纹的指纹识别器...
  10. 【教程】最新微信视频号视频批量下载保存方法,非常简单的方法
  11. 任正非《以客户为中心》
  12. java 计算周_java学期周数的计算,求算法
  13. 为什么网页游戏不停开新服务器,网页游戏为什么要不断开新服?
  14. mtk无线网卡 linux,模块编译问题 给MTK芯片的wifi网卡编译linux驱动 系统是mint
  15. BlogsToWordPress v16.9 – 将(新版)百度空间,网易163,新浪sina,QQ空间,人人网,CSDN,搜狐Sohu,博客大巴Blogbus,天涯博客,点点轻博客等博客搬家到Wor
  16. WAP 2.0 VS WEB 2.0
  17. 作为一个UI设计师的3个基本素养,你具备哪些?
  18. Threejs系列--11游戏开发--沙漠赛车游戏【初步加载地面】
  19. python 因子分析 权重计算方法_因子得分如何计算_spss如何计算因子得分
  20. 程序员:妹妹高考650多,她想选择互联网专业,我该怎么劝?

热门文章

  1. loaded the ViewController nib but the view outlet was not set. 处理方式
  2. Shell 编程基础之 Case 练习
  3. CentOS 5 上安装git
  4. Javascript滑动菜单(一)
  5. LoadRunner常用函数(转)
  6. 当强人工智能时代来临,哪些人不会失业?
  7. c++十进制转二进制_二进制与十进制相互转换的原理
  8. 史上最全jdk版本新特性大全
  9. 线程池开门营业招聘开发人员的一天
  10. 业务场景下数据采集机制和策略