
本教程旨在解决使用 PHP 和 MySQLi 显示标签时常见的 N+1 查询效率问题。通过分析逐个查询标签的低效方法,我们将介绍如何利用 SQL 的 `WHERE IN` 子句,结合预处理语句和动态参数绑定,将多个查询合并为一个高效的数据库操作,显著提升应用程序的性能和响应速度。
在 Web 开发中,尤其是在处理标签系统时,一个常见且容易被忽视的性能瓶颈是所谓的“N+1 查询问题”。当一个数据行包含多个标签的 ID(例如 1,2,3 这样的字符串),并且需要根据这些 ID 从另一个 tags 表中获取标签名称时,如果不加优化,很容易导致为每个标签 ID 执行一次独立的数据库查询。
考虑以下场景:一个文章可能关联了 5 个标签,它们的 ID 以逗号分隔的形式存储在文章记录中。如果您的代码逻辑是先获取文章记录,然后解析出标签 ID 列表,再对列表中的每个 ID 执行一个 SELECT 查询来获取标签名称,那么您将执行 1(获取文章)+ N(获取 N 个标签)次数据库查询。当 N 增大时,这种方法会迅速拖慢应用程序的性能。
以下是一个典型的低效实现示例:
立即学习“PHP免费学习笔记(深入)”;
// 假设 $row["tags"] 的值为 "1,2,3"
$tags = json_decode(json_encode(explode(',', $row["tags"]))); // 示例中这步略显多余,explode已足够
foreach($tags as $tag) {
$fetchTags = $conn->prepare("SELECT id, name FROM tags WHERE id = ? AND type = 1");
$fetchTags->bind_param("i", $tag);
$fetchTags->execute();
$fetchResult = $fetchTags->get_result();
if($fetchResult->num_rows === 0) {
print('No rows');
}
while($resultrow = $fetchResult->fetch_assoc()) {
?><span class="badge bg-primary me-2"><?php echo $resultrow["name"]; ?></span><?php
}
$fetchTags->close();
}上述代码清晰地展示了 N+1 查询问题:对于 $row["tags"] 中包含的每个标签 ID,都会执行一次 prepare、bind_param、execute 和 close 操作。这不仅增加了数据库的负担,也增加了网络往返的开销,严重影响了性能。
解决 N+1 查询问题的关键在于将多个独立的查询合并为一个高效的数据库查询。SQL 的 WHERE IN 子句正是为此而生。它允许您在单个查询中指定一组值,匹配其中任何一个值的记录都将被返回。
为了实现这一点,我们需要:
以下是优化后的实现代码:
<?php
// 假设 $conn 是已建立的 MySQLi 数据库连接
// 假设 $row["tags"] 的值为 "1,2,3"
// 1. 将逗号分隔的标签 ID 字符串转换为数组
$tags = explode(',', $row["tags"]);
// 确保 $tags 数组不为空,避免生成无效查询
if (empty($tags)) {
// 没有标签,直接跳过
return;
}
// 2. 动态生成 WHERE IN 子句的占位符
// 例如,如果 $tags 包含 3 个元素,则生成 "?,?,?"
$placeholders = implode(',', array_fill(0, count($tags), '?'));
// 3. 构建预处理语句
// 注意:ORDER BY id 可以确保结果的顺序一致,这在某些情况下可能有用
$fetchTags = $conn->prepare('SELECT id, name FROM tags WHERE id IN ('.$placeholders.') AND type = 1 ORDER BY id');
// 4. 动态绑定参数
// str_repeat('s', count($tags)) 生成与标签数量相匹配的类型字符串
// 例如,如果 $tags 包含 3 个元素,则生成 "sss"
// ...$tags (splat operator) 将数组元素作为单独的参数传递给 bind_param
$fetchTags->bind_param(str_repeat('s', count($tags)), ...$tags);
// 5. 执行查询
$fetchTags->execute();
// 6. 获取结果
$fetchResult = $fetchTags->get_result();
if($fetchResult->num_rows === 0) {
print('No rows');
} else {
// 遍历结果并显示标签
foreach($fetchResult as $resultRow) {
?><span class="badge bg-primary me-2"><?php echo htmlspecialchars($resultRow["name"]); ?></span><?php
}
}
// 关闭预处理语句
$fetchTags->close();
?>代码解析:
对于 PHP 8.1 及更高版本,execute() 方法得到了增强,可以直接接受一个数组作为参数,而无需显式调用 bind_param()。这进一步简化了代码:
<?php
// 假设 $conn 是已建立的 MySQLi 数据库连接
// 假设 $row["tags"] 的值为 "1,2,3"
$tags = explode(',', $row["tags"]);
if (empty($tags)) {
return;
}
$placeholders = implode(',', array_fill(0, count($tags), '?'));
$fetchTags = $conn->prepare('SELECT id, name FROM tags WHERE id IN ('.$placeholders.') AND type = 1 ORDER BY id');
// PHP 8.1+ 简化绑定
$fetchTags->execute($tags); // 直接传递数组
$fetchResult = $fetchTags->get_result();
if($fetchResult->num_rows === 0) {
print('No rows');
} else {
foreach($fetchResult as $resultRow) {
?><span class="badge bg-primary me-2"><?php echo htmlspecialchars($resultRow["name"]); ?></span><?php
}
}
$fetchTags->close();
?>这种方式更加简洁,推荐在支持 PHP 8.1+ 的环境中采用。
通过采用 WHERE IN 子句和预处理语句,我们可以有效地将多个独立的数据库查询合并为一个高效的单次查询,从而解决标签显示中的 N+1 查询问题。这种优化不仅提升了应用程序的性能,也使得代码更加健壮和易于维护。在开发任何涉及从关联表中获取多条记录的系统时,都应优先考虑这种批量查询的优化策略。
以上就是PHP/MySQLi 优化标签显示:告别 N+1 查询的详细内容,更多请关注php中文网其它相关文章!
PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号