私のDBAの経験は、単純なストレージ+ CMSスタイルのデータの取得よりもはるかに進んでいないため、これはばかげた質問かもしれませんが、わかりません!
特定のグループサイズと特定の期間内の特定の日数の休日価格を検索または計算する必要があるという問題があります。例えば。:
1人のホテルの部屋は、1月のいつでも2泊4泊でいくらですか?
たとえば、5000のホテルの料金と空き状況のデータが次のように保存されています。
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
このテーブルを使用すると、次のようなクエリを実行できます。
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;
結果
hotel_id | sum
----------------
123 | 400
ここでHAVING
句を使用すると、希望する日付の間に、使用可能なスペースがある毎日のエントリがあることを確認できます。すなわち。ホテル456にはJan2に1つのスペースがあり、HAVING句は3を返すため、ホテル456の結果は得られません。
ここまでは順調ですね。
しかし、利用できるスペースがある1月の4泊すべてを調べる方法はありますか?クエリを27回繰り返すことができます-毎回日付をインクリメントしますが、これは少し厄介に思えます。または、別の方法として、可能な組み合わせをすべてルックアップテーブルに格納することもできます。
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
等々。夜の最大数、および検索する最大人数を制限する必要があります。最大宿泊日数= 28、最大人数= 10(その日から始まる設定された期間に利用可能なスペースの数に制限されます)。
1つのホテルの場合、これにより年間28 * 10 * 365 = 102000の結果が得られます。 5000ホテル= 5億成果!
しかし、2人で1月に最も安い4泊の滞在を見つけるための非常に単純なクエリがあります。
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;
500mの行ルックアップテーブルを生成せずに、このクエリを初期テーブルで実行する方法はありますか?例えば一時テーブルまたはその他の内部クエリマジックで27の可能な結果を生成しますか?
現在、すべてのデータはPostgres DBに保持されています。この目的のために必要な場合、データを他のより適切なものに移動できますか?このタイプのクエリがNoSQLスタイルのDBのmap/reduceパターンに適合するかどうかは不明です...
ウィンドウ関数 で多くのことができます。提示2つのソリューション:1つはマテリアライズドビューあり、もう1つはマテリアライズドビューなし。
このテーブルに基づいて構築:
_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
);
_
_hotel_id
_あたりの日数はunique(ここではPKによって適用される)でなければなりません。
_CREATE INDEX mv_hotel_mult_idx ON mv_hotel (day, hotel_id);
_
PKとは逆の順序に注意してください。おそらく両方のインデックスが必要です。次のクエリでは、2番目のインデックスが不可欠です。詳細な説明:
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;
_
@ ypercubeのバリアントとlag()
も参照してください。これは、_day_ct
_と_day_diff
_を1つのチェックで置き換えることができます。
サブクエリでは、時間枠内の日のみを考慮します(「1月」は、最終日が時間枠に含まれることを意味します)。
ウィンドウ関数のフレームは、現在の行と次の_num_nights - 1
_(_4 - 1 = 3
_)行(日)にまたがります。 日数の差、行数、および範囲が十分に長い、ギャップレスであることを確認するための最小のスペースを計算しますそして常に十分なスペースがあります。
ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING`
_をパラメーター化することはできません。singleソートステップを使用して、同じウィンドウを再利用するために、サブクエリ内のすべてのウィンドウ関数を慎重に作成しました。
結果の価格_sum_price
_には、要求されたスペースの数がすでに乗算されています。
MATERIALIZED VIEW
_成功する可能性がない多くの行を検査しないようにするには、必要な列と3つの冗長な計算値だけをベーステーブルから保存します。 MVが最新であることを確認してください。概念に慣れていない場合は、 最初にマニュアルを読んでください 。
_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
_は、次の2つの目的で、各連続範囲の最初の日を格納します。
_range_len
_は、ギャップレス範囲の日数です。
_max_spaces
_は、範囲内の最大オープンスペースです。
ストレージを最適化するために、両方をsmallint
(最大32768で十分です)にキャストします:行ごとに52バイトのみ(ヒープタプルヘッダーとアイテムポインターを含む)。詳細:
_CREATE INDEX mv_hotel_mult_idx ON mv_hotel (range_len, max_spaces, day);
_
_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;
_
これは、より多くの行をすぐに削除できるため、テーブルのクエリよりも高速です。ここでも、インデックスは不可欠です。ここではパーティションがギャップレスであるため、_day_ct
_をチェックするだけで十分です。
SQL Fiddle デモ両方。
頻繁に使用する場合は、SQL関数を作成してパラメーターのみを渡します。または、動的SQLとEXECUTE
を使用したPL/pgSQL関数を使用して、フレーム句を適応させることができます。
_date_range
_ を使用した範囲タイプは、1つの行に連続した範囲を格納することも可能です-場合によっては複雑になります1日あたりの価格やスペースが変動する可能性があります。
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;
入力データのサイズ、インデックス構造、およびクエリプランナーの内部クエリの明るさによっては、余分な構造を必要とせずに、探している結果が得られるはずです。これにより、ディスクへのスプールが発生する可能性があります。ただし、十分に効率的であることがわかります。警告:私の専門知識はMS SQL Serverとそのクエリプランナーの機能です。 したがって、上記の構文では、関数名のみの場合、tweeksが必要になる場合があります(ypercubeは構文を調整したため、おそらく現在postgres互換です。TSQLバリアントの回答履歴を参照してください)。
上記は1月に始まるが2月まで続く滞在を見つけるでしょう。日付テストに追加の句を追加する(または開始する終了日の値を調整する)と、望ましくない場合でも簡単に対処できます。
別の方法として、LAG()
関数を使用します。
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 ;
テスト: SQL-Fiddle
HotelIDに関係なく、次のように、計算列を含む合計テーブルを使用できます。
値の複数の組み合わせをすばやく計算するためにのみ使用されるため、このテーブルには主キーまたは外部キーはありません。複数の計算値が必要な場合、または必要な場合は、各月の値の新しいビュー名を、PeopleおよびPriceと組み合わせて新しいビューを作成しますPP値:
疑似コードの例
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
最後に、ビューをHotelIDに結合します。これを行うには、HotelIDがビューでの計算に使用されていない場合でも、すべてのHotelIDのリストをSummingTableに保存する必要があります(上の表で確認しました)。そのようです:
もっと疑似コード
SELECT HotelID, NumOfDays, SummedColumn AS Total
FROM NightPeriods2People3DaysPricePP400
INNER JOIN Hotels
ON SummingTable.HotelID = Hotels.HotelID