数百万行の分類されたデータやSQLマジックを保存しますか?


8

私のDBAの経験は、単純なストレージ+ CMSスタイルのデータの取得よりもはるかに進んでいないため、これはばかげた質問かもしれませんが、わかりません!

特定のグループサイズと特定の期間内の特定の日数の休日価格を検索または計算する必要があるという問題があります。例えば:

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パターンに適合するかどうかは不明です...

回答:


6

ウィンドウ関数で多くのことができます。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必要があります(ここでは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_diff1つのチェックを。

どうやって?

  • サブクエリでは、時間枠内の日のみを考慮します(「1月」は、最終日が時間枠に含まれることを意味します)。

  • ウィンドウ関数のフレームは、現在の行と次のnum_nights - 14 - 1 = 3)行(日)にまたがります。計算日数の違いを行数とスペースの最小範囲があることを確認するために十分な長さギャップレスと常に持って十分なスペースを

    • 残念ながら、ウィンドウ関数のフレーム句は動的な値を受け入れないため、準備済みステートメント用にパラメーター化することはできません。ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING`
  • 単一の並べ替え手順を使用して、サブクエリ内のすべてのウィンドウ関数を慎重に作成し、同じウィンドウを再利用しました。

  • 結果の価格に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バイトのみ(ヒープタプルヘッダーとアイテム識別子を含む)。詳細:

MVの複数列インデックス:

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

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;

より多くの行をすぐに削除できるため、これはテーブルに対するクエリよりも高速です。ここでも、インデックスは不可欠です。ここではパーティションにギャップがないため、チェックday_ctで十分です。

SQLフィドル実証の両方を

繰り返し使用

頻繁に使用する場合は、SQL関数を作成してパラメーターのみを渡します。または、動的SQLを備えたPL / pgSQL関数EXECUTEを使用して、フレーム句を適応させることができます。

オルタナティブ

date_range1つの行に連続的な範囲を格納するための範囲タイプは代替となる可能性があります-場合によっては、1日あたりの価格またはスペースに変動が生じる可能性があるため、複雑になります。

関連:


@GuyBowden:ベターは善の敵です。ほぼ書き直された答えを考えてみましょう。
Erwin Brandstetter 2014

3

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


非常にエレガントなソリューション!多列インデックスがオンの(spaces, day)場合、おそらく非常に高速(spaces, day, hotel_id, price)です。
Erwin Brandstetter 2014

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;

入力データのサイズ、インデックス構造、およびクエリプランナーの内部クエリの明るさによっては、余分な構造を必要とせずに、探している結果が得られるはずです。これにより、ディスクへのスプールが発生する可能性があります。ただし、十分に効率的である場合があります。警告:私の専門知識はMS SQL Serverとそのクエリプランナーの機能に関するものであるため、関数名のみの場合、上記の構文にはtweeksが必要になる可能性があります (ypercubeが構文を調整したため、おそらく現在postgres互換です。TSQLバリアントの回答履歴を参照してください)

上記は1月に始まるが2月まで続く滞在を見つけるでしょう。日付テストに追加の句を追加する(または終了する日付の値を調整する)と、望ましくない場合でも簡単に対処できます。


1

HotelIDに関係なく、次のように計算列を含む合計テーブルを使用できます。

SummingTable Rev3

値の複数の組み合わせをすばやく計算するためにのみ使用されるため、このテーブルには主キーまたは外部キーはありません。複数の計算値が必要な場合、または必要な場合は、各月の値の新しいビュー名を、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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.