我正在开发一个拥有大型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;
但是,众所周知,MySQL杀死了你的服务器的RAND()
功能,我一直在寻找更好的解决方案。
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 | |
+----------------+------------------------------------------+------+-----+---------+----------------+
优化查询以减少服务器负载和执行时间,我们将不胜感激。
P.S。系统也具有删除功能,因此QUESTION和TEST表的AUTO_INCREMENT PRIMARY KEY可能有很大的间隙。
答案 0 :(得分:2)
我喜欢这个问题。这是一个非常好的优化难题,我们暂时假设性能对于此查询非常重要,并且您不能使用任何动态插入的值(例如来自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值的表中早期的行将受到青睐并且在结果中不成比例地显示。 (想象一下,如果第一行足够幸运,可以获得.95的兰德值)
更好的方法是创建一个具有连续整数的列,此列上的索引,然后随机选择一个插入点以获取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)
无论如何,我希望这会让你的想象力有所增加。
答案 1 :(得分:1)
为什么不用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
答案 2 :(得分:1)
创建以下索引:
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);
答案 3 :(得分:0)
前一段时间我对同一问题进行了调查,我的第一种方法是先加载所有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()
不是问题,而是将它与所有表连接一起使用 Magento的?如果我使用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),但复制需要时间。在Jan Kneschke撰写的MySQL中选择随机行时还有另一个famous blog post。他建议使用一个包含所有ID的索引表,它有自己的主键无间隙。此索引表将使用触发器自动更新,索引表可以使用min(key)和max(key)之间的随机键选择随机行。
如果您不使用任何其他条件并查询所有问题的随机条目,这应该对您有用。