Самостоятельное соединение с запросом внутреннего и внешнего соединения

У меня есть таблица, настроенная так, что один столбец (атрибут) содержит такую ​​информацию, как имя, фамилия, номер учетной записи и любую другую информацию, связанную с вещью в базе данных. Другой столбец (attributeType) содержит число, указывающее, что такое атрибут, например. 1 может быть именем, 2 фамилией и 3 номером счета и т. д. Существует еще один столбец (конечная дата), указывающий, является ли запись текущей, имея там дату. Обычно он будет установлен на 9999 год, если текущий, и на некоторую дату в прошлом в противном случае. Все данные, описывающие одно и то же, имеют уникальное значение и в другом столбце (объекте), так что каждая запись с одним и тем же номером в столбце объекта будет описывать одного человека. Например.

entity  attribute  attributetype  enddate
------  ---------  -------------  --------
1       ben        1              9999-1-1
1       alt        2              9999-1-1
1       12345      3              9999-1-1
2       sam        1              9999-1-1
2       smith      2              9999-1-1
2       98765      3              1981-1-1

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

select ta1.attribute '1st Name', ta2.attribute 'last name'
from tblAccount ta1
inner join tblAccount ta2 on ta1.entity = ta2.entity
where ta1.attribute = 'sam' and ta2.attribute = 'smith'
      and ta1.attributetype = 1 and ta2. attributetype = 2
      and ta1.enddate > getdate() and ta2.enddate > getdate()

и он выводит имя и фамилию, как и ожидалось, но когда я хочу включить столбец номера счета, я ничего не получаю:

select ta1.attribute '1st Name', ta2.attribute 'last name', ta3.attribute 'account#'
from tblAccount ta1
inner join tblAccount ta2 on ta1.entity = ta2.entity
left join tblAccount ta3 on ta1.entity = ta3.entity
where ta1.attribute = 'sam' and ta2.attribute = 'smith'
      and ta1.attributetype = 1 and ta2. attributetype = 2
      and ta1.enddate > getdate() and ta2.enddate > getdate()
      and ta3.attributetype = 3 and ta3.enddate > getdate()

То, что я хотел бы видеть, это вывод имени и фамилии без ничего в столбце account# в приведенном выше случае, когда он не является текущим. Что я делаю не так и как исправить этот запрос?


person Ban Atman    schedule 07.03.2013    source источник
comment
Вы не просто публикуете то, что хотите, в качестве окончательного набора результатов.   -  person Praveen Nambiar    schedule 07.03.2013
comment
Если вы вводите критерии в предложении where для ta3, это также может быть inner join.   -  person Jeff Rosenberg    schedule 07.03.2013
comment
Во-первых, по вашему второму запросу - нет необходимости в этом внутреннем соединении.   -  person Praveen Nambiar    schedule 07.03.2013
comment
Этот тип структуры данных представляет собой классический антишаблон SQL под названием Entity-Attribute-Value, и если у вас есть какой-либо контроль над базой данных, я бы рекомендовал вам не использовать его. См. dba .stackexchange.com/questions/20759/ для получения дополнительной информации.   -  person Simon Kingston    schedule 07.03.2013
comment
@Simon любит курсоры, динамический SQL и функции, EAV — это не дьявол, и я бы рекомендовал вам не делать общих заявлений.   -  person Aaron Bertrand    schedule 07.03.2013
comment
Попробуйте переместить ta3.attributetype = 3 и ta3.enddate › getdate() в конец предложения LEFT JOIN вместо предложения WHERE.   -  person Simon Kingston    schedule 07.03.2013
comment
@AaronBertrand Достаточно честно. Я внесу поправку в свое заявление, чтобы сказать, что мой опыт работы с этим типом структуры был болезненным, и я бы не рекомендовал его, если это не является абсолютно необходимым.   -  person Simon Kingston    schedule 07.03.2013
comment
@PraveenNambiar - как вы обходите внутреннее соединение?   -  person Ban Atman    schedule 07.03.2013
comment
@SimonKingston На самом деле, это может быть исключительный случай, когда имеет смысл использовать этот способ настройки. На самом деле я не имею дело со счетами, но упростил свою проблему, представив ее таким образом. Мы имеем дело со многими базами данных в разных местах, каждая из которых имеет общую тему и разные требования, которые могут меняться из года в год. Сделать это любым обычным способом означало бы иметь, скажем, 50 баз данных для индивидуального понимания вместо одной общей конструкции. Не то, чтобы я мог претендовать на экспорт в этих вопросах.   -  person Ban Atman    schedule 07.03.2013


Ответы (2)


Вы должны переместить сравнение дат в условие соединения:

select ta1.attribute '1st Name'
    , ta2.attribute 'last name'
    , ta3.attribute 'account#'
from tblAccount ta1
inner join tblAccount ta2 
    on ta1.entity = ta2.entity
     and ta1.attributetype = 1 and ta2. attributetype = 2
     and ta1.enddate > getdate() and ta2.enddate > getdate()
left join tblAccount ta3 on ta1.entity = ta3.entity
      and ta3.attributetype = 3 and ta3.enddate > getdate()
where ta1.attribute = 'sam' and ta2.attribute = 'smith'

Когда он находится в предложении where, он сравнивает getdate() с NULL, если нет учетной записи, которая возвращает NULL. Так что никакой записи.

РЕДАКТИРОВАТЬ:

В ответ на обоснованную озабоченность по поводу нескольких действительных записей и для того, чтобы сделать код немного более удобным для обслуживания:

DECLARE @FNAME VARCHAR(50) = 'sam'
    , @LNAME VARCHAR(50) = 'smith'
    , @now DATETIME2(7) = GETDATE();

SELECT 
    name.[1st Name]
    , name.[last name]
    , name.entity
    , 
        (
            select 
                top 1 
                ta3.attribute
            FROM tblAccount ta3 
            WHERE 
                ta3.entity = name.entity
                and 
                ta3.attributetype = 3 
                and 
                ta3.enddate > @now
            ORDER BY 
                ta3.enddate 
        )
FROM 
    (        
        select 
            ta1.attribute '1st Name'
            , ta2.attribute 'last name'
            , ta.entity
            , ROW_NUMBER()
                OVER(
                    PARTITION BY 
                        ta1.entity
                    ORDER BY 
                        ta1.enddate
                    ) r
        from 
            tblAccount ta1
        inner join tblAccount ta2 
            on 
            ta1.entity = ta2.entity
            and 
            ta2. attributetype = 2
            and 
            ta2.enddate > @now
            and 
            ta2.attribute = @LNAME
        where 
            ta1.attributetype = 1 
            and 
            ta1.attribute = @fname 
            and 
            ta1.enddate > @now
    ) name
WHERE    
    NAME.r = 1

;

Этот код работает с подразумеваемыми предположениями об одной сущности на имя/фамилию и ровно на одну дату окончания после времени выполнения. Переменные немного более удобны для хранимых процессов и позволяют вам изменить дату «на момент». И если вы застряли с EAV, вам, вероятно, понадобятся сохраненные процедуры. Я беру первую запись, заканчивающуюся после рассматриваемой даты, исходя из предположения, что любые более поздние записи должны быть действительны только после истечения срока действия этой. Может быть, это излишество, поскольку это выходит за рамки вопроса ОП, но это верная точка зрения.

Я говорю «застрял с EAV». Хотя EAV не всегда плохо; никто не стреляет в спину. В любом случае, вам лучше иметь веское обоснование, если вы хотите, чтобы дело прошло мимо присяжных. В шаблоне хранения NoSQL это нормально, но EAV обычно является плохим шаблоном реализации для парадигмы СУБД.

Хотя из более позднего комментария ОП, похоже, что он нашел одну из лучших причин.

person JAQFrost    schedule 07.03.2013
comment
Ссылка +1 на внешнюю таблицу в предложении WHERE превращает внешнее соединение во внутреннее. - person Aaron Bertrand; 07.03.2013
comment
Большое спасибо. Это сработало. Надеюсь, это будет прямо в следующий раз, когда мне нужно будет сделать что-то подобное. И спасибо @AaronBertrand за разработку. - person Ban Atman; 07.03.2013
comment
@Jason: что, если в базе данных есть записи с истекшим сроком действия, в чем смысл этой необычной структуры? - person Pieter Geerkens; 07.03.2013
comment
Записи с истекшим сроком действия не будут отображаться, что, по-видимому, является целью сравнения с getdate(). - person JAQFrost; 07.03.2013

Каждый атрибут фактически является отдельной сущностью в этой модели, но все они совместно используют одно и то же хранилище в одной и той же физической таблице (почему?). Это дает:

with data as (
   select entity = 1, attribute = 'ben',   attributeType=1, enddate = convert(datetime,'99990101') union all
   select entity = 1, attribute = 'alt',   attributeType=2, enddate = convert(datetime,'99990101') union all
   select entity = 1, attribute = '12345', attributeType=3, enddate = convert(datetime,'99990101') union all
   select entity = 2, attribute = 'sam',   attributeType=1, enddate = convert(datetime,'99990101') union all
   select entity = 2, attribute = 'smith', attributeType=2, enddate = convert(datetime,'99990101') union all
   select entity = 2, attribute = '67890', attributeType=3, enddate = convert(datetime,'99990101') union all
   select entity = 2, attribute = '68790', attributeType=3, enddate = convert(datetime,'20130331') union all
   select entity = 2, attribute = '876', attributeType=3, enddate = convert(datetime,'19810101') 
) 
select top 1
    FirstName, LastName, AccountNum
from (
  select top 1 
    a1.entity, FirstName, LastName
  from (
    select entity, enddate, attribute as FirstName
    from data d 
    where d.enddate >= getdate()
      and attributeType = 1
  ) a1
  join (
    select entity, enddate, attribute as LastName
    from data 
    where enddate >= getdate()
      and attributeType = 2
  ) a2 on a1.entity = a2.entity
     and a1.enddate = a2.enddate
  where FirstName = 'sam' and LastName = 'smith'
    and a1.enddate >= getdate() and a2.enddate >= getdate()
  order by a1.enddate
) E
left join (
  select entity, enddate, attribute as AccountNum
  from data 
  where enddate >= getdate()
    and attributeType = 3
) a3 on a3.entity = E.entity
order by a3.enddate

возвращение:

FirstName LastName AccountNum
--------- -------- ----------
sam       smith    68790

Обратите внимание, что для бухгалтерии довольно часто, по крайней мере, вводить будущие транзакции в спокойное время месяца, особенно если эти транзакции вступят в силу в загруженное время месяца (т.е. в конце месяца). То же самое для годовых сделок. Не следует предполагать, что может существовать только одна запись со сроком действия > getdate().

person Pieter Geerkens    schedule 07.03.2013
comment
Мне нравится соображение о нескольких допустимых записях, хотя ваша логика не будет правильно фильтровать. Ваши подзапросы занимают первое место по дате окончания без фильтрации для соответствующего объекта. - person JAQFrost; 07.03.2013
comment
Это не оптимизация, это логическая ошибка. Верхний заказ по дате окончания — это последний введенный тип атрибута. Таким образом, вы получаете последний тип 1, последний тип 2, последний тип 3 и надеетесь, что это одна и та же сущность, которую зовут Сэм Смит. В противном случае вы возвращаете NULL. - person JAQFrost; 07.03.2013
comment
@ Джейсон: понял. Позвольте мне задуматься на минуту. - person Pieter Geerkens; 07.03.2013
comment
Вы по-прежнему делаете первые 1 по дате окончания в подзапросе a3 без фильтра сущности. - person JAQFrost; 08.03.2013
comment
Я фильтрую по имени/фамилии в E и присоединяю a3 к E в экземпляре объекта, который соответствует всем критериям в подзапросе E. Что еще нужно? - person Pieter Geerkens; 08.03.2013
comment
Верхний 1 в подзапросе a3 вернет одну произвольную строку из всех строк типа 3 с ближайшей конечной датой после или равной текущему времени. Если эта единственная строка не относится к тому же объекту, что и в E, вы показываете NULL для учетной записи. - person JAQFrost; 08.03.2013
comment
@ Джейсон: ХОРОШО. Я перенесу предложение соединения в подзапрос a3, чтобы исправить это. - person Pieter Geerkens; 08.03.2013
comment
давайте продолжим это обсуждение в чате - person JAQFrost; 08.03.2013