【问题标题】:Optimize a JOIN ORDER BY RAND() mysql query in a large database在大型数据库中优化 JOIN ORDER BY RAND() mysql 查询
【发布时间】:2016-12-27 13:20:42
【问题描述】:

我正在从事一个拥有大型 Question 银行的项目,对于系统的 Tests added,根据以下查询在运行时动态获取 20 个问题:

SELECT Question.* from Question JOIN Test 
ON Question.Subject_ID = Test.Subject_ID 
AND Question.Question_Level = Test.Test_Level 
ORDER BY RAND() 
LIMIT 20;

不过,众所周知,RAND() 函数 MySQL 会杀死你的服务器,我一直在寻找更好的解决方案。

结果 EXPLAIN [above query]:

+----+-------------+----------+------+---------------+------+---------+------+------+----------------------------------------------------+
| id | select_type | table    | type | possible_keys | key  | key_len | ref  | rows | Extra                                              |
+----+-------------+----------+------+---------------+------+---------+------+------+----------------------------------------------------+
|  1 | SIMPLE      | Test     | ALL  | NULL          | NULL | NULL    | NULL |    5 | Using temporary; Using filesort                    |
|  1 | SIMPLE      | Question | ALL  | NULL          | NULL | NULL    | NULL |    7 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+----------+------+---------------+------+---------+------+------+----------------------------------------------------+

结果 EXPLAIN Question:

+-------------------+------------------------------------------+------+-----+---------+----------------+
| Field             | Type                                     | Null | Key | Default | Extra          |
+-------------------+------------------------------------------+------+-----+---------+----------------+
| Question_ID       | int(11)                                  | NO   | PRI | NULL    | auto_increment |
| Questions         | varchar(100)                             | NO   |     | NULL    |                |
| Available_Options | varchar(200)                             | NO   |     | NULL    |                |
| Correct_Answer    | varchar(50)                              | NO   |     | NULL    |                |
| Subject_ID        | int(11)                                  | NO   |     | NULL    |                |
| Question_Level    | enum('Beginner','Intermediate','Expert') | NO   |     | NULL    |                |
| Created_By        | int(11)                                  | NO   |     | NULL    |                |
+-------------------+------------------------------------------+------+-----+---------+----------------+

结果 EXPLAIN Test:

+----------------+------------------------------------------+------+-----+---------+----------------+
| Field          | Type                                     | Null | Key | Default | Extra          |
+----------------+------------------------------------------+------+-----+---------+----------------+
| Test_ID        | int(11)                                  | NO   | PRI | NULL    | auto_increment |
| Test_Name      | varchar(50)                              | NO   |     | NULL    |                |
| Test_Level     | enum('Beginner','Intermediate','Expert') | NO   |     | NULL    |                |
| Subject_ID     | int(11)                                  | NO   |     | NULL    |                |
| Question_Count | int(11)                                  | NO   |     | NULL    |                |
| Created_By     | int(11)                                  | NO   |     | NULL    |                |
+----------------+------------------------------------------+------+-----+---------+----------------+

如果能帮助优化查询以减少服务器负载和执行时间,我们将不胜感激。

附注系统也有删除能力,所以QUESTION和TEST表的AUTO_INCREMENT PRIMARY KEY可以有很大的差距。

【问题讨论】:

  • 显示EXPLAIN的结果
  • 添加了EXPLAIN结果
  • 您有关于 Question.Subject_ID、Test.Subject_ID、Question.Question_Level、Test.Test_Level 的索引吗?
  • 不,我正在加入非关键列,因为这是系统要求的。
  • 您必须为这些列添加索引以进行快速连接

标签: php mysql random


【解决方案1】:

我喜欢这个问题。这是一个非常好的优化难题,我们暂时假设性能对于这个查询非常重要,并且您不能使用任何动态插入的值(例如来自 PHP)。

一种高性能解决方案是添加具有随机值的列(例如称为“Rand”),按此值对表进行排序,并定期重新生成和重新排序表。然后,您可以使用这样的查询:

SELECT Question.* from Question 
JOIN Test 
ON Question.Subject_ID = Test.Subject_ID 
AND Question.Question_Level = Test.Test_Level  
WHERE Question.Rand > RAND() 
LIMIT 20

这将在 O(n) 时执行,只需要对表进行一次扫描,但如果生成非常接近 1 的值,则会有返回少于 20 个结果的风险。如果这是一个可接受的风险(例如,您可以通过编程方式检查不适当的结果并重新查询),您最终会获得良好的运行时性能。

数字的定期重新生成和重新排序是必要的,因为表中具有高 Rand 值的行将受到青睐,并且在结果中不成比例地频繁出现。 (想象一下,如果第一行有幸获得 0.95 的 Rand 值)

更好的办法是创建一个具有连续整数的列,在该列上建立索引,然后随机选择一个插入点来抓取 20 个结果。这样的查询可能如下所示:

SELECT Question.* from Question 
JOIN Test 
ON Question.Subject_ID = Test.Subject_ID 
AND Question.Question_Level = Test.Test_Level  
CROSS JOIN (SELECT MAX(Rand_id) AS max_id FROM Question)
WHERE Question.Rand_Id > ROUND(RAND() * max_id)
LIMIT 20

但是,如果您无法以任何方式更改您的表格怎么办?如果你的 SQL 有多混乱并不重要,并且丢失的 id 的比例相对较低(比如大约 1/10)。您可以使用以下 SQL 以很高的概率完成 20 个随机问题:

SELECT Question.* from Question JOIN Test 
  ON Question.Subject_ID = Test.Subject_ID 
  AND Question.Question_Level = Test.Test_Level 
  WHERE Question.Question_ID IN (
    SELECT DISTINCT(ROUND(rand * max_id)) AS rand_id 
    FROM ( --generate 30 random numbers to make sure we get 20 results
      SELECT RAND() AS rand UNION ALL
      SELECT RAND() AS rand UNION ALL
      SELECT RAND() AS rand UNION ALL
      SELECT RAND() AS rand UNION ALL
      ...
      SELECT RAND() AS rand UNION ALL
      SELECT RAND() AS rand UNION ALL
      SELECT RAND() AS rand
    ) a 
    CROSS JOIN ( --get the max possible id from the Question table
      SELECT MAX(id) AS max_id FROM Question
    ) b
  )
LIMIT 20 --finally pare our results down to 20 in case we got too many

但是,这会在您的用例中引起问题,因为您实际上无法知道联接后结果集中将有多少结果(及其 ID)。加入主题和难度后,遗漏 ID 的比例可能非常高,最终您可能得到的结果远少于 20 个,即使随机猜测数百个 ID 可能在一个表中。

如果您能够使用 PHP 中的逻辑(听起来很像),就会出现许多高性能解决方案。例如,您可以在 PHP 中创建一个对象,其工作是存储具有特定主题和难度级别的所有问题 ID 的数组。然后,您可以选择 20 个随机数组索引并取回 20 个有效 ID,从而允许您运行非常简单的查询。

SELECT Question.* from Question WHERE Question_ID IN ($dynamically_inserted_ids)

无论如何,我希望这能激发您的想象力。

【讨论】:

    【解决方案2】:

    为什么不在 PHP 中对数字进行排序,然后按 id 选择问题? 这是我的观点的逻辑:

    $MIN       = 1;
    $MAX       = 50000; // You may want to get the MAX from your database
    $questions = '';
    
    for($i = 0; $i < 20; $i++)
       $questions .= mt_rand($MIN, $MAX) . ',';
    
    // Removes last comma
    $questions = rtrim($questions, ',');
    
    $query = "SELECT * FROM Question WHERE Question.id IN ($questions)";
    

    编辑 1:

    我正在考虑这个问题,我发现您可以从您的数据库中选择所有 ID,然后使用 array_rand() 函数选择 20 个项目。

    $values    = array(1, 5, 10000, 102021, 1000000); // Your database ID's
    $questions = array_rand($values, 20);
    
    $questions[0];
    $questions[1];
    $questions[2]; // etc
    

    【讨论】:

    • 因为,正如我所提到的,我的问题 ID 会有空白。因此,如果 PHP 选择了 ID 2,它甚至可能不在我的数据库中。
    • 如何将您的问题 id 返回到一个简单的数组中,然后在循环中验证该值是否存在于数组中?
    • 因为,我将不得不在拥有超过一百万条记录的数据库上运行 SELECT Question_ID from Question 查询。
    • 更不用说 PHP 中 in_array() 函数的执行时间增加了,因为我是一个包含 100 万个问题 ID 的数组,对于 20 个问题,它必须遍历数组 20*1M = 20百万次
    【解决方案3】:

    创建以下索引:

    CREATE INDEX Question_Subject_ID_idx ON Question (Subject_ID);
    CREATE INDEX Test_Subject_ID_idx ON Test (Subject_ID);
    CREATE INDEX Question_Question_Level_idx ON Question (Question_Level);
    CREATE INDEX Test_Test_Level_idx ON Test (Test_Level);
    

    【讨论】:

      【解决方案4】:

      我不久前调查过同样的问题,我的第一种方法是先加载所有 ID,在 PHP 中随机选择 ID(请参阅:Efficiently pick n random elements from PHP array (without shuffle)),然后直接在 MySQL 中查询这些 ID。

      这是一项改进,但对于大型数据集来说会消耗内存。经过进一步调查,我发现了一种更好的方法:在一个查询中选择随机 ID,而不使用任何其他字段或 JOIN,然后通过这些 ID 进行真正的查询:

      SELECT Question.* from Question JOIN Test 
      ON Question.Subject_ID = Test.Subject_ID 
      AND Question.Question_Level = Test.Test_Level 
      WHERE Question_ID IN (
          SELECT Question_ID from Question
          ORDER BY RAND() 
          LIMIT 20
      );
      

      这是一篇博客文章,其中包含针对我的具体案例的基准:Show random products in Magento

      相关部分:

      除了内存问题,会不会是 ORDER BY RAND() 本身 不是问题,而是将它与所有表连接一起使用 麦金托?如果我预选带有ORDER BY RAND() 的随机ID 会怎样?

      [...]

      它比 PHP 预选方法稍慢,但仍明显支持按 rand 的纯排序,并且不会增加 PHP 中的内存使用量。

      [...]

      ORDER BY RAND() 的纯 MySQL 方法的问题变得更加明显。在使用mytop 监控 MySQL 时,我注意到除了 排序 之外,还有很多时间用于 复制。这里的问题似乎是,没有索引的排序,就像ORDER BY RAND() 一样,将数据复制到一个临时表并对其进行排序。使用平面索引,所有产品属性都从单个表中获取,这增加了复制到临时表和从临时表中复制以进行排序的数据量。我可能在这里遗漏了其他东西,但性能从糟糕下降到可怕,甚至导致我的 Vagrantbox 在第一次尝试时崩溃,因为它的磁盘已满(40 GB)。因此,虽然 PHP 使用这种方法使用更少的内存,但 MySQL 更需要资源。

      我不知道您的问题表有多大,在某些时候这种方法仍然存在缺陷:

      其次,如上所述,对于大型目录,您应该寻找不同的东西。 ORDER BY RAND() 的问题在于,即使我们最小化了要复制的数据,它仍然会将所有行复制到一个临时表并为每个行生成一个随机数。排序本身经过优化,不会对所有行进行排序(请参阅LIMIT Optimization),但复制需要时间。

      另一个famous blog post 是由 Jan Kneschke 编写的关于在 MySQL 中选择随机行的内容。他建议使用具有所有 id 的索引表,它有自己的主键没有间隙。该索引表将通过触发器自动更新,并且索引表可以使用 min(key) 和 max(key) 之间的随机键选择随机行。

      如果您不使用任何附加条件并从所有问题中查询随机条目,这应该适合您。

      【讨论】:

        猜你喜欢
        • 2012-06-12
        • 1970-01-01
        • 1970-01-01
        • 2012-12-28
        • 1970-01-01
        • 2022-01-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多