SQL应用之依赖计算与回路检测

依赖关系是数据库表中比较常见的一种设计,一个典型的应用就是薪资模块。具备这种依赖关系的数据结构需要解决“依赖计算”“循环检测”“依赖查看”等问题。本文旨在提供解决这些问题的一种通用解决方案。

什么是依赖计算?

举一个典型的例子,设计薪资模块,一个工资项,即可以是固定值value,也可以是由其它的一个或多个工资项通过表达式得到的结果。

工资项 依赖工资项
A value1
B value2
C A - B + value3
D C * 0.8

很明显工资项A、B不依赖任何项,工资项C依赖于A和B,而D依赖于C。在计算最终工资时,必须按照这种依赖顺序,才能够得到最终的准确结果。把工资按这种依赖顺序计算出来,就是“依赖计算”。

什么是回路检测呢?

细心的人,也许很快就反应过来,如果这里的依赖关系不严格(如A依赖B,B又依赖A),出现“依赖死循环”,该怎么办呢?这就是下面要解决的问题:“回路检测”,也许称为“循环检测”更恰当。在计算依赖之前必须通过“回路检测”保证数据有效性。

回路检测,除了找出具有循环依赖的数据项之外,还要找出这些循环依赖的过程。

-- @RTable存储依赖关系的数据
declare @RTable table(Id int,Name varchar(50),RefId int)
-- @table数据项列表,OrderNo表示其计算顺序编号
declare @table table(Id int,Name varchar(50),OrderNo int)

insert into @RTable values(1,'A',2),(2,'B',3),(3,'C',4),(4,'D',1),
    (5,'E',6),(6,'F',7),(7,'G',5),
    (8,'H',9),(9,'I',8),
    (10,'J',10),
    (11,'K',12),(12,'L',13),(12,'L',14),(13,'M',15),(14,'N',16),(15,'O',16)

insert into @table(Id,Name) select distinct Id,Name from @RTable
insert into @table(Id,Name) values(28,'AB'),(28,'AC'),(16,'P')

-- ===================== 回路检测(正向) =======================
-- 1、检测回路(IsCircle就是用于判断是否循环)
declare @RouteTable table(RouteNo int,RouteOrder int,Id int,IsCircle bit)
declare @resultTable table(RowNum int identity(1,1),Id int,Name varchar(50),InCircle bit,Marked bit/*查找回路路线使用*/)
insert into @resultTable(Id,Name) select Id,Name from @table

declare @i int,@maxi int,@tmpId int,@tmpRefId int--,@flag bit = 0
set @i = 1
set @maxi = (select COUNT(*) from @resultTable)
while @i <= @maxi
  begin
    select @tmpId = Id from @resultTable where RowNum = @i

    set @tmpRefId = @tmpId
    --set @flag = 0
    while 1=1
      begin
        -- 判断引用到末结点时跳出循环
        if not exists(select 1 from @RTable where Id = @tmpRefId)
          begin
            break
          end
        -- 下一步
        select @tmpRefId = RefId from @RTable where Id = @tmpRefId 
        -- 判断循环引用时跳出循环
        if @tmpRefId = @tmpId
          begin
            --set @flag = 1
            update @resultTable set InCircle = 1 where RowNum = @i -- 更新状态
            break
          end
      end

    set @i += 1
  end

select * from @resultTable where InCircle = 1

-- 2、计算回路
declare @tmpNode table(Id int)
declare @RouteCount int = 1,@IsMarked bit
set @i = 1
set @maxi = (select COUNT(*) from @resultTable)
while @i <= @maxi
  begin
    select @tmpId = Id,@IsMarked = Marked from @resultTable where RowNum = @i
    if @IsMarked = 1
      begin
        set @i += 1
        continue
      end

    set @tmpRefId = @tmpId
    delete @tmpNode
    while 1=1
      begin
        -- 判断引用到末结点时跳出循环(非回路)
        if not exists(select 1 from @RTable where Id = @tmpRefId)
          begin
            break
          end
        -- 下一步
        select @tmpRefId = RefId from @RTable where Id = @tmpRefId 
        insert into @tmpNode select @tmpRefId
        -- 判断循环引用时跳出循环(是回路)
        if @tmpRefId = @tmpId
          begin
            --set @flag = 1
            insert into @RouteTable(RouteNo,RouteOrder,Id,IsCircle) select @RouteCount,ROW_NUMBER() over(order by Id),Id,1 from @tmpNode
            set @RouteCount += 1
            update @resultTable set Marked = 1 where Id in(select Id from @tmpNode)

            update @resultTable set InCircle = 1 where RowNum = @i -- 更新状态
            break
          end
      end

    set @i += 1
  end

select * from @RouteTable

---- ===================== 忽略策略计算依赖顺序 =======================
---- 排除自身依赖(原因:1->1,1->2 => 1->2)
--delete @RTable where Id = RefId
--update @table set OrderNo = 0 where Id not in(select Id from @RTable)

---- 3、采用回路忽略策略,计算依赖顺序
--declare @tmpTable table(Id int,RefMax int)
--while 1=1
--  begin
--  -- 清空临时表
--  delete @tmpTable

--  insert into @tmpTable(Id,RefMax)
--  select t.Id,t2.TMax 
--  from @table t 
--  left join (
--      select r.Id,COUNT(*) as Total,MAX(t.OrderNo) as TMax,
--          SUM(CASE WHEN t.OrderNo IS NOT NULL THEN 1 ELSE 0 END) as TCount
--      from @RTable r  
--          left join @table t on t.Id = r.RefId
--      group by r.Id
--  ) t2 on t2.Id = t.Id 
--  where t2.Total = t2.TCount
--      and t.OrderNo is null       

--  if not exists(select 1 from @tmpTable)
--    begin
--      break;
--    end

--  update @table set OrderNo = t.RefMax + 1 from @tmpTable t where t.Id = [@table].Id
--  end

---- 查看计算顺序
--select * from @table

---- 查询所有回路数据(从侧面查看回路数据)
--select * from @table where OrderNo is null

-- ======================= 向上向下查找所有依赖项 ========================
---- 4、向上找某结点所有依赖项
declare @Id int = 12
;WITH cte AS
(  
    -- 起始条件
    SELECT Id,RefId FROM @RTable as t WHERE Id = @Id
    UNION ALL  
    -- 递归条件
    SELECT t.Id,t.RefId FROM @RTable as t,cte t2 WHERE t.RefId = t2.Id and t.Id <> @Id
)
select c.Id,t.Name,c.RefId from cte c left join @table t on t.Id = c.Id

---- 5、向下找某结点所有依赖项:查看某结点所在路线之后的所有数据(在回路返回当前结点时中止)
--declare @Id int = 11
set @Id = 12
;WITH cte AS
(  
    -- 起始条件
    SELECT Id,RefId FROM @RTable as t WHERE Id = @Id
    UNION ALL  
    -- 递归条件
    SELECT t.Id,t.RefId FROM @RTable as t,cte t2 WHERE t.Id = t2.RefId and t.RefId <> @Id
)
select c.Id,t.Name,c.RefId from cte c left join @table t on t.Id = c.Id

你可能感兴趣的:(数据库)