【问题标题】:SAS: Hash Table Lookup using indexed column as hintSAS:使用索引列作为提示的哈希表查找
【发布时间】:2018-09-29 13:20:56
【问题描述】:

在工作中,我有一个 200M 行(300+ 列,~200GB)的索赔表,它按成员编号 (memno) 和服务日期 (dos) 进行索引。

我必须每月两次从固定日期范围(例如 2017 年 1 月 1 日到 2018 年 5 月 1 日)中提取 50,000 个会员编号的声明,而我只需要来自声明的有限列。

MyInputList 只有 1 列(memno)。

proc sql;
    create table myClaims as
    select a.claimno, a.dos, a.memno
    from s.claims a inner join myInputList b
       on a.memno = b.memno
    where a.dos between '01Jan2017'd and '01May2018'd;
quit;

PROC SQL 运行通常需要大约 3-4 小时。数据本身不是由 RDMS 托管的,我阅读了许多关于 PROC SQL 是笛卡尔积的 SAS 文章,由于我不需要每条记录全部 300 列,我想知道使用哈希表是否会更好。

我的问题:我可以给哈希表“提示”,以便它可以利用索引列(memno, dos)?

data myClaimsTest (drop=rc);
if 0 then set myInputList;

declare hash vs(hashexp:7, dataset:'myInputList');
vs.definekey('memno');
vs.definedata();
vs.definedone();

do until (eof);
   set s.claims (keep=claimno dos) end=eof;
   if vs.find()=0 then output;
end;

stop;
run;

新部分(由 Richard 添加)

运行此代码以获取变量和索引的列表。

dm "clear output"; ods listing; ods noresults; options nocenter; title;
proc contents varum data=all_claims;
run;
dm "output" output; ods results;

在此处复制并粘贴输出的底部。将此示例替换为您的实际列表。

        Variables in Creation Order

#    Variable      Type    Len    Format

1    claim_id      Num       8
2    member_id     Num       8
3    claim_date    Num       8    YYMMDD10.


       Alphabetic List of Indexes and Attributes

                          # of
              Unique    Unique
#    Index    Option    Values    Variables

1    PICK     YES       333338    member_id claim_date

【问题讨论】:

  • 索引和哈希是完全不同的概念。一个是磁盘常驻(索引),而另一个是内存常驻。他们访问的方式也非常不同。所以索引列提示哈希对象没有意义。你有没有 Hash 技术,结果如何。
  • @Kiran 也许这是我的问题,我的哈希从未返回(在大小为 10 的测试输入上),它运行了一个多小时,我不得不终止它。
  • “我读了很多 SAS 论文”,你能回忆起并引用问题中的任何内容吗?你读过"Two Guys on Hash" Dorfman and Eberhardt (2010) 吗?您是否使用options MSGLEVEL=I; 启用了 INFO 日志记录?信息消息会告诉您是否使用了索引。
  • BIG 表索引是否唯一? (我认为没有)。如果不是,是否有可以添加到复合索引以使其唯一的第三变量? , 如果第三个 yes 是关联值的域,那么它是先验可知的,因此可以在每个 50K 选择器的查找级别进行迭代?
  • 我看到你的哈希查询有显着差异,它不包括 '01Jan2017'd 和 '01May2018'd 之间的 a.dos 位置

标签: sas proc-sql


【解决方案1】:

假设 BIG 是您的 200GB 索引 SAS 表,SMALL 是您的 50K 行选择标准行。

可能没有使用 BIG 索引(键),因为 SMALL 数据没有足够的信息(变量)来完成与 BIG 匹配的复合键。

有两种处理方式

  1. 全面扫描 BIG,通过查找 SMALL 来测试每条记录,或者
  2. 处理 SMALL 的每条记录,从 BIG 执行索引检索

您问题中的哈希码对应于 #1,SQL 连接是 #2,尽管 SQL 可能选择走 #1 的方式。

这是一个示例数据生成器

%let MEMBER_N = 1e5;
%let CLAIM_RATE = 0.00125;
%let MEMBER_SAMPLE_N = 1e2;
%let STUDY_PROPORTION = 0.001;

data ALL_CLAIMS
( label = "BIG"
  index=
  ( 
    PICK = (member_id claim_date) / unique
  )
);
  retain claim_id 0 member_id 0 claim_date 0 member_n 0;
  format claim_date yymmdd10.;

  do member_id = 1e7 by 1;
    claim_n = 1;
    do claim_date = '01jan2012'd to '31dec2018'd;
      if ranuni(123) > &CLAIM_RATE then continue;
      claim_id + 1;
      if claim_n = 1 then member_n + 1;
      output;
      claim_n + 1;
    end;

    if member_n = &MEMBER_N then leave;
  end;

  stop;

  drop member_n claim_n;
run;

%put note: sample population is %sysevalf(5e4/200e6*100)% of all claims;
%put note: or ~%sysevalf(5e4/200e6*1e6) rows in this example;

data STUDY_MEMBERS(keep=member_id label="SMALL");
  * k / n selection method, Proc SURVEYSELECT is better but not always available;
  * an early sighting on SAS-L would be https://listserv.uga.edu/cgi-bin/wa?A2=ind9909c&L=SAS-L&P=173979
  * Re: Random Selection (Sep 20, 1999);

  retain 
    k %sysevalf(&MEMBER_N*&STUDY_PROPORTION, FLOOR) 
    n &MEMBER_N
  ;

  set ALL_CLAIMS;
  by member_id;

  if first.member_id;

  if ranuni(123) < k/n then do;
    output;
    k + (-1);
  end;

  n + (-1);

  if n=0 then stop;
run;

和处理代码

options msglevel=i;

proc sql;
  create table ALL_STUDY_SUBSET as
  select ALL.claim_id, ALL.claim_date, ALL.member_id
  from ALL_CLAIMS ALL inner join STUDY_MEMBERS STUDY
    on ALL.member_id = STUDY.member_id
  where ALL.claim_date between '01Jan2017'd and '01May2018'd
  ;
quit;

* extend study data with a date variable that matches the data variable in the ALL index; 

data STUDY_MEMBERS_WITH_ITERATED_DATE;
  set STUDY_MEMBERS;
  do claim_date = '01Jan2017'd to '01May2018'd;
    output;
  end;
run;

* join on both variables in ALL key;

proc sql;
  create table ALL_STUDY_SUBSET2 as
  select ALL.claim_id, ALL.claim_date, ALL.member_id
  from ALL_CLAIMS ALL inner join STUDY_MEMBERS_WITH_ITERATED_DATE STUDY
    on ALL.member_id = STUDY.member_id
   and ALL.claim_date = STUDY.claim_date
  ;
quit;

* full scan with hash based match;

data ALL_STUDY_SUBSET3;
  SET ALL_CLAIMS;

  if _n_ = 1 then do;
    declare hash study (dataset:'STUDY_MEMBERS');
    study.defineKey('member_id');
    study.defineDone();
  end;

  if '01jan2017'd <= claim_date <= '01may2018'd;

  if study.find() = 0; 
run;

* SMALL scan with iterated dates to complete info to allow BIG index (key)
* to be used;

data ALL_STUDY_SUBSET4;
  set STUDY_MEMBERS;

  do claim_date = '01jan2017'd to '01may2018'd;
    set ALL_CLAIMS key=pick;
    if _iorc_ = 0 then output;
  end;

  _error_ = 0;
run;

【讨论】:

  • 感谢您精心编写的示例代码和测试用例,我将在周一尝试实现它们并报告结果。 (服务器正在刷新)。
  • 您提出了一个重要说明。 Claim 表有 8 个单独索引的列,我只使用 2 个,即 memno 和 dos(其余索引值在某种程度上是特定于声明的)。
【解决方案2】:

我在第二个答案中添加了稍微不同的问题条件——一个成员在给定的一天可以有多个声明,并且只有简单的单个变量索引。

制作每个会员/天有多个声明的数据

%let MEMBER_N = 1e5;
%let CLAIM_RATE = 0.00125;
%let MULTI_CLAIM_RATE = 0.05; %* iterative rate at which another claim is made on same day a claim is made;
%let STUDY_PROPORTION = 0.001;

data ALL_CLAIMS
( label = "BIG"
  index=
  ( 
/*    PICK = (member_id claim_date) / unique (not happening) */
    member_id
    claim_id
  )
);
  retain claim_id 0 member_id 0 claim_date 0 member_n 0;
  format claim_date yymmdd10.;

  do member_id = 1e7 by 1;
    claim_n = 1;
    do claim_date = '01jan2012'd to '31dec2018'd;
      if ranuni(123) > &CLAIM_RATE then continue;
      if claim_n = 1 then member_n + 1;

      do multi_n = 0 by 1 until (ranuni(123) > &MULTI_CLAIM_RATE); 
        claim_id + 1;
        output;
      end;

      if multi_n > 1 then put 'NOTE: ' member_id= claim_date= multi_n 'claims';

      claim_n + 1;
    end;

    if member_n = &MEMBER_N then leave;
  end;

  stop;

  drop member_n claim_n;
run;

使用claim_date 索引来初步选择候选索赔可能没有帮助 - 您可能在灾难性的一天拥有数万个索赔,您的处理必须在日期范围内迭代日期,按匹配日期设置索赔和对在这些匹配日期发生的每个 claim_id 记录进行哈希查找 (SMALL:member_id)。您必须进行试验,看看这种反直觉的方法是否真的对您特定的 ALL 和 SMALL 更有效。

如果您检查 SQL 的日志,您将看到查询优化器选择使用 member_id 索引(并且在内部将迭代找到的行以应用 where 子句)。未记录的Proc SQL 选项_method 和_tree 可以向您展示它将做什么——请参阅"The SQL Optimizer Project: _Method and _Tree in SAS®9.1" Lavery (SUGI 30)。

proc sql _method _tree;
  create table ALL_STUDY_SUBSET as
  select ALL.claim_id, ALL.claim_date, ALL.member_id
  from ALL_CLAIMS ALL inner join STUDY_MEMBERS STUDY
    on ALL.member_id = STUDY.member_id
  where ALL.claim_date between '01Jan2017'd and '01May2018'd
  ;
quit;

日志摘录

INFO: Index member_id of SQL table WORK.ALL_CLAIMS (alias = ALL) selected for SQL WHERE clause
      (join) optimization.

NOTE: SQL execution methods chosen are:

      sqxcrta
          sqxjndx
              sqxsrc( WORK.STUDY_MEMBERS(alias = STUDY) )
              sqxsrc( WORK.ALL_CLAIMS(alias = ALL) )

Tree as planned.
                               /-SYM-V-(ALL.claim_id:1 flag=0001)
                     /-OBJ----|
                    |         |--SYM-V-(ALL.claim_date:3 flag=0001)
                    |          \-SYM-V-(ALL.member_id:2 flag=0001)
           /-JOIN---|
          |         |                              /-SYM-V-(STUDY.member_id:1 flag=0001)
          |         |                    /-OBJ----|
          |         |          /-SRC----|
          |         |         |          \-TABL[WORK].STUDY_MEMBERS opt=''
          |         |--FROM---|
          |         |         |                    /-SYM-V-(ALL.claim_id:1 flag=0001)
          |         |         |          /-OBJ----|
          |         |         |         |         |--SYM-V-(ALL.claim_date:3 flag=0001)
          |         |         |         |          \-SYM-V-(ALL.member_id:2 flag=0001)
          |         |          \-SRC----|
          |         |                   |--TABL[WORK].ALL_CLAIMS opt=''
          |         |                   |          /-NAME--(claim_date:3)
          |         |                    \-IN-----|
          |         |                             |                    /-LITN(20820) DATE.
          |         |                             |          /-RANB---|
          |         |                             |         |          \-LITN(21305) DATE.
          |         |                              \-SET----|
          |         |--empty-
          |         |          /-SYM-V-(STUDY.member_id:1)
          |          \-CEQ----|
          |                    \-SYM-V-(ALL.member_id:2)
 --SSEL---|

和一个等效的 DATA 步

data ALL_STUDY_SUBSET5(label="Presuming a preponderance of members file few claims over their all_claims lifetime");
  set STUDY_MEMBERS;

  do until (_iorc_);
    set ALL_CLAIMS key=member_id;
    if _iorc_ = 0 and '01jan2017'd <= claim_date <= '01may2018'd then do;
      OUTPUT;
    end;
  end;

  _error_ = 0;
run;

还是很慢?

当出现最佳努力和最佳实践编程结果不够快的情况时,您将不得不通过包含的系统资源来寻求改进:

  • 能否使用(member_id claim_date) 添加新的复合索引?
  • 能否将数据集表移动到更快的驱动器?如:
    • 确保没有数据方面的网络交互
    • 碎片整理
    • 用 15000 RPM 替换 5400 RPM 驱动器
    • 用 SSD SATA 替换旋转驱动器
    • 用 NVMe 替换旋转或 SSD SATA
    • SAS 文件
      • 具有大量 RAM (>200GB) 的工作站/服务器
    • 将数据移动到云/远程/大铁数据仓库解决方案,具有更大的自动并行化和按需资源提升潜力

【讨论】:

    猜你喜欢
    • 2019-10-16
    • 1970-01-01
    • 2021-06-25
    • 1970-01-01
    • 2013-06-27
    • 1970-01-01
    • 2012-01-03
    • 1970-01-01
    • 2014-09-01
    相关资源
    最近更新 更多