如何缩短 Laravel 查询生成器生成的 SQL 查询的执行时间

How to improve execution time of a Laravel Query Builder generated SQL query

我有三个 table 与此查询有关

CREATE TABLE `tags` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `latName` varchar(191) NOT NULL,
  `araName` varchar(191) NOT NULL,
  `active` tinyint(1) NOT NULL DEFAULT 0,
  `img_name` varchar(191) DEFAULT NULL,
  `icon` varchar(191) DEFAULT NULL,
  `rgba_color` varchar(191) DEFAULT NULL,
  `color` varchar(191) DEFAULT NULL,
  `overlay` varchar(191) DEFAULT NULL,
  `position` int(11) NOT NULL,
  `mdi_icon` varchar(191) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `tags_latname_unique` (`latName`),
  UNIQUE KEY `tags_araname_unique` (`araName`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3
 CREATE TABLE `newspapers` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `latName` varchar(191) NOT NULL,
  `araName` varchar(191) NOT NULL,
  `img_name` varchar(191) DEFAULT NULL,
  `active` tinyint(1) NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY `newspapers_latname_unique` (`latName`),
  UNIQUE KEY `newspapers_araname_unique` (`araName`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb3
CREATE TABLE `articles` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `newspaper_id` bigint(20) unsigned NOT NULL,
  `tag_id` bigint(20) unsigned NOT NULL,
  `seen` int(10) unsigned NOT NULL,
  `link` varchar(1000) NOT NULL,
  `title` varchar(191) NOT NULL,
  `img_name` varchar(191) NOT NULL,
  `date` datetime NOT NULL,
  `paragraph` text NOT NULL,
  `read_time` int(11) DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `articles_link_unique` (`link`),
  UNIQUE KEY `articles_img_name_unique` (`img_name`),
  KEY `articles_newspaper_id_foreign` (`newspaper_id`),
  KEY `articles_tag_id_foreign` (`tag_id`),
  CONSTRAINT `articles_newspaper_id_foreign` FOREIGN KEY (`newspaper_id`) REFERENCES `newspapers` (`id`),
  CONSTRAINT `articles_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=47421 DEFAULT CHARSET=utf8mb3

基本上,我想加载最新的 5 篇具有活跃报纸和活跃标签的文章(按日期排序)。

目前文章 table 包含大约 40k 个条目。

这是由 Laravel 的查询生成器生成的查询

SELECT `articles`.*
FROM `articles`
INNER JOIN `tags` ON `tags`.`id` = `articles`.`tag_id`
  AND `tags`.`active` = 1
INNER JOIN `newspapers` ON `newspapers`.`id` = `articles`.`newspaper_id`
  AND `newspapers`.`active` = 1
ORDER BY `date` DESC
LIMIT 5;

查询 运行 需要 Mysql 大约 6 秒,当我删除 ORDER BY 子句时,查询变得非常快(0.001 秒)。

查询说明如下:

+------+-------------+------------+--------+-------------------------------------------------------+-------------------------------+---------+------------------------+------+----------------------------------------------+
| id   | select_type | table      | type   | possible_keys                                         | key                           | key_len | ref                    | rows | Extra                                        |
+------+-------------+------------+--------+-------------------------------------------------------+-------------------------------+---------+------------------------+------+----------------------------------------------+
|    1 | SIMPLE      | newspapers | ALL    | PRIMARY                                               | NULL                          | NULL    | NULL                   | 18   | Using where; Using temporary; Using filesort |
|    1 | SIMPLE      | articles   | ref    | articles_newspaper_id_foreign,articles_tag_id_foreign | articles_newspaper_id_foreign | 8       | mouhim.newspapers.id   | 1127 |                                              |
|    1 | SIMPLE      | tags       | eq_ref | PRIMARY                                               | PRIMARY                       | 8       | mouhim.articles.tag_id | 1    | Using where                                  |
+------+-------------+------------+--------+-------------------------------------------------------+-------------------------------+---------+------------------------+------+----------------------------------------------+

我尝试在日期属性上创建索引,但没有帮助。

为方便起见,这就是我为此查询使用查询生成器的方式:

Article::select("articles.*")
    ->join("tags", function ($join) {
        $join->on("tags.id", "articles.tag_id")
            ->where("tags.active", 1);
    })
    ->join("newspapers", function ($join) {
        $join->on("newspapers.id", "articles.newspaper_id")
            ->where("newspapers.active", 1);
    })
        ->orderBy("date", "desc")
        ->paginate(5)

起初,我使用的是 Eloquent (whereHas),但 Eloquent 使用 (where exists) 生成非优化查询,所以我不得不采用连接方式。

我可以做些什么来缩短这个查询的执行时间?

SHOW INDEXES FROM articles;

的结果
+----------+------------+-------------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+
| Table    | Non_unique | Key_name                      | Seq_in_index | Column_name  | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Ignored |
+----------+------------+-------------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+
| articles |          0 | PRIMARY                       |            1 | id           | A         |       36072 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| articles |          0 | articles_link_unique          |            1 | link         | A         |       36072 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| articles |          0 | articles_img_name_unique      |            1 | img_name     | A         |       36072 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| articles |          1 | articles_newspaper_id_foreign |            1 | newspaper_id | A         |          32 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| articles |          1 | articles_tag_id_foreign       |            1 | tag_id       | A         |          12 |     NULL | NULL   |      | BTREE      |         |               | NO      |
| articles |          1 | data                          |            1 | date         | A         |       36072 |     NULL | NULL   |      | BTREE      |         |               | NO      |
+----------+------------+-------------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+

此查询由 Rick James 建议作为解决方案

SELECT  `articles`.*
FROM  `articles`     
WHERE EXISTS ( SELECT 1 FROM tags WHERE id = `articles`.`tag_id` and active = 1)
AND EXISTS ( SELECT 1 FROM newspapers WHERE id = `articles`.`newspaper_id` and active = 1)     
ORDER BY `date` DESC
LIMIT  5;

运行 EXPLAIN 对此查询产生以下结果

+------+-------------+------------+--------+-------------------------------------------------------+-------------------------------+---------+------------------------+------+----------------------------------------------+
| id   | select_type | table      | type   | possible_keys                                         | key                           | key_len | ref                    | rows | Extra                                        |
+------+-------------+------------+--------+-------------------------------------------------------+-------------------------------+---------+------------------------+------+----------------------------------------------+
|    1 | PRIMARY     | newspapers | ALL    | PRIMARY                                               | NULL                          | NULL    | NULL                   | 18   | Using where; Using temporary; Using filesort |
|    1 | PRIMARY     | articles   | ref    | articles_newspaper_id_foreign,articles_tag_id_foreign | articles_newspaper_id_foreign | 8       | mouhim.newspapers.id   | 1127 |                                              |
|    1 | PRIMARY     | tags       | eq_ref | PRIMARY                                               | PRIMARY                       | 8       | mouhim.articles.tag_id | 1    | Using where                                  |
+------+-------------+------------+--------+-------------------------------------------------------+-------------------------------+---------+------------------------+------+----------------------------------------------+

我不确定这两个查询是否应该相同,但事实并非如此。

无论如何,对于第二个查询,我认为这应该更好

Article::leftJoin('tags', 'articles.tag_id', '=', 'tags.id)
   ->where('tags.latName', $tag)
   ->orderBy("articles.date", "desc")
   ->select(['articles.*'])
   ->paginate(5);

问题可能在于,您在 whereIn 中创建的子查询正在减慢它的速度,而 whereIn 本身也可能会减慢您的查询速度。这可以通过使用 join 和 where 来缓解。

关于第一个查询,你能展示一下你是如何为日期建立索引的吗? :)

假设你不想复制,改成这个;它可能会快得多:

SELECT  `articles`.*
    FROM  `articles`
    WHERE EXISTS ( SELECT 1 FROM tags
                        WHERE id = `articles`.`tag_id` )
      AND EXISTS ( SELECT 1 FROM newspapers
                        WHERE id = `articles`.`newspaper_id` )
    ORDER BY  `date` DESC
    LIMIT  5;

此外,在 articles 上有此索引:

INDEX(date)

(这是 罕见的 用例 starting 索引,其列将在 'range' 中使用。 )

(抱歉,我不会说 'Laravel';也许其他人可以帮助解决这部分问题。)

PS。在 table 上有 3 个 UNIQUE 键是非常不寻常的。它通常表示架构设计存在问题。

each article has one and only one Tag associated with it

多篇文章可以有相同的Tag吗?

when I remove the ORDER BY clause, the query becomes very fast (0.001sec).

那是因为您可以轻松地 return 得到任何 5 行。显然 ORDER BY 是要求的一部分。 “使用临时文件;使用文件排序”说至少有一种排序。它实际上是一种“文件”排序——因为 SELECT * 包含一个 TEXT 列。 (有一种技术可以避免“文件”,但我认为这里不需要。)