Бекапы баз данных для дальнейшей практики до конца курса (необходимо распаковать):
AdventureWorks2017.bak
AdventureWorksDW2017.bak
AdventureWorksLT2017.bak
Введение
Мы переходим к практике в рамках курса «Основы SQL». Будем писать запросы к тренировочным базам данных Microsoft. Используется
SQL Server 2017 (Structured Query Language Server), поэтому применяются соответствующие тренировочные базы. Основная —
AdventureWorks 2017 (AW2017). Также есть
AdventureWorks DW 2017 (Data Warehouse) и облегчённая версия
AdventureWorks LT (Lightweight) с меньшим числом таблиц. Кроме того, доступна база
Northwind. Все эти базы находятся в открытом доступе: можно скачать файлы резервных копий и развернуть их на своём SQL Server.
Начнём с самого простого запроса —
SELECT * (выбрать все столбцы) из таблицы.
Структура базы данных AdventureWorks LT
База данных AdventureWorks LT моделирует магазин, который торгует спортивными велосипедами и аксессуарами. У магазина есть розничные точки и онлайн-продажи.
Чтобы понять предметную область, рассмотрим диаграмму и связи между таблицами.
Таблица Customer и адреса
Customer (клиент) — покупатель магазина. С адресами она связана отношением «многие ко многим». Один клиент может иметь несколько зарегистрированных адресов. Связь осуществляется через таблицу
CustomerAddress с составным первичным ключом (CustomerID, AddressID). Адрес клиента и адрес доставки могут различаться: например, человек живёт по одному адресу, а заказал товар в подарок на другой.
Таблица
Address содержит подробную информацию: город, регион (StateProvince), страну (CountryRegion), почтовый индекс и полный адрес.
Товары, категории и модели
Таблица
Product представляет собой номенклатуру товаров. Каждый товар имеет:
•
ProductID — уникальный идентификатор;
•
Name — название;
•
ProductNumber — номер товара;
•
Color — цвет (может отсутствовать —
NULL);
•
StandardCost — закупочная цена;
•
ListPrice — розничная цена;
•
Size и
Weight — размер и вес;
•
ProductCategoryID — ссылка на категорию;
•
ProductModelID — ссылка на модель.
Существуют две независимые иерархии: категорий и моделей.
ProductCategory — таблица категорий, ссылающаяся сама на себя через
ParentProductCategoryID, что позволяет строить иерархию «родительская категория → подкатегория». Товар всегда относится к самой нижней (дочерней) категории.
ProductModel описывает модель товара. Её описания хранятся в связанных таблицах
ProductModelProductDescription и
ProductDescription, которые в рамках текущих занятий не играют большой роли.
Продажи: заказы и позиции
Основные таблицы для учёта продаж:
•
SalesOrderHeader — заголовок заказа (чек).
o
SalesOrderID — уникальный номер заказа.
o
OrderDate — дата заказа.
o
DueDate — дата, до которой заказ остаётся актуальным (например, пока не оплачен).
o
ShipDate — дата доставки.
o
Status — статус заказа (активен, удалён, доставлен и т.п.).
o
CustomerID — какой клиент сделал заказ.
o
ShipToAddressID и
BillToAddressID — адреса доставки и выставления счёта, оба ссылаются на таблицу Address.
o
SubTotal — промежуточный итог.
o
TaxAmt — налог.
o
Freight — стоимость доставки.
o
TotalDue — общая итоговая стоимость.
•
SalesOrderDetail — детализация заказа (позиции).
o
SalesOrderID и
SalesOrderDetailID — составной ключ (номер заказа + номер позиции).
o
ProductID — ссылка на товар.
o
OrderQty — количество заказанного товара.
o
LineTotal — итог по позиции (цена × количество).
Логика расчёта: LineTotal складываются по всем позициям, получается SubTotal, к нему прибавляются налог и стоимость доставки, образуя TotalDue.
Простейшие запросы SELECT
Выборка всех данных
Простой запрос извлекает все столбцы из таблицы Customer:
sql
SELECT * FROM SalesLT.Customer;
Результат — все строки с данными клиентов: идентификатор, NameStyle, Title, имя, фамилия, компания, email, телефон и т.д.
Выборка отдельных столбцов и сортировка
Следующий запрос возвращает название компании, имя и фамилию покупателя, упорядоченные по названию компании.
ORDER BY CompanyName по умолчанию сортирует по возрастанию (ASC), что можно указывать явно.
sql
SELECT CompanyName, FirstName, LastName
FROM SalesLT.Customer
ORDER BY CompanyName;
В результате видны повторяющиеся имена одного и того же покупателя. Это связано с тем, что при изменении любых данных клиента (компания, телефон, email и т.д.) не происходит обновления старой записи — создаётся новая, чтобы сохранить историю. Например, если покупатель сменил компанию, все прошлые заказы должны остаться привязанными к старой учётной записи, а новые — к новой. Так поддерживается корректность анализа (например, «сотрудники какой компании делают больше заказов»). В таблице есть поле
ModifiedDate, фиксирующее дату последнего изменения.
Сортировка по нескольким полям
Запрос возвращает CompanyName, FirstName, LastName и SalesPerson (сотрудник, привлёкший клиента). Сортировка производится последовательно: сначала по фамилии, затем по имени, затем по названию компании.
sql
SELECT CompanyName, FirstName, LastName, SalesPerson
FROM SalesLT.Customer
ORDER BY LastName, FirstName, CompanyName;
Если несколько записей для одного человека выглядят одинаково, значит, изменения касались других атрибутов (не компании).
Использование DISTINCT
Оператор
DISTINCT позволяет оставить только уникальные значения. Например, чтобы получить список продавцов, привлёкших хотя бы одного клиента:
sql
SELECT DISTINCT SalesPerson FROM SalesLT.Customer;
Без DISTINCT запрос возвращает 847 строк — по строке на каждого клиента с повторением продавцов. С DISTINCT — только девять уникальных имён.
Теперь применим группировку и агрегацию. Запрос выводит продавца и количество привлечённых им клиентов.
GROUP BY по полю SalesPerson,
COUNT(*) считает строки в каждой группе.
sql
SELECT SalesPerson, COUNT(*) AS NumberOfCustomers
FROM SalesLT.Customer
GROUP BY SalesPerson;
Результат показывает, сколько клиентов привлёк каждый продавец. Можно отсортировать по количеству клиентов:
sql
ORDER BY NumberOfCustomers DESC;
Фильтрация групп с помощью HAVING
Оператор
HAVING фильтрует группы после группировки. Он выполняется раньше формирования итоговой выборки, поэтому в условии нельзя использовать псевдоним (AS) — нужно указывать исходное выражение.
Например, оставить только продавцов, привлёкших более 100 клиентов:
sql
SELECT SalesPerson, COUNT(*) AS NumberOfCustomers
FROM SalesLT.Customer
GROUP BY SalesPerson
HAVING COUNT(*) > 100
ORDER BY NumberOfCustomers DESC;
Важно:
WHERE фильтрует строки до группировки,
HAVING — после. В данном случае фильтрация по количеству клиентов возможна только в HAVING.
Запросы к таблице Product
Вывести все товары, отсортированные по номеру продукта:
sql
SELECT * FROM SalesLT.Product
ORDER BY ProductNumber;
Всего 295 товаров.
Выбрать идентификатор, название, номер, модель и категорию, сортируя сначала по ID категории, затем по номеру продукта:
sql
SELECT ProductID, Name, ProductNumber, ProductModelID, ProductCategoryID
FROM SalesLT.Product
ORDER BY ProductCategoryID, ProductNumber;
Вывести товары с их ценами, отсортировав по розничной цене от самой высокой:
sql
SELECT ProductID, Name, Color, StandardCost, ListPrice
FROM SalesLT.Product
ORDER BY ListPrice DESC;
Самый дорогой товар — с ListPrice 3578, StandardCost 2171 (наценка около 60%).
DISTINCT на идентификаторах моделей и цветах
Список уникальных идентификаторов моделей из таблицы ProductModel (все 128 моделей):
sql
SELECT DISTINCT ProductModelID FROM SalesLT.ProductModel;
Из таблицы Product DISTINCT ProductModelID возвращает 119 моделей — значит, девять моделей не присвоены ни одному товару. Без DISTINCT получим 295 строк (по количеству товаров) с повторяющимися моделями.
Какие различные цвета встречаются у товаров?
sql
SELECT DISTINCT Color FROM SalesLT.Product
ORDER BY Color;
В результате появляется значение
NULL, означающее, что у некоторых товаров цвет не указан. DISTINCT обрабатывает все NULL как одно уникальное значение, что соответствует естественному ожиданию: «цвет не указан» должен считаться одним вариантом. При этом сравнение NULL с NULL в SQL не определено, но для DISTINCT сделано исключение.
Сколько товаров без указания цвета?
sql
SELECT ProductID, Name
FROM SalesLT.Product
WHERE Color IS NULL;
Возвращается 50 товаров.
Запросы к таблице Address
Извлечение всех полей:
sql
SELECT * FROM SalesLT.Address;
Уникальные комбинации города, региона, страны и почтового индекса:
sql
SELECT DISTINCT City, StateProvince, CountryRegion, PostalCode
FROM SalesLT.Address
ORDER BY CountryRegion, StateProvince, City;
Это позволяет увидеть все уникальные почтовые адреса, с учётом того, что одинаковый индекс в разных городах или регионах может обозначать разные места.
Уникальные регионы и страны:
sql
SELECT DISTINCT StateProvince, CountryRegion
FROM SalesLT.Address
ORDER BY CountryRegion, StateProvince;
Возвращается 25 комбинаций.
Запрос к детализации заказов
Все позиции заказов, сортировка по количеству товара в позиции:
sql
SELECT * FROM SalesLT.SalesOrderDetail
ORDER BY OrderQty;
В одной из позиций заказано 25 единиц товара с ProductID = 976. Выяснить название этого товара можно отдельным запросом:
sql
SELECT Name FROM SalesLT.Product WHERE ProductID = 976;
Это дорожный велосипед жёлтого цвета. В дальнейшем такие задачи будут решаться с помощью соединений (
JOIN), которые позволяют объединять данные из нескольких таблиц в одном запросе.
Фильтрация по текстовому полю
Найти компанию конкретного клиента по имени, отчеству и фамилии:
sql
SELECT CompanyName
FROM SalesLT.Customer
WHERE FirstName = 'James' AND MiddleName = 'D.' AND LastName = 'Kramer';
Результат — название компании, в которой работает James D. Kramer.
Переход к соединениям таблиц
Мы рассмотрели базовые возможности SELECT: выбор столбцов, фильтрацию (WHERE), устранение дубликатов (DISTINCT), сортировку (ORDER BY), группировку с агрегацией (GROUP BY, COUNT) и фильтрацию групп (HAVING). Следующий этап — соединения (JOIN), которые позволяют связывать таблицы для получения более сложных отчётов и анализа.
Краткие итоги
Знакомство с SQL начинается не с синтаксиса команд, а с погружения в структуру конкретной базы данных. Детальный разбор таблиц и связей AdventureWorks LT служит фундаментом для осмысленного написания запросов: прежде чем извлекать данные, необходимо понять, как они организованы, где хранятся ключевые сущности и как они соотносятся. Этот подход избавляет от механического заучивания и развивает способность самостоятельно ориентироваться в незнакомой схеме.
Построение запросов выстроено от простейшей операции — получения всех данных таблицы — до аналитических конструкций с группировкой и фильтрацией групп. Последовательное усложнение позволяет прочувствовать каждый элемент языка в действии. Применение DISTINCT для удаления дубликатов, сортировка по одному или нескольким полям, агрегация с COUNT и фильтрация через HAVING — всё это рассматривается не изолированно, а в контексте практических задач: кто из продавцов привлёк больше клиентов, какие товары самые дорогие, сколько моделей не представлены товарами, какие цвета встречаются в ассортименте.
Особое внимание уделено тонким моментам, критичным для реальной работы. Поведение DISTINCT при наличии NULL демонстрирует, как стандарт SQL адаптируется к человеческому восприятию, не нарушая формальной логики сравнения неопределённостей. Появление дублирующихся записей клиентов объясняется не ошибкой, а осознанным проектным решением — ведением истории изменений, что напрямую влияет на корректность аналитики. Разграничение WHERE и HAVING подчёркивает важность понимания порядка выполнения операторов: попытка отфильтровать агрегированные значения в WHERE обречена на неудачу, а использование псевдонимов в HAVING невозможно из-за стадии обработки запроса.
Практическая ценность освоенных приёмов выходит далеко за рамки учебной базы. Умение формулировать условия фильтрации, группировать данные и отсеивать группы по агрегированным показателям лежит в основе построения любых отчётов и информационных панелей. Заложенное в лекции понимание «почему это работает именно так», а не только «как написать», становится надёжной базой для перехода к более сложным темам — многотабличным запросам и соединениям, где эти элементарные блоки соберутся в мощный инструмент извлечения знаний из данных.
1. Перед написанием запросов необходимо разобраться в структуре базы данных и связях между таблицами.
2. Запрос SELECT * FROM таблица возвращает все строки и столбцы указанной таблицы.
3. ORDER BY позволяет сортировать результаты по одному или нескольким полям, с явным указанием направления (ASC/DESC).
4. Изменение данных клиента ведёт к созданию новой записи, чтобы сохранить историю и обеспечить корректность будущей аналитики.
5. DISTINCT удаляет дубликаты и возвращает только уникальные комбинации значений выбранных столбцов.
6. При использовании DISTINCT все значения NULL рассматриваются как один уникальный вариант.
7. Агрегатные функции, такие как COUNT, в сочетании с GROUP BY позволяют получать сводные показатели по группам.
8. HAVING фильтрует группы после выполнения группировки и не может использовать псевдонимы столбцов.
9. WHERE фильтрует строки до группировки, HAVING — после, поэтому условия с агрегатными функциями применимы только в HAVING.
10. Для поиска записей с отсутствующими значениями используется конструкция IS NULL.
11. Уникальные почтовые адреса определяются сочетанием города, региона, страны и индекса, а не индексом по отдельности.
12. Освоенные простые запросы формируют базу для изучения соединений таблиц (JOIN).
1. Какую информацию содержит таблица SalesOrderHeader, и как она связана с SalesOrderDetail?
2. Почему в результате запроса с DISTINCT по полю Color возвращается только одна строка с NULL, даже если товаров без цвета несколько?
3. Чем отличается фильтрация с помощью WHERE от HAVING? Приведите пример ситуации, когда необходимо использовать HAVING.
4. Можно ли в условии HAVING использовать псевдоним столбца, заданный через AS? Почему?
5. Какая сортировка применяется, если в ORDER BY не указано направление (ASC или DESC)?
6. Почему один и тот же покупатель может быть представлен несколькими записями в таблице Customer?
7. Сколько уникальных моделей фактически присвоено товарам, если в таблице ProductModel 128 записей, а запрос с DISTINCT ProductModelID из Product возвращает 119?
8. Какой запрос позволяет найти все товары, у которых не указан цвет?
9. Почему при формировании уникальных почтовых адресов недостаточно использовать только почтовый индекс, а нужны также город, регион и страна?
10. В каком порядке выполняются операторы GROUP BY, HAVING и ORDER BY в запросе?
11. Что произойдёт, если в запросе с GROUP BY и COUNT, но без HAVING, добавить условие WHERE для фильтрации по количеству клиентов?
12. Каким образом можно получить список продавцов, привлёкших больше 100 клиентов, и отсортировать их по убыванию количества клиентов?