简体   繁体   English

SQL - 如何在树级数据结构中查找值

[英]SQL - How to find a value in a tree level data structure

I have two SQL Server tables: 我有两个SQL Server表:

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

invoice table stores all invoice records with a transaction folio. invoice表存储具有事务作品集的所有发票记录。

在此输入图像描述

invoice_relation table stores any relation between invoices. invoice_relation表存储发票之间的任何关系。

在此输入图像描述

This is an example of how invoices can be related between each other: 这是发票如何相互关联的示例:

在此输入图像描述

So the goal is to find the " folio " under invoice table given an invoicenumber and a folio but the folio sometimes won't be the folio that the invoice has, so I need to do a search on all the tree relation in order to find if any invoice match the invoice number but also the folio is part of the relation. 因此,目标是在invoice表下找到“ folio ”,给出一个invoicenumberfoliofolio有时不会是invoicefolio ,所以我需要搜索所有树关系才能找到如果任何发票与发票编号匹配,但是folio是该关系的一部分。

For example I have to find the folio and match invoice number of: 例如,我必须找到对开的发票编号:

  • Folio: 1003 对开:1003
  • Invoice Number: A1122 发票编号:A1122

In my query I would need to first find by folio because it's my invoice table primary key. 在我的查询中,我需要首先通过作品集查找,因为它是我的invoice表主键。 Then, will try to match A1122 with D1122 that won't match, so then I have to search all the tree structure to find if there is a A1122 . 然后,将尝试匹配A1122与不匹配的D1122 ,因此我必须搜索所有树结构以查找是否存在A1122 The result will be that the invoice A1122 was found in folio 1000 . 结果是在对开页1000中找到了发票A1122

Any clue on how to do this? 有关如何做到这一点的任何线索?

Here is a script of how to create the above example tables with data: 这是一个如何使用数据创建上述示例表的脚本:

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

I am still not sure what you really want, I had written something similar to JamieD77 which is to find top parent and then walk back down tree but then you get children and granchildren that are not directly related to A1122..... 我仍然不确定你真正想要什么,我写了类似JamieD77的东西,找到顶级父母,然后走回树下但是你得到的孩子和granchildren与A1122没有直接关系.....

Here is a way to walk up and down the tree and return all children and parents directly related to an invoicenumber 这是一种在树上走来走去的方式,让所有直接与发票人相关的孩子和父母归来

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

But Depending on what exactly you are looking for you may actually get a couple of cousins that are not desired..... 但根据你究竟在寻找什么,你可能会得到几个不想要的堂兄......

--------------Here was a method to find top parent and get the whole tree --------------这是一个找到顶级父母并获得整棵树的方法

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

Your model is broken to begin with. 你的模型一开始就被打破了。 parentinvoice should be in the invoice table. parentinvoice应该在发票表中。 It's a recursive database model....so make the table schema recursive. 它是一个递归数据库模型....所以使表模式递归。 Have a nullable foreign key in a column that references it's own table. 在引用它自己的表的列中有一个可以为空的外键。 Any time that field (the parent invoice field) is null indicates that it is the primary invoice. 该字段(父发票字段)为空时,表示它是主发票。 Any row having a parent is a piece of invoice. 任何有父母的行都是一张发票。

When you want to find a value in a tree level structure you wrap your initial sql query into a 'SELECT(.....)' statement (creating your own custom selectable table) that filters out what you want. 如果要在树级结构中查找值,可以将初始sql查询包装到“SELECT(.....)”语句中(创建自己的自定义可选表),过滤掉所需的内容。 Let me know if you have any questions! 如果您有任何疑问,请告诉我!

I was a little unclear as to your actual requirements, so I figured a Table Valued Function may be appropriate here. 我对你的实际要求有点不清楚,所以我认为表值函数可能适合这里。 I added a few optional items, and if unwanted, they are easy enough to remove (ie TITLE, Nesting, TopInvoice, TopFolio). 我添加了一些可选项,如果不需要,它们很容易删除(即TITLE,Nesting,TopInvoice,TopFolio)。 Also, you may notice the Range Keys (R1/R2). 此外,您可能会注意到范围键(R1 / R2)。 These serve many functions: Presentation Sequence, Selection Criteria, Parent/Leaf Indicators, and perhaps most importantly Non-Recursive Aggregation. 它们提供许多功能:表示顺序,选择标准,父/叶指示符,也许最重要的是非递归聚合。

To Return the Entire Hierarchy 返回整个层次结构

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

在此输入图像描述


To Return an Invoice and ALL of its descendants 退回发票及其所有后代

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

在此输入图像描述


To Return the PATH of a Folio 返回Folio的PATH

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

在此输入图像描述


To Return a Folio Limited to an Invoice 将Folio限制为发票

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

在此输入图像描述

The following code requires SQL 2012+ 以下代码需要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
)

Final Thoughts: 最后的想法:

This certainly may be more than what you were looking, and there is good chance that I COMPLETELY misunderstood your requirements. 这当然可能比你看到的更多,而且很有可能我完全误解了你的要求。 That said, being a TVF you can expand with additional WHERE and/or ORDER clauses or even incorporate into a CROSS APPLY. 也就是说,作为一个TVF,您可以使用其他WHERE和/或ORDER子句进行扩展,甚至可以合并到CROSS APPLY中。

This uses an approach of using a hierarchyid , first generating hierarchyid for every row, then selecting the row where folio is 1003, then finding all ancestors who have an invoicenumber of 'A1122'. 这使用了使用hierarchyid的方法,首先为每一行生成hierarchyid,然后选择folio为1003的行,然后查找具有'A1122'的invoicenumber的所有祖先。 It's not very efficient but may give you some different ideas: 这不是很有效但可能会给你一些不同的想法:

;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'

Edited for the case highlighted by @jj32 where you wish to find the root element in the hierarchy which folio 1003 is in, then find any descendant of that root which has an invoice number of 'A1122'. 编辑了由@jj32突出显示的案例,您希望在其中找到对开页1003所在的层次结构中的根元素,然后查找该根的任何后代,其发票号为“A1122”。 See below: 见下文:

;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'

this was tricky since you have a separate relation table and the root invoice is not in it. 这很棘手,因为你有一个单独的关系表,根本发票不在其中。

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

Here's an attempt that flattens out the relations first so you can travel in any direction. 这是一种尝试,首先使关系变得平坦,以便你可以向任何方向旅行。 Then it does the recursive CTE to work through the levels: 然后它执行递归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'

I agree with SwampDev's comment that the parentinvoice should really be in the invoice table. 我同意SwampDev的评论,即parentinvoice应该在发票表中。 This could also be done without a recursive CTE if you know the maximum number of levels of separation between invoices. 如果您知道发票之间的最大分隔级别,也可以在没有递归CTE的情况下完成此操作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM