【问题标题】:How to echo random rows from database?如何从数据库中回显随机行?
【发布时间】:2016-03-04 21:11:08
【问题描述】:

我有一个包含大约 1.6 亿行的数据库表。

该表有两列:idlisting

我只需要使用 PHP 显示来自listing 列的 1000 个随机行并将它们放入<span> 标记中。像这样:

<span>Row 1</span>
<span>Row 2</span>
<span>Row 3</span>

我一直在尝试使用ORDER BY RAND() 来做这件事,但是在如此大的数据库上加载需要很长时间,而且我无法找到任何其他解决方案。

我希望有一种快速/简单的方法可以做到这一点。我无法想象简单地回显 1000 个随机行是不可能的……谢谢!

【问题讨论】:

  • 看起来它可能有效,我只需要弄清楚如何在 span 标签中回显这些结果。
  • 附注:1000 行的 UI 很笨拙。 (不,减少 1000 不会帮助查询。)如果每一行都是磁盘命中,那么 10 秒获取 1000 行是用户痛苦的一部分。
  • 什么是innodb_buffer_pool_size?多少内存?你的桌子合适吗?这些导致任务是 CPU 密集型还是 IO 密集型,差别很大。
  • 高效的ways 从大表中获取一个或多个随机行——无需进行表扫描。

标签: php mysql


【解决方案1】:

你想在 php.ini 中使用rand 函数。签名是

rand(min, max);

因此,将表中的行数设置为 $var 并将其设置为您的 max。 使用 SQL 的一种方法是

SELECT COUNT(*) FROM table_name;

然后简单地运行一个循环以使用上述函数生成 1000 rands 并使用它们来获取特定的行。

如果 ID 不是连续的,但如果它们很接近,您可以简单地测试每个 rand ID 以查看是否有命中。如果它们相距很远,您可以将整个 ID 空间拉入 php,然后通过类似的方式从该分布中随机抽样

$random = rand(0, count($rows)-1);

$rows 中的一组 ID。

【讨论】:

  • 只有当表中的 ID 是连续的时才会起作用。
  • SELECT COUNT(*)... 几乎和随机选取 1000 行一样慢。如果 id 没有间隙,请改用 SELECT MAX(id) ...
【解决方案2】:

如果您的 RAND() 函数太慢,并且您只需要准随机记录(用于测试样本)而不是真正随机的记录,您总是可以通过按中间字符排序来创建一个快速、有效的随机组 (在索引字段中使用 SUBSTRING)。例如,按电话号码的第 7 位排序……按降序排列……然后按第 6 位……按升序排列……这已经是准随机的了。您可以对字符列执行相同操作:人名中的第 6 个字符将是无意义的/随机的,等等。

【讨论】:

  • RAND 的速度与获取 160M 行所需的时间相比微不足道。
【解决方案3】:

ORDER BY RAND() 是一个 mysql 函数,适用于小型数据库,但如果您运行大于 10k 行的任何内容,则应在程序中构建函数,而不是使用 mysql 预制函数或以特殊方式组织数据。

我的建议:通过自动增量id 保持你的mysql数据索引,或者添加其他增量和唯一行。

然后构建一个选择函数:

<?php
//get total number of rows
$result = mysql_query('SELECT `id` FROM `table_name`', $link); 
$num_rows = mysql_num_rows($result); 

$randomlySelected = [];

for( $a = 0; $a < 1000; $a ++ ){

        $randomlySelected[$a] = rand(1,$num_rows);

}

//then select data by random ids
$where = "";

$control = 0;
foreach($randomlySelected as $key => $selectedID){

    if($control == 0){

        $where .= "`id` = '". $selectedID ."' ";

    } else {

        $where .= "OR `id` = '". $selectedID ."'";

    }
    $control ++;
}


$final_query = "SELECT * FROM `table_name` WHERE ". $where .";";
$final_results = mysql_query($final_query);    

?>

如果您在 1.6 亿个数据库中缺少一些增量 ID,那么如果随机选择的 id 数组包含的数量少于所需的数量,您可以轻松添加一个函数来添加另一个随机 ID(可能是一个 while 循环)。

如果您需要更多帮助,请告诉我。

【讨论】:

  • 如果 id 中有 很多 的间隙,这种方法就会变得笨拙。请注意,对表的每个探测都可能是磁盘命中。在 HDD 上,查找 1000 行大约需要 10 秒;如果需要处理差距,则更多。
【解决方案4】:

这里介绍了两种解决方案。这两个提议的解决方案都是 mysql-only,并且可以被任何编程语言作为消费者使用。 PHP 在这方面太慢了,但它可能是它的消费者。

更快的解决方案:我可以使用更高级的编程技术在大约十分之二秒内从 1900 万行的表中提取 1000 行。

较慢的解决方案:使用非功耗编程技术大约需要 15 秒。

顺便说一句,两者都使用我写的HERE 看到的数据生成。这就是我的小模式。我使用它,继续在那边看到 两个 自插入,直到我有 19M 行。所以我不会再展示了。但是要获得这 1900 万行,去看看,再做 2 次这样的插入,你就有了 1900 万行。

较慢的版本优先

首先,较慢的方法。

select id,thing from ratings order by rand() limit 1000;

这会在 15 秒内返回 1000 行。


对于刚接触 mysql 的任何人,甚至不要阅读以下内容。

更快的解决方案

这描述起来有点复杂。它的要点是您预先计算随机数并生成随机数的in clause 结尾,用逗号分隔,并用一对括号括起来。

它看起来像(1,2,3,4),但其中包含 1000 个数字。

然后您存储它们,并使用它们一次。就像密码学的一次性便笺簿。好吧,这不是一个很好的类比,但我希望你明白了。

将其视为 in 子句的结尾,并存储在 TEXT 列中(如 blob)。

为什么要这样做呢?因为 RNG(随机数生成器)速度非常慢。但是用几台机器生成它们可能能够相对快速地生产出数千个。顺便说一句(你会在我所谓的附录的结构中看到这一点,我记录了生成一行需要多长时间。使用 mysql 大约需要 1 秒。但是 C#、PHP、Java,任何东西都可以把它放在一起。重点不是你如何将它组合在一起,而是你想要它时拥有它。

这种策略,总而言之,当它与获取未用作随机列表的行相结合时,将其标记为已使用,并发出诸如

之类的调用
select id,thing from ratings where id in (a,b,c,d,e, ... )

in 子句中有 1000 个数字,不到半秒就可以得到结果。 有效地使用 mysql CBO(基于成本的优化器)而不是将其视为 PK 上的连接索引。

我将其保留为摘要形式,因为它在实践中有点复杂,但可能包含以下粒子

  • 包含预先计算的随机数的表格(附录 A)
  • 一个mysql创建事件策略(附录B)
  • 使用 Prepared Statement(附录 C)的存储过程
  • 一个 mysql-only 存储过程,用于演示 RNG in 子句(附录 D)

附录 A

保存预先计算的随机数的表

create table randomsToUse
(   -- create a table of 1000 random numbers to use
    -- format will be like a long "(a,b,c,d,e, ...)" string

    -- pre-computed random numbers, fetched upon needed for use

    id int auto_increment primary key,
    used int not null,  -- 0 = not used yet, 1= used
    dtStartCreate datetime not null, -- next two lines to eyeball time spent generating this row
    dtEndCreate datetime not null,
    dtUsed datetime null, -- when was it used
    txtInString text not null -- here is your in clause ending like (a,b,c,d,e, ... )
    -- this may only have about 5000 rows and garbage cleaned
    -- so maybe choose one or two more indexes, such as composites
);

附录 B

为了不把它变成一本书,请参阅我的回答HERE,了解运行重复性 mysql 事件的机制。它将使用附录 D 中介绍的技术和您想要构想的其他想法来推动附录 A 中的表格的维护。例如重复使用行、归档、删除等等。

附录 C

存储过程让我简单地获得 1000 个随机行。

DROP PROCEDURE if exists showARandomChunk;
DELIMITER $$
CREATE PROCEDURE showARandomChunk
(
)
BEGIN
  DECLARE i int;
  DECLARE txtInClause text;

  -- select now() into dtBegin;

  select id,txtInString into i,txtInClause from randomsToUse where used=0 order by id limit 1;
  -- select txtInClause as sOut; -- used for debugging

  -- if I run this following statement, it is 19.9 seconds on my Dell laptop
  -- with 19M rows
  -- select * from ratings order by rand() limit 1000; -- 19 seconds

  -- however, if I run the following "Prepared Statement", if takes 2 tenths of a second
  -- for 1000 rows

  set @s1=concat("select * from ratings where id in ",txtInClause);

  PREPARE stmt1 FROM @s1;
  EXECUTE stmt1; -- execute the puppy and give me 1000 rows
  DEALLOCATE PREPARE stmt1;
END
$$
DELIMITER ;

附录 D

可以与附录 B 概念交织在一起。但是,您想这样做。但它让您了解 mysql 如何在 RNG 方面独自完成所有工作。顺便说一句,参数1和2分别为1000和19M,在我的机器上需要800毫秒。

这个例程可以用开头提到的任何语言编写。

drop procedure if exists createARandomInString;
DELIMITER $$
create procedure createARandomInString
(   nHowMany int, -- how many numbers to you want
    nMaxNum int -- max of any one number
)
BEGIN
    DECLARE dtBegin datetime;
    DECLARE dtEnd datetime;
    DECLARE i int;
    DECLARE txtInClause text;
    select now() into dtBegin;

    set i=1;
    set txtInClause="(";
    WHILE i<nHowMany DO
        set txtInClause=concat(txtInClause,floor(rand()*nMaxNum)+1,", "); -- extra space good due to viewing in text editor
        set i=i+1;
    END WHILE;
    set txtInClause=concat(txtInClause,floor(rand()*nMaxNum)+1,")");
    -- select txtInClause as myOutput; -- used for debugging
    select now() into dtEnd;

    -- insert a row, that has not been used yet
    insert randomsToUse(used,dtStartCreate,dtEndCreate,dtUsed,txtInString) values 
       (0,dtBegin,dtEnd,null,txtInClause);
END
$$
DELIMITER ;

如何调用上述存储过程:

call createARandomInString(1000,18000000);

生成并保存 1 行,包含如上所述的 1000 个数字。大数字,1 到 18M

作为一个简单的说明,如果要修改存储过程,请取消显示“用于调试”的底部附近的行,并将其作为最后一行,在运行的存储过程中,然后运行这个:

call createARandomInString(4,18000000);

...生成 4 个最大 18M 的随机数,结果可能如下所示

+-------------------------------------+
| myOutput                            |
+-------------------------------------+
| (2857561,5076608,16810360,14821977) |
+-------------------------------------+

附录 E

现实检查。这些都是一些先进的技术,我不能辅导任何人。但我还是想分享它们。但是我教不了。一遍又一遍。

【讨论】:

  • “较慢的版本”涉及全表扫描; OP 的数据可能需要几 分钟
  • 如果没有缓存的行或非常快的 SSD,“更快的版本”无法在 0.5 秒内获得一千行。 (我对技术没问题,但我质疑时间。)
  • 我将不得不重新设置并再次测试。我想我得到了 1.9 秒
  • 我喜欢这样测试:FLUSH STATUS; SELECT ...; SHOW SESSION STATUS LIKE 'Handler%'; 如果你看到像表大小(19M)这样的数字,你就知道某处有表扫描。如果您只看到“1000”数量级的数字,您就知道努力仅限于结果集的大小(好),而不是表大小(坏)。
【解决方案5】:

请在选择语句期间在查询中使用 mysql rand。您的查询将如下所示

SELECT * FROM `table` ORDER BY RAND() LIMIT 0,1;

【讨论】:

  • 只有 1 行; OP 想要 1000。并且该查询仍然进行全表扫描; 1.6 亿行不酷!
猜你喜欢
  • 2014-06-02
  • 2014-02-01
  • 1970-01-01
  • 2021-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多