具有挑战性的递归T-SQL查询

时间:2013-03-22 23:16:19

标签: sql-server tsql recursion

这是我的情景。假设我有两张桌子“Car”和“CarPart”。汽车由许多部件组成,每个部件可以属于多辆汽车。在我的情况下,一个复杂的问题是,每个部件都会获得一个新的PartID,即使它是相同的部件名称,但它只是属于不同的汽车。这是我无法控制的,所以请耐心等待。这是设置的脚本。

IF OBJECT_ID('Car') IS NOT NULL DROP TABLE Car
CREATE TABLE Car (
CarID INT,
CarName VARCHAR(16)
)

IF OBJECT_ID('CarPart') IS NOT NULL DROP TABLE CarPart
CREATE TABLE CarPart (
PartID INT,
PartName VARCHAR(16),
CarID INT
)

INSERT INTO Car
VALUES (1, 'Chevy'),
    (2, 'Ford'),
    (3, 'Toyota'),
    (4, 'Honda'),
    (5, 'Nissan'),
    (6, 'Hugo')

INSERT INTO CarPart 
VALUES  (110, 'Engine', 1),
  (120, 'Engine', 2),
  (210, 'Door', 1),
  (220, 'Door', 3),
  (310, 'Seat', 4),
  (320, 'Seat', 5),
  (410, 'Window', 3),
  (510, 'Wheel', 2),
  (420, 'Window', 6)

正如您所看到的,“引擎”部分属于“雪佛兰”和“福特”,并且列出了两次不同的ID。再次,这是我必须忍受的设计限制。

这是我需要完成的任务:给一辆车,我需要找到这辆车的所有零件以及这些零件所属的所有其他车型。我必须以递归的方式继续寻找零件和汽车,直到我到达链的末端。逻辑可以概述如下:@StartCar - > @StartCar的一部分 - >其他相同名称的部分 - >得到那些“其他”部分的Id - >获得“拥有”这些部件的汽车 - >重新开始并重复直到达到链的末尾。

为了解决我的问题,我尝试了这个查询:

DECLARE @StartCar VARCHAR(16) = 'Chevy'

;WITH cte (CarName, PartName)
AS
(
SELECT c.CarName,
       cp.PartName
FROM CarPart cp
JOIN Car c ON cp.CarID = c.CarID
WHERE c.CarName = @StartCar
UNION ALL
SELECT c.CarName,
       cp.PartName
FROM CarPart cp
JOIN Car c ON cp.CarID = c.CarID
JOIN cte cte ON cp.PartName = cte.PartName
)
SELECT CarName, PartName
FROM cte

然而,它进入无限循环并终止。我希望看到输出类似于:

CarName PartName
Chevy Engine
Chevy Door
Ford Engine
Ford Wheel
Toyota Door
Toyota Window
Hugo Window

我赞赏任何指示。

谢谢!

4 个答案:

答案 0 :(得分:2)

SQL Fiddle

查询1

declare @t table (
  car_name varchar(100), 
  part_name varchar(100)
  )

declare @car int = 3

insert @t
select c.CarName, p.PartName
from Car c join CarPart p on c.CarID = p.CarID
where c.CarID = @car


while exists(
  select c.CarName, p.PartName
  from Car c join CarPart p on c.CarID = p.CarID
  where c.CarName in (
    select c.CarName
    from Car c join CarPart p on c.CarID = p.CarID
    where p.PartName in (select part_name from @t)
      and c.CarName not in (select car_name from @t)
    )
) 
insert @t
  select c.CarName, p.PartName
  from Car c join CarPart p on c.CarID = p.CarID
  where c.CarName in (
    select c.CarName
    from Car c join CarPart p on c.CarID = p.CarID
    where p.PartName in (select part_name from @t)
      and c.CarName not in (select car_name from @t)
    )

select * from @t

<强> Results

| CAR_NAME | PART_NAME |
------------------------
|   Toyota |      Door |
|   Toyota |    Window |
|    Chevy |    Engine |
|    Chevy |      Door |
|     Hugo |    Window |
|     Ford |    Engine |
|     Ford |     Wheel |

答案 1 :(得分:2)

您的cte进入无限循环的原因是您没有定义层次结构。如果您绘制关系图,您将看到许多圆圈导致任何演练永远循环。

要解决这个问题,首先要创建一个层次结构。 在我的代码中,第一个cte car_hierarchy通过查找所有CarID对来做到这一点,但是限制左边的一对必须小于右边的对。有了它,你现在有了一个无圆的定向关系图。 (如果忽略方向,你仍然可以找到圆圈,但这与算法无关。)

第二步是找到给定汽车的所有亲属。因为给出的汽车可能不在最后层,所以这是一个两步过程。首先找到最左侧连接的汽车,然后找到从那里开始的所有连接汽车。下面的查询中的car_leftcar_right就是这样做的。

最后一步是取出ID并将汽车和零件名称拉回:

IF OBJECT_ID('dbo.Car') IS NOT NULL DROP TABLE dbo.Car
CREATE TABLE dbo.Car (
CarID INT,
CarName VARCHAR(16)
)

IF OBJECT_ID('dbo.CarPart') IS NOT NULL DROP TABLE dbo.CarPart
CREATE TABLE dbo.CarPart (
PartID INT,
PartName VARCHAR(16),
CarID INT
)

INSERT INTO dbo.Car
VALUES (1, 'Chevy'),
    (2, 'Ford'),
    (3, 'Toyota'),
    (4, 'Honda'),
    (5, 'Nissan'),
    (6, 'Hugo')

INSERT INTO dbo.CarPart 
VALUES  (110, 'Engine', 1),
  (120, 'Engine', 2),
  (210, 'Door', 1),
  (220, 'Door', 3),
  (310, 'Seat', 4),
  (320, 'Seat', 5),
  (410, 'Window', 3),
  (510, 'Wheel', 2),
  (420, 'Window', 6)


DECLARE @StartCarID INT = 1;

WITH 
car_hierachy (CarID1, CarID2) AS (
  SELECT DISTINCT
         cp1.CarID CarID1,
         cp2.CarID CarID2
  FROM dbo.CarPart cp1
  JOIN dbo.CarPart cp2
  ON cp1.PartName = cp2.PartName
  AND cp1.CarID < cp2.CarID
),
car_left(CarID) AS (
  SELECT @StartCarID 
  UNION ALL
  SELECT ch.CarID1
  FROM car_hierachy ch
  JOIN car_left cl
  ON cl.CarID = ch.CarID2
),
car_right(CarID) AS (
  SELECT MIN(CarID) 
  FROM car_left
  UNION ALL
  SELECT ch.CarID2
  FROM car_hierachy ch
  JOIN car_right cr
  ON cr.CarID = ch.CarID1
)
SELECT *
FROM car_right ac
JOIN dbo.Car c
ON ac.CarID = c.CarID
JOIN dbo.CarPart cp
ON c.CarID = cp.CarID
ORDER BY c.CarId, cp.PartId; 

SQLFiddle

这应该可以解决您的问题。但是我不确定它会表现不错。使用大型数据集,您实际上可能更好地使用循环。但是通过适当的索引,它可能会起作用。所以试一试。

(我将起步车从雪佛兰转到丰田,以表明它适用于层级中间的ars。如果你只从丰田向外走,你会想念福特。)

答案 2 :(得分:2)

您基本上遍历的图形不是非循环的,因此您必须明确避免循环。一种方法是跟踪图中的路径。这是应该工作的代码。您也可以使用SQL Server的HIERARCHYID数据类型来保存路径。

我选择将CTE作为汽车表而不是零件表。你的规则永远不会导致特定汽车的某些部件,但不是所有部件,所以这看起来更简单。

WITH cte(CarID,hier) AS (
  SELECT CarID, CAST('/'+LTRIM(CarID)+'/' AS varchar(max))
  FROM Car
  WHERE CarName = @StartCar

  UNION ALL

  SELECT c2.CarID, hier+LTRIM(c2.CarID)+'/'
  FROM Car AS c
  JOIN cte ON cte.CarID = c.CarID
  JOIN CarPart AS c1 ON c.CarID = c1.CarID
  JOIN CarPart AS c2 ON c2.PartName = c1.PartName
  WHERE hier NOT LIKE '%/'+LTRIM(c2.CarID)+'/%'
)

SELECT
  c.CarName, cp.PartName
FROM Car AS c
JOIN CarPart AS cp ON cp.CarID = c.CarID
JOIN cte on cte.CarID = c.CarID

答案 3 :(得分:0)

看起来您需要一个junction table,以便您拥有带有序列号 - >汽车零件名称的car-&gt;汽车零件。 CarPart表中不应包含部件序列号。