慢查询分组,加入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_id
和 places.brand_id
列已编入索引
place_locations
数据透视表包含大约 550k+ 行,place_location.place_id
和 place_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_location
和product_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_a
、status_b
和display
,在GROUP BY
中也使用brand_id
。对于您的多列索引,请尝试找到这些列的有意义的组合。
这里的顺序很重要!
例如,如果只有 10% 的数据与 status_b = 2
匹配,那么您应该将该字段用作索引中的第一列,因为它会消除 90% 的行。那么第二个索引列要做的事情要少得多。想想上面的芝士蛋糕例子:我们已经知道芝士蛋糕是一道菜,所以我们直接看菜,就能排除其他 90% 的食谱。
这叫做“cardinality”。
优化表place_location和place_category:
不要忘记查看已连接的表并确保它们也被正确索引。
就像之前提到的,也尝试在这些表上找到有用的索引。查看您的查询所要求的内容,并尝试使用有用的索引来满足它。
一个表应该有多少个索引?
要回答这个问题,您只需要了解索引必须在每次 INSERT 或 UPDATE 时更新。因此,如果是写入密集型表,则应尽可能少使用索引。
如果表是读取密集型的,但不会经常写入,那么多一点索引应该没问题。请记住,他们将需要记忆。如果您的表有数百万行,那么如果您使用许多索引,您的索引可能会变得非常大。
【讨论】:
以上是关于慢查询分组,加入MYSQL laravel的主要内容,如果未能解决你的问题,请参考以下文章
MySQL慢查询 - pt-query-digest详解慢查询日志
MySQL慢查询 - pt-query-digest详解慢查询日志