慢查询分组,加入MYSQL laravel

Posted

技术标签:

【中文标题】慢查询分组,加入MYSQL laravel【英文标题】:Slow Query Groupby, Join MYSQL laravel 【发布时间】:2020-03-04 19:31:11 【问题描述】:

我有 4 个表:地点、品牌、类别、位置。

关系是 Places belongsTo Brands 和 Places ManytoMany with Categories and Locations。

我想获得具有特定类别和位置的搜索结果地点,但每个品牌只显示 1 个地点。

表格信息 places 表包含大约 100k+ 行

place_category 数据透视表包含 650k+ 行,place_category.place_idplaces.brand_id 列已编入索引

place_locations 数据透视表包含大约 550k+ 行,place_location.place_idplace_location.location_id 列被索引

到目前为止我得到的查询

Place::join('place_location', function ($join) use ($city) 
    $join->on('place_location.place_id', '=', 'places.id')
         ->where('place_location.location_id', '=',  $city->id);
)
->join('place_category', function ($join) 
    $join->on('place_category.place_id', '=', 'places.id')
         ->where('place_category.category_id', '=',  $category->id);
)
->groupBy('places.brand_id')
->take(5)
->get();

groupBy 导致查询速度慢,查询时间约为 2 秒。

解释结果如下所示

id | select_type | table          | possible_key            | key            | key_len | ref                | rows | Extra

1  | SIMPLE      | places         | PRIMARY                 | brand_id       | 4       | NULL               | 50   | Using where

1  | SIMPLE      | place_location | place_id,place_location | place_location | 4       | const,db.places.id | 1    | Using index

1  | SIMPLE      | place_category | place_category          | place_category | 4       | db.places.id,const | 1    | Using where; Using index

原始 mysql 查询如下所示

select 
    `places`.`id`, 
    `places`.`name`, 
    `places`.`display`, 
    `places`.`status_a`, 
    `places`.`status_b`, 
    `places`.`brand_id`, 
    `places`.`address` 
from `places` 
inner join `place_location` 
    on `place_location`.`place_id` = `places`.`id` 
    and `place_location`.`location_id` = 4047 
inner join `place_category` 
    on `place_category`.`place_id` = `places`.`id` 
    and `place_category`.`category_id` = 102 
where 
    `places`.`status_a` != 1 
    and `status_b` = 2 
    and `display` >= 5 
group by `places`.`brand_id` 
limit 4

显示创建表是这样的

CREATE TABLE `places` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `user_id` int(11) unsigned DEFAULT NULL,
 `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
 `desc` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
 `city_id` int(11) unsigned NOT NULL DEFAULT '102',
 `state_id` int(11) unsigned NOT NULL DEFAULT '34',
 `location_id` int(11) unsigned NOT NULL DEFAULT '15',
 `landmark_id` int(10) unsigned NOT NULL DEFAULT '1',
 `postcode` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `country_id` int(4) unsigned NOT NULL,
 `lat` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `long` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `phone` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
 `sec_phone` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
 `third_phone` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `fourth_phone` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `brand_id` int(10) NOT NULL DEFAULT '1',
 `display` int(11) NOT NULL DEFAULT '0',
 `view` int(10) unsigned NOT NULL DEFAULT '0',
 `status_b` tinyint(3) unsigned NOT NULL DEFAULT '2',
 `status_a` tinyint(4) NOT NULL DEFAULT '2',
 `company_name` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
 `slug` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
 `lock_status_id` tinyint(3) unsigned DEFAULT '1',
 `created_at` timestamp NULL DEFAULT NULL,
 `updated_at` timestamp NULL DEFAULT NULL,
 PRIMARY KEY (`id`),
 UNIQUE KEY `slug` (`slug`),
 KEY `city_id` (`city_id`),
 KEY `location_id` (`location_id`),
 KEY `user_id` (`user_id`),
 KEY `landmark_id` (`landmark_id`),
 KEY `name` (`name`),
 KEY `brand_id` (`brand_id`),
 KEY `groupby_brandid` (`status_b`, `display`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=116070 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci


CREATE TABLE `place_location` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `location_id` int(10) NOT NULL,
 `place_id` int(10) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `place_location` (`place_id`,`location_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=564259 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

CREATE TABLE `place_category` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `category_id` int(11) unsigned NOT NULL,
 `place_id` int(11) unsigned NOT NULL,
 `branch_id` int(11) unsigned NOT NULL,
 `created_at` timestamp NULL DEFAULT NULL,
 `updated_at` timestamp NULL DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `place_id` (`place_id`),
 KEY `place_category` (`category_id`,`place_id`)
) ENGINE=InnoDB AUTO_INCREMENT=905384 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

知道如何改进查询吗?错误的索引?还是错误的查询?

【问题讨论】:

作为一个练习,你能否也包括你当前从你的 Laravel 代码运行的原始 MySQL 查询?仅通过查看您的 php 代码就很难了解该查询是什么。 刚刚添加了原始 MySQL place_location 上的索引是 2 个键的索引还是 1 个键的 2 个索引? 它是一个有2列的索引,索引的列顺序是place_id和location_id列。我认为由 groupBY 引起的问题 注意(与性能无关)按brand_id 分组并且所有其他列未用聚合函数包装很可能会返回不确定的结果(我怀疑产品表中的所有列在功能上都依赖于brand_id ) 更多:Group by clause in mySQL and postgreSQL, why the error in postgreSQL? 【参考方案1】:
`products`.`status_a` != 1 
and `status_b` = 2 
and `display` >= 5 

邀请这个

INDEX(status_b, display)

这是 0/1 标志吗?

`products`.`status_a` != 1 

如果是,则改为

`products`.`status_a` = 0

然后你可以做得更好

INDEX(status_b, status_a, display)

product_locationproduct_category 听起来像是多对多映射表。他们需要复合索引,如下所述:http://mysql.rjweb.org/doc.php/index_cookbook_mysql#many_to_many_mapping_table 确保数据类型匹配。

【讨论】:

我现在正在对数据透视表 product_location 和 product_category 使用复合索引,(我刚刚更新了问题的解释)但它仍然很慢,如果我删除 groupby,它需要大约 10-12 秒, products.status_a != 1 需要 1s 以下,有 3 个值所以它不是 0/1 我尝试了你对索引 INDEX(status_b, status_a, display) 的回答,EXPLAIN 显示 mysql 不使用索引,它一直使用单列brand_id 索引。 @TomKur - != 通常无法编入索引,因此该 3 列索引失败。我真的需要为每张桌子看到SHOW CREATE TABLE 我用 SHOW CREATE TABLE 更新了地方表和数据透视表的问题 请听从我对多对多表和INDEX(status_b, display)的建议。【参考方案2】:

首先尝试执行下面的查询

explain 
select  `products`.`id`, `products`.`name`, `products`.`display`,
        `products`.`status_a`, `products`.`status_b`, `products`.`brand_id`,
        `products`.`address`
    from  `products`
    inner join  `product_location`  ON `product_location`.`product_id` = `products`.`id`
      and  `product_location`.`location_id` = 4047
    inner join  `product_category`  ON `product_category`.`product_id` = `products`.`id`
      and  `product_category`.`category_id` = 102
    where  `products`.`status_a` != 1
      and  `status_b` = 2
      and  `display` >= 5
    group by  `products`.`brand_id`
    limit  4 

EXPLAIN SELECT 语句显示 MySQL 查询优化器将如何执行查询

索引可以提高性能,索引也会产生负面影响 如果它们太多,则性能。这是因为越 索引一个表,MySQL 必须做的工作越多来保持它们的更新。 诀窍是在足够多的索引之间找到适当的平衡 提高性能,但不会产生负面影响 性能。

再次尝试添加索引并执行相同的说明语句

希望对您有所帮助谢谢...

【讨论】:

有什么建议要索引哪些列?【参考方案3】:

为什么你使用连接查询,你可以使用 laravel eloquent 关系来解决这个问题

Place::whereHas('placeLocation', function ($query) use ($city) 
    $query->where('location_id', '=',  $city->id);
)
->whereHas('placeCategory', function ($query) 
    $query->where('category_id', '=',  $category->id);
)
->groupBy('brand_id')
->take(5)
->get();

placeLocation (HasMany) 和 placeCategory(BelongsToMany) 这两个关系都是你必须在 Place 模型中写的。

【讨论】:

【参考方案4】:

通常数据库在查询执行期间将无法合并多个索引。这意味着,为表中的所有内容创建单列索引无济于事。

您似乎经常使用单列索引。尝试以满足您查询的方式组合它们。

多索引如何工作?

在创建数据库索引时,我总是尝试根据食谱中的索引来解释它们:这些通常是嵌套索引。首先,它们按膳食类型分类,如汤、菜、沙拉等。在这些类别中,它们按字母顺序排序。

这个索引在 SQL 中应该是这样的:

KEY `recipe` (`meal_type_id`, `name`)

作为人类,您现在可以通过先找到盘子(第一个索引)然后找到字母“C”(第二个索引)来找到芝士蛋糕。

所以在这种情况下,多列索引非常有用。

优化餐桌位置:

您在WHERE 子句中使用status_astatus_bdisplay,在GROUP BY 中也使用brand_id。对于您的多列索引,请尝试找到这些列的有意义的组合。

这里的顺序很重要!

例如,如果只有 10% 的数据与 status_b = 2 匹配,那么您应该将该字段用作索引中的第一列,因为它会消除 90% 的行。那么第二个索引列要做的事情要少得多。想想上面的芝士蛋糕例子:我们已经知道芝士蛋糕是一道菜,所以我们直接看菜,就能排除其他 90% 的食谱。

这叫做“cardinality”。

优化表place_location和place_category:

不要忘记查看已连接的表并确保它们也被正确索引。

就像之前提到的,也尝试在这些表上找到有用的索引。查看您的查询所要求的内容,并尝试使用有用的索引来满足它。

一个表应该有多少个索引?

要回答这个问题,您只需要了解索引必须在每次 INSERT 或 UPDATE 时更新。因此,如果是写入密集型表,则应尽可能少使用索引。

如果表是读取密集型的,但不会经常写入,那么多一点索引应该没问题。请记住,他们将需要记忆。如果您的表有数百万行,那么如果您使用许多索引,您的索引可能会变得非常大。

【讨论】:

以上是关于慢查询分组,加入MYSQL laravel的主要内容,如果未能解决你的问题,请参考以下文章

通过在mysql中使查询非常慢来分组和排序?

mysql 慢查询

MySQL慢查询 - pt-query-digest详解慢查询日志

MySQL慢查询 - pt-query-digest详解慢查询日志

mysql慢查询----pt-query-digest详解慢查询日志(linux系统)

mysql慢查询