web-dev-qa-db-ja.com

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

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

8
Guy Bowden

ウィンドウ関数 で多くのことができます。提示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バイトのみ(ヒープタプルヘッダーとアイテムポインターを含む)。詳細:

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 Fiddle デモ両方

繰り返し使用

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

オルタナティブ

_date_range_ を使用した範囲タイプは、1つの行に連続した範囲を格納することも可能です-場合によっては複雑になります1日あたりの価格やスペースが変動する可能性があります。

関連する回答

6
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月まで続く滞在を見つけるでしょう。日付テストに追加の句を追加する(または開始する終了日の値を調整する)と、望ましくない場合でも簡単に対処できます。

3
David Spillett

別の方法として、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

3
ypercubeᵀᴹ

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
1
eyoung100