2021年7月14日水曜日

正規化された表にデータをロードする

 以下の記事にて、政府CIOポータルから取得できる情報をもとにアプリケーションを作成しました。

全国医療機関の医療体制の状況をオープンデータをもとに地図上に表示する

オープンデータに限らず、CSV、Excel、JSONといった形式で提供されているデータは概ね正規化されていません。これらのデータは、それ自体で完結している方が扱いやすいためでしょう。しかし、保存する側はリレーショナル・データベースなので、データは正規化した方が情報としての活用範囲が広がります。

そのため、外部からのデータ・ロードは、一旦インターフェース表にロードし、その後、正規化された実表に転記するという作業が必要になることが多いように思います。

この記事では、正規化された表に医療体制の状況のデータをロードする実装をしてみます。


RESTデータ・ソースの同期先を変更する


RESTデータ・ソースcovid19DailySurveyREST同期化の設定を開き、設定のクリアを実行します。


これにより、すべての同期化設定がクリアされます。ローカル表は削除されません。続行しますか?とプロンプトが表示されます。OKをクリックします。


同期化の設定が削除されます。同期先新規表とし、表名としてC19_MEDICAL_FACILITY_STATUSES_INTを指定し、保存をクリックします。この表がインターフェース表になります。


表の作成をクリックし、同期化表(インターフェース表)を作成します。


詳細同期タイプとして置換を選択します。インターフェース表のデータは、実表に転記された後は不要です。詳細設定置換タイプとしてDELETEもしくはTRUNCATEを選択します。今回はDELETEを選択しました。

一般にインターフェース表は、データの投入と削除が繰り返されます。そのため索引はつけません。その結果として、ほとんどの操作は全表スキャンになります。他のデータベースはわかりませんが、Oracle Databaseでの全表スキャンは保存されている行数ではなく、表が占有しているブロック数で処理時間が決まります。置換タイプがDELETEの場合は、データを削除してもブロック数は変わりません。TRUNCATEの場合はブロックが開放されます。

一度大量のデータをロードすると表は多数のブロックを取得します。その後の投入データが少ないと転記の処理を行う際に全ブロックが読み込み対象となるため、DELETEで削除していると性能面で不利になります。ほとんどの場合で投入するデータは少量ですが、たまに大量のデータが投入される、といった場合はTRUNCATEの方が性能面で有利になるでしょう。

データの削除については、トランザクションを維持しないためTRUNCATEの方が高速です。

データのロードについては、DELELEでデータを削除した場合はすでにブロックは取得済みであるため、より短時間で処理が終了します。TRUNCATEは新たにブロックを取得する必要があります。ブロックの取得は負荷の高い処理です。

ロードするデータの特性に応じて、DELETEかTRUNCATEのどちらを選択することになります。

RESTデータ・ソースとしては定期実行はしません。以上の設定を行った後、保存して実行をクリックします。



正規化した表を準備する


医療機関の状況(ANS_TYPE)、医療機関のタイプ(FACILITY_TYPE)はルックアップ表を定義します。医療機関はFACILITY_IDを主キーとした別表とし、また、自治体コードも別表とします。

医療機関の状況のルックアップ表C19_ANS_TYPESを作成します。以下のスクリプトをSQLワークショップSQLスクリプトより実行します。

drop table c19_ans_types purge;
create table c19_ans_types(
ans_type_cd varchar2(1),
ans_type varchar2(16) not null,
constraint c19_ans_types_pk primary key (ans_type_cd)
);
insert into c19_ans_types(ans_type_cd, ans_type) values('N','通常');
insert into c19_ans_types(ans_type_cd, ans_type) values('L','制限');
insert into c19_ans_types(ans_type_cd, ans_type) values('H','停止');
insert into c19_ans_types(ans_type_cd, ans_type) values('V','設置なし');
insert into c19_ans_types(ans_type_cd, ans_type) values('X','未回答');
commit;


医療機関のタイプのルックアップ表C19_FACILITY_TYPESを作成します。

drop table c19_facility_types purge;
create table c19_facility_types(
facility_type_cd varchar2(1),
facility_type varchar2(16) not null,
constraint c19_facility_types_pk primary key(facility_type_cd)
);
insert into c19_facility_types(facility_type_cd, facility_type) values('M','外来');
insert into c19_facility_types(facility_type_cd, facility_type) values('E','救急');
insert into c19_facility_types(facility_type_cd, facility_type) values('H','入院');
commit;

自治体コードのマスター表C19_LOCAL_GOV_CODESを作成します。本来であれば総務省よりデータを入手するところですが、手順を簡略にするためインターフェース表に取り込まれたデータを使います。

drop table c19_local_gov_codes purge;
create table c19_local_gov_codes(
local_gov_code varchar2(6),
pref_name varchar2(20) not null,
city_name varchar2(40) not null,
constraint c19_local_gov_codes_pk primary key(local_gov_code)
);
insert into c19_local_gov_codes(local_gov_code, pref_name, city_name)
select distinct local_gov_code, pref_name, city_name from c19_medical_facility_statuses_int;
commit;

医療機関のマスター表C19_MEDICAL_FACILITIESを作成します。こちらもインターフェース表に取り込まれたデータを使います。緯度経度は数値列LATITUDE, LONGITUDEからSDO_GEOMETRY型へ変換しています。

drop table c19_medical_facilities purge;
create table c19_medical_facilities(
facility_id varchar2(16),
facility_code varchar2(16),
zip_code varchar2(8),
facility_name varchar2(255) not null,
facility_addr varchar2(255) not null,
facility_tel varchar2(16),
local_gov_code varchar2(6) not null,
longitude number,
latitude number,
geometry sdo_geometry,
constraint c19_medical_facilities_pk primary key(facility_id),
constraint c19_medical_facilities_fk foreign key(local_gov_code)
references c19_local_gov_codes
);
insert into c19_medical_facilities(
facility_id, facility_code, zip_code, facility_name, facility_addr,
facility_tel, local_gov_code, longitude, latitude)
select distinct
facility_id, facility_code, replace(zip_code,'〒'), facility_name, facility_addr,
facility_tel, local_gov_code, longitude, latitude
from c19_medical_facility_statuses_int
group by
facility_id, facility_code, zip_code, facility_name, facility_addr,
facility_tel, local_gov_code, longitude, latitude;
update c19_medical_facilities
set geometry = sdo_geometry(2001, 4326,
sdo_point_type(longitude, latitude, null),
null, null);
commit;

医療機関の状況を保存する表C19_MEDICAL_FACILITY_STATUSESを作成します。

drop table c19_medicaL_facility_statuses purge;
create table c19_medical_facility_statuses (
id number generated by default on null as identity
constraint c19_medical_facili_id_pk primary key,
ans_type varchar2(1)
constraint c19_medical_facili_ans_type_fk
references c19_ans_types on delete cascade,
submit_date date,
facility_type varchar2(1)
constraint c19_medical_fac_facility_ty_fk
references c19_facility_types on delete cascade,
facility_id varchar2(16)
constraint c19_medical_fac_facility_id_fk
references c19_medical_facilities on delete cascade
)
;
create index c19_medical_facili_i1 on c19_medical_facility_statuses (ans_type);
create index c19_medical_facili_i2 on c19_medical_facility_statuses (facility_id);
create index c19_medical_facili_i3 on c19_medical_facility_statuses (facility_type);

インターフェース表C19_MEDICAL_FACILITY_STATUSES_INTに投入済みのデータを、表C19_MEDICAL_FACILITY_STATUSESへ転記します。

insert into c19_medical_facility_statuses(ans_type, submit_date, facility_type, facility_id)
select a.ans_type_cd, i.submit_date, f.facility_type_cd, i.facility_id
from c19_medical_facility_statuses_int i
join c19_ans_types a on i.ans_type = a.ans_type
join c19_facility_types f on i.facility_type = f.facility_type;

正規化した表より、元々の形式でアクセスするためのビューC19_MEDICAL_FACILITY_STATUSES_Vを作成します。

create or replace view c19_medical_facility_statuses_v
as
select
a.ans_type
, '〒' || f.zip_code zip_code
, l.city_name
, f.latitude
, l.pref_name
, f.longitude
, f.facility_id
, s.submit_date
, f.facility_tel
, f.facility_addr
, f.facility_code
, f.facility_name
, t.facility_type
, f.local_gov_code
, f.geometry
from c19_medical_facility_statuses s
join c19_medical_facilities f on s.facility_id = f.facility_id
join c19_local_gov_codes l on f.local_gov_code = l.local_gov_code
join c19_ans_types a on s.ans_type = a.ans_type_cd
join c19_facility_types t on s.facility_type = t.facility_type_cd
;

作成したビューからインターフェース表の内容を引き算(minus)して行が選択されなければ、すべてのデータが転記されているといえます。

select
ans_type, zip_code, city_name, latitude, pref_name, longitude, facility_id, submit_date
, facility_tel, facility_addr, facility_code, facility_name, facility_type, local_gov_code
from c19_medical_facility_statuses_int
minus
select
ans_type, zip_code, city_name, latitude, pref_name, longitude, facility_id, submit_date
, facility_tel, facility_addr, facility_code, facility_name, facility_type, local_gov_code
from c19_medical_facility_statuses_v


アプリケーションを修正する


アプリケーションはRESTデータ・ソースを直接参照せず、転記済みのローカル表をデータ・ソースとします。

対話モード・レポートのページから、検索条件となるページ・アイテムを削除します。


対話モード・レポートのソース表名としてC19_MEDICAL_FACILITY_STATUSES_Vを指定します。


マップソースSQLを以下に変更します。同一の医療機関の複数の施設を1行にします。医療機関のタイプごとの状況はそれぞれ列ANS_TYPE_M(外来)、ANS_TYPE_E(救急)、ANS_TYPE_H(入院)とします。列ANS_TYPEは医療機関の代表となる状況を返し、その状況をレイヤーへの割り当てに使用します。

select f.zip_code, f.facility_name, f.facility_addr, f.geometry
, m.ans_type ans_type_m, e.ans_type ans_type_e, h.ans_type ans_type_h
,
case
when m.ans_type = 'H' or e.ans_type = 'H' or h.ans_type = 'H' then
'H'
when m.ans_type = 'L' or e.ans_type = 'L' or h.ans_type = 'L' then
'L'
when m.ans_type = 'N' or e.ans_type = 'N' or h.ans_type = 'N' then
'N'
when m.ans_type = 'V' or e.ans_type = 'V' or h.ans_type = 'V' then
'V'
else
'X'
end ans_type
from c19_medical_facilities f
left outer join (
select ans_type, facility_type, facility_id
from c19_medical_facility_statuses
where facility_type = 'M') m
on f.facility_id = m.facility_id
left outer join (
select ans_type, facility_type, facility_id
from c19_medical_facility_statuses
where facility_type = 'E') e
on f.facility_id = e.facility_id
left outer join (
select ans_type, facility_type, facility_id
from c19_medical_facility_statuses
where facility_type = 'H') h
on f.facility_id = h.facility_id


それぞれのレイヤー設定は以下にように変更します。ツールチップHTML式template directiveを使用することで状況ごとに色分けも行います。

<b>&FACILITY_NAME.</b>
<br>郵便番号: &ZIP_CODE.
<br>住所: &FACILITY_ADDR.
<br>
<table><tr><th>医療区分</th><th>医療提供状況</th></tr>
{case ANS_TYPE_M/}
{when N/}<tr class="u-success-bg"><td>外来</td><td >通常</td></tr>
{when L/}<tr class="u-warning-bg"><td>外来</td><td>制限</td></tr>
{when H/}<tr class="u-danger-bg"><td>外来</td><td>停止</td></tr>
{when V/}<tr class="u-color-44-bg"><td>外来</td><td >設置なし</td></tr>
{when X/}<tr class="u-color-45-bg"><td>外来</td><td>未回答</td></tr>
{endcase/}
{case ANS_TYPE_E/}
{when N/}<tr class="u-success-bg"><td>救急</td><td >通常</td></tr>
{when L/}<tr class="u-warning-bg"><td>救急</td><td>制限</td></tr>
{when H/}<tr class="u-danger-bg"><td>救急</td><td>停止</td></tr>
{when V/}<tr class="u-color-44-bg"><td>救急</td><td >設置なし</td></tr>
{when X/}<tr class="u-color-45-bg"><td>救急</td><td>未回答</td></tr>
{endcase/}
{case ANS_TYPE_H/}
{when N/}<tr class="u-success-bg"><td>入院</td><td >通常</td></tr>
{when L/}<tr class="u-warning-bg"><td>入院</td><td>制限</td></tr>
{when H/}<tr class="u-danger-bg"><td>入院</td><td>停止</td></tr>
{when V/}<tr class="u-color-44-bg"><td>入院</td><td >設置なし</td></tr>
{when X/}<tr class="u-color-45-bg"><td>入院</td><td>未回答</td></tr>
{endcase/}
</table>

その他、行割当てこのレイヤーの値N, L, H, V, Xといったルックアップで使うコードに置き換えます。列のマッピングジオメトリ列のデータ型SDO_GEOMETRYジオメトリ列GEOMETRYに変更します。ポイント・オブジェクトスタイルはデフォルトのSVGに戻します。

ツールチップHTML式は上記のコードに変更します。


医療機関のタイプを1行にまとめたので、ファセットのP4_FACILITY_TYPEは不要になりました。削除しておきます。

アプリケーションの改変は以上で完了です。検索結果の行数が1/3になっているので、若干パフォーマンスが上がりました。




自動化の設定をする


RESTデータ・ソースからインターフェース表へデータの取り込みを行い、正規化された実表へ転記する一連の処理を自動化として実装します。

最初にロード対象とする自治体コードを保存する表C19_LOAD_TARGETSの作成を行います。また、対象となる自治体コードをいくつか登録します。

drop table c19_load_targets purge;
create table c19_load_targets(
local_gov_code varchar2(6) primary key
);
-- 東京都世田谷区
insert into c19_load_targets values('131121');
-- 神奈川県横浜市
insert into c19_load_targets values('141003');
-- 神奈川県川崎市
insert into c19_load_targets values('141305');
commit;

自動化を作成します。名前医療提供状況の同期静的IDsyncdailysurveyとします(以前の記事の作業で作成済みであれば、設定を更新します)。

設定タイプオンデマンドにします。アクションの開始問合せとします。ソースデータ・ソースローカル・データベースソース・タイプとし、表/ビューの名前として先ほど作成したC19_LOAD_TARGETSを選択します。


アクションの実行を設定します。アクションの実行時間行が返されるです(翻訳が不適切です。問合せが実行された時行が返される行が返されないか、という意味です)。主キー列LOCAL_GOV_CODE (Varchar2)を選択します。コミット各行としました。自治体ごとに処理をコミットします。


アクションはそれぞれPL/SQLコードで実装します。

最初のアクションは、RESTデータ・ソースから取得したデータをインターフェース表に保存します。アクション名前自治体ごとの更新としました。

declare
l_parameters apex_exec.t_parameters;
l_sync_id varchar2(200);
l_message varchar2(4000);
l_rows number;
begin
l_parameters.delete;
apex_exec.add_parameter(
p_parameters => l_parameters,
p_name => 'localGovCode',
p_value => :LOCAL_GOV_CODE );
apex_exec.add_parameter(
p_parameters => l_parameters,
p_name => 'since',
p_value => to_char(sysdate-1,'YYYYMMDD') );
l_sync_id := '同期 - 自治体コード: ' || :LOCAL_GOV_CODE;
apex_rest_source_sync.dynamic_synchronize_data(
p_module_static_id => 'covid19DailySurvey',
p_sync_static_id => l_sync_id,
p_sync_parameters => l_parameters );
-- 実行結果のログ
select count(*) into l_rows from c19_medical_facility_statuses_int;
l_message := 'Load data from REST data source, rows = ' || l_rows;
apex_automation.log_info(l_message);
end;

次に医療機関のマスター表C19_MEDICAL_FACILITIESを更新します。アクション名前医療機関の追加としました。長いコードに見えますが、実質は1行のMERGE文です。

merge into c19_medical_facilities f
using
(
select distinct
facility_id, facility_code, zip_code
, facility_name, facility_addr,facility_tel, local_gov_code
, longitude, latitude
from c19_medical_facility_statuses_int
group by
facility_id, facility_code, zip_code
, facility_name, facility_addr, facility_tel, local_gov_code
, longitude, latitude
) u
on (f.facility_id = u.facility_id)
when matched then
update set
facility_code  = u.facility_code
, zip_code = replace(u.zip_code,'〒')
, facility_name = u.facility_name
, facility_addr = u.facility_addr
, facility_tel = u.facility_tel
, local_gov_code = u.local_gov_code
, longitude = u.longitude
, latitude = u.latitude
, geometry = sdo_geometry(2001, 4326,
sdo_point_type(u.longitude, u.latitude, null),
null, null)
when not matched then
insert (
facility_id, facility_code, zip_code
, facility_name, facility_addr, facility_tel, local_gov_code
, longitude, latitude
, geometry
)
values (
u.facility_id, u.facility_code, replace(u.zip_code,'〒')
, u.facility_name, u.facility_addr, u.facility_tel, u.local_gov_code
, u.longitude, u.latitude
, sdo_geometry(2001, 4326,
sdo_point_type(u.longitude, u.latitude, null),
null, null)
)
;
-- 実行結果のログ
apex_automation.log_info('Update/Insert Medical Facilities, rows = ' || SQL%ROWCOUNT );

最後に表C19_MEDICAL_FAICLITY_STATUSESへデータを転記します。アクション名前転記としました。

merge into c19_medical_facility_statuses s
using (
select
i.facility_id, i.submit_date
, t.facility_type_cd facility_type, a.ans_type_cd ans_type
from c19_medical_facility_statuses_int i
join c19_ans_types a on i.ans_type = a.ans_type
join c19_facility_types t on i.facility_type = t.facility_type
) u
on (s.facility_id = u.facility_id and s.facility_type = u.facility_type)
when matched then
update set
submit_date = u.submit_date
, ans_type = u.ans_type
when not matched then
insert (
facility_id, facility_type
, submit_date, ans_type
)
values (
u.facility_id, u.facility_type
, u.submit_date, u.ans_type
)
;
-- 実行結果のログ
apex_automation.log_info('Update/Insert Medical Facilities Statuses, rows = ' || SQL%ROWCOUNT );

アクションは実行順序の昇順で実行されます。レポートの表示はデフォルトでは実行順序の昇順になっていません。一般的な対話グリッドの操作により実行順序で昇順ソートできます。


保存して実行をクリックし、動作を確認します。


自動化実行ログのタブを開きます。メッセージの数値をクリックします。


アクションのコード中で呼び出したAPEX_AUTOMATION.LOG_INFOによるログ出力を確認することができます。


以上で今回の作業は完了です。アプリケーションを実行すると、以下のようになります。


作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/c19-medical-facilities.sql

自治体コードを表C19_LOCAL_GOV_CODESに投入するために、サポートするオブジェクトとしてインストール・スクリプトを登録しました。1200行を超えるスクリプト(INSERT文)だったのでインストール・スクリプトが内部的に分割され、その分割された位置のSQLが不正になりました。不具合だとは思いますが、インストール・スクリプトを200行ずつ、7つのファイルに分割することで、この不具合を回避しています。

それと記事には含めていませんが、エクスポートしたアプリケーションには表C19_LOAD_TARGETSへ自治体コードを挿入するページを含めています。


それではOracle APEXのアプリケーション作成の参考になれば幸いです。


追記

インターフェース表というのは、オラクル用語かもしれません。このような用途の表を、オラクル社のOracle E-Business Suiteではインターフェース表と呼んでいます。