Problem With Recursive CTE

  • Yikes! ...the "bill of materials" comment should have tipped me off. Good catch Jeff!

    With this set of test data it is much clearer what is required.

    There are no special teachers of virtue, because virtue is taught by the whole community.
    --Plato

  • David-155102 (5/30/2011)


    Hi Jeff,

    I see your point; if I change the test data to the following then "Part G" is not returned when it should be :crazy::

    Ok... and I'm not being sarcastic here... I'm just confused :blink:. What is it that you actually want? Are you suggesting that you want only leaf-level information no matter which part you ask for? 🙂

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Jeff Moden (5/30/2011)


    Are you suggesting that you want only leaf-level information no matter which part you ask for? 🙂

    That's what I got out of it this time around but I reserve the right to have misinterpreted the req...again 😀

    There are no special teachers of virtue, because virtue is taught by the whole community.
    --Plato

  • opc.three (5/30/2011)


    Jeff Moden (5/30/2011)


    Are you suggesting that you want only leaf-level information no matter which part you ask for? 🙂

    That's what I got out of it this time around but I reserve the right to have misinterpreted the req...again 😀

    Heh... understood. I need to find out "Why" only leaf-level stuff. It's not a typical request of a BOM although I believe I know why. I'll let David come back with an answer on that one.

    I have solved the single-most major problem with this hierarchical problem... it has no real "top level" and that's been making life a whole lot more difficult in solving this problem. I'm also just about done solving the "leaf-level-only" problem, as well although I still want to know "Why" it's necessary.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Yes to the question about only returning leaf level products, I'll try and explain. "Product A" consists of three components (Part a1, Part a2 and Part a3). Most customers will purchase "Product A" but some customers will want to purchase the exact same product but want to give it a name that fits in with their internal systems e.g "Kraft A". So we now have "Product A" and "Kraft A" which have the same components. When these products are sold the product name ("Product A" or "Kraft A") is recorded, therefore from a stocking/replenishment point of view it is necessary to understand which components have been sold. I hope this makes some sort of sense because I've really shortened the actual process here 🙂

  • It actually doesn't make sense to me: why don't you just store the customer specific product name together with your internal product name in a separate table? Then you wouldn't have to worry about different names for the same product...



    Lutz
    A pessimist is an optimist with experience.

    How to get fast answers to your question[/url]
    How to post performance related questions[/url]
    Links for Tally Table [/url] , Cross Tabs [/url] and Dynamic Cross Tabs [/url], Delimited Split Function[/url]

  • The example I gave was really simplified but another example is where a product, which is made of many components, is itself a component of another product. Again, this is a simplified example.

  • Hi, please try this function.

    CREATE FUNCTION BomExplode (@product NCHAR(20))

    RETURNS TABLE

    WITH schemabinding

    AS

    RETURN (

    WITH cte (product,component)

    AS

    (

    SELECT product,component

    FROM dbo.ProductAssembly

    WHERE product=ISNULL(@product,product)

    UNION ALL

    SELECT cte.product,PA.component

    FROM dbo.ProductAssembly AS PA

    JOIN cte

    ON cte.component = PA.product

    )

    SELECT cte.product

    , cte.component

    FROM

    cte

    WHERE

    NOT EXISTS (SELECT 1

    FROM

    dbo.ProductAssembly AS PA

    WHERE

    PA.product = cte.component)

    )

    Invocation Examples:

    SELECT be.product

    , be.component

    FROM

    dbo.BomExplode(NULL) AS be

    WHERE

    be.product = 'Part D'

    OPTION (MAXRECURSION 10)

    Or....

    SELECT be.product

    , be.component

    FROM

    dbo.BomExplode('Part D') AS be

    OPTION (MAXRECURSION 10)

    MM



    select geometry::STGeomFromWKB(0x0106000000020000000103000000010000000B0000001000000000000840000000000000003DD8CCCCCCCCCC0840000000000000003DD8CCCCCCCCCC08408014AE47E17AFC3F040000000000104000CDCCCCCCCCEC3F9C999999999913408014AE47E17AFC3F9C99999999991340000000000000003D0000000000001440000000000000003D000000000000144000000000000000400400000000001040000000000000F03F100000000000084000000000000000401000000000000840000000000000003D0103000000010000000B000000000000000000143D000000000000003D009E99999999B93F000000000000003D009E99999999B93F8014AE47E17AFC3F400000000000F03F00CDCCCCCCCCEC3FA06666666666FE3F8014AE47E17AFC3FA06666666666FE3F000000000000003D1800000000000040000000000000003D18000000000000400000000000000040400000000000F03F000000000000F03F000000000000143D0000000000000040000000000000143D000000000000003D, 0);

  • Forum Etiquette: How to post Reporting Services problems
  • [/url]
  • Forum Etiquette: How to post data/code on a forum to get the best help - by Jeff Moden
  • [/url]
  • How to Post Performance Problems - by Gail Shaw
  • [/url]

  • Magoo's code seems to do the trick for leaf-node detection. I decided to go "Full Monty" so that other "questions" could be asked of the hierarchy.

    First, here's the test data... notice the bit of a change I made (at the very bottom of the next code window) to show some extra utility in the code that follows it.

    --===== Always do experiments in a nice, safe place that everyone has just to be sure.

    USE TempDB

    ;

    --===== Conditionally drop the test table to make reruns in SSMS easier.

    -- This is NOT a part of the solution.

    --===== Conditionally drop temp tables to make reruns easier in SSMS

    IF OBJECT_ID('TempDB..#ProductAssembly','U') IS NOT NULL DROP TABLE #ProductAssembly;

    --===== Create and populate the test table

    CREATE TABLE #ProductAssembly

    (

    Product NVARCHAR(20),

    Component NVARCHAR(20)

    )

    ;

    INSERT INTO dbo.ProductAssembly

    (Product, Component)

    SELECT 'Part A','Part Z' UNION ALL

    SELECT 'Part B','Part Z' UNION ALL

    SELECT 'Part C','Part Z' UNION ALL

    SELECT 'Part D','Part Z' UNION ALL

    SELECT 'Part Z','Part Y' UNION ALL

    SELECT 'Part D','Part X' UNION ALL

    SELECT 'Part D','Part W' UNION ALL

    SELECT 'Part Z','Part G'

    UNION ALL SELECT 'Part A', 'Part D' --<< Added this little nuance

    You'll need this "very odd" splitter function which doesn't actually split anything. It breaks each instance of a hierarchical path into all of the paths it took to get to that given line item so we can figure out how many nodes are in each subpath and THAT's critical to the Set-Based method I use. As complicated as it looks, it's Nasty Fast...

    CREATE FUNCTION dbo.SplitCommonPath

    -- Jeff Moden - 31 May 2011

    --===== Define I/O parameters

    (@pString NVARCHAR(4000), @pDelimiter NCHAR(1))

    RETURNS TABLE WITH SCHEMABINDING AS

    RETURN

    --===== "Inline" CTE Driven "Tally Table" produces values from 0 up to 10,000...

    -- enough to cover NVARCHAR(4000)

    WITH E1(N) AS (

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1

    ), --10E+1 or 10 rows

    E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows

    E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max

    cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front

    -- for both a performance gain and prevention of accidental "overruns"

    SELECT TOP (ISNULL(DATALENGTH(@pString)/2,0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4

    ),

    cteStart(N) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)

    SELECT t.N FROM cteTally t WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter

    )

    --===== Do the actual split.

    -- CommonPath always contains 1 less node

    SELECT CommonPath = SUBSTRING(@pString, 1 ,s.N)

    FROM cteStart s

    ;

    Now, we go on to converting the Adjacency List to a Hierarchical Path. This code also builds the missing "Top Level" node and the related top level parts that "report" to it.

    --===== Declare and populate an obviously named variable for what to finpe.

    DECLARE @SearchFor NVARCHAR(20);

    SELECT @SearchFor = 'ALL'; -- or can be a single part

    --===== Conditionally drop temp tables to make reruns easier in SSMS

    IF OBJECT_ID('TempDB..#HierarchyWork' ,'U') IS NOT NULL DROP TABLE #HierarchyWork;

    IF OBJECT_ID('TempDB..#FinalHierarchy','U') IS NOT NULL DROP TABLE #FinalHierarchy;

    --===== Build a working table to include the missing "top level" so we can convert the

    -- Adjacency List to a Hierarchical Path

    ------- Copy all existing data into a new table on the fly

    SELECT Product, Component

    INTO #HierarchyWork

    FROM #ProductAssembly

    UNION ALL

    ------- Create the missing top level and copy it, as well.

    SELECT DISTINCT

    'Top Level', Product

    FROM #ProductAssembly

    WHERE Product NOT IN (SELECT Component FROM #ProductAssembly)

    UNION ALL

    SELECT ' ', 'Top Level'

    ;

    --===== Create the Hierarchical Path model and some

    -- extra columns to create a Nested Sets model.

    WITH

    ctePartsExplosion AS

    (

    SELECT Component, Product, RelativeLevel = 1,

    HierarchicalPath = CAST(Component+ N'/' AS NVARCHAR(4000))

    FROM #HierarchyWork

    WHERE (@SearchFor <> 'ALL' AND Component = @SearchFor)

    OR (@SearchFor = 'ALL' AND Product = ' ')

    UNION ALL

    SELECT h.Component, h.Product, RelativeLevel = pe.RelativeLevel + 1,

    HierarchicalPath = CAST(pe.HierarchicalPath + h.Component + N'/' AS NVARCHAR(4000))

    FROM #HierarchyWork h

    INNER JOIN ctePartsExplosion pe ON h.Product = pe.Component

    ) --=== We put it all into a Temp Table because we'll need to address the

    -- Final Hierarchy table more than once which would cause a CTE to

    -- execute more than once making the code twice as slow.

    SELECT NodeNumber = ISNULL(ROW_NUMBER() OVER (ORDER BY HierarchicalPath),0),

    LeftBower = CAST(NULL AS INT),

    RightBower = CAST(NULL AS INT),

    NodeCount = CAST(1 AS INT),

    Component,

    Product = ISNULL(Product,''),

    RelativeLevel = CAST(RelativeLevel AS TINYINT),

    IsLeafLevel = CAST(NULL AS TINYINT),

    HierarchicalPath

    INTO #FinalHierarchy

    FROM ctePartsExplosion

    ;

    Then we convert the Hierarchical Path to Nested Sets...

    --===== Declare a variable to remember the Left Bower

    -- of a line item in a Nested Set to calculate the

    -- Right Bower from.

    DECLARE @LeftBower INT

    ;

    --===== Convert the Hierarchical Path to a Nested Sets

    WITH

    cteCountNodes AS

    ( --=== Count the number of "downline" nodes for each line item

    -- based on the number of occurances for each "common path".

    SELECT split.CommonPath,

    NodeCount = COUNT(*)

    FROM #FinalHierarchy fh

    CROSS APPLY dbo.SplitCommonPath(fh.HierarchicalPath, N'/') split

    GROUP BY split.CommonPath

    ) --=== This bit of computational heaven creates the "Bowers" for the

    -- Nested Sets and determines if a line item is at the Leaf Level.

    UPDATE fh

    SET NodeCount = cn.NodeCount,

    @LeftBower = LeftBower = (fh.NodeNumber * 2) - fh.RelativeLevel,

    RightBower = ((cn.NodeCount-1) * 2) + @LeftBower + 1,

    IsLeafLevel = CASE WHEN cn.NodeCount > 1 THEN 0 ELSE 1 END

    FROM #FinalHierarchy fh

    INNER JOIN cteCountNodes cn

    ON fh.HierarchicalPath = cn.CommonPath

    ;

    Here's what the final hierarchy ends up looking like...

    Do an Internet search for "Nested Sets" to find out more about how to use them and how very fast and flexible they are. There's a whole lot more you can do with them other than just calculating leaf-levels.

    This, obviously, doesn't take long to run at all. The advantage of having this stuff stored in a table (you'll have to convert the Temp Table I created to a real table) is that you're not calculating the same thing over and over. You calculate it once and won't need to calculate it again until you make a change in the ProductAssembly table. Shoot. You could even run the hierarchical code to create the final hierarchy table as part of a Trigger on the ProductAssembly code.

    If you have any questions, please don't hesitate to ask. (Except for the "Bower" columns which form the Nested Set, all the columns have some fairly obvious names).

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Jeff/Magoo,

    Thanks both for your suggestions; I've very quickly tested Magoo's code against my sample data and it does return the expected results. I'm going to need a bit of time though to look at Jeff's code because I'm not familiar with nested sets but I will look into it!

    Thanks again 🙂

  • David-155102 (5/31/2011)


    Jeff/Magoo,

    Thanks both for your suggestions; I've very quickly tested Magoo's code against my sample data and it does return the expected results. I'm going to need a bit of time though to look at Jeff's code because I'm not familiar with nested sets but I will look into it!

    Thanks again 🙂

    You bet. Thank you for the feedback.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Viewing 11 posts - 16 through 25 (of 25 total)

    You must be logged in to reply to this topic. Login to reply