在具有 10+ 百万行的表上使用 1 个连接优化查询

Optimize query with 1 join, on tables with 10+ millions rows

我正在考虑使用 2 table 秒更快地发出请求。
我有以下 2 tables :

Table“日志”

table 具有 PHP Laravel 框架所称的与其他几个对象的“多态多对多”关系,因此存在第二个 table” logs_pivot" :

logs_pivot 中的每个条目在 logs 中有一个或多个条目。它们分别有 20+ 和 10+ 百万行。

我们这样查询:

select * from logs 
join logs_pivot on logs.id = logs_pivot.log_id
where model_id = 'some_id' and model_type = 'My\Class'
order by date desc
limit 50;

显然我们在 model_id 和 model_type 字段上都有一个复合索引,但请求仍然很慢:每次几(几十)秒。
我们在 date 字段上也有一个索引,但是 EXPLAIN 表明这是使用的 model_id_model_type 索引。

解释语句:

+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
| id | select_type | table       | partitions | type   | possible_keys                                                                  | key                                           | key_len | ref                                       | rows | filtered | Extra                           |
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
|  1 | SIMPLE      | logs_pivot  | NULL       | ref    | logs_pivot_model_id_model_type_index,logs_pivot_log_id_index | logs_pivot_model_id_model_type_index | 364     | const,const                               |    1 |   100.00 | Using temporary; Using filesort |
|  1 | SIMPLE      | logs        | NULL       | eq_ref | PRIMARY                                                                        | PRIMARY                                       | 146     | the_db_name.logs_pivot.log_id |    1 |   100.00 | NULL                            |
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+

在其他 table 中,通过在索引中包含日期字段,我能够更快地发出类似的请求。但在那种情况下,它们位于单独的 table.

当我们想要访问这些数据时,它们通常有一些 hours/days 旧。
我们的 InnoDB 池太小,无法在内存中保存所有数据(+所有其他 tables),因此数据很可能总是在磁盘上查询。

我们可以通过哪些方式更快地提出请求?
理想情况下仅使用另一个索引,或通过更改其完成方式。

非常感谢!


编辑 17h05 :
到目前为止,谢谢大家的回答,我会尝试像 O Jones 建议的那样,并以某种方式将日期字段包含在数据透视表 table 中,以便我可以包含在索引索引中。


编辑 14/10 10h。

解决方案:

所以我最终改变了请求的实际完成方式,通过对数据透视表 table 的 id 字段进行排序,这确实允许将其放入索引中。

此外,计算总行数的请求已更改为仅在数据透视表 table 上完成,当它未按日期过滤时。

谢谢大家!

我看到两个问题:

  • 当 tables 相对于 RAM 大小来说很大时,UUID 是昂贵的。

  • LIMIT无法得到最佳处理,因为WHERE子句来自一个table,但是ORDER BY 列来自另一个 table。也就是说,它将执行所有 JOIN,然后排序,最后剥离几行。

只是一个建议。使用复合索引显然是一件好事。另一个可能是按日期 pre-qualify 一个 ID,并根据你的 logs_pivot table 索引扩展你的索引 (model_id, model_type, log_id).

如果您查询数据,并且整个历史记录有 20 多万条记录,那么在您处理每个给定模型类别的限制为 50 条记录的地方,数据回溯到多远 id/type。说3个月? vs 说你的 5 年日志? (未在 post 中列出,只是一个 for-instance)。因此,如果您可以查询日期大于 3 个月前的最小日志 ID,则该 ID 可以限制您的 logs_pivot table.

发生的其他事情

类似

select
      lp.*,
      l.date
   from
      logs_pivot lp
         JOIN Logs l
            on lp.log_id = l.id
   where
          model_id = 'some_id' 
      and model_type = 'My\Class'
      and log_id >= ( select min( id )
                         from logs
                        where date >= datesub( curdate(), interval 3 month ))
   order by 
      l.date desc
   limit  
      50;

因此,log_id 的 where 子句只执行一次,returns 只是 3 个月前的 ID,而不是 logs_pivot 的整个历史记录。然后你使用模型 id/type 的优化 two-part 键查询,但也跳转到其索引的末尾,ID 包含在索引键中以跳过所有历史。

您可能想要包括的另一件事是一些 pre-aggregate table 记录的数量,例如每个 month/year 每个给定模型 type/id。将其用作 pre-query 以呈现给用户,然后您可以将其用作 drill-down 以进一步获取更多详细信息。 pre-aggregate table 可以在所有历史资料上完成一次,因为它是静态的,不会改变。您唯一需要不断更新的是当前单月期间的任何内容,例如每晚。或者甚至可能更好,通过一个触发器,该触发器要么在每次添加完成时插入一条记录,要么根据 year/month 聚合更新给定 model/type 的计数。同样,只是一个建议,没有关于如何/为什么将数据呈现给 end-user.

的其他上下文

SELECT columns FROM big table ORDER BY something LIMIT small number 是一个臭名昭著的查询性能反模式。为什么?服务器对一大堆长行进行排序,然后丢弃几乎所有的行。您的 columns 之一是 LOB -- TEXT 列也无济于事。

这里有一种方法可以减少这种开销:通过查找所需的主键集来确定所需的行,然后仅获取这些行的内容。

你想要什么行?此子查询找到它们。

                  SELECT id
                    FROM logs
                    JOIN logs_pivot 
                            ON logs.id = logs_pivot.log_id
                   WHERE logs_pivot.model_id = 'some_id'
                     AND logs_pivot.model_type = 'My\Class'
                   ORDER BY logs.date DESC
                   LIMIT 50

这会完成计算所需行的所有繁重工作。所以,这是您需要优化的查询。

logs

可以通过这个索引加速
CREATE INDEX logs_date_desc ON logs (date DESC);

logs_pivot

上的 three-column 复合索引
CREATE INDEX logs_pivot_lookup ON logs_pivot (model_id, model_type, log_id);

这个索引可能会更好,因为优化器会在 logs_pivot 而不是 logs 上看到过滤。因此,它将首先查找 logs_pivot

或者也许

CREATE INDEX logs_pivot_lookup ON logs_pivot (log_id, model_id, model_type);

先试一试,看看哪个产生的结果更快。 (我不确定 JOIN 将如何使用复合索引。)(或者简单地添加两者,然后使用 EXPLAIN 来查看它使用了哪一个。)

然后,当您对子查询的性能感到满意或满意时,使用它来获取您需要的行,就像这样

SELECT * 
  FROM logs
  WHERE id IN (
                  SELECT id
                    FROM logs
                    JOIN logs_pivot 
                            ON logs.id = logs_pivot.log_id
                   WHERE logs_pivot.model_id = 'some_id'
                     AND model_type = 'My\Class'
                   ORDER BY logs.date DESC
                   LIMIT 50
              )
  ORDER BY date DESC

之所以有效,是因为它对较少的数据进行了排序。 logs_pivot 上的覆盖 three-column 索引也有帮助。

注意子查询和主查询都有ORDER BY子句,确保返回的详细结果集是你需要的顺序。

Edit Darnit,一直在使用 MariaDB 10+ 和 MySQL 8+,我忘记了旧的限制。试试这个。

SELECT * 
  FROM logs
  JOIN (
                  SELECT id
                    FROM logs
                    JOIN logs_pivot 
                            ON logs.id = logs_pivot.log_id
                   WHERE logs_pivot.model_id = 'some_id'
                     AND model_type = 'My\Class'
                   ORDER BY logs.date DESC
                   LIMIT 50
        ) id_set ON logs.id = id_set.id
  ORDER BY date DESC

最后,如果您知道自己只关心比某个特定时间更新的行,则可以将类似的内容添加到您的子查询中。

                  AND logs.date >= NOW() - INTERVAL 5 DAY

如果您的 table 中有大量历史数据,这将有很大帮助。