根据排名在Oracle中生成随机数据

Generate random data in Oracle based on ranks

给出以下场景。我有一个包含 3000 个名字的列表和一个包含 2500 个姓氏的列表。他们每个人都有一个 "ranking" 代表名字顶部的位置。两个或多个名称可以具有相同的排名。此外,还给出了一个包含 1500 个城市的 table,每个城市在某些年份有 4 个人口普查值。

从上面的 tables 中,我必须生成 500 万个随机条目,其中包含一个人的名字、姓氏、出生日期和出生地点,这些条目应遵循姓名排名给出的规则和城市人口数。

这必须仅使用 Oracle(存储函数、存储过程等)生成。我怎样才能做到这一点?

免责声明:我不是统计专家,可能有更有效的方法来做到这一点。

最具挑战性的任务似乎是根据等级创建500万个名字。在现实世界中,这些人在人口中的分布会很不均匀:倒数第二和倒数的差距是 1-2 人,而第一和第二的差距可能是数千人。也就是说,我不知道如何实现它,所以我们将以其他方式对其进行建模。假设我们有 100 的总人口和四个排名名称的列表:

Alice: 1
Bob: 2
Betty: 2
Claire: 3

我们可以进行分配 "even",这样排名 3 有 X 人,排名 2 有两倍,排名 1 有三倍。如果排名是唯一的,公式将像 X + 2X + 3X = 100 一样简单,但是我们有两个名称在排名 2 中,所以它应该是 X + 2*2X + 3X = 100,所以 X = 12.5。我们可以将它截断为整数,并获取除第一个(12、24 和 24)之外的所有等级的人数,而第一个等级将得到剩余的:40。看起来足够好,但当你有多个第一个时它不适用于边缘情况排名。

不过有点问题。对于 3000 个不同的名字,系数之和为 4501500。因此,截断后的 X 将为 1,使得排名 3000 到排名 2 分别有 1 到 2999 人,排名 1 略低于 500000。这还不够好。为了用上面的四个名字来说明,假设总数为 15。使用当前算法,X 也将是 1,分布将是 1-2-2-10。幸运的是,我们将在程序中逐一处理排名,因此我们可以从等式中删除处理过的人并重新计算 X。例如。首先是 X + 2*2X + 3X = 15X=1,然后是 2*2X + 3X = 14X=2。这样,分配将是 1-4-4-6,这远非理想,但更好。

现在,这已经可以表示为PL/SQL。我建议使用以下列创建 table:LAST_NAMEFIRST_NAMEBIRTHDAYCITYRAND_ROWNO

首先,让我们填入500万个姓氏。假设您对他们的 table 是 last_names(name, name_rank),您将需要以下内容:

declare
  cursor cur_last_name_ranks is
    select name_rank, count(*) cnt, row_number() over (order by name_rank desc) coeff
      from last_names l
     group by name_rank;
  cursor cur_last_names (c_rank number) is
    select name from last_names
     where name_rank = c_rank;
  v_coeff_sum number;
  v_total_people_count number:= 5000000;
  v_remaining_people number;
  v_x number;
  v_insert_cnt number;
begin

  --Get a sum of all coefficients for our formula
  select sum(coeff) into v_coeff_sum
    from 
    (
    select count(*) * row_number() over (order by name_rank desc) coeff
      from last_names l
     group by name_rank
    );

  v_remaining_people := v_total_people_count;

  --Now, loop for all coefficients
  for r in cur_last_name_ranks loop
    --Recalculate X
    v_x := trunc(v_remaining_people / v_coeff_sum);
    --First, determine how many rows should be inserted per last name with such rank
    if r.name_rank = 1 then
      if r.cnt > 1 then
        --raise an exception here, we don't allow multiple first ranks
        raise TOO_MANY_ROWS;
      end if;
      v_insert_cnt := v_remaining_people;
    else
      v_insert_cnt := v_x*r.coeff;
    end if;
    --Insert last names N times.
    --Instead of multiple INSERT statements, use select from dual with connect trick.
    for n in cur_last_names(r.name_rank) loop
      insert into result_table(last_name)
      select n.name from dual connect by level <= v_insert_cnt;
    end loop;
    commit;
    --Calculate remaining people count
    v_remaining_people := v_remaining_people - v_x*r.cnt*r.coeff;
    --Recalculate remmaining coefficients
    v_coeff_sum := v_coeff_sum - r.cnt*r.coeff;
  end loop;

end;

现在你有 500 万行姓氏是根据等级填写的。现在,我们需要为每一行分配 1 到 5000000 之间的随机数 - 你会明白为什么的。这是通过在 self:

上使用 merge 的单个查询完成的
merge into result_table t1
using (select rowid rid, row_number() over (ORDER BY DBMS_RANDOM.VALUE) rnk from result_table) t2
on (t1.rowid = t2.rid)
when matched then update set t1.rand_rowno = t2.rnk

请注意,由于尺寸较大,需要一些时间。

现在您必须对名字重复相同的步骤。它与姓氏非常相似,只是您将更新现有记录,而不是插入新记录。如果您跟踪已经更新了多少行,将其放入内部循环会很简单:

  update result_table
    set first_name = n.name
   where rand_rowno between 
          (v_processed_rows+1) and 
          (v_processed_rows+v_insert_cnt);
  v_processed_rows := v_processed_rows+v_insert_cnt;

就是这样 - 根据您的排名,您现在有 500 万个名字的不错样本,姓氏与名字随机匹配。

现在,进行人口普查。我不太了解您的格式,但这相对简单。如果您获取 "N people were born in city C between DATE1 and DATE2" 形式的数据,您可以在循环中更新 table,将 N 行设置为具有 CITY = C 和 BIRTHDAY = DATE1 和 DATE2 之间的随机日期。您需要一个函数来 return 一个时间段内的随机日期,请参阅 this。另外,不要忘记在执行此操作之前再次分配随机行号。

我会把人口普查部分留给你来实现,我已经花了太多时间写这个了。感谢您的脑力锻炼!