Нека започнем нашето SQL пътуване, за да разберем агрегирането на данни в SQL и видовете агрегирания, включително прости и плъзгащи се агрегати.
Преди да преминем към агрегирането, си струва да помислим за интересни факти, често пропускани от някои разработчици, когато става въпрос за SQL като цяло и за агрегирането в частност.
В тази статия SQL се отнася до T-SQL, който е версията на SQL на Microsoft и има повече функции от стандартния SQL.
Математиката зад SQL
Много е важно да се разбере, че T-SQL се основава на някои солидни математически концепции, въпреки че не е твърд език, базиран на математиката.
Според книгата „Microsoft_SQL_Server_2008_T_SQL_Fundamentals“ от Ицик Бен-Ган, SQL е предназначен да заявява и управлява данни в система за управление на релационна база данни (RDBMS).
Самата система за управление на релационна база данни се основава на два солидни математически клона:
- Теория на множеството
- Предикатна логика
Теория на множеството
Теорията на множествата, както показва името, е клон на математиката за множествата, които също могат да бъдат наречени колекции от определени различни обекти.
Накратко, в теорията на множеството ние мислим за нещата или обектите като цяло по същия начин, по който мислим за отделен елемент.
Например, една книга е набор от всички определени отделни книги, така че вземаме книга като цяло, което е достатъчно, за да получим подробности за всички книги в нея.
Предикатна логика
Предикатната логика е булева логика, която връща true или false в зависимост от условието или стойностите на променливите.
Предикатната логика може да се използва за налагане на правила за интегритет (цената трябва да е по-голяма от 0,00) или филтриране на данни (където цената е повече от 10,00), но в контекста на T-SQL имаме три логически стойности, както следва:
- Вярно
- Невярно
- Неизвестно (Null)
Това може да се илюстрира по следния начин:
Пример за предикат е „Където цената на книгата е по-голяма от 10,00“.
Стига за математиката, но моля, имайте предвид, че ще се позова на нея по-късно в статията.
Защо агрегирането на данни в SQL е лесно
Обобщаването на данни в SQL в най-простата им форма е свързано с опознаването на общите суми наведнъж.
Например, ако имаме таблица с клиенти, която съдържа списък на всички клиенти заедно с техните данни, тогава обобщените данни от таблицата с клиенти могат да ни дадат общия брой клиенти, които имаме.
Както беше обсъдено по-рано, ние мислим за набор като един елемент, така че просто прилагаме агрегатна функция към таблицата, за да получим общите суми.
Тъй като SQL първоначално е език, базиран на набори (както беше обсъдено по-рано), така че е относително по-лесно да се прилагат агрегатни функции към него в сравнение с други езици.
Например, ако имаме таблица с продукти, която има записи за всички продукти в базата данни, тогава можем веднага да приложим функцията за броене към таблица с продукти, за да получим общия брой продукти, вместо да ги броим един по един в цикъл.
Рецепта за агрегиране на данни
За да агрегираме данни в SQL, ни трябват най-малко следните неща:
- Данни (таблица) с колони, които, когато са обобщени, имат смисъл
- Агрегирана функция, която да се приложи към данните
Подготовка на примерни данни (Таблица)
Нека вземем пример за проста таблица с поръчки, която съдържа три неща (колони):
- Номер на поръчката (OrderId)
- Дата на направена поръчка (OrderDate)
- Сума на поръчката (TotalAmount)
Нека създадем базата данни AggregateSample, за да продължим по-нататък:
-- Create aggregate sample database CREATE DATABASE AggregateSample
Сега създайте таблицата на поръчките в примерната база данни, както следва:
-- Create order table in the aggregate sample database USE AggregateSample CREATE TABLE SimpleOrder (OrderId INT PRIMARY KEY IDENTITY(1,1), OrderDate DATETIME2, TotalAmount DECIMAL(10,2) )
Попълване на примерни данни
Попълнете таблицата, като добавите един ред:
INSERT INTO dbo.SimpleOrder ( OrderDate ,TotalAmount ) VALUES ( '20180101' -- OrderDate - datetime2 ,20.50 -- TotalAmount - decimal(10, 2) ); GO
Нека сега да разгледаме таблицата:
-- View order table SELECT OrderId ,OrderDate ,TotalAmount FROM SimpleOrder
Моля, обърнете внимание, че използвам dbForge Studio за SQL Server в тази статия, така че само изходният вид може да се различава, ако стартирате същия код в SSMS (SQL Server Management Studio), няма разлика що се отнася до скриптовете и техните резултати.
Основни агрегатни функции
Основните агрегатни функции, които могат да бъдат приложени към таблицата, са както следва:
- Сбор
- Преброяване
- Мин.
- Макс.
- Средно
Агрегираща таблица с единични записи
Сега интересният въпрос е „можем ли да обобщим (сумираме или броим) данни (записи) в таблица, ако тя има само един ред, както в нашия случай?“ Отговорът е „Да“, можем, въпреки че няма много смисъл, но може да ни помогне да разберем как данните се подготвят за агрегиране.
За да получим общия брой поръчки, използваме функцията count() с таблицата, както беше обсъдено по-рано, можем просто да приложим агрегатната функция към таблицата, тъй като SQL е език, базиран на набори и операциите могат да се прилагат към набор директно.
-- Getting total number of orders placed so far SELECT COUNT(*) AS Total_Orders FROM SimpleOrder
Сега, какво ще кажете за поръчката с минимална, максимална и средна сума за един запис:
-- Getting order with minimum amount, maximum amount, average amount and total orders SELECT COUNT(*) AS Total_Orders ,MIN(TotalAmount) AS Min_Amount ,MAX(TotalAmount) AS Max_Amount ,AVG(TotalAmount) Average_Amount FROM SimpleOrder
Както можем да видим от изхода, минималната, максималната и средната сума са еднакви, ако имаме един запис, така че прилагането на агрегатна функция към един запис е възможно, но ни дава същите резултати.
Нуждаем се от поне повече от един запис, за да разберем обобщените данни.
Таблица за обобщаване на множество записи
Нека сега добавим още четири записа, както следва:
INSERT INTO dbo.SimpleOrder ( OrderDate ,TotalAmount ) VALUES ( '20180101' -- OrderDate - datetime2 ,20.50 -- TotalAmount - decimal(10, 2) ), ( '20180102' -- OrderDate - datetime2 ,30.50 -- TotalAmount - decimal(10, 2) ), ( '20180103' -- OrderDate - datetime2 ,10.50 -- TotalAmount - decimal(10, 2) ), ( '20180110' -- OrderDate - datetime2 ,100.50 -- TotalAmount - decimal(10, 2) ); GO
Сега таблицата изглежда по следния начин:
Ако приложим агрегатните функции към таблицата сега, ще получим добри резултати:
-- Getting order with minimum amount, maximum amount, average amount and total orders SELECT COUNT(*) AS Total_Orders ,MIN(TotalAmount) AS Min_Amount ,MAX(TotalAmount) AS Max_Amount ,AVG(TotalAmount) Average_Amount FROM SimpleOrder
Групиране на обобщени данни
Можем да групираме обобщените данни по всяка колона или набор от колони, за да получим обобщени данни въз основа на тази колона.
Например, ако искаме да знаем общия брой поръчки на дата, трябва да групираме таблицата по дата, като използваме клауза Group by, както следва:
-- Getting total orders per date SELECT OrderDate ,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY OrderDate
Резултатът е както следва:
Така че, ако искаме да видим сумата от цялата сума на поръчката, можем просто да приложим функцията сума към колоната с обща сума без групиране, както следва:
-- Sum of all the orders amount SELECT SUM(TotalAmount) AS Sum_of_Orders_Amount FROM SimpleOrder
За да получим сумата от сумата на поръчките на дата, ние просто добавяме група по дата към горния SQL оператор, както следва:
-- Sum of all the orders amount per date SELECT OrderDate ,SUM(TotalAmount) AS Sum_of_Orders FROM SimpleOrder GROUP BY OrderDate
Получаване на суми без групиране на данни
Можем веднага да получим суми като общи поръчки, максимална сума на поръчката, минимална сума на поръчката, сума от сума на поръчките, средна сума на поръчката, без да е необходимо групирането й, ако агрегирането е предназначено за всички таблици.
-- Getting order with minimum amount, maximum amount, average amount, sum of amount and total orders SELECT COUNT(*) AS Total_Orders ,MIN(TotalAmount) AS Min_Amount ,MAX(TotalAmount) AS Max_Amount ,AVG(TotalAmount) AS Average_Amount ,SUM(TotalAmount) AS Sum_of_Amount FROM SimpleOrder
Добавяне на клиенти към поръчките
Нека добавим малко забавление, като добавим клиенти в нашата таблица. Можем да направим това, като създадем друга таблица с клиенти и предадем идентификатор на клиента към таблицата с поръчки, но за да бъде опростено и да се подигравам на стила на склад за данни (където таблиците са денормализирани), добавям колоната с име на клиента в таблицата с поръчки, както следва :
-- Adding CustomerName column and data to the order table ALTER TABLE SimpleOrder ADD CustomerName VARCHAR(40) NULL GO UPDATE SimpleOrder SET CustomerName = 'Eric' WHERE OrderId = 1 GO UPDATE SimpleOrder SET CustomerName = 'Sadaf' WHERE OrderId = 2 GO UPDATE SimpleOrder SET CustomerName = 'Peter' WHERE OrderId = 3 GO UPDATE SimpleOrder SET CustomerName = 'Asif' WHERE OrderId = 4 GO UPDATE SimpleOrder SET CustomerName = 'Peter' WHERE OrderId = 5 GO
Получаване на общи поръчки на клиент
Можете ли да познаете сега как да получите общите поръчки на клиент? Трябва да групирате по клиент (CustomerName) и да приложите обобщената функция count() към всички записи, както следва:
-- Total orders per customer SELECT CustomerName,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY CustomerName
Добавяне на още пет записа към таблицата с поръчки
Сега ще добавим още пет реда към простата таблица за поръчки, както следва:
-- Adding 5 more records to order table INSERT INTO SimpleOrder (OrderDate, TotalAmount, CustomerName) VALUES ('01-Jan-2018', 70.50, 'Sam'), ('02-Jan-2018', 170.50, 'Adil'), ('03-Jan-2018',50.00,'Sarah'), ('04-Jan-2018',50.00,'Asif'), ('11-Jan-2018',50.00,'Peter') GO
Разгледайте данните сега:
-- Viewing order table after adding customer name and five more rows SELECT OrderId,CustomerName,OrderDate,TotalAmount FROM SimpleOrder GO
Получаване на общите поръчки на клиент, сортирани по максимални към минимални поръчки
Ако се интересувате от общите поръчки на клиент, сортирани по максимални към минимални поръчки, изобщо не е лоша идея да разделите това на по-малки стъпки, както следва:
-- (1) Getting total orders SELECT COUNT(*) AS Total_Orders FROM SimpleOrder
-- (2) Getting total orders per customer SELECT CustomerName,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY CustomerName
За да сортираме броя на поръчките от максимум до минимум, трябва да използваме клаузата Order By DESC (низходящ ред) с count() в края, както следва:
-- (3) Getting total orders per customer from maximum to minimum orders SELECT CustomerName,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY CustomerName ORDER BY COUNT(*) DESC
Получаване на общите поръчки на дата, сортирани първо по най-новата поръчка
Използвайки горния метод, сега можем да разберем общия брой поръчки на дата, сортирани първо по най-новата поръчка, както следва:
-- Getting total orders per date from most recent first SELECT CAST(OrderDate AS DATE) AS OrderDate,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY OrderDate ORDER BY OrderDate DESC
Функцията CAST ни помага да получим само частта за дата. Резултатът е както следва:
Можете да използвате колкото се може повече комбинации, стига да имат смисъл.
Изпълнение на агрегати
След като вече сме запознати с прилагането на агрегатни функции към нашите данни, нека да преминем към усъвършенстваната форма на агрегиране и едно такова агрегиране е текущото агрегиране.
Изпълняващите се агрегирания са агрегатите, приложени към подмножество от данни, а не към целия набор от данни, което ни помага да създаваме малки прозорци върху данните.
Досега видяхме, че всички агрегатни функции се прилагат към всички редове на таблицата, които могат да бъдат групирани по някаква колона, като дата на поръчка или име на клиента, но при работещи агрегати имаме свободата да прилагаме агрегатните функции, без да групираме цялото набор от данни.
Очевидно това означава, че можем да приложим агрегатната функция, без да използваме клаузата Group By, което е малко странно за онези начинаещи в SQL (или понякога някои разработчици пренебрегват това), които не са запознати с функциите за прозорец и изпълняването на агрегати.
Windows on Data
Както беше казано по-рано, текущото агрегиране се прилага към подмножество от набор от данни или (с други думи) към малки прозорци от данни.
Мислете за прозорците като набор(и) в набор или таблица(и) в таблица. Добър пример за показване на данни в прозореца в нашия случай е, че имаме таблицата с поръчки, която съдържа поръчки, поставени на различни дати, така че ако всяка дата е отделен прозорец, тогава можем да приложим обобщени функции към всеки прозорец по същия начин, по който приложихме към масата.
Ако сортираме таблицата с поръчки (SimpleOrder) по дата на поръчка (OrderDate), както следва:
-- View order table sorted by order date SELECT so.OrderId ,so.OrderDate ,so.TotalAmount ,so.CustomerName FROM SimpleOrder so ORDER BY so.OrderDate
Windows върху данни, готови за изпълнение на агрегации, можете да видите по-долу:
Можем също да разгледаме тези прозорци или подмножества като шест мини таблици, базирани на дата на поръчка, и агрегати, които могат да бъдат приложени към всяка от тези мини таблици.
Използване на дял от вътре OVER() Клауза
Изпълняващите се агрегации могат да се прилагат чрез разделяне на таблицата с помощта на „Разделяне по“ в клаузата OVER().
Например, ако искаме да разделим таблицата с поръчки по дати, като всяка дата е подтаблица или прозорец в набор от данни, тогава трябва да разделим данните по дата на поръчка и това може да се постигне с помощта на агрегатна функция като COUNT( ) с OVER() и разделяне от вътре OVER(), както следва:
-- Running Aggregation on Order table by partitioning by dates SELECT OrderDate, Total_Orders=COUNT(*) OVER(PARTITION BY OrderDate) FROM SimpleOrder
Получаване на текущи суми за прозорец за дата (раздел)
Текущите агрегирания ни помагат да ограничим обхвата на агрегиране само до определения прозорец и можем да получим текущи суми на прозорец, както следва:
-- Getting total orders, minimum amount, maximum amount, average amount and sum of all amounts per date window (partition by date) SELECT CAST (OrderDate AS DATE) AS OrderDate, Count=COUNT(*) OVER (PARTITION BY OrderDate), Min_Amount=MIN(TotalAmount) OVER (PARTITION BY OrderDate) , Max_Amount=MAX(TotalAmount) OVER (PARTITION BY OrderDate) , Average_Amount=AVG(TotalAmount) OVER (PARTITION BY OrderDate), Sum_Amount=SUM(TotalAmount) OVER (PARTITION BY OrderDate) FROM SimpleOrder
Получаване на текущи суми на клиентски прозорец (раздел)
Точно както текущите суми за прозорец с дата, ние също можем да изчислим текущите суми за прозорец на клиент, като разделим набора от поръчки (таблица) на малки подмножества от клиенти (раздели), както следва:
-- Getting total orders, minimum amount, maximum amount, average amount and sum of all amounts per customer window (partition by customer) SELECT CustomerName, CAST (OrderDate AS DATE) AS OrderDate, Count=COUNT(*) OVER (PARTITION BY CustomerName), Min_Amount=MIN(TotalAmount) OVER (PARTITION BY CustomerName) , Max_Amount=MAX(TotalAmount) OVER (PARTITION BY CustomerName) , Average_Amount=AVG(TotalAmount) OVER (PARTITION BY CustomerName), Sum_Amount=SUM(TotalAmount) OVER (PARTITION BY CustomerName) FROM SimpleOrder ORDER BY Count DESC,OrderDate
Плъзгащи се агрегати
Плъзгащите се агрегирания са агрегатите, които могат да бъдат приложени към рамките в рамките на прозорец, което означава допълнително стесняване на обхвата в рамките на прозореца (раздел).
С други думи, текущите суми ни дават суми (сума, средна, мин., макс., брой) за целия прозорец (подмножество), който създаваме в таблица, докато плъзгащите суми ни дават суми (сума, средна, мин., макс., брой) за рамката (подмножество от подмножество) в прозореца (подмножество) на таблицата.
Например, ако създадем прозорец върху данни, базирани на (разделяне по клиент) клиент, тогава можем да видим, че клиентът „Питър“ има три записа в прозореца си и всички агрегирания се прилагат към тези три записа. Сега, ако искаме да създадем рамка само за два реда наведнъж, това означава, че агрегирането се стеснява допълнително и след това се прилага към първия и втория ред и след това към втория и третия ред и така нататък.
Използване на ROWS ПРЕДШЕСТВУВАЩИ с клауза Order By вътре OVER()
Плъзгащите се агрегирания могат да бъдат приложени чрез добавяне на ROWS <брой редове> ПРЕДИШНИ с Подреждане по (след разделяне по), докато ROWS <число> ПРЕДИШНИ определят обхвата на рамката в прозореца.
Например, ако искаме да обобщим данни само за два реда наведнъж за всеки клиент, тогава се нуждаем от плъзгащи се агрегирания, които да бъдат приложени към таблицата с поръчки, както следва:
-- Getting minimum amount, maximum amount, average amount per frame per customer window SELECT CustomerName, Min_Amount=Min(TotalAmount) OVER (PARTITION BY CustomerName ORDER BY OrderDate ROWS 1 PRECEDING), Max_Amount=Max(TotalAmount) OVER (PARTITION BY CustomerName ORDER BY OrderDate ROWS 1 PRECEDING) , Average_Amount=AVG(TotalAmount) OVER (PARTITION BY CustomerName ORDER BY OrderDate ROWS 1 PRECEDING) FROM SimpleOrder so ORDER BY CustomerName
За да разберем как работи, нека разгледаме оригиналната таблица в контекста на рамки и прозорци:
В първия ред на прозореца на клиента Peter той направи поръчка със сумата от 30,50, тъй като това е началото на рамката в прозореца на клиента, така че min и max са същите, тъй като няма предходен ред за сравнение.
След това минималната сума остава същата, но максималната става 100,50, тъй като сумата на предишния ред (първи ред) е 30,50, а сумата на този ред е 100,50, така че максималната от двете е 100,50.
След това, преминавайки към третия ред, сравнението ще се извърши с втория ред, така че минималната сума на двата е 50,00, а максималната сума на двата реда е 100,50.
MDX функция от година до дата (от началото на годината) и работещи агрегати
MDX е език за многоизмерни изрази, използван за запитване на многоизмерни данни (като куб) и се използва в решенията за бизнес разузнаване (BI).
Според https://docs.microsoft.com/en-us/sql/mdx/ytd-mdx, функцията от година до дата (YTD) в MDX работи по същия начин, както работят изпълняваните или плъзгащите се агрегати. Например YTD, често използван в комбинация без предоставен параметър, показва текущата сума към момента.
Това означава, че ако приложим тази функция към годината, тя дава всички данни за годината, но ако направим разбивка до март, тя ще ни даде всички суми от началото на годината до март и така нататък.
Това е много полезно в SSRS отчети.
Неща за правене
Това е! Готови сте да направите някои основни анализи на данни, след като преминете през тази статия и можете да подобрите уменията си допълнително чрез следните неща:
- Моля, опитайте да напишете работещ скрипт за агрегати, като създадете прозорци в други колони, като например Обща сума.
- Моля, опитайте също да напишете скрипт за плъзгащи се агрегати, като създадете рамки в други колони, като например Обща сума.
- Можете да добавите още колони и записи към таблицата (или дори повече таблици), за да опитате други комбинации за агрегиране.
- Примерните скриптове, споменати в тази статия, могат да бъдат превърнати в съхранени процедури, които да се използват в SSRS отчети зад набор(и) от данни.
Препратки:
- Ytd (MDX)
- dbForge Studio за SQL Server