【问题标题】:SQL - How to find a value in a tree level data structureSQL - 如何在树级数据结构中查找值
【发布时间】:2016-08-18 16:23:52
【问题描述】:

我有两个SQL Server 表:

  • 发票 (invoice)
  • 发票关系 (invoice_relation)

invoice 表存储所有带有交易对账单的发票记录。

invoice_relation 表存储发票之间的任何关系。

这是发票如何相互关联的示例:

所以目标是在给定invoicenumberfolioinvoice 表下找到“folio”,但folio 有时不会是invoice 所具有的folio ,所以我需要对所有树关系进行搜索,以查找是否有任何发票与发票编号匹配,而且 folio 是关系的一部分。

例如,我必须找到对开并匹配以下发票编号:

  • 对开页:1003
  • 发票编号:A1122

在我的查询中,我需要先按 folio 查找,因为它是我的 invoice 表主键。然后,将尝试将A1122 与不匹配的D1122 匹配,因此我必须搜索所有树结构以查找是否有A1122。结果将是发票 A1122 在文件夹 1000 中找到。

关于如何做到这一点的任何线索?

以下是如何使用数据创建上述示例表的脚本:

CREATE TABLE [dbo].[invoice](
    [folio] [int] NOT NULL,
    [invoicenumber] [nvarchar](20) NOT NULL,
    [isactive] [bit] NOT NULL,
 CONSTRAINT [PK_invoice] PRIMARY KEY CLUSTERED 
(
    [folio] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO


CREATE TABLE [dbo].[invoice_relation](
    [relationid] [int] NOT NULL,
    [invoice] [nvarchar](20) NOT NULL,
    [parentinvoice] [nvarchar](20) NOT NULL,
 CONSTRAINT [PK_invoice_relation_1] PRIMARY KEY CLUSTERED 
(
    [relationid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO
INSERT [dbo].[invoice] ([folio], [invoicenumber], [isactive]) VALUES (1000, N'A1122', 1)
GO
INSERT [dbo].[invoice] ([folio], [invoicenumber], [isactive]) VALUES (1001, N'B1122', 1)
GO
INSERT [dbo].[invoice] ([folio], [invoicenumber], [isactive]) VALUES (1002, N'C1122', 1)
GO
INSERT [dbo].[invoice] ([folio], [invoicenumber], [isactive]) VALUES (1003, N'D1122', 1)
GO
INSERT [dbo].[invoice] ([folio], [invoicenumber], [isactive]) VALUES (1004, N'F1122', 1)
GO
INSERT [dbo].[invoice] ([folio], [invoicenumber], [isactive]) VALUES (1005, N'G1122', 1)
GO
INSERT [dbo].[invoice_relation] ([relationid], [invoice], [parentinvoice]) VALUES (1, N'A1122', N'B1122')
GO
INSERT [dbo].[invoice_relation] ([relationid], [invoice], [parentinvoice]) VALUES (2, N'C1122', N'A1122')
GO
INSERT [dbo].[invoice_relation] ([relationid], [invoice], [parentinvoice]) VALUES (3, N'D1122', N'A1122')
GO
INSERT [dbo].[invoice_relation] ([relationid], [invoice], [parentinvoice]) VALUES (4, N'F1122', N'B1122')
GO
INSERT [dbo].[invoice_relation] ([relationid], [invoice], [parentinvoice]) VALUES (5, N'G1122', N'F1122')
GO

【问题讨论】:

  • 您可以将示例输出粘贴为文本
  • 我同意样本输出和输入会很好,因为我对您的叙述有点迷茫:“我必须找到对开页并匹配发票编号”哪个对开页最上面的父项?孙子????
  • 通常为了递归,还有一种方法可以识别最顶层的父母或最底层的孩子。我没有看到在您的表格示例中是否有一种方法可以识别它,或者递归是否必须假设起始值可能在某个地方的中间?
  • 马特可能在中间的某个地方。

标签: sql sql-server


【解决方案1】:

我仍然不确定你真正想要什么,我写了一些类似于 JamieD77 的东西,即找到顶级父母,然后从树上走下来,但是你得到的孩子和孙子与 A1122 没有直接关系...... .

这是一种在树上走来走去并返回与发票编号直接相关的所有孩子和父母的方法

DECLARE @InvoiceNumber NVARCHAR(20) = 'A1122'
DECLARE @Folio INT = 1003

;WITH cteFindParents AS (
    SELECT
       i.folio
       ,i.invoicenumber
       ,CAST(NULL AS NVARCHAR(20)) as ChildInvoiceNumber
       ,CAST(NULL AS NVARCHAR(20)) as ParentInvoiceNumber
       ,0 as Level
    FROM
       dbo.invoice i
    WHERE
       i.invoicenumber = @InvoiceNumber

    UNION ALL

    SELECT
       i.folio
       ,i.invoicenumber
       ,c.invoicenumber as ChildInvoiceNumber
       ,i.invoicenumber as ParentInvoiceNumber
       ,c.Level - 1 as Level
    FROM
       cteFindParents c
       INNER JOIN dbo.invoice_relation r
       ON c.invoicenumber = r.invoice
       INNER JOIN dbo.invoice i
       ON r.parentinvoice = i.invoicenumber
)

, cteFindChildren as (
    SELECT *
    FROM
       cteFindParents

    UNION ALL

    SELECT
       i.folio
       ,i.invoicenumber
       ,i.invoicenumber AS ChildInvoiceNumber
       ,c.invoicenumber AS ParentInvoiceNumber
       ,Level + 1 as Level
    FROM
       cteFindChildren c
       INNER JOIN dbo.invoice_relation r
       ON c.invoicenumber = r.parentinvoice
       INNER JOIN dbo.invoice i
       ON r.invoice = i.invoicenumber
    WHERE
       c.Level = 0
)

SELECT *
FROM
    cteFindChildren

但是根据您要寻找的具体内容,您实际上可能会得到几个不想要的表亲.....

-------------这是一种找到顶级父并获取整棵树的方法

DECLARE @InvoiceNumber NVARCHAR(20) = 'A1122'
DECLARE @Folio INT = 1003

;WITH cteFindParents AS (
    SELECT
       i.folio
       ,i.invoicenumber
       ,CAST(NULL AS NVARCHAR(20)) as ChildInvoiceNumber
       ,0 as Level
    FROM
       dbo.invoice i
    WHERE
       i.invoicenumber = @InvoiceNumber

    UNION ALL

    SELECT
       i.folio
       ,i.invoicenumber
       ,c.invoicenumber as ChildInvoiceNumber
       ,c.Level + 1 as Level
    FROM
       cteFindParents c
       INNER JOIN dbo.invoice_relation r
       ON c.invoicenumber = r.invoice
       INNER JOIN dbo.invoice i
       ON r.parentinvoice = i.invoicenumber

)

, cteGetTopParent AS  (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY 1 ORDER BY LEVEL DESC) as RowNum
    FROM
       cteFindParents
)

, cteGetWholeTree AS (
    SELECT
       p.folio
       ,p.invoicenumber
       ,p.invoicenumber as TopParent
       ,p.invoicenumber as Parent
       ,CAST(p.invoicenumber AS NVARCHAR(1000)) as Hierarchy
       ,0 as Level
    FROM
       cteGetTopParent p
    WHERE
       RowNum = 1

    UNION ALL

    SELECT
       i.folio
       ,i.invoicenumber
       ,c.TopParent
       ,c.invoicenumber AS Parent
       ,CAST(c.TopParent + '|' + (CASE WHEN Level > 0 THEN c.invoicenumber + '|' ELSE '' END) + i.invoicenumber  AS NVARCHAR(1000)) as Hierarchy
       ,Level + 1 as Level
    FROM
       cteGetWholeTree c
       INNER JOIN dbo.invoice_relation r
       ON c.invoicenumber = r.parentinvoice
       INNER JOIN dbo.invoice i
       ON r.invoice = i.invoicenumber
)

SELECT *
FROM
    cteGetWholeTree

【讨论】:

  • 好的,我在想一个更好的方法...在给定的作品集中,然后返回所有可能在树关系中的作品集。我猜这最后一个脚本做的对吗?然后我将在这些作品集中制作其他选择匹配的发票编号。
  • 所以是的,第二个脚本可以是叔叔或叔叔。走上去找到最高父母,然后走下来,直到找到发票号码。可能有一种方法可以在找到 invoicenumber 的情况下打破递归,但现实情况是,如果您同时拥有父母或孩子的作品集和 invoicenumber,则不需要递归或任何此类混乱,除非您只是想确认他们属于同一血统?
【解决方案2】:

你的模型一开始就坏了。 parentinvoice 应该在 invoice 表中。这是一个递归数据库模型......所以使表模式递归。在引用它自己的表的列中有一个可为空的外键。任何时候该字段(父发票字段)为空都表示它是主发票。任何有父级的行都是一张发票。

当您想在树级结构中查找值时,您可以将初始 sql 查询包装到“SELECT(.....)”语句(创建您自己的自定义可选择表)中,该语句会过滤掉您想要的内容。如果您有任何问题,请告诉我!

【讨论】:

  • 同意,在使用 1-n 关系之前,您不需要 *_relation 表。还有更多关于树表的问题 - 您可以使用物化路径或范围树而不是 ParentId 模式。甚至可以定义您自己的变体,更适合您的查询。
【解决方案3】:

我对您的实际要求有点不清楚,所以我认为表值函数在这里可能是合适的。我添加了一些可选项目,如果不需要,它们很容易删除(即 TITLE、Nesting、TopInvoice、TopFolio)。此外,您可能会注意到范围键 (R1/R2)。它们有许多功能:表示序列、选择标准、父/叶指示符,也许最重要的是非递归聚合。

返回整个层次结构

Select * from [dbo].[udf_SomeFunction](NULL,NULL)     


退回发票及其所有后代

Select * from [dbo].[udf_SomeFunction]('A1122',NULL) 


返回作品集的 PATH

Select * from [dbo].[udf_SomeFunction](NULL,'1003') 


将 Folio Limited 退回发票

Select * from [dbo].[udf_SomeFunction]('A1122','1003')

以下代码需要 SQL 2012+

CREATE FUNCTION [dbo].[udf_SomeFunction](@Invoice nvarchar(25),@Folio nvarchar(25))
Returns Table
As  
Return (
with cteBld as (
      Select Seq  = cast(1000+Row_Number() over (Order By Invoice) as nvarchar(500)),I.Invoice,I.ParentInvoice,Lvl=1,Title = I.Invoice,F.Folio
        From (
              Select Distinct 
                     Invoice=ParentInvoice
                    ,ParentInvoice=cast(NULL as nvarchar(20)) 
              From   [Invoice_Relation] 
              Where  @Invoice is NULL and ParentInvoice Not In (Select Invoice from [Invoice_Relation])
              Union  All
              Select Invoice
                    ,ParentInvoice 
              From   [Invoice_Relation] 
              Where  Invoice=@Invoice
             ) I
        Join Invoice F on I.Invoice=F.InvoiceNumber
      Union  All
      Select Seq  = cast(concat(A.Seq,'.',1000+Row_Number() over (Order by I.Invoice)) as nvarchar(500))
            ,I.Invoice
            ,I.ParentInvoice
            ,A.Lvl+1
            ,I.Invoice,F.folio
      From   [Invoice_Relation] I
      Join   cteBld  A on I.ParentInvoice = A.Invoice 
      Join   Invoice F on I.Invoice=F.InvoiceNumber )
     ,cteR1  as (Select Seq,Invoice,Folio,R1=Row_Number() over (Order By Seq) From cteBld)
     ,cteR2  as (Select A.Seq,A.Invoice,R2=Max(B.R1) From cteR1 A Join cteR1 B on (B.Seq like A.Seq+'%') Group By A.Seq,A.Invoice )

Select Top 100 Percent 
       B.R1
      ,C.R2
      ,A.Invoice
      ,A.ParentInvoice
      ,A.Lvl
      ,Title = Replicate('|-----',A.Lvl-1)+A.Title    -- Optional: Added for Readability
      ,A.Folio
      ,TopInvoice  = First_Value(A.Invoice) over (Order By R1) 
      ,TopFolio    = First_Value(A.Folio)   over (Order By R1) 
 From  cteBld A 
 Join  cteR1  B on A.Invoice=B.Invoice 
 Join  cteR2  C on A.Invoice=C.Invoice 
 Where (@Folio is NULL)
    or (@Folio is Not NULL and (Select R1 from cteR1 Where Folio=@Folio) between R1 and R2)
 Order By R1
)

最后的想法:

这当然可能超出您的预期,而且我很可能完全误解了您的要求。也就是说,作为 TVF,您可以使用额外的 WHERE 和/或 ORDER 子句进行扩展,甚至可以合并到 CROSS APPLY 中。

【讨论】:

  • 几个cmets:(1)Where子句中1=1的目的是什么? (2) 我不确定在最后一个示例中搜索 folio 时返回多行的目的,尽管我可能不理解其背后的逻辑 (3) 当我尝试使用来自最后一个示例的调用来搜索任何其他与 folio 1003 相关的发票我一无所获。不过它确实适用于 A1122。
  • @JJ32 令人印象深刻的眼睛。 1) 1=1 是一个占位符……我在想我可能需要其他选项。接得好。我会删除它。 2)这是为了适应更深和可变深度的层次结构。只是想把事情放在上下文中。 3) D1122 也可以。同样,我在考虑更深层次的层次结构。
【解决方案4】:

这使用了一种使用hierarchyid 的方法,首先为每一行生成hierarchyid,然后选择folio 为1003 的行,然后查找所有具有发票编号“A1122”的祖先。效率不是很高,但可能会给你一些不同的想法:

;WITH
Allfolios
AS
(
    Select i.folio, i.InvoiceNumber,
          hierarchyid::Parse('/' + 
               CAST(ROW_NUMBER() 
                        OVER (ORDER BY InvoiceNumber) AS VARCHAR(30)
                   ) + '/') AS hierarchy, 1 as level
    from invoice i
    WHERE NOT EXISTS 
        (SELECT * FROM invoice_relation ir WHERE ir.invoice = i. invoicenumber)
    UNION ALL
    SELECT i.folio, i.invoiceNumber,
          hierarchyid::Parse(CAST(a.hierarchy as VARCHAR(30)) + 
                             CAST(ROW_NUMBER() 
                                   OVER (ORDER BY a.InvoiceNumber) 
                               AS VARCHAR(30)) + '/') AS hierarchy, 
          level + 1
    FROM Allfolios A
    INNER JOIN invoice_relation ir
        on a.InvoiceNumber = ir.ParentInvoice
    INNER JOIN invoice i
        on ir.Invoice = i.invoicenumber
),
Ancestors
AS
(
    SELECT folio, invoiceNumber, hierarchy, hierarchy.GetAncestor(1) as AncestorId
    from Allfolios
    WHERE folio = 1003
    UNION ALL
    SELECT af.folio, af.invoiceNumber, af.hierarchy, af.hierarchy.GetAncestor(1)
      FROM Allfolios AF
      INNER JOIN 
            Ancestors a ON Af.hierarchy= a.AncestorId
)
SELECT *
FROM Ancestors
WHERE InvoiceNumber = 'A1122'

针对@jj32 突出显示的情况进行了编辑,您希望在其中查找对开 1003 所在的层次结构中的根元素,然后查找发票编号为“A1122”的该根的任何后代。见下文:

;WITH
Allfolios -- Convert all rows to a hierarchy
AS
(
    Select i.folio, i.InvoiceNumber,
          hierarchyid::Parse('/' + 
               CAST(ROW_NUMBER() 
                        OVER (ORDER BY InvoiceNumber) AS VARCHAR(30)
                   ) + '/') AS hierarchy, 1 as level
    from invoice i
    WHERE NOT EXISTS 
        (SELECT * FROM invoice_relation ir WHERE ir.invoice = i. invoicenumber)
    UNION ALL
    SELECT i.folio, i.invoiceNumber,
          hierarchyid::Parse(CAST(a.hierarchy as VARCHAR(30)) + 
                             CAST(ROW_NUMBER() 
                                   OVER (ORDER BY a.InvoiceNumber) 
                               AS VARCHAR(30)) + '/') AS hierarchy, 
          level + 1
    FROM Allfolios A
    INNER JOIN invoice_relation ir
        on a.InvoiceNumber = ir.ParentInvoice
    INNER JOIN invoice i
        on ir.Invoice = i.invoicenumber
),
Root -- Find Root
AS
(
    SELECT *
    FROM AllFolios AF
    WHERE Level = 1 AND 
    (SELECT hierarchy.IsDescendantOf(AF.hierarchy)  from AllFolios AF2 WHERE folio = 1003) = 1
)
-- Find all descendants of the root element which have an invoicenumber = 'A1122'
SELECT *
FROM ALLFolios
WHERE hierarchy.IsDescendantOf((SELECT TOP 1 hierarchy FROM Root)) = 1 AND
invoicenumber = 'A1122'

【讨论】:

  • 这会查找直接层次结构中的发票,但不会从其他分支返回相关记录,例如。 G1122,C1122。目前尚不清楚要求是什么,但问题下的最后一条评论暗示相关发票可能出现在树结构中的任何位置。不过,这种方法很有趣。
  • @JJ32 在这种情况下可以修改为使用 GetRoot 获取根级别,然后 GetDescendant 递归获取所有后代,然后过滤 InvoiceNumber
  • 你是对的,这不如其他方法有效。但是,如果 OP 按照设计使用它,将 hierarchyid 作为节点存储在表中,则检索值非常简单快捷:SELECT @hierarchy = hierarchyid::GetRoot() FROM invoice WHERE folio = @folio SELECT folio FROM invoice WHERE invoicenumber = @invoice and hierarchyid.IsDescendantOf(@hierarchy) = 1 好主意!
【解决方案5】:

这很棘手,因为您有一个单独的关系表并且根发票不在其中。

DECLARE @folio INT = 1003,
        @invoice NVARCHAR(20) = 'A1122'


-- find highest level of relationship
;WITH cte  AS (
    SELECT  i.folio,
            i.invoicenumber,
            ir.parentinvoice,
            0 AS [level]
    FROM    invoice i
            LEFT JOIN invoice_relation ir  ON ir.invoice = i.invoicenumber
    WHERE   i.folio = @folio
    UNION ALL 
    SELECT  i.folio,
            i.invoicenumber,
            ir.parentinvoice,
            [level] + 1
    FROM    invoice i
            JOIN invoice_relation ir  ON ir.invoice = i.invoicenumber
            JOIN cte r ON r.parentinvoice = i.invoicenumber
),
-- make sure you get the root folio
rootCte AS (
    SELECT  COALESCE(oa.folio, c.folio) AS rootFolio 
    FROM    (SELECT     *,
                        ROW_NUMBER() OVER (ORDER BY [level] DESC) Rn
                FROM    cte ) c
             OUTER APPLY (SELECT folio FROM invoice i WHERE i.invoicenumber = c.parentinvoice) oa
    WHERE    c.Rn = 1
),
-- get all children of root folio
fullTree AS (
    SELECT  i.folio,
            i.invoicenumber
    FROM    rootCte r
            JOIN invoice i ON r.rootFolio = i.folio
    UNION ALL 
    SELECT  i.folio,
            i.invoicenumber
    FROM    fullTree ft
            JOIN invoice_relation ir ON ir.parentinvoice = ft.invoicenumber
            JOIN invoice i ON ir.invoice = i.invoicenumber
)
-- search for invoice
SELECT  * 
FROM    fullTree
WHERE   invoicenumber = @invoice

【讨论】:

  • 让我为我的真实桌子做些调整,让你知道。看起来很有趣很!!
【解决方案6】:

这是一种首先拉平关系的尝试,以便您可以向任何方向移动。然后它执行递归 CTE 以通过各个级别:

WITH invoicerelation AS
(
select relationid, invoice, parentinvoice AS relatedinvoice
from invoice_relation
union
select relationid, parentinvoice AS invoice, invoice AS relatedinvoice 
from invoice_relation
),

cteLevels AS
(
select 0 AS relationid, invoice.folio, 
   invoicenumber AS invoice, invoicenumber AS relatedinvoice, 
   0 AS Level
from invoice 
UNION ALL
select invoicerelation.relationid, invoice.folio,
    invoicerelation.invoice, cteLevels.relatedinvoice, 
    Level + 1 AS Level
from invoice INNER JOIN
   invoicerelation ON invoice.invoicenumber = invoicerelation.invoice INNER JOIN
   cteLevels ON invoicerelation.relatedinvoice = cteLevels.invoice
      and (ctelevels.relationid <> invoicerelation.relationid)
)

SELECT cteLevels.folio, relatedinvoice, invoice.folio AS invoicefolio, cteLevels.level
from cteLevels INNER JOIN
    invoice ON cteLevels.relatedinvoice = invoice.invoicenumber
WHERE cteLevels.folio = 1003 AND cteLevels.relatedinvoice = 'a1122'

我同意 SwampDev 的评论,即 parentinvoice 确实应该在 invoice 表中。如果您知道发票之间的最大分隔级别数,也可以在没有递归 CTE 的情况下完成此操作。

【讨论】:

    猜你喜欢
    • 2021-07-29
    • 1970-01-01
    • 2013-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-15
    • 2022-11-16
    • 1970-01-01
    相关资源
    最近更新 更多