Получите DISTINCT COUNT за один проход в SQL Server

У меня есть таблица, как показано ниже:

Region    Country    Manufacturer    Brand    Period    Spend
R1        C1         M1              B1       2016      5
R1        C1         M1              B1       2017      10
R1        C1         M1              B1       2017      20
R1        C1         M1              B2       2016      15
R1        C1         M1              B3       2017      20
R1        C2         M1              B1       2017      5
R1        C2         M2              B4       2017      25
R1        C2         M2              B5       2017      30
R2        C3         M1              B1       2017      35
R2        C3         M2              B4       2017      40
R2        C3         M2              B5       2017      45
...

Я написал запрос ниже, чтобы их объединить:

SELECT [Region]
    ,[Country]
    ,[Manufacturer]
    ,[Brand]
    ,Period
    ,SUM([Spend]) AS [Spend]
FROM myTable
GROUP BY [Region]
    ,[Country]
    ,[Manufacturer]
    ,[Brand]
    ,[Period]
ORDER BY 1,2,3,4

что дает что-то вроде ниже:

Region    Country    Manufacturer    Brand    Period    Spend
R1        C1         M1              B1       2016      5
R1        C1         M1              B1       2017      30 -- this row is an aggregate from raw table above
R1        C1         M1              B2       2016      15
R1        C1         M1              B3       2017      20
R1        C2         M1              B1       2017      4  -- aggregated result
R1        C2         M2              B4       2017      25
R1        C2         M2              B5       2017      30
R2        C3         M2              B4       2017      40
R2        C3         M2              B5       2017      45

Я хотел бы добавить еще один столбец к приведенной выше таблице, который показывает DISTINCT COUNT из Brand, сгруппированных по Region, Country, Manufacturer и Period. Таким образом, финальный стол будет выглядеть следующим образом:

Region    Country    Manufacturer    Brand    Period    Spend    UniqBrandCount
R1        C1         M1              B1       2016      5        2 -- two brands by R1, C1, M1 in 2016
R1        C1         M1              B1       2017      30       1
R1        C1         M1              B2       2016      15       2 -- same as first row's result
R1        C1         M1              B3       2017      20       1
R1        C2         M1              B1       2017      4        1
R1        C2         M2              B4       2017      25       2
R1        C2         M2              B5       2017      30       2
R2        C3         M2              B4       2017      40       2
R2        C3         M2              B5       2017      45       2

Я знаю, как достичь конечного результата за три шага.

  1. Выполните этот запрос (запрос №1):

    ВЫБРАТЬ [Регион] ,[Страна] , [Производитель] ,[Период] , COUNT (DISTINCT [Brand]) AS [BrandCount] INTO Temp1 ИЗ myTable ГРУППА ПО [региону] ,[Страна] , [Производитель] ,[Период]

  2. Запустите этот запрос (запрос №2)

    ВЫБРАТЬ [Регион] ,[Страна] , [Производитель] , [Бренд] , ГОД ([Период]) КАК Период , SUM ([Spend]) AS [Spend] INTO Temp2 ИЗ myTable ГРУППА ПО [региону] ,[Страна] , [Производитель] , [Бренд] ,[Период]

  3. Затем LEFT JOIN, Temp2 и Temp1, чтобы получить [BrandCount] из последнего, как показано ниже:

    Выберите.* , Б. * ОТ Temp2 AS a LEFT JOIN Temp1 AS b ON a. [Region] = b. [Region] И a. [Страна] = b. [Страна] И а. [Рекламодатель] = б. [Рекламодатель] И a. [Период] = b. [Период]

Я почти уверен, что есть более эффективный способ сделать это, не так ли? Заранее благодарим вас за ваши предложения / ответы!

ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
2
0
318
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Тег к вашему вопросу;

window-functions

предполагает, что у вас есть неплохая идея.

Для РАЗЛИЧНОЕ КОЛИЧЕСТВО брендов, сгруппированных по региону, стране, производителю и периоду: вы можете написать:

Select   Region 
        ,Country
        ,Manufacturer
        ,Brand
        ,Period
        ,Spend
        ,DENSE_RANK() Over (Partition By Region, Country, Manufacturer, Period Order By Brand asc) 
         + DENSE_RANK() Over (Partition By Region, Country, Manufacturer, Period Order By Brand desc) 
         -1 UniqBrandCount
From myTable T1
Order By 1,2,3,4
Ответ принят как подходящий

Сильно заимствовано из этого вопроса: https://dba.stackexchange.com/questions/89031/using-distinct-in-window-function-with-over

Счетчик Distinct не работает, поэтому требуется density_rank. Ранжирование брендов в прямом и обратном порядке с последующим вычитанием 1 дает различный счет.

Функцию суммы также можно переписать с использованием логики PARTITION BY. Таким образом, вы можете использовать разные уровни группировки для каждой агрегации:

SELECT 
[Region]
,[Country]
,[Manufacturer]
,[Brand]
,[Period]
,dense_rank() OVER 
    (PARTITION BY 
     [Region] 
    ,[Country]
    ,[Manufacturer]
    ,[Period] Order by Brand) 
+ dense_rank() OVER 
    (PARTITION BY 
     [Region] 
    ,[Country]
    ,[Manufacturer]
    ,[Period] Order by Brand Desc) 
- 1  
AS [BrandCount]
,SUM([Spend]) OVER
    (PARTITION BY
     [Region] 
    ,[Country]
    ,[Manufacturer]
    ,[Brand]
    ,[Period]) as [Spend]
from
myTable
ORDER BY 1,2,3,4

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

R1  C1  M1  B1  2016    2   5
R1  C1  M1  B1  2017    2   30 --dup1
R1  C1  M1  B1  2017    2   30 --dup1
R1  C1  M1  B2  2016    2   15
R1  C1  M1  B3  2017    2   20
R1  C2  M1  B1  2017    1   5
R1  C2  M2  B4  2017    2   25
R1  C2  M2  B5  2017    2   30
R2  C3  M1  B1  2017    1   35
R2  C3  M2  B4  2017    2   40
R2  C3  M2  B5  2017    2   45

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

Как работает трюк "density_rank"

Рассмотрим эти данные:

Col1    Col2
B       1
B       1
B       3
B       5
B       7
B       9

density_rank () ранжирует данные по количеству отдельных элементов перед текущим плюс 1. Итак:

1->1, 3->2, 5->3, 7->4, 9->5.

В обратном порядке (с использованием desc) получается обратная картина:

1->5, 3->4, 5->3, 7->2, 9->1:

Сложение этих рангов дает одно и то же значение:

1+5 = 2+4 = 3+3 = 4+2 = 5+1 = 6

Формулировка здесь полезна,

(number of distinct items before + 1) + (number of distinct items after + 1) 
= number of distinct OTHER items before AND after + 2 
= Total number of distinct items + 1

Итак, чтобы получить общее количество различных элементов, сложите ascending и descending density_ranks вместе и вычтите 1.

Спасибо за исчерпывающий ответ! Немного подумав, чтобы сразу понять, почему DESNSE_RANK вперед и назад - 1 может оказаться тем же самым, что и DISTINCT COUNT Brand. Если есть какой-либо ресурс, объясняющий, почему это работает, не могли бы вы поделиться? Я проверю результат и выберу ваш ответ в качестве ответа (потому что он наиболее исчерпывающий). Я также попытаюсь написать здесь в качестве комментария то, что я узнал о том, почему трюк с DENSE_RANK работает после того, как подумал над этим сегодня вечером. Большое спасибо!

user1330974 24.05.2018 02:12

Большое спасибо за очень четкое объяснение !! Теперь мне понятно, почему DENSE_RANK должен работать. Хотя приведенный ниже ответ Мартина также хорош (и не требует впоследствии дедупликации строк), я приму этот. Тем, кто ищет ответы, также можно прочитать ответ Мартина ниже. Большое спасибо, @mjsqu!

user1330974 24.05.2018 15:45

Нет проблем, я сначала этого не понял, поэтому мне пришлось создать рабочий пример, чтобы понять, почему!

mjsqu 25.05.2018 00:41

Идея двойного dense_rank означает, что вам нужно две сортировки (при условии, что не существует индекса, обеспечивающего порядок сортировки). Предполагая, что нет брендов NULL (как эта идея), вы можете использовать один dense_rank и оконный MAX, как показано ниже (демонстрация)

WITH T1
     AS (SELECT *,
                DENSE_RANK() OVER (PARTITION BY [Region], [Country], [Manufacturer], [Period] ORDER BY Brand) AS [dr]
         FROM   myTable),
     T2
     AS (SELECT *,
                MAX([dr]) OVER (PARTITION BY [Region], [Country], [Manufacturer], [Period]) AS UniqBrandCount
         FROM   T1)
SELECT [Region],
       [Country],
       [Manufacturer],
       [Brand],
       Period,
       SUM([Spend])        AS [Spend],
       MAX(UniqBrandCount) AS UniqBrandCount
FROM   T2
GROUP  BY [Region],
          [Country],
          [Manufacturer],
          [Brand],
          [Period]
ORDER  BY [Region],
          [Country],
          [Manufacturer],
          [Period],
          Brand 

Вышеупомянутое имеет некоторую неизбежную буферизацию (невозможно сделать это 100% потоковым способом), но только один вид.

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

Большое спасибо за то, что поделились альтернативным (и, предположительно, более эффективным) подходом! Основываясь на вашем объяснении, я понимаю, что использование DENSE_RANK влечет за собой одну сортировку, а MAX - нет (я имею в виду, я вижу это в выходных данных анализатора запросов, но просто интересно, как без сортировки были найдены значения MAX)? Я все еще новичок в изучении SQL, поэтому, возможно, однажды я смогу лучше понять внутреннюю работу. :) Еще один вопрос, спулинг из-за того, что часть MAX должна ждать завершения работы части DENSE_RANK? Большое спасибо!

user1330974 24.05.2018 02:24

@ user1330974 - справа налево строки считываются из таблицы и сортируются в порядке [Region], [Country], [Manufacturer], [Period], Brand. Следующие два оператора проекта сегмента и последовательности вычисляют плотный_ранк. Строки с этим рассчитанным значением density_rank считываются в катушку. Как только достигается новая группа [Region], [Country], [Manufacturer], [Period], для строк в катушке для предыдущей группы вычисляется MAX, а затем строки из катушки воспроизводятся с добавленным к ним значением MAX.

Martin Smith 24.05.2018 07:04

Подробнее о density_rank sqlblog.com/blogs/paul_white/archive/2010/07/28/… и оконных агрегатах sqlblog.com/blogs/paul_white/archive/2010/07/28/…

Martin Smith 24.05.2018 07:08

Большое спасибо за подробное объяснение того, как читать план запроса, а также за то, что вы поделились действительно хорошим ресурсом / блогом, чтобы узнать больше о функциях окна! Благодаря вашему объяснению план запроса теперь имеет для меня частичный смысл, и я могу подтвердить, что предложенное вами решение работает и занимает всего 6-8 секунд! Боковое примечание: я принял другое решение, указанное выше, потому что оно появилось немного раньше, чем ваше, и оно также сработало. Надеюсь, ты поймешь. :)

user1330974 24.05.2018 16:50

Другие вопросы по теме