Используя SQL, как правильно суммировать столбцы из нескольких таблиц в общие группы?

Я пишу панель мониторинга, и мне нужно извлечь некоторые данные из Microsoft SQL Server.

В качестве простого примера у меня есть три таблицы, одна основная таблица категорий и две таблицы, содержащие значения, связанные с таблицей категорий посредством отношения первичного/внешнего ключа (таблицы значений Blue и Green).

Используя Microsoft SQL Sever (t-sql), я хочу суммировать (суммировать) значения в двух таблицах значений, сгруппированных по общей категории, найденной в таблице категорий.

Таблица категорий

CategoryID (PK) | CategoryName
1               | Square
2               | Circle

Синий стол

BlueID (PK) | CategoryID (FK) | BlueValue | BlueMonth | BlueYear
1           | 1               | 10        | 6         | 2012
2           | 1               | 20        | 12        | 2012
3           | 2               | 5         | 6         | 2012
4           | 2               | 9         | 12        | 2012
5           | 1               | 12        | 6         | 2013
6           | 1               | 21        | 12        | 2013
7           | 2               | 4         | 6         | 2013
8           | 2               | 8         | 12        | 2013

Зеленый стол

GreenID (PK)| CategoryID (FK) | GreenValue| GreenMonth| GreenYear
1           | 1               | 3         | 6         | 2012
2           | 1               | 6         | 12        | 2012
3           | 2               | 2         | 6         | 2012
4           | 2               | 7         | 12        | 2012
5           | 1               | 2         | 6         | 2013
6           | 1               | 5         | 12        | 2013
7           | 2               | 4         | 6         | 2013
8           | 2               | 8         | 12        | 2013

Если я использую следующий SQL, я получаю ожидаемые результаты.

SELECT
    [Category].[CategoryName],
    SUM([Green].[GreenValue]) AS [GreenTotal]
FROM
    [Category]
LEFT JOIN
    [Green] ON [Category].[CategoryID] = [Green].[CategoryID]
GROUP BY
    [Category].[CategoryName]

Полученные результаты:

CategoryName | GreenTotal
Square       | 16
Triangle     | 21

Однако, если я добавлю таблицу Blue, чтобы попытаться получить общее значение для BlueValue, мой очевидно неверный T-SQL даст мне неожиданные результаты.

SELECT
    [Category].[CategoryName],
    SUM([Green].[GreenValue]) AS [GreenTotal],
    SUM([Blue].[BlueValue]) AS [BlueTotal]
FROM
    [Category]
LEFT JOIN
    [Green] ON [Category].[CategoryID] = [Green].[CategoryID]
LEFT JOIN
    [Blue] ON [Category].[CategoryID] = [Blue].[CategoryID]
GROUP BY
    [Category].[CategoryName]

Неверные результаты:

CategoryName | GreenTotal | BlueTotal
Square       | 64         | 252
Triangle     | 84         | 104

Кажется, что все результаты отличаются в 4 раза, что равно общему количеству строк в каждой таблице значений для каждой категории.

Я стремлюсь увидеть следующие результаты:

CategoryName | GreenTotal | BlueTotal
Square       | 16         | 63
Triangle     | 21         | 26

Я был бы на седьмом небе от счастья, если бы кто-нибудь мог сказать мне, что я делаю неправильно?

Спасибо, Марк.


person Mark B    schedule 13.11.2013    source источник


Ответы (5)


То, что вы получаете, является декартовым произведением. Вы можете увидеть последствия этого, удалив группировку и просмотрев данные.

Например; если ваша зеленая таблица содержит 2 строки, а синяя таблица — 4, ваше соединение вернет в общей сложности 8 записей.

Чтобы решить проблему, ну, вы почти у цели. У вас есть все нужные части, просто вы не совсем правильно их собрали.

Предположим, что следующий запрос возвращает правильные результаты для зеленого цвета:

SELECT CategoryID
     , Sum(GreenValue) As GreenTotal
FROM   Green
GROUP
    BY CategoryID

Результаты для синего можно получить, следуя тому же методу:

SELECT CategoryID
     , Sum(BueValue) As BlueTotal
FROM   Blue
GROUP
    BY CategoryID

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

SELECT Category.CategoryName
     , GreenSummary.GreenTotal
     , BlueSummary.BlueTotal
FROM   Category
 LEFT
  JOIN (    
        SELECT CategoryID
             , Sum(GreenValue) As GreenTotal
        FROM   Green
        GROUP
            BY CategoryID
       ) As GreenSummary
    ON GreenSummary.CategoryID = Category.CategoryID
 LEFT
  JOIN (    
        SELECT CategoryID
             , Sum(BlueValue) As BlueTotal
        FROM   Blue
        GROUP
            BY CategoryID
       ) As BlueSummary
    ON BlueSummary.CategoryID = Category.CategoryID
person gvee    schedule 13.11.2013
comment
Блестящий. Спасибо, работает отлично. Я знал, что это будет что-то вроде этого, я просто не мог пройти последнюю милю и заставить это работать. В вашем примере есть небольшая ошибка :-) Во втором подзапросе у вас есть «AS GreenTotal», где должно быть «AS BlueTotal». Еще раз большое спасибо! Отметка. - person Mark B; 13.11.2013
comment
Ошибка копирования-вставки на максимум! Ответ обновлен, чтобы скрыть ошибку - person gvee; 13.11.2013
comment
Хороший пример, вам нужен только isnull в последнем запросе, чтобы получить серо, где итог имеет нулевое значение. - person Juan; 13.11.2013
comment
Согласовано! Только Coalesce() вместо неприглядного, приличия IsNull() ;) - person gvee; 14.11.2013

На мой взгляд, что-то подобное лучше всего сделать с помощью APPLY. Быстрая с точки зрения производительности, простая в использовании и легкая в управлении в случае изменений в запросе.

IE:

SELECT C.[CategoryName], G.[GreenTotal], B.[BlueTotal]
FROM [Category] C
OUTER APPLY (SELECT SUM([GreenValue]) AS [GreenTotal] FROM [Green] WHERE [CategoryID] = C.CategoryID) G
OUTER APPLY (SELECT SUM([BlueValue]) AS [BlueTotal] FROM [Blue] WHERE [CategoryID] = C.CategoryID) B
person Kahn    schedule 13.11.2013
comment
Отлично, хорошо выглядит. Извините, я еще не могу проголосовать, я слишком новичок! - person Mark B; 13.11.2013

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

Затем используйте эти запросы в качестве подзапроса и выполните левое внешнее соединение с основной таблицей. Это может дать вам ожидаемый результат и может иметь нулевые значения, когда сумма недоступна. Вы можете использовать функцию isnull для преобразования нулевых значений в 0.

person Juan    schedule 13.11.2013
comment
Спасибо за это. Почти то, что сказал gvee без примера. Извините, я не могу проголосовать, я слишком новичок! - person Mark B; 13.11.2013

Я бы суммировал их сначала с CTE. Затем просто соедините 2 вместе в том, что является общим, только с одним вхождением в каждом, CategoryName. Таким образом, вы не можете получить декартово произведение. Я вставил оператор isnull, потому что есть вероятность, что нет результатов для CategoryName, выделенного синим или зеленым цветом. Если вы этого не сделаете, вы можете получить null для своего CategoryName.

WITH GREENSUM as (
SELECT
    [Category].[CategoryName],
    SUM([Green].[GreenValue]) AS [GreenTotal]
FROM
    [Category]
LEFT JOIN
    [Green] ON [Category].[CategoryID] = [Green].[CategoryID]
GROUP BY
    [Category].[CategoryName]
),
WITH BLUESUM as (
SELECT
    [Category].[CategoryName],
    SUM([Blue].[BlueValue]) AS [BlueTotal]
FROM
    [Category]
LEFT JOIN
    [Blue] ON [Category].[CategoryID] = [Blue].[CategoryID]
GROUP BY
    [Category].[CategoryName])
SELECT isnull(GREENSUM.CategoryName, BLUESUM.CategoryName) as CategoryName, 
    GreenTotal, BlueTotal 
FROM [GREENSUM] 
FULL OUTER JOIN 
    [BLUESUM] ON [GREENSUM].CategoryName = [BLUESUM].CategoryName)
person John Wesley Gordon    schedule 13.11.2013

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

/*
create table Category ( CategoryId Integer, CategoryName nvarchar(50) )
create table Green ( CategoryId Integer, GreenValue Integer )
create table Blue ( CategoryId Integer, BlueValue Integer )

insert into Category VALUES (1,'Square'),(2,'Circle')
insert into Blue VALUES (1,10),(1,20),(2,5),(2,9),(1,12),(1,21),(2,4),(2,8)
insert into Green VALUES (1,3),(1,6),(2,2),(2,7),(1,2),(1,5),(2,4),(2,8)
*/

with CatSums(ColorRank, CategoryId, CategoryValue) as
(
  select 1, CategoryId, GreenValue from Green
  union all
  select 2, CategoryId, BlueValue  from Blue
)

select 
    C.CategoryName, 
    Sum(case when ColorRank = 1 then CategoryValue else 0 end) as GreenTotal,
    Sum(case when ColorRank = 2 then CategoryValue else 0 end) as BlueTotal
 from CatSums S left join Category C on C.CategoryId = S.CategoryId
group by C.CategoryName
  • Хотя я должен признать, что мне все больше нравится решение OUTER APPLY.
person par    schedule 13.11.2013