PostgreSQL Antipatterns: meneruskan set dan pilihan ke SQL

Dari waktu ke waktu, pengembang perlu memberikan serangkaian parameter ke permintaan atau bahkan seluruh pilihan "input". Solusi yang sangat aneh untuk masalah ini terkadang muncul.

Mari kita “dari kebalikan” dan melihat bagaimana itu tidak layak dilakukan, mengapa, dan bagaimana melakukan yang lebih baik.

Penyisipan langsung nilai ke dalam tubuh permintaan


Biasanya terlihat seperti ini:

query = "SELECT * FROM tbl WHERE id = " + value 

... atau lebih:

 query = "SELECT * FROM tbl WHERE id = :param".format(param=value) 

Tentang metode ini dikatakan, ditulis, dan bahkan digambar dengan berlimpah:



Hampir selalu, ini adalah jalur langsung ke injeksi SQL dan beban tambahan pada logika bisnis, yang dipaksa untuk "merekatkan" string kueri Anda.

Pendekatan semacam itu hanya dapat dibenarkan sebagian jika perlu menggunakan sectioning dalam versi PostgreSQL 10 dan lebih rendah untuk mendapatkan rencana yang lebih efisien. Dalam versi ini, daftar bagian yang dipindai ditentukan bahkan tanpa memperhitungkan parameter yang dikirimkan, hanya berdasarkan badan permintaan.

argumen $ n


Menggunakan penampung parameter adalah baik, ini memungkinkan Anda untuk menggunakan PERNYATAAN YANG DIPERSIAPKAN , mengurangi beban pada kedua logika bisnis (string kueri dihasilkan dan dikirim hanya sekali) dan server database (penguraian ulang dan penjadwalan untuk setiap instance dari permintaan tidak diperlukan).

Jumlah argumen yang bervariasi


Masalah akan menunggu kami saat kami ingin menyampaikan terlebih dahulu sejumlah argumen yang tidak diketahui:

 ... id IN ($1, $2, $3, ...) -- $1 : 2, $2 : 3, $3 : 5, ... 

Jika Anda meninggalkan permintaan dalam formulir ini, maka meskipun itu akan menyelamatkan kami dari kemungkinan suntikan, namun tetap akan mengarah pada kebutuhan untuk mengelem / mem-parsing permintaan untuk setiap opsi dari sejumlah argumen . Sudah lebih baik daripada melakukannya setiap saat, tetapi Anda bisa melakukannya tanpa itu.

Cukup hanya melewatkan satu parameter yang berisi representasi serial array :

 ... id = ANY($1::integer[]) -- $1 : '{2,3,5,8,13}' 

Satu-satunya perbedaan adalah kebutuhan untuk secara eksplisit mengkonversi argumen ke tipe array yang diinginkan. Tapi ini tidak menimbulkan masalah, karena kita sudah tahu sebelumnya di mana kita menangani.

Transfer sampel (matriks)


Biasanya ini adalah segala macam opsi untuk mentransfer set data untuk dimasukkan ke dalam database "dalam satu permintaan":

 INSERT INTO tbl(k, v) VALUES($1,$2),($3,$4),... 

Selain masalah yang dijelaskan di atas dengan "menempel kembali" permintaan, ini juga dapat menyebabkan kehabisan memori dan server crash. Alasannya sederhana - PG menyimpan memori tambahan untuk argumen, dan jumlah catatan di set hanya dibatasi oleh logika bisnis Wishlist yang diterapkan. Dalam kasus-kasus klinis khususnya, orang harus melihat argumen "bernomor" lebih dari $ 9.000 - tidak perlu.

Kami menulis ulang permintaan, menerapkan serialisasi "dua tingkat" :

 INSERT INTO tbl SELECT unnest[1]::text k , unnest[2]::integer v FROM ( SELECT unnest($1::text[])::text[] -- $1 : '{"{a,1}","{b,2}","{c,3}","{d,4}"}' ) T; 

Ya, dalam kasus nilai "kompleks" di dalam array, mereka harus dikelilingi oleh tanda kutip.
Jelas bahwa dengan cara ini Anda dapat "memperluas" seleksi dengan jumlah bidang yang berubah-ubah.

tidak, tidak, ...


Secara berkala, ada opsi transmisi alih-alih “array array” dari beberapa “array kolom”, yang saya sebutkan di artikel sebelumnya :

 SELECT unnest($1::text[]) k , unnest($2::integer[]) v; 

Dengan metode ini, membuat kesalahan saat membuat daftar nilai untuk kolom yang berbeda, sangat mudah untuk mendapatkan hasil yang benar-benar tidak terduga , yang juga tergantung pada versi server:

 -- $1 : '{a,b,c}', $2 : '{1,2}' -- PostgreSQL 9.4 k | v ----- a | 1 b | 2 c | 1 a | 2 b | 1 c | 2 -- PostgreSQL 11 k | v ----- a | 1 b | 2 c | 

Json


Dimulai dengan versi 9.3, PostgreSQL memperkenalkan fungsi lengkap untuk bekerja dengan tipe json. Oleh karena itu, jika definisi parameter input di browser Anda berlangsung, Anda dapat membuat objek json untuk permintaan SQL di sana:

 SELECT key k , value v FROM json_each($1::json); -- '{"a":1,"b":2,"c":3,"d":4}' 

Untuk versi sebelumnya, metode yang sama dapat digunakan untuk masing - masing (hstore) , tetapi "konvolusi" yang benar dengan melarikan diri objek kompleks di hstore dapat menyebabkan masalah.

json_populate_recordset


Jika Anda tahu sebelumnya bahwa data dari "input" json array akan masuk untuk mengisi beberapa jenis tabel, Anda dapat menyimpan banyak dalam "dereferencing" bidang dan casting ke tipe yang diperlukan menggunakan fungsi json_populate_recordset:

 SELECT * FROM json_populate_recordset( NULL::pg_class , $1::json -- $1 : '[{"relname":"pg_class","oid":1262},{"relname":"pg_namespace","oid":2615}]' ); 

json_to_recordset


Dan fungsi ini hanya "memperluas" array yang ditransfer objek ke dalam seleksi, tanpa bergantung pada format tabel:
 SELECT * FROM json_to_recordset($1::json) T(k text, v integer); -- $1 : '[{"k":"a","v":1},{"k":"b","v":2}]' k | v ----- a | 1 b | 2 

MEJA SEMENTARA


Tetapi jika jumlah data dalam sampel yang ditransmisikan sangat besar, maka melemparkannya ke dalam satu parameter berseri adalah sulit, dan kadang-kadang tidak mungkin, karena memerlukan alokasi satu kali dari sejumlah besar memori . Misalnya, Anda perlu mengumpulkan paket besar data tentang peristiwa dari sistem eksternal untuk waktu yang sangat lama, dan kemudian Anda ingin memprosesnya sekali di sisi basis data.

Dalam hal ini, solusi terbaik adalah dengan menggunakan tabel sementara :

 CREATE TEMPORARY TABLE tbl(k text, v integer); ... INSERT INTO tbl(k, v) VALUES($1, $2); --  -  ... --   -       

Metode ini baik untuk pengiriman data dalam jumlah besar yang langka .
Dari sudut pandang menggambarkan struktur datanya, tabel sementara berbeda dari yang "normal" hanya dengan satu fitur dalam tabel sistem pg_class , dan dalam pg_type, pg_depend, pg_attribute, pg_attrdef, ... - tidak ada sama sekali.

Oleh karena itu, dalam sistem web dengan sejumlah besar koneksi berumur pendek untuk masing-masingnya, tabel seperti itu akan menghasilkan catatan sistem baru setiap kali, yang dihapus dengan koneksi ke database ditutup. Akibatnya, penggunaan TEMP TABLE yang tidak terkontrol menyebabkan "pembengkakan" tabel di pg_catalog dan memperlambat banyak operasi yang menggunakannya.
Tentu saja, ini dapat diperjuangkan dengan bantuan lulus berkala VACUUM FULL melalui tabel katalog sistem.

Variabel sesi


Misalkan pemrosesan data dari kasus sebelumnya cukup rumit untuk satu query SQL, tetapi Anda cukup sering melakukannya. Artinya, kami ingin menggunakan pemrosesan prosedural di blok DO , tetapi menggunakan transfer data melalui tabel sementara akan terlalu mahal.

Kami tidak akan dapat menggunakan $ n-parameter untuk mentransfer ke blok anonim. Variabel sesi dan fungsi current_setting akan membantu kita keluar dari situasi ini.

Sebelum versi 9.2, perlu untuk mengkonfigurasi ruang nama custom_variable_classes untuk variabel sesi "Anda". Pada versi saat ini, Anda dapat menulis sesuatu seperti ini:

 SET my.val = '{1,2,3}'; DO $$ DECLARE id integer; BEGIN FOR id IN (SELECT unnest(current_setting('my.val')::integer[])) LOOP RAISE NOTICE 'id : %', id; END LOOP; END; $$ LANGUAGE plpgsql; -- NOTICE: id : 1 -- NOTICE: id : 2 -- NOTICE: id : 3 

Bahasa prosedural yang didukung lainnya dapat menemukan solusi lain.

Apakah Anda tahu lebih banyak cara? Bagikan komentar Anda!

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


All Articles