Guy*_*den 8 postgresql window-functions datetime denormalization
我的 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
Run Code Online (Sandbox Code Playgroud)
有了这个表,我可以做一个这样的查询:
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;
Run Code Online (Sandbox Code Playgroud)
结果
hotel_id | sum
----------------
123 | 400
Run Code Online (Sandbox Code Playgroud)
HAVING
这里的条款确保在我想要的日期之间的每一天都有一个条目,并且有可用的空间。IE。酒店 456 在 1 月 2 日有 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
Run Code Online (Sandbox Code Playgroud)
等等。我们必须限制最大晚数,以及我们要搜索的最大人数 - 例如最大晚数 = 28,最大人数 = 10(仅限于从该日期开始的该设定时间段的可用空间数)。
对于一家酒店,这每年可以为我们带来 28*10*365=102000 的结果。5000 家酒店 = 5 亿结果!
但是我们有一个非常简单的查询来查找 1 月 2 人的最便宜的 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;
Run Code Online (Sandbox Code Playgroud)
有没有办法在初始表上执行这个查询而不必生成 500m 行查找表!?例如在临时表中生成 27 个可能的结果或其他一些内部查询魔法?
目前所有数据都保存在 Postgres 数据库中 - 如果需要为此目的,我们可以将数据移到其他更合适的地方吗?不确定这种类型的查询是否适合 NoSQL 风格数据库的映射/减少模式......
你可以用窗口函数做很多事情。提出两种解决方案:一种有物化视图,一种没有物化视图。
建立在这张桌子上:
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
);
Run Code Online (Sandbox Code Playgroud)
Days perhotel_id
必须是唯一的(此处由 PK 强制执行),否则其余部分无效。
CREATE INDEX mv_hotel_mult_idx ON mv_hotel (day, hotel_id);
Run Code Online (Sandbox Code Playgroud)
请注意与 PK 相比的相反顺序。您可能需要两个索引,对于以下查询,第二个索引是必不可少的。详细解释:
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;
Run Code Online (Sandbox Code Playgroud)
另请参阅@ypercube 的带有 的变体lag()
,它可以用一个检查替换day_ct
和day_diff
。
在子查询中,只考虑时间范围内的天数(“一月”意味着最后一天包含在时间范围内)。
窗口函数的框架跨越当前行加上接下来的num_nights - 1
( 4 - 1 = 3
) 行(天)。计算日差,行数和最小的空间,以确保该范围足够长,无间隙,总是有足够的空间。
ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING`
我仔细地起草了子查询中的所有窗口函数,以使用单个排序步骤重用同一个窗口。
结果价格sum_price
已经乘以请求的空间数量。
MATERIALIZED VIEW
为避免在没有成功机会的情况下检查多行,请仅保存您需要的列以及来自基表的三个冗余计算值。确保 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);
Run Code Online (Sandbox Code Playgroud)
range_start
存储每个连续范围的第一天有两个目的:
range_len
是无间隙范围内的天数。
max_spaces
是范围内开放空间的最大值。
我将两者都转换为smallint
(最大 32768 对两者来说都足够了)以优化存储:每行只有 52 个字节(包括堆元组标题和项目标识符)。细节:
CREATE INDEX mv_hotel_mult_idx ON mv_hotel (range_len, max_spaces, day);
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
这比在表上查询要快,因为可以立即消除更多行。再次,索引是必不可少的。由于这里的分区是无间隙的,检查day_ct
就足够了。
SQL Fiddle演示了.
如果你经常使用它,我会创建一个 SQL 函数并且只传递参数。或者具有动态 SQL的PL/pgSQL函数并EXECUTE
允许调整框架子句。
date_range
用于在单行中存储连续范围的范围类型可能是另一种选择 - 在您的情况下很复杂,每天的价格或空间可能会发生变化。
归档时间: |
|
查看次数: |
769 次 |
最近记录: |