火车票查询/订票算法初探

331 阅读6分钟

初步假设

假设某列车共途径9站,A-B-C-D-E-F-G-H-I。

每位乘客实际上是完成连续的、一段又一段的乘段,则每个席位的可划分为8个乘段,以二进制表示为11111111。

此时席位表的数据结构类似于下面的结构:

席位号其他字段乘段码
14A...11111111
14B...00011111

假设乘客需要从B点到E点,则乘客的车票需求为01110000。

查询时,对通过车票需求和席位表的段位码进行按位与,得到的结果与乘客需求相同的,就意味着可以出售,从而获得余票数。(这里先不考虑乘客选定车站时需要查询出的多个车次,只针对单个车次)

11111111 & 01110000 = 01110000 √

00011111 & 01110000 = 00010000 ×

原则上,当每次乘客订票时,系统为其选择1最少的符合条件的席位,能够达到席位的最佳利用。

如果乘客退票,只需要将乘客的车票与原本的席位的段位码按位或(|),则可完成退票的操作。

预留车票

实际情况中,乘客旅程的分布是不均匀的,票务部门需要为部分乘段预留车票,此时,我们可以引入掩码,在席位数据初始化时,暂时禁止某些席位特定乘段的售卖,或者当需要为始发站乘客保留车票时,用掩码去进行车票的售卖(对业务了解的不是很清楚,就不过多介绍了)。

此时,席位表的数据结果如下:

席位号其他字段乘段码掩码
14A...0000111111110000(表明暂时保留A-D阶段车票)
14B...0001111100000000(不保留)

当售票时间到达尾声,则使用当前的乘段码与掩码按位或(类似于乘客退票操作),完成客票的释放。

性能问题

前面我们刻意忽略了数据库中按位与查询的性能问题,这种查询主要发生在两个地方:

  1. 乘客查询某天从站点X至站点Y的列车车票情况,此时不仅涉及到车票查询问题,还涉及到乘段车次查询的问题。
  2. 用户已查询到理想车次,需要下单,下单过程中需要获取符合条件的车票。

关于车站车次查询的问题不在本次讨论的范围内,所以只考虑解决第二个问题。

如何查询?首先要看乘段码是如何存储的。存储方式大致上有三种:varchar,number,bit(n)

其中varchar,number是关系数据库通常会提供的功能,bit(n)是postgre提供的数据类型。

仍然采用上面的例子,查询B站到E站的车票:

使用varchar,则需要使用like查询。

select * from xxx where section_code like '_111____'

全表扫描,性能会比较差。

使用number, 则在查询中可以使用类似于Oracle中的bitand函数。

select * from xxx where bitand(section_code, 112) = 112

函数索引应该也是用不上的,还是全表扫描。

至于bit(n) 类型……我对PG不是很精通,并没有查到PG对该类型有什么特殊的查询优化。

性能优化?

全表扫描性能太差,有没有什么办法进行优化?最开始我有两个思路。

1. 二进制转换成十进制

我们先来分析一下车票的特征,非常明显的是,车票编码中一定是多个1连续出现。

比如01110000,转换成十进制是112,能够售卖的席位的乘段码,一定是大于等于112的(但大于等于112的乘段码不一定能够满足售卖条件,例如10000000)。

所以,我们是否可以加入一条检索条件 section_code >= 112 利用索引进行初步的筛选?

这种做法的弊端是,当席位的乘段码越小,筛选的识别度越差。

2. 游程编码

或者,我们将车票需求按照游程编码进行处理呢?

01110000 => 134

如果忽略结尾0的数量,就是13。

对于席位乘段码进行游程编码:

11111111 => 08

0001111 => 35

11110011 => 0422

10110010 => 011221

会发现,在这种情况下,编码以1X(X>=3)开头和0X(X>=4)开头的席位能够满足乘客的需求。但是,并没有一个很好的索引结构来支持这一检索过程。

并且越到售票的后期,乘段码越零散,游程编码越长,查询和统计的方法会越复杂

两个思路都走不通。

时空转换

通过对于乘客购票需求和席位乘段码的进一步观察,会发现,乘段码本身是可以拆成一段段连续的1的。

所以,我考虑通过空间换时间,通过冗余一张查询表,来提升查询的性能。

此时,数据表结构大概类似下面的样子:

席位表

席位号其他字段乘段码掩码
14A...0000111100000000
14B...0001111100000000
14D...1100111100000000
14F...0111010100000000

席位查询表

席位号其他字段起始位结束位
14A...58
14B...48
14D...12
14D...58
14F...14
14F...66
14F...88

席位表和席位查询表是一对多的结构,起始位表示从第几位开始为1,结束位表示最后一个1的位置。

我们看到,14D的乘段码在席位查询表中被拆成了两条记录,便于后续的查询操作。

当乘客有购票需求时,比如从B-E,需求先是被转换成01110000,然后转换成(2,4)这样的元组。此时,我们只需要在查询表中查询

select * form xxx where start_point <=2 and end_point >=4

一个联合索引就能解决。

当然,如果认为购票时需要对查询表进行增删改的工作可能影响性能,也可以自己实现内存内的索引结构来解决这一问题,此处就不再过多讨论了。

如果想要查询最符合需求的席位,还可以添加字段(比如1的连续数量),通过排序,选择最合适的席位。