Несколько левых соединений - ведение обратного отсчета строк? - PullRequest
4 голосов
/ 23 февраля 2012

Я пытаюсь выяснить, как лучше всего выполнять запросы к схеме, состоящей из одной центральной таблицы, плюс несколько таблиц «атрибутов» (извините, не уверен в лучшей терминологии здесь), которые записывают один-ко-многимотношения.На бизнес-уровне каждая из этих таблиц соответствует коллекции, которая может содержать ноль или более элементов.

Прямо сейчас код, на который я смотрю, получает данные, получая список значений из основной таблицы,затем цикл по нему и запрос к каждой из «вспомогательных» таблиц для заполнения этих коллекций.

Я хотел бы попытаться свести его к одному запросу, если смогу.Я пытался использовать несколько 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');

В привязке к бизнес-объекту, как это:

class Container {
    public string Name;
    public string[] Animals;  // may be empty
    public string[] Foods;    // may be empty
}

И вот способ, которым ястрою запрос против него:

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;

, который дает следующие результаты:

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

Вместо этого я хотел бы видеть количество строк, равное максимальному числузначения, связанные с корневой таблицей в любом из отношений, с пустым пространством, заполненным значениями NULL.Это позволило бы сохранить количество возвращаемых строк в обратном порядке, при этом их легко преобразовать в объекты.Примерно так:

container animal   food
--------- -------- --------
box       NULL     NULL
sack      monkey   lime
bucket    dog      apple
bucket    lemur    bread
bucket    whale    chips
bucket    NULL     grape

Можно ли это сделать?

Ответы [ 3 ]

4 голосов
/ 23 февраля 2012

Почему бы просто не вернуть два набора данных, упорядоченных по контейнерам, а затем выполнить логическое объединение слиянием на них в клиенте?То, о чем вы просите, заставит механизм БД выполнять гораздо больше работы, с гораздо более сложным запросом, для (для меня) небольшой выгоды.

Это будет выглядеть примерно так.Используйте два левых объединения, чтобы убедиться, что в каждом наборе данных есть хотя бы один экземпляр всех имен контейнеров, а затем выполните их одновременный цикл.Вот примерный псевдокод:

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

Однако, конечно, вы просите, чтобы возможно !Вот два способа:

  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;
    
  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;
    
0 голосов
/ 23 февраля 2012
WITH
  ca_ranked AS (
    SELECT
      *,
      rnk = ROW_NUMBER() OVER (PARTITION BY Container ORDER BY Animal)
    FROM Containers_Animals
  ),
  cf_ranked AS (
    SELECT
      *,
      rnk = ROW_NUMBER() OVER (PARTITION BY Container ORDER BY Food)
    FROM Containers_Foods
  )
SELECT
  container = c.Name,
  animal    = ca.Animal,
  food      = cf.Food
FROM ca_ranked ca
  FULL  JOIN cf_ranked cf ON ca.Container = cf.Container AND ca.rnk = cf.rnk
  RIGHT JOIN Containers c ON c.Id = COALESCE(ca.Container, cf.Container)
;
0 голосов
/ 23 февраля 2012
; with a as (
    select ID, c.Name container, a.Animal animal
    , r=row_number()over(partition by c.ID order by a.Animal)
    from Containers c
      left join Containers_Animals a on a.Container = c.Id
)
, b as (
    select ID, c.Name container, f.Food food
    , r=row_number()over(partition by c.ID order by f.Food)
    from Containers c
      left join Containers_Foods f on f.Container = c.Id

)
select a.container, a.animal, b.food
from a
left join b on a.container=b.container and a.r=b.r
union
select b.container, a.animal, b.food
from b
left join a on a.container=b.container and a.r=b.r
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...