使 MySQL 查询更高效

问题描述

我有两个表,userspoints。目前 users 有 84,263 行,而 points 有 1,636,119 行。每个用户可以有 0 个或多个点,我需要提取最后创建的点。

show create table users
CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,`email` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,`password` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,`remember_token` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,`role` varchar(15) COLLATE utf8_unicode_ci DEFAULT 'consument',`created_at` timestamp NOT NULL DEFAULT current_timestamp(),`updated_at` timestamp NOT NULL DEFAULT current_timestamp(),`deleted_at` timestamp NULL DEFAULT NULL,`email_verified_at` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,`email_verify_token` text COLLATE utf8_unicode_ci DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `users_email_unique` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=84345 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

show create table points
CREATE TABLE `points` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,`user_id` int(10) unsigned NOT NULL,`tablet_id` int(10) unsigned DEFAULT NULL,`parent_company` int(10) unsigned NOT NULL,`company_id` int(10) unsigned NOT NULL,`points` int(10) unsigned NOT NULL,`mutation_type` tinyint(3) unsigned NOT NULL,KEY `points_user_id_foreign` (`user_id`),KEY `points_company_id_foreign` (`company_id`),KEY `points_parent_company_index` (`parent_company`),KEY `points_tablet_id_index` (`tablet_id`),KEY `points_mutation_type_company_id_created_at_index` (`mutation_type`,`company_id`,`created_at`),KEY `created_at_user_id` (`created_at`,`user_id`),CONSTRAINT `points_company_id_foreign` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `points_parent_company_foreign` FOREIGN KEY (`parent_company`) REFERENCES `parent_company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `points_tablet_id_foreign` FOREIGN KEY (`tablet_id`) REFERENCES `tablets` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,CONSTRAINT `points_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1798627 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

我尝试过的查询,但花费的时间太长(我们在几分钟内而不是几秒钟内讨论):

select
       `users`.`id`,`users`.`email`,`users`.`role`,`users`.`created_at`,`users`.`updated_at`,max(pt.created_at) as `last_transaction`
from `users`
left join points as pt on pt.user_id = users.id
where `users`.`role` = 'consument' and `users`.`deleted_at` is null
group by users.id

select
       `users`.`id`,pt.created_at as `last_transaction`
from `users`
left join (select points.user_id,points.created_at from points order by points.created_at desc) as pt on pt.user_id = users.id
where `users`.`role` = 'consument' and `users`.`deleted_at` is null
group by users.id

为什么我不限制结果并且一次只返回 100 个?因为我正在为 Laravel 使用 Yajra DataTables,并且在限制结果时,它只返回有限的结果,并且无法识别出更多的结果。所以我只有 100 行而不是 84,263 行,仅此而已。

解决方法

基本上你的“用户”表有一个“角色”列。它没有索引。因此,您的查询正在对具有 84263 行的“用户”表进行全表扫描。优化它的一种方法是在“角色”列上建立索引。但我可以看到“消费”是默认值,您正在通过该值进行查询。现在假设 95% 的用户具有“消费”角色。那么即使在“角色”上添加索引也无济于事。您必须添加更多条件来过滤查询并为该条件建立索引。

您的第一个查询更好,因为它可以避免第二个不必要的内部查询。

如果您需要返回 84263 行,那么这是一个单独的问题。不知何故,您将不得不引入分页。您必须将查询分解为多个查询。假设在每次调用中您返回 500 个用户数据。您可以按 id 对其进行排序。在随后的调用中,您可以要求下一个 500,其中 id 大于上一个查询中返回的最后一个 id(对于第一次调用,最后一个 id 值为 0)。然后查询可以使用“id”作为索引。

您可以使用“explain”关键字查看查询计划并更好地理解。

,

编辑

我尝试在具有 1000 个用户和 50000 个点的 role 表的 users 上添加索引,您的第一个查询花费了大约 4 秒,这太长了。

所以我尝试了这个需要大约 0.5 秒的查询,但还是太长了:

select
       `users`.`id`,`users`.`email`,`users`.`role`,`users`.`created_at`,`users`.`updated_at`,pt.created_at as `last_transaction`
from `users`
left join points pt on pt.id = (select pt2.id from points pt2 WHERE pt2.user_id = users.id ORDER BY pt2.created_at DESC limit 1)
where `users`.`role` = 'consument' and `users`.`deleted_at` is null

所以我在 points.created_at 上添加了一个索引,现在查询用了 0.05 秒,这更容易接受

,

您似乎想要一个结果集,其中包含 users 表中的一些列,以及每个用户的 created_at 表中的最新 points 值。

所谓的复合覆盖索引通常有助于加快此类查询的速度。因此,让我们从您需要的 points 开始。这个子查询得到它。

               SELECT user_id,MAX(created_at) last_transaction
                 FROM points
                GROUP BY user_id

这会为您提供一个虚拟表,其中包含您想要的每个 user_idcreated_at 值。以下索引

CREATE INDEX points_maxcreated ON points (user_id,created_at DESCENDING);

将使 MySQL 以几乎惊人的速度 loose index scan 满足子查询。

然后,让我们考虑您的查询的其余部分。

select
       `users`.`id`,`users`.`updated_at`
from `users`
where `users`.`role` = 'consument' and `users`.`deleted_at` is null

为此您需要以下索引

CREATE INDEX users_del_role_etc 
    ON users 
      (deleted_at,role,id,email,created_at,updated_at);

MySQL 可以直接从这个索引满足你的查询。将这些索引视为按顺序存储。 MySQL随机访问索引到第一个符合条件的行(null deleted_at,role = 'consument'),然后逐行读取索引,而不是表,得到你想要的数据。>

综合起来,你就明白了

select
       `users`.`id`,`subquery`.`last_transaction`
from `users`
left join (
                   SELECT user_id,MAX(created_at) last_transaction
                     FROM points
                    GROUP BY user_id
          ) subquery ON users.id = subquery.user_id
where `users`.`role` = 'consument' and `users`.`deleted_at` is null

对于您给我们的查询,这应该相当快。 尽管如此,您希望返回数万行的查询也需要一些时间。没有什么魔法可以让 SQL 快速处理非常大的结果集。它旨在从大表中快速检索小结果集。

顺便说一下,您对如何从结果集中对行进行分页的理解并不完全正确。很难相信您的用户实际上会检查数万行。如果您的查询中没有 ORDER BY 操作,LIMIT 是一个非常便宜的操作。如果您需要 ORDER BY ... LIMIT 对结果进行分页,请提出另一个问题,因为也可以管理该性能。