昨天写postgresql代码的时候遇到一个棘手的问题,题目是要计算在各个城市使用次数最多的手机型号。此时只有一个表,叫做usage,里面存储了每一次用户使用服务时的手机型号以及使用的城市,即,usage里有phone_id和城市的zip_code。
此时要统计每个城市使用次数最多的手机型号,自然而然地想到先进行一次GROUP BY,于是写出如下代码:
SELECT u.zip_code,u.phone_id,COUNT(*) number FROM usage u GROUP BY u.zip_code,u.phone_id;
此时发现出来的结果是:
zip_code | phone_id | number
----------+----------+--------
888 | 22222 | 2
444 | 11111 | 2
444 | 44444 | 1
888 | 33333 | 3
444 | 99999 | 1
222 | 44444 | 4
888 | 11111 | 8
444 | 00000 | 1
444 | 22222 | 6
444 | 55555 | 2
(10 行记录)
这个时候发现只分一次组是不够的,这样只统计出了在某个城市使用某个手机型号的次数,但是题目要求要算出每个城市使用次数最多的手机型号,所以需要二次分组。于是,我尝试着写出了如下代码:
SELECT pc.zip_code,pc.phone_id,MAX(pc.number) FROM ( SELECT u.zip_code,COUNT(*) number FROM usage u GROUP BY u.zip_code,u.phone_id )pc GROUP BY pc.zip_code;这个时候问题来了,报错了:
错误: 字段 "pc.phone_id" 必须出现在 GROUP BY 子句中或者在聚合函数中使用
第1行SELECT pc.zip_code,MAX(pc.number)
在GROUP BY中没出现的字段,无法被SELECT出来,那么我在GROUP BY中加上pc.phone_id字段?不行,这样一分组,就又变成了第一次分组的效果了!然后我开始想,能不能在第一次分组中就使用两次的聚合函数——MAX(COUNT(*))?试了一下,果然报错。突然脑洞一开就觉得sql应该改进这一部分的功能,这样很不合理,之所以不能使用两次聚合函数的本质是行数不匹配,但是我用两组信息分组,针对第一组信息的二次聚合函数就应该可以使用才对的!墙裂要求sql开发人员在下一版本中增加此功能!然后我又尝试在WHERE中使用pc.number = MAX(pc.number),但是可惜的是,where中不允许出现聚合函数,想想也有道理,where是在GROUP BY之前执行的,如果能让你使用了,那岂不是乱套了?但是题目要求啊!怎么办!我就这样纠结了一个晚上,直到睡觉了也没想出来,不开心地睡下了。第二天早上起来接着想!想啊想,题目要求只能使用一个view就把一切信息全部都整出来,那么肯定是利用子查询,那么子查询可以出现的位置都有哪些地方呢?SELECT后面?不对!FROM后面?使用过了啊!WHERE里?对!WHERE里使用子查询!试试看!于是我先编写了一个子查询以供使用:
SELECT pc.zip_code,u.phone_id )pc GROUP BY pc.zip_code
放在命令行里验证了一下,正确!
zip_code | max
----------+-----
888 | 8
222 | 4
444 | 6
(3 行记录)
只是缺少phone_id字段而已,没关系,在外部补上!
正确代码如下:
第一部分,没有加外连接版本: CREATE OR REPLACE VIEW view_most_used_phone_per_county AS SELECT c.county_name county,p.phone_name phone FROM ( SELECT u.zip_code,u.phone_id )pc JOIN county c ON pc.zip_code = c.zip_code JOIN phone p ON pc.phone_id = p.phone_id WHERE (pc.zip_code,pc.number) IN ( SELECT pc.zip_code,MAX(pc.number) FROM ( SELECT u.zip_code,COUNT(*) number FROM usage u GROUP BY u.zip_code,u.phone_id )pc GROUP BY pc.zip_code ) ORDER BY county; 第二部分,加了外连接: CREATE OR REPLACE VIEW view_most_used_phone_per_county AS SELECT c.county_name county,u.phone_id )pc RIGHT OUTER JOIN county c ON pc.zip_code = c.zip_code LEFT OUTER JOIN phone p ON pc.phone_id = p.phone_id WHERE (pc.zip_code,u.phone_id )pc GROUP BY pc.zip_code ) OR c.zip_code NOT IN (SELECT u.zip_code FROM usage u) ORDER BY county;
一试,果然成功啦!!
在这里说一个小插曲,我中间把
WHERE (pc.zip_code,pc.number) IN (......)
写成了
WHERE (pc.zip_code,pc.number) = (......)结果一直报错说:“作为一个表达式使用的子查询返回了多列”,然后我就一直想不明白,后来百度了一下,无意中瞄到一个回答提醒了是等号还是IN的问题,这才恍然大悟!
返回多行多列的时候,应该使用IN!记住!!!
好吧,我以为题目被我完美解决了,可是刚才看到了题目中的一句话:若在没有手机在该城市使用,也要显示出来,只不过在右边的phone那一栏显示为空。好了,这个就是外连接的问题了。之前的代码只显示zip_code和phone_id,但是题目是要求显示城市名称和手机型号,此时需要JOIN表格county和表格phone。一开始我是这么连接的:
SELECT c.county_name county,u.phone_id )pc JOIN county c ON pc.zip_code = c.zip_code JOIN phone p ON pc.phone_id = p.phone_id ............
这样的结果就是,只有有手机使用的城市才会被显示出来。于是我理所当然地改成如下代码,只在county和pc那里加了RIGHT OUTER JOIN:
SELECT c.county_name county,u.phone_id )pc RIGHT OUTER JOIN county c ON pc.zip_code = c.zip_code JOIN phone p ON pc.phone_id = p.phone_id ............
但是意外的是,这样出来的结果和之前一样,没有任何变化,咦,怎么回事,我明明外连接了呀?不应该全部出来吗?于是我删掉了phone表格,测试了一下。
SELECT c.county_name county FROM ( SELECT u.zip_code,u.phone_id )pc RIGHT OUTER JOIN county c ON pc.zip_code = c.zip_code ........
这样测试下来是正确的,所有的城市名称都显示出来了。于是我又加上了phone,不用JOIN,而改用WHERE限定连接条件,结果还是一样的。挫败的我静静坐下来思考,到底是为什么!
然后我在phone的连接方式上,加上了 LEFT OUTER JOIN:
SELECT c.county_name county,u.phone_id )pc RIGHT OUTER JOIN county c ON pc.zip_code = c.zip_code LEFT OUTER JOIN phone p ON pc.phone_id = p.phone_id .............
奇迹出现了!所有的城市名称都出来了!
这说明什么?在多次JOIN的时候,你以为第在第三个phone表格写的连接方式是和第一个pc表格相连接,所以没有第二个表格county什么事,但实际上,第三个表格是和所有的前两个都相连接了,你要让左边所有的都显示出来,就必须加上LEFT OUTER JOIN,否则就只有符合条件的行被显示出来。具体来讲就是只有符合pc.phone_id = p.phone_id的行被显示出来,就算你在前两个表格中想全部显示出county,可是第三个连接方式中明确指出,只有pc表格里有的phone_id所对应的zip_code才能被显示,也就是说,只有在pc表格里存在有对应手机型号的城市才能被显示出来,所以此时就算在前两个表格中加了RIGHT OUTER JOIN,在第三个连接方式没加LEFT OUTER JOIN,也是白搭!
多个表格进行外连接时,要注意这些约束条件都是相互之间的,而不是说只有邻近的两个表格才有约束力,要注意全部加上外连接符号,并且方向写对才可。
原文链接:https://www.f2er.com/postgresql/195281.html