【问题标题】:MySQL/Eloquent Query OptimizationMySQL/Eloquent 查询优化
【发布时间】:2020-12-17 22:28:00
【问题描述】:

我有一个包含多个表的数据库,我要优化的查询中涉及的只有 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.4 到 1.6 秒。 我想尽可能减少它。理想的目标是小于 10 毫秒 ????

我已经在几个表中使用了索引,我已经设法将其他查询的时间从最多 4 秒减少到只有 15-20 毫秒。我愿意使用任何东西将性能降低到最低限度。

我正在使用 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

【问题讨论】:

  • 只想指出几点: 1. 提供的架构不完整,因为任何表上都没有release_date 字段。 2. 您使用$album-&gt;genres-&gt;pluck('id') 执行查询。 3. 您应该尝试在每个单独的查询上运行 EXPLAIN 以确保它们正在使用索引。
  • 1.你是对的,我想让这个问题保持简单,事实上每个表中有很多字段。 2.在问题中我明确表示之前已经加载了流派,我需要这样。因此,$album-&gt;genres 不会进行其他查询。 3 从一开始我就一直这样做。只有索引不适用于EXISTS。这就是我在这里寻求帮助的原因。
  • 对每个单独的查询运行解释,简单地按没有索引的字段字段排序会使您的查询变慢。
  • 为什么不提供一些样本数据和所需数量的结果?
  • 不要认为EXISTS 是这里的瓶颈。 Mysql EXISTS 非常高效。我会听从@Pablo 的建议,也许会分享结果让我们看看?我们谈论的数据集有多大?另外,您提到有很多领域。根据字段的类型,您可能会通过仅选择子查询中的必填字段来获得优势。

标签: mysql sql laravel


【解决方案1】:

没有看到真实数据就很难猜测......但无论如何:

我认为问题在于,即使您将所需的行数限制为 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;

【讨论】:

  • 很好的答案,你最后的加入建议,有重复行的问题。所以需要使用distinct a.*。我在想这也是一个性能缺陷吗? (我实际上运行了这个查询)
  • 这个查询运行得很快,它已经减少到 28 毫秒。然而,正如 Tharaka Dilshan 提到的,存在重复专辑的问题。我在没有创建任何索引的情况下尝试了查询,如果我添加这些索引,您认为这次会改善吗?我还通过添加结构和测试数据来编辑我的答案,以防你想看看并做一些测试。
  • 我已经解决了这个问题。即使使用原始查询,只需添加您指定的索引,查询已减少到仅 1.75ms。非常感谢,答案很好。
  • 重要的是要对要排序的列进行索引。这样,一旦 mysql 获得 6 行进行切割,它就可以“停止”查询。在末尾添加 published 会有所改进,因为您会获得 covering 索引。当你说你按release_datecreated_at 排序时,真的是和吗? (那是你做ORDER BY release_date, created_at还是OR?你有时ORDER BY release_date或有时ORDER BY created_at?@MrEduar
  • 关于降序索引:Mysql = 8 确实有并且可以使用 DESC 索引。所以如果你真的需要降序排序,你最好将所需列的索引定义为DESC
【解决方案2】:

我注意到的一件事是您不必加入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】:

    当然,您必须优化您的查询以获得快速响应时间,但这里有另一个提示,它可以加快您的响应时间。

    我也遇到过响应时间慢的类似问题,我通过简单地使用缓存来大幅减少它。

    您可以在 Laravel 中使用redis 驱动进行缓存,它可以避免您一次又一次地查询数据库,因此您的响应时间将自动得到改善,因为 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

    【讨论】:

      【解决方案4】:

      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),(published,relase_date) 上的索引可能会有所帮助。

      【讨论】:

      • Here 是对这个查询的解释。与其他答案一样,这个答案也重复了结果。
      • 如果你想要 DISTNCT 结果,只需使用 DISTINCT
      • 我使用了它,但是,distinct 使查询增加,我得到了与问题相同的时间。
      • (published,relase_date) 上的索引可能会有所帮助。已编辑。
      猜你喜欢
      • 1970-01-01
      • 2018-05-27
      • 2021-07-22
      • 2017-11-14
      • 2017-10-07
      • 2020-09-24
      • 2011-01-22
      • 2011-07-07
      • 2018-12-21
      相关资源
      最近更新 更多