初步假设
假设某列车共途径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 | ... | 00001111 | 11110000(表明暂时保留A-D阶段车票) |
| 14B | ... | 00011111 | 00000000(不保留) |
当售票时间到达尾声,则使用当前的乘段码与掩码按位或(类似于乘客退票操作),完成客票的释放。
性能问题
前面我们刻意忽略了数据库中按位与查询的性能问题,这种查询主要发生在两个地方:
- 乘客查询某天从站点X至站点Y的列车车票情况,此时不仅涉及到车票查询问题,还涉及到乘段车次查询的问题。
- 用户已查询到理想车次,需要下单,下单过程中需要获取符合条件的车票。
关于车站车次查询的问题不在本次讨论的范围内,所以只考虑解决第二个问题。
如何查询?首先要看乘段码是如何存储的。存储方式大致上有三种: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 | ... | 00001111 | 00000000 |
| 14B | ... | 00011111 | 00000000 |
| 14D | ... | 11001111 | 00000000 |
| 14F | ... | 01110101 | 00000000 |
席位查询表
| 席位号 | 其他字段 | 起始位 | 结束位 |
|---|---|---|---|
| 14A | ... | 5 | 8 |
| 14B | ... | 4 | 8 |
| 14D | ... | 1 | 2 |
| 14D | ... | 5 | 8 |
| 14F | ... | 1 | 4 |
| 14F | ... | 6 | 6 |
| 14F | ... | 8 | 8 |
席位表和席位查询表是一对多的结构,起始位表示从第几位开始为1,结束位表示最后一个1的位置。
我们看到,14D的乘段码在席位查询表中被拆成了两条记录,便于后续的查询操作。
当乘客有购票需求时,比如从B-E,需求先是被转换成01110000,然后转换成(2,4)这样的元组。此时,我们只需要在查询表中查询
select * form xxx where start_point <=2 and end_point >=4
一个联合索引就能解决。
当然,如果认为购票时需要对查询表进行增删改的工作可能影响性能,也可以自己实现内存内的索引结构来解决这一问题,此处就不再过多讨论了。
如果想要查询最符合需求的席位,还可以添加字段(比如1的连续数量),通过排序,选择最合适的席位。