这个问题有点意思。
按照我的理解,“从10万条数据里随机取3000条用于数据分析”这个需求,关键是取样的均匀性。也就是说,不能简单地连续取3000条数据,而是要让取样尽量覆盖整个数据区间。但是对于均匀性来说,随机取样并不是必要的,或者更严格地说,随机取样其实不能完全满足均匀性的要求,因为在极端情况下,随机取样仍然可能集中在区间的某一部分。简单的均匀取样的方式就是从区间m中均匀地取n个样本,也就是每m/n条记录取一条。
但是,更重要的问题是ID是不连续的。也就是说,无法简单地通过n/m的步长来事先计算id以取得数据。也就是说,这个问题归结为“在倾斜数据集中进行均匀取样”的算法。
在这种情况下,最直观的方法是使用临时表先将ID变成连续的。楼上Rio提出的算法:
取出当日更新的10万 id放入一个数组
在数组中随机取出3000个id
用select in读取指定的3000条记录
就是“通过临时表先将ID变成连续”的解决方式。另外几位提出了进一步的优化,即在每次更新原表时,就将ID先存入一个临时表或数组里,避免了在读取数据时再创建临时表,但算法本质上是一样的。
这个算法最大的开销是要建立一个临时表。假设当日更新有记录有m条,需要随时选取n条,那么成本包括:
读取原表中所有当日更新的id O(m)
把所有这些id写入临时表
O(m)
在临时表中随机选取n个id O(n)
在原表中读取n条记录
O(n)
一共是O(m)*2 + O(n)*2,其中最大的成本是两个O(m)。一般来说,取样数量n比较小,但是区间m可能会非常大。如果m不是10万条,而是10亿条甚至更多呢?O(m)*2就会大得让人吃不消了。
有没有办法只对m读取一次呢?有。如果我们知道m和n,就可以在遍历m的时候,直接按照n/m的步长取值。
实现如下:创建一个store procedure。首先通过select count(*)取得所有当日更新记录的数量(m,假设为10万)。已知n为3000,所以步长为100000/3000=33。在select where date_refresh = 20120329上打开一个cursor,往下fetch,每33条记录中返回一条。比较郁闷的是,mysql的cursor不支持跳过记录,所以必须逐行fetch。当cursor结束时,取样即完成。
这里有一个小变化。如果你实在不满意每隔m/n条取一条的方式,坚持要随机取样,那么可以在每个m/n条记录的区间进行随机取样,也就是说,每次返回记录时,按rand()*33决定返回的记录。随机函数的调用次数为n次而不是m次,所以是可以接受的。
但是且慢。当通过select count(*)取得所有当日更新记录的数量,仍然需要扫描所有当日更新的id。当然,由于它不需要返回m个id,而只返回一个数字,运行速度肯定比select id要快得多。但是算法复杂度仍然为O(m)。
一个优化方法是,设置一个内存计数器,在每次更新原表记录时,将计数器加1,最终可以得到所有当日更新记录的数量。这看起来,跟“更新原表时将ID先存入一个临时表或数组里”的方法差不多,实际性能差别极大。因为对整数进行单调递增,是一个CPU原子操作。既不需要建立临时表,也不需要建立数组,更不用处理锁和并行的问题,简单地执行一条CPU指令就可以了。
到这里,可以看出新算法在性能上提高了很多,但是看来我们还是没有完全避免O(m)*2。有没有办法达到O(m)?有没有办法在事先不知道m的情况下进行取样?甚至进一步,连O(m)都避免,只是简单地读取n条记录,完全无视没有落在取样范围以外的其它记录?
回答是有。这要用到一种特殊的可变步长采样算法,而且可能无法用sql实现,需要hack mysql的内核。如果有机会我会贴一篇文章,暂时就不多说了。