大量データをselectし、別テーブルに一括でインサートする方法をご教示ください。
<実装したい背景>30万件ほどのデータのselectをカーソルにて行い、loop内で1件ずつinsertしたところ、タイムアウトしてしまったためです。insert文を大量に発行していることが原因だと考えています。
また、複雑なチェック処理やデータの加工を行っている関係上、insert前に1件ずつの処理が必要です。
そのため、「insert ~ select」で一括インサートが難しい状況です。
<試したこと>
Oracleのバルクインサートのようなものが使えないかと考えましたが、
下記処理でもエラーが起きています。
Postgres
1//データを取得するテーブル 2CREATE TABLE manager ( 3 id text, 4 first_name text, 5 age numeric, 6 tel text 7); 8 9INSERT INTO manager (id, first_name, age, tel) VALUES 10('1', '名字', 30, '090-9999-9999'); 11 12 13//データを登録するテーブル 14CREATE TABLE test ( 15 test1 text, 16 test2 text, 17 test_num numeric, 18 test4 text 19);
Postgres
1CREATE OR REPLACE FUNCTION fnc_main(user_id text) 2 RETURNS character varying 3 LANGUAGE plpgsql 4AS $function$ 5declare 6 7-- データが30万件ほどある 8cur_manager cursor is 9 select 10 first_name 11 from 12 manager; 13 14 rec_manager record; 15 tmp_test test%rowtype; 16 rec_test record; 17 fnc_return text; 18 err_msg text; 19 20begin 21 22open cur_manager; 23fetch cur_manager into rec_manager; 24loop 25 --値の入力チェックや編集を行う(15項目) 26 --項目の入力チェックは省略しています 27 --値をセット 28 tmp_test.test1 = rec_manager.first_name; 29 if rec_manager.id = 1 then 30 tmp_test.test_num = rec_manager.age; 31 -- 別functionで年齢から西暦等を取得する処理 32-- select fnc_get_seireki(rec_manager.age) into fnc_return; 33 34 --エラーが発生している場合 35 if fnc_return = 'true' then 36 err_msg = '西暦が取得できません'; 37 else 38 tmp_test.test2 = fnc_return; 39 end if; 40 --... 41 42// rec_test = tmp_test; --恐らくエラーが発生している 43end loop; 44 45//insert into test values(rec_test); --恐らくエラーが発生している 46 47return ''; 48end; 49 50$function$
>下記処理でもエラーが起きています。
どういうエラーでしょうか。文法上のものかそうでないかでも違います。
https://teratail.com/help/question-tips#questionTips3-4-2

コミットはどの程度の頻度で行っているのでしょうか?
procedureでなくfunctionにしている理由は何かありますか?

>m.ts10806様
エラーコード「55000 必要条件を満たさないオブジェクト」の「ERROR: record “ rec_test” is not assigned yet」と出力されました。

>dameo様
単独でfunctionを呼び出しているため、関数終了時に自動コミットをしていると思われます。トランザクションの処理に疎く、間違っていたら申し訳ありません。
procedureはPostgres11からの機能であり、10以前から運用していた流れか、procedureを使用せず、functionでの開発が要望として上がっているためです。(現実的でないことをご理解くださらない、、、)
procedureかfunctionかは呼び出す形式が異なるだけで、やれない事があるという訳では無いですから関係無いですよ。

@sazi様
functionの場合、SQL内に式として書けることもあり、あまり書き換えなどはしないと思いますし、commitが出来ないという問題があります。
@Unknown2030様
大量登録の場合、設定にもよるし、どこに時間がかかっているのか実際には測ってみないと分かりませんが、体感的には、何行ごとにcommitするかが結構大きく効く気がするのです。なので、可能であればprocedureで一定行ごとにcommitしてみてほしいです。他には
・トランザクションログなどを残すか
・indexや制約などがあるか
・bulk処理されているか
などでしょうか。実際には固定データならcopyコマンドが一番速いようですが。
あと、fetch loopの処理が不思議な感じになってるので、まずはそこから分かるように書きましょう。
(もしかしたらこのループが終わってないだけかもしれません。)
また、バージョンや使える機能に制限がある場合は事前に書いておいた方がいいかもです。
申し訳ありませんがbulk collectやis table ofなどがpostgresqlでも使えるのか知りません。ただtableタイプはあるようなので、似たような書き方はできるのではないかと思っています。時間があったら私も試してみますが、期待しないでください。

起きてから軽く書いてみました。bulk fetchも変数からのbulk insertもなさげですね。
手元の骨だけ環境ではinsert投げまくるよりは半分くらいの時間になります。procedureにしてcommitすると1割くらい減ります。indexなどは皆無だし、DB自体もdockerのデフォのまま使ってるので、意味のない値ですが。
CREATE OR REPLACE FUNCTION fnc_main(id text)
RETURNS character varying
LANGUAGE plpgsql
AS $function$
declare
cur_manager cursor is select * from manager;
rec_manager manager;
rec_test test;
tmp_test test[];
i integer;
bulk_count integer;
begin
i := 1;
bulk_count := 1000; -- 1000は適当
open cur_manager;
loop
fetch cur_manager into rec_manager; -- postgresqlにbulk fetchはない
if not found then
insert into test select * from unnest(tmp_test); -- insert selectしかなさそう
-- commit;
exit;
end if;
rec_test.test1 = rec_manager.first_name;
--
tmp_test := array_append(tmp_test, rec_test);
if i = bulk_count then
insert into test select * from unnest(tmp_test); -- 面倒なので共通化はしてません
-- commit;
i := 1;
tmp_test := null;
else
i := i + 1;
end if;
end loop;
return '';
end;
$function$;

>dameo様
具体的にご教示くださりありがとうございます。
tmp_test test[];のテーブルの配列が使えることはとても勉強になりました。
ご教示いただいた内容で色々と試してみたいと思います。

回答3件
あなたの回答
tips
プレビュー