MySQL /雄辩的查询优化

时间:2020-08-29 03:20:24

标签: mysql sql laravel

我有一个包含多个表的数据库,此查询中要优化的表只有4个。

albumssongsgenresgenre_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

4 个答案:

答案 0 :(得分:3)

不获取真实数据就很难进行猜测...但是无论如何:

我认为问题在于,即使您将所需的行限制为6,也必须读取所有专辑表,因为:

  • 您正在按非索引列对其进行过滤
  • 您正在按非索引列对其进行排序
  • 您不知道哪些专辑会大放异彩(将播放要求类型的歌曲)。因此,您需要计算所有参数,然后按release_date排序,并保持前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索引会更好

请同时测试它们,但不要让两个索引同时存在。如果要测试_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);
 }

You can read more about cache from Laravel's documentation