SQL Server 基于集合的 while 循环避免万圣节保护 (LAHP) 用于层次结构透视将某些结果减半
Posted
技术标签:
【中文标题】SQL Server 基于集合的 while 循环避免万圣节保护 (LAHP) 用于层次结构透视将某些结果减半【英文标题】:SQL Server set-based while loop avoiding halloween protection (LAHP) for hierarchy lookthrough halving certain results 【发布时间】:2015-09-09 01:13:05 【问题描述】:我一直在研究改进对金融投资组合的分层查看(目前使用 C# 任务执行)的方法。目的是通过相应地分配子投资组合的持股量来确定***母公司投资组合的最终持股量。
当前的数据集大约需要 30 分钟才能运行。这适用于大约 1000 个金融投资组合,每个投资组合最多可以链接大约 4 个投资组合(4 个深度,每个平均大约 40 个行项目)。
数据存储在 SQL Server 中,因此这是我的第一个途径,最初尝试了 CTE 方法。 CTE 方法将运行时间缩短到 7 分钟左右,这是一个巨大的改进。但是,当针对更大的数据集运行时,它会停止并且比 C# 任务花费的时间更长。
在阅读了以下文章后,我转向了基于集合的 while 循环 (LAHP) 方法: https://www.simple-talk.com/sql/performance/the-performance-of-traversing-a-sql-hierarchy-/
我不想详细介绍它是如何工作的,因为这个问题已经足够长了,但是它使用了 2 个临时表的组合,然后是一个 while 循环插入到交替表中,侧步万圣节保护。
当前的 LAHP 甚至比 CTE 更快,并且似乎更擅长处理更大的投资组合集,并且更深入地观察,但是一些结果(投资组合中工具(儿童/证券)的金融持有价值)出来的结果正好是应有的一半。
请看下面的查询,这是以存储过程格式使用的,然后将每个根投资组合作为一个变量。
DECLARE @root AS VARCHAR(50) = 'Portfolio1';
CREATE TABLE #T1
(
ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
lvl INT NOT NULL,
PortfolioCode varchar(MAX) NULL,
InstrumentCode varchar(MAX) NULL,
LinkedID varchar(MAX) NULL,
TradedMarketValue NUMERIC(28, 8) NOT NULL
);
CREATE NONCLUSTERED INDEX IDX_Pcode ON #T1(ID)
INCLUDE (lvl)
CREATE TABLE #T2
(
ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
lvl INT NOT NULL,
PortfolioCode varchar(MAX) NULL,
InstrumentCode varchar(MAX) NULL,
LinkedID varchar(MAX) NULL,
TradedMarketValue NUMERIC(28, 8) NOT NULL
);
CREATE NONCLUSTERED INDEX IDX_Pcode ON #T2(ID)
INCLUDE (lvl)
DECLARE @lvl AS INT = 0;
-- insert root node
INSERT INTO #T1
(
lvl,
LinkedID,
PortfolioCode,
InstrumentCode,
TradedMarketValue
)
SELECT
@lvl,
I.LinkedID,
P.PortfolioCode,
I.Code,
P.TradedMarketValue
FROM PortfolioHolding P
join Instrument I on I.Code = P.InstrumentCode
WHERE P.PortfolioCode = @root
WHILE @@ROWCOUNT > 0
BEGIN
SET @lvl += 1;
-- insert children of nodes in prev level
IF @lvl % 2 = 1
INSERT INTO #T2
(
lvl,
LinkedID,
PortfolioCode,
InstrumentCode,
TradedMarketValue
)
SELECT
@lvl,
I.LinkedID,
P.PortfolioCode,
I.Code,
--calculate ratio of holding and apply to holding
cast(cast(cast(ISNULL(T.TradedMarketValue, 0) as float) /
NULLIF(sum(cast(ISNULL(P.TradedMarketValue, 0) as float)) over (partition by P.PortfolioCode) , 0) as float)
* ISNULL(P.TradedMarketValue, 0) as float)
FROM #T1 AS T
INNER JOIN PortfolioHolding AS P
ON T.lvl = @lvl - 1
AND P.PortfolioCode = T.LinkedID
JOIN Instrument I on I.Code = P.InstrumentCode;
ELSE
INSERT INTO #T1
(
lvl,
LinkedID,
PortfolioCode,
InstrumentCode,
TradedMarketValue
)
SELECT
@lvl,
I.LinkedID,
P.PortfolioCode,
I.Code,
--calculate ratio of holding and apply to holding
(cast(cast(cast(ISNULL(T.TradedMarketValue, 0) as float) /
NULLIF(sum(cast(ISNULL(P.TradedMarketValue, 0) as float)) over (partition by P.PortfolioCode) , 0) as float)
* ISNULL(P.TradedMarketValue, 0) as float))
FROM #T2 AS T
INNER JOIN PortfolioHolding AS P
ON T.lvl = @lvl - 1
AND P.PortfolioCode = T.LinkedID
JOIN Instrument I on I.Code = P.InstrumentCode;
END
SELECT
@root, InstrumentCode ,sum(tradedmarketvalue) As TradedMarketValue
FROM
(SELECT * FROM #T1 UNION ALL SELECT * FROM #T2) AS U
WHERE LinkedID is null
group by InstrumentCode
DROP TABLE #T1, #T2;
从查询中可以看出,PortfolioCode (ParentID
) 包含 InstrumentCodes (ChildId
),LinkedId
将 InstrumentCode
与其相关的 PortfolioCode
匹配。
将TradedMarketValue
与LinkedID
与null
相加得到所有叶节点。
如前所述,一些 TradedMarketValues 的收入仅为应有的一半。非常感谢您的帮助。
【问题讨论】:
这是一个非常好的第一篇文章,人们可能会发现唯一有用的是一组简化的源数据以及所需的输出,以便他们可以运行您的代码并尝试产生所需的输出.这样他们就可以看到它的作用并尝试对其进行优化。 有点困惑。你想让这个更快吗?还是说 TradedMarketValue 不正确?鉴于这非常复杂,如果您提供示例数据和预期输出将会很有帮助。 您好,感谢您的提示,我将提供一组示例数据。问题是它提供了不正确的结果。但只是一部分。大约 70% 的结果是正确的,另外 30% 的结果(对于 TradedMarketValue)正好是一半。 @Thematkinson 如果您可以提供一些示例行来展示理想的输出好坏 @Tanner,我正忙于混淆一些数据,因为它的机密客户持有数据。 【参考方案1】:好吧好吧……在花了 2 个小时创建示例数据以发布我的问题并运行它之后,答案就出现了!
请看下面的示例数据:
仪器表:
InstrumentCode LinkedId
Inst1 NULL
Inst2 Port2
Inst3 NULL
Inst4 Port4
Inst5 Port5
Inst6 NULL
Inst7 NULL
Inst8 NULL
Inst9 NULL
Inst10 NULL
Inst11 NULL
投资组合表:
PortfolioCode InstrumentCode TradedMarketValue
Port1 Inst1 150.00
Port1 Inst2 60.00
Port1 Inst3 45.00
Port2 Inst1 75.00
Port2 Inst4 95.00
Port2 Inst5 100.00
Port3 Inst2 110.00
Port3 Inst6 95.00
Port4 Inst8 145.00
Port4 Inst9 100.00
Port4 Inst7 125.00
Port5 Inst8 150.00
Port5 Inst11 175.00
Port5 Inst10 120.00
举个例子,如果我们想要分解投资组合 1 Port1
,这将最终形成 3 个投资组合深度,首先是 Port2
,然后是第三层 Port4
和 Port5
见图表:
整个系统工作正常,查询提供正确的结果。
当一个投资组合中的两种不同工具指向相同的LinkedId
或“投资组合代码”时,就会出现问题。
见图表:
然后发生的是这部分代码:
--calculate ratio of holding and apply to holding
cast(cast(cast(ISNULL(T.TradedMarketValue, 0) as float) /
NULLIF(sum(cast(ISNULL(P.TradedMarketValue, 0) as float)) over (partition by P.PortfolioCode) , 0) as float)
* ISNULL(P.TradedMarketValue, 0) as float)
计算父投资组合的 TradedMarketValue (TMV) 与子投资组合总价值的比率是两次求和,导致分母加倍。
要解决此问题,可以将上述代码部分更改为
--calculate ratio of holding and apply to holding
cast(cast(cast(ISNULL(T.TradedMarketValue, 0) as float) /
NULLIF(sum(cast(ISNULL(P.TradedMarketValue, 0) as float)) over (partition by T.InstrumentCode, P.PortfolioCode) , 0) as float)
* ISNULL(P.TradedMarketValue, 0) as float)
添加一个额外的变量以按(分组)T.InstrumentCode
进行分区,然后停止上述发生。
我希望这对以后的人有所帮助。
【讨论】:
以上是关于SQL Server 基于集合的 while 循环避免万圣节保护 (LAHP) 用于层次结构透视将某些结果减半的主要内容,如果未能解决你的问题,请参考以下文章
如何在 SQL Server 的 While 循环中设置变量