PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

Lebih Banyak SQL, Lebih Sedikit Kode, Dengan PostgreSQL

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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Wadah Docker untuk Postgres 9.1 tidak mengekspos port 5432 ke host

  2. PostgreSQL - klausa GROUP BY atau digunakan dalam fungsi agregat

  3. Bagaimana cara mengubah kata sandi pengguna PostgreSQL?

  4. Kecepatan Pemotongan Postgresql

  5. Batasan postgres untuk rentang tanggal waktu yang unik