我怎样才能优化MySQL的ORDER BY RAND()函数?

我想优化我的查询,所以我看着mysql-slow.log

我的大部分慢查询都包含ORDER BY RAND() 。 我无法find解决此问题的真正解决scheme。 Theres是MySQLPerformanceBlog中的一个可能的解决scheme,但我不认为这是足够的。 在优化不佳(或频繁更新,用户pipe理)的表,它不工作,或者我需要运行两个或更多的查询,然后才能select我的PHP生成的随机行。

有没有解决这个问题?

一个虚拟的例子:

 SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, accomodation_category WHERE accomodation.ac_status != 'draft' AND accomodation.ac_category = accomodation_category.acat_id AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' ORDER BY RAND() LIMIT 1 

尝试这个:

 SELECT * FROM ( SELECT @cnt := COUNT(*) + 1, @lim := 10 FROM t_random ) vars STRAIGHT_JOIN ( SELECT r.*, @lim := @lim - 1 FROM t_random r WHERE (@cnt := @cnt - 1) AND RAND(20090301) < @lim / @cnt ) i 

这在MyISAM上特别有效(因为COUNT(*)是即时的),但即使在InnoDB它的效率也比ORDER BY RAND()10倍。

这里的主要思想是我们不sorting,而是保留两个variables,并计算当前步骤中要select的行的running probability

有关更多详细信息,请参阅我的博客中的文章

  • select随机行

更新:

如果您需要select一个随机logging,请尝试以下操作:

 SELECT aco.* FROM ( SELECT minid + FLOOR((maxid - minid) * RAND()) AS randid FROM ( SELECT MAX(ac_id) AS maxid, MIN(ac_id) AS minid FROM accomodation ) q ) q2 JOIN accomodation aco ON aco.ac_id = COALESCE ( ( SELECT accomodation.ac_id FROM accomodation WHERE ac_id > randid AND ac_status != 'draft' AND ac_images != 'b:0;' AND NOT EXISTS ( SELECT NULL FROM accomodation_category WHERE acat_id = ac_category AND acat_slug = 'vendeglatohely' ) ORDER BY ac_id LIMIT 1 ), ( SELECT accomodation.ac_id FROM accomodation WHERE ac_status != 'draft' AND ac_images != 'b:0;' AND NOT EXISTS ( SELECT NULL FROM accomodation_category WHERE acat_id = ac_category AND acat_slug = 'vendeglatohely' ) ORDER BY ac_id LIMIT 1 ) ) 

这假设你的ac_id分布或多或less均匀。

这取决于你需要如何随机。 你链接的解决scheme非常好IMO。 除非你在ID领域有很大的差距,它还是非常随机的。

但是,您应该可以在一个查询中使用它(用于select单个值):

 SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*MAX(id)) LIMIT 1 

其他解决scheme

  • 将一个名为random的永久浮动字段添加到表中,并用随机数填充。 然后你可以在PHP中生成一个随机数,然后执行"SELECT ... WHERE rnd > $random"
  • 抓住整个ID列表,并caching在一个文本文件。 阅读文件并从中select一个随机ID。
  • 将查询结果caching为HTML并保留几个小时。

以下是我如何做到这一点:

 SET @r := (SELECT ROUND(RAND() * (SELECT COUNT(*) FROM accomodation a JOIN accomodation_category c ON (a.ac_category = c.acat_id) WHERE a.ac_status != 'draft' AND c.acat_slug != 'vendeglatohely' AND a.ac_images != 'b:0;'; SET @sql := CONCAT(' SELECT a.ac_id, a.ac_status, a.ac_name, a.ac_status, a.ac_images FROM accomodation a JOIN accomodation_category c ON (a.ac_category = c.acat_id) WHERE a.ac_status != ''draft'' AND c.acat_slug != ''vendeglatohely'' AND a.ac_images != ''b:0;'' LIMIT ', @r, ', 1'); PREPARE stmt1 FROM @sql; EXECUTE stmt1; 

这会给你一个单一的子查询,将使用索引来获得一个随机的id然后另一个查询将触发获取您的连接表。

 SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, accomodation_category WHERE accomodation.ac_status != 'draft' AND accomodation.ac_category = accomodation_category.acat_id AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' AND accomodation.ac_id IS IN ( SELECT accomodation.ac_id FROM accomodation ORDER BY RAND() LIMIT 1 ) 

你的虚拟示例的解决scheme将是:

 SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, JOIN accomodation_category ON accomodation.ac_category = accomodation_category.acat_id JOIN ( SELECT CEIL(RAND()*(SELECT MAX(ac_id) FROM accomodation)) AS ac_id ) AS Choices USING (ac_id) WHERE accomodation.ac_id >= Choices.ac_id AND accomodation.ac_status != 'draft' AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' LIMIT 1 

要阅读更多关于ORDER BY RAND()替代方法,你应该阅读这篇文章 。

我在我的项目中优化了很多现有的查询。 Quassnoi的解决scheme帮助我加快了查询速度! 但是,我发现很难在所有查询中合并所述解决scheme,特别是对于涉及多个大型表上的许多子查询的复杂查询。

所以我正在使用一个不太优化的解决scheme 从根本上说,它和Quassnoi的解决scheme一样。

 SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, accomodation_category WHERE accomodation.ac_status != 'draft' AND accomodation.ac_category = accomodation_category.acat_id AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' AND rand() <= $size * $factor / [accomodation_table_row_count] LIMIT $size 

$size * $factor / [accomodation_table_row_count]出挑选一个随机行的概率。 rand()将生成一个随机数。 如果rand()小于或等于概率,则该行将被选中。 这有效地执行随机select来限制表的大小。 由于有可能返回小于定义的限制数量,我们需要增加概率以确保select足够的行数。 因此,我们将$ size乘以一个$因子(我通常设置$ factor = 2,在大多数情况下都适用)。 最后我们做limit $size

现在的问题是解决accomodation_table_row_count 。 如果我们知道表的大小,我们可以硬编码表的大小。 这会跑得最快,但显然这并不理想。 如果你正在使用Myisam,获得餐桌计数是非常有效的。 由于我使用innodb,我只是做一个简单的计数+select。 在你的情况下,它看起来像这样:

 SELECT accomodation.ac_id, accomodation.ac_status, accomodation.ac_name, accomodation.ac_status, accomodation.ac_images FROM accomodation, accomodation_category WHERE accomodation.ac_status != 'draft' AND accomodation.ac_category = accomodation_category.acat_id AND accomodation_category.acat_slug != 'vendeglatohely' AND ac_images != 'b:0;' AND rand() <= $size * $factor / (select (SELECT count(*) FROM `accomodation`) * (SELECT count(*) FROM `accomodation_category`)) LIMIT $size 

棘手的部分是正确的概率。 正如你可以看到下面的代码实际上只计算粗糙的临时表大小(实际上,太粗糙!):( (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))但是,这个逻辑给出一个更接近的表大小的近似值。 请注意,OVER-select比下select行更好。 即如果概率设置得太低,则可能会导致未select足够的行。

这个解决scheme比Quassnoi的解决scheme运行得慢,因为我们需要重新计算表的大小。 不过,我觉得这个编码更易于pipe理。 这是精度+性能编码复杂度之间的折衷。 话虽如此,在大型桌面上,这仍然比Rand()的Order快得多。

注意:如果查询逻辑允许,在任何联接操作之前尽可能早地执行随机select。

(是的,我会因为这里没有足够的肉而遭到殴打,但是有一天你不能成为素食主义者吗?)

案例:连续AUTO_INCREMENT无间隙,返回1行
案例:连续AUTO_INCREMENT没有空白,10行
案例:AUTO_INCREMENT与空白,1行返回
案例:额外FLOAT列随机
案例:UUID或MD5列

对于大型桌子,这5种情况可以非常有效。 看到我的博客的细节。

 function getRandomRow(){ $id = rand(0,NUM_OF_ROWS_OR_CLOSE_TO_IT); $res = getRowById($id); if(!empty($res)) return $res; return getRandomRow(); } //rowid is a key on table function getRowById($rowid=false){ return db select from table where rowid = $rowid; }