热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

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

我们经常在数据库中使用 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 的数据表,表中包含了四大名著中的全部语句,数据条数上千万:

4370940082d632ec1df0128a4154a702.png

左前缀匹配查询优化

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

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

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

a4d9ffc6e6f0147a999450a28957bcc8.png

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

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

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

96737d80289fc053313a02550f48d4a1.png

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

右后缀匹配查询优化

在右后缀匹配查询中,上述索引对右后缀匹配并不生效。使用以下 SQL 语句查询所有以“孙悟空”结尾的数据:

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

效率十分低下,耗时达到了 2.5秒:

8ddd6443750d9e8559fb0c0253913685.png

我们可以采用“以空间换时间”的方式来解决右后缀匹配查询时效率低下的问题。

简单来说,我们可以将字符串倒过来,让右后缀匹配变成左前缀匹配。以“防着古海回来再抓孙悟空”为例,将其倒置之后的字符串是“空悟孙抓再来回海古着防”。当需要查找结尾为“孙悟空”的数据时,去查找以“空悟孙”开头的数据即可。

具体做法是:在该表中增加“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 '空悟孙%'

此番操作下来,执行速度就非常迅速了:

1cd5e8509748dfae7aa32c8633fd2359.png

由此可知:对于右后缀匹配,我们可以建立倒序字段将右后缀匹配变成左前缀匹配来加快查询速度。

模糊查询优化

在查询所有包含“悟空”的语句时,我们使用以下的 SQL 语句:

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

该语句无法利用到索引,所以查询非常慢,需要 2.7 秒:

ec34d5e3d90721fe83a3ebe47acbdc5b.png

遗憾的是,我们并没有一个简单的办法可以优化这个查询。但没有简单的办法,并不代表没有办法。解决办法之一就是:分词+倒排索引。

分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。我们知道,在英文的行文中,单词之间是以空格作为自然分界符的,而中文只是字、句和段能通过明显的分界符来简单划界,唯独词没有一个形式上的分界符,虽然英文也同样存在短语的划分问题,不过在词这一层上,中文比之英文要复杂得多、困难得多。

倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(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

测试一下结果:

8627c1a48e16f467bd816fbfa397d35a.png

我们需要一张数据表,把分词后的词条和原始数据对应起来,为了获得更好的效率,我们还用到了覆盖索引:

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 &#61; 0; const int limit &#61; 3000; var wordList &#61; new List(); while (true) { $"开始处理&#xff1a;{maxId} 之后 {limit} 条".Dump("Log"); //分批次读取 var items &#61; Tbl_likes .Where(i &#61;> i.Id > maxId) .OrderBy(i &#61;> i.Id) .Select(i &#61;> new { i.Id, i.Txt }) .Take(limit) .ToList(); if (items.Count &#61;&#61; 0) { break; } //逐条生产 foreach (var item in items) { maxId &#61; item.Id; //单个字的数据跳过 if (item.Txt.Length <2) { continue; } var words &#61; Cut(item.Txt); wordList.AddRange(words.Select(str &#61;> new Tbl_like_word { Rid &#61; item.Id, Word &#61; str })); } } "处理完毕&#xff0c;开始入库。".Dump("Log"); this.BulkInsert(wordList); SaveChanges(); "入库完成".Dump("Log");}// Define other methods, classes and namespaces herepublic static List Cut(String str){ var list &#61; new List(); var buffer &#61; new Char[2]; for (int i &#61; 0; i

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

之后&#xff0c;就可以把查询安排上&#xff0c;先查询倒排索引&#xff0c;然后关联到主表&#xff1a;

SELECT TOP 10 * FROM tbl_like WHERE id IN (SELECT rid FROM tbl_like_word WHERE word IN (&#39;悟空&#39;))

查询速度很快&#xff0c;仅需十几毫秒&#xff1a;

3d1558415d4559bf870cc5263e456227.png

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

SELECT TOP 10*FROM tbl_like WHERE id IN (SELECT rid FROM tbl_like_word WHERE word IN (&#39;东土&#39;,&#39;土大&#39;,&#39;大唐&#39;))

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

650a86244075a5beb597e2fc9808aa6a.png

我们可以采取一些技巧来解决这个问题&#xff0c;比如先 GROUP 一下&#xff1a;

SELECT TOP 10 *FROM tbl_likeWHERE id IN ( SELECT rid FROM tbl_like_word WHERE word IN ( &#39;东土&#39;, &#39;土大&#39;, &#39;大唐&#39; ) GROUP BY rid HAVING COUNT ( DISTINCT ( word ) ) &#61; 3 )

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

d1fd5f5ba065d0faaf270da708c971bd.png

由此可知&#xff1a;对于模糊查询&#xff0c;我们可以通过分词&#43;倒排索引的方式优化查询速度。

后记

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

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

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



推荐阅读
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • 在Kubernetes上部署JupyterHub的步骤和实验依赖
    本文介绍了在Kubernetes上部署JupyterHub的步骤和实验所需的依赖,包括安装Docker和K8s,使用kubeadm进行安装,以及更新下载的镜像等。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 电话号码的字母组合解题思路和代码示例
    本文介绍了力扣题目《电话号码的字母组合》的解题思路和代码示例。通过使用哈希表和递归求解的方法,可以将给定的电话号码转换为对应的字母组合。详细的解题思路和代码示例可以帮助读者更好地理解和实现该题目。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
author-avatar
天晴的故事_665
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有