Lưu trữ hàng triệu hàng dữ liệu có mệnh giá hoặc một số phép thuật SQL?


8

Trải nghiệm DBA của tôi không đi xa hơn việc lưu trữ đơn giản + truy xuất dữ liệu kiểu CMS - vì vậy đây có thể là một câu hỏi ngớ ngẩn, tôi không biết!

Tôi có một vấn đề trong đó tôi cần tra cứu hoặc tính giá kỳ nghỉ cho một quy mô nhóm nhất định và một số ngày nhất định trong một khoảng thời gian nhất định. Ví dụ:

Một phòng khách sạn cho 2 người trong 4 đêm vào tháng 1 là bao nhiêu?

Tôi có dữ liệu về giá cả và tính khả dụng cho 5000 khách sạn được lưu trữ như vậy:

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

Với bảng này, tôi có thể thực hiện một truy vấn như vậy:

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;

các kết quả

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

Điều HAVINGkhoản ở đây đảm bảo rằng có một mục nhập cho mỗi ngày giữa các ngày mong muốn của tôi có khoảng trống có sẵn. I E. Khách sạn 456 có 1 chỗ trống vào ngày 2 tháng 1, điều khoản HAVING sẽ trả lại 3, vì vậy chúng tôi không nhận được kết quả cho khách sạn 456.

Càng xa càng tốt.

Tuy nhiên, có cách nào để tìm hiểu tất cả 4 khoảng thời gian đêm vào tháng 1 khi có chỗ trống không? Chúng tôi có thể lặp lại truy vấn 27 lần - tăng ngày mỗi lần, điều này có vẻ hơi khó xử. Hoặc cách khác có thể là lưu trữ tất cả các kết hợp có thể có trong bảng tra cứu như vậy:

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

Và như thế. Chúng tôi phải giới hạn số đêm tối đa và số người tối đa chúng tôi sẽ tìm kiếm - ví dụ: tối đa đêm = 28, tối đa người = 10 (giới hạn số lượng không gian có sẵn cho khoảng thời gian đó bắt đầu từ ngày đó).

Đối với một khách sạn, điều này có thể mang lại cho chúng tôi 28 * 10 * 365 = 102000 kết quả mỗi năm. 5000 khách sạn = 500m kết quả!

Nhưng chúng tôi có một truy vấn rất đơn giản để tìm được 4 đêm rẻ nhất trong tháng 1 cho 2 người:

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;

Có cách nào để thực hiện truy vấn này trên bảng ban đầu mà không phải tạo bảng tra cứu hàng 500m không!? ví dụ: tạo ra 27 kết quả có thể có trong một bảng tạm thời hoặc một số phép thuật truy vấn bên trong khác?

Tại thời điểm này, tất cả dữ liệu được lưu trữ trong DB Postgres - nếu cần cho mục đích này, chúng ta có thể chuyển dữ liệu ra một thứ khác phù hợp hơn không? Không chắc chắn nếu loại truy vấn này phù hợp với các mẫu bản đồ / thu nhỏ cho DB kiểu NoQuery ...

Câu trả lời:


6

Bạn có thể làm nhiều với các chức năng cửa sổ . Trình bày hai giải pháp : một với và một mà không có quan điểm cụ thể hóa.

Trường hợp thử nghiệm

Xây dựng trên bảng này:

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
);

Số ngày hotel_idphải là duy nhất (được thi hành bởi PK tại đây) hoặc phần còn lại không hợp lệ.

Chỉ số nhiều lớp cho bảng cơ sở:

CREATE INDEX mv_hotel_mult_idx ON mv_hotel (day, hotel_id);

Lưu ý thứ tự đảo ngược so với PK. Bạn có thể sẽ cần cả hai chỉ mục, cho truy vấn sau, chỉ mục thứ 2 là điều cần thiết. Giải thích chi tiết:

Truy vấn trực tiếp mà không cần 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;

Cũng xem biến thể của @ ypercube vớilag() , có thể thay thế day_ctday_diffbằng một kiểm tra duy nhất.

Giải thích

  • Trong truy vấn con, chỉ xem xét các ngày trong khung thời gian của bạn (nghĩa là "vào tháng 1", ngày cuối cùng được bao gồm trong khung thời gian).

  • Khung cho các chức năng cửa sổ kéo dài hàng hiện tại cộng với tiếp theo num_nights - 1( 4 - 1 = 3) hàng (ngày). Tính toán sự khác biệt về ngày , số lượng hàng và tối thiểu của không gian để đảm bảo phạm vi đủ dài , không có khe hở và luôn có đủ khoảng trắng .

    • Thật không may, mệnh đề khung của các hàm cửa sổ không chấp nhận các giá trị động, do đó không thể được tham số hóa cho một câu lệnh được chuẩn bị.ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING`
  • Tôi cẩn thận phác thảo tất cả các chức năng của cửa sổ trong truy vấn con để sử dụng lại cùng một cửa sổ, sử dụng một bước sắp xếp duy nhất .

  • Giá kết quả sum_priceđã được nhân với số lượng không gian được yêu cầu.

Với MATERIALIZED VIEW

Để tránh kiểm tra nhiều hàng mà không có cơ hội thành công, chỉ lưu các cột bạn cần cộng với ba giá trị được tính toán dự phòng từ bảng cơ sở. Hãy chắc chắn rằng MV được cập nhật. Nếu bạn không quen thuộc với khái niệm này, hãy đọc hướng dẫn trước .

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 lưu trữ ngày đầu tiên của mỗi phạm vi liên tục cho hai mục đích:

    • để đánh dấu một tập hợp các hàng là thành viên của một phạm vi chung
    • để hiển thị bắt đầu của phạm vi cho các mục đích khác có thể.
  • range_lenlà số ngày trong phạm vi khoảng cách.
    max_spaceslà tối đa của không gian mở trong phạm vi.

    • Cả hai cột được sử dụng để loại trừ các hàng không thể khỏi truy vấn ngay lập tức.
  • Tôi đã chuyển cả hai thành smallint(tối đa 32768 cho cả hai) để tối ưu hóa lưu trữ: chỉ 52 byte mỗi hàng (bao gồm tiêu đề heap tuple và con trỏ mục). Chi tiết:

Chỉ số nhiều màu cho MV:

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

Truy vấn dựa trên 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;

Điều này nhanh hơn truy vấn trên bảng vì nhiều hàng có thể được loại bỏ ngay lập tức. Một lần nữa, chỉ số là điều cần thiết. Kể từ phân vùng đã được Gapless đây, kiểm tra day_ctlà đủ.

SQL Fiddle chứng minh cả hai .

Sử dụng nhiều lần

Nếu bạn sử dụng nó nhiều, tôi sẽ tạo một hàm SQL và chỉ truyền tham số. Hoặc một hàm PL / pgQuery với SQL động và EXECUTEđể cho phép điều chỉnh mệnh đề khung.

Thay thế

Các loại phạm vi date_rangeđể lưu trữ phạm vi liên tục trong một hàng có thể là một lựa chọn thay thế - phức tạp trong trường hợp của bạn với các biến thể tiềm năng về giá hoặc không gian mỗi ngày.

Câu trả lời liên quan


@GuyBowden: Tốt hơn là kẻ thù của tốt. Hãy xem xét câu trả lời phần lớn viết lại.
Erwin Brandstetter

3

Một cách khác, sử dụng LAG()chức năng:

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 ;

Kiểm tra tại: SQL-Fiddle


Giải pháp rất thanh lịch! Có lẽ rất nhanh với chỉ số nhiều màu trên (spaces, day), thậm chí có thể là chỉ số bao phủ trên (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;

sẽ mang lại cho bạn kết quả mà bạn đang tìm kiếm mà không cần các cấu trúc bổ sung, mặc dù tùy thuộc vào kích thước của dữ liệu đầu vào, cấu trúc chỉ mục của bạn và trình lập kế hoạch truy vấn sáng đến mức nào mà truy vấn bên trong có thể dẫn đến một bộ đệm vào đĩa. Bạn có thể tìm thấy nó đủ hiệu quả mặc dù. Hãy cẩn thận : chuyên môn của tôi là với MS SQL Server và các khả năng của trình lập kế hoạch truy vấn của nó, vì vậy cú pháp trên có thể cần hai lần nếu chỉ trong tên hàm (ypercube đã điều chỉnh cú pháp để bây giờ có thể tương thích với postgres, xem lịch sử trả lời cho biến thể TSQL) .

Ở trên sẽ tìm thấy ở lại bắt đầu vào tháng một nhưng tiếp tục vào tháng hai. Thêm một mệnh đề bổ sung vào kiểm tra ngày (hoặc điều chỉnh giá trị ngày kết thúc) sẽ dễ dàng xử lý nếu điều đó không mong muốn.


1

Bất kể HotelID, bạn có thể sử dụng bảng tổng hợp, với một cột được tính toán, như vậy:

SummingTable Rev3

Không có Khóa chính hoặc Khóa ngoại trong bảng này, vì nó chỉ được sử dụng để tính toán nhanh chóng nhiều kết hợp Giá trị. Nếu bạn cần hoặc muốn nhiều hơn một giá trị được tính toán, hãy tạo chế độ xem mới với tên chế độ xem mới cho từng giá trị tháng kết hợp với từng Giá trị PP của mọi người và Giá:

VÍ DỤ MÃ 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

Cuối cùng, Tham gia Chế độ xem đến HotelID. Để làm điều đó, bạn sẽ cần lưu trữ một danh sách tất cả các HotelID trong SummingTable (Tôi đã làm trong Bảng trên), mặc dù HotelID không được sử dụng để tính toán trong Chế độ xem. Giống như vậy:

MÃ PSEUDO THÊM

SELECT HotelID, NumOfDays, SummedColumn AS Total
FROM NightPeriods2People3DaysPricePP400
INNER JOIN Hotels
ON SummingTable.HotelID = Hotels.HotelID
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.