Problem With Recursive CTE

  • I am trying to create a function that returns a bill of materials for a given product. The problem is difficult to explain so I've included some test data etc. Basically, I have a product which only has one component but this product is also a component of other products. The product can be sold by itself or included within its parent products. If I run the query for the specific product incorrect results are returned, instead of one row being returned (the correct result) I get 4:

    --Create temp table for example

    CREATE TABLE #ProductAssembly

    (

    product nchar(20),

    component nchar(20)

    )

    --Insert some test data

    INSERT INTO #ProductAssembly

    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'

    ;WITH Structure (product, component, structure_level)

    AS

    (

    /* Define anchor members i.e. those products that

    ** are not themselves components of other products

    */

    SELECT parent_parts.product,

    parts.component,

    0 AS structure_level

    FROM (

    SELECT product FROM #ProductAssembly (NOLOCK)

    EXCEPT

    SELECT component FROM #ProductAssembly (NOLOCK)

    ) parent_parts

    JOIN #ProductAssembly parts (NOLOCK)

    ON parts.product = parent_parts.product

    UNION ALL

    /* Join the temp table back on to itself where the

    ** the component is equal to the parent product

    */

    SELECT ps.product,

    ps.component,

    s.structure_level + 1

    FROM dbo.#ProductAssembly ps (NOLOCK)

    JOIN Structure s

    ON s.component = ps.product

    )

    SELECT *

    FROM Structure

    WHERE product = 'Part Z'

    DROP TABLE #ProductAssembly

  • Please state expected results for

    SELECT *

    FROM Structure

    WHERE product = 'Part A'

    Would you want to see Part Y in the results?

    The absence of evidence is not evidence of absence
    - Martin Rees
    The absence of consumable DDL, sample data and desired results is, however, evidence of the absence of my response
    - Phil Parkin

  • Phil Parkin (5/25/2011)


    Please state expected results for

    SELECT *

    FROM Structure

    WHERE product = 'Part A'

    Would you want to see Part Y in the results?

    Yes, I would need to see 'Part Y' and not 'Part Z' as the component because the bill of materials is only concerned with the bottom level parts.

  • I took a shot at this one...see if this one works for you:

    IF OBJECT_ID(N'tempdb..#ProductAssembly ') > 0

    DROP TABLE #ProductAssembly ;

    GO

    --Create temp table for example

    CREATE TABLE #ProductAssembly

    (

    product nchar(20),

    component nchar(20)

    )

    --Insert some test data

    INSERT INTO #ProductAssembly

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

    WITH Structure(product, component, structure_level)

    AS (

    /* Define anchor members i.e. those products that

    ** are not themselves components of other products

    */

    SELECT parent_parts.product,

    parent_parts.product,

    0 AS structure_level

    FROM #ProductAssembly parent_parts

    UNION ALL

    /* Join the temp table back on to itself where the

    ** the component is equal to the parent product

    */

    SELECT s.product,

    ps.component,

    s.structure_level + 1

    FROM dbo.#ProductAssembly ps

    JOIN Structure s ON ps.product = s.component

    )

    SELECT s.product,

    s.component,

    s.structure_level

    FROM Structure s

    WHERE s.structure_level > 0

    AND s.product = 'Part A'

    AND s.structure_level = (

    SELECT MAX(structure_level)

    FROM Structure

    WHERE product = s.product

    )

    ORDER BY s.structure_level,

    s.product,

    s.component ;

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

  • Thanks, it works using the sample data but not on my production data....digging around now to try and figure out why it doesn't!

  • Are you getting an error by chance or just wrong results? If you post some sample data that demonstrates why it is not working I'll try to take another look at it.

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

  • To reproduce the error (or rather incorrect results) I've added some more items to the sample table:

    IF OBJECT_ID(N'tempdb..#ProductAssembly ') > 0

    DROP TABLE #ProductAssembly ;

    GO

    --Create temp table for example

    CREATE TABLE #ProductAssembly

    (

    product nchar(20),

    component nchar(20)

    )

    --Insert some test data

    INSERT INTO #ProductAssembly

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

    WITH Structure(product, component, structure_level)

    AS (

    /* Define anchor members i.e. those products that

    ** are not themselves components of other products

    */

    SELECT parent_parts.product,

    parent_parts.product,

    0 AS structure_level

    FROM #ProductAssembly parent_parts

    UNION ALL

    /* Join the temp table back on to itself where the

    ** the component is equal to the parent product

    */

    SELECT s.product,

    ps.component,

    s.structure_level + 1

    FROM dbo.#ProductAssembly ps

    JOIN Structure s ON ps.product = s.component

    )

    SELECT s.product,

    s.component,

    s.structure_level

    FROM Structure s

    WHERE s.structure_level > 0

    AND s.product = 'Part D'

    AND s.structure_level = (

    SELECT MAX(structure_level)

    FROM Structure

    WHERE product = s.product

    )

    ORDER BY s.structure_level,

    s.product,

    s.component ;

  • Just to clarify, the error is not the duplication (resolved using DISTINCT), it is that only the very last level in the structure is returned even though the Part D has components at different levels.

  • What is the expected result from this new set of test data please?

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

  • Using "Part D" as the example the result would be the following components:

    Part X

    Part W

    Part Y

  • OK, had to add another layer in the form of a iTVF and then use CROSS APPLY. See if this setup works for you:

    IF EXISTS ( SELECT *

    FROM sys.objects

    WHERE object_id = OBJECT_ID(N'dbo.get_leaf') )

    DROP FUNCTION dbo.get_leaf ;

    GO

    CREATE FUNCTION dbo.get_leaf

    (

    @product NCHAR(20),

    @component NCHAR(20)

    )

    RETURNS TABLE

    AS

    RETURN

    (

    WITH Structure(top_level_product, product, component, structure_level)

    AS (

    /* Define anchor members i.e. those products that

    ** are not themselves components of other products

    */

    SELECT DISTINCT

    p1.product,

    p1.product,

    p1.component,

    0 AS structure_level

    FROM dbo.ProductAssembly p1

    WHERE p1.product = @product

    AND p1.component = @component

    UNION ALL

    /* Join the temp table back on to itself where the

    ** the component is equal to the parent product

    */

    SELECT s.top_level_product,

    ps.product,

    ps.component,

    s.structure_level + 1

    FROM Structure s

    JOIN dbo.ProductAssembly ps ON s.component = ps.product

    )

    SELECT TOP 1

    top_level_product,

    component

    FROM Structure

    ORDER BY structure_level DESC ) ;

    GO

    IF OBJECT_ID(N'dbo.ProductAssembly ') > 0

    DROP TABLE dbo.ProductAssembly ;

    GO

    --Create temp table for example

    CREATE TABLE dbo.ProductAssembly

    (

    product NCHAR(20),

    component NCHAR(20)

    )

    --Insert some test data

    INSERT INTO dbo.ProductAssembly

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

    GO

    SELECT DISTINCT

    gl.top_level_product,

    gl.component

    FROM dbo.ProductAssembly pa

    CROSS APPLY dbo.get_leaf(pa.product, pa.component) gl

    WHERE pa.product = 'Part D' ;

    GO

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

  • Well, initial testing on my production data looks good! So thank you very much for your help with this problem 😀

  • Excellent, happy to assist 🙂 Thanks for posting back!

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

  • David-155102 (5/26/2011)


    Using "Part D" as the example the result would be the following components:

    Part X

    Part W

    Part Y

    Gosh... what would you want if Part Z had more than just Part Y as a component?

    --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/29/2011)


    David-155102 (5/26/2011)


    Using "Part D" as the example the result would be the following components:

    Part X

    Part W

    Part Y

    Gosh... what would you want if Part Z had more than just Part Y as a component?

    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::

    --Insert some test data

    INSERT INTO dbo.ProductAssembly

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

    GO

    SELECT DISTINCT

    gl.top_level_product,

    gl.component

    FROM dbo.ProductAssembly pa

    CROSS APPLY dbo.get_leaf(pa.product, pa.component) gl

    WHERE pa.product = 'Part A' ;

    GO

Viewing 15 posts - 1 through 15 (of 25 total)

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