模糊搜索優(yōu)化體驗(yàn)

背景:很多業(yè)務(wù)需求都需要模糊搜索功能诈豌,常見(jiàn)的有列表的文本框搜索组题。

????許多新手或是未主動(dòng)刷新自我認(rèn)知的老手在編寫(xiě)模糊搜索功能時(shí)容易攜帶感性思維橱脸,把需求一對(duì)础米,然后就是編碼了,使用測(cè)試環(huán)境隨意創(chuàng)建的一些數(shù)據(jù)來(lái)測(cè)試添诉,發(fā)現(xiàn)功能實(shí)現(xiàn)得很好呀屁桑,真的是這樣嗎?或許你已經(jīng)發(fā)現(xiàn)問(wèn)題了栏赴,前面提到的“隨意創(chuàng)建的一些數(shù)據(jù)”正是問(wèn)題的關(guān)鍵所在蘑斧,測(cè)試環(huán)境的數(shù)據(jù)具有隨意性、量級(jí)小等特點(diǎn)须眷,不容易暴露出問(wèn)題竖瘾,與生產(chǎn)環(huán)境的真實(shí)數(shù)據(jù)存在比較大的差異,如果你們的生產(chǎn)環(huán)境產(chǎn)出的數(shù)據(jù)量一直穩(wěn)定在一個(gè)小的數(shù)量級(jí)花颗,那么在未來(lái)很長(zhǎng)一段時(shí)間都很難暴露出問(wèn)題捕传。
????模糊搜索看起來(lái)是個(gè)比較簡(jiǎn)單的功能,對(duì)用戶來(lái)說(shuō)扩劝,只要關(guān)注使用就夠了庸论,對(duì)程序員來(lái)說(shuō),要把它寫(xiě)好寫(xiě)妙卻絕非易事棒呛。那么把模糊搜索寫(xiě)好到底有什么意義呢葡公?在筆者看來(lái),一是職業(yè)素養(yǎng)条霜,對(duì)技術(shù)的熱愛(ài)和極致的追求驅(qū)使我們不將就催什,其實(shí)這也完全看個(gè)人,因?yàn)椴⒎撬械念I(lǐng)導(dǎo)會(huì)關(guān)注你實(shí)現(xiàn)的細(xì)節(jié)宰睡,善于自己發(fā)現(xiàn)問(wèn)題可以走的更遠(yuǎn)蒲凶;二是性能,性能是一個(gè)十分復(fù)雜的話題拆内,在應(yīng)對(duì)不同的數(shù)據(jù)場(chǎng)景往往表現(xiàn)的性能也有所差異旋圆。大數(shù)據(jù)的環(huán)境下,一個(gè)功能將更加容易暴露出性能問(wèn)題麸恍。本篇文章將全面地解讀如何能更好地實(shí)現(xiàn)模糊搜索功能灵巧。

表搀矫、數(shù)據(jù)準(zhǔn)備

CREATE TABLE `sys_user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
  `username` varchar(64) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '用戶名',
  `realname` varchar(64) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '真實(shí)姓名',
  `password` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '密碼',
  `salt` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '隨機(jī)鹽',
  `email` varchar(45) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '電子郵件',
  `phone` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '電話',
  `lock_state` tinyint(1) unsigned zerofill DEFAULT '0' COMMENT '鎖定狀態(tài)(0-正常,1-鎖定)',
  `del_state` tinyint(1) unsigned zerofill DEFAULT '0' COMMENT '刪除狀態(tài)(0-正常,1-已刪除)',
  `create_by` bigint(20) unsigned NOT NULL COMMENT '創(chuàng)建人',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
  `update_by` bigint(20) unsigned DEFAULT NULL COMMENT '更新人',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時(shí)間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='用戶表';

CREATE TABLE `sys_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
  `user_id` bigint(20) unsigned NOT NULL COMMENT '所屬用戶ID',
  `type` tinyint(1) unsigned zerofill NOT NULL COMMENT '類型(1-登錄日志,2-操作日志)',
  `content` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '內(nèi)容',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='日志表';

INSERT INTO `sys_user` (`username`, `realname`, `password`, `salt`, `email`, `phone`, `lock_state`, `del_state`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('admin', '張三', 'd80cad79777159cddb847759881b8cb9', 'FRFZ3y63', '', '', '0', '0', '1', '2020-01-01 00:00:00', NULL, NULL);
INSERT INTO `sys_user` (`username`, `realname`, `password`, `salt`, `email`, `phone`, `lock_state`, `del_state`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('admin1', '李四', '594727eda22074a7a5d53650a88c0446', 'jDq57dif', '', '', '0', '0', '1', '2020-01-01 00:00:00', NULL, NULL);
INSERT INTO `sys_user` (`username`, `realname`, `password`, `salt`, `email`, `phone`, `lock_state`, `del_state`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('admin2', '王五', '3f10f0f132650cfd03ad52f5b4bed12d', 'v653euxV', '', '', '0', '0', '1', '2020-01-01 00:00:00', NULL, NULL);

INSERT INTO `sys_log` (`user_id`, `type`, `content`) VALUES ('1', '1','用戶【admin1】登錄成功');
INSERT INTO `sys_log` (`user_id`, `type`, `content`) VALUES ('2', '1','用戶【admin2】登錄成功');
INSERT INTO `sys_log` (`user_id`, `type`, `content`) VALUES ('3', '1','用戶【admin3】登錄成功');

業(yè)務(wù)一:?jiǎn)伪韱巫侄尾樵?/h1>
explain select * from sys_user where del_state = 0 and username like '%admin1%' limit 0,10
1.png

type是ALL,全表掃描刻肄,性能低瓤球。
加入索引:

alter table sys_user add index idx_username(`username`);
explain select * from sys_user where del_state = 0 and username like '%admin1%' limit 0,10
2.png

type還是ALL,說(shuō)明“%xxx%”的寫(xiě)法會(huì)使索引失效敏弃。
使用右模糊匹配查詢卦羡,索引不變:

explain select * from sys_user where del_state = 0 and username like 'admin1%' limit 0,10
3.png

type是range,查詢性能明顯提升麦到。key是idx_username绿饵,用到了索引。rows為1瓶颠,只掃描了一行數(shù)據(jù)拟赊。
到這里,查詢用戶名的功能再難優(yōu)化下去粹淋,但是真實(shí)的業(yè)務(wù)需求往往不會(huì)這么簡(jiǎn)單要门。

業(yè)務(wù)二:?jiǎn)伪矶嘧侄尾樵?/h1>

使用or拼接多個(gè)查詢字段:

explain select * from sys_user where del_state = 0 and (username like 'admin1%' or realname like 'admin1%') limit 0,10;
4.png

使用or關(guān)鍵字無(wú)法用到索引。
為每個(gè)查詢字段加索引廓啊,使用union拼接多個(gè)查詢:

alter table sys_user add index idx_realname(`realname`);
explain 
select * from sys_user where del_state = 0 and username like 'admin1%'
union
select * from sys_user where del_state = 0 and realname like 'admin1%'
limit 0,10;
5.png

????每個(gè)獨(dú)立的查詢都用到了索引欢搜,但因?yàn)閡nion關(guān)鍵字,所以會(huì)產(chǎn)生中間表谴轮。這種做法依然存在弊端炒瘟,如果我們查詢十幾個(gè)甚至是幾十個(gè)個(gè)字段呢,使用union編寫(xiě)的SQL語(yǔ)句會(huì)過(guò)于冗長(zhǎng)第步,可讀性低疮装。建立過(guò)多的索引需要占用更多的內(nèi)存,消耗更多的硬盤(pán)空間粘都,索引不是越多越好的廓推。

業(yè)務(wù)三:多表散列字段查詢

多表散列字段查表意味著一個(gè)以上的表都包含查詢字段,這樣一來(lái)我們就不能再使用union了翩隧,會(huì)造成結(jié)果錯(cuò)誤樊展。

select
sl.id,
sl.content,
sl.create_time,
su.username
from sys_log sl left join sys_user su on sl.user_id = su.id  
where sl.content like 'admin1%' or su.username like 'admin1%'
limit 0,10

????了解前兩種業(yè)務(wù)后,我們不需要使用執(zhí)行計(jì)劃也可以知道結(jié)果了堆生,這顯然對(duì)于性能提升來(lái)說(shuō)是不友好的专缠。
????既然無(wú)法只用SQL語(yǔ)言就能寫(xiě)好模糊搜索,我們?yōu)楹尾粐L試在程序里解決呢淑仆?將搜索工作交給代碼去處理涝婉,不要將思維限制在SQL語(yǔ)言中,可能你花費(fèi)了大量時(shí)間去優(yōu)化SQL蔗怠,結(jié)果還是不盡人意墩弯,既浪費(fèi)了時(shí)間又可能耽誤團(tuán)隊(duì)開(kāi)發(fā)進(jìn)度吩跋。處理步驟如下:

第一步、SQL中忽視掉文本查詢

select
sl.id,
sl.content,
sl.create_time,
su.username
from sys_log sl 
  left join sys_user su on sl.user_id = su.id  
6.png

第一個(gè)表做了全表掃描渔工,第二個(gè)表使用到了主鍵索引锌钮。

第二步、程序處理

場(chǎng)景一涨缚、查詢列表

/**
  * 查詢?nèi)罩玖斜?  * 
  * @param searchText 查詢文本
  * @return
  */
@GetMapping("logs")
public JsonResult getLogsList(@RequestParam(value = "searchText", require = false) String searchText) {
    // 查詢?nèi)罩玖斜?    List<LogVO> logVos = logService.listLogs();
  
    // 如果列表為空則返回空列表
    if (CollectionUtil.isEmpty(logVos)) {
        return JsonResult.success(CollectionUtil.newArrayList());
    }
  
    // 文本搜索
    if (StrUtil.isNotBlank(searchText)) {
        logVos = logVos.stream()
            .filter(i -> i.getUsername().contains(searchText) || i.getRealname().contains(searchText))
            .collect(Collectors.toList());
    }

    return JsonResult.success(logVos);
}

場(chǎng)景二、分頁(yè)查詢

/**
  * 分頁(yè)查詢?nèi)罩?  *
  * @param page       分頁(yè)參數(shù)集
  * @param searchText 查詢文本
  * @return
  */
@GetMapping("logs")
public PageResult<LogVO> getLogPage(Page page, @RequestParam(value = "searchText", require = false) String searchText) {
    // 分頁(yè)查詢?nèi)罩?    Ipage iPage = logService.getLogPage(page);
    long total = iPage.getTotal();
    List<LogVO> records = iPage.getRecords();

    // 文本搜索
    if (StrUtil.isNotBlank(searchText)) {
        // 查詢所有的日志
        List<LogVO> completeLogVos = logService.listLogs();

        records = completeLogVos.stream()
                .filter(i -> i.getUsername().contains(searchText) || i.getRealname().contains(searchText))
                .collect(Collectors.toList());
        total = records.size();

        if (CollectionUtil.isNotEmpty(records)) {
            // 通過(guò)分頁(yè)參數(shù)截取列表
            records = MyPageUtil.subListByPage(page.getCurrent(), page.getSize(), records);
        }
    }

    return new PageResult<>(total, records);
}

MyPageUtil(自定義分頁(yè)工具類)

public class MyPageUtil {
    /**
     * 通過(guò)分頁(yè)參數(shù)集截取數(shù)據(jù)列表
     *
     * @param current  當(dāng)前頁(yè)
     * @param size     每頁(yè)記錄數(shù)
     * @param dataList 數(shù)據(jù)列表
     * @param <T>
     * @return
     */
    public static <T> List<T> subListByPage(long current, long size, List<T> dataList) {
        int total = dataList.size();

        int fromIndex = (int) ((current - 1) * size);
        int initToIndex = (int) (fromIndex + size);
        int toIndex = initToIndex > total ? total : initToIndex;

        return dataList.subList(fromIndex, toIndex);
    }
}

這樣一來(lái)策治,雖然需要在程序中編寫(xiě)額外的邏輯代碼塊脓魏,但是對(duì)于SQL優(yōu)化的幫助其實(shí)是巨大的。

注:本文的實(shí)驗(yàn)基于MySql數(shù)據(jù)庫(kù)通惫,版本是5.6.16茂翔。程序使用Java語(yǔ)言編寫(xiě),基于JDK1.8
附框架清單:

框架 名稱 版本號(hào)
Spring Boot 容器 2.1.6.RELEASE
MyBatis-Plus 增強(qiáng)版ORM框架 3.3.2
Hutool Java工具類庫(kù) 5.3.3
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末履腋,一起剝皮案震驚了整個(gè)濱河市珊燎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌遵湖,老刑警劉巖悔政,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異延旧,居然都是意外死亡谋国,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)迁沫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)芦瘾,“玉大人,你說(shuō)我怎么就攤上這事集畅〗埽” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵挺智,是天一觀的道長(zhǎng)祷愉。 經(jīng)常有香客問(wèn)我,道長(zhǎng)赦颇,這世上最難降的妖魔是什么谣辞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮沐扳,結(jié)果婚禮上泥从,老公的妹妹穿的比我還像新娘。我一直安慰自己沪摄,他們只是感情好躯嫉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布纱烘。 她就那樣靜靜地躺著,像睡著了一般祈餐。 火紅的嫁衣襯著肌膚如雪擂啥。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,604評(píng)論 1 305
  • 那天帆阳,我揣著相機(jī)與錄音哺壶,去河邊找鬼。 笑死蜒谤,一個(gè)胖子當(dāng)著我的面吹牛山宾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鳍徽,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼资锰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了阶祭?” 一聲冷哼從身側(cè)響起绷杜,我...
    開(kāi)封第一講書(shū)人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎濒募,沒(méi)想到半個(gè)月后鞭盟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瑰剃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年懊缺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片培他。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹃两,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出舀凛,到底是詐尸還是另有隱情俊扳,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布猛遍,位于F島的核電站馋记,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏懊烤。R本人自食惡果不足惜梯醒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望腌紧。 院中可真熱鬧茸习,春花似錦、人聲如沸壁肋。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至猫胁,卻和暖如春箱亿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背弃秆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工届惋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人菠赚。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓脑豹,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親锈至。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晨缴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容