Как вычислить сумму значений в дереве с помощью SQL

Мне нужно суммировать очки на каждом уровне, заработанные деревом пользователей. Уровень 1 - это сумма баллов пользователей на 1 уровень ниже пользователя. Уровень 2 - это баллы уровня 1 пользователей на 2 уровня ниже пользователя и т. Д.

Расчет происходит один раз в месяц на непроизводственном сервере, не беспокойтесь о производительности.

Как бы выглядел SQL для этого?

Если вы запутались, не волнуйтесь, я тоже!

Таблица пользователей:

ID    ParentID    Points
1     0           230
2     1           150
3     0           80
4     1           110
5     4           54
6     4           342

Tree:
0
|---\
1    3
| \
2  4---
    \  \
     5  6

Результат должен быть:

ID    Points    Level1     Level2
1     230       150+110    150+110+54+342
2     150
3     80
4     110       54+342
5     54
6     342

Желательно синтаксис и функции SQL Server ...


person Jrgns    schedule 18.09.2008    source источник


Ответы (9)


Если бы вы использовали СУБД Oracle, это было бы довольно просто, поскольку Oracle поддерживает древовидные запросы с синтаксисом CONNECT BY / STARTS WITH. Думаю, что для SQL Server вам может пригодиться Общие табличные выражения.

person Manrico Corazzi    schedule 18.09.2008

Деревья плохо работают с SQL. Если у вас очень (очень очень) мало обращений на запись, вы можете изменить реализацию дерева, чтобы использовать вложенные наборы, что сделало бы этот запрос невероятно простым.

Пример (если не ошибаюсь):

SELECT SUM(points) 
FROM users 
where left > x and right < y 

Однако любые изменения в дереве требуют касания большого количества строк. Вероятно, лучше просто выполнить рекурсию в своем клиенте.

person Matthias Winkelmann    schedule 18.09.2008

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

(Или вы можете получить MS SQL Server 2008 и получить новые функции иерархии ...;))

person Grad van Horck    schedule 18.09.2008
comment
Я тоже думаю, но как будет выглядеть Процедура? - person Jrgns; 18.09.2008

SQL в целом, как говорили другие, плохо справляется с такими отношениями. Обычно требуется суррогатная таблица отношений (id, parent_id, уникальный ключ на (id, parent_id)), где:

  • каждый раз, когда вы добавляете запись в таблицу, вы:

    INSERT INTO relations (id, parent_id) VALUES ([current_id], [current_id]);

    INSERT INTO relations (id, parent_id) VALUES ([current_id], [current_parent_id]);

    INSERT INTO relations (id, parent_id) SELECT [current_id], parent_id FROM relations WHERE id = [current_parent_id];

  • иметь логику, чтобы избежать циклов

  • убедитесь, что обновления и удаления "отношений" обрабатываются хранимыми процедурами.

Учитывая эту таблицу, вы хотите:

SELECT rel.parent_id, SUM(tbl.points)
FROM table tbl INNER JOIN relations rel ON tbl.id=rel.id
WHERE rel.parent_id <> 0
GROUP BY rel.parent_id;
person tzot    schedule 18.09.2008

Хорошо, это дает вам результаты, которые вы ищете, но нет никаких гарантий, что я что-то пропустил. Считайте это отправной точкой. Я использовал для этого SQL 2005, SQL 2000 не поддерживает CTE.

WITH Parent (id, GrandParentId, parentId, Points, Level1Points, Level2Points)
AS
(
    -- Find root
    SELECT id,  
            0 AS GrandParentId,
            ParentId,
            Points,
            0 AS Level1Points,
            0 AS Level2Points
    FROM tblPoints ptr
    WHERE ptr.ParentId = 0

    UNION ALL (
    -- Level2 Points
    SELECT pa.GrandParentId AS Id,
            NULL AS GrandParentId,
            NULL AS ParentId,
            0 AS Points, 
            0 AS Level1Points,
            pa.Points  AS Level2Points
    FROM tblPoints pt
            JOIN Parent pa ON pa.GrandParentId = pt.Id 
    UNION  ALL
    -- Level1 Points
    SELECT pt.ParentId AS Id,
            NULL AS GrandParentId,
            NULL AS ParentId,
            0 AS Points, 
            pt.Points AS Level1Points,
            0 AS Level2Points
    FROM tblPoints pt
            JOIN Parent pa ON pa.Id = pt.ParentId AND pa.ParentId IS NOT NULL 
    UNION  ALL
    -- Points
    SELECT pt.id,
            pa.ParentId AS GrandParentId,
            pt.ParentId,
            pt.Points, 
            0 AS Level1Points,
            0 AS Level2Points
    FROM tblPoints pt
            JOIN Parent pa ON pa.Id = pt.ParentId AND pa.ParentId IS NOT NULL )
)
SELECT id, 
    SUM(Points) AS Points,  
    SUM(Level1Points) AS Level1Points,
    CASE WHEN SUM(Level2Points) > 0 THEN  SUM(Level1Points) + SUM(Level2Points) ELSE 0 END AS Level2Points
FROM Parent
GROUP BY id 
ORDER by id
person Darrel Miller    schedule 18.09.2008

Если вы работаете с деревьями, хранящимися в реляционной базе данных, я бы посоветовал взглянуть на «вложенный набор» или «модифицированный обход дерева предварительного заказа». SQL будет таким простым:

SELECT id, 
       SUM(value) AS value 
FROM table 
WHERE left>left\_value\_of\_your\_node 
  AND right<$right\_value\_of\_your\_node;

... и сделайте это для каждого интересующего вас узла.

Возможно, это поможет вам: http://www.dbazine.com/oracle/or-articles/tropashko4 или воспользуйтесь гуглом.

person Matthias Kestenholz    schedule 18.09.2008

У вас есть несколько вариантов:

  1. Используйте курсор и рекурсивный вызов пользовательской функции (это довольно медленно)
  2. Создайте таблицу кеша, обновите ее на INSERT с помощью триггера (это самое быстрое решение, но может быть проблематичным, если у вас много обновлений в основной таблице)
  3. Выполните рекурсивный расчет на стороне клиента (желательно, если у вас не слишком много записей)
person Ilya Kochetov    schedule 18.09.2008

Вы можете написать простую рекурсивную функцию для выполнения этой работы. Мой MSSQL немного заржавел, но будет выглядеть так:

CREATE FUNCTION CALC
(
@node integer,
)
returns 
(
@total integer
)
as
begin
    select @total = (select node_value from yourtable where node_id = @node);

    declare @children table (value integer);
    insert into @children   
    select calc(node_id) from yourtable where parent_id = @node;

    @current = @current + select sum(value) from @children;
    return
end
person Milan Babuškov    schedule 18.09.2008
comment
Хорошо, как будет выглядеть функция? - person Jrgns; 18.09.2008
comment
У меня здесь нет установки MSSQL, но она будет выглядеть примерно так: getsum (parentNode int) sum = select value, где node = parentNode; foreach строка в выбранных дочерних элементах из таблицы, где parent = parentNode sum = sum + getsum (childnode) Вы бы назвали его на верхнем узле. - person Milan Babuškov; 18.09.2008

Следующая таблица:

Id   ParentId
1   NULL
11    1
12    1
110 11
111 11
112 11
120 12
121 12
122 12
123 12
124 12

И следующая таблица сумм:

Id     Val
110 500
111 50
112 5
120 3000
121 30000
122 300000

Только идентификаторы листьев (последний уровень) имеют значение. SQL-запрос для получения данных выглядит так:

;WITH Data (Id, Val) AS
(
    select t.Id, SUM(v.val) as Val from dbo.TestTable t
    join dbo.Amount v on t.Id = v.Id
    group by t.Id
)

select cd.Id, ISNULL(SUM(cd.Val), 0) as Amount FROM
(
    -- level 3
    select t.Id, d.val from TestTable t
    left join Data d on d.id = t.Id

    UNION

    -- level 2
    select t.parentId as Id, sum(y.Val) from TestTable t
    left join Data y on y.id = t.Id
    where t.parentId is not null
    group by t.parentId

    UNION

    -- level 1
    select t.parentId as Id, sum(y.Val) from TestTable t
    join TestTable c on c.parentId = t.Id
    left join Data y on y.id = c.Id
    where t.parentId is not null
    group by t.parentId
) AS cd
group by id

это приводит к выводу:

Id     Amount
1     333555
11   555
12   333000
110 500
111 50
112 5
120 3000
121 30000
122 300000
123 0
124 0

Надеюсь, это поможет.

person Stef Heyenrath    schedule 13.12.2010