楼主: 〇〇

NineData第2届数据库编程大赛:一条SQL秒 杀100万张火车票

[复制链接]
论坛徽章:
519
奥运会纪念徽章:垒球
日期:2008-09-15 01:28:12生肖徽章2007版:鸡
日期:2008-11-17 23:40:58生肖徽章2007版:马
日期:2008-11-18 05:09:48数据库板块每日发贴之星
日期:2008-11-29 01:01:02数据库板块每日发贴之星
日期:2008-12-05 01:01:03生肖徽章2007版:虎
日期:2008-12-10 07:47:462009新春纪念徽章
日期:2009-01-04 14:52:28数据库板块每日发贴之星
日期:2009-02-08 01:01:03生肖徽章2007版:蛇
日期:2009-03-09 22:18:532009日食纪念
日期:2009-07-22 09:30:00
11#
发表于 2024-12-17 22:52 | 只看该作者
Naldonado 发表于 2024-12-17 14:09
我选择按20一个批次,切分

活动还没结束,当心评委剥夺你的资格。

使用道具 举报

回复
论坛徽章:
169
SQL数据库编程大师
日期:2016-01-13 10:30:43SQL极客
日期:2013-12-09 14:13:35SQL大赛参与纪念
日期:2013-12-06 14:03:45最佳人气徽章
日期:2015-03-19 09:44:03现任管理团队成员
日期:2015-08-26 02:10:00秀才
日期:2015-07-28 09:12:12举人
日期:2015-07-13 15:30:15进士
日期:2015-07-28 09:12:58探花
日期:2015-07-28 09:12:58榜眼
日期:2015-08-18 09:48:03
12#
发表于 2024-12-18 23:23 来自手机 | 只看该作者
newkid 发表于 2024-12-17 22:52
活动还没结束,当心评委剥夺你的资格。

itpub我娘家,我还不能做主了

使用道具 举报

回复
论坛徽章:
407
紫蛋头
日期:2012-05-21 10:19:41迷宫蛋
日期:2012-06-06 16:02:49奥运会纪念徽章:足球
日期:2012-06-29 15:30:06奥运会纪念徽章:排球
日期:2012-07-10 21:24:24鲜花蛋
日期:2012-07-16 15:24:59奥运会纪念徽章:拳击
日期:2012-08-07 10:54:50奥运会纪念徽章:羽毛球
日期:2012-08-21 15:55:33奥运会纪念徽章:蹦床
日期:2012-08-21 21:09:51奥运会纪念徽章:篮球
日期:2012-08-24 10:29:11奥运会纪念徽章:体操
日期:2012-09-07 16:40:00
13#
 楼主| 发表于 2024-12-20 21:48 来自手机 | 只看该作者
本帖最后由 〇〇 于 2024-12-20 21:51 编辑

比赛结束了,Naldonado获得 冠    军

使用道具 举报

回复
论坛徽章:
519
奥运会纪念徽章:垒球
日期:2008-09-15 01:28:12生肖徽章2007版:鸡
日期:2008-11-17 23:40:58生肖徽章2007版:马
日期:2008-11-18 05:09:48数据库板块每日发贴之星
日期:2008-11-29 01:01:02数据库板块每日发贴之星
日期:2008-12-05 01:01:03生肖徽章2007版:虎
日期:2008-12-10 07:47:462009新春纪念徽章
日期:2009-01-04 14:52:28数据库板块每日发贴之星
日期:2009-02-08 01:01:03生肖徽章2007版:蛇
日期:2009-03-09 22:18:532009日食纪念
日期:2009-07-22 09:30:00
14#
发表于 2024-12-20 23:57 | 只看该作者
我自己写了一个:

普通挑战
完成普通挑战的前 50 名选手即可获得参赛纪念奖品
要求选手用一条 SQL 实现乘客分配车次信息,具体要求:
1. 只能用一条 SQL ,可以使用数据库内置函数,不能使用自定义函数和存储过程
2. 每个乘客最多只能分配一个车次
3. 每列火车分配的乘客数不能超过座位数
4. 不能出现火车有空余座位,但对应起始站和终点站的乘客未分配票
5. 如果没有可分配的车次, train_id 返回空值 ( null )
6. 返回结果按 passenger_id 字符顺序排序
返回的结果集示例如下:


思路:需求是将每行乘客记录分配到一趟列车,也就是说连接两张表。连接条件首先是起点和终点站,这两个列重复的火车记录有多行,而每名乘客最多只能连接到其中的一条,所以需要对每趟车做一个编号限制,使得乘客按照编号只能对应到其中的一行。把列车和乘客分别按照起点和终点站分组,同组的乘客按某种方法排序(不妨就按照乘客id排序)利用row_number()获得一个序号,而分组后的列车也按主键排序,根据每趟列车的容量算出其座位序号的上下限,形成一张拉链表,这样就可以用乘客序号和列车座位序号建立一个对应关系,使得每名乘客最多分配到一趟列车。

with p as (
    select passenger.*
          ,row_number() over(partition by departure_station,arrival_station order by passenger_id) rn ---- 为每组起终点的乘客编一个序号
     from passenger)
,tr as (
   select t.*
         ,sum(seat_count) over(partition by departure_station,arrival_station order by train_id) s2 ---- 对每组起终点的列车座位数做滚动累计,这个累计值就是座位序号的上限
     from train t
   )
select p.passenger_id,p.departure_station,p.arrival_station
      ,tr.train_id
  from p left join tr on p.departure_station =tr.departure_station and p.arrival_station =tr.arrival_station
       and p.rn between tr.s2-tr.seat_count+1 and tr.s2  ----- 建立乘客序号和座位序号的对应关系,座位号上限是列车容量的滚动累计值,下限则为该累计值减去当前车的容量
;


进阶挑战
完成普通挑战的选手,可以进一步完成进阶挑战。(注:提交进阶挑战 SQL 前必须先提交普通挑战的 SQL )
进阶挑战性能排名前8的选手进入决赛成绩评选。

在普通挑战基础上,要求给每个乘客分配具体座位号,每列火车根据座位数有若干个车厢(编号 1~N ),每节车厢有 100 个座位,分 20 排,每排5个座位( A,B,C,E,F ),注意:没有编号 D 的座位。
每列火车允许发售 10% 的无座票,无座的乘客 coach_number 为空,seat_number =无座

火车票分配时需要优先分配有座的票,即仅当A地到B地的所有有座票分配完后,才分配无座车票


思路:和前面的分配思路一样,无非就是多了无座票,这些无座票可以看作是容量为10%的额外车次,只需把它们叠加到原来车次记录上,在排序编号时优先级放在所有旧车次记录的后面。叠加的方法可以用和两条记录的小集合做笛卡尔积,或者用union all。
分配完了要计算车厢和座位编号,这只是简单的算术:
将当前乘客序号减去本车次座位序号的下限,得到一个车次内的相对序号;
把这个车次内的相对序号除以车厢大小(100),其商就是车厢号,余数则是车厢内的相对序号;
把这个车厢内的相对序号除以每排座位数(5),其商就是排号,余数则是座位号,可以简单地用SUBSTR映射到ABCEF。

with p as (
    select passenger.*
          ,row_number() over(partition by departure_station,arrival_station order by passenger_id) rn ---- 为每组起终点的乘客编一个序号
     from passenger)
,tr as (
select tr1.*
      ,s2-seat_count as s1  ----- 本车次座位序号的下限,上限是s2
  from (select train_id,departure_station,arrival_station,flag
              ,case when flag=0 then seat_count else trunc(seat_count*0.1) end as seat_count ---- flag=0为原车次记录,flag=1为无座票,容量只有10%
              ,sum(case when flag=0 then seat_count else trunc(seat_count*0.1) end) over(partition by departure_station,arrival_station order by flag,train_id) as s2  ---- 对每组起终点的列车座位数做滚动累计,这个累计值就是座位序号的上限
         from train t
              cross join (select 0 flag from dual union all select 1 from dual) ---和两条记录的小集合做笛卡尔积,生成额外的车次记录
        ) tr1
)
select p.passenger_id,p.departure_station,p.arrival_station
      ,tr.train_id
      ,case when tr.flag=0 then ceil((p.rn-tr.s1)/100) end as coach_number ---- 算出车厢号
      ,case when tr.flag=0 then
            -- mod(p.rn-tr.s1-1,100)+1 ---得到1-100的编号
            ceil((mod(p.rn-tr.s1-1,100)+1)/5)||SUBSTR('FABCE',mod(mod(p.rn-tr.s1-1,100)+1,5)+1,1)
            when tr.flag=1 then ' no seat'
       end as seat_number
  from p left join tr on p.departure_station =tr.departure_station and p.arrival_station =tr.arrival_station
       and p.rn between tr.s1+1 and tr.s2  ----- 建立乘客序号和座位序号的对应关系
       ;

------------

改进思路:上述写法在ORACLE典型的执行计划就是一个带过滤条件的hash join, 等值比较的部分用hash join的access, 不等值比较则用filter。当起终点重复的车次很多,乘客很多时,这个filter是很耗时间的。如果有一种方法把乘客序号和列车上的座位编号建立一种等值对应关系,那么filter就可以省去了,效率会提高很多,这个就是所谓的拉链表流水化。最直接思路是把每个座位都生成出一行记录,这样就和乘客一一对应了,但是容量1600的车次就会变成1600条记录,这个hash表会很大。因为题目限制了车次容量只有600,800,1200,1600这几种,那么取其最大公约数200就可以把每200个座位划分成一个块,这就保证所有车次都能划分成大小均等的块,而乘客序号只需除以200取整就可以建立起这个映射关系。

把不等值比较条件转化为等值比较的技巧,我在十几年前就见itpub的大神用过,几年前和老虎刘讨论时也用过(见下贴20楼):
https://www.itpub.net/forum.php?mod=viewthread&tid=120140

普通挑战:
with p as (
    select passenger.*
          ,row_number() over(partition by departure_station,arrival_station order by passenger_id) rn ---- 为每组起终点的乘客编一个序号
     from passenger)
,tr as (
    select tr1.*
          ,s2-chunk_count+lvl as chunk_id ---- 把每趟车复制成多行,每个分块一行,并进行编号
      from (select t.*
                ,sum(seat_count/200) over(partition by departure_station,arrival_station order by train_id) s2 ---- 把座位数按200分块,然后做滚动累计
                ,seat_count/200 as chunk_count  ----- 计算当前车次要分成多少块
            from train t
           ) tr1
          ,lateral(select level lvl from dual connect by level<=chunk_count)  ---- lateral view会把每一行数据复制成多行
   )
select p.passenger_id,p.departure_station,p.arrival_station
      ,tr.train_id
  from p left join tr on p.departure_station =tr.departure_station and p.arrival_station =tr.arrival_station
       and ceil(p.rn/200) = tr.chunk_id  ----- 建立乘客序号和座位序号的映射关系,只需将乘客号除以200并座取整
;




进阶挑战:和前面类似,但是因为新加入的无座车次记录容量只有10%,分块时也只能按20个座位一个块来分,这样HASH表会膨胀不少,但从效率而言还是值得的。

with p as (
    select passenger.*
          ,row_number() over(partition by departure_station,arrival_station order by passenger_id) rn ---- 为每组起终点的乘客编一个序号
     from passenger)
,tr as (
    select tr1.*
          ,s2-chunk_count+lvl as chunk_id ---- 把每趟车复制成多行,每个分块一行,并进行编号
          ,s2*20-chunk_count*20  as s1  ----- 本车次座位序号的下限,上限是s2
       from (select train_id,departure_station,arrival_station,flag
                  ,case when flag=0 then seat_count/20 else trunc(seat_count*0.1/20) end as chunk_count ---- flag=0为原车次记录,flag=1为无座票,容量只有10%, 这样排序时原车优先于无座
                  ,sum(case when flag=0 then seat_count/20 else trunc(seat_count*0.1/20) end) over(partition by departure_station,arrival_station order by flag,train_id) as s2  ---- 把座位数按20分块,然后做滚动累计
             from train t
                  cross join (select 0 flag from dual union all select 1 from dual) ---和两条记录的小集合做笛卡尔积,生成额外的车次记录
             ) tr1
          ,lateral(select level lvl from dual connect by level<=chunk_count)  ---- lateral view会把每一行数据复制成多行
)
select p.passenger_id,p.departure_station,p.arrival_station
      ,tr.train_id
      ,case when tr.flag=0 then ceil((p.rn-tr.s1)/100) end as coach_number ---- 算出车厢号
      ,case when tr.flag=0 then
            -- mod(p.rn-tr.s1-1,100)+1 ---得到1-100的编号
            ceil((mod(p.rn-tr.s1-1,100)+1)/5)||SUBSTR('FABCE',mod(mod(p.rn-tr.s1-1,100)+1,5)+1,1)
            when tr.flag=1 then ' no seat'
       end as seat_number
  from p left join tr on p.departure_station =tr.departure_station and p.arrival_station =tr.arrival_station
       and ceil(p.rn/20) = tr.chunk_id  ----- 建立乘客序号和座位序号的映射关系,只需将乘客号除以20并座取整
       ;

使用道具 举报

回复
论坛徽章:
407
紫蛋头
日期:2012-05-21 10:19:41迷宫蛋
日期:2012-06-06 16:02:49奥运会纪念徽章:足球
日期:2012-06-29 15:30:06奥运会纪念徽章:排球
日期:2012-07-10 21:24:24鲜花蛋
日期:2012-07-16 15:24:59奥运会纪念徽章:拳击
日期:2012-08-07 10:54:50奥运会纪念徽章:羽毛球
日期:2012-08-21 15:55:33奥运会纪念徽章:蹦床
日期:2012-08-21 21:09:51奥运会纪念徽章:篮球
日期:2012-08-24 10:29:11奥运会纪念徽章:体操
日期:2012-09-07 16:40:00
15#
 楼主| 发表于 2024-12-21 05:41 来自手机 | 只看该作者
学习了

使用道具 举报

回复
论坛徽章:
169
SQL数据库编程大师
日期:2016-01-13 10:30:43SQL极客
日期:2013-12-09 14:13:35SQL大赛参与纪念
日期:2013-12-06 14:03:45最佳人气徽章
日期:2015-03-19 09:44:03现任管理团队成员
日期:2015-08-26 02:10:00秀才
日期:2015-07-28 09:12:12举人
日期:2015-07-13 15:30:15进士
日期:2015-07-28 09:12:58探花
日期:2015-07-28 09:12:58榜眼
日期:2015-08-18 09:48:03
16#
发表于 2024-12-21 11:22 来自手机 | 只看该作者
new叔,我这是不是有你的影子…

使用道具 举报

回复
论坛徽章:
169
SQL数据库编程大师
日期:2016-01-13 10:30:43SQL极客
日期:2013-12-09 14:13:35SQL大赛参与纪念
日期:2013-12-06 14:03:45最佳人气徽章
日期:2015-03-19 09:44:03现任管理团队成员
日期:2015-08-26 02:10:00秀才
日期:2015-07-28 09:12:12举人
日期:2015-07-13 15:30:15进士
日期:2015-07-28 09:12:58探花
日期:2015-07-28 09:12:58榜眼
日期:2015-08-18 09:48:03
17#
发表于 2024-12-21 17:16 | 只看该作者
Naldonado 发表于 2024-12-21 11:22
new叔,我这是不是有你的影子…
  1. WITH TMP_TRAIN AS (
  2.     SELECT  
  3.           T.*,
  4.           SUM(SEAT_COUNT) OVER(PARTITION BY DEPARTURE_STATION,ARRIVAL_STATION ORDER BY TRAIN_ID)-SEAT_COUNT SEAT_COUNT_AC, --坐票累加值
  5.           SUM(SEAT_COUNT*0.1) OVER(PARTITION BY DEPARTURE_STATION,ARRIVAL_STATION ORDER BY TRAIN_ID)-SEAT_COUNT*0.1 NO_SEAT_COUNT_AC, --无座累加值
  6.           SUM(SEAT_COUNT) OVER(PARTITION BY DEPARTURE_STATION,ARRIVAL_STATION) SEAT_TOTAL  --总座位数,计算使用
  7.    FROM TRAIN_1k T
  8. )
  9. ,TMP_TRAIN_SEED AS(
  10.     SELECT T1.*,T2.N,
  11.            CASE WHEN N*20>SEAT_COUNT THEN SEAT_TOTAL+NO_SEAT_COUNT_AC-SEAT_COUNT
  12.                 ELSE SEAT_COUNT_AC END SEAT_TMP --核心的临时值,用它来确保33行的条件为等值条件,尽量将计算不留在hash,压榨性能
  13.     FROM TMP_TRAIN T1,(SELECT ROWNUM N FROM TRAIN WHERE ROWNUM<=88) T2   --按最高(1600*1.1)/20借表构造序列
  14.     WHERE T1.SEAT_COUNT*1.1>=T2.N*20
  15. )
  16. ,PASSENGER_RK AS (         
  17.     SELECT
  18.           PASSENGER_ID,
  19.           DEPARTURE_STATION,
  20.           ARRIVAL_STATION,
  21.           ROW_NUMBER() OVER(PARTITION BY DEPARTURE_STATION, ARRIVAL_STATION ORDER BY NULL) RK ,--直接顺序号,null值避开排序
  22.           FLOOR((ROW_NUMBER() OVER(PARTITION BY DEPARTURE_STATION, ARRIVAL_STATION ORDER BY NULL)-1)/20)+1 RN --按20最大公约数确定批次号
  23.     FROM PASSENGER_100w T
  24. )
  25. SELECT /*+parallel(8)*/ T1.PASSENGER_ID,T1.DEPARTURE_STATION,T1.ARRIVAL_STATION,T2.TRAIN_ID,
  26.       CASE WHEN T1.RK<=SEAT_TOTAL THEN FLOOR((T1.RK-SEAT_COUNT_AC-1)/100)+1
  27.            ELSE NULL END COACH_NUMBER,
  28.       CASE WHEN T2.TRAIN_ID IS NULL THEN NULL   --先null判断,减少次数
  29.            WHEN T1.RK<=SEAT_TOTAL THEN MOD(FLOOR((T1.RK-SEAT_COUNT_AC-1)/5),20)+1 ||
  30.                                        SUBSTR('ABCEF',MOD(T1.RK-SEAT_COUNT_AC-1,5)+1,1)  --借substr直接输出字母,减少一批casewhen
  31.            ELSE '无座' END SEAT_NUMBER
  32. FROM  PASSENGER_RK T1 LEFT JOIN TMP_TRAIN_SEED T2
  33. ON T1.RN=T2.N+(T2.SEAT_TMP/20) AND T1.ARRIVAL_STATION=T2.ARRIVAL_STATION AND T1.DEPARTURE_STATION=T2.DEPARTURE_STATION
  34. ORDER BY T1.PASSENGER_ID
复制代码

使用道具 举报

回复
论坛徽章:
519
奥运会纪念徽章:垒球
日期:2008-09-15 01:28:12生肖徽章2007版:鸡
日期:2008-11-17 23:40:58生肖徽章2007版:马
日期:2008-11-18 05:09:48数据库板块每日发贴之星
日期:2008-11-29 01:01:02数据库板块每日发贴之星
日期:2008-12-05 01:01:03生肖徽章2007版:虎
日期:2008-12-10 07:47:462009新春纪念徽章
日期:2009-01-04 14:52:28数据库板块每日发贴之星
日期:2009-02-08 01:01:03生肖徽章2007版:蛇
日期:2009-03-09 22:18:532009日食纪念
日期:2009-07-22 09:30:00
18#
发表于 2024-12-21 21:34 来自手机 | 只看该作者
我现在只能在手机上看个大概,但是好像你并没有把所有座位都卖完再卖无座票?你只是将车次直接扩容。如果乘客少,原来票就够,你还是会卖无座票。

使用道具 举报

回复
论坛徽章:
169
SQL数据库编程大师
日期:2016-01-13 10:30:43SQL极客
日期:2013-12-09 14:13:35SQL大赛参与纪念
日期:2013-12-06 14:03:45最佳人气徽章
日期:2015-03-19 09:44:03现任管理团队成员
日期:2015-08-26 02:10:00秀才
日期:2015-07-28 09:12:12举人
日期:2015-07-13 15:30:15进士
日期:2015-07-28 09:12:58探花
日期:2015-07-28 09:12:58榜眼
日期:2015-08-18 09:48:03
19#
发表于 2024-12-21 21:36 来自手机 | 只看该作者
newkid 发表于 2024-12-21 21:34
我现在只能在手机上看个大概,但是好像你并没有把所有座位都卖完再卖无座票?你只是将车次直接扩容。如果乘 ...

回先卖坐票的,无座票的值从总坐票的值开始

使用道具 举报

回复
论坛徽章:
519
奥运会纪念徽章:垒球
日期:2008-09-15 01:28:12生肖徽章2007版:鸡
日期:2008-11-17 23:40:58生肖徽章2007版:马
日期:2008-11-18 05:09:48数据库板块每日发贴之星
日期:2008-11-29 01:01:02数据库板块每日发贴之星
日期:2008-12-05 01:01:03生肖徽章2007版:虎
日期:2008-12-10 07:47:462009新春纪念徽章
日期:2009-01-04 14:52:28数据库板块每日发贴之星
日期:2009-02-08 01:01:03生肖徽章2007版:蛇
日期:2009-03-09 22:18:532009日食纪念
日期:2009-07-22 09:30:00
20#
发表于 2024-12-21 23:10 来自手机 | 只看该作者
不错,看到你把临时票加上 seat total 了,这样确实能错开了。等我有空再仔细看看

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

TOP技术积分榜 社区积分榜 徽章 团队 统计 知识索引树 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档
  ChinaUnix | ChinaUnix博客 | ChinaUnix论坛
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 未成年人举报专区 
京ICP备16024965号-8  北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表