SQL-запрос — подзапросы при объединении

вступление

У меня есть сложный запрос, который мне нужно написать (для БД Oracle). Я попытался провести аналогию, чтобы избежать использования данных компании, поэтому дайте мне знать, если что-то покажется странным.

Столы

КОШКА

  • CAT_ID (ПК)

КОТЕНОК

  • КОТЕНОК_ID (ПК)

CAT_KITTEN_PLAY

  • PLAY_ID (ПК)
  • CAT_ID (FK в таблицу CAT)
  • KITTEN_ID (от FK до таблицы KITTEN)
  • KITTEN_GENDER (строка — М или Ж)
  • PLAY_TIME (дата)

КОТЕНОК_INFO

  • KITTEN_ID (от FK до таблицы KITTEN)
  • ИМЯ (Строка, например, "Имя")
  • ЗНАЧЕНИЕ (строка, например, "Герцогиня")

Описание

Таким образом, таблицы CAT и KITTEN просто содержат идентификаторы и некоторую другую случайную информацию. KITTEN_INFO содержит имена котят (среди прочего). А CAT_KITTEN_PLAY регистрирует все случаи, когда кошка играет с котенком.

Что мне нужно, так это написать запрос, который возвращает всех КОШЕК, чье последнее время игры с КОТЕНКОМ мужского пола было в определенном диапазоне дат. Давайте назовем его с 1 января 2011 года по 31 января 2011 года. Мне также нужно указать дату игры и имя последнего котенка и котенка, с которыми играла кошка.

Уже

Ниже то, что у меня есть до сих пор:

SELECT cat.*, lastMale.PLAY_TIME maleTime, lastFemale.PLAY_TIME femaleTime, maleName.VALUE male, femaleName.VALUE female
FROM CAT cat
LEFT JOIN CAT_KITTEN_PLAY lastMale ON lastMale.CAT_ID = cat.CAT_ID 
                                  AND lastMale.GENDER = 'M'
LEFT JOIN CAT_KITTEN_PLAY lastFemale ON lastFemale.CAT_ID = cat.CAT_ID 
                                    AND lastFemale.GENDER = 'F'
LEFT JOIN KITTEN_INFO femaleName ON femaleName.KITTEN_ID = lastFemale.KITTEN_ID 
                                AND maleName.NAME = 'Name'
LEFT JOIN KITTEN_INFO maleName ON maleName.KITTEN_ID = lastMale.KITTEN_ID 
                              AND femaleName.NAME = 'Name'
    WHERE lastMale.PLAY_TIME BETWEEN '01-JAN-2011 12:00:00 AM'
                                 AND '31-JAN-2011 11:59:59 PM'

Проблемы

Это не учитывает тот факт, что кошка могла играть с котенком женского пола более одного раза. Итак, я бы хотел добавить «AND lastMale.PLAY_TIME = (SELECT MAX(PLAY_TIME) FROM CAT_KITTEN_PLAY, WHERE CAT_ID = cat.ID AND KITTEN_GENDER = 'M')» в первое соединение и аналогичный для второго соединения. Но подзапросы не разрешены для соединений.

Какие-нибудь мысли? Обратите внимание, что кошка, возможно, никогда не играла с котенком женского пола. Но кошка все равно должна быть включена, если они соответствуют критериям (отсюда присоединяется левый).


person Snowy Coder Girl    schedule 28.06.2011    source источник
comment
Примечание: добавление AND lastFemale.PLAY_TIME = (SELECT MAX(PLAY_TIME) WHERE ... и т. д. в конец не сработает, так как это удалит всех кошек, которые не играли с котенком женского пола.   -  person Snowy Coder Girl    schedule 28.06.2011
comment
Почему KITTEN_GENDER хранится в CAT_KITTEN_PLAY? Разве это не часть KITTEN_INFO?   -  person Andriy M    schedule 28.06.2011
comment
Помните, аналогия. В моей модели больше смысла.   -  person Snowy Coder Girl    schedule 28.06.2011


Ответы (3)


Аналитические функции, вероятно, лучший ответ на вашу проблему:

SELECT DISTINCT
       cat.*, 
       first_value(lastMale.PLAY_TIME) 
          over (partition by cat.id 
                order by lastMale.PLAY_TIME desc nulls last) maleTime,
       first_value(lastFemale.PLAY_TIME) 
          over (partition by cat.id 
                order by lastFemale.PLAY_TIME desc nulls last) femaleTime,
       first_value(maleName.VALUE)  
          over (partition by cat.id 
                order by lastMale.PLAY_TIME desc nulls last) male,
       first_value(femaleName.VALUE)  
          over (partition by cat.id 
                order by lastFemale.PLAY_TIME desc nulls last) female
...
person Allan    schedule 28.06.2011
comment
Если я сделаю это, и любое из значений PLAY_TIME будет пустым, все значения в столбце вернутся пустыми. Может быть, потому что он всегда хватает первым, независимо от идентификатора кота? - person Snowy Coder Girl; 28.06.2011
comment
Да, похоже, что все значения femaleTime равны первому значению (пусто или нет). - person Snowy Coder Girl; 29.06.2011
comment
См. следующее. Столбцы всегда содержат одно и то же значение: gerardnico.com/wiki/database/oracle/first_value - person Snowy Coder Girl; 29.06.2011
comment
Причина, по которой вы видели пустые столбцы, заключается в том, что я указал desc без nulls last (см. редактирование). Добавление partition by дает вам значение выше указанного условия. Без SSCCE сложно предоставить полностью протестированное решение. - person Allan; 29.06.2011
comment
Круто, вроде работает. Спасибо большое. Надеюсь, я смогу распространить это на весь запрос. Который содержит около миллиарда соединений. Проклятые дизайнеры БД. Ржунимагу. Спасибо еще раз. Вы очень облегчили мне жизнь. знак равно - person Snowy Coder Girl; 29.06.2011
comment
+1 вау, first_value действительно хорош. Вы случайно не знаете, когда он был введен - person Conrad Frix; 29.06.2011
comment
Аналитические функции (например, first_value) появились в версии 8i. Есть одно предостережение: они не всегда работают хорошо, потому что должны работать со всем набором результатов, но для некоторых вещей, которые они делают (особенно lag и lead), они незаменимы. - person Allan; 29.06.2011

Просто время последней игры, а не имена котят:

SELECT cat.CAT_ID
     , MAX(play.PLAY_TIME) AS maleTime
     , ( SELECT MAX(playf.PLAY_TIME)
         FROM CAT_KITTEN_PLAY AS playf
         WHERE playf.CAT_ID = cat.CAT_ID
           AND playf.KITTEN_GENDER = 'F'
       ) AS femaleTime
FROM CAT AS cat
  JOIN CAT_KITTEN_PLAY AS play
    ON play.CAT_ID = cat.CAT_ID
WHERE play.KITTEN_GENDER = 'M'
GROUP BY cat.CAT_ID 
HAVING MAX(play.PLAY_TIME) BETWEEN '01-JAN-2011 12:00:00 AM'
                               AND '31-JAN-2011 11:59:59 PM'

Совсем не уверен, что это сработает:

SELECT cat.CAT_ID
     , lastplayM.PLAY_TIME AS maleTime
     , lastplayF.PLAY_TIME AS femaleTime
     , kittenM.VALUE AS male
     , kittenF.VALUE AS female
FROM CAT AS cat
  JOIN 
    ( SELECT CAT_ID
           , KITTEN_GENDER
           , KITTEN_ID
           , MAX(PLAY_TIME) OVER(PARTITION BY CAT_ID, KITTEN_GENDER) AS PLAY_TIME
      FROM CAT_KITTEN_PLAY
      WHERE PLAY_TIME = MAX(PLAY_TIME) OVER(PARTITION BY CAT_ID, KITTEN_GENDER)
    ) AS lastplayM
    ON lastplayM.CAT_ID = cat.CAT_ID
    AND lastplayM.KITTEN_GENDER = 'M'
    AND lastplayM.PLAY_TIME BETWEEN '01-JAN-2011 12:00:00 AM'
                               AND '31-JAN-2011 11:59:59 PM'
  JOIN KITTEN_INFO AS kittenM
    ON kittenM.KITTEN_ID = lastplayM.KITTEN_ID

  LEFT JOIN 
    ( SELECT CAT_ID
           , KITTEN_GENDER
           , KITTEN_ID
           , MAX(PLAY_TIME) OVER(PARTITION BY CAT_ID, KITTEN_GENDER) AS PLAY_TIME
      FROM CAT_KITTEN_PLAY
      WHERE PLAY_TIME = MAX(PLAY_TIME) OVER(PARTITION BY CAT_ID, KITTEN_GENDER)
    ) AS lastplayF
    ON lastplayF.CAT_ID = cat.CAT_ID
    AND lastplayF.KITTEN_GENDER = 'F'
  JOIN KITTEN_INFO AS kittenF
    ON kittenF.KITTEN_ID = lastplayF.KITTEN_ID
person ypercubeᵀᴹ    schedule 28.06.2011
comment
Как бы вы добавили имена тогда? Кажется, некоторые ссылки утеряны. - person Snowy Coder Girl; 29.06.2011
comment
Спасибо за предложение. Я выбрал решение FIRST_VALUE Аллана. На мой взгляд немного понятнее. Но спасибо за труд =) - person Snowy Coder Girl; 29.06.2011

Вы можете использовать предложение WITH, чтобы сделать это несколько проще.

WITH MAX_MALE_KITTEN AS
(

     SELECT
        CAT_ID,
        MAX(PLAY_TIME) PLAY_TIME
     FROM
        CAT_KITTEN_PLAY
     WHERE
        KITTEN_GENDER = 'M'
     GROUP BY
        CAT_ID
),
MAX_FEMALE_KITTEN AS
(

     SELECT
        CAT_ID,
        MAX(PLAY_TIME) PLAY_TIME
     FROM
        CAT_KITTEN_PLAY
     WHRE
        KITTEN_GENDER = 'F'
     GROUP BY
        CAT_ID
)

SELECT
    C.CAT_ID,
    F_K_I.NAME LastFemaleName,
    F_PLAY_INFO.PLAY_TIME LastFemalePlayTime,
    M_K_I.NAME LastMaleName,
    M_PLAY_INFO.PLAY_TIME LastMalePlayTime

FROM
    CAT C
    LEFT JOIN CAT_KITTEN_PLAY F_PLAY_INFO
    ON C.CAT_ID = F_PLAY_INFO.CAT_ID
    AND F_PLAY_INFO.KITTEN_GENER= 'F'
    LEFT JOIN MAX_FEMALE_KITTEN M_F_K
    ON F_PLAY_INFO.CAT_ID = M_F_K.CAT_ID
    AND F_PLAY_INFO.PLAY_TIME = M_F_K.PLAY_TIME
    LEFT JOIN KITTEN_INFO F_K_I
    ON F_PLAY_INFO.KITTEN_ID = F_K_I.KITTEN_ID

    INNER JOIN CAT_KITTEN_PLAY M_PLAY_INFO
    ON C.CAT_ID = M_PLAY_INFO.CAT_ID
    AND M_PLAY_INFO.KITTEN_GENER= 'M'
    LEFT JOIN MAX_MALE_KITTEN M_F_K
    ON M_PLAY_INFO.CAT_ID = M_F_K.CAT_ID
    AND M_PLAY_INFO.PLAY_TIME = M_F_K.PLAY_TIME
    LEFT JOIN KITTEN_INFO M_K_I
    ON M_PLAY_INFO.KITTEN_ID = M_K_I.KITTEN_ID
WHERE 
       MAX_MALE_KITTEN.PLAY_TIME    BETWEEN '01-JAN-2011 12:00:00 AM'
                           AND '31-JAN-2011 11:59:59 PM
person Conrad Frix    schedule 28.06.2011