Dengan hanya sedikit mengutak-atik dan meningkatkan kueri SQL Postgres Anda, Anda dapat mengurangi jumlah berulang, kode aplikasi rawan kesalahan yang diperlukan untuk berinteraksi dengan database Anda. Lebih sering, perubahan seperti itu juga meningkatkan kinerja kode aplikasi.
Berikut adalah beberapa tip dan trik yang dapat membantu kode aplikasi Anda melakukan outsourcing lebih banyak pekerjaan ke PostgreSQL, dan membuat aplikasi Anda lebih ramping dan lebih cepat.
Upsert
Sejak Postgres v9.5, dimungkinkan untuk menentukan apa yang harus terjadi ketika penyisipan gagal karena "konflik". Konflik dapat berupa pelanggaran indeks unik (termasuk kunci utama) atau batasan apa pun (dibuat sebelumnya menggunakan CREATE CONSTRAINT).
Fitur ini dapat digunakan untuk menyederhanakan logika aplikasi insert-or-update ke dalam satu pernyataan SQL. Misalnya, diberikan tabel kv dengan kunci dan nilai kolom, pernyataan di bawah ini akan menyisipkan baris baru (jika tabel tidak memiliki baris dengan key='host') atau memperbarui nilainya (jika tabel memiliki baris dengan key='host'):
CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT);
INSERT INTO kv (key, value)
VALUES ('host', '10.0.10.1')
ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value;
Perhatikan bahwa kolom key
adalah kunci utama satu kolom dari tabel, dan ditetapkan sebagai klausa konflik. Jika Anda memiliki kunci utama dengan banyak kolom, tentukan nama indeks kunci utama di sini.
Untuk contoh lanjutan, termasuk menentukan indeks dan batasan parsial, lihat dokumen Postgres.
Sisipkan .. kembali
Pernyataan INSERT juga dapat mengembalikan satu atau lebih baris, seperti pernyataan SELECT. Itu dapat mengembalikan nilai yang dihasilkan oleh fungsi, kata kunci seperti current_timestamp dan serial /sequence/identity kolom.
Misalnya, berikut adalah tabel dengan kolom identitas yang dibuat secara otomatis dan kolom yang menyimpan stempel waktu pembuatan baris:
db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(> at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(> foo text);
Kita dapat menggunakan pernyataan INSERT .. RETURNING untuk menentukan hanya nilai untuk kolom foo , dan biarkan Postgres mengembalikan nilai yang dihasilkannya untuk id dan di kolom:
db=> INSERT INTO t1 (foo) VALUES ('first'), ('second') RETURNING id, at, foo;
id | at | foo
----+----------------------------------+--------
1 | 2022-01-14 11:52:09.816787+01:00 | first
2 | 2022-01-14 11:52:09.816787+01:00 | second
(2 rows)
INSERT 0 2
Dari kode aplikasi, gunakan pola/API yang sama dengan yang Anda gunakan untuk menjalankan pernyataan SELECT dan membaca nilai (seperti executeQuery() di JDBC atau db.Query() di Go).
Berikut adalah contoh lain, yang ini memiliki UUID yang dibuat secara otomatis:
CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);
INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;
Mirip dengan INSERT, pernyataan UPDATE dan DELETE juga dapat berisi klausa RETURNING di Postgres. Klausa RETURNING adalah ekstensi Postgres, dan bukan bagian dari standar SQL.
Apa saja dalam satu set
Dari kode aplikasi, bagaimana Anda membuat klausa WHERE yang perlu mencocokkan nilai kolom dengan sekumpulan nilai yang dapat diterima? Ketika jumlah nilai diketahui sebelumnya, SQL menjadi statis:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);
Tapi bagaimana jika jumlah kuncinya bukan 2 tapi bisa berapa saja? Apakah Anda akan membuat pernyataan SQL secara dinamis? Opsi yang lebih mudah adalah menggunakan array Postgres:
SELECT key, value FROM kv WHERE key = ANY(?)
Operator APAPUN di atas menggunakan array sebagai argumen. Klausa key =ANY(?) memilih semua baris di mana nilai kunci adalah salah satu elemen dari array yang disediakan. Dengan ini, kode aplikasi dapat disederhanakan menjadi:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);
Pendekatan ini layak untuk jumlah nilai yang terbatas, jika Anda memiliki banyak nilai untuk dicocokkan, pertimbangkan opsi lain seperti bergabung dengan tabel (sementara) atau tampilan yang terwujud.
Memindahkan baris antar tabel
Ya, Anda dapat menghapus baris dari satu tabel dan memasukkannya ke tabel lain dengan satu pernyataan SQL! Pernyataan INSERT utama dapat menarik baris untuk disisipkan menggunakan CTE, yang membungkus DELETE.
WITH items AS (
DELETE FROM todos_2021
WHERE NOT done
RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;
Melakukan yang setara dalam kode aplikasi bisa sangat bertele-tele, melibatkan penyimpanan seluruh hasil penghapusan dalam memori dan menggunakannya untuk melakukan beberapa INSERT. Memang, memindahkan baris mungkin bukan kasus penggunaan yang umum, tetapi jika logika bisnis memerlukannya, penghematan memori aplikasi dan perjalanan bolak-balik database yang disajikan oleh pendekatan ini menjadikannya solusi yang ideal.
Kumpulan kolom dalam tabel sumber dan tujuan tidak harus identik, Anda tentu saja dapat menyusun ulang, mengatur ulang, dan menggunakan fungsi untuk memanipulasi nilai dalam daftar pilih/pengembalian.
Bergabung
Menyerahkan nilai NULL dalam kode aplikasi biasanya membutuhkan langkah ekstra. Di Go, misalnya, Anda perlu menggunakan tipe seperti sql.NullString; di Java/JDBC, berfungsi seperti resultSet.wasNull() . Ini rumit dan rawan kesalahan.
Jika mungkin untuk menangani, katakanlah NULL sebagai string kosong, atau bilangan bulat NULL sebagai 0, dalam konteks kueri tertentu, Anda dapat menggunakan fungsi COALESCE. Fungsi COALESCE dapat mengubah nilai NULL menjadi nilai tertentu. Misalnya, pertimbangkan kueri ini:
SELECT invoice_num, COALESCE(shipping_address, '')
FROM invoices
WHERE EXTRACT(month FROM raised_on) = 1 AND
EXTRACT(year FROM raised_on) = 2022
yang mendapatkan nomor faktur dan alamat pengiriman faktur yang diajukan pada Jan 2022. Agaknya, alamat_pengiriman adalah NULL jika barang tidak harus dikirim secara fisik. Jika kode aplikasi hanya ingin menampilkan string kosong di suatu tempat dalam kasus seperti itu, katakanlah, lebih mudah menggunakan COALESCE, dan menghapus kode penanganan NULL dalam aplikasi.
Anda juga dapat menggunakan string lain sebagai pengganti string kosong:
SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...
Anda bahkan bisa mendapatkan nilai non-NULL pertama dari daftar, atau gunakan string yang ditentukan sebagai gantinya. Misalnya untuk menggunakan alamat penagihan atau alamat pengiriman, Anda dapat menggunakan:
SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...
Kasus
CASE adalah konstruksi lain yang bermanfaat untuk menangani data kehidupan nyata yang tidak sempurna. Katakanlah daripada memiliki NULL di shipping_address untuk barang yang tidak dapat dikirim, perangkat lunak pembuatan faktur kami yang tidak begitu sempurna telah dimasukkan ke dalam "TIDAK DIJELASKAN". Anda ingin memetakan ini ke NULL atau string kosong saat Anda membaca data. Anda dapat menggunakan KASUS:
-- map NOT-SPECIFIED to an empty string
SELECT invoice_num,
CASE shipping_address
WHEN 'NOT-SPECIFIED' THEN ''
ELSE shipping_address
END
FROM invoices;
-- same result, different syntax
SELECT invoice_num,
CASE
WHEN shipping_address = 'NOT-SPECIFIED' THEN ''
ELSE shipping_address
END
FROM invoices;
CASE memiliki sintaks yang kaku, tetapi secara fungsional mirip dengan pernyataan switch-case dalam bahasa mirip-C. Ini contoh lain:
SELECT invoice_num,
CASE
WHEN shipping_address IS NULL THEN 'NOT SHIPPING'
WHEN billing_address = shipping_address THEN 'SHIPPING TO PAYER'
ELSE 'SHIPPING TO ' || shipping_address
END
FROM invoices;
Pilih .. union
Data dari dua (atau lebih) pernyataan SELECT terpisah dapat digabungkan menggunakan UNION. Misalnya jika Anda memiliki dua tabel, satu menyimpan pengguna saat ini dan satu lagi menghapus, berikut cara mengkueri keduanya secara bersamaan:
SELECT id, name, address, FALSE AS is_deleted
FROM users
WHERE email = ?
UNION
SELECT id, name, address, TRUE AS is_deleted
FROM deleted_users
WHERE email = ?
Kedua kueri harus memiliki daftar pilih yang sama, yaitu, harus mengembalikan jumlah dan jenis kolom yang sama.
UNION juga menghapus duplikat. Hanya baris unik yang dikembalikan. Jika Anda lebih suka mempertahankan baris duplikat, gunakan “UNION ALL” daripada UNION.
Selain UNION, ada juga INTERSECT dan EXCEPT, lihat dokumen PostgreSQL untuk info lebih lanjut.
Pilih .. berbeda pada
Baris duplikat yang dikembalikan oleh SELECT dapat digabungkan (yaitu, hanya baris unik yang dikembalikan) dengan menambahkan kata kunci DISTINCT setelah SELECT. Meskipun ini adalah SQL standar, Postgres menyediakan ekstensi, "DISTINCT ON". Ini sedikit rumit untuk digunakan, tetapi dalam praktiknya sering kali ini merupakan cara paling ringkas untuk mendapatkan hasil yang Anda butuhkan.
Pertimbangkan pelanggan tabel dengan baris per pelanggan, dan pembelian meja dengan satu baris per pembelian yang dilakukan oleh (beberapa) pelanggan. Kueri di bawah menampilkan semua pelanggan, bersama dengan setiap pembelian mereka:
SELECT C.id, P.at
FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
ORDER BY C.id ASC, P.at ASC;
Setiap baris pelanggan diulang untuk setiap pembelian yang mereka lakukan. Bagaimana jika kita hanya ingin mengembalikan pembelian pertama dari pelanggan? Kami pada dasarnya ingin mengurutkan baris berdasarkan pelanggan, mengelompokkan baris berdasarkan pelanggan, dalam setiap grup mengurutkan baris berdasarkan waktu pembelian, dan akhirnya hanya mengembalikan baris pertama dari setiap grup. Sebenarnya lebih pendek untuk menulisnya dalam SQL dengan DISTINCT ON:
SELECT DISTINCT ON (C.id) C.id, P.at
FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
ORDER BY C.id ASC, P.at ASC;
Klausa “DISTINCT ON (C.id)” yang ditambahkan melakukan persis seperti yang dijelaskan di atas. Itu banyak pekerjaan hanya dengan beberapa huruf tambahan!
Menggunakan angka secara berurutan menurut klausa
Pertimbangkan untuk mengambil daftar nama pelanggan dan kode area nomor telepon mereka dari tabel. Kami akan menganggap bahwa nomor telepon AS disimpan dalam format (123) 456-7890
. Untuk negara lain, kami hanya akan mengatakan "NON-US" sebagai kode area.
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers;
Itu semua bagus, dan kita juga memiliki konstruksi CASE, tetapi bagaimana jika kita perlu mengurutkannya berdasarkan kode area sekarang?
Ini berfungsi:
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers
ORDER BY
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END ASC;
Tapi ugh! Mengulangi klausa kasus jelek dan rawan kesalahan. Kita dapat menulis fungsi tersimpan yang mengambil kode negara dan telepon dan mengembalikan kode area, tetapi sebenarnya ada opsi yang lebih bagus:
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers
ORDER BY 3 ASC;
"ORDER BY 3" mengatakan pesanan berdasarkan bidang ke-3! Anda harus ingat untuk memperbarui nomor saat mengatur ulang daftar pilihan, tetapi biasanya tidak sia-sia.