The Nameless City

何故か製薬やSAS関連のブログ、の予定。

レコード件数を取り出す「if 0 then set <dataset name> NOBS=<variable>」は、特定条件下で想定外の結果を返すので注意。

「if 0 then set 」って昔調べた時には情報ホント無かったんだけど、今SASプログラマの人がボチボチネットに書いてますね。
で、見てた時に、そういや某所でやってた時に、ここら辺で面倒臭い話あったなと思い出して調べてみた所、やっぱりこのコードは特定条件下でバグになります。まあ普通はならんのですけど。

参考までに、関係しそうな所に対してリンク貼っときます。TB打てないんだよなはてなブログ
データステップ100万回      SAS新手一生: 【訂正追補】SASデータセットのオブザベーション数をマクロ変数に格納する方法_call symput
SAS忘備録: データステップ内で、色々なデータセットのオブザベーション数を取得する
SAS忘備録: 行削除の落とし穴

通常の使い方。

プログラムは↓

data _NULL_ ;
	if 0 then set SASHELP.CLASS nobs=_SYS_OBS ;
	put 'トータルの物理オブザベーション数->' _SYS_OBS ;
	stop ;
run ;

で、ログが↓

66   data _NULL_ ;
67       if 0 then set SASHELP.CLASS nobs=_SYS_OBS ;
68       put 'トータルの物理オブザベーション数->' _SYS_OBS ;
69       stop ;
70   run ;

トータルの物理オブザベーション数->19
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.00 秒

簡単に説明すると、最初、定義とかは取りに行くんですが、実行部分で「if 0 then」となっているので、レコード自体は読み込まれません。
この場合に、通常はdataステップはオブザベーション数分実行されるんですが、1行も読み込まない設定の場合には、1回だけ実行されます。
データの定義部に、オブザベーション数が書き込まれてるんですね。
で、「トータルの」と但し書きを入れたのは、例えば「set SASHELP.CLASS SASHELP.CLASS」とかにすると、この合算のオブザベーション数(38)が書き込まれます。


本題にも関わってくるのですが、このデータの定義部に書かれているオブザベーション数は、「物理オブザベーション数」であり、「論理オブザベーション数」ではないという事を頭に入れておいて下さい。

特殊条件下で想定外の結果を返す例。

プログラムは以下です。

proc sql ;
	create table CLASSDEL as select * from SASHELP.CLASS ;
	delete * from CLASSDEL where AGE=15 ;
quit ;

data _NULL_ ;
	if 0 then set CLASSDEL nobs=_SYS_OBS ;
	put 'トータルの物理オブザベーション数->' _SYS_OBS ;
	stop ;
run ;

SASHELP.CLASSを複写したCLASSDELというデータセットを作成します。
そして、「SQLプロシージャで」DELETE文を発行しています。この場合、AGE=15(4オブザベーション)を削除します(ホントどうでもいい事ですが、CLASSデータセットは日本語版、UTF-8英語版などでNAMEやSEXの中の値が変わるためAGEにしてます)
その結果は?

71   proc sql ;
72       create table CLASSDEL as select * from SASHELP.CLASS ;
NOTE: テーブルWORK.CLASSDEL(行数19、列数5)が作成されました。

73       delete * from CLASSDEL where AGE=15 ;
NOTE: 4行がWORK.CLASSDELから削除されました。

74   quit ;
NOTE: PROCEDURE SQL処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.01 秒


75
76   data _NULL_ ;
77       if 0 then set CLASSDEL nobs=_SYS_OBS ;
78       put 'トータルの物理オブザベーション数->' _SYS_OBS ;
79       stop ;
80   run ;

トータルの物理オブザベーション数->19
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.00 秒

さてと。
「このデータの定義部に書かれているオブザベーション数は、「物理オブザベーション数」であり、「論理オブザベーション数」ではない」と言っていた事に繋がるのですが、SQLプロシージャでDELETE文を発行した場合、オブザベーションがまるっと消されるのではなく、単にDELフラグが立つだけです。
そしてこの際定義部は書き換えられないです。
その為、こうなります。

同様の操作をdataステップで実行する事は可能です。但し、MODIFYなんて、まあ滅多に使わないと思いますが。

data CLASSDEL ;
	set SASHELP.CLASS ;
run ;
data CLASSDEL ;
	modify CLASSDEL ;
	if AGE=15 then remove ;
run ;
data _NULL_ ;
	if 0 then set CLASSDEL nobs=_SYS_OBS ;
	put 'トータルの物理オブザベーション数->' _SYS_OBS ;
	stop ;
run ;
308  data CLASSDEL ;
309      set SASHELP.CLASS ;
310  run ;

NOTE: データセットSASHELP.CLASSから19オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSDELは19オブザベーション、5変数です。
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.01 秒


311  data CLASSDEL ;
312      modify CLASSDEL ;
313      if AGE=15 then remove ;
314  run ;

NOTE: データセットWORK.CLASSDELから19オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSDELを更新しました。
 オブザベーションの再書き込み0、追加0、削除4
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


315  data _NULL_ ;
316      if 0 then set CLASSDEL nobs=_SYS_OBS ;
317      put 'トータルの物理オブザベーション数->' _SYS_OBS ;
318      stop ;
319  run ;

トータルの物理オブザベーション数->19
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.00 秒


現在、「SQLプロシージャでDELETEステートメントを発行する」「DATAステップでREMOVEステートメントを発行する(MODYFYステートメントでデータセットを取り込む場合)」場合に、「物理オブザベーション数が論理オブザベーション数から乖離する」事がわかっています。

この時他の方法でデータを取得するとどうなっているか?

他に、オブザベーション数を取得する方法は、ざっと考えて二つあります。
CONTENTSプロシージャを利用する方法と、SQLプロシージャでSASHELP.VTABLE(あるいはDICTIONARY.TABLES)を利用する方法です。

proc sql ;
	create table CLASSDEL as select * from SASHELP.CLASS ;
	delete * from CLASSDEL where AGE=15 ;
quit ;
data _NULL_ ;
	if 0 then set CLASSDEL nobs=_SYS_OBS ;
	put 'トータルの物理オブザベーション数->' _SYS_OBS ;
	stop ;
run ;

proc contents data=CLASSDEL out=INFODEL noprint ;
quit ;
proc print data=INFODEL ;
	var MEMNAME NOBS DELOBS ;
quit ;

proc sql ;
	create table INFODEL2 as
	select MEMNAME,NOBS,DELOBS,NLOBS from SASHELP.VTABLE
	where LIBNAME='WORK' and MEMNAME='CLASSDEL' ;
quit ;
proc print data=INFODEL2 ;
	var MEMNAME NOBS DELOBS NLOBS ;
quit ;

今度は、アウトプット(LISTINGで出力)を。

OBS    MEMNAME     NOBS    DELOBS

 1     CLASSDEL     15        4
 2     CLASSDEL     15        4
 3     CLASSDEL     15        4
 4     CLASSDEL     15        4
 5     CLASSDEL     15        4
OBS    memname     nobs    delobs    nlobs

 1     CLASSDEL     19        4        15

CONTENTSプロシージャの出力には変数の情報も入ってくる為その分オブザベーションが増えてしまっているのですが、NOBSは想定のものに「書き換えられている」イメージです。
SQLプロシージャで、SASHELP.VTABLEを利用すると(これは裏でせっせと情報集めに行っているので注意が必要です--対象になるテーブルだけ参照しに行きますが、WHEREの書き方やハンドリング次第ではかなり時間がかかります)、書き換わっていない「NOBS」が表示されます。
DELOBSという変数を表示させていますが、こちらが「削除された(が物理的には残っている)オブザベーション数」になります。
NLOBSはVTABLEにしかありませんが、こちら「論理オブザベーション数」です。

余談

削除されたオブザベーションは、内部的にはデータを保持していますのでバイナリエディタとかで確認出来たりします。
あと、当然ながらパフォーマンス劣化につながります(以前SASサーバのレポジトリ肥大とかあったのはそのせいだろうなあ。今のバージョンでは確認してませんが)。
その場合には、

data <dataset> ;
	set <dataset> ;
run ;

とか書ければ良いのですが。
あと、普段のSAS使いの人はあまり影響ないとは思いますが、Data Integration Studioで、SQLのDELETE文はわりと発行しているので注意です。


ただ、この削除方法等はRDBMSでは見かけるものです。その目的は、マルチアクセスという事にあるわけで、そういや、SAS/SHARE辺りは特に考えてなかったなあ・・・・・・面倒臭い。