Postgresql基础实践教程(五)

⭐️⭐️⭐️⭐️⭐️
完整数据详见 练习数据·免费

⭐️⭐️⭐️⭐️⭐️

四十一、列出每个月每个设施的总预订时段数(第二部分)

题目

生成2012年每个月每个设施的总预订时段数列表。在这个版本中,输出结果需要包含每个设施所有月份的汇总行,以及所有设施所有月份的总计行。输出表格应包含设施ID、月份和时段数,并按设施ID和月份排序。在计算所有月份和所有设施的聚合值时,月份和设施ID列应返回空值。

预期结果

facid month slots
0 7 270
0 8 459
0 9 591
0 1320
1 7 207
1 8 483
1 9 588
1 1278
2 7 180
2 8 459
2 9 570
2 1209
3 7 104
3 8 304
3 9 422
3 830
4 7 264
4 8 492
4 9 648
4 1404
5 7 24
5 8 82
5 9 122
5 228
6 7 164
6 8 400
6 9 540
6 1104
7 7 156
7 8 326
7 9 426
7 908
8 7 117
8 8 322
8 9 471
8 910
9191

[答案与解析]

sql 复制代码
select facid, extract(month from starttime) as month, sum(slots) as slots
	from cd.bookings
	where
		starttime >= '2012-01-01'
		and starttime < '2013-01-01'
	group by rollup(facid, month)
order by facid, month;

在做数据分析时,我们有时需要进行多层级的聚合,以便能够"放大"和"缩小"到不同的粒度级别。在这种情况下,我们可能想查看每个设施的整体使用情况,然后又想深入了解它们在每个月的表现。使用我们目前掌握的SQL知识,要编写一个满足需求的单一查询相当麻烦------实际上我们不得不借助UNION ALL来拼接多个查询:

sql 复制代码
select facid, extract(month from starttime) as month, sum(slots) as slots
    from cd.bookings
    where
        starttime >= '2012-01-01'
        and starttime < '2013-01-01'
    group by facid, month
union all
select facid, null, sum(slots) as slots
    from cd.bookings
    where
        starttime >= '2012-01-01'
        and starttime < '2013-01-01'
    group by facid
union all
select null, null, sum(slots) as slots
    from cd.bookings
    where
        starttime >= '2012-01-01'
        and starttime < '2013-01-01'
order by facid, month;

如你所见,每个子查询执行不同层级的聚合,然后我们将结果合并。我们可以通过CTE提取公共部分来简化这个查询:

sql 复制代码
with bookings as (
	select facid, extract(month from starttime) as month, slots
	from cd.bookings
	where
		starttime >= '2012-01-01'
		and starttime < '2013-01-01'
)
select facid, month, sum(slots) from bookings group by facid, month
union all
select facid, null, sum(slots) from bookings group by facid
union all
select null, null, sum(slots) from bookings
order by facid, month;

这个版本看起来还算清晰,但随着聚合列数量的增加,写法会变得很繁琐。幸运的是,PostgreSQL 9.5引入了ROLLUP操作符的支持,我们在标准答案中就用它来简化了查询。

ROLLUP会按照传入的顺序生成分层聚合:例如,ROLLUP(facid, month)会输出(facid, month)、(facid)和()三个层级的聚合结果。如果我们想要按月份聚合所有设施的数据(而不是按设施聚合所有月份),就需要颠倒顺序,使用ROLLUP(month, facid)。另外,如果我们想要传入列的所有可能组合,可以使用CUBE代替ROLLUP。这会生成(facid, month)、(month)、(facid)和()四种组合。

ROLLUP和CUBE都是GROUPING SETS的特例。GROUPING SETS允许你指定精确的聚合组合:比如,你可以只要求(facid, month)和(facid)两种组合,跳过顶层聚合。


四十二、列出每个命名设施的总预订小时数

题目

生成每个设施的总预订_小时数_列表,记住一个时段是半小时。输出表格应包含设施ID、名称和预订小时数,按设施ID排序。尝试将小时数格式化为两位小数。

预期结果

facid name Total Hours
0 Tennis Court 1 660.00
1 Tennis Court 2 639.00
2 Badminton Court 604.50
3 Table Tennis 415.00
4 Massage Room 1 702.00
5 Massage Room 2 114.00
6 Squash Court 552.00
7 Snooker Table 454.00
8 Pool Table 455.50

[答案与解析]

sql 复制代码
select facs.facid, facs.name,
	trim(to_char(sum(bks.slots)/2.0, '9999999999999999D99')) as "Total Hours"

	from cd.bookings bks
	inner join cd.facilities facs
		on facs.facid = bks.facid
	group by facs.facid, facs.name
order by facs.facid;

这道题有几个值得注意的小细节。首先,你可以看到当我们将表以1:1的关系连接时,聚合操作依然正常工作。另外要注意我们同时按facs.facid和facs.name进行了分组。这看起来可能有点奇怪:毕竟,由于facid是facilities表的主键,每个facid只有一个对应的name,所以按两个字段分组和只按facid分组效果是一样的。实际上,如果你从GROUP BY子句中移除facs.name,查询仍然可以正常工作:Postgres能够识别出这种1:1的映射关系,不会强制要求我们按两个字段分组。

不过,不同的数据库系统在这方面的智能程度可能不一样,有些可能无法识别出这种严格的1:1映射关系。在这种情况下,如果每个facid对应多个name而我们没有按name分组,数据库系统就不得不在多个(同样有效的)name值中做出选择。由于这是不合法的,数据库系统会坚持要求我们按两个字段都进行分组。一般来说,我建议对所有没有使用聚合函数的列都进行分组,这样可以确保更好的跨平台兼容性。

接下来是除法运算。熟悉MySQL的朋友可能知道,整数除法会自动转换为浮点数。Postgres在这方面比较传统,需要你明确指定是否要进行浮点除法。在这个例子中,你可以通过除以2.0而不是2来轻松实现。

最后,让我们看看格式化部分。TO_CHAR函数将值转换为字符串。它需要一个格式化字符串,我们在这里指定为:小数点前很多位数字、小数点、以及小数点后两位数字。这个函数的输出可能会在前面带上一个空格,所以我们需要在外层使用TRIM函数去掉空格。


四十三、列出每个会员在2012年9月1日之后的首次预订

题目

生成每个会员的姓名、ID以及他们在2012年9月1日之后的首次预订时间列表。按会员ID排序。

预期结果

surname firstname memid starttime
GUEST GUEST 0 2012-09-01 08:00:00
Smith Darren 1 2012-09-01 09:00:00
Smith Tracy 2 2012-09-01 11:30:00
Rownam Tim 3 2012-09-01 16:00:00
Joplette Janice 4 2012-09-01 15:00:00
Butters Gerald 5 2012-09-02 12:30:00
Tracy Burton 6 2012-09-01 15:00:00
Dare Nancy 7 2012-09-01 12:30:00
Boothe Tim 8 2012-09-01 08:30:00
Stibbons Ponder 9 2012-09-01 11:00:00
Owen Charles 10 2012-09-01 11:00:00
Jones David 11 2012-09-01 09:30:00
Baker Anne 12 2012-09-01 14:30:00
Farrell Jemima 13 2012-09-01 09:30:00
Smith Jack 14 2012-09-01 11:00:00
Bader Florence 15 2012-09-01 10:30:00
Baker Timothy 16 2012-09-01 15:00:00
Pinker David 17 2012-09-01 08:30:00
Genting Matthew 20 2012-09-01 18:00:00
Mackenzie Anna 21 2012-09-01 08:30:00
Coplin Joan 22 2012-09-02 11:30:00
Sarwin Ramnaresh 24 2012-09-04 11:00:00
Jones Douglas 26 2012-09-08 13:00:00
Rumney Henrietta 27 2012-09-16 13:30:00
Farrell David 28 2012-09-18 09:00:00
Worthington-Smyth Henry 29 2012-09-19 09:30:00
Purview Millicent 30 2012-09-19 11:30:00
Tupperware Hyacinth 33 2012-09-20 08:00:00
Hunt John 35 2012-09-23 14:00:00
Crumpet Erica 36 2012-09-27 11:30:00

[答案与解析]

sql 复制代码
select mems.surname, mems.firstname, mems.memid, min(bks.starttime) as starttime
	from cd.bookings bks
	inner join cd.members mems on
		mems.memid = bks.memid
	where starttime >= '2012-09-01'
	group by mems.surname, mems.firstname, mems.memid
order by mems.memid;

这个答案展示了如何对日期使用聚合函数。MIN函数的作用完全符合你的预期,它会从结果集中提取最早的日期。为了让这个查询正常工作,我们需要确保结果集只包含9月及之后的日期,这是通过WHERE子句来实现的。

通常你会用类似这样的查询来查找客户的下一次预订。你可以将日期'2012-09-01'替换为now()函数来实现这个功能。


四十四、生成会员姓名列表,每行包含会员总数

题目

生成会员姓名列表,每行都要包含会员总数。按加入日期排序,并包含访客会员。

预期结果

count firstname surname
31 GUEST GUEST
31 Darren Smith
31 Tracy Smith
31 Tim Rownam
31 Janice Joplette
31 Gerald Butters
31 Burton Tracy
31 Nancy Dare
31 Tim Boothe
31 Ponder Stibbons
31 Charles Owen
31 David Jones
31 Anne Baker
31 Jemima Farrell
31 Jack Smith
31 Florence Bader
31 Timothy Baker
31 David Pinker
31 Matthew Genting
31 Anna Mackenzie
31 Joan Coplin
31 Ramnaresh Sarwin
31 Douglas Jones
31 Henrietta Rumney
31 David Farrell
31 Henry Worthington-Smyth
31 Millicent Purview
31 Hyacinth Tupperware
31 John Hunt
31 Erica Crumpet
31 Darren Smith

[答案与解析]

sql 复制代码
select count(*) over(), firstname, surname
	from cd.members
order by joindate

根据我们目前掌握的知识,最显而易见的答案如下所示。我们使用子查询是因为如果不这样做,SQL会要求我们按firstname和surname进行分组,这样得到的结果就不是我们想要的了。

sql 复制代码
select (select count(*) from cd.members) as count, firstname, surname
	from cd.members
order by joindate

这个答案本身没有任何问题,但我们选择了另一种方法来介绍一个叫做窗口函数的新概念。窗口函数提供了非常强大的功能,而且往往比标准的聚合函数使用起来更方便。虽然这个练习只是个简单的例子,但我们在不久的将来会处理更复杂的场景。

窗口函数在(子)查询的结果集上运行,也就是在WHERE子句和所有标准聚合操作之后执行。它们在一个"窗口"的数据上运行。默认情况下,这个窗口是不受限制的:即整个结果集,但也可以对其进行限制以获得更有用的结果。例如,假设我们不想要所有会员的总数,而是想要知道与当前会员在同一月份加入的会员数量:

sql 复制代码
select count(*) over(partition by date_trunc('month',joindate)),
	firstname, surname
	from cd.members
order by joindate

在这个例子中,我们按月份对数据进行分区。对于窗口函数处理的每一行,窗口就是所有加入日期在同一月份的行。因此,窗口函数会生成在该月份加入的会员数量统计。

你还可以更进一步。如果你不想知道该月份加入的会员总数,而是想知道某个会员是该月份第几个加入的,可以通过在窗口函数中添加ORDER BY来实现:

sql 复制代码
select count(*) over(partition by date_trunc('month',joindate) order by joindate),
	firstname, surname
	from cd.members
order by joindate

ORDER BY再次改变了窗口的范围。现在每行的窗口不再是整个分区,而是从分区开始到当前行为止,不会超出这个范围。因此,对于某个月份第一个加入的会员,计数是1;第二个加入的会员,计数是2,依此类推。

最后值得一提的是,你可以在同一个查询中使用多个互不相关的窗口函数。试试下面的查询,你会发现会员的序号朝着相反的方向变化!这种灵活性可以让查询更加简洁、易读且易于维护。

sql 复制代码
select count(*) over(partition by date_trunc('month',joindate) order by joindate asc), 
	count(*) over(partition by date_trunc('month',joindate) order by joindate desc), 
	firstname, surname
	from cd.members
order by joindate

窗口函数功能极其强大,它们会改变你编写和思考SQL的方式。好好利用它们吧!


四十五、生成带序号的会员列表

题目

生成一个单调递增的带序号会员列表(包括访客),按加入日期排序。记住,会员ID不一定是连续的。

预期结果

row_number firstname surname
1 GUEST GUEST
2 Darren Smith
3 Tracy Smith
4 Tim Rownam
5 Janice Joplette
6 Gerald Butters
7 Burton Tracy
8 Nancy Dare
9 Tim Boothe
10 Ponder Stibbons
11 Charles Owen
12 David Jones
13 Anne Baker
14 Jemima Farrell
15 Jack Smith
16 Florence Bader
17 Timothy Baker
18 David Pinker
19 Matthew Genting
20 Anna Mackenzie
21 Joan Coplin
22 Ramnaresh Sarwin
23 Douglas Jones
24 Henrietta Rumney
25 David Farrell
26 Henry Worthington-Smyth
27 Millicent Purview
28 Hyacinth Tupperware
29 John Hunt
30 Erica Crumpet
31 Darren Smith

[答案与解析]

sql 复制代码
select row_number() over(order by joindate), firstname, surname
	from cd.members
order by joindate

这个练习是一个简单的窗口函数实践!你也可以在这里使用count(*) over(order by joindate),所以如果你用的是那个也不用担心。

在这个查询中,我们没有定义分区,这意味着分区就是整个数据集。由于我们为窗口函数定义了排序,所以对于任何给定的行,窗口范围是:数据集开始 -> 当前行。


四十六、输出预订时段数最多的设施ID(再探)

题目

输出预订时段数最多的设施ID。确保在出现平局的情况下,所有并列的结果都要输出。

预期结果

facid total
4 1404

[答案与解析]

sql 复制代码
select facid, total from (
	select facid, sum(slots) total, rank() over (order by sum(slots) desc) rank
        	from cd.bookings
		group by facid
	) as ranked
	where rank = 1

你可能还记得,这是我们在之前的练习中已经解决过的问题。当时我们得出的答案类似下面这样,然后用CTE进行了简化:

sql 复制代码
select facid, sum(slots) as totalslots
	from cd.bookings
	group by facid
	having sum(slots) = (select max(sum2.totalslots) from
		(select sum(slots) as totalslots
		from cd.bookings
		group by facid
		) as sum2);

清理过后,这个解决方案完全够用。不过,解释这个查询的工作原理会让它显得有些奇怪------"找出最佳设施的预订时段数。计算每个设施的总预订时段数,然后只返回那些时段数与最佳设施相同的行"。如果能够这样说岂不是更优雅:"计算每个设施的预订时段数,对它们进行排名,然后选出排名第1的设施"?

幸运的是,窗口函数让我们能够做到这一点------虽然对于未经训练的眼睛来说,这样做并不简单。第一个关键信息是RANK函数的存在。这个函数根据传递给它的ORDER BY对值进行排名。如果(比如)第二名出现平局,下一个就会被排在第4位。所以,我们需要做的是获取每个设施的时段数,对它们进行排名,然后选出最高排名的那些。第一次尝试可能看起来像下面这样:

sql 复制代码
select facid, total from (
	select facid, total, rank() over (order by total desc) rank from (
		select facid, sum(slots) total
			from cd.bookings
			group by facid
		) as sumslots
	) as ranked
where rank = 1

内部查询计算总预订时段数,中间查询对它们进行排名,外部查询则提取出排名最高的。我们实际上可以稍微整理一下:回想一下,窗口函数在SELECT函数中应用得很晚,是在聚合之后。既然如此,我们可以将聚合移到函数的ORDER BY部分,如标准答案所示。

虽然窗口函数方法在代码行数上并没有大幅减少,但它在语义上 arguably 更加清晰合理。


四十七、按(四舍五入的)使用小时数对会员排名

题目

生成会员列表(包括访客),以及他们在设施中预订的小时数,四舍五入到最近的10小时。按这个四舍五入后的数值进行排名,输出名字、姓氏、四舍五入后的小时数和排名。按排名、姓氏和名字排序。

预期结果

firstname surname hours rank
GUEST GUEST 1200 1
Darren Smith 340 2
Tim Rownam 330 3
Tim Boothe 220 4
Tracy Smith 220 4
Gerald Butters 210 6
Burton Tracy 180 7
Charles Owen 170 8
Janice Joplette 160 9
Anne Baker 150 10
Timothy Baker 150 10
David Jones 150 10
Nancy Dare 130 13
Florence Bader 120 14
Anna Mackenzie 120 14
Ponder Stibbons 120 14
Jack Smith 110 17
Jemima Farrell 90 18
David Pinker 80 19
Ramnaresh Sarwin 80 19
Matthew Genting 70 21
Joan Coplin 50 22
David Farrell 30 23
Henry Worthington-Smyth 30 23
John Hunt 20 25
Douglas Jones 20 25
Millicent Purview 20 25
Henrietta Rumney 20 25
Erica Crumpet 10 29
Hyacinth Tupperware 10 29

[答案与解析]

sql 复制代码
select firstname, surname,
	((sum(bks.slots)+10)/20)*10 as hours,
	rank() over (order by ((sum(bks.slots)+10)/20)*10 desc) as rank

	from cd.bookings bks
	inner join cd.members mems
		on bks.memid = mems.memid
	group by mems.memid
order by rank, surname, firstname;

这个答案相比我们之前的练习并没有太大突破,但它更好地展示了RANK函数的功能。你可以看到,有些会员的四舍五入后的小时数是相同的,他们的排名也一样。如果第2名被两个会员共享,下一个就会得到第4名。还有一个不同的函数DENSE_RANK,它会给那个会员分配第3名。

值得一提的是我们这里使用的四舍五入技巧。加5,除以10,再乘以10,这样的效果(多亏了整数运算会截断小数部分)是将数字四舍五入到最近的10的倍数。在我们的例子中,因为每个时段是半小时,我们需要加10,除以20,再乘以10。当然,有人可能会说我们应该将时段到小时的转换与四舍五入分开处理,这样会更清晰。

说到清晰度,这种四舍五入的操作开始引入明显的代码重复。到了这个阶段,这是一个见仁见智的问题,但你可能会希望像下面这样用子查询将其提取出来:

sql 复制代码
select firstname, surname, hours, rank() over (order by hours desc) from
	(select firstname, surname,
		((sum(bks.slots)+10)/20)*10 as hours

		from cd.bookings bks
		inner join cd.members mems
			on bks.memid = mems.memid
		group by mems.memid
	) as subq
order by rank, surname, firstname;

四十八、找出收入最高的三个设施

题目

生成收入最高的三个设施列表(包括并列情况)。输出设施名称和排名,按排名和设施名称排序。

预期结果

name rank
Massage Room 1 1
Massage Room 2 2
Tennis Court 2 3

[答案与解析]

sql 复制代码
select name, rank from (
	select facs.name as name, rank() over (order by sum(case
				when memid = 0 then slots * facs.guestcost
				else slots * membercost
			end) desc) as rank
		from cd.bookings bks
		inner join cd.facilities facs
			on bks.facid = facs.facid
		group by facs.name
	) as subq
	where rank <= 3
order by rank;

这个问题没有引入任何新概念,只是为了让你有机会练习已经掌握的知识。我们使用CASE语句来计算每个时段的收入,然后用SUM按设施进行聚合。接着使用RANK窗口函数生成排名,将所有内容包装在子查询中,最后提取排名小于或等于3的所有记录。


四十九、按价值对设施进行分类

题目

根据设施的收入,将它们分类为高、中、低三个同等大小的组。按分类和设施名称排序。

预期结果

name revenue
Massage Room 1 high
Massage Room 2 high
Tennis Court 2 high
Badminton Court average
Squash Court average
Tennis Court 1 average
Pool Table low
Snooker Table low
Table Tennis low

[答案与解析]

sql 复制代码
select name, case when class=1 then 'high'
		when class=2 then 'average'
		else 'low'
		end revenue
	from (
		select facs.name as name, ntile(3) over (order by sum(case
				when memid = 0 then slots * facs.guestcost
				else slots * membercost
			end) desc) as class
		from cd.bookings bks
		inner join cd.facilities facs
			on bks.facid = facs.facid
		group by facs.name
	) as subq
order by class, name;

这个练习主要使用我们熟悉的概念,不过我们引入了NTILE窗口函数。NTILE将值尽可能均匀地分组到指定数量的组中。它输出从1到组数的数字。然后我们使用CASE语句将这个数字转换为标签!


五十、计算每个设施的投资回收期

题目

基于目前3个月的完整数据,计算每个设施收回其拥有成本所需的时间。记住要考虑每月的持续维护费用。输出设施名称和回收期(以月为单位),按设施名称排序。不必担心月份长度的差异,我们这里只需要一个大概的数值!

预期结果

name months
Badminton Court 6.8317677198975235
Massage Room 1 0.18885741265344664778
Massage Room 2 1.7621145374449339
Pool Table 5.3333333333333333
Snooker Table 6.9230769230769231
Squash Court 1.1339582703356516
Table Tennis 6.4000000000000000
Tennis Court 1 2.2624434389140271
Tennis Court 2 1.7505470459518600

[答案与解析]

sql 复制代码
select 	facs.name as name,
	facs.initialoutlay/((sum(case
			when memid = 0 then slots * facs.guestcost
			else slots * membercost
		end)/3) - facs.monthlymaintenance) as months
	from cd.bookings bks
	inner join cd.facilities facs
		on bks.facid = facs.facid
	group by facs.facid
order by name;

与我们最近的所有练习不同,解决这个问题不需要使用窗口函数:这只是一个涉及月收入、初始投资和月度维护费用的数学计算。同样,对于生产代码,你可能希望用子查询来澄清这里的逻辑(不过既然我们已经硬编码了月份数,把它投入生产的可能性不大!)。整理后的版本可能看起来像这样:

sql 复制代码
select 	name, 
	initialoutlay / (monthlyrevenue - monthlymaintenance) as repaytime 
	from 
		(select facs.name as name, 
			facs.initialoutlay as initialoutlay,
			facs.monthlymaintenance as monthlymaintenance,
			sum(case
				when memid = 0 then slots * facs.guestcost
				else slots * membercost
			end)/3 as monthlyrevenue
		from cd.bookings bks
		inner join cd.facilities facs
			on bks.facid = facs.facid
		group by facs.facid
	) as subq
order by name;

但是,你可能会问,自动化的版本会是什么样子?一个不需要硬编码月份数的版本?那就稍微复杂一些,涉及到一些日期计算。我将其分解到CTE中,使其更加清晰。

sql 复制代码
with monthdata as (
	select 	mincompletemonth,
		maxcompletemonth,
		(extract(year from maxcompletemonth)*12) +
			extract(month from maxcompletemonth) -
			(extract(year from mincompletemonth)*12) -
			extract(month from mincompletemonth) as nummonths 
	from (
		select 	date_trunc('month', 
				(select max(starttime) from cd.bookings)) as maxcompletemonth,
			date_trunc('month', 
				(select min(starttime) from cd.bookings)) as mincompletemonth
	) as subq
)
select 	name, 
	initialoutlay / (monthlyrevenue - monthlymaintenance) as repaytime 
	
	from
		(select facs.name as name,
			facs.initialoutlay as initialoutlay,
			facs.monthlymaintenance as monthlymaintenance,
			sum(case
				when memid = 0 then slots * facs.guestcost
				else slots * membercost
			end)/(select nummonths from monthdata) as monthlyrevenue
			
			from cd.bookings bks
			inner join cd.facilities facs
				on bks.facid = facs.facid
			where bks.starttime < (select maxcompletemonth from monthdata)
			group by facs.facid
		) as subq
order by name;

这段代码限制了进入的数据为完整的月份。它通过选择最大日期,向下舍入到月份,并剔除所有大于该日期的日期来实现。即使是这样的代码也不是完全完善的。它没有处理设施亏损的情况。修复这个问题并不太难,就留给读者作为(又一个)练习吧!


五十一、计算总收入的滚动平均值

题目

对于2012年8月的每一天,计算过去15天的总收入滚动平均值。输出应包含日期和收入列,按日期排序。记住要考虑某天收入为零的可能性。这道题有点难度,所以别害怕查看提示!

预期结果

date revenue
2012-08-01 1126.8333333333333333
2012-08-02 1153.0000000000000000
2012-08-03 1162.9000000000000000
2012-08-04 1177.3666666666666667
2012-08-05 1160.9333333333333333
2012-08-06 1185.4000000000000000
2012-08-07 1182.8666666666666667
2012-08-08 1172.6000000000000000
2012-08-09 1152.4666666666666667
2012-08-10 1175.0333333333333333
2012-08-11 1176.6333333333333333
2012-08-12 1195.6666666666666667
2012-08-13 1218.0000000000000000
2012-08-14 1247.4666666666666667
2012-08-15 1274.1000000000000000
2012-08-16 1281.2333333333333333
2012-08-17 1324.4666666666666667
2012-08-18 1373.7333333333333333
2012-08-19 1406.0666666666666667
2012-08-20 1427.0666666666666667
2012-08-21 1450.3333333333333333
2012-08-22 1539.7000000000000000
2012-08-23 1567.3000000000000000
2012-08-24 1592.3333333333333333
2012-08-25 1615.0333333333333333
2012-08-26 1631.2000000000000000
2012-08-27 1659.4333333333333333
2012-08-28 1687.0000000000000000
2012-08-29 1684.6333333333333333
2012-08-30 1657.9333333333333333
2012-08-31 1703.4000000000000000

[答案与解析]

sql 复制代码
select 	dategen.date,
	(
		-- 相关子查询,对于传入的每一天,
		-- 找出过去15天的平均收入
		select sum(case
			when memid = 0 then slots * facs.guestcost
			else slots * membercost
		end) as rev

		from cd.bookings bks
		inner join cd.facilities facs
			on bks.facid = facs.facid
		where bks.starttime > dategen.date - interval '14 days'
			and bks.starttime < dategen.date + interval '1 day'
	)/15 as revenue
	from
	(
		-- 生成8月份的日期列表
		select 	cast(generate_series(timestamp '2012-08-01',
			'2012-08-31','1 day') as date) as date
	)  as dategen
order by dategen.date;

这道题至少有两种同样优秀的解决方案。我把最容易编写的作为答案,但还有一个使用窗口函数的更灵活的解决方案。

让我们先看看选定的答案。当我阅读SQL查询时,我倾向于最后阅读SELECT部分------FROM和WHERE部分往往更有趣。那么,我们的FROM中有什么呢?是对GENERATE_SERIES函数的调用。这个函数的作用正如其名------生成一系列值。你可以指定起始值、终止值和增量。它适用于整数类型和日期------不过,正如你所看到的,我们需要明确指定进入和离开函数的数据类型。试着移除类型转换,看看结果如何!

好了,我们已经生成了8月份每一天的时间戳。现在,对于每一天,我们需要生成平均值。我们可以使用相关子查询来完成这个任务。如果你还记得,相关子查询是使用外部查询值的子查询。这意味着它对外部查询的每个结果行执行一次。这与不相关子查询不同,后者只需要执行一次。

如果我们查看相关子查询,可以看到它在dategen.date字段上是相关的。它为这一天以及之前的14天生成收入总和,然后将该总和除以15。这就产生了我们想要的输出!

我提到过这个问题还有一个基于窗口函数的解决方案------你可以在下面看到。我们使用的方法是生成每天的收入列表,然后在该列表上使用窗口函数聚合。这种方法的好处是,一旦你有了每天的收入,就可以轻松地生成各种各样的结果------例如,你可能想要前一个月、15天和5天的滚动平均值。使用这种方法很容易做到,而使用传统聚合则相当困难。

sql 复制代码
select date, avgrev from (
	-- 对当前行和之前的14行求平均值
	select 	dategen.date as date,
		avg(revdata.rev) over(order by dategen.date rows 14 preceding) as avgrev
	from
		-- 生成日期列表。这确保了即使某天收入为0也会生成一行。
		-- 注意我们生成了10月开始之前的日期------这是因为我们的窗口函数
		-- 需要知道这些天的收入来进行计算。
		(select
			cast(generate_series(timestamp '2012-07-10', '2012-08-31','1 day') as date) as date
		)  as dategen
		left outer join
			-- 左连接到每天收入的表
			(select cast(bks.starttime as date) as date,
				sum(case
					when memid = 0 then slots * facs.guestcost
					else slots * membercost
				end) as rev

				from cd.bookings bks
				inner join cd.facilities facs
					on bks.facid = facs.facid
				group by cast(bks.starttime as date)
			) as revdata
			on dategen.date = revdata.date
	) as subq
	where date >= '2012-08-01'
order by date;

你会注意到我们经常想要计算每日收入。与其将这些计算插入到我们所有的查询中(这样会很混乱,而且如果我们 ever 改变模式会导致大麻烦),我们可能希望将这些信息存储在某个地方。你的第一个想法可能是计算信息并将其存储起来以备后用。这是大型数据仓库的常见策略,但它可能会给我们带来一些问题------如果我们回头编辑数据,需要记得重新计算。对于我们这里处理的非超大规模的数据,我们可以创建一个视图。视图本质上是一个存储的查询,看起来完全像一个表。在底层,当你从视图中选择数据时,DBMS只是替换视图定义中的相关部分。它们非常容易创建,如下所示:

sql 复制代码
create or replace view cd.dailyrevenue as
	select 	cast(bks.starttime as date) as date,
		sum(case
			when memid = 0 then slots * facs.guestcost
			else slots * membercost
		end) as rev

		from cd.bookings bks
		inner join cd.facilities facs
			on bks.facid = facs.facid
		group by cast(bks.starttime as date);

你可以看到这使我们的查询变得简单多了!

sql 复制代码
select date, avgrev from (
	select  dategen.date as date,
		avg(revdata.rev) over(order by dategen.date rows 14 preceding) as avgrev
	from		
		(select
			cast(generate_series(timestamp '2012-07-10', '2012-08-31','1 day') as date) as date
		)  as dategen
		left outer join
			cd.dailyrevenue as revdata on dategen.date = revdata.date
		) as subq
	where date >= '2012-08-01'
order by date;

除了存储常用的查询片段外,视图还可以用于各种目的,包括限制对表中某些列的访问。

相关推荐
lqj_本人11 小时前
鸿蒙PC:Qt适配OpenHarmony实战【花账】:从一笔支出开始,做一个本地记账小应用
数据库·qt·harmonyos
kaico201811 小时前
数据库操作
数据库·python
TDengine (老段)11 小时前
TDengine 存储引擎概览 — TSDB 分层存储架构与数据流转全景
大数据·数据库·物联网·架构·时序数据库·tdengine·涛思数据
Full Stack Developme11 小时前
SQL like 与 正则 区别
数据库·sql·mysql
pixcarp11 小时前
Redis ZSet:底层设计与实践
数据库·redis·后端·学习·golang·web
我是一颗柠檬11 小时前
【MySQL全面教学】MySQL多表查询与JOIN Day6(2026年)
数据库·后端·sql·mysql
倒流时光三十年11 小时前
PostgreSQL COPY命令:高效数据导入的最佳实践
数据库·postgresql
shuair11 小时前
redis分布式锁
数据库·redis·分布式
今天背单词了吗98012 小时前
MySQL InnoDB引擎八大核心特性详解(高频面试题)
java·数据库·mysql