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.