Masalah dengan variabel terkait: cara mengubah pengoptimal dari musuh ke teman

Penulis artikel ini adalah Victor Varlamov ( varlamovVp18 ), OCP.
Artikel asli diterbitkan pada 07/07/2017.
Terima kasih khusus kepada penulis terjemahan - brutaltag .

Sistem pelaporan kami biasanya mengeksekusi ratusan permintaan panjang yang dipicu oleh berbagai peristiwa. Parameter kueri adalah daftar klien dan interval waktu (harian, mingguan, bulanan). Karena data yang tidak rata dalam tabel, satu kueri dapat menghasilkan satu baris dan satu juta baris, tergantung pada parameter laporan (klien yang berbeda memiliki jumlah baris yang berbeda dalam tabel fakta). Setiap laporan dibuat dalam bentuk paket dengan fungsi utama yang mengambil parameter input, melakukan transformasi tambahan, lalu membuka kursor statis dengan variabel terkait dan akhirnya mengembalikan kursor terbuka ini. Parameter DB CURSOR_SHARING diatur ke FORCE.
Dalam situasi seperti itu, kita harus berurusan dengan kinerja yang buruk, baik dalam hal penggunaan kembali rencana permintaan oleh pengoptimal, dan dengan analisis lengkap permintaan dengan parameter dalam bentuk literal . Variabel terikat dapat menyebabkan rencana kueri optimal.

Dalam bukunya "Oracle Expert Practices," Alex Gorbachev memberikan kisah menarik yang diceritakan oleh Tom Kite. Setiap Senin hujan, pengguna harus berurusan dengan rencana kueri yang dimodifikasi. Sulit dipercaya, tapi itu:
“Menurut pengamatan pengguna akhir, saat hujan deras pada hari Senin, kinerja basis data sangat buruk. Pada hari lain dalam seminggu atau pada hari Senin tidak ada masalah tanpa hujan. Dari percakapan dengan DBA, Tom Kite mengetahui bahwa kesulitan berlanjut sampai database dipaksa untuk memulai kembali, setelah itu kinerja menjadi normal. Itu solusi yang sangat baik: hari Senin hujan - mulai lagi. ”

Ini adalah kasus nyata, dan masalahnya diselesaikan sepenuhnya tanpa sihir, hanya berkat pengetahuan yang sangat baik tentang cara kerja Oracle. Saya akan menunjukkan solusinya di akhir artikel.
Ini adalah contoh kecil bagaimana variabel terkait bekerja.
Buat tabel dengan data tidak rata.

SQL> CREATE TABLE VVP_HARD_PARSE_TEST(C1 NUMBER, C2 NUMBER, C3 VARCHAR2(300)); TABLE created. SQL> INSERT INTO VVP_HARD_PARSE_TEST SELECT ROWNUM C1, CASE WHEN LEVEL < 9 THEN 1 WHEN MOD(ROWNUM, 100)=99 THEN 99 ELSE 1000000 END C2, RPAD('A', 300, 'A') C3 FROM DUAL CONNECT BY LEVEL CREATE INDEX IND_VVP_HARD_PARSE_TEST_C2 ON VVP_HARD_PARSE_TEST(C2); INDEX created. SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME => USER, TABNAME => 'VVP_HARD_PARSE_TEST', CASCADE => TRUE, METHOD_OPT=>'FOR ALL INDEXED COLUMNS SIZE 254'); PL/SQL PROCEDURE successfully completed. SQL> SELECT histogram FROM user_tab_columns WHERE table_name = 'VVP_HARD_PARSE_TEST' AND column_name = 'C2'; HISTOGRAM --------- FREQUENCY SQL> SELECT c2, COUNT(*) FROM VVP_HARD_PARSE_TEST GROUP BY c2 ORDER BY 1; C2 COUNT(*) ----------------------- 1 8 99 10000 1000000 989992 


Dengan kata lain, kami memiliki tabel VVP_HARD_PARSE_TEST dengan sejuta baris, di mana dalam 10.000 kasus bidang adalah C2 = 99, 8 catatan dengan C2 = 1, dan sisanya dengan C2 = 1.000.000. Histogram melintasi bidang C2 menunjukkan pengoptimal Oracle tentang distribusi data ini. Situasi ini dikenal sebagai distribusi data yang tidak merata , dan histogram dapat membantu Anda memilih paket kueri yang tepat berdasarkan data yang diminta.

Kami mengamati pertanyaan sederhana ke tabel ini. Jelas untuk permintaan itu

SELECT * FROM VVP_HARD_PARSE_TEST WHERE c2 = :p

jika p = 1, maka INDEX RANGE SCAN adalah pilihan terbaik, untuk kasus p = 1000000 lebih baik menggunakan FULL TABLE SCAN. Query1 dan Query1000000 queries identik, dengan pengecualian teks dalam komentar, ini dilakukan untuk mendapatkan pengidentifikasi rencana kueri yang berbeda.

 DECLARE p NUMBER; v NUMBER; BEGIN V := 0; p := 1000000; FOR rec IN (SELECT /*+query1000000*/ * FROM VVP_HARD_PARSE_TEST WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); v : =0; p := 1; FOR rec IN (SELECT /*+query1000000*/ * FROM VVP_HARD_PARSE_TEST WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); ----------------- V := 0; p := 1; FOR rec IN (SELECT /*+query1*/ * FROM VVP_HARD_PARSE_TEST WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); v := 0; p := 1000000; FOR rec IN (SELECT /*+query1*/ * FROM VVP_HARD_PARSE_TEST WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); END; 

Sekarang mari kita lihat rencana permintaan:

 SQL> SELECT sql_id, child_number, executions, plan_hash_value, sql_text FROM v$sql WHERE sql_text LIKE 'SELECT % * FROM VVP_HARD_PARSE_TEST WHERE C2%'; SQL_ID CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE SQL_TEXT ------------------------------------------------- 7rqnhhp6pahw2 0 2 2782757451 SELECT /*+query1000000*/ * FROM VVP_HARD_PARSE_TEST WHERE C2 = :B1 7xwt28hvw3u9s 0 2 2463783749 SELECT /*+query1*/ * FROM VVP_HARD_PARSE_TEST WHERE C2 = :B1 SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '7rqnhhp6pahw2', format => 'basic +peeked_binds')); SELECT /*+query1000000*/ * FROM VVP_HARD_PARSE_TEST WHERE C2 = :B1 PLAN hash VALUE: 2782757451 ------------------------------------------------- | Id | Operation | Name | ------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | TABLE ACCESS FULL| VVP_HARD_PARSE_TEST | ------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1000000 SQl> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '7xwt28hvw3u9s', format => 'basic +peeked_binds')); SELECT /*+query1*/ * FROM VVP_HARD_PARSE_TEST WHERE C2 = :B1 PLAN hash VALUE: 2463783749 ------------------------------------------------------------------ | Id | Operation | Name | ------------------------------------------------------------------ | 0 | SELECT STATEMENT | | | 1 | TABLE ACCESS BY INDEX ROWID| VVP_HARD_PARSE_TEST | | 2 | INDEX RANGE SCAN | IND_VVP_HARD_PARSE_TEST_C2 | ------------------------------------------------------------------ Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1 

Seperti yang Anda lihat, rencana untuk berbagai permintaan dibuat hanya sekali, pada saat eksekusi pertama (hanya satu kursor anak dengan CHILD_NUMBER = 0 ada untuk setiap permintaan). Setiap permintaan dieksekusi dua kali (EXECUTION = 2). Selama penguraian keras, Oracle mengambil nilai dari variabel terkait dan memilih paket sesuai dengan nilai-nilai ini. Tapi dia menggunakan rencana yang sama untuk putaran berikutnya, meskipun variabel terkait berubah pada putaran kedua. Rencana non-optimal digunakan - Query1000000 dengan variabel C2 = 1 menggunakan FULL TABLE SCAN, bukan INDEX RANGE SCAN, dan sebaliknya.

Jelas bahwa memperbaiki aplikasi dan menggunakan parameter sebagai literal dalam kueri adalah cara yang paling cocok untuk memecahkan masalah, tetapi mengarah ke SQL dinamis dengan kekurangan yang diketahui. Cara lain adalah dengan menonaktifkan kueri untuk variabel terkait ( ALTER SESSION SET "_OPTIM_PEEK_USER_BINDS" = FALSE ) atau hapus histogram ( tautan ).

Salah satu solusi yang mungkin adalah penggunaan alternatif kebijakan akses data, juga dikenal sebagai Virtual Private Database (kontrol akses terperinci, Fine Grained Access Control , kontrol level baris). Ini memungkinkan Anda untuk mengubah permintaan dengan cepat dan karenanya dapat menyebabkan analisis lengkap dari rencana permintaan setiap kali permintaan menggunakan kontrol akses terperinci. Teknik ini dijelaskan secara rinci dalam sebuah artikel oleh Randalph Geist . Kerugian dari metode ini adalah meningkatnya jumlah parse lengkap dan ketidakmampuan untuk memanipulasi rencana kueri.

Lihat apa yang akan kita lakukan sekarang. Setelah menganalisis data kami, kami memutuskan untuk membagi pelanggan menjadi tiga kategori - Besar, Menengah dan Kecil (LMS atau 9-5-1) - sesuai dengan jumlah transaksi atau transaksi selama tahun tersebut. Juga, jumlah baris dalam laporan sangat tergantung pada periode: Bulanan - Besar, Mingguan - Tengah, Harian - Kecil atau 9-5-1. Selanjutnya, solusinya sederhana - kami akan membuat predikat kebijakan keamanan tergantung pada setiap kategori dan pada setiap periode. Jadi, untuk setiap permintaan kami mendapatkan 9 kursor anak yang memungkinkan. Selain itu, kueri dengan kebijakan berbeda akan mengarahkan kita ke pengidentifikasi kueri yang sama, ini memungkinkan untuk mengimplementasikan SQL PLAN MANAGEMENT (baseline rencana sql).

 SQL> CREATE TABLE HARD_PARSE_TABLE AS SELECT * FROM dual; TABLE created. SQL> CREATE TABLE CLIENTS_HP_STATISTICS (client_seqno NUMBER, client_id VARCHAR2(255), cnt_year NUMBER); TABLE created. SQL> INSERT INTO CLIENTS_HP_STATISTICS (client_seqno, client_id, cnt_year) VALUES (1, 'SMALL CLIENT', 8); 1 ROW inserted. SQL> INSERT INTO CLIENTS_HP_STATISTICS (client_seqno, client_id, cnt_year) VALUES (99, 'MIDDLE CLIENT', 50001); 1 ROW inserted. SQL> INSERT INTO CLIENTS_HP_STATISTICS (client_seqno, client_id, cnt_year) VALUES (1000000,'LARGE CLIENT', 989992); 1 ROW inserted. SQL> CREATE OR REPLACE PACKAGE FORCE_HARD_PARSE_PKG IS gc_small CONSTANT NUMBER := 1; gc_middle CONSTANT NUMBER := 5; gc_large CONSTANT NUMBER := 9; gc_client_middle CONSTANT NUMBER := 50000; gc_client_large CONSTANT NUMBER := 500000; gc_daterange_middle CONSTANT NUMBER := 10; gc_daterange_large CONSTANT NUMBER := 50; FUNCTION FORCE_HARD_PARSE(in_schema VARCHAR2, in_object VARCHAR2) RETURN VARCHAR2; PROCEDURE SET_PREDICATE (n NUMBER); PROCEDURE SET_PREDICATES (p_daterange NUMBER DEFAULT NULL, p_clientrange NUMBER DEFAULT NULL); PROCEDURE CALC_PREDICATE; PROCEDURE CALC_PREDICATES(p_date_interval NUMBER DEFAULT 1, p_client_seqno NUMBER DEFAULT NULL, p_client_id VARCHAR2 DEFAULT NULL, p_client_seqno_list VARCHAR2 DEFAULT NULL ); END FORCE_HARD_PARSE_PKG; PACKAGE created. SQL> CREATE OR REPLACE PACKAGE BODY FORCE_HARD_PARSE_PKG IS g_predicate NUMBER; -- g_daterange || 0 || g_clientrange g_daterange NUMBER; -- 1 - small, 5 - middle, 9 - large g_clientrange NUMBER; -- 1 - small, 5 - middle, 9 - large -- FUNCTION FORCE_HARD_PARSE(in_schema VARCHAR2, in_object VARCHAR2) RETURN VARCHAR2 IS BEGIN IF NVL(g_predicate, 0) = 0 THEN RETURN NULL; ELSE RETURN TO_CHAR(g_predicate, 'TM') || ' = ' || TO_CHAR(g_predicate, 'TM'); END IF; END FORCE_HARD_PARSE; -- PROCEDURE SET_PREDICATE (n NUMBER) IS BEGIN g_predicate := n; END; PROCEDURE SET_PREDICATES (p_daterange NUMBER DEFAULT NULL, p_clientrange NUMBER DEFAULT NULL) IS BEGIN IF p_daterange IS NOT NULL THEN g_daterange := p_daterange; CALC_PREDICATE; END IF; IF p_clientrange IS NOT NULL THEN g_clientrange := p_clientrange; CALC_PREDICATE; END IF; END SET_PREDICATES; PROCEDURE CALC_PREDICATE IS BEGIN g_predicate := NVL(g_daterange, 0) * 100 + NVL(g_clientrange, 0); END CALC_PREDICATE; PROCEDURE CALC_PREDICATES (p_date_interval NUMBER DEFAULT 1, p_client_seqno NUMBER DEFAULT NULL, p_client_id VARCHAR2 DEFAULT NULL, p_client_seqno_list VARCHAR2 DEFAULT NULL) IS v_cnt NUMBER; BEGIN IF p_date_interval IS NOT NULL THEN g_daterange := CASE WHEN p_date_interval < gc_daterange_middle THEN gc_small WHEN p_date_interval < gc_daterange_large THEN gc_middle ELSE gc_large END; CALC_PREDICATE; END IF; IF COALESCE(p_client_seqno, p_client_id, p_client_seqno_list) IS NOT NULL THEN SELECT NVL(SUM(cnt_year), 0) AS cnt INTO v_cnt FROM CLIENTS_HP_STATISTICS t WHERE 1=1 AND (p_client_seqno IS NULL OR p_client_seqno = t.client_seqno) AND (p_client_id IS NULL OR p_client_id = t.client_id) AND (p_client_seqno_list IS NULL OR t.client_seqno IN (SELECT SUBSTR(s, CASE WHEN LEVEL > 1 THEN INSTR(s, ',', 1, LEVEL - 1 ) + 1 ELSE 1 END, INSTR(s, ',', 1, LEVEL) – CASE WHEN LEVEL > 1 THEN INSTR(s, ',', 1, LEVEL – 1) + 1 ELSE 1 END) FROM (SELECT p_client_seqno_list||',' AS s FROM DUAL) CONNECT BY INSTR(s, ',', 1, LEVEL) > 0)); g_clientrange := CASE WHEN v_cnt > gc_client_large THEN gc_large WHEN v_cnt > gc_client_middle THEN gc_middle ELSE gc_small END; CALC_PREDICATE; END IF; END CALC_PREDICATES; END FORCE_HARD_PARSE_PKG; PACKAGE BODY created. SQL> EXEC DBMS_RLS.ADD_POLICY (USER, 'HARD_PARSE_TABLE', 'HARD_PARSE_POLICY', USER, 'FORCE_HARD_PARSE_PKG.FORCE_HARD_PARSE', 'select'); PL/SQL PROCEDURE successfully completed. 

Sekarang, jika kami ingin menanamkan teknologi seperti itu dalam laporan, kami perlu menambahkan HARD_PARSE_TABLE ke kueri (ini tidak akan merusaknya sedikit pun) dan panggil CALC_PREDICATES sebelum kueri utama dijalankan.

Mari kita lihat bagaimana teknik ini dapat mengubah contoh sebelumnya:

 DECLARE p NUMBER; v NUMBER; BEGIN V := 0; p := 1000000; FORCE_HARD_PARSE_PKG.SET_PREDICATE(1000000); FOR rec IN (SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); v := 0; p := 1; FORCE_HARD_PARSE_PKG.SET_PREDICATE(1); FOR rec IN (SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); ----------------- V := 0; p := 1; FORCE_HARD_PARSE_PKG.SET_PREDICATE(1); FOR rec IN (SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); v := 0; p := 1000000; FORCE_HARD_PARSE_PKG.SET_PREDICATE(1000000); FOR rec IN (SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); END; 

Mari kita lihat rencana eksekusi:

 SQL> SELECT sql_id, child_number, executions, plan_hash_value, sql_text, s.* FROM v$sql s WHERE sql_text LIKE 'SELECT % * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2%' ORDER BY 1,2; SQL_ID CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE SQL_TEXT -------------------------------------------------------------------------------- 7wva3uqbgh4qf 0 1 1136240498 SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 7wva3uqbgh4qf 1 1 3246475190 SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 8cju3tfjvwm1p 0 1 3246475190 SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 8cju3tfjvwm1p 1 1 1136240498 SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 -- SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '7wva3uqbgh4qf', cursor_child_no => 0, format => 'basic +peeked_binds')); SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 PLAN hash VALUE: 1136240498 ---------------------------------------------------- | Id | Operation | Name | ---------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | MERGE JOIN CARTESIAN| | | 2 | TABLE ACCESS FULL | HARD_PARSE_TABLE | | 3 | BUFFER SORT | | | 4 | TABLE ACCESS FULL | VVP_HARD_PARSE_TEST | ---------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1000000 -- SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '7wva3uqbgh4qf', cursor_child_no => 1, format => 'basic +peeked_binds')); SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 PLAN hash VALUE: 3246475190 -------------------------------------------------------------------- | Id | Operation | Name | -------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | MERGE JOIN CARTESIAN | | | 2 | TABLE ACCESS FULL | HARD_PARSE_TABLE | | 3 | BUFFER SORT | | | 4 | TABLE ACCESS BY INDEX ROWID| VVP_HARD_PARSE_TEST | | 5 | INDEX RANGE SCAN | IND_VVP_HARD_PARSE_TEST_C2 | -------------------------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1 -- SQl> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '8cju3tfjvwm1p', cursor_child_no => 0, format => 'basic +peeked_binds')); SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 PLAN hash VALUE: 3246475190 -------------------------------------------------------------------- | Id | Operation | Name | -------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | MERGE JOIN CARTESIAN | | | 2 | TABLE ACCESS FULL | HARD_PARSE_TABLE | | 3 | BUFFER SORT | | | 4 | TABLE ACCESS BY INDEX ROWID| VVP_HARD_PARSE_TEST | | 5 | INDEX RANGE SCAN | IND_VVP_HARD_PARSE_TEST_C2 | -------------------------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1 -- SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '8cju3tfjvwm1p', cursor_child_no => 1, format => 'basic +peeked_binds')); SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 PLAN hash VALUE: 1136240498 ---------------------------------------------------- | Id | Operation | Name | ---------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | MERGE JOIN CARTESIAN| | | 2 | TABLE ACCESS FULL | HARD_PARSE_TABLE | | 3 | BUFFER SORT | | | 4 | TABLE ACCESS FULL | VVP_HARD_PARSE_TEST | ---------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1000000 

Tampak hebat! Setiap kueri dieksekusi dua kali, dengan kursor anak berbeda dan paket berbeda. Untuk parameter C2 = 1.000.000, kita melihat FULL TABLE SCAN di kedua kueri, dan untuk parameter C1 = 1 kita selalu melihat INDEX RANGE SCAN.

Pada akhirnya saya memberikan solusi untuk kasus Senin hujan:

“Ternyata setiap akhir pekan pada hari Minggu ada cadangan dingin , sehingga semua rencana permintaan dibuat kembali pada eksekusi pertama pada Senin pagi. Salah satu karyawan biasanya memulai pekerjaan mereka lebih awal dari yang lain, dan rencana permintaannya dieksekusi dengan baik untuk pengguna lain selama seminggu. Namun, jika hujan, pengguna ini terlambat pada awal hari kerja karena masalah dengan rute paginya. Kemudian perhitungan batch laporan adalah yang pertama kali dimulai, tetapi rencana kueri benar-benar buruk untuk kasus yang tersisa karena nilai yang tidak sesuai dari variabel terkait. "

Dan beberapa tampilan sistem yang berguna:
dba_tab_histograms, all_tab_histograms, user_tab_histograms
v$vpd_policy
v$sql_bind_capture
dba_hist_sqlbind

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


All Articles