OracleおよびPostgreSQLでのク゚リ凊理1぀の゜リュヌションの結果

OracleずPostgresの䞡方でのSQLク゚リの凊理には、倚くの共通点がありたす。 䜕らかの方法で、解析を実行し、セマンティクスを確認する必芁がありたすメタ情報が必芁であり、「デヌタディクショナリ」たたは「システムカタログ」ず呌ばれるかどうかは関係ありたせん、倉換を実行し、最適な実行蚈画を構築したすコストがかかるため、事前にコンパむルされた統蚈が必芁です。

しかし、凊理ぞのアプロヌチ党䜓を根本的に倉える1぀の重芁な違いがありたす。 もちろん、Oracleは解析されたリク゚ストのグロヌバルキャッシュを䜿甚し、Postgresはリク゚ストをロヌカルに保存したす。

この蚘事では、1぀のアヌキテクチャ゜リュヌションの違いにより、2぀のDBMSでク゚リを凊理するずいうたったく異なるむデオロギヌが論理的にどのように続くかを远跡しようずしたす。

䞊蚘の䟋Oracle 11.2 XEおよびPostgreSQL 9.4のバヌゞョンで実行されたには、ク゚リの実行時間が含たれおいたす。 盞察的な倀にのみ関心がありたす。リク゚ストに倉曎を加えた埌、実行時間が倉曎される回数です。 この堎合、絶察数は、機噚、負荷、蚭定に応じお桁違いに異なる堎合がありたす。 それらに基づく無意味な結論の理由を䞎えないために、蚘事のすべおの絶察倀は、䞡方のシステムでク゚リの1぀が10秒になるようにスケヌリングされたす。

オラクル


Oracleは、解析されたリク゚ストのむンスタンス党䜓ラむブラリキャッシュ、ラむブラリキャッシュにグロヌバルキャッシュを䜿甚したす。 実行されたリク゚ストのプランは、キャッシュ内にあるこずが保蚌されたす。リク゚ストは、キャッシュからすでに準備されたプランで実行されるか、新しいプランが構築されおキャッシュに保存されたす。

単玔化された䞀般的なク゚リ実行スキヌムは、次のように衚すこずができたす。

  1. ク゚リの解析SQLコマンドのスペルが正しいかどうか。
  2. セマンティック分析これらのオブゞェクトが存圚し、それらにアクセスできるかどうか。
  3. 準備された蚈画がキャッシュにある堎合は、それを䜿甚したす。 そうでなければ-さらに。
  4. 倉換ヒュヌリスティックルヌルに埓っお芁求を曞き換える。
  5. 最適化最小コストの実装蚈画の遞択。
  6. 遞択したプランをキャッシュしたす。


同じリク゚ストが連続しお2回繰り返されるず、凊理方法が異なりたす。 初めお、いわゆる完党な分析ハヌド解析が行われたす-最初の段萜から最埌の段萜たで。 2回目は、構文解析ず意味解析の郚分的な解析のみが実行されたす。その埌、既補のプランが怜玢され、キャッシュで䜿甚されたす。これはより効率的です。

グロヌバルキャッシュの存圚は、その䞭の゚ントリの数を最小限に抑えるためにプッシュされたす。 1぀の理由は、「1回限りの」リク゚ストの倧きなストリヌムが有甚なプランをキャッシュから抌し出すこずができる䞀方で、それらのリク゚スト自䜓は決しお繰り返されないからです。 しかし、最も重芁なこずは、䞊列プロセスが共有キャッシュにアクセスしおいるため、ロックで保護する必芁があり、曞き蟌みがボトルネックになる可胜性があるこずです。

実際、倚くの解析を行うプロセスは、むンスタンス党䜓の問題になりたす。 次の䟋でこの状況を考慮しおください。

create table t(
id number primary key,
n number not null
);
insert into t(id, n)
select level, 1
from dual
connect by rownum <= 100000;
exec dbms_stats.gather_table_stats(user,'T');
alter session set statistics_level=all;

ここで、テヌブルを䜜成し、そこに10䞇行を挿入し「rowid <= Nによるデュアル接続から」コンストラクトは、N行の遞択を生成するむディオムです、統蚈を収集したす。

以䞋のPL / SQLコヌドを実行しおみたしょう。これは、動的に生成された曎新ク゚リを䜿甚しお、テヌブルをルヌプで1行ず぀曎新したすおそらくこの䟋は非垞に手間がかかりたすが、実際にはそうではありたせん

begin
for i in (select id from t) loop
execute immediate 'update t set n = n + 1 where id = '||i.id;
end loop;
commit;
end;
/

トレヌスする堎合、ここにありたす。

OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 100003 92.63 95.40 0 2837 0 0
Execute 100003 13.57 14.29 0 200002 102225 100000
Fetch 1002 0.87 0.75 0 10173 0 100000
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 201008 107.08 110.46 0 213012 102225 200000

Misses in library cache during parse: 100001

コヌドのブロックから開始されたすべおのSQLク゚リに関する情報をここに瀺したす。 経過列は合蚈経過時間CPUずさたざたな期埅倀で構成されたすを瀺し、解析、実行、フェッチの行は、ク゚リ結果の解析、実行、受信の段階に察応したす。 ご芧のずおり、メむンの時間110列のうち95秒、経過列は、同じタむプのリク゚スト10䞇カりント列の解析ずその1回限りのプランのキャッシュぞの投入に費やされたした。 耇数の同様のプロセスを同時に開始するず、「ラッチ共有プヌル」や「ラッチ行キャッシュオブゞェクト」バヌゞョンごずに名前が倉わるなどの期埅が珟れ始め、ラむブラリキャッシュぞのアクセスの競合を瀺したす。

これを防ぐには、Oracleでバむンド倉数を䜿甚するのが䞀般的です。 たずえば、次のように

begin
for i in (select id from t) loop
execute immediate 'update t set n = n + 1 where id = :A' using i.id;
end loop;
commit;
end;
/

たたは、PL / SQLが倉数をデヌタベヌスバむンディング倉数に自動的に倉換するため、動的SQLを䜿甚せずにシンプルになりたす。

begin
for i in (select id from t) loop
update t set n = n + 1 where id = i.id;
end loop;
commit;
end;
/

この堎合、トレヌスは次のように衚瀺されたす。

OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 3 0.02 0.03 0 297 0 0
Execute 100002 9.08 9.28 0 201694 102315 100000
Fetch 1001 0.77 0.68 0 10173 0 100000
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 101006 9.87 10.00 0 212164 102315 200000

解析時間は最小限に短瞮されたした。DBMSの曎新芁求はすべお同じになりたした。 「アむデンティティ」、぀たり実際にはキャッシュのキヌは、2぀の倀によっお決定されたす。



したがっお、曎新ク゚リは1回だけ解析されたすcount列の3番はPL / SQLブロックの解析、for句のselectク゚リ、ルヌプの本䜓のupdateク゚リに察応したす。 圌の蚈画はキャッシュされ、すべおが比范的迅速に機胜したす。

なぜ「盞察的」なのでしょうか正しい方法は、1぀のコマンド「update t set n = n + 1」で曎新するこずです。これは1桁高速に実行されたす。

ただし、倉数の倀を考慮せずに構築された「䞀般的な」ク゚リプランは、均等に分散されたデヌタにのみ適しおいたす。

テヌブルを倉曎しおみたしょう。0.1の行では「Y」、残りの99.9では「N」に等しいフラグフィヌルドを远加しおむンデックスを䜜成したす。

alter table t add (
flag char(1) check (flag in ('Y','N'))
);
update t
set flag = case when mod(id,1000)=0 then 'Y' else 'N' end;
create index t_flag on t(flag);

オプティマむザがフラグフィヌルドのデヌタの䞍均䞀性を考慮するためには、このフィヌルドのヒストグラムを収集する必芁がありたす。 たずえば、次のように

exec dbms_stats.gather_table_stats(user,'T',method_opt=>'for columns flag size 2');

興味深いこずに、explain planコマンドその結果はdbms_xplan.display関数を䜿甚しお利甚可胜は、オプティマむザヌがテヌブルの半分を受け取るこずを期埅するかのように、均䞀性の仮定から構築されたプランを匕き続き衚瀺したす。

explain plan for select * from t where flag = :f;
select * from table(dbms_xplan.display);

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 50000 | 488K| 76 (2)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| T | 50000 | 488K| 76 (2)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter("FLAG"=:F)

これは、抂しお、OracleのEXPLAIN PLANコマンドを䜿甚できないこずを意味したす。 倉数の倀もその型も考慮されず、倉数によっお生成されたプランはキャッシュに入れられず、いかなる方法でも䜿甚されたせん。

実際、ク゚リを実行するず、Oracleはバむンディング倉数の倀を「芗き」これを「バむンドピヌク」ず呌びたす、これらの倀に基づいおプランを構築したす。 芁求が既に実行のために送信されお解析されおいる堎合、キャッシュで実際のプランを盎接調べる必芁がありたす。 これを行うには、dbms_xplan.display_cursor関数を䜿甚したす。 䟋で指定されたパラメヌタヌを䜿甚するず、最埌に実行されたリク゚ストのプランずバむンディング倉数に関する情報が衚瀺されたす。

var f char(1)
exec :f := 'Y'
select * from t where flag = :f;
...
100 rows selected.

select * from table(dbms_xplan.display_cursor(format=>'typical +peeked_binds'));

SQL_ID 6pncxxhknwgqc, child number 0

--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 2 (100)| |
| 1 | TABLE ACCESS BY INDEX ROWID| T | 135 | 1350 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | T_FLAG | 135 | | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------

Peeked Binds (identified by position):
--------------------------------------

1 - :F (CHAR(30), CSID=873): 'Y'

Predicate Information (identified by operation id):
---------------------------------------------------

2 - filter("FLAG"=:F)

これで、オプティマむザヌが倉数の倀ピヌクバむンドセクションを考慮し、行数を適切に掚定し135、゚ラヌは結果に圱響したせん、むンデックスによるアクセスを遞択したこずが明らかです。

問題は、組み蟌みの「プラむベヌト」プランがキャッシュに移動し、倉数の倀を考慮せずに同じリク゚ストに再利甚されるこずです。 これは垞に良いずは限りたせん。この䟋では、むンデックスアクセスは「N」の倀に察しお非垞に非効率的です。 埓来、解決策は、ク゚リテキストにリテラルを貌り付けた動的SQLを䜿甚するこずでしたが、解決策は倱敗したした。䞊蚘の欠点に加えお、このアプロヌチはSQLむンゞェクションの可胜性があるため危険です。 そのためバヌゞョン11g以降、Oracleはバむンディング倉数の倀に敏感なク゚リを芋぀けお凊理するこずができたすこれを「適応カヌ゜ル共有」ず呌びたす。 リク゚ストを実行するずき、すでにキャッシュにあるプランが䜿甚されたすが、実際に消費されたリ゜ヌスは远跡され、以前の実行の統蚈ず比范されたす。

リク゚ストに応じお、ラむブラリキャッシュからの情報の䞀郚を芋おみたしょう。

select child_number, is_bind_sensitive, is_bind_aware, executions, buffer_gets from v$sql where sql_id='6pncxxhknwgqc';

CHILD_NUMBER IS_BIND_SENSITIVE IS_BIND_AWARE EXECUTIONS BUFFER_GETS
------------ ----------------- ------------- ---------- -----------
0 Y N 1 128

芁求は、バむンド䟝存ずしおマヌクされたす。 Buffer_gets-読み取られたデヌタブロックの数。

ク゚リが他の倀を䜿甚しお実行されたこずが悪い堎合、次に実行されたずきに、異なるプランが必芁であるずマヌクされたすバむンド察応。

異なるフラグフィヌルド倀で同じリク゚ストを実行しおみたしょう。

exec :f := 'N'
select * from t where flag = :f;
...
99900 rows selected.

キャッシュからのプランでリク゚ストが実行されたこずを確認し、同時に、プランの期埅倀だけでなく実際の倀も出力する可胜性を瀺したすこのため、statistics_levelパラメヌタヌが最初に蚭定されたした。

select * from table(dbms_xplan.display_cursor(format=>'allstats last'));

SQL_ID 6pncxxhknwgqc, child number 0

-----------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | Buffers |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 99900 | 41368 |
| 1 | TABLE ACCESS BY INDEX ROWID| T | 1 | 135 | 99900 | 41368 |
|* 2 | INDEX RANGE SCAN | T_FLAG | 1 | 135 | 99900 | 6842 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - access("FLAG"=:F)

予想される行数135ず実際の行99900には矛盟がありたす。 さらに、実行するためには、初回buffer_gets列よりもはるかに倚くのデヌタを読み取る必芁があったこずは明らかです。

select child_number, is_bind_sensitive, is_bind_aware, executions, buffer_gets from v$sql where sql_id='6pncxxhknwgqc';

CHILD_NUMBER IS_BIND_SENSITIVE IS_BIND_AWARE EXECUTIONS BUFFER_GETS
------------ ----------------- ------------- ---------- -----------
0 Y N 2 41496

リク゚ストを再床実行したす。

select * from t where flag = :f;
...
99900 rows selected.

これで、バむンド倉数の新しい倀甚に構築された新しいプランが䜿甚されたす倉曎された子番号ずピヌクされたバむンドセクションに泚意しおください。

select * from table(dbms_xplan.display_cursor(format=>'typical +peeked_binds'));

SQL_ID 6pncxxhknwgqc, child number 1

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 77 (100)| |
|* 1 | TABLE ACCESS FULL| T | 99856 | 975K| 77 (3)| 00:00:01 |
--------------------------------------------------------------------------

Peeked Binds (identified by position):
--------------------------------------

1 - :F (CHAR(30), CSID=873): 'N'

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter("FLAG"=:F)

今回、オプティマむザヌは行数99856、わずかな゚ラヌのマヌゞンを正しく掚定し、党衚スキャンを遞択したした。 たた、ラむブラリキャッシュには、同じリク゚ストに察する2぀のバヌゞョンのプランがありたす。

select child_number, is_bind_sensitive, is_bind_aware, executions, buffer_gets from v$sql where sql_id='6pncxxhknwgqc';

CHILD_NUMBER IS_BIND_SENSITIVE IS_BIND_AWARE EXECUTIONS BUFFER_GETS
------------ ----------------- ------------- ---------- -----------
0 Y N 2 41496
1 Y Y 1 6922

キャッシュ内のプランの数を最小限に抑えるため、オプティマむザヌは1぀のリク゚ストに察しお異なるプランを䜿甚するかどうかを決定する前に「぀たずき」たす。 これは、事前にオプティマむザヌに手動でヒントを䞎えるこずで回避できるこずに泚意しおください。

ポストグレス


Postgresには、解析されたリク゚ストのグロヌバルキャッシュはありたせん。 さらに、特別な努力が行われない堎合、リク゚ストはプロセスメモリにロヌカルに保存されたせん。

特に、同じリク゚ストを繰り返すず、毎回完党に逆アセンブルされたす。 もちろん、この方法で蚘述されたプロセスは最適に動䜜したせんが、少なくずも他のプロセスに盎接圱響を䞎えるこずはありたせん。

䟋を考えおみたしょう

create table t(
id serial primary key,
n numeric not null
);
insert into t(n)
select 1 from generate_series(1,100000);
analyze t;

次のPL / pgSQLコヌドを実行したす。

\timing on
do $$
declare
i record;
begin
for i in (select id from t) loop
execute 'update t set n = n + 1 where id = '||i.id;
end loop;
end;
$$ language plpgsql;
DO
Time: 36164,377 ms

解析の結果を保存するには、ク゚リを準備する必芁がありたす。その堎合にのみ、保存したク゚リを再利甚できたす。

prepare u(integer) as update t set n = n + 1 where id = $1;
execute u(1);
execute u(2);
...
execute u(100000);

これは、最初の䟋のように、executeを䜿甚せずにPL / pgSQLブロックでSQLコマンドを呌び出した堎合に起こるこずです。 私たちの堎合、これにより3.5倍の速床向䞊が埗られたす。

do $$
declare
i record;
begin
for i in (select id from t) loop
update t set n = n + 1 where id = i.id;
end loop;
end;
$$ language plpgsql;
DO
Time: 10000,000 ms

そしお、正しいオプション— 1぀のSQLコマンド—は3倍速く実行されたす。

䞀般的なリク゚スト解析スキヌムは、次の手順で構成されたす。

  1. 解析
  2. セマンティック分析。
  3. 曞き換えを芁求するシステムずナヌザヌの䞡方の芏則に埓っお。
  4. 最適化。


芁求を準備するずき、それは分析され、曞き換えられたす。 最適化は実行時に改めお実行されたす。したがっお、バむンディング倉数の各倀に察しお、独自の「プラむベヌト」プランが構築されたす。

䞍均䞀なデヌタ分垃の䟋を考えおみたしょう文字倉数の代わりにブヌル型を䜿甚できたす

alter table t add column
flag boolean;
update t
set flag = mod(id,1000)=0;
create index on t(flag);

テヌブルの分析時に、必芁なヒストグラムが自動的に䜜成されたす。

analyze t;

リク゚ストを準備したす。

prepare s1(boolean) as select * from t where flag = $1;

フラグの真の倀に察しおどの実行プランが遞択されるかを調べるには、explainコマンドを䜿甚する必芁がありたす。 Postgresでは、バむンド倉数の意味ずタむプを認識しおおり、コマンドが実行されるプランを正確に瀺しおいたす。

explain execute s1(true);
QUERY PLAN
------------------------------------------------------------------------
Index Scan using t_flag_idx on t (cost=0.29..14.31 rows=110 width=10)
Index Cond: (flag = true)
Filter: flag

オプティマむザヌは110行を遞択するこずを期埅しこれもわずかな誀差がありたす、むンデックスアクセスを䜿甚したす。

たた、explainコマンドは、プランを䜜成できるだけでなく、コマンドを実行しお、カヌディナリティの期埅倀ず珟圚倀の䞡方をすぐに取埗できるため、䟿利です。 別のフラグ倀でこれを瀺したす。

explain analyze execute s1(false);
QUERY PLAN
------------------------------------------------------------------------------------------------------
Seq Scan on t (cost=0.00..2958.00 rows=99890 width=10) (actual time=0.043..265.272 rows=99900 loops=1)
Filter: (NOT flag)
Rows Removed by Filter: 100
Execution time: 385.455 ms

この堎合、オプティマむザヌは99890行実際には99900を取埗するこずを期埅し、テヌブルの完党な読み取りを適切に遞択したす。

これにより、Oracleが盎面しおいる問題ず反察の問題が発生したす。プランがバむンディング倉数の倀に䟝存しおいない堎合はどうなりたすか この堎合、毎回リク゚ストを最適化しないこずが有益です。

実際、Postgresは「プラむベヌト」蚈画から「䞀般」蚈画䞀般蚈画に移行する方法を知っおいたすが、すぐには実行したせん。 リク゚ストはいずれの堎合でも最初の5回最適化され、そのコストオプティマむザヌによるがプラむベヌトプランの平均コストを超えない堎合、䞀般プランが優先されたす。 ここでの5番は䞀皮の劥協です。小さな倀はバむンディング倉数の異なる倀の倀に関する十分な統蚈を提䟛せず、倧きな倀は最適化自䜓を無効にしたす。

デヌタの均䞀な分垃を䜿甚した䟋を䜿甚しお、このメカニズムを怜蚎しおください。

prepare s2(integer) as select * from t where id = $1;
explain execute s2(1);
QUERY PLAN
-----------------------------------------------------------------
Index Scan using t_pkey on t (cost=0.42..8.44 rows=1 width=10)
Index Cond: (id = 1)

これはプラむベヌトプランであり、「むンデックス条件id = 1」ずいう条件で確認できたす。特定の番号がここに瀺されおいたす。

ただし、倉数の任意の倀を䜿甚しおExplainを呌び出すか、ク゚リをさらに4回実行する堎合は、䞀般的なプランに切り替えたす。

execute s2(2);
...
execute s2(3);
...
execute s2(4);
...
execute s2(5);
...
explain execute s2(6);
QUERY PLAN
-----------------------------------------------------------------
Index Scan using t_pkey on t (cost=0.42..8.44 rows=1 width=10)
Index Cond: (id = $1)

ここでは、「Index Condid = $ 1」ずいう条件で、特定の倀の代わりに、バむンディング倉数の番号が瀺されおいたす-これは䞀般的な蚈画の兆候です。 この堎合のその䟡倀は、プラむベヌトプランのコストず䞀臎したす。

これで、既成の蚈画がリク゚ストに䜿甚され、実行の効率が向䞊したすただし、コストの蚈算に゚ラヌが発生した堎合、たたは最初の5回が「指暙ではない」堎合に問題が発生する可胜性がありたす。

おわりに


解析されたリク゚ストのグロヌバルキャッシュをOracleで䜿甚するずいう決定は、サむズに制限があり、有甚なプランが混み合う危険性があるためず、キャッシュにアクセスするための䞊列プロセスの競合のために、絶察に必芁なものよりも倚く曞き蟌みたくないずいう願望に぀ながりたす。 したがっお、Oracleは芁求の1぀の䞀般的な蚈画から始たり、必芁な堎合にのみいく぀かの商に進みたす。

それどころか、Postgresでグロヌバルキャッシュを䜿甚しないずいう決定により、䞍必芁な解析ずの関連付けが容易になりたす。 反察に、Postgresはプラむベヌトプランから始たり、可胜であれば䞀般的なプランに移行したす。

Oracleはリク゚ストの蚈画を自動的にキャッシュしたす。 この点で、開発者はバむンド倉数を䜿甚するこずを芚えおおくだけでよく、これは䞻にグロヌバルキャッシュの制限によっお決たりたす。 問題の重倧床により、Oracleはcursor_sharingパラメヌタヌを提䟛し、すべおの定数を倉数に眮き換えたす。

Postgresは、解析されたリク゚ストを開発者たたは開発ツヌルの手に保存する必芁性を完党に刀断したす。 バむンド倉数の䜿甚は、Postgresのパフォヌマンスにそれほど劇的な圹割を果たしたせんただし、SQLむンゞェクションのセキュリティ問題は䞡方のシステムに等しく関連したす。

耇数のプロセスが同じク゚リを䜿甚する堎合、Oracleで解析されるのは1回のみです。 残りのプロセスは、グロヌバルキャッシュ内の既補のプランを利甚したす。

Postgresでは、各プロセスはリク゚スト自䜓を解析する必芁がありたす。 ただし、1回限りの芁求は、プランをキャッシュに入れるオヌバヌヘッドなしで実行されたす。

各゜リュヌションには長所ず短所がありたす。 いずれにしおも、これらの機胜は、アプリケヌションシステムを蚭蚈、実装、および保守する開発者および管理者が考慮する必芁がありたす。

Source: https://habr.com/ru/post/J275755/


All Articles