zoukankan      html  css  js  c++  java
  • 租车信息系统数据库设计(5)

    前篇回顾

    租车信息系统数据库设计(1)租车信息系统数据库设计(4)我们完成了一个简单的租车信息系统的数据库设计。

    从功能上来讲还有很多可以扩展的方面,如权限管理、发票管理等等,本文不将展开。大家可以对这些需求进行设想,设计相应的表、字段和关联,并融合到整体设计中。

    本篇是本系列的最后一篇,我们将利用先前设计的数据库结构来写一些查询,完成一些业务需求,同时也反过来审视先前的设计。

    获取需要催促还车的订单

    我们的业务人员每天都要获取超出预订期限未还车的订单。对于这些订单,业务人员需要一一电话客户。

    那就让我们来帮助业务人员写这个查询吧!

    select	
    	RentalOrder.Order_ID
    from
    	Table_Order RentalOrder
    where
    	RentalOrder.Order_BookEndDate < GETDATE()
    	and
    	OrderStatus_ID = (select
    						OrderStatus.OrderStatus_ID
    					from
    						Table_OrderStatus OrderStatus
    					where
    						OrderStatus.OrderStatus_Name = 'Inuse')          

    这个查询把订单预订终止日期比当前时间早,但订单状态还是‘Inuse’的订单好找了出来(订单状态种类请参见租车信息系统数据库设计(2))。

    注:1. 此查询中有一个子查询,大家可以直接使用查出来的OrderStatus_ID代替之,这样对性能有好处。此处是为了增加可读性。

              2. 此查询仅输出了Order_ID,大家可根据需要连接相应的表,输出需要的字段。

    顾客预订用车

    当某顾客决定要租车时,他会在租车网站上选定某型号的车、哪个门店、某个时间段,并点击预订。

    此时系统要查找指定门店的该车型车辆在相应时间是否有档期,我们分步骤来写这个查询。

    查询的输入为:

    1. 车型ID:@CarCategory_ID

    2. 门店ID:@Store_ID

    3. 租用起始日期:@StartDate

    4. 租用结束日期:@EndDate

    查询的输出为:

    符合条件的车辆ID:Car_ID

    --1 选出指定车型(Category),指定门店(Store),且车辆状态为Ready和Inuse的车辆
    select	
    	Car_ID
    into
    	#CandidateCars1
    from
    	Table_Car
    where
    	CarCategory_ID = @CarCategory_ID
    	and
    	Store_ID = @Store_ID
    	and
    	CarStatus_ID in (select
    						CarStatus_ID
    					from
    						Table_CarStatus
    					where
    						CarStatus_Name in ('Ready', 'Inuse'));

    注:1. 我们把第一步的查询结果放到临时表中,供下一步使用。

              2. 车辆状态的枚举值请参见租车信息系统数据库设计(1)。车辆状态为Inuse表示此时该车正在被租用,不在车库中(但对于未来某段时间的预订,该车可能会有档期)。车辆状态和订单状态都有一个Inuse的枚举值,请不要混淆。

              3. 此查询中的子查询可以用查出的ID值代替。

    --2 在候选车辆中,选出在@StartDate和@EndDate之间有档期的车辆
    select
    	CandidateCars.Car_ID
    into
    	#CandidateCars2
    from
    	#CandidateCars1 CandidateCars
    where
    	CandidateCars.Car_ID 
    	not in 
    	(
    		select distinct
    			RentalOrder.Car_ID
    		from
    			#CandidateCars1 CandidateCars
    			inner join
    			Table_Order RentalOrder
    			on
    				CandidateCars.Car_ID = RentalOrder.Car_ID
    		where
    			(
    				@StartDate >= RentalOrder.Order_BookStartDate
    				and
    				@StartDate <= RentalOrder.Order_BookEndDate
    			)
    			or
    			(
    				@EndDate >= RentalOrder.Order_BookStartDate
    				and
    				@EndDate <= RentalOrder.Order_BookEndDate
    			)
    	);
    

    到第二步结束我们的结果集中可能会有多辆符合条件的车。那到底选哪一辆车最合适呢?

    我首先想到的是尽可能把车辆的档期排满。那就是一辆一辆的来排,第一辆排不下了,再排到第二辆,这样也许能使车辆的利用率最高。

    但这种方案很快就被我排除了,原因有3个方面:

    1. 这种方式会使某些车辆的租用很稠密,而另一些车可能一个订单都没有。

    2. 某些车太稠密的订单容易产生意外情况,如前一个租车的顾客还车晚了,造成后面的顾客无法领到订单中的车辆。这种情况虽然可以通过临时更改车辆,但需要人为处理的意外境况增多了,且还需要修改原先的订单中的车辆信息。

    3. 某些车太稠密的订单又会使某些车辆的使用率特别高,造成其维修率和折损率也特别高。

    所以在第三步选车策略的目标是使每辆车的租用尽可能平均,车辆的租用间隔稀疏一些。相应的选车策略可以有:

    1. 选从当前时间开始,被预订的时间总和最少的那辆车

    2. 选从当前时间开始,被预订次数最少的那辆车

    3. 选之前预订时间段与本次预订时间段前后间隔最大的那辆车

    4. 随机选择一辆车

    这几种策略哪个最好,我很难说,可能需要进行一些实验。大家可以思考讨论。

    在这里我选择第3个策略来写查询。第3个策略相对于其他的策略从文字上稍难理解一些,我先来看一个图示:

    打印

    图中是某辆车的预订时间轴,我希望本次预订期与之前的预订期之间的间隔尽可能大,这样当某顾客还车迟了时就能有多一些的缓冲时间。

    要用SQL实现这一策略,我想到了2种算法:

    1. 把前后间隔时间求和后倒序排列,取Top 1的记录

    2. 先按与‘之前预订期1’的间隔时间倒序排列后取Top 2的2条记录,再拿这2条记录按与‘之前预订期2’的间隔时间倒序排列后取Top 1的记录

    我最后选择了算法1来写SQL语句

    --3 在候选车辆中,选之前预订时间段与本次预订时间段前后间隔最大的那辆车
    With CandidateCarsWithDate as
    (
    	select
    		CandidateCars.Car_ID,
    		(select
    			MAX(RentalOrder1.Order_BookEndDate)
    		from
    			Table_Order RentalOrder1
    		where
    			RentalOrder1.Car_ID = CandidateCars.Car_ID
    			and
    			RentalOrder1.Order_BookEndDate < @StartDate) MaxBeforeDate,
    		(select
    			MIN(RentalOrder2.Order_BookStartDate)
    		from
    			Table_Order RentalOrder2
    		where
    			RentalOrder2.Car_ID = CandidateCars.Car_ID
    			and
    			RentalOrder2.Order_BookStartDate > @EndDate) MinAfterDate
    	from
    		#CandidateCars2 CandidateCars
    )
    select top(1)
    	Car_ID
    from
    	CandidateCarsWithDate
    order by
    	DATEDIFF(day, ISNULL(MaxBeforeDate, '1900-01-01'), ISNULL(MinAfterDate, '9999-01-01')) desc;

    我使用了CTE表达式来写这个查询,语句在SQL Server 2008中测试通过。

    注:1. With中的查询包含了两个子查询,分别获得比本次预订起始日期早的最大的终止日期和比本次预订终止日期晚的最小的起始日期。

              2. 在With子查询下的select语句,按照With中计算得到的MinAfterDate和MaxBeforeDate的差值降序排列(按照算法描述,应该减去本次预订的间隔天数,但这不会影响排序结果),并取出Top 1的车辆ID。

              3. 最后一行使用ISNULL的原因是在本次预订之前或之后可能没有其他预订,故取了两个特别的日期。

    至此我们得到了符合条件的车辆ID了。

    总结与思考

    本篇我们设想了2个需求,并撰写了查询。‘获取需要催促还车的订单’较为简单一些,我就不多说什么了。对于‘顾客预订用车’相对复杂些,我再多说两句。

    1. 对于‘顾客预订用车’,我为了把逻辑清晰分成了3小段,使用了2个临时表。大家可以考虑把这些逻辑合在一起,写成一个CTE或使用嵌套子查询(如果大家使用临时表,记得在最后把这些表drop掉)。

    2. 大家可以再进一步加入用户验证和生成Order的逻辑后封装成存储过程供应用程序端调用。

    3. 本文中选用的策略和算法未必是最优的,大家可以进一步思考、探索与分享。

    4. 本文为了逻辑清晰,写出的查询性能并不是很好,有很多可以提升的地方。大家在把三段查询合并的时候可以从全局进行考虑来调校查询性能。

    后记

    非常感谢“es潇潇”的反馈。原文第二部分第二段的SQL查询中,原先使用的是内连接,这会使新购置的车辆无法被预订到。这是本文代码的疏漏,对原文的SQL语句已进行修正。

    “es潇潇”的问题又给了我两个方面的思考:
    1. 我们的门店
    可以根据地域分成组,当在一个门店无法预订到指定车型时,可以在同一组的其他门店查找合适车辆进行预订。对于在一个区域中的门店可以通过内部调车来满足顾客的需求。这样的改动对于顾客来说又是完全透明的。要做这样的改动就需要增加新表,并修改原先的查询逻辑,大家可以进一步思考。

    2. “当我们有一把锤子时,看到的任何东西都像钉子”。对于我来说我的锤子就是SQL了,所以我把选车的逻辑放在了SQL查询中。这样是否好呢?是否把候选车辆集返回给前台应用服务器来做逻辑筛选更好呢?这里要权衡两个资源要素。SQL端进行筛选,损失了数据库的CPU等资源,但数据库到应用服务器间的网络负载减轻了,在Application进行筛选就正好反过来。如何进行选择呢?

    大家的反馈印证了,集众智而有所进这句话。再次感谢大家的支持与建议。

  • 相关阅读:
    程序自动网站留言,自动登录,自动投票等做法 httpclient 拂晓风起
    CruiseControl 安装 配置 教程 实例 搭建服务器 (CruiseControl + git/svn) 拂晓风起
    编码和字符集的关系 拂晓风起
    PuttyGen生成SSH(key) 带图 TortoiseGit和Github的SSH生成 拂晓风起
    计算时间差,将yyyyMMddHHmmss字符格式转为时间
    C# 操作非标准的xml文件
    SqlServer2000中调度包到作业中,自动执行失败的解决方法
    ReSharper 命名规则
    Js中setInterval、setTimeout不能传递参数问题 及各自的关闭方法
    获取存储过程返回值
  • 原文地址:https://www.cnblogs.com/DBFocus/p/1917324.html
Copyright © 2011-2022 走看看