PHP数据库模糊查询优化:提升LIKE查询效率的实践方法
在PHP开发的业务场景中,模糊查询是非常常见的需求,比如搜索商品名称、用户昵称、文章标题等场景,基本都会用到SQL的LIKE关键字。但如果数据量较大时,不合理的模糊查询写法很容易导致查询性能急剧下降,甚至出现慢查询拖垮数据库的情况。本文将结合实际开发案例,讲解PHP中模糊查询的常见优化思路。
一、模糊查询的性能瓶颈
首先明确为什么LIKE查询容易变慢:默认的LIKE查询如果以通配符%开头,比如LIKE '%关键词%',数据库引擎无法使用索引,只能进行全表扫描,表数据量越大,扫描的成本就越高。而如果是LIKE '关键词%'这种前缀匹配的形式,部分存储引擎(比如InnoDB)是可以利用普通索引来加速查询的。
下面先来看一个未优化的模糊查询示例,假设我们有一个用户表users,存储了百万级的用户数据,需要搜索昵称包含"张三"的用户:
<?php
// 未优化的模糊查询示例
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '123456');
$keyword = '张三';
// 直接拼接关键词,存在SQL注入风险,且查询效率极低
$sql = "SELECT id, nickname, email FROM users WHERE nickname LIKE '%{$keyword}%'";
$stmt = $pdo->query($sql);
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
var_dump($result);
?>上面的代码存在两个问题:一是直接拼接搜索关键词,有SQL注入风险;二是使用了LIKE '%关键词%'的写法,百万级数据下基本会触发全表扫描,查询耗时可能达到数秒甚至更久。
二、基础优化:避免SQL注入 + 调整匹配规则
1. 使用预处理语句防注入
首先必须解决SQL注入问题,PHP中使用PDO或者mysqli的预处理语句可以天然避免拼接带来的注入风险,同时让查询逻辑更清晰。
<?php
// 使用预处理语句的模糊查询
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '123456');
$keyword = '张三';
// 预处理SQL,使用占位符
$sql = "SELECT id, nickname, email FROM users WHERE nickname LIKE :keyword";
$stmt = $pdo->prepare($sql);
// 拼接通配符后绑定参数
$searchKey = "%{$keyword}%";
$stmt->bindParam(':keyword', $searchKey, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
var_dump($result);
?>2. 优先使用前缀匹配
如果业务场景允许,尽量把模糊查询改成前缀匹配,比如搜索以"张"开头的昵称,使用LIKE '张%',此时如果nickname字段有普通索引,查询可以直接走索引,效率会提升几十甚至上百倍。
我们可以在nickname字段上创建普通索引:
-- 给nickname字段创建普通索引 CREATE INDEX idx_nickname ON users(nickname);
然后调整查询逻辑为前缀匹配:
<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '123456');
$keyword = '张';
// 前缀匹配,走索引
$sql = "SELECT id, nickname, email FROM users WHERE nickname LIKE :keyword";
$stmt = $pdo->prepare($sql);
$searchKey = "{$keyword}%";
$stmt->bindParam(':keyword', $searchKey, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
var_dump($result);
?>三、进阶优化:无法避免全模糊查询时的方案
如果业务必须支持中间匹配(比如搜索昵称里包含某个关键词的所有用户),前缀匹配无法满足需求,可以尝试以下几种优化方案。
1. 使用覆盖索引减少回表
如果查询的字段不多,可以创建包含所有查询字段的联合索引,让查询直接走覆盖索引,不需要回表查询数据,能大幅提升效率。比如我们的查询只需要id、nickname、email三个字段,可以创建联合索引:
-- 创建覆盖索引,包含查询所需的所有字段 CREATE INDEX idx_nickname_cover ON users(nickname, id, email);
此时即使使用LIKE '%关键词%',如果查询的字段都在索引中,数据库会优先扫描索引树,减少数据页的读取,效率会有明显提升。
2. 结合全文索引(FULLTEXT)使用
MySQL从5.6版本开始,InnoDB引擎已经支持全文索引,对于大文本字段的模糊查询,全文索引的效率远高于LIKE。首先给nickname字段添加全文索引:
-- 添加全文索引 ALTER TABLE users ADD FULLTEXT INDEX ft_idx_nickname (nickname);
然后在PHP中使用全文索引查询,注意全文索引的匹配语法和LIKE不同,使用MATCH...AGAINST:
<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '123456');
$keyword = '张三';
// 使用全文索引查询
$sql = "SELECT id, nickname, email FROM users WHERE MATCH(nickname) AGAINST (:keyword IN BOOLEAN MODE)";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':keyword', $keyword, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
var_dump($result);
?>需要注意全文索引有最小搜索长度的限制,默认是4个字符,中文的话需要配置ngram解析器才能正常支持,创建全文索引时可以指定解析器:
-- 为中文昵称创建支持ngram的全文索引 ALTER TABLE users ADD FULLTEXT INDEX ft_idx_nickname (nickname) WITH PARSER ngram;
3. 应用层缓存热门查询
如果某些搜索关键词是高频查询(比如热门商品名称、热门搜索词),可以在PHP应用层使用Redis做缓存,将查询结果缓存起来,有效期内直接返回缓存数据,减少数据库查询次数。示例代码如下:
<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '123456');
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$keyword = '张三';
$cacheKey = "user_search_" . md5($keyword);
// 先查缓存
$cacheResult = $redis->get($cacheKey);
if ($cacheResult !== false) {
$result = json_decode($cacheResult, true);
var_dump($result);
exit;
}
// 缓存未命中,查询数据库
$sql = "SELECT id, nickname, email FROM users WHERE nickname LIKE :keyword";
$stmt = $pdo->prepare($sql);
$searchKey = "%{$keyword}%";
$stmt->bindParam(':keyword', $searchKey, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 缓存结果,过期时间设为300秒
$redis->setex($cacheKey, 300, json_encode($result));
var_dump($result);
?>四、不同优化方案的效果对比
下面用测试数据对比不同方案在100万条用户数据下的查询耗时(仅供参考,实际效果和数据分布、服务器配置有关):
| 查询方案 | 平均耗时 | 适用场景 |
|---|---|---|
| LIKE '%关键词%' 无索引 | 2.8秒 | 小数据量场景 |
| LIKE '关键词%' 有普通索引 | 0.02秒 | 前缀匹配场景 |
| LIKE '%关键词%' 有覆盖索引 | 0.5秒 | 必须全模糊查询,字段少 |
| 全文索引 MATCH...AGAINST | 0.1秒 | 文本搜索、中文场景 |
| 缓存+数据库查询 | 缓存命中时0.001秒 | 高频热门搜索 |
五、总结
PHP中模糊查询的优化没有万能方案,需要结合业务场景选择:如果可以接受前缀匹配,优先用普通索引+前缀查询;必须全模糊查询时,优先考虑覆盖索引或者全文索引;高频查询可以叠加缓存层。同时所有查询都要使用预处理语句,避免SQL注入风险,保证业务安全。