我有一个维度(星型模式)数据模型,支持劳动力数据的 BI。它包括一个员工维度表。该表是类型 2 缓慢变化的维度表,即将历史记录存储为单独的行。目前大约有 9k 行。
我需要在此员工维度表(在 SQL 视图中)上创建一个新列,其中将包含每个员工的“组织层次结构”(主管列表)的管道分隔列表。例如,如果员工 C 向经理 B 汇报,经理 B 在时间 t1 向经理 A 汇报,则此新列将包含值“A|B|C”。如果员工向经理 D 汇报,经理 D 在时间 t2 向经理 A 汇报,则此新列将包含值“A|D|C”。
我正在为这个新列使用递归 CTE。对于一些小容量(9 行)的模拟数据,这种递归 CTE 效果很好。然而,当我对实际数据(同样,只有大约 9k 行)运行此递归 CTE 时,30 分钟后它仍然没有返回结果集。鉴于数据量非常低,这令人惊讶。
如果重要的话,这是在 Azure SQL MI 上运行的。并且,表的主键 ([EmployeeHistoryKey]) 上有一个聚集索引。
在查看估计的查询执行计划时,我注意到大部分查询成本来自 [ManagerID] 列上的表扫描。因此,我在 [ManagerID] 列上放置了非聚集索引,但这并没有提高性能。那么,如何改进查询以提高性能呢?
下面是我的模拟数据和查询:
IF OBJECT_ID('SomeDB.some_schema.OrgHierarchyMockup', 'U') IS NOT NULL
DROP TABLE SomeDB.some_schema.OrgHierarchyMockup
;
CREATE TABLE
SomeDB.some_schema.OrgHierarchyMockup
(
EmployeeHistoryKey int
,EmployeeID char(1)
,ManagerID char(1)
,SomeAttribute char(1)
,RowEffectiveDate date
,RowExpirationDate date
)
;
INSERT INTO
SomeDB.some_schema.OrgHierarchyMockup
VALUES
(1, 'a', NULL, 'x', '2023-01-01', '2023-06-01')
,(2, 'a', NULL, 'y', '2023-06-02', NULL)
,(3, 'b', 'a', 'x', '2023-01-01', '2023-06-01')
,(4, 'b', 'a', 'y', '2023-06-02', NULL)
,(5, 'c', 'a', 'x', '2023-01-01', NULL)
,(6, 'd', 'b', 'x', '2023-01-01', NULL)
,(7, 'e', 'b', 'x', '2023-01-01', '2023-06-01')
,(8, 'e', 'c', 'x', '2023-06-02', NULL)
,(9, 'f', 'c', 'x', '2023-01-01', NULL)
;
WITH
traversed_hierarchy AS
(
SELECT --anchor member
EmployeeHistoryKey
,EmployeeID
,ManagerID
,RowEffectiveDate
,RowExpirationDate
,CAST(EmployeeID AS varchar(max)) AS OrgHierarchy --this is the org hierarchy
,EmployeeHistoryKey AS m_EmployeeHistoryKey --necessary to "de-dupe" the result set
FROM
SomeDB.some_schema.OrgHierarchyMockup
WHERE
ManagerID IS NULL
UNION ALL
SELECT --recursive member
s.EmployeeHistoryKey
,s.EmployeeID
,s.ManagerID
,s.RowEffectiveDate
,s.RowExpirationDate
,m.OrgHierarchy1 + '|' + s.EmployeeID
,m.EmployeeHistoryKey
FROM
SomeDB.some_schema.OrgHierarchyMockup AS s --direct subordinates
INNER JOIN --must be an inner join (not a left join) because we want the direct subordinates of the previously-fetched level in traversed_hierarchy
traversed_hierarchy AS m
ON
m.EmployeeID = s.ManagerID
)
,rownumbered AS
(
SELECT
EmployeeHistoryKey
,EmployeeID
,ManagerID
,RowEffectiveDate
,RowExpirationDate
,OrgHierarchy
,m_EmployeeHistoryKey
,ROW_NUMBER() OVER( --this will allow us to de-dupe the result set
PARTITION BY
EmployeeHistoryKey
ORDER BY
m_EmployeeHistoryKey
) AS RowNum
FROM
traversed_hierarchy
)
SELECT
EmployeeHistoryKey
,EmployeeID
,OrgHierarchy
,m_EmployeeHistoryKey
FROM
rownumbered
WHERE
RowNum = 1
ORDER BY
EmployeeID
,EmployeeHistoryKey
,m_EmployeeHistoryKey
;
使用临时表而不是 CTE,仅此一项就可以为您带来更好的性能。当您使用临时表创建查询计划时,SQL Server 可以使用统计信息并做出更好的决策。随着时间的推移,您的数据增长得越多,参与大表联接的嵌套 CTE 请求的内存授予就越大,如果服务器出现内存争用,则请求内存授予的 CTE 将挂起。在您与我们分享的场景中也不要使用表变量。
此外,使用临时表,您可以创建索引来支持涉及临时表的联接操作。对于 CTE,您无法做到这一点。