sql server: Graphs, Trees, Hierarchies and Recursive Queries
--------------------------------------------------------------------- -- Chapter 09 - Graphs, Trees, Hierarchies and Recursive Queries --图,树,层次结构,递归查询 --------------------------------------------------------------------- --------------------------------------------------------------------- -- Scenarios --------------------------------------------------------------------- --------------------------------------------------------------------- -- Employees Organizational Chart 组织结构图 --------------------------------------------------------------------- -- Listing 9-1: DDL & Sample Data for Employees SET NOCOUNT ON; USE tempdb; GO IF OBJECT_ID('dbo.Employees') IS NOT NULL DROP TABLE dbo.Employees; GO CREATE TABLE dbo.Employees ( empid INT NOT NULL PRIMARY KEY, mgrid INT NULL REFERENCES dbo.Employees, empname VARCHAR(25) NOT NULL, salary MONEY NOT NULL, CHECK (empid <> mgrid) ); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(1, NULL, 'David', $10000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(2, 1, 'Eitan', $7000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(3, 1, 'Ina', $7500.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(4, 2, 'Seraph', $5000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(5, 2, 'Jiru', $5500.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(6, 2, 'Steve', $4500.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(7, 3, 'Aaron', $5000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(8, 5, 'Lilach', $3500.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(9, 7, 'Rita', $3000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(10, 5, 'Sean', $3000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(11, 7, 'Gabriel', $3000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(12, 9, 'Emilia' , $2000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(13, 9, 'Michael', $2000.00); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(14, 9, 'Didi', $1500.00); CREATE UNIQUE INDEX idx_unc_mgrid_empid ON dbo.Employees(mgrid, empid); GO --------------------------------------------------------------------- -- Bill Of Materials (BOM) 物料 --------------------------------------------------------------------- -- Listing 9-2: DDL & Sample Data for Parts, BOM SET NOCOUNT ON; USE tempdb; GO IF OBJECT_ID('dbo.BOM') IS NOT NULL DROP TABLE dbo.BOM; GO IF OBJECT_ID('dbo.Parts') IS NOT NULL DROP TABLE dbo.Parts; GO CREATE TABLE dbo.Parts ( partid INT NOT NULL PRIMARY KEY, partname VARCHAR(25) NOT NULL ); INSERT INTO dbo.Parts(partid, partname) VALUES( 1, 'Black Tea'); INSERT INTO dbo.Parts(partid, partname) VALUES( 2, 'White Tea'); INSERT INTO dbo.Parts(partid, partname) VALUES( 3, 'Latte'); INSERT INTO dbo.Parts(partid, partname) VALUES( 4, 'Espresso'); INSERT INTO dbo.Parts(partid, partname) VALUES( 5, 'Double Espresso'); INSERT INTO dbo.Parts(partid, partname) VALUES( 6, 'Cup Cover'); INSERT INTO dbo.Parts(partid, partname) VALUES( 7, 'Regular Cup'); INSERT INTO dbo.Parts(partid, partname) VALUES( 8, 'Stirrer'); INSERT INTO dbo.Parts(partid, partname) VALUES( 9, 'Espresso Cup'); INSERT INTO dbo.Parts(partid, partname) VALUES(10, 'Tea Shot'); INSERT INTO dbo.Parts(partid, partname) VALUES(11, 'Milk'); INSERT INTO dbo.Parts(partid, partname) VALUES(12, 'Coffee Shot'); INSERT INTO dbo.Parts(partid, partname) VALUES(13, 'Tea Leaves'); INSERT INTO dbo.Parts(partid, partname) VALUES(14, 'Water'); INSERT INTO dbo.Parts(partid, partname) VALUES(15, 'Sugar Bag'); INSERT INTO dbo.Parts(partid, partname) VALUES(16, 'Ground Coffee'); INSERT INTO dbo.Parts(partid, partname) VALUES(17, 'Coffee Beans'); CREATE TABLE dbo.BOM ( partid INT NOT NULL REFERENCES dbo.Parts, assemblyid INT NULL REFERENCES dbo.Parts, unit VARCHAR(3) NOT NULL, qty DECIMAL(8, 2) NOT NULL, UNIQUE(partid, assemblyid), CHECK (partid <> assemblyid) ); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 1, NULL, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 2, NULL, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 3, NULL, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 4, NULL, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 5, NULL, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 6, 1, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 7, 1, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(10, 1, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(14, 1, 'mL', 230.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 6, 2, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 7, 2, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(10, 2, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(14, 2, 'mL', 205.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(11, 2, 'mL', 25.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 6, 3, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 7, 3, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(11, 3, 'mL', 225.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(12, 3, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 9, 4, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(12, 4, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES( 9, 5, 'EA', 1.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(12, 5, 'EA', 2.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(13, 10, 'g' , 5.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(14, 10, 'mL', 20.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(14, 12, 'mL', 20.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(16, 12, 'g' , 15.00); INSERT INTO dbo.BOM(partid, assemblyid, unit, qty) VALUES(17, 16, 'g' , 15.00); GO --------------------------------------------------------------------- -- Road System 道路 --------------------------------------------------------------------- -- Listing 9-3: DDL & Sample Data for Cities, Roads SET NOCOUNT ON; USE tempdb; GO IF OBJECT_ID('dbo.Roads') IS NOT NULL DROP TABLE dbo.Roads; GO IF OBJECT_ID('dbo.Cities') IS NOT NULL DROP TABLE dbo.Cities; GO CREATE TABLE dbo.Cities ( cityid CHAR(3) NOT NULL PRIMARY KEY, city VARCHAR(30) NOT NULL, region VARCHAR(30) NULL, country VARCHAR(30) NOT NULL ); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('ATL', 'Atlanta', 'GA', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('ORD', 'Chicago', 'IL', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('DEN', 'Denver', 'CO', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('IAH', 'Houston', 'TX', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('MCI', 'Kansas City', 'KS', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('LAX', 'Los Angeles', 'CA', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('MIA', 'Miami', 'FL', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('MSP', 'Minneapolis', 'MN', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('JFK', 'New York', 'NY', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('SEA', 'Seattle', 'WA', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('SFO', 'San Francisco', 'CA', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('ANC', 'Anchorage', 'AK', 'USA'); INSERT INTO dbo.Cities(cityid, city, region, country) VALUES('FAI', 'Fairbanks', 'AK', 'USA'); CREATE TABLE dbo.Roads ( city1 CHAR(3) NOT NULL REFERENCES dbo.Cities, city2 CHAR(3) NOT NULL REFERENCES dbo.Cities, distance INT NOT NULL, PRIMARY KEY(city1, city2), CHECK(city1 < city2), CHECK(distance > 0) ); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('ANC', 'FAI', 359); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('ATL', 'ORD', 715); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('ATL', 'IAH', 800); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('ATL', 'MCI', 805); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('ATL', 'MIA', 665); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('ATL', 'JFK', 865); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('DEN', 'IAH', 1120); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('DEN', 'MCI', 600); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('DEN', 'LAX', 1025); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('DEN', 'MSP', 915); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('DEN', 'SEA', 1335); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('DEN', 'SFO', 1270); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('IAH', 'MCI', 795); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('IAH', 'LAX', 1550); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('IAH', 'MIA', 1190); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('JFK', 'ORD', 795); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('LAX', 'SFO', 385); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('MCI', 'ORD', 525); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('MCI', 'MSP', 440); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('MSP', 'ORD', 410); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('MSP', 'SEA', 2015); INSERT INTO dbo.Roads(city1, city2, distance) VALUES('SEA', 'SFO', 815); GO --------------------------------------------------------------------- -- Iterations/Recursion --------------------------------------------------------------------- --------------------------------------------------------------------- -- Subordinates --------------------------------------------------------------------- -- Listing 9-4: Creation Script for Function fn_subordinates1 --------------------------------------------------------------------- -- Function: fn_subordinates1, Descendants -- -- Input : @root INT: Manager id -- -- Output : @Subs Table: id and level of subordinates of -- input manager (empid = @root) in all levels -- -- Process : * Insert into @Subs row of input manager -- * In a loop, while previous insert loaded more than 0 rows -- insert into @Subs next level of subordinates --------------------------------------------------------------------- USE tempdb; GO IF OBJECT_ID('dbo.fn_subordinates1') IS NOT NULL DROP FUNCTION dbo.fn_subordinates1; GO CREATE FUNCTION dbo.fn_subordinates1(@root AS INT) RETURNS @Subs Table ( empid INT NOT NULL PRIMARY KEY NONCLUSTERED, lvl INT NOT NULL, UNIQUE CLUSTERED(lvl, empid) -- Index will be used to filter level ) AS BEGIN DECLARE @lvl AS INT; SET @lvl = 0; -- Initialize level counter with 0 -- Insert root node to @Subs INSERT INTO @Subs(empid, lvl) SELECT empid, @lvl FROM dbo.Employees WHERE empid = @root; WHILE @@rowcount > 0 -- while previous level had rows BEGIN SET @lvl = @lvl + 1; -- Increment level counter -- Insert next level of subordinates to @Subs INSERT INTO @Subs(empid, lvl) SELECT C.empid, @lvl FROM @Subs AS P -- P = Parent JOIN dbo.Employees AS C -- C = Child ON P.lvl = @lvl - 1 -- Filter parents from previous level AND C.mgrid = P.empid; END RETURN; END GO -- Node ids of descendants of a given node SELECT empid, lvl FROM dbo.fn_subordinates1(3) AS S; -- Descendants of a given node SELECT E.empid, E.empname, S.lvl FROM dbo.fn_subordinates1(3) AS S JOIN dbo.Employees AS E ON E.empid = S.empid; -- Leaf nodes underneath a given node SELECT empid FROM dbo.fn_subordinates1(3) AS P WHERE NOT EXISTS (SELECT * FROM dbo.Employees AS C WHERE C.mgrid = P.empid); -- Listing 9-5: Subtree of a Given Root, CTE Solution DECLARE @root AS INT; SET @root = 3; WITH SubsCTE AS ( -- Anchor member returns root node SELECT empid, empname, 0 AS lvl FROM dbo.Employees WHERE empid = @root UNION ALL -- Recursive member returns next level of children SELECT C.empid, C.empname, P.lvl + 1 FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid ) SELECT * FROM SubsCTE; -- Listing 9-6: Creation Script for Function fn_partsexplosion --------------------------------------------------------------------- -- Function: fn_partsexplosion, Parts Explosion -- -- Input : @root INT: Root part id -- -- Output : @PartsExplosion Table: -- id and level of contained parts of input part -- in all levels -- -- Process : * Insert into @PartsExplosion row of input root part -- * In a loop, while previous insert loaded more than 0 rows -- insert into @PartsExplosion next level of parts --------------------------------------------------------------------- USE tempdb; GO IF OBJECT_ID('dbo.fn_partsexplosion') IS NOT NULL DROP FUNCTION dbo.fn_partsexplosion; GO CREATE FUNCTION dbo.fn_partsexplosion(@root AS INT) RETURNS @PartsExplosion Table ( partid INT NOT NULL, qty DECIMAL(8, 2) NOT NULL, unit VARCHAR(3) NOT NULL, lvl INT NOT NULL, n INT NOT NULL IDENTITY, -- surrogate key UNIQUE CLUSTERED(lvl, n) -- Index will be used to filter lvl ) AS BEGIN DECLARE @lvl AS INT; SET @lvl = 0; -- Initialize level counter with 0 -- Insert root node to @PartsExplosion INSERT INTO @PartsExplosion(partid, qty, unit, lvl) SELECT partid, qty, unit, @lvl FROM dbo.BOM WHERE partid = @root; WHILE @@rowcount > 0 -- while previous level had rows BEGIN SET @lvl = @lvl + 1; -- Increment level counter -- Insert next level of subordinates to @PartsExplosion INSERT INTO @PartsExplosion(partid, qty, unit, lvl) SELECT C.partid, P.qty * C.qty, C.unit, @lvl FROM @PartsExplosion AS P -- P = Parent JOIN dbo.BOM AS C -- C = Child ON P.lvl = @lvl - 1 -- Filter parents from previous level AND C.assemblyid = P.partid; END RETURN; END GO -- Parts Explosion SELECT P.partid, P.partname, PE.qty, PE.unit, PE.lvl FROM dbo.fn_partsexplosion(2) AS PE JOIN dbo.Parts AS P ON P.partid = PE.partid; -- Listing 9-7: CTE Solution for Parts Explosion DECLARE @root AS INT; SET @root = 2; WITH PartsExplosionCTE AS ( -- Anchor member returns root part SELECT partid, qty, unit, 0 AS lvl FROM dbo.BOM WHERE partid = @root UNION ALL -- Recursive member returns next level of parts SELECT C.partid, CAST(P.qty * C.qty AS DECIMAL(8, 2)), C.unit, P.lvl + 1 FROM PartsExplosionCTE AS P JOIN dbo.BOM AS C ON C.assemblyid = P.partid ) SELECT P.partid, P.partname, PE.qty, PE.unit, PE.lvl FROM PartsExplosionCTE AS PE JOIN dbo.Parts AS P ON P.partid = PE.partid; -- Parts Explosion, Aggregating Parts SELECT P.partid, P.partname, PES.qty, PES.unit FROM (SELECT partid, unit, SUM(qty) AS qty FROM dbo.fn_partsexplosion(2) AS PE GROUP BY partid, unit) AS PES JOIN dbo.Parts AS P ON P.partid = PES.partid; SELECT P.partid, P.partname, PES.qty, PES.unit FROM (SELECT partid, unit, SUM(qty) AS qty FROM dbo.fn_partsexplosion(5) AS PE GROUP BY partid, unit) AS PES JOIN dbo.Parts AS P ON P.partid = PES.partid; -- Listing 9-8: Creation Script for Function fn_subordinates2 --------------------------------------------------------------------- -- Function: fn_subordinates2, -- Descendants with optional level limit -- -- Input : @root INT: Manager id -- @maxlevels INT: Max number of levels to return -- -- Output : @Subs TABLE: id and level of subordinates of -- input manager in all levels <= @maxlevels -- -- Process : * Insert into @Subs row of input manager -- * In a loop, while previous insert loaded more than 0 rows -- and previous level is smaller than @maxlevels -- insert into @Subs next level of subordinates --------------------------------------------------------------------- USE tempdb; GO IF OBJECT_ID('dbo.fn_subordinates2') IS NOT NULL DROP FUNCTION dbo.fn_subordinates2; GO CREATE FUNCTION dbo.fn_subordinates2 (@root AS INT, @maxlevels AS INT = NULL) RETURNS @Subs TABLE ( empid INT NOT NULL PRIMARY KEY NONCLUSTERED, lvl INT NOT NULL, UNIQUE CLUSTERED(lvl, empid) -- Index will be used to filter level ) AS BEGIN DECLARE @lvl AS INT; SET @lvl = 0; -- Initialize level counter with 0 -- If input @maxlevels is NULL, set it to maximum integer -- to virtually have no limit on levels SET @maxlevels = COALESCE(@maxlevels, 2147483647); -- Insert root node to @Subs INSERT INTO @Subs(empid, lvl) SELECT empid, @lvl FROM dbo.Employees WHERE empid = @root; WHILE @@rowcount > 0 -- while previous level had rows AND @lvl < @maxlevels -- and previous level < @maxlevels BEGIN SET @lvl = @lvl + 1; -- Increment level counter -- Insert next level of subordinates to @Subs INSERT INTO @Subs(empid, lvl) SELECT C.empid, @lvl FROM @Subs AS P -- P = Parent JOIN dbo.Employees AS C -- C = Child ON P.lvl = @lvl - 1 -- Filter parents from previous level AND C.mgrid = P.empid; END RETURN; END GO -- Descendants of a given node, no limit on levels SELECT empid, lvl FROM dbo.fn_subordinates2(3, NULL) AS S; -- Descendants of a given node, limit 2 levels SELECT empid, lvl FROM dbo.fn_subordinates2(3, 2) AS S; -- Descendants that are 2 levels underneath a given node SELECT empid FROM dbo.fn_subordinates2(3, 2) AS S WHERE lvl = 2; GO -- Listing 9-9: Subtree with level limit, CTE Solution, with MAXRECURSION DECLARE @root AS INT; SET @root = 3; WITH SubsCTE AS ( SELECT empid, empname, 0 AS lvl FROM dbo.Employees WHERE empid = @root UNION ALL SELECT C.empid, C.empname, P.lvl + 1 FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid ) SELECT * FROM SubsCTE OPTION (MAXRECURSION 2); GO -- Listing 9-10: Subtree with level limit, CTE Solution, with level column DECLARE @root AS INT, @maxlevels AS INT; SET @root = 3; SET @maxlevels = 2; WITH SubsCTE AS ( SELECT empid, empname, 0 AS lvl FROM dbo.Employees WHERE empid = @root UNION ALL SELECT C.empid, C.empname, P.lvl + 1 FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid AND P.lvl < @maxlevels -- limit parent's level ) SELECT * FROM SubsCTE; --------------------------------------------------------------------- -- Sub-Path --------------------------------------------------------------------- -- Listing 9-11: Creation Script for Function fn_managers --------------------------------------------------------------------- -- Function: fn_managers, Ancestors with optional level limit -- -- Input : @empid INT : Employee id -- @maxlevels : Max number of levels to return -- -- Output : @Mgrs Table: id and level of managers of -- input employee in all levels <= @maxlevels -- -- Process : * In a loop, while current manager is not null -- and previous level is smaller than @maxlevels -- insert into @Mgrs current manager, -- and get next level manager --------------------------------------------------------------------- USE tempdb; GO IF OBJECT_ID('dbo.fn_managers') IS NOT NULL DROP FUNCTION dbo.fn_managers; GO CREATE FUNCTION dbo.fn_managers (@empid AS INT, @maxlevels AS INT = NULL) RETURNS @Mgrs TABLE ( empid INT NOT NULL PRIMARY KEY, lvl INT NOT NULL ) AS BEGIN IF NOT EXISTS(SELECT * FROM dbo.Employees WHERE empid = @empid) RETURN; DECLARE @lvl AS INT; SET @lvl = 0; -- Initialize level counter with 0 -- If input @maxlevels is NULL, set it to maximum integer -- to virtually have no limit on levels SET @maxlevels = COALESCE(@maxlevels, 2147483647); WHILE @empid IS NOT NULL -- while current employee has a manager AND @lvl <= @maxlevels -- and previous level < @maxlevels BEGIN -- Insert current manager to @Mgrs INSERT INTO @Mgrs(empid, lvl) VALUES(@empid, @lvl); SET @lvl = @lvl + 1; -- Increment level counter -- Get next level manager SET @empid = (SELECT mgrid FROM dbo.Employees WHERE empid = @empid); END RETURN; END GO -- Ancestors of a given node, no limit on levels SELECT empid, lvl FROM dbo.fn_managers(8, NULL) AS M; -- Listing 9-12: Ancestors of a Given Node, CTE Solution -- Ancestors of a given node, CTE Solution DECLARE @empid AS INT; SET @empid = 8; WITH MgrsCTE AS ( SELECT empid, mgrid, empname, 0 AS lvl FROM dbo.Employees WHERE empid = @empid UNION ALL SELECT P.empid, P.mgrid, P.empname, C.lvl + 1 FROM MgrsCTE AS C JOIN dbo.Employees AS P ON C.mgrid = P.empid ) SELECT * FROM MgrsCTE; -- Ancestors of a given node, limit 2 levels SELECT empid, lvl FROM dbo.fn_managers(8, 2) AS M; GO -- Listing 9-13: Ancestors with Level Limit, CTE Solution DECLARE @empid AS INT, @maxlevels AS INT; SET @empid = 8; SET @maxlevels = 2; WITH MgrsCTE AS ( SELECT empid, mgrid, empname, 0 AS lvl FROM dbo.Employees WHERE empid = @empid UNION ALL SELECT P.empid, P.mgrid, P.empname, C.lvl + 1 FROM MgrsCTE AS C JOIN dbo.Employees AS P ON C.mgrid = P.empid AND C.lvl < @maxlevels -- limit child's level ) SELECT * FROM MgrsCTE; -- Ancestor that is 2 levels above a given node SELECT empid FROM dbo.fn_managers(8, 2) AS M WHERE lvl = 2; GO --------------------------------------------------------------------- -- Subtree/Subgraph with Path Enumeration --------------------------------------------------------------------- -- Listing 9-14: Creation Script for Function fn_subordinates3 --------------------------------------------------------------------- -- Function: fn_subordinates3, -- Descendants with optional level limit, -- and path enumeration -- -- Input : @root INT: Manager id -- @maxlevels INT: Max number of levels to return -- -- Output : @Subs TABLE: id, level and materialized ancestors path -- of subordinates of input manager -- in all levels <= @maxlevels -- -- Process : * Insert into @Subs row of input manager -- * In a loop, while previous insert loaded more than 0 rows -- and previous level is smaller than @maxlevels: -- - insert into @Subs next level of subordinates -- - calculate a materialized ancestors path for each -- by concatenating current node id to parent's path --------------------------------------------------------------------- USE tempdb; GO IF OBJECT_ID('dbo.fn_subordinates3') IS NOT NULL DROP FUNCTION dbo.fn_subordinates3; GO CREATE FUNCTION dbo.fn_subordinates3 (@root AS INT, @maxlevels AS INT = NULL) RETURNS @Subs TABLE ( empid INT NOT NULL PRIMARY KEY NONCLUSTERED, lvl INT NOT NULL, path VARCHAR(900) NOT NULL UNIQUE CLUSTERED(lvl, empid) -- Index will be used to filter level ) AS BEGIN DECLARE @lvl AS INT; SET @lvl = 0; -- Initialize level counter with 0 -- If input @maxlevels is NULL, set it to maximum integer -- to virtually have no limit on levels SET @maxlevels = COALESCE(@maxlevels, 2147483647); -- Insert root node to @Subs INSERT INTO @Subs(empid, lvl, path) SELECT empid, @lvl, '.' + CAST(empid AS VARCHAR(10)) + '.' FROM dbo.Employees WHERE empid = @root; WHILE @@rowcount > 0 -- while previous level had rows AND @lvl < @maxlevels -- and previous level < @maxlevels BEGIN SET @lvl = @lvl + 1; -- Increment level counter -- Insert next level of subordinates to @Subs INSERT INTO @Subs(empid, lvl, path) SELECT C.empid, @lvl, P.path + CAST(C.empid AS VARCHAR(10)) + '.' FROM @Subs AS P -- P = Parent JOIN dbo.Employees AS C -- C = Child ON P.lvl = @lvl - 1 -- Filter parents from previous level AND C.mgrid = P.empid; END RETURN; END GO -- Return descendants of a given node, along with a materialized path SELECT empid, lvl, path FROM dbo.fn_subordinates3(1, NULL) AS S; -- Return descendants of a given node, sorted and indented SELECT E.empid, REPLICATE(' | ', lvl) + empname AS empname FROM dbo.fn_subordinates3(1, NULL) AS S JOIN dbo.Employees AS E ON E.empid = S.empid ORDER BY path; -- Listing 9-15: Subtree with Path Enumeration, CTE Solution -- Descendants of a given node, with Materialized Path, CTE Solution DECLARE @root AS INT; SET @root = 1; WITH SubsCTE AS ( SELECT empid, empname, 0 AS lvl, -- Path of root = '.' + empid + '.' CAST('.' + CAST(empid AS VARCHAR(10)) + '.' AS VARCHAR(MAX)) AS path FROM dbo.Employees WHERE empid = @root UNION ALL SELECT C.empid, C.empname, P.lvl + 1, -- Path of child = parent's path + child empid + '.' CAST(P.path + CAST(C.empid AS VARCHAR(10)) + '.' AS VARCHAR(MAX)) AS path FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid ) SELECT empid, REPLICATE(' | ', lvl) + empname AS empname FROM SubsCTE ORDER BY path; --------------------------------------------------------------------- -- Sorting --------------------------------------------------------------------- -- Listing 9-16: Creation Script for Procedure usp_sortsubs --------------------------------------------------------------------- -- Stored Procedure: usp_sortsubs, -- Descendants with optional level limit and sort values -- -- Input : @root INT: Manager id -- @maxlevels INT: Max number of levels to return -- @orderby sysname: determines sort order -- -- Output : Rowset: id, level and sort values -- of subordinates of input manager -- in all levels <= @maxlevels -- -- Process : * Use a loop to load the desired subtree into #SubsPath -- * For each node, construct a binary sort path -- * The row number represents the node's position among -- siblings based on the input ORDER BY list -- * Load the rows from #SubPath into #SubsSort sorted -- by the binary sortpath -- * IDENTITY values representing the global sort value -- in the subtree will be generated in the target -- #SubsSort table -- * Return all rows from #SubsSort sorted by the -- sort value --------------------------------------------------------------------- USE tempdb; GO IF OBJECT_ID('dbo.usp_sortsubs') IS NOT NULL DROP PROC dbo.usp_sortsubs; GO CREATE PROC dbo.usp_sortsubs @root AS INT = NULL, @maxlevels AS INT = NULL, @orderby AS sysname = N'empid' AS SET NOCOUNT ON; -- #SubsPath is a temp table that will hold binary sort paths CREATE TABLE #SubsPath ( rownum INT NOT NULL IDENTITY, nodeid INT NOT NULL, lvl INT NOT NULL, sortpath VARBINARY(900) NULL ); CREATE UNIQUE CLUSTERED INDEX idx_uc_lvl_empid ON #SubsPath(lvl, nodeid); -- #SubsPath is a temp table that will hold the final -- integer sort values CREATE TABLE #SubsSort ( nodeid INT NOT NULL, lvl INT NOT NULL, sortval INT NOT NULL IDENTITY ); CREATE UNIQUE CLUSTERED INDEX idx_uc_sortval ON #SubsSort(sortval); -- If @root is not specified, set it to root of the tree IF @root IS NULL SET @root = (SELECT empid FROM dbo.Employees WHERE mgrid IS NULL); -- If @maxlevels is not specified, set it maximum integer IF @maxlevels IS NULL SET @maxlevels = 2147483647; DECLARE @lvl AS INT, @sql AS NVARCHAR(4000); SET @lvl = 0; -- Load row for input root to #SubsPath -- The root's sort path is simply 1 converted to binary INSERT INTO #SubsPath(nodeid, lvl, sortpath) SELECT empid, @lvl, CAST(1 AS BINARY(4)) FROM dbo.Employees WHERE empid = @root; -- Form a loop to load the next level of suboridnates -- to #SubsPath in each iteration WHILE @@rowcount > 0 AND @lvl < @maxlevels BEGIN SET @lvl = @lvl + 1; -- Insert next level of subordinates -- Initially, just copy parent's path to child -- Note that IDENTITY values will be generated in #SubsPath -- based on input order by list -- -- Then update the path of the employees in the current level -- to their parent's path + their rownum converted to binary INSERT INTO #SubsPath(nodeid, lvl, sortpath) SELECT C.empid, @lvl, P.sortpath FROM #SubsPath AS P JOIN dbo.Employees AS C ON P.lvl = @lvl - 1 AND C.mgrid = P.nodeid ORDER BY -- determines order of siblings CASE WHEN @orderby = N'empid' THEN empid END, CASE WHEN @orderby = N'empname' THEN empname END, CASE WHEN @orderby = N'salary' THEN salary END; UPDATE #SubsPath SET sortpath = sortpath + CAST(rownum AS BINARY(4)) WHERE lvl = @lvl; END -- Load the rows from #SubsPath to @SubsSort sorted by the binary -- sort path -- The target identity values in the sortval column will represent -- the global sort value of the nodes within the result subtree INSERT INTO #SubsSort(nodeid, lvl) SELECT nodeid, lvl FROM #SubsPath ORDER BY sortpath; -- Return for each node the id, level and sort value SELECT nodeid AS empid, lvl, sortval FROM #SubsSort ORDER BY sortval; GO -- Get all employees with sort values by empname -- (relying on proc's defaults) EXEC dbo.usp_sortsubs @orderby = N'empname'; -- Get 3 levels of subordinates underneath employee 1 -- with sort values by empname EXEC dbo.usp_sortsubs @root = 1, @maxlevels = 3, @orderby = N'empname'; GO -- Listing 9-17: Script Returning All Employees, Sorted by empname CREATE TABLE #Subs ( empid INT NULL, lvl INT NULL, sortval INT NULL ); CREATE UNIQUE CLUSTERED INDEX idx_uc_sortval ON #Subs(sortval); -- By empname INSERT INTO #Subs(empid, lvl, sortval) EXEC dbo.usp_sortsubs @orderby = N'empname'; SELECT E.empid, REPLICATE(' | ', lvl) + E.empname AS empname FROM #Subs AS S JOIN dbo.Employees AS E ON S.empid = E.empid ORDER BY sortval; -- Listing 9-18: Script Returning All Employees, Sorted by salary TRUNCATE TABLE #Subs; INSERT INTO #Subs(empid, lvl, sortval) EXEC dbo.usp_sortsubs @orderby = N'salary'; SELECT E.empid, salary, REPLICATE(' | ', lvl) + E.empname AS empname FROM #Subs AS S JOIN dbo.Employees AS E ON S.empid = E.empid ORDER BY sortval; -- Cleanup DROP TABLE #Subs GO -- Listing 9-19: Sorting Hierarchy by empname, CTE Solution DECLARE @root AS INT; SET @root = 1; WITH SubsCTE AS ( SELECT empid, empname, 0 AS lvl, -- Path of root is 1 (binary) CAST(1 AS VARBINARY(MAX)) AS sortpath FROM dbo.Employees WHERE empid = @root UNION ALL SELECT C.empid, C.empname, P.lvl + 1, -- Path of child = parent's path + child row number (binary) P.sortpath + CAST( ROW_NUMBER() OVER(PARTITION BY C.mgrid ORDER BY C.empname) -- sort col(s) AS BINARY(4)) FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid ) SELECT empid, ROW_NUMBER() OVER(ORDER BY sortpath) AS sortval, REPLICATE(' | ', lvl) + empname AS empname FROM SubsCTE ORDER BY sortval; GO -- Listing 9-20: Sorting Hierarchy by salary, CTE Solution DECLARE @root AS INT; SET @root = 1; WITH SubsCTE AS ( SELECT empid, empname, salary, 0 AS lvl, -- Path of root = 1 (binary) CAST(1 AS VARBINARY(MAX)) AS sortpath FROM dbo.Employees WHERE empid = @root UNION ALL SELECT C.empid, C.empname, C.salary, P.lvl + 1, -- Path of child = parent's path + child row number (binary) P.sortpath + CAST( ROW_NUMBER() OVER(PARTITION BY C.mgrid ORDER BY C.salary) -- sort col(s) AS BINARY(4)) FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid ) SELECT empid, salary, ROW_NUMBER() OVER(ORDER BY sortpath) AS sortval, REPLICATE(' | ', lvl) + empname AS empname FROM SubsCTE ORDER BY sortval; --------------------------------------------------------------------- -- Cycles --------------------------------------------------------------------- -- Create a cyclic path UPDATE dbo.Employees SET mgrid = 14 WHERE empid = 1; GO -- Listing 9-21: Detecting Cycles, CTE Solution DECLARE @root AS INT; SET @root = 1; WITH SubsCTE AS ( SELECT empid, empname, 0 AS lvl, CAST('.' + CAST(empid AS VARCHAR(10)) + '.' AS VARCHAR(MAX)) AS path, -- Obviously root has no cycle 0 AS cycle FROM dbo.Employees WHERE empid = @root UNION ALL SELECT C.empid, C.empname, P.lvl + 1, CAST(P.path + CAST(C.empid AS VARCHAR(10)) + '.' AS VARCHAR(MAX)) AS path, -- Cycle detected if parent's path contains child's id CASE WHEN P.path LIKE '%.' + CAST(C.empid AS VARCHAR(10)) + '.%' THEN 1 ELSE 0 END FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid ) SELECT empid, empname, cycle, path FROM SubsCTE; GO -- Listing 9-22: Not Pursuing Cycles, CTE Solution DECLARE @root AS INT; SET @root = 1; WITH SubsCTE AS ( SELECT empid, empname, 0 AS lvl, CAST('.' + CAST(empid AS VARCHAR(10)) + '.' AS VARCHAR(MAX)) AS path, -- Obviously root has no cycle 0 AS cycle FROM dbo.Employees WHERE empid = @root UNION ALL SELECT C.empid, C.empname, P.lvl + 1, CAST(P.path + CAST(C.empid AS VARCHAR(10)) + '.' AS VARCHAR(MAX)) AS path, -- Cycle detected if parent's path contains child's id CASE WHEN P.path LIKE '%.' + CAST(C.empid AS VARCHAR(10)) + '.%' THEN 1 ELSE 0 END FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid AND P.cycle = 0 -- do not pursue branch for parent with cycle ) SELECT empid, empname, cycle, path FROM SubsCTE; GO -- Listing 9-23: Isolating Cyclic Paths, CTE Solution DECLARE @root AS INT; SET @root = 1; WITH SubsCTE AS ( SELECT empid, empname, 0 AS lvl, CAST('.' + CAST(empid AS VARCHAR(10)) + '.' AS VARCHAR(MAX)) AS path, -- Obviously root has no cycle 0 AS cycle FROM dbo.Employees WHERE empid = @root UNION ALL SELECT C.empid, C.empname, P.lvl + 1, CAST(P.path + CAST(C.empid AS VARCHAR(10)) + '.' AS VARCHAR(MAX)) AS path, -- Cycle detected if parent's path contains child's id CASE WHEN P.path LIKE '%.' + CAST(C.empid AS VARCHAR(10)) + '.%' THEN 1 ELSE 0 END FROM SubsCTE AS P JOIN dbo.Employees AS C ON C.mgrid = P.empid AND P.cycle = 0 ) SELECT path FROM SubsCTE WHERE cycle = 1; -- Fix cyclic path UPDATE dbo.Employees SET mgrid = NULL WHERE empid = 1; --------------------------------------------------------------------- -- Materialized Path --------------------------------------------------------------------- --------------------------------------------------------------------- -- Maintaining Data --------------------------------------------------------------------- -- Listing 9-24: DDL for Employees with Path SET NOCOUNT ON; USE tempdb; GO IF OBJECT_ID('dbo.Employees') IS NOT NULL DROP TABLE dbo.Employees; GO CREATE TABLE dbo.Employees ( empid INT NOT NULL PRIMARY KEY NONCLUSTERED, mgrid INT NULL REFERENCES dbo.Employees, empname VARCHAR(25) NOT NULL, salary MONEY NOT NULL, lvl INT NOT NULL, path VARCHAR(900) NOT NULL UNIQUE CLUSTERED ); CREATE UNIQUE INDEX idx_unc_mgrid_empid ON dbo.Employees(mgrid, empid); GO --------------------------------------------------------------------- -- Adding Employees who Manage No One (Leaves) --------------------------------------------------------------------- -- Listing 9-25: Creation Script for Procedure usp_insertemp --------------------------------------------------------------------- -- Stored Procedure: usp_insertemp, -- Inserts new employee who manages no one into the table --------------------------------------------------------------------- USE tempdb; GO IF OBJECT_ID('dbo.usp_insertemp') IS NOT NULL DROP PROC dbo.usp_insertemp; GO CREATE PROC dbo.usp_insertemp @empid INT, @mgrid INT, @empname VARCHAR(25), @salary MONEY AS SET NOCOUNT ON; -- Handle case where the new employee has no manager (root) IF @mgrid IS NULL INSERT INTO dbo.Employees(empid, mgrid, empname, salary, lvl, path) VALUES(@empid, @mgrid, @empname, @salary, 0, '.' + CAST(@empid AS VARCHAR(10)) + '.'); -- Handle subordinate case (non-root) ELSE INSERT INTO dbo.Employees(empid, mgrid, empname, salary, lvl, path) SELECT @empid, @mgrid, @empname, @salary, lvl + 1, path + CAST(@empid AS VARCHAR(10)) + '.' FROM dbo.Employees WHERE empid = @mgrid; GO -- Listing 9-26: Sample Data for Employees with Path EXEC dbo.usp_insertemp @empid = 1, @mgrid = NULL, @empname = 'David', @salary = $10000.00; EXEC dbo.usp_insertemp @empid = 2, @mgrid = 1, @empname = 'Eitan', @salary = $7000.00; EXEC dbo.usp_insertemp @empid = 3, @mgrid = 1, @empname = 'Ina', @salary = $7500.00; EXEC dbo.usp_insertemp @empid = 4, @mgrid = 2, @empname = 'Seraph', @salary = $5000.00; EXEC dbo.usp_insertemp @empid = 5, @mgrid = 2, @empname = 'Jiru', @salary = $5500.00; EXEC dbo.usp_insertemp @empid = 6, @mgrid = 2, @empname = 'Steve', @salary = $4500.00; EXEC dbo.usp_insertemp @empid = 7, @mgrid = 3, @empname = 'Aaron', @salary = $5000.00; EXEC dbo.usp_insertemp @empid = 8, @mgrid = 5, @empname = 'Lilach', @salary = $3500.00; EXEC dbo.usp_insertemp @empid = 9, @mgrid = 7, @empname = 'Rita', @salary = $3000.00; EXEC dbo.usp_insertemp @empid = 10, @mgrid = 5, @empname = 'Sean', @salary = $3000.00; EXEC dbo.usp_insertemp @empid = 11, @mgrid = 7, @empname = 'Gabriel', @salary = $3000.00; EXEC dbo.usp_insertemp @empid = 12, @mgrid = 9, @empname = 'Emilia', @salary = $2000.00; EXEC dbo.usp_insertemp @empid = 13, @mgrid = 9, @empname = 'Michael', @salary = $2000.00; EXEC dbo.usp_insertemp @empid = 14, @mgrid = 9, @empname = 'Didi', @salary = $1500.00; GO -- Examine data after load SELECT empid, mgrid, empname, salary, lvl, path FROM dbo.Employees ORDER BY path; --------------------------------------------------------------------- -- Moving a Subtree --------------------------------------------------------------------- -- Listing 9-27: Creation Script for Procedure usp_movesubtree --------------------------------------------------------------------- -- Stored Procedure: usp_movesubtree, -- Moves a whole subtree of a given root to a new location -- under a given manager --------------------------------------------------------------------- USE tempdb; GO IF OBJECT_ID('dbo.usp_movesubtree') IS NOT NULL DROP PROC dbo.usp_movesubtree; GO CREATE PROC dbo.usp_movesubtree @root INT, @mgrid INT AS SET NOCOUNT ON; BEGIN TRAN; -- Update level and path of all employees in the subtree (E) -- Set level = -- current level + new manager's level - old manager's level -- Set path = -- in current path remove old manager's path -- and substitute with new manager's path UPDATE E SET lvl = E.lvl + NM.lvl - OM.lvl, path = STUFF(E.path, 1, LEN(OM.path), NM.path) FROM dbo.Employees AS E -- E = Employees (subtree) JOIN dbo.Employees AS R -- R = Root (one row) ON R.empid = @root AND E.path LIKE R.path + '%' JOIN dbo.Employees AS OM -- OM = Old Manager (one row) ON OM.empid = R.mgrid JOIN dbo.Employees AS NM -- NM = New Manager (one row) ON NM.empid = @mgrid; -- Update root's new manager UPDATE dbo.Employees SET mgrid = @mgrid WHERE empid = @root; COMMIT TRAN; GO -- Before moving subtree SELECT empid, REPLICATE(' | ', lvl) + empname AS empname, lvl, path FROM dbo.Employees ORDER BY path; -- Move Subtree BEGIN TRAN; EXEC dbo.usp_movesubtree @root = 7, @mgrid = 10; -- After moving subtree SELECT empid, REPLICATE(' | ', lvl) + empname AS empname, lvl, path FROM dbo.Employees ORDER BY path; ROLLBACK TRAN; -- rollback used in order not to apply the change --------------------------------------------------------------------- -- Removing a Subtree --------------------------------------------------------------------- -- Before deleteting subtree SELECT empid, REPLICATE(' | ', lvl) + empname AS empname, lvl, path FROM dbo.Employees ORDER BY path; -- Delete Subtree BEGIN TRAN; DELETE FROM dbo.Employees WHERE path LIKE (SELECT M.path + '%' FROM dbo.Employees as M WHERE M.empid = 7); -- After deleting subtree SELECT empid, REPLICATE(' | ', lvl) + empname AS empname, lvl, path FROM dbo.Employees ORDER BY path; ROLLBACK TRAN; -- rollback used in order not to apply the change --------------------------------------------------------------------- -- Querying Materialized Path --------------------------------------------------------------------- -- Subtree of a given root SELECT REPLICATE(' | ', E.lvl - M.lvl) + E.empname FROM dbo.Employees AS E JOIN dbo.Employees AS M ON M.empid = 3 -- root AND E.path LIKE M.path + '%' ORDER BY E.path; -- Subtree of a given root, excluding root SELECT REPLICATE(' | ', E.lvl - M.lvl - 1) + E.empname FROM dbo.Employees AS E JOIN dbo.Employees AS M ON M.empid = 3 AND E.path LIKE M.path + '_%' ORDER BY E.path; -- Leaf nodes under a given root SELECT E.empid, E.empname FROM dbo.Employees AS E JOIN dbo.Employees AS M ON M.empid = 3 AND E.path LIKE M.path + '%' WHERE NOT EXISTS (SELECT * FROM dbo.Employees AS E2 WHERE E2.mgrid = E.empid); -- Subtree of a given root, limit number of levels SELECT REPLICATE(' | ', E.lvl - M.lvl) + E.empname FROM dbo.Employees AS E JOIN dbo.Employees AS M ON M.empid = 3 AND E.path LIKE M.path + '%' AND E.lvl - M.lvl <= 2 ORDER BY E.path; -- Nodes that are n levels under a given root SELECT E.empid, E.empname FROM dbo.Employees AS E JOIN dbo.Employees AS M ON M.empid = 3 AND E.path LIKE M.path + '%' AND E.lvl - M.lvl = 2; -- Ancestors of a given node (requires a table scan) SELECT REPLICATE(' | ', M.lvl) + M.empname FROM dbo.Employees AS E JOIN dbo.Employees AS M ON E.empid = 14 AND E.path LIKE M.path + '%' ORDER BY E.path; -- Listing 9-28: Creating and Populating Auxiliary Table of Numbers SET NOCOUNT ON; USE tempdb; GO IF OBJECT_ID('dbo.Nums') IS NOT NULL DROP TABLE dbo.Nums; GO CREATE TABLE dbo.Nums(n INT NOT NULL PRIMARY KEY); DECLARE @max AS INT, @rc AS INT; SET @max = 8000; SET @rc = 1; INSERT INTO Nums VALUES(1); WHILE @rc * 2 <= @max BEGIN INSERT INTO dbo.Nums SELECT n + @rc FROM dbo.Nums; SET @rc = @rc * 2; END INSERT INTO dbo.Nums SELECT n + @rc FROM dbo.Nums WHERE n + @rc <= @max; GO -- Listing 9-29: Creation Script for Function fn_splitpath USE tempdb; GO IF OBJECT_ID('dbo.fn_splitpath') IS NOT NULL DROP FUNCTION dbo.fn_splitpath; GO CREATE FUNCTION dbo.fn_splitpath(@empid AS INT) RETURNS TABLE AS RETURN SELECT n - LEN(REPLACE(LEFT(path, n), '.', '')) AS pos, CAST(SUBSTRING(path, n + 1, CHARINDEX('.', path, n+1) - n - 1) AS INT) AS empid FROM dbo.Employees JOIN dbo.Nums ON empid = @empid AND n < LEN(path) AND SUBSTRING(path, n, 1) = '.' GO -- Test fn_splitpath function SELECT pos, empid FROM dbo.fn_splitpath(14); -- Getting ancestors using fn_splitpath function SELECT REPLICATE(' | ', lvl) + empname FROM dbo.fn_splitpath(14) AS SP JOIN dbo.Employees AS E ON E.empid = SP.empid ORDER BY path; --------------------------------------------------------------------- -- Nested Sets --------------------------------------------------------------------- --------------------------------------------------------------------- -- Assigning Left and Right Values --------------------------------------------------------------------- -- Listing 9-30: Producing Binary Sort Paths Representing Nested Sets Relationships USE tempdb; GO -- Create index to speed sorting siblings by empname, empid CREATE UNIQUE INDEX idx_unc_mgrid_empname_empid ON dbo.Employees(mgrid, empname, empid); GO DECLARE @root AS INT; SET @root = 1; -- CTE with two numbers: 1 and 2 WITH TwoNumsCTE AS ( SELECT 1 AS n UNION ALL SELECT 2 ), -- CTE with two binary sort paths for each node: -- One smaller than descendants sort paths -- One greater than descendants sort paths SortPathCTE AS ( SELECT empid, 0 AS lvl, n, CAST(n AS VARBINARY(MAX)) AS sortpath FROM dbo.Employees CROSS JOIN TwoNumsCTE WHERE empid = @root UNION ALL SELECT C.empid, P.lvl + 1, TN.n, P.sortpath + CAST( (-1+ROW_NUMBER() OVER(PARTITION BY C.mgrid -- *** determines order of siblings *** ORDER BY C.empname, C.empid))/2*2+TN.n AS BINARY(4)) FROM SortPathCTE AS P JOIN dbo.Employees AS C ON P.n = 1 AND C.mgrid = P.empid CROSS JOIN TwoNumsCTE AS TN ) SELECT * FROM SortPathCTE ORDER BY sortpath; GO -- Listing 9-31: CTE Code That Creates Nested Sets Relationships DECLARE @root AS INT; SET @root = 1; -- CTE with two numbers: 1 and 2 WITH TwoNumsCTE AS ( SELECT 1 AS n UNION ALL SELECT 2 ), -- CTE with two binary sort paths for each node: -- One smaller than descendants sort paths -- One greater than descendants sort paths SortPathCTE AS ( SELECT empid, 0 AS lvl, n, CAST(n AS VARBINARY(MAX)) AS sortpath FROM dbo.Employees CROSS JOIN TwoNumsCTE WHERE empid = @root UNION ALL SELECT C.empid, P.lvl + 1, TN.n, P.sortpath + CAST( ROW_NUMBER() OVER(PARTITION BY C.mgrid -- *** determines order of siblings *** ORDER BY C.empname, C.empid, TN.n) AS BINARY(4)) FROM SortPathCTE AS P JOIN dbo.Employees AS C ON P.n = 1 AND C.mgrid = P.empid CROSS JOIN TwoNumsCTE AS TN ), -- CTE with Row Numbers Representing sortpath Order SortCTE AS ( SELECT empid, lvl, ROW_NUMBER() OVER(ORDER BY sortpath) AS sortval FROM SortPathCTE ), -- CTE with Left and Right Values Representing -- Nested Sets Relationships NestedSetsCTE AS ( SELECT empid, lvl, MIN(sortval) AS lft, MAX(sortval) AS rgt FROM SortCTE GROUP BY empid, lvl ) SELECT * FROM NestedSetsCTE ORDER BY lft; GO -- Listing 9-32: Creation Script for the Function fn_empsnestedsets --------------------------------------------------------------------- -- Function: fn_empsnestedsets, Nested Sets Relationships -- -- Input : @root INT: Root of subtree -- -- Output : @NestedSets Table: employee id, level in the subtree, -- left and right values representing -- nested sets relationships -- -- Process : * Loads subtree into @SortPath, -- first root, then a level at a time. -- Note: two instances of each employee are loaded; -- one representing left arm (n = 1), -- and one representing right (n = 2). -- For each employee and arm, a binary path is constructed, -- representing the nested sets poition. -- The binary path has 4 bytes for each of the employee's -- ancestors. For each ancestor, the 4 bytes represent -- its position in the level (calculated with identity). -- Finally @SortPath will contain a pair of rows for each -- employee along with a sort path represeting the arm's -- nested sets position. -- * Next, the rows from @SortPath are loaded -- into @SortVals, sorted by sortpath. After the load, -- an integer identity column sortval holds sort values -- representing the nested sets position of each arm. -- * The data from @SortVals is grouped by employee -- generating the left and right values for each employee -- in one row. The resultset is loaded into the -- @NestedSets table, which is the function's output. -- --------------------------------------------------------------------- SET NOCOUNT ON; USE tempdb; GO IF OBJECT_ID('dbo.fn_empsnestedsets') IS NOT NULL DROP FUNCTION dbo.fn_empsnestedsets; GO CREATE FUNCTION dbo.fn_empsnestedsets(@root AS INT) RETURNS @NestedSets TABLE ( empid INT NOT NULL PRIMARY KEY, lvl INT NOT NULL, lft INT NOT NULL, rgt INT NOT NULL ) AS BEGIN DECLARE @lvl AS INT; SET @lvl = 0; -- @TwoNums: Table Variable with two numbers: 1 and 2 DECLARE @TwoNums TABLE(n INT NOT NULL PRIMARY KEY); INSERT INTO @TwoNums(n) SELECT 1 AS n UNION ALL SELECT 2; -- @SortPath: Table Variable with two binary sort paths -- for each node: -- One smaller than descendants sort paths -- One greater than descendants sort paths DECLARE @SortPath TABLE ( empid INT NOT NULL, lvl INT NOT NULL, n INT NOT NULL, sortpath VARBINARY(900) NOT NULL, rownum INT NOT NULL IDENTITY, UNIQUE(lvl, n, empid) ); -- Load root into @SortPath INSERT INTO @SortPath(empid, lvl, n, sortpath) SELECT empid, @lvl, n, CAST(n AS BINARY(4)) AS sortpath FROM dbo.Employees CROSS JOIN @TwoNums WHERE empid = @root WHILE @@rowcount > 0 BEGIN SET @lvl = @lvl + 1; -- Load next level into @SortPath INSERT INTO @SortPath(empid, lvl, n, sortpath) SELECT C.empid, @lvl, TN.n, P.sortpath FROM @SortPath AS P JOIN dbo.Employees AS C ON P.lvl = @lvl - 1 AND P.n = 1 AND C.mgrid = P.empid CROSS JOIN @TwoNums AS TN -- *** Determines order of siblings *** ORDER BY C.empname, C.empid, TN.n; -- Update sort path to include child's position UPDATE @SortPath SET sortpath = sortpath + CAST(rownum AS BINARY(4)) WHERE lvl = @lvl; END -- @SortVals: Table Variable with row numbers -- representing sortpath order DECLARE @SortVals TABLE ( empid INT NOT NULL, lvl INT NOT NULL, sortval INT NOT NULL IDENTITY ) -- Load data from @SortPath sorted by sortpath -- to generate sort values INSERT INTO @SortVals(empid, lvl) SELECT empid, lvl FROM @SortPath ORDER BY sortpath; -- Load data into @NestedStes generating left and right -- values representing nested sets relationships INSERT INTO @NestedSets(empid, lvl, lft, rgt) SELECT empid, lvl, MIN(sortval), MAX(sortval) FROM @SortVals GROUP BY empid, lvl RETURN; END GO -- Test the fn_empsnestedsets function SELECT * FROM dbo.fn_empsnestedsets(1) ORDER BY lft; GO -- Listing 9-33: Materializing Nested Sets Relationships in a Table SET NOCOUNT ON; USE tempdb; GO DECLARE @root AS INT; SET @root = 1; WITH TwoNumsCTE AS ( SELECT 1 AS n UNION ALL SELECT 2 ), SortPathCTE AS ( SELECT empid, 0 AS lvl, n, CAST(n AS VARBINARY(MAX)) AS sortpath FROM dbo.Employees CROSS JOIN TwoNumsCTE WHERE empid = @root UNION ALL SELECT C.empid, P.lvl + 1, TN.n, P.sortpath + CAST( ROW_NUMBER() OVER(PARTITION BY C.mgrid -- *** determines order of siblings *** ORDER BY C.empname, C.empid, TN.n) AS BINARY(4)) FROM SortPathCTE AS P JOIN dbo.Employees AS C ON P.n = 1 AND C.mgrid = P.empid CROSS JOIN TwoNumsCTE AS TN ), SortCTE AS ( SELECT empid, lvl, ROW_NUMBER() OVER(ORDER BY sortpath) AS sortval FROM SortPathCTE ), NestedSetsCTE AS ( SELECT empid, lvl, MIN(sortval) AS lft, MAX(sortval) AS rgt FROM SortCTE GROUP BY empid, lvl ) SELECT E.empid, E.empname, E.salary, NS.lvl, NS.lft, NS.rgt INTO dbo.EmployeesNS FROM NestedSetsCTE AS NS JOIN dbo.Employees AS E ON E.empid = NS.empid; ALTER TABLE dbo.EmployeesNS ADD PRIMARY KEY NONCLUSTERED(empid); CREATE UNIQUE CLUSTERED INDEX idx_unc_lft_rgt ON dbo.EmployeesNS(lft, rgt); GO --------------------------------------------------------------------- -- Querying --------------------------------------------------------------------- -- Descendants of a given root SELECT C.empid, REPLICATE(' | ', C.lvl - P.lvl) + C.empname AS empname FROM dbo.EmployeesNS AS P JOIN dbo.EmployeesNS AS C ON P.empid = 3 AND C.lft >= P.lft AND C.rgt <= P.rgt ORDER BY C.lft; -- Descendants of a given root, limiting 2 levels SELECT C.empid, REPLICATE(' | ', C.lvl - P.lvl) + C.empname AS empname FROM dbo.EmployeesNS AS P JOIN dbo.EmployeesNS AS C ON P.empid = 3 AND C.lft >= P.lft AND C.rgt <= P.rgt AND C.lvl - P.lvl <= 2 ORDER BY C.lft; -- Leaf nodes under a given root SELECT C.empid, C.empname FROM dbo.EmployeesNS AS P JOIN dbo.EmployeesNS AS C ON P.empid = 3 AND C.lft >= P.lft AND C.rgt <= P.rgt AND C.rgt - C.lft = 1; -- Count of subordinates of each node SELECT empid, (rgt - lft - 1) / 2 AS cnt, REPLICATE(' | ', lvl) + empname AS empname FROM dbo.EmployeesNS ORDER BY lft; -- Ancestors of a given node SELECT P.empid, P.empname, P.lvl FROM dbo.EmployeesNS AS P JOIN dbo.EmployeesNS AS C ON C.empid = 14 AND C.lft >= P.lft AND C.rgt <= P.rgt; GO -- Cleanup DROP TABLE dbo.EmployeesNS; GO --------------------------------------------------------------------- -- Transitive Closure --------------------------------------------------------------------- --------------------------------------------------------------------- -- Directed Acyclic Graph (DAG) --------------------------------------------------------------------- -- Listing 9-34: Transitive Closure of BOM (DAG) WITH BOMTC AS ( -- Return all first-level containment relationships SELECT assemblyid, partid FROM dbo.BOM WHERE assemblyid IS NOT NULL UNION ALL -- Return next-level containment relationships SELECT P.assemblyid, C.partid FROM BOMTC AS P JOIN dbo.BOM AS C ON C.assemblyid = P.partid ) -- Return distinct pairs that have -- transitive containment relationships SELECT DISTINCT assemblyid, partid FROM BOMTC; GO -- Listing 9-35: Creation Script for the fn_BOMTC UDF IF OBJECT_ID('dbo.fn_BOMTC') IS NOT NULL DROP FUNCTION dbo.fn_BOMTC; GO CREATE FUNCTION fn_BOMTC() RETURNS @BOMTC TABLE ( assemblyid INT NOT NULL, partid INT NOT NULL, PRIMARY KEY (assemblyid, partid) ) AS BEGIN INSERT INTO @BOMTC(assemblyid, partid) SELECT assemblyid, partid FROM dbo.BOM WHERE assemblyid IS NOT NULL WHILE @@rowcount > 0 INSERT INTO @BOMTC SELECT P.assemblyid, C.partid FROM @BOMTC AS P JOIN dbo.BOM AS C ON C.assemblyid = P.partid WHERE NOT EXISTS (SELECT * FROM @BOMTC AS P2 WHERE P2.assemblyid = P.assemblyid AND P2.partid = C.partid); RETURN; END GO -- Use the fn_BOMTC UDF SELECT assemblyid, partid FROM fn_BOMTC(); GO -- Listing 9-36: All Paths in BOM WITH BOMPaths AS ( SELECT assemblyid, partid, 1 AS distance, -- distance in first level is 1 -- path in first level is .assemblyid.partid. '.' + CAST(assemblyid AS VARCHAR(MAX)) + '.' + CAST(partid AS VARCHAR(MAX)) + '.' AS path FROM dbo.BOM WHERE assemblyid IS NOT NULL UNION ALL SELECT P.assemblyid, C.partid, -- distance in next level is parent's distance + 1 P.distance + 1, -- path in next level is parent_path.child_partid. P.path + CAST(C.partid AS VARCHAR(MAX)) + '.' FROM BOMPaths AS P JOIN dbo.BOM AS C ON C.assemblyid = P.partid ) -- Return all paths SELECT * FROM BOMPaths; -- Listing 9-37: Shortest Paths in BOM WITH BOMPaths -- All paths AS ( SELECT assemblyid, partid, 1 AS distance, '.' + CAST(assemblyid AS VARCHAR(MAX)) + '.' + CAST(partid AS VARCHAR(MAX)) + '.' AS path FROM dbo.BOM WHERE assemblyid IS NOT NULL UNION ALL SELECT P.assemblyid, C.partid, P.distance + 1, P.path + CAST(C.partid AS VARCHAR(MAX)) + '.' FROM BOMPaths AS P JOIN dbo.BOM AS C ON C.assemblyid = P.partid ), BOMMinDist AS -- Minimum distance for each pair ( SELECT assemblyid, partid, MIN(distance) AS mindist FROM BOMPaths GROUP BY assemblyid, partid ) -- Shortest path for each pair SELECT BP.* FROM BOMMinDist AS BMD JOIN BOMPaths AS BP ON BMD.assemblyid = BP.assemblyid AND BMD.partid = BP.partid AND BMD.mindist = BP.distance; GO --------------------------------------------------------------------- -- Undirected Cyclic Graph --------------------------------------------------------------------- -- Listing 9-38: Transitive Closure of Roads (Undirected Cyclic Graph) WITH Roads2 -- Two rows for each pair (f-->t, t-->f) AS ( SELECT city1 AS from_city, city2 AS to_city FROM dbo.Roads UNION ALL SELECT city2, city1 FROM dbo.Roads ), RoadPaths AS ( -- Return all first-level reachability pairs SELECT from_city, to_city, -- path is needed to identify cycles CAST('.' + from_city + '.' + to_city + '.' AS VARCHAR(MAX)) AS path FROM Roads2 UNION ALL -- Return next-level reachability pairs SELECT F.from_city, T.to_city, CAST(F.path + T.to_city + '.' AS VARCHAR(MAX)) FROM RoadPaths AS F JOIN Roads2 AS T -- if to_city appears in from_city's path, cycle detected ON CASE WHEN F.path LIKE '%.' + T.to_city + '.%' THEN 1 ELSE 0 END = 0 AND F.to_city = T.from_city ) -- Return Transitive Closure of Roads SELECT DISTINCT from_city, to_city FROM RoadPaths; GO -- Listing 9-39: Creation Script for the fn_RoadsTC UDF IF OBJECT_ID('dbo.fn_RoadsTC') IS NOT NULL DROP FUNCTION dbo.fn_RoadsTC; GO CREATE FUNCTION dbo.fn_RoadsTC() RETURNS @RoadsTC TABLE ( from_city VARCHAR(3) NOT NULL, to_city VARCHAR(3) NOT NULL, PRIMARY KEY (from_city, to_city) ) AS BEGIN DECLARE @added as INT; INSERT INTO @RoadsTC(from_city, to_city) SELECT city1, city2 FROM dbo.Roads; SET @added = @@rowcount; INSERT INTO @RoadsTC SELECT city2, city1 FROM dbo.Roads SET @added = @added + @@rowcount; WHILE @added > 0 BEGIN INSERT INTO @RoadsTC SELECT DISTINCT TC.from_city, R.city2 FROM @RoadsTC AS TC JOIN dbo.Roads AS R ON R.city1 = TC.to_city WHERE NOT EXISTS (SELECT * FROM @RoadsTC AS TC2 WHERE TC2.from_city = TC.from_city AND TC2.to_city = R.city2) AND TC.from_city <> R.city2; SET @added = @@rowcount; INSERT INTO @RoadsTC SELECT DISTINCT TC.from_city, R.city1 FROM @RoadsTC AS TC JOIN dbo.Roads AS R ON R.city2 = TC.to_city WHERE NOT EXISTS (SELECT * FROM @RoadsTC AS TC2 WHERE TC2.from_city = TC.from_city AND TC2.to_city = R.city1) AND TC.from_city <> R.city1; SET @added = @added + @@rowcount; END RETURN; END GO -- Use the fn_RoadsTC UDF SELECT * FROM dbo.fn_RoadsTC(); GO -- Listing 9-40: All paths and distances in Roads (15262 rows) WITH Roads2 AS ( SELECT city1 AS from_city, city2 AS to_city, distance FROM dbo.Roads UNION ALL SELECT city2, city1, distance FROM dbo.Roads ), RoadPaths AS ( SELECT from_city, to_city, distance, CAST('.' + from_city + '.' + to_city + '.' AS VARCHAR(MAX)) AS path FROM Roads2 UNION ALL SELECT F.from_city, T.to_city, F.distance + T.distance, CAST(F.path + T.to_city + '.' AS VARCHAR(MAX)) FROM RoadPaths AS F JOIN Roads2 AS T ON CASE WHEN F.path LIKE '%.' + T.to_city + '.%' THEN 1 ELSE 0 END = 0 AND F.to_city = T.from_city ) -- Return all paths and distances SELECT * FROM RoadPaths; -- Listing 9-41: Shortest paths in Roads WITH Roads2 AS ( SELECT city1 AS from_city, city2 AS to_city, distance FROM dbo.Roads UNION ALL SELECT city2, city1, distance FROM dbo.Roads ), RoadPaths AS ( SELECT from_city, to_city, distance, CAST('.' + from_city + '.' + to_city + '.' AS VARCHAR(MAX)) AS path FROM Roads2 UNION ALL SELECT F.from_city, T.to_city, F.distance + T.distance, CAST(F.path + T.to_city + '.' AS VARCHAR(MAX)) FROM RoadPaths AS F JOIN Roads2 AS T ON CASE WHEN F.path LIKE '%.' + T.to_city + '.%' THEN 1 ELSE 0 END = 0 AND F.to_city = T.from_city ), RoadsMinDist -- Min distance for each pair in TC AS ( SELECT from_city, to_city, MIN(distance) AS mindist FROM RoadPaths GROUP BY from_city, to_city ) -- Return shortest paths and distances SELECT RP.* FROM RoadsMinDist AS RMD JOIN RoadPaths AS RP ON RMD.from_city = RP.from_city AND RMD.to_city = RP.to_city AND RMD.mindist = RP.distance; GO -- Listing 9-42: Load Shortest Road Paths Into a Table WITH Roads2 AS ( SELECT city1 AS from_city, city2 AS to_city, distance FROM dbo.Roads UNION ALL SELECT city2, city1, distance FROM dbo.Roads ), RoadPaths AS ( SELECT from_city, to_city, distance, CAST('.' + from_city + '.' + to_city + '.' AS VARCHAR(MAX)) AS path FROM Roads2 UNION ALL SELECT F.from_city, T.to_city, F.distance + T.distance, CAST(F.path + T.to_city + '.' AS VARCHAR(MAX)) FROM RoadPaths AS F JOIN Roads2 AS T ON CASE WHEN F.path LIKE '%.' + T.to_city + '.%' THEN 1 ELSE 0 END = 0 AND F.to_city = T.from_city ), RoadsMinDist AS ( SELECT from_city, to_city, MIN(distance) AS mindist FROM RoadPaths GROUP BY from_city, to_city ) SELECT RP.* INTO dbo.RoadPaths FROM RoadsMinDist AS RMD JOIN RoadPaths AS RP ON RMD.from_city = RP.from_city AND RMD.to_city = RP.to_city AND RMD.mindist = RP.distance; CREATE UNIQUE CLUSTERED INDEX idx_uc_from_city_to_city ON dbo.RoadPaths(from_city, to_city); GO -- Return shortest path between Los Angeles and New York SELECT * FROM dbo.RoadPaths WHERE from_city = 'LAX' AND to_city = 'JFK'; GO -- Listing 9-43: Creation Script for the fn_RoadsTC UDF IF OBJECT_ID('dbo.fn_RoadsTC') IS NOT NULL DROP FUNCTION dbo.fn_RoadsTC; GO CREATE FUNCTION dbo.fn_RoadsTC() RETURNS @RoadsTC TABLE ( uniquifier INT NOT NULL IDENTITY, from_city VARCHAR(3) NOT NULL, to_city VARCHAR(3) NOT NULL, distance INT NOT NULL, route VARCHAR(MAX) NOT NULL, PRIMARY KEY (from_city, to_city, uniquifier) ) AS BEGIN DECLARE @added AS INT; INSERT INTO @RoadsTC SELECT city1 AS from_city, city2 AS to_city, distance, '.' + city1 + '.' + city2 + '.' FROM dbo.Roads; SET @added = @@rowcount; INSERT INTO @RoadsTC SELECT city2, city1, distance, '.' + city2 + '.' + city1 + '.' FROM dbo.Roads; SET @added = @added + @@rowcount; WHILE @added > 0 BEGIN INSERT INTO @RoadsTC SELECT DISTINCT TC.from_city, R.city2, TC.distance + R.distance, TC.route + city2 + '.' FROM @RoadsTC AS TC JOIN dbo.Roads AS R ON R.city1 = TC.to_city WHERE NOT EXISTS (SELECT * FROM @RoadsTC AS TC2 WHERE TC2.from_city = TC.from_city AND TC2.to_city = R.city2 AND TC2.distance <= TC.distance + R.distance) AND TC.from_city <> R.city2; SET @added = @@rowcount; INSERT INTO @RoadsTC SELECT DISTINCT TC.from_city, R.city1, TC.distance + R.distance, TC.route + city1 + '.' FROM @RoadsTC AS TC JOIN dbo.Roads AS R ON R.city2 = TC.to_city WHERE NOT EXISTS (SELECT * FROM @RoadsTC AS TC2 WHERE TC2.from_city = TC.from_city AND TC2.to_city = R.city1 AND TC2.distance <= TC.distance + R.distance) AND TC.from_city <> R.city1; SET @added = @added + @@rowcount; END RETURN; END GO -- Return shortest paths and distances SELECT from_city, to_city, distance, route FROM (SELECT from_city, to_city, distance, route, RANK() OVER (PARTITION BY from_city, to_city ORDER BY distance) AS rk FROM dbo.fn_RoadsTC()) AS RTC WHERE rk = 1; GO -- Cleanup DROP TABLE dbo.RoadPaths; GO
https://www.microsoft.com/mspress/companion/0-7356-2313-9/
https://www.microsoftpressstore.com/store/inside-microsoft-sql-server-2008-t-sql-querying-9780735626034
--随机行 SELECT TOP(1) * FROM dbo.Employees ORDER BY RAND(); GO SELECT TOP(1) * FROM dbo.Employees ORDER BY checksum(NEWID()); GO
--------------------------------------------------------------------- -- Analytical Ranking Functions --------------------------------------------------------------------- -- Listing 4-3: Creating and Populating the Sales Table SET NOCOUNT ON; USE tempdb; GO IF OBJECT_ID('dbo.Sales') IS NOT NULL DROP TABLE dbo.Sales; GO CREATE TABLE dbo.Sales ( empid VARCHAR(10) NOT NULL PRIMARY KEY, mgrid VARCHAR(10) NOT NULL, qty INT NOT NULL ); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('A', 'Z', 300); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('B', 'X', 100); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('C', 'X', 200); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('D', 'Y', 200); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('E', 'Z', 250); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('F', 'Z', 300); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('G', 'X', 100); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('H', 'Y', 150); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('I', 'X', 250); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('J', 'Z', 100); INSERT INTO dbo.Sales(empid, mgrid, qty) VALUES('K', 'Y', 200); CREATE INDEX idx_qty_empid ON dbo.Sales(qty, empid); CREATE INDEX idx_mgrid_qty_empid ON dbo.Sales(mgrid, qty, empid); GO -- Rank and dense rank 排名和密集排名 SELECT empid, qty, (SELECT COUNT(*) FROM dbo.Sales AS S2 WHERE S2.qty < S1.qty) + 1 AS rnk, (SELECT COUNT(DISTINCT qty) FROM dbo.Sales AS S2 WHERE S2.qty < S1.qty) + 1 AS drnk FROM dbo.Sales AS S1 ORDER BY qty; --------------------------------------------------------------------- -- NTILE --------------------------------------------------------------------- --------------------------------------------------------------------- -- NTILE Function, SQL Server 2005 --------------------------------------------------------------------- -- NTILE SELECT empid, qty, NTILE(3) OVER(ORDER BY qty, empid) AS tile FROM dbo.Sales ORDER BY qty, empid; -- Descriptive Tiles SELECT empid, qty, CASE NTILE(3) OVER(ORDER BY qty, empid) WHEN 1 THEN 'low' WHEN 2 THEN 'meduim' WHEN 3 THEN 'high' END AS lvl FROM dbo.Sales ORDER BY qty, empid; -- Ranges of Quantities Corresponding to each Category WITH Tiles AS ( SELECT empid, qty, NTILE(3) OVER(ORDER BY qty, empid) AS tile FROM dbo.Sales ) SELECT tile, MIN(qty) AS lb, MAX(qty) AS hb FROM Tiles GROUP BY tile ORDER BY tile; --------------------------------------------------------------------- -- Other Solutions to NTILE --------------------------------------------------------------------- -- NTILE, even Distribution of Remainder DECLARE @numtiles AS INT; SET @numtiles = 3; SELECT empid, qty, CAST((rn - 1) / tilesize + 1 AS INT) AS tile FROM (SELECT empid, qty, rn, 1.*numrows/@numtiles AS tilesize FROM (SELECT empid, qty, (SELECT COUNT(*) FROM dbo.Sales AS S2 WHERE S2.qty < S1.qty OR S2.qty = S1.qty AND S2.empid <= S1.empid) AS rn, (SELECT COUNT(*) FROM dbo.Sales) AS numrows FROM dbo.Sales AS S1) AS D1) AS D2 ORDER BY qty, empid; GO -- NTILE, pre-2005, remainder added to first groups DECLARE @numtiles AS INT; SET @numtiles = 9; SELECT empid, qty, CASE WHEN rn <= (tilesize+1) * remainder THEN (rn-1) / (tilesize+1) + 1 ELSE (rn - remainder - 1) / tilesize + 1 END AS tile FROM (SELECT empid, qty, rn, numrows/@numtiles AS tilesize, numrows%@numtiles AS remainder FROM (SELECT empid, qty, (SELECT COUNT(*) FROM dbo.Sales AS S2 WHERE S2.qty < S1.qty OR S2.qty = S1.qty AND S2.empid <= S1.empid) AS rn, (SELECT COUNT(*) FROM dbo.Sales) AS numrows FROM dbo.Sales AS S1) AS D1) AS D2 ORDER BY qty, empid; GO
http://tsql.solidq.com/SampleContent/InsideTSQLQuerying2005_06.pdf
https://math.ucsd.edu/~crypto/Monty/montybg.html
哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)