May 30, 2011 at 7:03 am
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
May 30, 2011 at 9:42 am
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
Change is inevitable... Change for the better is not.
May 30, 2011 at 10:55 am
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
May 30, 2011 at 11:24 am
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
Change is inevitable... Change for the better is not.
May 30, 2011 at 12:26 pm
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 🙂
May 30, 2011 at 12:46 pm
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...
May 30, 2011 at 1:37 pm
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.
May 30, 2011 at 6:37 pm
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);
May 30, 2011 at 10:56 pm
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
Change is inevitable... Change for the better is not.
May 31, 2011 at 1:33 am
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 🙂
May 31, 2011 at 6:12 am
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
Change is inevitable... Change for the better is not.
Viewing 11 posts - 16 through 25 (of 25 total)
You must be logged in to reply to this topic. Login to reply