我有一个包含多个表的数据库,此查询中要优化的表只有4个。
albums
,songs
,genres
,genre_song
一首歌曲可以有多种流派,而一首流派也可以有多种流派。一张专辑可以包含很多歌曲。专辑通过歌曲与流派相关。
目标是能够推荐与专辑类型相关的专辑。
因此导致我有这个查询。
SELECT *
FROM `albums`
WHERE EXISTS
(SELECT *
FROM `songs`
WHERE `albums`.`id` = `songs`.`album_id`
AND EXISTS
(SELECT *
FROM `genres`
INNER JOIN `genre_song` ON `genres`.`id` = `genre_song`.`genre_id`
WHERE `songs`.`id` = `genre_song`.`song_id`
AND `genres`.`id` IN (6)))
AND `id` <> 37635
AND `published` = 1
ORDER BY `release_date` DESC
LIMIT 6
此查询使我在1.4s和1.6s之间。 我想尽可能减少它。理想的目标是少于10ms ?
我已经在多个表中使用了索引,我设法将其他查询中的时间从最多4秒减少到只有15-20ms。我愿意使用任何方法将性能降低到最低。
我正在使用Laravel,所以这将是Eloquent的查询。
$relatedAlbums = Album::whereHas('songs.genres', function ($query) use ($album) {
$query->whereIn('genres.id', $album->genres->pluck('id'));
})->where('id', '<>', $album->id)
->orderByDesc('release_date')
->take(6)
->get();
注意:以前,流派已加载。
如果您要在数据库中重新创建表和一些虚假数据,请here is the structure
答案 0 :(得分:3)
不获取真实数据就很难进行猜测...但是无论如何:
我认为问题在于,即使您将所需的行限制为6,也必须读取所有专辑表,因为:
如果您以已发布的状态和发布的日期访问了专辑,一旦获得了前6张专辑的首张专辑,mysql将停止处理查询。当然,您可能会遇到“运气不好”的情况,也许拥有流派6歌曲的专辑是发行最早的专辑,因此您无论如何都要阅读和处理许多专辑。无论如何,这种优化不应该受到损害,因此值得一试,并且应该期望数据最终会有所分布。
此外,如其他答案所述,您实际上不需要访问geres表(尽管这可能不是查询中最严重的问题)。您可以只访问genre_song,也可以为所需的两列创建一个新索引。
create index genre_song_id_id on genre_song(genre_id, song_id);
请注意,仅当您更改查询时,前一个索引才有意义(如答案末尾所建议)
对于专辑表,您可以创建以下两个索引中的任何一个:
create index release_date_desc_v1 on albums (published, release_date desc);
create index release_date_desc_v2 on albums (release_date desc, published);
选择更适合您数据的索引:
请同时测试它们,但不要让两个索引同时存在。如果要测试_v1,请确保已删除_v2,反之亦然。
此外,将查询更改为不使用genre
表:
SELECT *
FROM `albums`
WHERE EXISTS
(SELECT *
FROM `songs`
WHERE `albums`.`id` = `songs`.`album_id`
AND EXISTS
(SELECT *
FROM `genre_song`
WHERE `songs`.`id` = `genre_song`.`song_id`
AND `genre_song`.`genre_id` IN (6)))
AND `id` <> 37635
AND `published` = 1
ORDER BY `release_date` DESC
LIMIT 6;
答案 1 :(得分:1)
FWIW,我发现以下内容更容易理解,因此我想查看对此的解释:
SELECT DISTINCT a.*
FROM albums a
JOIN songs s
ON s.album_id = a.id
JOIN genre_song gs
ON gs.song_id = s.id
JOIN genres g
ON g.id = gs.genre_id
WHERE g.id IN (6)
AND a.id <> 37635
AND a.published = 1
ORDER
BY a.release_date DESC
LIMIT 6
在这种情况下(并假设表是InnoDB),(发布日期,relase_date)上的索引可能会有所帮助。
答案 2 :(得分:1)
我注意到的一件事是,您不必在以下子查询中加入genres
表
AND EXISTS
(SELECT *
FROM `genres`
INNER JOIN `genre_song` ON `genres`.`id` = `genre_song`.`genre_id`
WHERE `songs`.`id` = `genre_song`.`song_id`
AND `genres`.`id` IN (6))
我们可以简化此过程,以下可能是整个查询。
SELECT *
FROM `albums`
WHERE EXISTS
(SELECT *
FROM `songs`
WHERE `albums`.`id` = `songs`.`album_id`
AND EXISTS
(SELECT *
FROM `genre_song`
WHERE `songs`.`id` = `genre_song`.`song_id`
AND `genre_song`.`genre_id` IN (6)))
AND `id` <> 37635
AND `published` = 1
ORDER BY `release_date` DESC
LIMIT 6
答案 3 :(得分:1)
当然,您必须优化查询以缩短响应时间,但这是另一个提示,可以提高响应时间。
我曾经遇到过类似的慢响应时间问题,并且我已经设法通过简单地使用缓存来大大减少了响应时间。
您可以使用redis
驱动程序在Laravel中进行缓存,这将使您免于一次又一次查询数据库,因此响应时间将自动得到改善,因为Redis将查询及其结果存储在键值对中,因此下次您进行api调用时,将从缓存中返回结果,而不会查询数据库。使用redis驱动程序进行高速缓存将为您带来我爱戴的一项出色优势。
您可以使用缓存标签
缓存标签允许您标记缓存中的相关项目,然后刷新分配给定标签的所有缓存值。例如,您有一个api可以检索具有$id=1
用户的帖子,然后您可以动态将数据放入缓存标签中,以便下次查询同一条记录可以加快响应时间,如果您要更新数据库中的数据,也可以将其也更新为缓存标签。您可以执行以下操作>
public $cacheTag = 'user';
// checking if the record exists in cache already then retrieve it from cache
//other wise retrieve it from database and store it in cache as well for next time
//to boost response time.
$item = Cache::tags([$cacheTag])->get($cacheTag.$id);
if($item == NULL) {
if(!$row) {
$row = $this->model->find($id);
}
if($row != NULL || $row != false) {
$item = (object) $row->toArray();
Cache::tags([$cacheTag])->forever($this->cacheTag.$id, $item);
}
}
在更新数据库中的数据时,您可以从缓存中删除数据并进行更新
if($refresh)
{
Cache::tags([$cacheTag])->forget($cacheTag.$id);
}