Menyimpan jutaan baris data denominasi atau sihir SQL?

8

Pengalaman DBA saya tidak lebih jauh dari penyimpanan sederhana + pengambilan data gaya CMS - jadi ini mungkin pertanyaan konyol, saya tidak tahu!

Saya memiliki masalah di mana saya perlu mencari atau menghitung harga liburan untuk ukuran grup tertentu dan beberapa hari dalam periode waktu tertentu. Misalnya:

Berapa harga kamar hotel untuk 2 orang selama 4 malam kapan saja di bulan Januari?

Saya memiliki data harga dan ketersediaan untuk, katakanlah, 5000 hotel disimpan seperti ini:

Hotel ID | Date | Spaces | Price PP
-----------------------------------
     123 | Jan1 | 5      | 100
     123 | Jan2 | 7      | 100
     123 | Jan3 | 5      | 100
     123 | Jan4 | 3      | 100
     123 | Jan5 | 5      | 100
     123 | Jan6 | 7      | 110
     456 | Jan1 | 5      | 120
     456 | Jan2 | 1      | 120
     456 | Jan3 | 4      | 130
     456 | Jan4 | 3      | 110
     456 | Jan5 | 5      | 100
     456 | Jan6 | 7      |  90

Dengan tabel ini, saya dapat melakukan kueri seperti ini:

SELECT hotel_id, sum(price_pp)
FROM hotel_data
WHERE
    date >= Jan1 and date <= Jan4
    and spaces >= 2
GROUP BY hotel_id
HAVING count(*) = 4;

hasil

hotel_id | sum
----------------
     123 | 400

The HAVINGklausul sini membuat yakin bahwa ada sebuah entri untuk setiap hari antara tanggal yang diinginkan saya yang memiliki ruang yang tersedia. yaitu. Hotel 456 memiliki 1 ruang yang tersedia pada Jan2, klausa HAVING akan mengembalikan 3, jadi kami tidak mendapatkan hasil untuk hotel 456.

Sejauh ini bagus.

Namun, apakah ada cara untuk mengetahui semua periode 4 malam di bulan Januari di mana ada ruang yang tersedia? Kami dapat mengulangi kueri 27 kali - menambah tanggal setiap kali, yang sepertinya sedikit canggung. Atau cara lain untuk menyimpan semua kemungkinan kombinasi dalam tabel pencarian seperti:

Hotel ID | total price pp | num_people | num_nights | start_date
----------------------------------------------------------------
     123 |            400 | 2          | 4          | Jan1
     123 |            400 | 2          | 4          | Jan2
     123 |            400 | 2          | 4          | Jan3
     123 |            400 | 3          | 4          | Jan1
     123 |            400 | 3          | 4          | Jan2
     123 |            400 | 3          | 4          | Jan3

Dan seterusnya. Kami harus membatasi jumlah maksimum malam, dan jumlah maksimum orang yang akan kami cari - mis. Maks malam = 28, maks orang = 10 (terbatas pada jumlah ruang yang tersedia untuk periode yang ditetapkan yang dimulai pada tanggal itu).

Untuk satu hotel, ini bisa memberi kita 28 * 10 * 365 = 102000 hasil per tahun. 5000 hotel = hasil 500 juta!

Tapi kami akan memiliki pertanyaan yang sangat sederhana untuk menemukan menginap 4 malam termurah di Jan untuk 2 orang:

SELECT
hotel_id, start_date, price
from hotel_lookup
where num_people=2
and num_nights=4
and start_date >= Jan1
and start_date <= Jan27
order by price
limit 1;

Apakah ada cara untuk melakukan kueri ini di tabel awal tanpa harus membuat tabel pencarian 500m baris !? misalnya menghasilkan 27 hasil yang mungkin dalam tabel sementara atau beberapa sihir permintaan dalam lainnya?

Saat ini semua data disimpan dalam Postgres DB - jika perlu untuk tujuan ini kita dapat memindahkan data ke hal lain yang lebih cocok? Tidak yakin apakah jenis kueri ini cocok dengan peta / kurangi pola untuk DB gaya NoSQL ...

Guy Bowden
sumber

Jawaban:

6

Anda dapat melakukan banyak hal dengan fungsi jendela . Menghadirkan dua solusi : satu dengan dan satu tanpa tampilan terwujud.

Kasus cobaan

Bangunan di atas meja ini:

CREATE TABLE hotel_data (
   hotel_id int
 , day      date  -- using "day", not "date"
 , spaces   int
 , price    int
 , PRIMARY KEY (hotel_id, day)  -- provides essential index automatically
);

Hari per hotel_idharus unik (diberlakukan oleh PK di sini), atau sisanya tidak valid.

Indeks multikolom untuk tabel dasar:

CREATE INDEX mv_hotel_mult_idx ON mv_hotel (day, hotel_id);

Perhatikan urutan terbalik dibandingkan dengan PK. Anda mungkin perlu kedua indeks, untuk permintaan berikut, indeks ke-2 sangat penting. Penjelasan detail:

Permintaan langsung tanpa MATERIALIZED VIEW

SELECT hotel_id, day, sum_price
FROM  (
   SELECT hotel_id, day, price, spaces
        , sum(price)      OVER w * 2   AS sum_price
        , min(spaces)     OVER w       AS min_spaces
        , last_value(day) OVER w - day AS day_diff
        , count(*)        OVER w       AS day_ct
   FROM   hotel_data
   WHERE  day BETWEEN '2014-01-01'::date AND '2014-01-31'::date
   AND    spaces >= 2
   WINDOW w AS (PARTITION BY hotel_id ORDER BY day
                ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) -- adapt to nights - 1
   ) sub
WHERE  day_ct = 4
AND    day_diff = 3  -- make sure there is not gap
AND    min_spaces >= 2
ORDER  BY sum_price, hotel_id, day;
-- LIMIT 1 to get only 1 winner;

Juga lihat varian @ ypercube denganlag() , yang dapat menggantikan day_ctdan day_diffdengan satu cek.

Bagaimana?

  • Dalam subquery, hanya pertimbangkan hari dalam kerangka waktu Anda ("pada bulan Januari" berarti, hari terakhir termasuk dalam kerangka waktu).

  • Frame untuk fungsi jendela meliputi baris saat ini ditambah berikutnya num_nights - 1( 4 - 1 = 3) baris (hari). Hitung selisih hari , hitungan baris, dan minimum ruang untuk memastikan rentangnya cukup panjang , tanpa celah dan selalu memiliki cukup ruang .

    • Sayangnya, bingkai klausa fungsi jendela tidak menerima nilai dinamis, jadi tidak dapat diparameterisasi untuk pernyataan yang disiapkan.ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING`
  • Saya hati-hati menyusun semua fungsi jendela di subquery untuk menggunakan kembali jendela yang sama, menggunakan satu langkah semacam.

  • Harga yang dihasilkan sum_pricesudah dikalikan dengan jumlah ruang yang diminta.

Dengan MATERIALIZED VIEW

Untuk menghindari memeriksa banyak baris tanpa peluang untuk berhasil, simpan hanya kolom yang Anda butuhkan ditambah tiga nilai yang berlebihan dan dihitung dari tabel dasar. Pastikan MV-nya sudah mutakhir. Jika Anda tidak terbiasa dengan konsep itu, bacalah manualnya terlebih dahulu .

CREATE MATERIALIZED VIEW mv_hotel AS
SELECT hotel_id, day
     , first_value(day) OVER (w ORDER BY day) AS range_start
     , price, spaces
     ,(count(*)    OVER w)::int2 AS range_len
     ,(max(spaces) OVER w)::int2 AS max_spaces

FROM  (
   SELECT *
        , day - row_number() OVER (PARTITION BY hotel_id ORDER BY day)::int AS grp
   FROM   hotel_data
   ) sub1
WINDOW w AS (PARTITION BY hotel_id, grp);
  • range_start menyimpan hari pertama setiap rentang berkelanjutan untuk dua tujuan:

    • untuk menandai satu set baris sebagai anggota dari rentang umum
    • untuk menunjukkan awal rentang untuk kemungkinan keperluan lain.
  • range_lenadalah jumlah hari dalam rentang gapless.
    max_spacesadalah maksimum ruang terbuka dalam jangkauan.

    • Kedua kolom digunakan untuk mengecualikan baris yang tidak mungkin dari permintaan segera.
  • Saya menggunakan keduanya untuk smallint(maks. 32768 harus cukup untuk keduanya) untuk mengoptimalkan penyimpanan: hanya 52 byte per baris (termasuk header heap tuple dan pengidentifikasi item). Detail:

Indeks multikolom untuk MV:

CREATE INDEX mv_hotel_mult_idx ON mv_hotel (range_len, max_spaces, day);

Permintaan berdasarkan MV

SELECT hotel_id, day, sum_price
FROM  (
   SELECT hotel_id, day, price, spaces
        , sum(price)      OVER w * 2   AS sum_price
        , min(spaces)     OVER w       AS min_spaces
        , count(*)        OVER w       AS day_ct
   FROM   mv_hotel
   WHERE  day BETWEEN '2014-01-01'::date AND '2014-01-31'::date
   AND    range_len >= 4   -- exclude impossible rows
   AND    max_spaces >= 2  -- exclude impossible rows
   WINDOW w AS (PARTITION BY hotel_id, range_start ORDER BY day
                ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) -- adapt to $nights - 1
   ) sub
WHERE  day_ct = 4
AND    min_spaces >= 2
ORDER  BY sum_price, hotel_id, day;
-- LIMIT 1 to get only 1 winner;

Ini lebih cepat daripada kueri di atas meja karena lebih banyak baris dapat dihilangkan dengan segera. Sekali lagi, indeks sangat penting. Karena partisi tidak ada celah di sini, pemeriksaan day_ctsudah cukup.

SQL Fiddle mendemonstrasikan keduanya .

Penggunaan berulang

Jika Anda sering menggunakannya, saya akan membuat fungsi SQL dan hanya meneruskan parameter. Atau fungsi PL / pgSQL dengan SQL dinamis dan EXECUTEuntuk memungkinkan mengadaptasi frame clause.

Alternatif

Jenis rentang dengan date_rangemenyimpan rentang berkelanjutan dalam satu baris mungkin menjadi alternatif - rumit dalam kasus Anda dengan variasi potensial pada harga atau ruang per hari.

Terkait:

Erwin Brandstetter
sumber
@GuyBowden: Lebih baik adalah musuh dari kebaikan. Pertimbangkan jawaban yang sebagian besar ditulis ulang.
Erwin Brandstetter
3

Cara lain, menggunakan LAG()fungsi:

WITH x AS
  ( SELECT hotel_id, day, 
           LAG(day, 3) OVER (PARTITION BY hotel_id 
                             ORDER BY day)
              AS day_start,
           2 * SUM(price) OVER (PARTITION BY hotel_id 
                                ORDER BY day
                                ROWS BETWEEN 3 PRECEDING 
                                         AND CURRENT ROW)
              AS sum_price
    FROM hotel_data
    WHERE spaces >= 2
   -- AND day >= '2014-01-01'::date      -- date restrictions 
   -- AND day <  '2014-02-01'::date      -- can be added here
  )
SELECT hotel_id, day_start, sum_price
FROM x
WHERE day_start = day - 3 ;

Tes di: SQL-Fiddle

ypercubeᵀᴹ
sumber
Solusi yang sangat elegan! Mungkin sangat cepat dengan indeks multicolumn aktif (spaces, day), bahkan mungkin indeks penutup aktif (spaces, day, hotel_id, price).
Erwin Brandstetter
3
SELECT hotel, totprice
FROM   (
       SELECT r.hotel, SUM(r.pricepp)*@spacesd_needed AS totprice
       FROM   availability AS a
       JOIN   availability AS r 
              ON r.date BETWEEN a.date AND a.date + (@days_needed-1) 
              AND a.hotel = r.hotel
              AND r.spaces >= @spaces_needed
       WHERE  a.date BETWEEN '2014-01-01' AND '2014-01-31'
       GROUP BY a.date, a.hotel
       HAVING COUNT(*) >= @days_needed
       ) AS matches
ORDER BY totprice ASC
LIMIT 1;

seharusnya memberi Anda hasil yang Anda cari tanpa memerlukan struktur tambahan, meskipun tergantung pada ukuran data input, struktur indeks Anda, dan seberapa cerah perencana kueri adalah permintaan dalam dapat menghasilkan spool ke disk. Anda mungkin menemukannya cukup efisien. Peringatan: keahlian saya adalah dengan MS SQL Server dan kemampuan perencana kuasanya , jadi sintaks di atas mungkin perlu tweeks jika hanya dalam nama fungsi (ypercube telah menyesuaikan sintaks sehingga mungkin postgres kompatibel sekarang, lihat riwayat jawaban untuk varian TSQL) .

Hal di atas akan menemukan masa inap yang dimulai pada bulan Januari tetapi berlanjut hingga Februari. Menambahkan klausa tambahan ke pengujian tanggal (atau menyesuaikan nilai tanggal akhir yang masuk) akan dengan mudah mengatasinya jika tidak diinginkan.

David Spillett
sumber
1

Terlepas dari HotelID, Anda bisa menggunakan tabel penjumlahan, dengan kolom terhitung, seperti:

SummingTable Rev3

Tidak ada Kunci Utama atau Asing dalam tabel ini, karena ini hanya digunakan untuk dengan cepat menghitung beberapa kombinasi Nilai. Jika Anda membutuhkan atau menginginkan lebih dari satu nilai yang dihitung, buat tampilan baru dengan nama tampilan baru untuk setiap nilai bulan dalam kombinasi dengan masing-masing Nilai People dan PP Nilai:

CONTOH KODE PSEUDO

CREATE VIEW NightPeriods2People3DaysPricePP400 AS (
SELECT (DaysInverse - DaysOfMonth) AS NumOfDays, (NumberOfPeople * PricePP * NumOfDays) AS SummedColumn 
FROM SummingTable
WHERE NumberOfPeople = 2) AND (DaysInverse = 4) AND (DaysOfMonth = 1) AND (PricePP = 400)
)

SummedColumn = 2400

Terakhir, Gabung View ke HotelID. Untuk melakukan itu, Anda perlu Menyimpan daftar semua HotelID di SummingTable (Saya Lakukan di Tabel Di Atas), meskipun HotelID tidak digunakan untuk menghitung dalam Tampilan. Seperti begitu:

LEBIH BANYAK KODE PSEUDO

SELECT HotelID, NumOfDays, SummedColumn AS Total
FROM NightPeriods2People3DaysPricePP400
INNER JOIN Hotels
ON SummingTable.HotelID = Hotels.HotelID
eyoung100
sumber