Оптимизация агрегирования данных ветви дерева в SQL Server 2008 (рекурсия) - PullRequest
5 голосов
/ 10 августа 2011

У меня есть таблица, содержащая этапы и подэтапы определенных проектов, и таблица с конкретными задачами и сметными затратами.
Мне нужен какой-то способ агрегирования каждого уровня (этапов / подэтапов), чтобы увидеть, сколькоэто стоит, но сделать это с минимальными затратами на производительность.

Чтобы проиллюстрировать это, я буду использовать следующую структуру данных:

CREATE TABLE stage
(
    id int not null,
    fk_parent int
)

CREATE TABLE task
(
    id int not null,
    fk_stage int not null,
    cost decimal(18,2) not null default 0
)

со следующими данными:

==stage==
id  fk_parent
1   null
2   1
3   1

==task==
id  fk_stage  cost
1   2         100
1   2         200
1   3         600

Я хочу получить таблицу с общими затратами по каждому филиалу.Примерно так:

Stage ID      Total Cost
1             900
2             300
3             600

Но я тоже хочу, чтобы это было продуктивно.Я не хочу, чтобы в итоге получились крайне плохие решения, такие как Худший алгоритм в мире .Я имею в виду, что это так.В случае, если я буду запрашивать данные для всех элементов в таблице stage, с общими затратами, каждая общая стоимость будет оценена D раз, где D - глубина в дереве (уровень), на которомон расположен.Боюсь, у меня будет очень низкая производительность при большом количестве данных с большим количеством уровней.

ТАК,

Я решил сделать что-то, что заставило меня задать этот вопрос здесь.
Я решил добавить еще 2 столбца в таблицу stage для кэширования.

...
calculated_cost decimal(18,2),
date_calculated_cost datetime
...

Итак, я хотел передать другую переменную в коде, значение datetime, равноевремя, когда этот процесс был запущен (в значительной степени уникальным).Таким образом, если в строке stage уже есть date_calculated_cost, равный тому, который я несу, я не буду рассчитывать его снова, а просто верну значение calculated_cost.

Iне могу сделать это с помощью функций (необходимо обновить таблицу stage, после расчета затрат)
Я не могу сделать это с помощью процедур (рекурсия внутри запущенных курсоров не нужна)
Яне уверен, что временные таблицы подходят, потому что они не разрешают одновременные запросы к одной и той же процедуре (что наименее вероятно, но в любом случае я хочу сделать это правильно)
Я не мог найти другие способы.

Я не ожидаю однозначного ответа на мой вопрос, но вознагражу любую хорошую идею, и в качестве ответа будет выбрана лучшая.

1 Ответ

2 голосов
/ 11 августа 2011

1.Способ запроса таблиц для получения агрегированных затрат.

  1. Расчет стоимости для каждого этапа.
  2. Использование рекурсивного CTE для получения уровня для каждого этапа.
  3. Сохраните результат во временной таблице.
  4. Добавьте несколько индексов во временную таблицу.
  5. Обновление стоимости в таблице temp в цикле для каждого уровня

Первые три шага объединяются в один оператор.Для производительности может быть полезно выполнить первое вычисление, cteCost, для собственной временной таблицы и использовать эту временную таблицу в рекурсивном cteLevel.

;with cteCost as
(
  select s.id,
         s.fk_parent,
         isnull(sum(t.cost), 0) as cost
  from stage as s
    left outer join task as t
      on s.id = t.fk_stage
  group by s.id, s.fk_parent
),
cteLevel as
(
  select cc.id,
         cc.fk_parent,
         cc.cost,
         1 as lvl
  from cteCost as cc
  where cc.fk_parent is null
  union all
  select cc.id,
         cc.fk_parent,
         cc.cost,
         lvl+1
  from cteCost as cc
    inner join cteLevel as cl
      on cc.fk_parent = cl.id       
)
select *
into #task
from cteLevel

create clustered index IX_id on #task (id)
create index IX_lvl on #task (lvl, fk_parent)

declare @lvl  int
select @lvl = max(lvl)
from #task

while @lvl > 0
begin

  update T1 set
    T1.cost = T1.cost + T2.cost
  from #task as T1
    inner join (select fk_parent, sum(cost) as cost
                from #task
                where lvl = @lvl
                group by fk_parent) as T2
      on T1.id = T2.fk_parent

  set @lvl = @lvl - 1
end

select id as [Stage ID],
       cost as [Total Cost] 
from #task

drop table #task

2.Триггер в таблице task, который поддерживает поле calculated_cost в stage.

create trigger tr_task 
on task 
after insert, update, delete
as
  -- Table to hold the updates
  declare @T table
  (
    id int not null, 
    cost decimal(18,2) not null default 0
  )

  -- Get the updates from inserted and deleted tables
  insert into @T (id, cost)
  select fk_stage, sum(cost)
  from (
          select fk_stage, cost
          from inserted
          union all
          select fk_stage, -cost
          from deleted
       ) as T   
  group by fk_stage

  declare @id int
  select @id = min(id)
  from @T

  -- For each updated row
  while @id is not null
  begin

    -- Recursive update of stage
    with cte as 
    (
      select s.id,
             s.fk_parent
      from stage as s
      where id = @id
      union all
      select s.id,
             s.fk_parent
      from stage as s
        inner join cte as c
          on s.id = c.fk_parent    
    )
    update s set
      calculated_cost = s.calculated_cost + t.cost 
    from stage as s
      inner join cte as c
        on s.id = c.id
      cross apply (select cost
                   from @T
                   where id = @id) as t   

    -- Get the next id
    select @id = min(id)
    from @T
    where id > @id
  end
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...