SQL SELECT: объединение и группировка данных между тремя таблицами с использованием подзапросов

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

У меня есть три таблицы базы данных:

TABLE A:  
AID PK  
STATUS VARCHAR

TABLE B:  
BID PK  
AID FK  
CID FK

TABLE C:  
CID PK  
CREATIONTIME DATE

Для каждой строки STATUS = 'OK' в таблице A я хочу найти соответствующую строку в C, которая имеет самое последнее время создания.

Сначала я могу получить все строки из таблицы A, где STATUS = 'OK'.
Затем я могу получить все соответствующие строки из таблицы B.
Но как продолжить оттуда?

Например:

select AID, CID from B where AID in (select AID from A where STATUS = 'OK')

может вернуть что-то вроде:

AID, CID  
1    1  
2    2  
2    3  
3    4  
4    5  
4    6  

Предположим, что CID 2 имеет более позднее время создания, чем CID 3, а CID 6 новее, чем CID 5. Это означает, что правильным результатом будут строки 1, 2, 4 и 6 в таблице C.

Есть ли способ выразить это с помощью запроса?

РЕДАКТИРОВАТЬ: Извините, что я не был достаточно конкретным. Я хочу получить CID из таблицы C.

РЕДАКТИРОВАТЬ: я посчитал возвращенные строки с различными решениями. Результаты были очень интересными и разнообразными:
HAINSTECH: 298 473 ряда
JMUCCHIELLO: 298 473 ряда
RUSS CAM: 290 121 ряд
CHRIS: 344 093 ряда
TYRANNOSAURS: 290 119 рядов

У меня еще не было времени для глубокого анализа возвращаемых строк, но я был бы очень признателен за мнение о том, какие из запросов «сломаны» и почему.


person tputkonen    schedule 30.04.2009    source источник
comment
Если CID 2 имеет то же время создания, что и CID 3, нужны ли вам оба результата? Если CID X имеет самое последнее время создания для нескольких AID, хотите ли вы, чтобы CID X появлялся в списке результатов несколько раз?   -  person Alohci    schedule 01.05.2009
comment
Спасибо за отличные вопросы. Я хочу получить последнюю версию C для каждого A. Если CID 2 и CID 3 связаны с одним и тем же AID, у них никогда не будет одинакового времени создания. Также CID X никогда не будет связан более чем с одним AID. (Я признаю, что если бы мы могли изменить схему, мы бы обязательно это сделали!)   -  person tputkonen    schedule 01.05.2009
comment
Я посчитал возвращенные строки с разными решениями. Результаты были очень интересными и разнообразными: HAINSTECH: 298 473 строки JMUCCHIELLO: 298 473 строки RUSS CAM: 290 121 CHRIS: 344 093 TYRANNOSAURS: 290 119 ценим просмотры, какие из запросов не работают и почему.   -  person tputkonen    schedule 04.05.2009
comment
@tputkonen - Пожалуйста, добавьте приведенный выше комментарий в качестве редактирования исходного вопроса. Таким образом, новым людям, просматривающим ваш вопрос, достаточно будет заглянуть в одно место, чтобы получить всю информацию.   -  person Russ Cam    schedule 04.05.2009
comment
Готово, строки добавлены в вопрос.   -  person tputkonen    schedule 04.05.2009


Ответы (5)


Примерно так, если я правильно вас понял

SELECT
    MAX(CREATIONTIME),
    A.AID
FROM
    A
INNER JOIN
    B
    ON 
    A.AID = B.AID
INNER JOIN
    C
    ON 
    B.CID = C.CID
WHERE
    A.STATUS = 'OK'
GROUP BY
    A.AID

ИЗМЕНИТЬ:

Теперь я проверил следующее в SQL Server (я бы ожидал того же результата в Oracle), и он возвращает CID для записи C с максимальным CREATIONTIME, где STATUS для связанной записи в A id 'OK'.

SELECT C.CID
FROM 
C C
INNER JOIN
B B
ON 
C.CID = B.CID
INNER JOIN
(
    SELECT
        MAX(C.CREATIONTIME) CREATIONTIME,
        A.AID
    FROM
        A A
    INNER JOIN
        B B
        ON 
        A.AID = B.AID
    INNER JOIN
        C C
        ON 
        B.CID = C.CID
    WHERE
        A.STATUS = 'OK'
    GROUP BY
        A.AID
) ABC
ON B.AID = ABC.AID
AND C.CREATIONTIME = ABC.CREATIONTIME

Продемонстрировано со следующим T-SQL

DECLARE @A TABLE(AID INT IDENTITY(1,1), STATUS VARCHAR(10))
DECLARE @B TABLE(BID INT IDENTITY(1,1), AID INT, CID INT)
DECLARE @C TABLE(CID INT IDENTITY(1,1), CREATIONTIME DATETIME)

INSERT INTO @A VALUES ('OK')
INSERT INTO @A VALUES ('OK')
INSERT INTO @A VALUES ('NOT OK')
INSERT INTO @A VALUES ('OK')
INSERT INTO @A VALUES ('NOT OK')

INSERT INTO @C VALUES ('10 MAR 2008')
INSERT INTO @C VALUES ('13 MAR 2008')
INSERT INTO @C VALUES ('15 MAR 2008')
INSERT INTO @C VALUES ('17 MAR 2008')
INSERT INTO @C VALUES ('21 MAR 2008')

INSERT INTO @B VALUES (1,1)
INSERT INTO @B VALUES (1,2)
INSERT INTO @B VALUES (1,3)
INSERT INTO @B VALUES (2,2)
INSERT INTO @B VALUES (2,3)
INSERT INTO @B VALUES (2,4)
INSERT INTO @B VALUES (3,3)
INSERT INTO @B VALUES (3,4)
INSERT INTO @B VALUES (3,5)
INSERT INTO @B VALUES (4,5)
INSERT INTO @B VALUES (4,1)
INSERT INTO @B VALUES (4,2)


SELECT C.CID
FROM 
@C C
INNER JOIN
@B B
ON 
C.CID = B.CID
INNER JOIN
(
SELECT
    MAX(C.CREATIONTIME) CREATIONTIME,
    A.AID
FROM
    @A A
INNER JOIN
    @B B
    ON 
    A.AID = B.AID
INNER JOIN
    @C C
    ON 
    B.CID = C.CID
WHERE
    A.STATUS = 'OK'
GROUP BY
    A.AID
) ABC
ON B.AID = ABC.AID
AND C.CREATIONTIME = ABC.CREATIONTIME

Результаты в следующем

CID
-----------
3
4
5

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

В ответ на ваш комментарий о том, что каждое из утверждений дает разные результаты, я провел здесь несколько разных ответов через SQL Server 2005, используя мои тестовые данные выше (я ценю, что вы используете Oracle). Вот результаты

--Expected results for CIDs would be

--CID
-----------
--3
--4
--5

--As indicated in the comments next to the insert statements

DECLARE @A TABLE(AID INT IDENTITY(1,1), STATUS VARCHAR(10))
DECLARE @B TABLE(BID INT IDENTITY(1,1), AID INT, CID INT)
DECLARE @C TABLE(CID INT IDENTITY(1,1), CREATIONTIME DATETIME)

INSERT INTO @A VALUES ('OK') -- AID 1
INSERT INTO @A VALUES ('OK') -- AID 2
INSERT INTO @A VALUES ('NOT OK')
INSERT INTO @A VALUES ('OK') -- AID 4
INSERT INTO @A VALUES ('NOT OK')

INSERT INTO @C VALUES ('10 MAR 2008')
INSERT INTO @C VALUES ('13 MAR 2008')
INSERT INTO @C VALUES ('15 MAR 2008')
INSERT INTO @C VALUES ('17 MAR 2008')
INSERT INTO @C VALUES ('21 MAR 2008')

INSERT INTO @B VALUES (1,1)
INSERT INTO @B VALUES (1,2)
INSERT INTO @B VALUES (1,3) -- Will be CID 3 For AID 1
INSERT INTO @B VALUES (2,2)
INSERT INTO @B VALUES (2,3)
INSERT INTO @B VALUES (2,4) -- Will be CID 4 For AID 2
INSERT INTO @B VALUES (3,3)
INSERT INTO @B VALUES (3,4)
INSERT INTO @B VALUES (3,5)
INSERT INTO @B VALUES (4,5) -- Will be CID 5 FOR AID 4
INSERT INTO @B VALUES (4,1)
INSERT INTO @B VALUES (4,2)

-- Russ Cam
SELECT C.CID, ABC.CREATIONTIME
FROM 
@C C
INNER JOIN
@B B
ON 
C.CID = B.CID
INNER JOIN
(
SELECT
    MAX(C.CREATIONTIME) CREATIONTIME,
    A.AID
FROM
    @A A
INNER JOIN
    @B B
    ON 
    A.AID = B.AID
INNER JOIN
    @C C
    ON 
    B.CID = C.CID
WHERE
    A.STATUS = 'OK'
GROUP BY
    A.AID
) ABC
ON B.AID = ABC.AID
AND C.CREATIONTIME = ABC.CREATIONTIME

-- Tyrannosaurs
select   A.AID,  
         max(AggC.CREATIONTIME)  
from    @A A,  
         @B B,  
         (  select  C.CID,  
             max(C.CREATIONTIME) CREATIONTIME  
            from @C C  
            group by CID
          ) AggC  
where    A.AID = B.AID  
and    B.CID = AggC.CID  
and    A.Status = 'OK'  
group by A.AID

-- jmucchiello
SELECT c.cid, max(c.creationtime)
FROM @B b, @C c
WHERE b.cid = c.cid
 AND b.aid IN (SELECT a.aid FROM @A a WHERE status = 'OK')
GROUP BY c.cid

-- hainstech
SELECT agg.aid, agg.cid
FROM (
    SELECT a.aid
        ,c.cid
        ,max(c.creationtime) as maxcCreationTime
    FROM @C c INNER JOIN @B b ON b.cid = c.cid
        INNER JOIN @A a on a.aid = b.aid
    WHERE a.status = 'OK'
    GROUP BY a.aid, c.cid
) as agg

--chris
SELECT A.AID, C.CID, C.CREATIONTIME
FROM @A A, @B B, @C C
WHERE A.STATUS = 'OK'
AND A.AID = B.AID
AND B.CID = C.CID
AND C.CREATIONTIME = 
(SELECT MAX(C2.CREATIONTIME) 
FROM @C C2, @B B2 
WHERE B2.AID = A.AID
AND C2.CID = B2.CID);

Результаты приведены ниже

--Russ Cam - Correct CIDs (I have added in the CREATIONTIME for reference)
CID         CREATIONTIME
----------- -----------------------
3           2008-03-15 00:00:00.000
4           2008-03-17 00:00:00.000
5           2008-03-21 00:00:00.000

--Tyrannosaurs - No CIDs in the resultset
AID         
----------- -----------------------
1           2008-03-15 00:00:00.000
2           2008-03-17 00:00:00.000
4           2008-03-21 00:00:00.000


--jmucchiello - Incorrect CIDs in the resultset
cid         
----------- -----------------------
1           2008-03-10 00:00:00.000
2           2008-03-13 00:00:00.000
3           2008-03-15 00:00:00.000
4           2008-03-17 00:00:00.000
5           2008-03-21 00:00:00.000

--hainstech - Too many CIDs in the resultset, which CID has the MAX(CREATIONTIME) for each AID?
aid         cid
----------- -----------
1           1
1           2
1           3
2           2
2           3
2           4
4           1
4           2
4           5

--chris - Correct CIDs, it is the same SQL as mine
AID         CID         CREATIONTIME
----------- ----------- -----------------------
1           3           2008-03-15 00:00:00.000
2           4           2008-03-17 00:00:00.000
4           5           2008-03-21 00:00:00.000

Я бы рекомендовал запускать каждый из данных ответов с меньшим количеством записей, чтобы вы могли убедиться, что возвращаемый набор результатов является ожидаемым.

person Russ Cam    schedule 30.04.2009
comment
Это очень близко, но он не возвращает C.CID, который является единственным интересующим меня столбцом. - person tputkonen; 01.05.2009
comment
Спасибо за ваши усилия! Я попробую это в ближайшее время, но я не понимаю, как в этом случае работает оператор select во внутреннем соединении? Как он учитывает косвенную связь между таблицами A и C? Я просто не понимаю. - person tputkonen; 01.05.2009
comment
Расс, я очень ценю твои усилия! Сегодня национальный праздник, поэтому я не буду тщательно его проверять, но я довольно внимательно изучил запрос, и он имел смысл, и мне удалось запустить его в Oracle. Кроме того, количество строк, которые он возвращает в нашей относительно большой базе данных, кажется очень разумным. - person tputkonen; 01.05.2009
comment
Прочитав различные ответы, я думаю, что это правильно (я переделал свой, и они используют ту же логику)... - person Jon Hopkins; 05.05.2009

Выберите поле, которое вы ищете, используя объединение всех трех таблиц, а затем ограничьте результаты теми, где CREATIONDATE является самым последним.

SELECT A.AID, C.CID, C.CREATIONTIME
FROM A A, B B, C C
WHERE A.STATUS = 'OK'
AND A.AID = B.AID
AND B.CID = C.CID
AND C.CREATIONTIME = 
(SELECT MAX(C2.CREATIONTIME) 
FROM C C2, B B2 
WHERE B2.AID = A.AID
AND C2.CID = B2.CID);
person Chris    schedule 02.05.2009

РЕДАКТИРОВАТЬ: мой предыдущий ответ был ерундой. Теперь это полная переработка

На самом деле это проблема, которая беспокоила меня на протяжении всей моей жизни с SQL. Решение, которое я вам дам, чертовски запутано, но оно работает, и я был бы признателен, если бы кто-нибудь сказал: «Да, это чертовски запутано, но это единственный способ сделать это», или сказал: «Нет, сделайте это... ".

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

В любом случае, разбивая это, вам нужно сделать это в два этапа.

1) Первый — вернуть набор результатов [AID], [наиболее раннее время создания], указывающий вам самое раннее время создания для каждого AID.

2) Затем вы можете использовать lastCreationTime, чтобы получить нужный CID.

Итак, для части (1) я бы лично создал представление, чтобы сделать это просто для того, чтобы все было аккуратно. Это позволяет вам протестировать эту часть и заставить ее работать, прежде чем объединять ее с другими вещами.

create view LatestCreationTimes
as
select b.AID,
       max(c.CreationTime) LatestCreationTime
from   TableB b,
       TableC c
where  b.CID = c.CID
group by b.AID

Обратите внимание, что в этот момент мы не учитывали статус.

Затем вам нужно присоединить его к TableA (чтобы получить статус), а также к TableB и TableC (чтобы получить CID). Вам нужно сделать все очевидные ссылки (AID, CID), а также соединить столбец LatestCreationTime в представлении со столбцом CreationTime в TableC. Не забудьте также присоединиться к представлению в AID, иначе, если две записи были созданы одновременно для разных записей A, у вас возникнут проблемы.

select A.AID,
       C.CID
from   TableA a,
       TableB b,
       TableC c,
       LatestCreationTimes lct
where  a.AID = b.AID
and    b.CID = c.CID
and    a.AID = lct.AID
and    c.CreationTime = lct.LatestCreationTime
and    a.STATUS = 'OK'

Я уверен, что это работает — я протестировал его, подправил данные, повторно протестировал, и он себя ведет. По крайней мере, он делает то, для чего, как мне кажется, предназначен.

Однако это не касается возможности двух идентичных CreationTimes в таблице C для одной и той же записи. Я предполагаю, что этого не должно происходить, однако, если вы не написали что-то, что абсолютно ограничивает это, это необходимо учитывать.

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

select A.AID,
       max(C.CID) CID
from   TableA a,
       TableB b,
       TableC c,
       LatestCreationTimes lct
where  a.AID = b.AID
and    b.CID = c.CID
and    c.CreationTime = lct.LatestCreationTime
and    a.STATUS = 'OK'
group by A.AID

И это, я считаю, должно работать для вас. Если вы хотите, чтобы это был один запрос, а не представление, то:

select A.AID,
       max(C.CID) CID
from   TableA a,
       TableB b,
       TableC c,
       (select b.AID,
               max(c.CreationTime) LatestCreationTime
        from   TableB b,
               TableC c
        where  b.CID = c.CID
        group by b.AID) lct
where  a.AID = b.AID
and    b.CID = c.CID
and    c.CreationTime = lct.LatestCreationTime
and    a.STATUS = 'OK'
group by A.AID

(Я только что встроил представление в запрос, в остальном принцип точно такой же).

person Jon Hopkins    schedule 30.04.2009
comment
Извините, что вопрос не был точным сначала о том, что я ищу. Мне нужно получить CID. - person tputkonen; 01.05.2009
comment
Не проблема, вы можете просто добавить C.CID в список выбора, и он все равно будет работать. - person Jon Hopkins; 01.05.2009
comment
Полностью переписал мой ответ... Иди еще раз посмотри. - person Jon Hopkins; 05.05.2009
comment
Я действительно поражен вашими усилиями, ребята. Я бы очень хотел выбрать два принятых ответа, однако система допускает только один, и Расс был первым. Очень ценю ваши усилия и хорошие объяснения. - person tputkonen; 06.05.2009

Нет необходимости в подзапросе, агрегация для определения последнего времени создания cid проста:

SELECT a.aid
    ,c.cid
    ,max(c.creationtime) as maxcCreationTime
FROM c INNER JOIN b ON b.cid = c.cid
    INNER JOIN a on a.aid = b.aid
WHERE a.status = 'OK'
GROUP BY a.aid, c.cid

Если вам действительно не нужно время создания в наборе строк, вы можете просто обернуть его в подзапрос и удалить из проекции:

SELECT agg.aid, agg.cid
FROM (
    SELECT a.aid
        ,c.cid
        ,max(c.creationtime) as maxcCreationTime
    FROM c INNER JOIN b ON b.cid = c.cid
        INNER JOIN a on a.aid = b.aid
    WHERE a.status = 'OK'
    GROUP BY a.aid, c.cid
) as agg

Кодирование на веб-странице, пожалуйста, извините за любые синтаксические ошибки. Кроме того, я парень mssql, поэтому я надеюсь, что в мире Oracle нет ничего другого для этого.

Обратите внимание, что предоставленная вами схема не обеспечивает уникальность CREATIONTIME для каждого cid. Если есть два значения cid, которые соответствуют данному вспомогательному значению с одинаковым временем создания, они оба будут выведены. Если вы полагаетесь на то, что пара cid,creationtime будет уникальной, вы должны принудительно применить ее декларативно с помощью ограничения.

person ahains    schedule 02.05.2009
comment
Это не сработает. Поскольку вы группируете по AID и CID одновременно, он вернет несколько строк из TableC. Допустим, у вас есть одна строка в таблице A (ID 1, статус OK), две в таблице C (ID 1 и 2, даты сегодня и завтра соответственно) и две в таблице C (связывающие обе записи таблицы C с одним значением в таблицу А). Ваш запрос вернет обе записи таблицы C, так как сегодня максимальное значение для AID = 1 и CID = 1, а завтра максимальное значение для AID = 1 и CID = 2. - person Jon Hopkins; 05.05.2009

Я что-то упускаю? Что не так с:

РЕДАКТИРОВАТЬ: Хорошо, я вижу, вы действительно хотите сгруппироваться с помощью.

SELECT c.cid FROM b, c,
    (SELECT b.aid as aid, max(c.creationtime) as creationtime
     FROM b, c
     WHERE b.cid = c.cid
       AND b.aid IN (SELECT a.aid FROM a WHERE status = 'OK')
     GROUP BY b.aid) as z
WHERE b.cid = c.cid
  AND z.aid = b.aid
  AND z.creationtime = c.creationtime
person jmucchiello    schedule 02.05.2009
comment
Это самый бесполезный комментарий. Вы можете сказать людям, какая часть исходного вопроса не выполняется. Это не то же самое для всех. - person jmucchiello; 04.05.2009
comment
Прошу прощения, я думал протестировать его завтра с меньшим количеством данных и опубликовать результаты, но Расс уже был достаточно любезен, чтобы сделать это. - person tputkonen; 04.05.2009