SQL 查询显示树结构中的子级总数

标签 sql sql-server t-sql common-table-expression

我正在尝试计算树结构的父节点的总计,但由于某种原因,计算父节点的总计值让我无法计算。

假设我有三张表

地区

GroupID     ParentID      Name
1           null          NorthAmerica
2           null          Asia
3           null          Europe
4           1             NorthEast
5           1             WestCoast
6           3             UK
7           3             Germany
8           2             Hong Kong
9           2             Japan

区域成员

GroupID     EmpID   
4           10000   
4           10001   
5           10011   
6           20455   
6           10003   
7           34567   
9           43589   
9           54890   
8           84320   
8           84560   

员工销售

EmployeeID     Name     Sales ($)
10000          Joe      $ 150,000.00 
10001          Mary     $ 200,000.00 
10011          John     $ 175,000.00 
20455          Fred     $ 100,000.00 
10003          Bill     $ 250,000.00 
34567          Abe      $ 142,000.00 
43589          Jack     $ 260,000.00 
54890          Amanda   $ 300,000.00 
84320          Jane     $ 15,000.00 
84560          Oscar    $ 175,000.00 

目标是查询树中的不同级别并查看这些区域的总数。
例如,一个 View 将显示销售总额最高的地区:

NorthAmerica    525,000.00 *(The sum of NorthEast and WestCoast)*
Asia            750,000.00 *(The sum of Hong Kong and Japan)*
Europe          492,000.00 *(The sum of UK and Germany)*

另一个 View 将显示区域总数(针对单亲):

NorthAmerica    525,000.00 *(Total of the region members NorthEast and WestCoast)*
Northeast       350,000.00 *(Total of NorthEast Leaves Joe and Mary)*
WestCoast       175,000.00 *(Total of WestCoast Leaves John)*

当然,这些树的分支可以更深,但我认为这个例子说明了我正在解决的问题。

到目前为止,使用 CTE,我可以相当轻松地浏览树结构,并且可以获得最终分支(或分支的叶子)的总计,但我似乎无法获得汇总的总计。

所以从上面的例子中,我可以得到以下输出:

NorthAmerica    NULL
NorthEast       350,000.00
WestCoast       175,000.00

我会提供现有的代码,但实际的表和连接数量与我的实际表有很大不同,并且可能会混淆总体目标。然而,这个问题与我想要完成的任务类似,但它似乎并不完全适合填充:

CTE Sum of Child Levels

非常感谢任何帮助。

构建脚本如下:

create table Regions 
(
GroupID int, 
ParentID int,
Name Varchar(40)
)

create table RegionMember
(
GroupID int,
empid int
)

Create Table EmployeeSales
(
EmployeeID int,
Name Varchar(50),
Sales float,
)

Insert into Regions Values
(1, null, 'NorthAmerica'),
(2, null, 'Asia'),
(3, null, 'Europe'),
(4, 1, 'NorthEast'),
(5, 1, 'WestCoast'),
(6, 3, 'UK'),
(7, 3, 'Germany'),
(8, 2, 'Hong Kong'),
(9, 2, 'Japan');

Insert into RegionMember Values
(4, 10000),   
(4, 10001),   
(5, 10011),  
(6, 20455),   
(6, 10003),
(7, 34567),   
(9, 43589),   
(9, 54890),   
(8, 84320),   
(8, 84560);

Insert into EmployeeSales Values
(10000, 'Joe', 150000), 
(10001, 'Mary', 200000), 
(10011, 'John', 175000), 
(20455, 'Fred', 100000),
(10003, 'Bill', 250000),
(34567, 'Abe', 142000), 
(43589, 'Jack', 260000), 
(54890, 'Amanda', 300000), 
(84320, 'Jane', 15000), 
(84560, 'Oscar', 175000); 

还使用上面的内容启动了 SQL Fiddle:http://sqlfiddle.com/#!6/4ee0c/1

最佳答案

我在示例数据中添加了几行,因为原始数据太简单了。这分为三个级别。

Insert into Regions Values
(10, null, 'A1'),
(40, 10, 'B1'),
(50, 10, 'B2'),
(60, 10, 'B3'),
(70, 40, 'C1'),
(80, 40, 'C2');

Insert into RegionMember Values
(40, 104),
(50, 105),
(60, 106),
(70, 107),
(80, 108);

Insert into EmployeeSales Values
(104, '104', 104),
(105, '105', 105),
(106, '106', 106),
(107, '107', 107),
(108, '108', 108);

热门地区

此查询是直接递归 CTE,它从最高级别 (WHERE ParentID IS NULL) 开始,并对所有子项求和。这里的“技巧”是在我们遍历树时包含该组的原始 StartIDStartName,这样我们就可以对它们进行 GROUP BY结束。

WITH
CTE
AS
(
    SELECT
        Regions.GroupID AS StartID
        ,Regions.Name AS StartName
        ,Regions.GroupID
        ,Regions.ParentID
        ,Regions.Name
        ,1 AS Lvl
    FROM Regions
    WHERE ParentID IS NULL

    UNION ALL

    SELECT
        CTE.StartID
        ,CTE.StartName
        ,Regions.GroupID
        ,Regions.ParentID
        ,Regions.Name
        ,CTE.Lvl + 1 AS Lvl
    FROM
        Regions
        INNER JOIN CTE ON CTE.GroupID = Regions.ParentID
)
SELECT
    CTE.StartID
    ,CTE.StartName
    ,SUM(EmployeeSales.Sales) AS SumSales
FROM
    CTE
    INNER JOIN RegionMember ON RegionMember.GroupID = CTE.GroupID
    INNER JOIN EmployeeSales ON EmployeeSales.EmployeeID = RegionMember.empid
GROUP BY
    CTE.StartID
    ,CTE.StartName
ORDER BY
    CTE.StartID;

逐步运行查询以了解其工作原理。

结果

+---------+--------------+----------+
| StartID |  StartName   | SumSales |
+---------+--------------+----------+
|       1 | NorthAmerica |   525000 |
|       2 | Asia         |   750000 |
|       3 | Europe       |   492000 |
|      10 | A1           |      530 |
+---------+--------------+----------+

地区总计和小计

第二个查询并不那么容易。第一部分 CTE_Groups 与前面的查询非常相似,但具有针对特定起始 GroupID 的过滤器。 CTE_Sums 计算起始组及其每个子组的销售汇总。 CTE_Totals 再次递归地遍历 CTE_Sums 的结果,并根据需要重复子行,以获得每个组的总计(包括子项汇总)。

再次,逐个 CTE 逐步运行查询以了解其工作原理。 并非所有列都会在最终结果中使用,但它们有助于了解中间步骤中发生的情况。

WITH
CTE_Groups
AS
(
    SELECT
        Regions.GroupID AS StartID
        ,Regions.Name AS StartName
        ,Regions.GroupID
        ,Regions.ParentID
        ,Regions.Name
        ,1 AS Lvl
    FROM Regions
    WHERE Regions.GroupID = 1 -- North America
    --WHERE Regions.GroupID = 10

    UNION ALL

    SELECT
        CTE_Groups.StartID
        ,CTE_Groups.StartName
        ,Regions.GroupID
        ,Regions.ParentID
        ,Regions.Name
        ,CTE_Groups.Lvl + 1 AS Lvl
    FROM
        Regions
        INNER JOIN CTE_Groups ON CTE_Groups.GroupID = Regions.ParentID
)
,CTE_Sums
AS
(
    SELECT
        CTE_Groups.GroupID
        ,CTE_Groups.ParentID
        ,CTE_Groups.Name
        ,SUM(EmployeeSales.Sales) AS SumSales
    FROM
        CTE_Groups
        LEFT JOIN RegionMember ON RegionMember.GroupID = CTE_Groups.GroupID
        LEFT JOIN EmployeeSales ON EmployeeSales.EmployeeID = RegionMember.empid
    GROUP BY
        CTE_Groups.GroupID
        ,CTE_Groups.ParentID
        ,CTE_Groups.Name
)
,CTE_Totals
AS
(
    SELECT
        CTE_Sums.GroupID AS StartID
        ,CTE_Sums.Name AS StartName
        ,CTE_Sums.GroupID
        ,CTE_Sums.ParentID
        ,CTE_Sums.Name
        ,CTE_Sums.SumSales
        ,1 AS Lvl
    FROM CTE_Sums

    UNION ALL

    SELECT
        CTE_Totals.StartID
        ,CTE_Totals.StartName
        ,CTE_Sums.GroupID
        ,CTE_Sums.ParentID
        ,CTE_Sums.Name
        ,CTE_Totals.SumSales
        ,CTE_Totals.Lvl + 1 AS Lvl
    FROM
        CTE_Sums
        INNER JOIN CTE_Totals ON CTE_Totals.ParentID = CTE_Sums.GroupID
)
SELECT
    GroupID
    ,Name
    ,SUM(SumSales) AS SumTotal
FROM CTE_Totals
GROUP BY
    GroupID
    ,Name
ORDER BY
    GroupID
    ,Name
;

GroupID = 1 的结果

+---------+--------------+----------+
| GroupID |     Name     | SumTotal |
+---------+--------------+----------+
|       1 | NorthAmerica |   525000 |
|       4 | NorthEast    |   350000 |
|       5 | WestCoast    |   175000 |
+---------+--------------+----------+

GroupID = 10 的结果

+---------+------+----------+
| GroupID | Name | SumTotal |
+---------+------+----------+
|      10 | A1   |      530 |
|      40 | B1   |      319 |
|      50 | B2   |      105 |
|      60 | B3   |      106 |
|      70 | C1   |      107 |
|      80 | C2   |      108 |
+---------+------+----------+

关于SQL 查询显示树结构中的子级总数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37395409/

相关文章:

mysql - 整理两个表中的数据

java - Hibernate:Criteria 和 createSQLQuery:如何获取正确的 JSON?

sql-server - 需要更快地连接 sql server 表中的列

t-sql - ExecuteSqlRaw() 中 'GO' 附近的语法不正确

sql - 将字符串转换为具有非标准值的日期时间格式

sql - SQL中根据 "First Name"+ "Last Name"过滤行

sql - 如何排除或否定两个查询?

SQL 选择主记录并显示每个主记录的详细记录数

sql - 根据字符范围选择字符串字段的最佳方法是什么?

sql - 存储过程输出参数上的合并/IsNull