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

Lakukan kueri jam operasi ini di PostgreSQL

Tata letak tabel

Mendesain ulang tabel untuk menyimpan jam buka (jam operasi) sebagai kumpulan tsrange (rentang timestamp without time zone ) nilai-nilai. Memerlukan Postgres 9.2 atau lebih baru .

Pilih minggu acak untuk mengatur jam buka Anda. Saya suka minggu ini:
1996-01-01 (Senin) ke 1996-01-07 (Minggu)
Itu adalah tahun kabisat terbaru dimana 1 Januari kebetulan adalah hari Senin. Tapi itu bisa menjadi minggu acak untuk kasus ini. Konsisten saja.

Instal modul tambahan btree_gist pertama:

CREATE EXTENSION btree_gist;

Lihat:

  • Setara dengan batasan pengecualian yang terdiri dari bilangan bulat dan rentang

Kemudian buat tabel seperti ini:

CREATE TABLE hoo (
   hoo_id  serial PRIMARY KEY
 , shop_id int NOT NULL -- REFERENCES shop(shop_id)     -- reference to shop
 , hours   tsrange NOT NULL
 , CONSTRAINT hoo_no_overlap EXCLUDE USING gist (shop_id with =, hours WITH &&)
 , CONSTRAINT hoo_bounds_inclusive CHECK (lower_inc(hours) AND upper_inc(hours))
 , CONSTRAINT hoo_standard_week CHECK (hours <@ tsrange '[1996-01-01 0:0, 1996-01-08 0:0]')
);

satu kolom hours menggantikan semua kolom Anda:

opens_on, closes_on, opens_at, closes_at

Misalnya, jam operasional dari Rabu, 18:30 sampai Kamis, 05:00 UTC dimasukkan sebagai:

'[1996-01-03 18:30, 1996-01-04 05:00]'

Batasan pengecualian hoo_no_overlap mencegah entri yang tumpang tindih per toko. Ini diimplementasikan dengan indeks GiST , yang juga mendukung kueri kami. Pertimbangkan bab "Indeks dan Kinerja" di bawah ini membahas strategi pengindeksan.

Batasan pemeriksaan hoo_bounds_inclusive memberlakukan batasan inklusif untuk rentang Anda, dengan dua konsekuensi penting:

  • Titik waktu yang jatuh pada batas bawah atau atas selalu disertakan.
  • Entri yang berdekatan untuk toko yang sama secara efektif tidak diizinkan. Dengan batas inklusif, itu akan "tumpang tindih" dan batasan pengecualian akan menimbulkan pengecualian. Entri yang berdekatan harus digabungkan menjadi satu baris saja. Kecuali saat mereka berputar di sekitar Minggu tengah malam , dalam hal ini mereka harus dipecah menjadi dua baris. Fungsi f_hoo_hours() di bawah menangani ini.

Batasan pemeriksaan hoo_standard_week memberlakukan batas luar minggu pementasan menggunakan operator "rentang dikandung oleh" <@ .

Dengan inklusif batas, Anda harus mengamati kasus sudut di mana waktu berakhir pada Minggu tengah malam:

'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
 Mon 00:00 = Sun 24:00 (= next Mon 00:00)

Anda harus mencari kedua cap waktu sekaligus. Berikut adalah kasus terkait dengan eksklusif batas atas yang tidak menunjukkan kekurangan ini:

  • Mencegah entri yang berdekatan/tumpang tindih dengan EXCLUDE di PostgreSQL

Fungsi f_hoo_time(timestamptz)

Untuk "menormalkan" setiap timestamp with time zone :

CREATE OR REPLACE FUNCTION f_hoo_time(timestamptz)
  RETURNS timestamp
  LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
$func$
SELECT timestamp '1996-01-01' + ($1 AT TIME ZONE 'UTC' - date_trunc('week', $1 AT TIME ZONE 'UTC'))
$func$;

PARALLEL SAFE hanya untuk Postgres 9.6 atau lebih baru.

Fungsi ini mengambil timestamptz dan mengembalikan timestamp . Itu menambahkan interval yang telah berlalu dari minggu masing-masing ($1 - date_trunc('week', $1) dalam waktu UTC ke titik awal minggu pementasan kami. (date + interval menghasilkan timestamp .)

Fungsi f_hoo_hours(timestamptz, timestamptz)

Untuk menormalkan rentang dan membagi yang melintasi Senin 00:00. Fungsi ini mengambil interval apa pun (sebagai dua timestamptz ) dan menghasilkan satu atau dua tsrange nilai-nilai. Ini mencakup apa saja masukan hukum dan melarang sisanya:

CREATE OR REPLACE FUNCTION f_hoo_hours(_from timestamptz, _to timestamptz)
  RETURNS TABLE (hoo_hours tsrange)
  LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE COST 500 ROWS 1 AS
$func$
DECLARE
   ts_from timestamp := f_hoo_time(_from);
   ts_to   timestamp := f_hoo_time(_to);
BEGIN
   -- sanity checks (optional)
   IF _to <= _from THEN
      RAISE EXCEPTION '%', '_to must be later than _from!';
   ELSIF _to > _from + interval '1 week' THEN
      RAISE EXCEPTION '%', 'Interval cannot span more than a week!';
   END IF;

   IF ts_from > ts_to THEN  -- split range at Mon 00:00
      RETURN QUERY
      VALUES (tsrange('1996-01-01', ts_to  , '[]'))
           , (tsrange(ts_from, '1996-01-08', '[]'));
   ELSE                     -- simple case: range in standard week
      hoo_hours := tsrange(ts_from, ts_to, '[]');
      RETURN NEXT;
   END IF;

   RETURN;
END
$func$;

Untuk INSERT sebuah tunggal baris masukan:

INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');

Untuk apa saja jumlah baris masukan:

INSERT INTO hoo(shop_id, hours)
SELECT id, f_hoo_hours(f, t)
FROM  (
   VALUES (7, timestamptz '2016-01-11 00:00+0', timestamptz '2016-01-11 08:00+0')
        , (8, '2016-01-11 00:00+1', '2016-01-11 08:00+1')
   ) t(id, f, t);

Masing-masing dapat menyisipkan dua baris jika rentang perlu dipisahkan pada Senin 00:00 UTC.

Kueri

Dengan desain yang disesuaikan, seluruh permintaan Anda yang besar, kompleks, dan mahal bisa diganti dengan ... ini:

SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());

Untuk sedikit ketegangan, saya meletakkan spoiler plate di atas solusinya. Gerakkan mouse ke atas itu.

Kueri didukung oleh indeks GiST tersebut dan cepat, bahkan untuk tabel besar.

db<>main biola di sini (dengan lebih banyak contoh)
sqlfiddle lama

Jika ingin menghitung total jam buka (per toko), berikut resepnya:

  • Menghitung jam kerja antara 2 tanggal di PostgreSQL

Indeks dan Performa

Operator penahanan untuk jenis rentang dapat didukung dengan GiST atau SP-GiST indeks. Keduanya dapat digunakan untuk menerapkan batasan pengecualian, tetapi hanya GiST yang mendukung indeks multikolom:

Saat ini, hanya tipe indeks B-tree, GiST, GIN, dan BRIN yang mendukung indeks multikolom.

Dan urutan kolom indeks penting:

Indeks GiST multikolom dapat digunakan dengan kondisi kueri yang melibatkan subset kolom indeks apa pun. Kondisi pada kolom tambahan membatasi entri yang dikembalikan oleh indeks, tetapi kondisi pada kolom pertama adalah yang paling penting untuk menentukan berapa banyak indeks yang perlu dipindai. Indeks GiST akan relatif tidak efektif jika kolom pertamanya hanya memiliki beberapa nilai yang berbeda, bahkan jika ada banyak nilai yang berbeda di kolom tambahan.

Jadi kami memiliki kepentingan yang bertentangan di sini. Untuk tabel besar, akan ada lebih banyak nilai berbeda untuk shop_id daripada hours .

  • Indeks GiST dengan shop_id terkemuka lebih cepat untuk menulis dan menerapkan batasan pengecualian.
  • Tapi kami mencari hours dalam kueri kami. Memiliki kolom itu terlebih dahulu akan lebih baik.
  • Jika kita perlu mencari shop_id dalam kueri lain, indeks btree biasa jauh lebih cepat untuk itu.
  • Untuk melengkapinya, saya menemukan SP-GiST indeks hanya dalam hours menjadi tercepat untuk kueri.

Tolok ukur

Tes baru dengan Postgres 12 di laptop lama. Skrip saya untuk menghasilkan data dummy:

INSERT INTO hoo(shop_id, hours)
SELECT id
     , f_hoo_hours(((date '1996-01-01' + d) + interval  '4h' + interval '15 min' * trunc(32 * random()))            AT TIME ZONE 'UTC'
                 , ((date '1996-01-01' + d) + interval '12h' + interval '15 min' * trunc(64 * random() * random())) AT TIME ZONE 'UTC')
FROM   generate_series(1, 30000) id
JOIN   generate_series(0, 6) d ON random() > .33;

Menghasilkan ~ 141rb baris yang dibuat secara acak, ~ 30rb shop_id , ~ 12rb hours . Ukuran tabel 8 MB.

Saya menghapus dan membuat ulang batasan pengecualian:

ALTER TABLE hoo
  DROP CONSTRAINT hoo_no_overlap
, ADD CONSTRAINT hoo_no_overlap  EXCLUDE USING gist (shop_id WITH =, hours WITH &&);  -- 3.5 sec; index 8 MB
    
ALTER TABLE hoo
  DROP CONSTRAINT hoo_no_overlap
, ADD CONSTRAINT hoo_no_overlap  EXCLUDE USING gist (hours WITH &&, shop_id WITH =);  -- 13.6 sec; index 12 MB

shop_id pertama ~ 4x lebih cepat untuk distribusi ini.

Selain itu, saya menguji dua lagi untuk kinerja baca:

CREATE INDEX hoo_hours_gist_idx   on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours);  -- !!

Setelah VACUUM FULL ANALYZE hoo; , saya menjalankan dua kueri:

  • Q1 :larut malam, hanya menemukan 35 baris
  • Q2 :di sore hari, menemukan 4547 baris .

Hasil

Mendapat pemindaian hanya indeks untuk masing-masing (kecuali untuk "tidak ada indeks", tentu saja):

index                 idx size  Q1        Q2
------------------------------------------------
no index                        38.5 ms   38.5 ms 
gist (shop_id, hours)    8MB    17.5 ms   18.4 ms
gist (hours, shop_id)   12MB     0.6 ms    3.4 ms
gist (hours)            11MB     0.3 ms    3.1 ms
spgist (hours)           9MB     0.7 ms    1.8 ms  -- !
  • SP-GiST dan GiST setara untuk kueri yang menemukan sedikit hasil (GiST bahkan lebih cepat untuk sangat sedikit).
  • SP-GiST berskala lebih baik dengan jumlah hasil yang terus bertambah, dan juga lebih kecil.

Jika Anda membaca lebih banyak daripada menulis (kasus penggunaan umum), pertahankan batasan pengecualian seperti yang disarankan di awal dan buat indeks SP-GiST tambahan untuk mengoptimalkan kinerja membaca.




  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Apa yang baru di Postgres-XL 9.6

  2. KUNCI UTAMA Gabungan memberlakukan batasan NOT NULL pada kolom yang terlibat

  3. Dapatkan Ukuran Semua Basis Data di PostgreSQL (psql)

  4. Cara memperbarui ID urutan postgreSQL secara massal untuk semua tabel

  5. Bagaimana cara melakukan pembaruan + bergabung di PostgreSQL?