调用Tour_Bill函数获取单条账单却返回多条数据的问题
问题分析与解决办法
咱们先把问题拆解开:你调用Tour_Bill函数传入TourId=4,却得到多条数据,核心问题主要出在两个地方,咱们一个个说:
为什么会返回多条数据?
1. 硬编码的WHERE条件完全忽略了传入参数
你看函数里的SELECT语句最后写的是WHERE Tour.TourId = 4——这意味着不管你调用时传101还是4,函数都会一股脑返回TourId=4的所有相关数据!完全没用到你传入的@TourID参数,这是最直观的问题。
2. 多表连接产生了笛卡尔积
再看函数里的表连接逻辑:
SpecialActivity和VisitingPlaces都是和Itinerary关联的,如果一个行程(Itinerary)有多个特殊活动或者多个景点,INNER JOIN会把这些记录两两组合,直接导致结果行数翻倍。- 另外你还连了
Participant表,如果同一个Tour有多个参与者,这个连接也会把每条行程数据重复多次,每条对应一个参与者。 - 更关键的是,你没有对这些多对多关联的费用做聚合,比如多个活动的费用是分开计算的,而不是汇总成总活动费用,这既导致数据重复,总费用计算也肯定不对。
3. 函数返回表定义还有语法错误
你看返回表的这一行:CustomerId varchar(30) Name varchar(180) NOT NULL——两个字段之间缺了逗号,这虽然不直接导致多数据,但会引发语法错误,得先补上。
解决办法
步骤1:先把参数传递的问题修好
把函数里的WHERE条件改成用传入的参数,替换掉硬编码的4:
WHERE Tour.TourId = @TourID
步骤2:处理多对多连接的笛卡尔积
对于SpecialActivity和VisitingPlaces,咱们需要先按ItineraryId汇总总费用,再和主表连接,避免重复行。可以用CTE(公共表表达式)来做这个聚合:
步骤3:调整Participant表的连接(按需)
如果你的业务是要给每个参与者生成单独的账单,那保留Participant连接没问题;但如果是要生成整个Tour的总账单,这个连接只会带来重复数据,建议去掉。
修正后的完整函数示例
下面是调整后的函数,修复了所有上述问题:
CREATE FUNCTION dbo.Tour_Bill(@TourID int) RETURNS @Bill TABLE ( TourID int, ItineraryID int, StartDate date, EndDate date, Duration int, Distance float, CustomerId varchar(30), Name varchar(180) NOT NULL, ContractNo varchar(12) NOT NULL, GuideID int, PaymentForGuide money, TotalSpecialActivityCost money, TotalVisitingPlaceTicketCost money, NumberOfPeople int, CostForMeal money, Accomadation varchar(100), TotalAccommodationCost money, TourPackegeCost money, GRAND_COST money ) AS BEGIN -- 先汇总每个行程的特殊活动总费用 WITH SpecialActivityTotals AS ( SELECT ItineraryId, SUM(Cost) AS TotalSpecialActivityCost FROM SpecialActivity GROUP BY ItineraryId ), -- 汇总每个行程的景点门票总费用 VisitingPlacesTotals AS ( SELECT ItineraryId, SUM(Cost) AS TotalVisitingPlaceTicketCost FROM VisitingPlaces GROUP BY ItineraryId ) INSERT INTO @Bill SELECT Tour.TourId, Itinerary.ItineraryId, Tour.StartDate, Tour.EndDate, DATEDIFF(day, Tour.StartDate, Tour.EndDate) AS Duration, Itinerary.EstTravelDist, Tour.CustomerId, Person.FirstName + ' ' + Person.LastName AS FullName, Contract.ContractNo, Guide.IdNo, CAST(500 * DATEDIFF(day, Tour.StartDate, Tour.EndDate) AS money) AS PaymentForGuide, ISNULL(SAT.TotalSpecialActivityCost, 0) AS TotalSpecialActivityCost, ISNULL(VPT.TotalVisitingPlaceTicketCost, 0) AS TotalVisitingPlaceTicketCost, Tour.NumberOfPeople, CAST(Contract.UnitPrice * DATEDIFF(day, Tour.StartDate, Tour.EndDate) * Tour.NumberOfPeople AS money) AS CostForMeal, Accommodation.Location AS Accomadation, -- 注意:原Accommodation表没有UnitPrice字段,这里假设你是想用Contract的UnitPrice,或者需要调整 CAST(Contract.UnitPrice * DATEDIFF(day, Tour.StartDate, Tour.EndDate) AS money) AS TotalAccommodationCost, CAST(Itinerary.EstTravelDist * 40 AS money) AS TourPackegeCost, -- 重新计算总费用,用汇总后的活动和景点费用 CAST( Itinerary.EstTravelDist * 40 + ISNULL(SAT.TotalSpecialActivityCost, 0) * Tour.NumberOfPeople + ISNULL(VPT.TotalVisitingPlaceTicketCost, 0) * Tour.NumberOfPeople + 500 * DATEDIFF(day, Tour.StartDate, Tour.EndDate) + Contract.UnitPrice * DATEDIFF(day, Tour.StartDate, Tour.EndDate) + Contract.UnitPrice * DATEDIFF(day, Tour.StartDate, Tour.EndDate) * Tour.NumberOfPeople AS money) AS GRAND_COST FROM Itinerary INNER JOIN Tour ON Tour.TourId = Itinerary.TourId INNER JOIN Guide ON Guide.TourId = Tour.TourId INNER JOIN Contract ON Itinerary.ItineraryId = Contract.ItineraryId INNER JOIN Person ON Person.IdNo = Guide.IdNo LEFT JOIN Accommodation ON Accommodation.TourId = Tour.TourId LEFT JOIN SpecialActivityTotals SAT ON SAT.ItineraryId = Itinerary.ItineraryId LEFT JOIN VisitingPlacesTotals VPT ON VPT.ItineraryId = Itinerary.ItineraryId -- 去掉Participant连接,如果你需要按参与者生成账单,再加回来 WHERE Tour.TourId = @TourID -- 用传入的参数,不再硬编码 RETURN; END
额外提醒
- 原函数里
Accommodation表没有UnitPrice字段,我在修正时用了Contract.UnitPrice,你需要根据实际业务逻辑调整这部分。 - 原GRAND_COST计算里有重复的
UnitPrice * DATEDIFF(...)项,可能是输入错误,你要确认下业务上的总费用计算规则。 - 如果确实需要每个参与者的账单,把Participant表加回来即可,此时每条数据对应一个参与者,这是合理的业务场景。
内容的提问来源于stack exchange,提问作者ray.one
相关产品推荐
相关产品推荐

