优化 mysql 查询 - 避免创建临时表?

Posted

技术标签:

【中文标题】优化 mysql 查询 - 避免创建临时表?【英文标题】:Optimizing the mysql query - Avoid creation of temporary table? 【发布时间】:2014-03-30 16:51:53 【问题描述】:

这是我在表上使用的查询:productsreviewsrepliesreview_images

查询:

SELECT products.id, reviews.*,
GROUP_CONCAT(DISTINCT CONCAT_WS('~',replies.reply, replies.time)) AS Replies,
GROUP_CONCAT(DISTINCT CONCAT_WS('~',review_images.image_title, review_images.image_location)) AS ReviewImages
FROM products
LEFT JOIN reviews on products.id = reviews.product_id
LEFT JOIN replies on reviews.id = replies.review_id
LEFT JOIN review_images on reviews.id = review_images.review_id
WHERE products.id = 1
GROUP BY products.id, reviews.id;

架构

产品:

id  |  name  |  product_details....

评论:

id  |  product_id  |  username  |  review  |  time  | ...

回复:

id  |  review_id   |  username  |  reply  |  time  | ...

查看图片:

id  |  review_id  |  image_title  |  image_location  | ...

索引

产品:

主键 - id

评论:

主键 - id

FOREIGN KEY - product_id(产品表中的id)

外键 - 用户名(用户表中的用户名)

回复:

主键 - id

FOREIGN KEY - review_id(评论表中的 ID)

外键 - 用户名(用户表中的用户名)

查看图片:

主键 - id

FOREIGN KEY - review_id(评论表中的 ID)


解释查询:

id | 选择类型 | 表格 | 类型 | possible_keys | | 额外

1 |简单 |产品 |索引 |空 | 1 |使用索引;使用临时的;使用文件排序

1 |简单 |评论 |全部 |产品编号 | 4 |使用哪里;使用连接缓冲区(块嵌套循环)

1 |简单 |回复 |参考 | review_id | 1 |空

1 |简单 |评论图片 |全部 | review_id | 5 |使用哪里;使用连接缓冲区(块嵌套循环)

不知道这里出了什么问题,需要使用filesort并创建一个临时表?

以下是一些分析结果:

打开表 140 µs

初始化 139 µs

系统锁定 34 µs

优化 21 µs

统计 106 µs

准备 146 µs

创建 Tmp 表 13.6 毫秒

排序结果 27 µs

执行 11 µs

发送数据 11.6 毫秒

创建排序索引 1.4 毫秒

结束 89 µs

删除 Tmp 表 8.9 毫秒

结束 34 µs

查询结束 25 µs

关闭表 66 µs

释放项目 41 µs

删除 Tmp 表 1.4 毫秒

释放项目 46 µs

删除 Tmp 表 1.2 毫秒

释放项目 203 µs

清理 55 µs


从解释和分析结果来看,很明显创建了临时表来产生结果。如何优化此查询以获得相似的结果和更好的性能并避免创建临时表?

我们将不胜感激。提前致谢。

编辑

创建表格

CREATE TABLE `products` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(100) NOT NULL,
 `description` varchar(100) NOT NULL,
 `items` int(11) NOT NULL,
 `price` int(11) NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB

CREATE TABLE `reviews` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `username` varchar(30) NOT NULL,
 `product_id` int(11) NOT NULL,
 `review` text NOT NULL,
 `time` datetime NOT NULL,
 `ratings` int(11) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `product_id` (`product_id`),
 KEY `username` (`username`)
) ENGINE=InnoDB

CREATE TABLE `replies` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `review_id` int(11) NOT NULL,
 `username` varchar(30) NOT NULL,
 `reply` text NOT NULL,
 `time` datetime NOT NULL,
 PRIMARY KEY (`id`),
 KEY `review_id` (`review_id`)
) ENGINE=InnoDB

CREATE TABLE `review_images` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `review_id` int(11) NOT NULL,
 `image_title` text NOT NULL,
 `image_location` text NOT NULL,
 PRIMARY KEY (`id`),
 KEY `review_id` (`review_id`)
) ENGINE=InnoDB

编辑

我简化了上面的查询,现在它不会创建临时表。 @Bill Karwin 提到的唯一原因是我在联接的第二张表上使用了 GROUP BY

简化查询:

SELECT reviews. * ,
GROUP_CONCAT( DISTINCT CONCAT_WS( '~', replies.reply, replies.time ) ) AS Replies,
GROUP_CONCAT( DISTINCT CONCAT_WS( '~', review_images.image_title, review_images.image_location ) ) AS ReviewImages
FROM reviews
LEFT JOIN replies ON reviews.id = replies.review_id
LEFT JOIN review_images ON reviews.id = review_images.review_id
WHERE reviews.product_id = 1
GROUP BY reviews.id

现在我面临的问题是:

因为我使用的是 GROUP_CONCAT,所以它可以保存的数据有一个限制,它位于变量 GROUP_CONCAT_MAX_LEN 中,所以当我连接用户给出的回复时,它可能会持续很长时间并且可以可能超出定义的内存。我知道我可以更改当前会话的GROUP_CONCAT_MAX_LEN 的值,但它仍然存在一个限制,即在某个时间点,查询可能会失败或无法获取完整的结果。

如何修改我的查询以便不使用 GROUP_CONCAT 并仍然获得预期的结果。

可能的解决方案:

只需使用 LEFT JOINS,它会为最后一列中的每个新结果创建重复的行,这使得在 php 中难以遍历?有什么建议吗?

我看到这个问题没有得到 SO 成员的足够回应。但从上周到上周,我一直在寻找解决方案并搜索概念。仍然没有运气。希望你们中的一些专业人士可以帮助我。提前致谢。

【问题讨论】:

【参考方案1】:

当您的 GROUP BY 子句引用来自两个不同表的列时,您无法避免创建临时表。

在此查询中避免临时表的唯一方法是将数据的非规范化版本存储在一个表中,并为您分组所依据的两列建立索引。


另一种可以简化并以更易于在 PHP 中使用的格式获得结果的方法是执行多个查询,无需 GROUP BY。

首先获得评论。示例在 PHP 和 PDO 中,但原理适用于任何语言。

$review_stmt = $pdo->query("
    SELECT reviews.*,
    FROM reviews
    WHERE reviews.product_id = 1");

将它们排列在由 review_id 键入的关联数组中。

$reviews = array();
while ($row => $review_stmt->fetch(PDO::FETCH_ASSOC)) 
    $reviews[$row['d']] = $row;

然后获取回复并使用键“回复”将它们附加到数组中。使用 INNER JOIN 而不是 LEFT JOIN,因为没有回复也没关系。

$reply_stmt = $pdo->query("
    SELECT replies.*
    FROM reviews
    INNER JOIN replies ON reviews.id = replies.review_id
    WHERE reviews.product_id = 1");
while ($row = $reply_stmt->fetch(PDO::FETCH_ASSOC)) 
    $reviews[$row['review_id']]['replies'][] = $row; 

对 review_images 做同样的事情。

$reply_stmt = $pdo->query("
    SELECT review_images.*
    FROM reviews
    INNER JOIN review_images ON reviews.id = review_images.review_id
    WHERE reviews.product_id = 1");
while ($row = $reply_stmt->fetch(PDO::FETCH_ASSOC)) 
    $reviews[$row['review_id']]['review_images'][] = $row; 

最终结果是一个评论数组,其中包含的元素分别是相关回复和图像的嵌套数组。

运行简单查询的效率可以弥补运行三个查询的额外工作。另外,您不必向explode() 组连接的字符串编写代码。

【讨论】:

我在 Docs 中读到过,但有没有其他方法可以实现此查询提供的类似结果? 在正常加入没有 GROUP_CONCAT 和 GROUP_BY 的表时,我得到了理想的结果。不创建临时表,也不应用文件排序。但是结果会扩展到许多行,其中包含最后一个连接表中不同值的重复数据。我可以在 PHP 中遍历它,但我认为 mysql 可能会提供更好的方法来做到这一点。有什么想法吗? 另外,GROUP_CONCAT 依赖于GROUP_CONCAT_MAX_LEN,这可能会导致连接长评论和回复时出现问题。所以,我很困惑。 @比尔卡尔文 那么,您认为解决这个特定问题的更好方法是什么?你会怎么做,因为你很有经验,我想学习。 请看我的问题的编辑部分。如果你能提供帮助,我会很高兴。

以上是关于优化 mysql 查询 - 避免创建临时表?的主要内容,如果未能解决你的问题,请参考以下文章

mysql数据库怎么把查询出来的数据生成临时表

Mysql 临时表 视图

mysql创建临时表,将查询结果插入已有表中

MYSQL存储引擎InnoDB(三十五):临时表空间

MySQL5.7性能优化系列——SQL语句优化——使用物化策略优化子查询

Mysql 使用临时表比较数据差异以及 临时表的优化