多个左联接-保持返回的行递减计数?

Sea*_*n U 5 sql t-sql sql-server

我试图弄清楚如何最好地查询由一个中央表以及记录一对多关系的多个“属性”表(抱歉,这里不确定最好的术语)组成的架构。在业务层中,这些表中的每一个对应于一个可能包含零个或多个元素的集合。

现在,我正在查看的代码通过从主表中获取值列表来检索数据,然后对其进行循环并查询每个“附件”表以填充这些集合。

如果可以的话,我想尝试将其简化为单个查询。我尝试使用多个LEFT JOIN。但这有效地连接了附件表中值的叉积,从而导致行激增-尤其是当您添加更多连接时。该表包括五个此类关系,因此每个记录返回的行数可能很大,并且几乎完全由冗余数据组成。

这是一些表,数据,我正在使用的查询结构和结果的较小综合示例:

数据库结构和数据:

create table Containers (
  Id int not null primary key,
  Name nvarchar(8) not null);

create table Containers_Animals (
  Container int not null references Containers(Id),
  Animal nvarchar(8) not null,
  primary key (Container, Animal)
  );

create table Containers_Foods (
  Container int not null references Containers(Id),
  Food nvarchar(8) not null,
  primary key (Container, Food)
  );

insert into Containers (Id, Name) 
  values (0, 'box'), (1, 'sack'), (2, 'bucket');

insert into Containers_Animals (Container, Animal)
  values (1, 'monkey'), (2, 'dog'), (2, 'whale'), (2, 'lemur'); 

insert into Containers_Foods (Container, Food)
  values (1, 'lime'), (2, 'bread'), (2, 'chips'), (2, 'apple'), (2, 'grape');
Run Code Online (Sandbox Code Playgroud)

耦合到这样的业务对象:

class Container {
    public string Name;
    public string[] Animals;  // may be empty
    public string[] Foods;    // may be empty
}
Run Code Online (Sandbox Code Playgroud)

这是我针对它构造查询的方式:

select c.Name container, a.Animal animal, f.Food food from Containers c
  left join Containers_Animals a on a.Container = c.Id
  left join Containers_Foods f on f.Container = c.Id;
Run Code Online (Sandbox Code Playgroud)

得到以下结果:

container animal   food
--------- -------- --------
box       NULL     NULL
sack      monkey   lime
bucket    dog      apple
bucket    dog      bread
bucket    dog      chips
bucket    dog      grape
bucket    lemur    apple
bucket    lemur    bread
bucket    lemur    chips
bucket    lemur    grape
bucket    whale    apple
bucket    whale    bread
bucket    whale    chips
bucket    whale    grape
Run Code Online (Sandbox Code Playgroud)

我想看到的是,行数等于与任何关系上的根表关联的值的最大数量,并用NULL填充空白空间。这样可以使返回的行数,返回值,返回值保持向下,同时仍易于转换为对象。像这样:

container animal   food
--------- -------- --------
box       NULL     NULL
sack      monkey   lime
bucket    dog      apple
bucket    lemur    bread
bucket    whale    chips
bucket    NULL     grape
Run Code Online (Sandbox Code Playgroud)

能做到吗

Eri*_*ikE 4

为什么不直接返回按容器排序的两个数据集,然后在客户端对它们进行逻辑合并连接呢?您所要求的是让数据库引擎做更多的工作,进行更复杂的查询,(对我来说)有一点好处。

它看起来像这样。使用两个左连接确保每个数据集至少具有所有容器名称的一个实例,然后同时循环遍历它们。这是一些粗略的伪代码:

Dim CurrentContainer
If Not Animals.Eof Then
   CurrentContainer = Animals.Container
End If
Do While Not Animals.Eof Or Not Foods.Eof
   Row = New Couplet(AnimalType, FoodType);
   If Animals.Animal = CurrentContainer Then
      Row.AnimalType = Animals.Animal
      Animals.MoveNext
   End If
   If Foods.Container = CurrentContainer Then
      Row.FoodType = Foods.Food
      Foods.MoveNext
   End If
   If Not Animals.Eof AndAlso Animals.Container <> CurrentContainer _
      AndAlso Not Foods.Eof AndAlso Foods.Container <> CurrentContainer Then
      CurrentContainer = [Container from either non-Eof recordset]
   EndIf
   'Process the row, output it, put it in a stack, build a new recordset, whatever.
Loop
Run Code Online (Sandbox Code Playgroud)

但是,您所要求的当然是可能的!这里有两种方法。

  1. 单独处理输入并连接它们的位置:

    WITH CA AS (
        SELECT *,
            Row_Number() OVER (PARTITION BY Container ORDER BY Animal) Pos
        FROM Containers_Animals
    ), CF AS (
        SELECT *,
            Row_Number() OVER (PARTITION BY Container ORDER BY Food) Pos
        FROM Containers_Foods
    )
    SELECT
        C.Name,
        CA.Animal,
        CF.Food
    FROM
        Containers C
        LEFT JOIN (
            SELECT Container, Pos FROM CA
            UNION SELECT Container, Pos FROM CF
        ) P ON C.Id = P.Container
        LEFT JOIN CA
            ON C.Id = CA.Container
            AND P.Pos = CA.Pos
        LEFT JOIN CF
            ON C.Id = CF.Container
            AND P.Pos = CF.Pos;
    
    Run Code Online (Sandbox Code Playgroud)
  2. 垂直连接输入并旋转它们:

    WITH FoodAnimals AS (
        SELECT
            C.Name,
            1 Which,
            CA.Animal Item,
            Row_Number() OVER (PARTITION BY C.Id ORDER BY (CA.Animal)) Pos
        FROM
            Containers C
            LEFT JOIN Containers_Animals CA
                ON C.Id = CA.Container
        UNION
        SELECT
            C.Name,
            2 Which,
            CF.Food,
            Row_Number() OVER (PARTITION BY C.Id ORDER BY (CF.Food)) Pos
        FROM
            Containers C
            LEFT JOIN Containers_Foods CF
                ON C.Id = CF.Container
    )
    SELECT
        P.Name,
        P.[1] Animal,
        P.[2] Food
    FROM
        FoodAnimals FA
        PIVOT (Max(Item) FOR Which IN ([1], [2])) P;
    
    Run Code Online (Sandbox Code Playgroud)