Реляционная модель определяет способ представления данных (структуру данных), методы защиты данных (целостность данных), и операции, которые можно выполнять с данными (манипулирование данными).
Отношение - структура данных целиком, набор записей (иногда называют таблица)
Кортеж - каждая строка, содержащая данные (иногда называют запись)
Мощность - число кортежей в таблице
Атрибут - столбец в таблице (иногда называют поле)
Размерность - число атрибутов в таблице
Домен атрибута - допустимые значения, которые можно занести в поле
Обязательным этапом перед созданием БД является ее проектирование.
Имя таблицы (отношения) должно быть уникальным в пределах БД. Рекомендуется существительное в единственном числе, понятное имя, описывающее моделируемый объект, имя должно быть короткое (до 10 символов).
Имя поля должно быть уникальным в пределах таблицы. Рекомендуется разделять составные имена подчеркиванием.
Ключевые поля должны состоять из двух частей: названия таблицы и id
, например book_id
.
int, integer
- целые числа (32 bit)
decimal, numeric
- вещественное число, в скобках указывается максимальная длина числа. Например decimal(6,3)
означает вещественное число с 6 цифрами в числе и 3 цифры после запятой, например для decimal(6,3)
это 123,456
.
date
- дата в формате ГГГГ-ММ-ДД
varchar
- строка длиной не более 255 символов в однобайтовой кодировке.
Для описания ключевого поля можно использовать int primary key auto_increment
Для создания таблицы используется SQL-запрос. Ключевое слово create table
. Пример запроса:
create table genre(
genre_id int primary key auto_increment,
name_genre varchar(30)
);
В конце sql-запроса ставится точка с запятой. Но для одного запроса это не обязательно.
Для занесения новой записи в таблицу используется SQL-запрос, в котором указывается в какую таблицу, в какие поля заносить новые значения.
Пример запроса:
insert into table(field1, field2)
values (value1, value2);
В результате выполнения этого запроса новая запись добавится в конец указанной таблицы.
Рекомендуется заполнять все поля записи, если же поле пропущено, значение этого поля зависит от установленных по умолчанию значений, если значения не установлены, то вставляется null
.
Значения поля primary key auto_increment
формируется автоматически, т.е. его не нужно указывать при добавлении значения в таблицу.
Для выборки данных из таблицы используется ключевое слово select
. Также для выборки всех данных без указания атрибутов таблицы можно использовать знак *
. Таким образом для выборки всех записей из таблицы используется запрос:
select * from table
Для выборки отдельных столбцов после ключевого слова select
необходимо написать атрибуты, которые мы хотим видеть в выборке. Например:
select attribute1,
attrubite2
from table;
Для того чтобы присвоить атрибуту новое имя используется ключевое слово as
. Например:
select attribute1 as new_name_attribute1,
attribute2
from table;
Результатом выборки является таблица, в которую включены все данные из указанных после select
столбцов, а также новый столбец, в каждой строке которого вычисляется заданное выражение.
Например:
select attribute1,
attribute2,
attribute3 * constant as new_attribute3
from table;
Некоторые из функций, реализованных в SQL:
-
ceiling(x)
- округление до целого в большую сторону -
round(x,k)
- округление x до k знаков после запятой, если не указано k, то округлят до целого -
floor(x)
- округляет до целого числа в меньшую сторону -
power(x,y)
- возведение x в степень y -
sqrt(x)
- квадратный корень из x -
degrees(x)
- конвертирует значение x из радиан в градусы -
radians(x)
- конвертирует значение и из градусов в радианы -
abs(x)
- модуль числа x -
PI()
- число$\pi$
В SQL можно заносить в поле значение на основании условия. Для это применяют функцию if()
:
if(expression, result_if_true, result_if_false)
С помощью запросов можно включать в итоговую выборку не все строки исходной таблицы, а только те, которые соответствуют условию. Для этого после указания таблицы задается ключевое слово where
и логическое выражение.
Операторы сравнения могут быть:
Оператор | Значение |
---|---|
= | равно |
<> | не равно |
> | больше |
< | меньше |
>= | больше или равно |
<= | меньше или равно |
Например:
select attribute1,
attribute2,
from table
where attribute1 * constant1 >= contstant2;
Логическое выражение может включать логические операции and
,or
,not
и круглые скобки.
Приоритет операций:
-
Скобки
-
Умножение, деление
-
Сложение, вычитание
-
Операторы сравнения
-
not
-
and
-
or
Например:
select attribute1,
attribute2,
from table
where attribute1 > constant1 and attribute2 = contstant2;
Логические выражения могут включать операторы between
и in
. Приоритет этих операторов как у оператора if
.
Оператор between
отбирает данные, относящиеся к некоторому интервалу, включая границы.
Оператор in
выбирает данные, соответствующие значениям из списка.
Для сортировки используется ключевое слово order by
, после которого указываются имена столбцов. Строки сортируются по первому столбцу, а если указан второй, то сортировка происходит только для тех строк, у которых значение первого столбца одинаковы. По умолчанию сортировка при помощи order by
происходит по возрастанию, чтобы указать порядок сортировки после имени столбца можно указать ключевое слово asc
(по возрастанию) или desc
(по убыванию).
Столбцы для order by
можно указывать названием столбца, номером столбца или псевдонимом (указанным после as
).
Оператор like
используется для сравнения строк. Этот оператор позволяет сравнивать строки на неполное совпадение, а по шаблону. Шаблон может включать любой символ и символ-шаблон (%
и _
). Где %
- строка любой длины, а _
- любой одиночный символ.
Например like '%M.%'
выберет все строки, содержащие M.
, а like 'Поэм_'
выберет все строки, начинающиеся с Поэм
и заканчивающиеся любым символом.
Чтобы отобрать уникальные элементы используют ключевое слово distinct
после select
.
Например:
select distinct attribute1
from table;
Для этого также можно использовать оператор group by
, который группирует данные при выборке с одинаковыми значениями.
Тот же пример, что и выше, но с использованием group by
select attribute1
from table
group by author;
В разделе group by
также можно указывать несколько столбцов, разделяя их запятыми. Тогда к одной группе будут относиться записи, у которых значения столбцов, входящих в группу, равны.
Пример:
select name, number_plate, violation, count(*)
from fine
group by name, number_plate, violation;
При группировке можно выполнить действие с элементами, входящими в группу, например просуммировать все сгруппированные значения.
Пример группировки:
select attribute1,
sum(attribute2),
sum(attribute3)
from table
group by attribute1;
sum
позволяет сложить элементы каждой группы, а count
позволяет посчитать количество элементов в каждой группе.
count(*)
- подсчитывает все записи в группе, в том числе со значением null
.
count(attribute)
- подсчитывает все записи в группе с условием not null
.
В агрегатных функциях (таких как sum
и count
) можно использовать ключевое слово distinct
для подсчета уникальных значений.
К групповым функциям относят: min()
, max()
, avg()
.
В качестве аргумента групповой функции может использоваться любое допустимое в SQL арифметическое выражение.
Например:
select attribute1,
sum(attribute2 * attribute3) as name
from table
group by attribute1;
Групповые функции могут быть элементами выражений. Т.е. можно написать
round(sum(attribute2 * attribute3), 2)
Групповые функции можно применять к целому столбцу без группировки.
Например:
select sum(attribute)
from table;
Результатом таких запросов является единственная строка с вычисленными по таблице значениями.
В запросы с групповыми функциями можно включать условие отбора строк. В таких запросах вместо where
используется ключевое слово having
, которое размещается после group by
.
Например:
select attribute1,
min(attribute2),
max(attribute2)
from table
group by attribute1
having sum(attribute2 * attribute3) > constant1
Оптимизация от ChatGPT:
WITH temp AS (
SELECT attribute1, attribute2, attribute3
FROM table
WHERE (attribute2 IS NOT NULL) AND (attribute3 IS NOT NULL)
)
SELECT attribute1, MIN(attribute2), MAX(attribute2)
FROM temp
GROUP BY attribute1
HAVING SUM(attribute2 * attribute3) > constant1;
where
и having
могут использоваться в одном запросе. При этом необходимо учитывать порядок выполнения SQL-запроса:
-
FROM
-
WHERE
-
GROUP BY
-
HAVING
-
SELECT
-
ORDER BY
Например:
select author,
min(price) as Minimum_price,
max(price) as Maximum_price
from book
where author <> 'Есенин С.А.'
group by author
having sum(amount) > 10;
Вложенный запрос - запрос внутри другого запроса SQL.
Вложенный запрос используется для выборки данных, которые будут использоваться в условии отбора записей основного запроса. Его применяют для:
-
сравнения выражения с результатом вложенного запроса
-
определения того, включено ли выражение в результаты вложенного запроса
-
проверки того, выбирает ли запрос определенные строки
Составляющие вложенного запроса:
- Ключевое слово
select
после которого указываются имена столбцов или выражения - Ключевое слово
from
и имя таблицы - Необязательное предложение
where
- Необязательное предложение
group by
иhaving
Вложенные запросы могут включаться в where
и having
так:
where | having
expression оператор сравнения (вложенный запрос)where | having
expression, включающее вложенный запросwhere | having
expression[NOT] IN
(вложенный запрос)where | having
expression оператор сравненияANY | ALL
(вложенный запрос)
Также вложенные запросы могут вставляться в основной запрос после ключевого слова select
.
Такой запрос может использоваться в условии отбора записей where
как обычное значение вместе с операторами сравнения.
Например:
select title, author, price, amount
from book
where price = (
select min(price)
from book
);
Этот запрос выводит информацию о самых дешевых книгах, хранящихся на складе.
Вложенный запрос, возвращающий одно значение, можно использовать в выражениях как обычный операнд.
Например:
select title, author, amount
from book
where abs(amount - (select avg(amount) from book)) >3;
Этот запрос выведет информацию о книгах, количество экземпляров которых отличается от среднего количества экземпляров книг на складе более чем на 3.
Вложенный запрос может возвращать несколько значений одного столбца. Тогда его можно использовать внутри оператора in
.
where name_of_attribute in (subquery)
Например:
select title, author, amount, price
from book
where author in (
select author
from book
group by author
having sum(amount) >= 12
);
Этот запрос выводит информацию о книгах тех авторов, общее количество экземпляров книг которых не менее 12.
Вложенный запрос, возвращающий несколько значений одного столбца можно использовать для отбора записей с помощью операторов ANY
и ALL
совместно с операциями сравнения.
Операторы ANY
и ALL
используются для сравнения некоторого значения с результирующим набором вложенного запроса, состоящим из одного столбца.
При использовании ANY
в результирующую таблицу будут включены все записи, для которых выражение со знаком сравнения верно хотя бы для одного элемента результирующего запроса.
Работа ANY
:
-
amount > ANY(10,12)
эквивалентноamount > 10
-
amount < ANY(10,12)
эквивалентноamount < 12
-
amount = ANY(10,12)
эквивалентноamount IN (10,12)
или(amount = 10) or (amount = 12)
-
amount <> ANY(10,12)
вернет все записи с любым значением, включая 10 и 12.
При использовании оператора ALL
в результирующую таблицу будут включены все записи, для которых выражение со знаком сравнения верно для всех элементов результирующего запроса.
Работа ALL
:
-
amount > ALL(10,12)
эквивалентноamount > 12
-
amount < ALL(10,12)
эквивалентноamount < 10
-
amount = ALL(10,12)
эквивалентно(amount = 10) and (amount = 12)
такая запись ничего не вернет -
amount <> ALL(10,12)
вернет все записи кроме тех, где amount равно 10 или 12
Операторы ALL
и ANY
можно использовать только с вложенными запросами.
Например:
select title, author, amount, price
from book
where amount < all(
select avg(amount)
from book
group by author
);
Этот запрос выведет информацию о тех книгах, количество которых меньше самого маленького среднего количества книг каждого автора.
В этом случае результат выполнения запроса выводится в отдельном столбце результирующей таблицы. При этом результатом запроса может быть только одно значение, тогда оно будет повторяться во всех строках. Также вложенный запрос может использоваться в выражениях.
Например:
select title, author, amount,
(
select avg(amount)
from book
) as Среднее_количество
from book
where abs(amount - (select avg(amount) from book)) >3;
Данный запрос выводит информацию о книгах, количество экземпляров которых отличается от среднего количества экземпляров книга на складе более чем на 3.
Создание таблицы осуществляется с помощью запроса create
.
Добавление одной записи в таблицу осуществляется с помощью запроса insert
.
Допускается вставка нескольких записей одновременно, для этого используется запрос такого вида:
insert into table_name(attribute1, attribute2, ..., attributeN)
values
(value1_1, value1_2, ..., value1_N),
(value2_1, value2_2, ..., value2_N),
...
(valueM_1, valueM_2, ..., valueM_N);
Пример:
insert into book (title, author, price, amount)
values
('Война и мир','Толстой Л.Н.', 1070.20, 2),
('Анна Каренина', 'Толстой Л.Н.', 599.90, 3);
Вместо раздела values
записывается запрос на выборку, начинающийся с select
. В нем можно использовать where, group by, order by
.
Правила соответствия между полями таблицы и вставляемыми значениями из запроса:
-
Количество полей в таблице и количество полей в запросе должны совпадать
-
Должно существовать прямое соответствие между позицией одного и того же элемента в обоих списках, поэтому первый столбец запроса должен относиться к первому столбцу в списке столбцов таблицы, второй - ко второму и т.д.
-
Типы столбцов запроса должны быть совместимы с типами данных соответствующих столбцов таблицы.
Например:
insert into book (title, author, price, amount)
select title, author, price, amount
from supply;
Этот запрос вносит все книги из таблицы supply
в таблицу book
.
В запросах на добавление можно использовать вложенные запросы.
Например:
insert into book (title, author, price, amount)
select title, author, price, amount
from supply
where title not in(
select title
from book
);
select * from book;
Этот запрос вносит в таблицу book записи из таблицы supply, которые отсутствуют в book.
Обновление данных - изменение существующих записей в таблице. Изменение записей доступно с помощью запроса update
.
Пример:
update table_name set field = expression;
С помощью запросов на обновление можно изменять не все записи в таблице, а только часть из них. Для этого в запрос добавляют where
, после которого указывается условие отбора строк для изменения.
Пример:
update book
set price = 0.7 * price
where amount < 5;
Пример:
update table_name set field1 = expression1, field2 = expression2
Пример:
update
book
set
buy = if (buy > amount, amount, buy),
price = if (buy = 0, price * 0.9, price);
В запросах на обновление можно использовать несколько таблиц, но тогда:
-
Для столбцов, имеющих одинаковые имена, необходимо указывать имя таблицы, к которой они относятся (например
book.price
илиsupply.price
) -
Все таблицы, используемые в запросе, нужно перечислить после ключевого слова
update
-
В запросе обязательно условие
where
, в котором указывается условие при котором обновляются данные
Пример:
update book, supply
SET book.amount = book.amount + supply.amount
WHERE book.title = supply.title AND book.author = supply.author;
Запросы корректировки данных позволяют удалить одну или несколько записей из таблицы.
Пример:
delete from table_name
Этот запрос удаляет все записи из указанной таблицы.
Удалять можно по условию.
Пример:
delete from table_name
where condition;
Новая таблица может быть создана на основе данных из другой таблицы. Для этого используется запрос select
, результирующая таблица которого и будет новой таблицей базы данных.
Пример:
create table table_name as
select ...
Пример:
create table ordering as
select author, title, 5 as amount
from book
where amount < 4;
Этот запрос создает таблицу заказ, куда включает авторов и название тех книг, количество экземпляров которых в таблице book меньше 4 и устанавливает количество экземпляров равным 5.
При создании таблицы можно использовать вложенные запросы после select
и where
.
Пример:
create table ordering as
select author,
title,
(select round(avg(amount))
from book) as amount
from book
where amount < 4;
Используется для ограничения вывода записей. Результирующая таблица будет иметь количество строк не более указанного после limit
. limit
размещается после раздела order by
.
Пример:
select *
from trip
order by date_first
limit 1;
Для вычитания двух дат используется функция datediff(date1, date2)
. Результатом является количество дней между date1 и date2.
Пример:
DATEDIFF('2020-04-01', '2020-03-28')=4
DATEDIFF('2020-05-09','2020-05-01')=8
Для того, чтобы выделить номер месяца из даты используется функция month(date)
.
Пример:
month('2020-04-12')=4
Для выделения названия месяца из даты используется функция monthname(date)
, которая возвращает название месяца на английском языке для указанной даты.
Пример:
monthname('2020-04-12')='April'
Алиас - псевдоним, который присваивается столбцам или таблицам. Есть 2 варианта присваивания:
-
С использованием ключевого слова
as
from table_name as t
-
Без ключевого слова
as
from table_name t
После присвоения таблице алиаса его можно использовать во всех разделах запроса.
Пример:
select f.name, f.number_plate, f.violation,
if(
f.sum_fine = tv.sum_fine, "Стандартная сумма штрафа",
if(
f.sum_fine < tv.sum_fine, "Уменьшенная сумма штрафа", "Увеличенная сумма штрафа"
)
) as description
from fine f, traffic_violation tv
where tv.violation = f.violation and f.sum_fine is not NULL;
Средствами SQL запросов можно выбирать и обрабатывать данные не только из одной таблицы, но из нескольких связанных таблиц.
Рассмотрим таблицу book:
book_id | title | name_author | price | amount |
---|---|---|---|---|
1 | Мастер и Маргарита | Булгаков М.А. | 670.99 | 3 |
2 | Белая гвардия | Булгаков М.А. | 540.50 | 5 |
3 | Идиот | Достоевский Ф.М. | 460.00 | 10 |
4 | Браться Карамазовы | Достоевский Ф.М. | 799.01 | 2 |
5 | Стихотворения и Поэмы | Есенин С.А. | 650.00 | 15 |
В этой таблице фамилии авторов повторяются для нескольких книг. А что, если придется вместо инициалов для каждого автора хранить его полное имя и отчество? Тогда, если в таблице содержится информация о 50 книгах Достоевского, придется 50 раз исправлять «Ф.М.» на «Федор Михайлович». При этом, если в некоторых записях использовать «Фёдор Михайлович» (c буквой ё), то мы вообще получим двух разных авторов...
Чтобы устранить эту проблему в реляционных БД создается новая таблица author
, в которой перечисляются все различные авторы, а затем эта таблица связывается с таблицей book
. При этом такая связь называется "один ко многим", таблицы author
называется главной, таблица book
- связанной или подчиненной.
Связь "один ко многим" имеет место, когда одной записи главной таблицы соответствует несколько записей связанной таблицы, а каждой записи связанной таблицы соответствует только одна запись главной таблицы.
Обозначается это так:
[Автор] --> [Книга]
Чтобы реализовать связь "один ко многим" для нашего примера (где автор может написать несколько книг, а одна книга написана только одним автором) необходимо:
-
Создать таблицу
author
, в которую включить уникальных авторов книг, хранящихся на складе -
Обе таблицы должны содержать первичный ключ, в таблице
book
он уже есть, а в таблицеauthor
добавим ключauthor_id
-
Включим в таблицу
book
связанный столбец (внешний ключ,foreign key
) соответствующий по имени и типу ключевому столбцу главной таблицы (в нашем случае этоauthor_id
)
Есть книги, написанные двумя и более авторами, для таких книг связь "один ко многим" не подходит.
Для таких случаев используется связь многие ко многим.
Связь "многие ко многим" имеет место когда каждой записи одной таблицы соответствует несколько записей во второй, и наоборот, каждой записи второй таблицы, соответствует несколько записей в первой.
Обозначается это так:
[Автор] <--> [Книга]
Чтобы реализовать связь "многие ко многим" нужно:
-
Создать таблицу
author
, в которую включить уникальных авторов книг, хранящихся на складе -
В обеих таблицах необходимо определить первичный ключ, в таблице
book
он уже есть, а для таблицыauthor
он будетauthor_id
-
Создать новую таблицу-связку, состоящую из двух столбцов, соответствующих по имени и типу ключевым столбцам исходных таблиц. Каждый из этих столбцов является внешним ключом (
foreign key
) и связан с ключевым столбцом каждой таблицы. -
Определяемся с первичным ключом таблицы-связки. Можно сделать два ключевых столбца, тогда все записи в этой таблице должны быть уникальными, то есть не повторяться. Для связи автор-книга это подходит, но в некоторых случаях записи в таблице-связке могут повторяться, например, если мы будем продавать книги покупателям (один человек может купить несколько книг, а одну и ту же книгу могут купить несколько человек). Тогда в таблицу-связку включают дополнительные столбцы для идентификации записей, например, дату продажи, также в таблицу-связку добавляют первичный ключ. Второй вариант предпочтительнее.
При создании зависимой таблицы (таблицы, которая содержит внешние ключи) необходимо учитывать, что:
-
каждый внешний ключ должен иметь такой же тип данных, как связанное поле главной таблицы
-
необходимо указать главную для нее таблицу и столбец, по которому осуществляется связь
foreign key (связанное_поле_зависимой_таблицы) references главная_таблица (связанное_поле_главной_таблицы)
Пример добавления внешнего ключа:
foreign key (author_id) references author (author_id)
По умолчанию любой столбец, кроме ключевого, может содержать значение null
. При создании таблицы это можно переопределить, используя ограничение not null
для этого столбца:
create table table_name (
attribute1 int not null,
attribute2 varchar(10)
);
В созданной таблице attribute1
не может быть null
, а attribute2
может.
Для внешних ключей рекомендуется устанавливать ограничение not null
(если это совместимо с другими опциями).
С помощью выражения on delete
можно установить действия, которые выполняются для записей подчиненной таблицы при удалении связанной строки из главной таблицы. При удалении можно установить следующие опции:
-
cascade
- автоматически удаляет строки из зависимой таблицы при удалении связанных строк в главной таблице -
set null
- при удалении связанной строки из главной таблицы устанавливает для столбца внешнего ключа значениеnull
(в этом случае столбец внешнего ключа должен поддерживать установкуnull
) -
set default
- значение внешнего ключа устанавливается в значение по умолчанию для данного столбца -
restrict
- отклоняет удаление строк в главной таблице при наличии связанных строк в зависимой таблице
Например, будем считать сто при удалении автора из таблицы author
необходимо удалить все записи о его книгах из таблицы book
. Это действие необходимо прописать при создании таблицы:
create table book (
book_id int primary key auto_increment,
title varchar(50),
author_id int not null,
price decimal(8,2),
amount int,
foreign key (author_id) references author (author_id) on delete cascade
);
В SQL запросах могут участвовать несколько таблиц БД. При этом необходимо указать, как эти таблицы соединены между собой.
Операция соединения join
предназначена для обеспечения выборки данных из двух таблиц и включения этих данных в один результирующий набор. При необходимости соединения не двух, а нескольких таблиц, операция соединения применяется несколько раз (последовательно).
Операторы соединения входят в раздел from
SQL запросов.
Оператор внутреннего соединения inner join
соединяет две таблицы. Порядок таблиц для оператора неважен, поскольку оператор симметричен. В условии пишется условие сопоставление каких-либо атрибутов первой и второй таблицы.
Пример:
select
*
from
table_name1 inner join table_name2
on condition
Результат запроса сформируется по следующему алгоритму:
-
Каждая строка одной таблицы сопоставляется с каждой строкой второй таблицы
-
Для получения соединенной строки проверяется условие соединения
-
если условие истинно, в таблицу результата добавляется соответствующая "соединенная" строка
Пример:
select
title,
name_genre,
price
from
book inner join genre
on genre.genre_id = book.genre_id
where book.amount > 8
order by price desc;
Запрос выводит название, жанр и цену тех книг, количество которых больше 8, в отсортированном по убыванию цены виде.
Оператор внешнего соединения LEFT OUTER JOIN
(или просто LEFT JOIN
) соединяет две таблицы. Порядок таблиц для оператора важен, поскольку оператор несимметричен.
Пример:
select
*
from
table_name1 left join table_name2
on
condition
Результат формируется по следующему алгоритму:
-
В результат включается внутренне соединение (
inner join
) первой и второй таблицы в соответствии с условием -
Затем в результат добавляются те записи первой таблицы (второй для
right join
), которые не вошли во внутреннее соединение на шаге 1, для таких записей соответствующие поля второй таблицы заполняютсяnull
.
Пример:
select
name_genre
from
genre left join book
on
genre.genre_id = book.genre_id
where
title is null;
Запрос выводит все жанры, которые не представлены на складе.
Этот оператор также называется декартовым произведением, соединяет две таблицы (в запросе вместо cross join
можно указать ,
между таблицами). Порядок таблиц неважен - оператор симметричен.
Пример:
select
*
from
table_name1 cross join table_name2 ИЛИ (table_name1, table_name2)
Результат запроса формируется так: каждая строка одной таблицы соединяется с каждой строкой другой таблицы, формируя в результате все возможные сочетания строк двух таблиц.
Пример:
select
name_author,
name_genre
from
author cross join genre
Этот запрос каждому автору из таблицы author
поставит в соответствие все возможные жанры из таблицы genre
.
Запрос на выборку может выбирать данные из двух и более таблиц БД, которые логически связаны между собой. Для каждой пары таблиц, включаемых в запрос, необходимо указать свой оператор соединения. Наиболее распространенным является внутренне inner join
соединение.
Пример:
select
*
from
first
inner join second on first.first_id = second.first_id
inner join third on second.secnd_id = third.secnd_id
Пример:
select
*
from
first
inner join third on first.first_id = third.first_id
inner join second in third.second_id = second.second_id
В этом случае рекомендуется соединение таблиц записывать последовательно, "по кругу": first -> third -> second
.
Пример:
select
title,
name_author,
name_genre,
price,
amount
from
author
inner join book on author.author_id = book.author_id
inner join genre on genre.genre_id = book.genre_id
where price between 500 and 700;
Этот запрос выводит информацию о тех книгах, авторах и жанрах, цена которых принадлежит интервалу от 500 до 700 рублей включительно.
В запросах с групповыми функциями могут использоваться несколько таблиц, между которыми используются различные типы соединений.
Пример:
select
name_author,
count(title) as Количество
from
author inner join book
on author.author_id = book.author_id
group by
name_author
order by
name_author;
Запрос выводит количество различных книг каждого автора.
При использовании inner join
мы не можем узнать, что какие-то книги отсутствуют на складе, но предполагается, что они могут быть. Чтобы автор, не представленный на складе был включен в результат необходимо изменить способ соединения таблиц.
Пример:
select
name_author,
count(title) as Количество
from
author left join book
on author.author_id = book.author_id
group by
name_author
order by
name_author;
В запросах, построенных на нескольких таблицах, можно использовать вложенные запросы. Вложенный запрос может быть включен: после ключевого слова select
, после from
и в условие обора после where(having)
.
Пример:
Вывести авторов, общее количество книг которых на складе максимально.
Шаг 1: Найти суммарное количество книг на складе по каждому автору
select
author_id,
sum(amount) as sum_amount
FROM
book
GROUP BY
author_id;
Шаг 2: Находим максимальное значение из прошлого шага
select
max(sum_amount) as max_sum_amount
from
(
select author_id, sum(amount) as sum_amount
from book
group by author_id
) query_in
В этом запросе таблица из шага 1 используется в качестве источника данных с именем query_in
.
Шаг 3: Выведем фамилию автора и общее количество книг для него
select
name_author,
sum(amount) as Количество
from
author inner join book
on author.author_id = book.author_id
group by
name_author
Шаг 4:
Включим запрос из шага 2 в условие обора запроса с шага 3, и получим всех авторов, общее количество книг максимально.
select
name_author,
sum(amount) as Количество
from
author inner join book
on author.author_id = book.author_id
group by
name_author
having sum(amount) =
(/* считаем максимальное из общего количества книг каждого автора */
SELECT MAX(sum_amount) AS max_sum_amount
FROM
(/* считаем количество книг каждого автора */
SELECT author_id, SUM(amount) AS sum_amount
FROM book GROUP BY author_id
) query_in
);
Синтаксис:
select
*
from
table_name1 join
(
select
*
from
...
) name_of_inner_query
on condition
...
Вложенный запрос может стоять как справа, так и слева от оператора join. Допускается использование двух запросов в операторах соединения.
Пример:
Вывести авторов, пишущих книги в самом популярном жанре (количество книг в этом жанре на складе максимально)
Шаг 1: Находим общее количество книг по каждому жанру, сортируем по убыванию и оставляем одну запись
select
genre_id,
sum(amount) as sum_amount
from
book
group by
genre_id
order by
sum_amount desc
limit 1
Шаг 2: Найдем id самых популярных жанров используя запрос из 1 шага
select query_in_1.genre_id
from
(/* выбираем код жанра и количество произведений, относящихся к нему */
select
genre_id,
sum(amount) as sum_amount
from
book
group by
genre_id
)query_in_1
inner join
(/* выбираем запись, в которой указан код жанр с максимальным количеством книг */
select
genre_id,
sum(amount) as sum_amount
from
book
group by
genre_id
order by
sum_amount desc
LIMIT 1
) query_in_2
on query_in_1.sum_amount=query_in_2.sum_amount;
Шаг 3. Используя запрос с шага 2, выведем фамилии авторов, которые пишут в самых популярных жанрах, и названия этих жанров. Группировка дб осуществлена по автору и id жанра.
select name_author, name_genre
from
author
inner join book on author.author_id = book.author_id
inner join genre on book.genre_id = genre.genre_id
group by author.name_author, genre.name_genre, genre.genre_id
having genre.genre_id in
(/* выбираем автора, если он пишет книги в самых популярных жанрах*/
select query_in_1.genre_id
from
( /* выбираем код жанра и количество произведений, относящихся к нему */
select genre_id, sum(amount) as sum_amount
from book
group by genre_id
)query_in_1
inner join
( /* выбираем запись, в которой указан код жанр с максимальным количеством книг */
select genre_id, sum(amount) as sum_amount
from book
group by genre_id
order by sum_amount desc
limit 1
) query_in_2
on query_in_1.sum_amount=query_in_2.sum_amount
);
При описании соединения таблиц с помощью join
в некоторых случаях вместо on
и следующего за ним условия можно использовать оператор using
.
using
позволяет указать набор столбцов, которые есть в обеих объединяемых таблицах. Если база данных хорошо спроектирована, а каждый внешний ключ имеет такое же имя, как и соответствующий первичный ключ (например genre.genr._id = book.genre_id
), тогда можно использовать using
для реализации операции join
.
При этом после select
, при использовании столбцов из using
, необязательно указывать, из какой именно таблицы берется столбец.
Пример:
Вариант с ON:
select
title,
name_author,
author.author_id
from
author inner join book
on author.author_id = book.author_id;
Вариант с USING:
select
title,
name_author,
author_id
from
author inner join book
using(author_id);
Запись условия соединения с использованием on
более гибко, поскольку позволяет:
-
задавать соединение не только по одноименным полям
-
использовать произвольное условие на соединение таблиц, при этом в условие может включаться произвольное выражение, например, можно указать два или более столбца
С помощью запросов корректировки данных решим задачу о занесении в базу книг, привезенных на склад поставщиком.
В запросах на обновление можно использовать связанные таблицы. Исправлять данные можно во всех таблицах, участвующих в запросе.
update
table_name1 join table_name2
on expression
set ...
where ...
Пример:
Для книг, которые уже есть на складе (в таблице book
) по той же цене, что и в поставке (supply
), увеличить количество на значение, указанное в поставке, а также обнулить количество этих книг в поставке.
update
book inner join author on author.author_id = book.author_id
inner join supply on book.title = supply.title
and supply.author = author.name_author
set
book.amount = book.amount + supply.amount,
supply.amount = 0
where
book.price = supply.price;
Запросом на добавление можно добавить записи, отобранные с помощью запроса на выборку, который включает несколько таблиц:
insert into
table_name1(attribute_list)
select
(attribute_list_from_another_tables)
from
table_name2 join table_name3
on ...
...
Пример:
В таблице supply
есть новые книги, которых на складе еще не было. Прежде чем добавлять их в таблицу book
, необходимо из таблицы supply
отобрать новых авторов, если таковые имеются.
insert into
author
select
supply.author
from
author
right join supply on author.name_author = supply.author
where name_author is null;
Пример:
update
book
set
genre_id = 1
where
book_id = 9;
При создании таблицы для внешних ключей с помощью on delete
устанавливаются опции, которые определяют действия, выполняемые при удалении связанной строки из главной таблицы.
on delete cascade
автоматически удаляет строки из зависимой таблицы при удалении связанных строк в главной таблице.
Пример:
В таблице book
эта опция установлена для поля author_id
.
delete from
author
where
name_author like "Д%";
При создании таблицы для внешних ключей с помощью on delete
устанавливают действия, выполняемые при удалении связанной строки из главной таблицы.
Если задано set null
, то при удалении связанной строки из главной таблицы в зависимой, в столбце внешнего ключа устанавливается значение null
.
Пример:
В таблице book
эта опция установлена на поле genre_id
. Удалим из таблицы genre
все жанры, название которых заканчивается на "я", а в таблице book
для этих жанров установим значение null
.
delete from
genre
where
name_genre like "%я";
При удалении записей из таблицы можно использовать информацию из других связанных с ней таблиц. В этом случае синтаксис запроса имеет вид:
delete from
table_name1
using
table_name1 inner join table_name2
on ...
where
...
Пример:
Удалить всех авторов из таблицы author
, у которых есть книги, количество экземпляров которых меньше 3. Из таблицы book
удалить все книги этих авторов.
delete from
author
using
author inner join book
on author.author_id = book.author_id
where book.amount < 3;
В интернет-магазине продаются книги
. Каждая книга имеет название, написана одним автором
, относится к одному жанру
, имеет определенную цену. В магазине в наличии есть несколько экземпляров каждой книги.
Покупатель
регистрируется на сайте интернет-магазина, задает свое имя и фамилию, электронную почту и город
проживания. Он может сформировать один или несколько заказов
, для каждого заказа написать какие-то пожелания. Каждый заказ включает одну или несколько книг, каждую книгу можно заказать в нескольких экземплярах. Затем заказ проходит ряд последовательных этапов
(операций): оплачивается, упаковывается, передается курьеру или транспортной компании для транспортировки и, наконец, доставляется покупателю. Фиксируется дата каждой операции. Для каждого города известно среднее время доставки книг.
При этом в магазине ведется учет книг, при покупке их количество уменьшается, при поступлении товара увеличивается, при исчерпании количества – оформляется заказ и прочее.
Шаг 1. Детально анализируем предметную область и выделяем объекты, которые будут храниться в БД (выше выделены так
)
Шаг 2. Для каждого информационного объекта указываем его характеристики (выше выделены вот так)
Затем связываем их с информационными объектами:
-
Книга - название, количество, цена
-
Автор - фамилия, инициалы
-
Жанр - название
-
Покупатель - фамилия и имя, электронная почта
-
Город - название, среднее время доставки
-
Заказ - код заказа, пожелания
-
Этап - названия этапов
Перечислим характеристики, которые остались не привязанными к информационным объектам (к ним необходимо вернуться при реализации связей между таблицами):
-
книги в заказе
-
количество книг в заказе
-
дата каждой операции
Шаг 3. Схема информационных объектов в виде прямоугольников
Шаг 4. Установить связи между информационными объектами.
На основе концептуальной модели БД создается логическая модель, в которой информационные объекты описываются в виде реляционных таблиц.
Для каждой пары таблиц выполняются шаги:
Шаг 1. Выбрать пару таблиц из схемы
Шаг 2. Разработать структуру таблиц для каждого информационного объекта. Таблица в качестве столбцов включает все характеристики объекта, кроме тех, которые соответствуют названиям других объектов. Они будут включены в таблицы при создании связей. Получаем:
Шаг 3. Реализуем связь между таблицами (в данном случае "один ко многим")
Шаг 4. Вернуться к описанию концептуальной модели и проверить, нужно ли включить какие-то характеристики, непривязанные к объектам? В нашем случае таких характеристик нет (чаще всего такие характеристики появляются при связи "многие ко многим").
Теперь на примере (книга <--> заказ
)
Шаг 1. Выберем следующую пару таблиц:
Шаг 2. Структура таблиц для каждого объекта следующая:
Шаг 3. Реализуем связь "многие ко многим"
Шаг 4. В описании предметной области указывается, что нужно хранить количество книг, которые включены в заказ. Добавим этот атрибут в таблицу-связку buy_book
.
Пример:
Вывести фамилии всех клиентов, которые заказали книгу Булгакова "Мастер и Маргарита".
select distinct
name_client
from
client inner join buy on client.client_id = buy.client_id
inner join buy_book on buy_book.buy_id = buy.buy_id
inner join book on buy_book.book_id = book.book_id
where
title ='Мастер и Маргарита' and author_id = 1;
Используется для объединения двух и более запросов.
select
att1_1,
att1_2
from
...
union или union all
select
att2_1,
att2_2
from
...
Каждый из select
должен иметь в своем запросе одинаковое количество столбцов и совместимые типы возвращаемых данных. Каждый запрос может включать where, group by
и т.д.
В результате выполнения запроса будет выведена таблица, имена столбцов которой соответствуют именам столбцов в первом запросе. А в таблице результата сначала отображаются записи-результаты первого запроса, а затем второго. Если указано ключевое слово all
, то в результат включаются все записи запросов, в противном случае - различные.
Пример:
Вывести всех клиентов, которые делали заказы или в этом, или в предыдущем году.
С union
клиенты выведутся без повторений, с union all
будут выведены клиенты с повторением (те, кто заказывал книги в обоих годах, а также несколько раз в одном году).
select
name_client
from
buy_archive inner join client using(client_id)
union или union all
select
name_client
from
buy inner join client using(client_id)
Пример:
Вывести информацию об оплаченных заказах за предыдущий и текущий год, информацию отсортировать по client_id
.
select
buy_id, client_id, book_id, date_payment, amount, price
from
buy_archive
union all
select buy.buy_id, client_id, book_id, date_step_end, buy_book.amount, price
from
book
inner join buy_book using(book_id)
inner join buy using(buy_id)
inner join buy_step using(buy_id)
inner join step using(step_id)
where
date_step_end is not null and name_step = "Оплата"
В SQL переменные используются для хранения временных значений, которые могут использоваться в запросах. Использование переменных может упростить и ускорить написание запросов, особенно если вы используете одно и то же значение несколько раз.
Вот примеры, как использовать переменные в SQL:
- Объявление переменной:
DECLARE @myVariable INT;
- Присвоение значения переменной:
SET @myVariable = 5;
- Использование переменной в запросе:
SELECT * FROM myTable WHERE myColumn = @myVariable;
- Использование переменной в условном операторе:
IF @myVariable > 10
BEGIN
SELECT * FROM myTable WHERE myColumn > @myVariable;
END
ELSE
BEGIN
SELECT * FROM myTable WHERE myColumn < @myVariable;
END
- Использование переменной в цикле:
DECLARE @i INT = 0;
WHILE @i < 10
BEGIN
SET @i = @i + 1;
SELECT @i;
END
В университете реализуется on-line тестирование по нескольким дисциплинам. Каждая дисциплина включает некоторое количество вопросов. Ответы на вопрос представлены в виде вариантов ответов, один из этих вариантов правильный.
Студент регистрируется в системе, указав свое имя, фамилию и отчество. После этого он может проходить тестирование по одной или нескольким дисциплинам. Студент имеет несколько попыток для прохождения тестирования (необходимо сохранять дату попытки). Каждому студенту случайным образом выбирается набор вопросов по дисциплине и формируется индивидуальный тест. Студент отвечает на вопросы, выбирая один из предложенных вариантов ответа.
После окончания тестирования вычисляется и сохраняется результат (в процентах) попытки.
Концептуальная схема БД
Логическая схема БД
Университет состоит из совокупности факультетов (школ). Поступление абитуриентов осуществляется на образовательные программы по результатам Единого государственного экзамена (ЕГЭ). Каждая образовательная программа относится к определенному факультету, для нее определены необходимые для поступления предметы ЕГЭ, минимальный балл по этим предметам, а также план набора (количество мест) на образовательную программу.
В приемную комиссию абитуриенты подают заявления на образовательную программу, каждый абитуриент может выбрать несколько образовательных программ (но не более трех). В заявлении указывается фамилия, имя, отчество абитуриента, а также его достижения: получил ли он медаль за обучение в школе, имеет ли значок ГТО и пр. При этом за каждое достижение определен дополнительный балл. Абитуриент предоставляет сертификат с результатами сдачи ЕГЭ. Если абитуриент выбирает образовательную программу, то у него обязательно должны быть сданы предметы, определенные на эту программу, причем балл должен быть не меньше минимального по данному предмету.
Зачисление абитуриентов осуществляется так: сначала вычисляется сумма баллов по предметам на каждую образовательную программу, добавляются баллы достижения, затем абитуриенты сортируются в порядке убывания суммы баллов и отбираются первые по количеству мест, определенному планом набора.
Концептуальная схема БД
Логическая схема БД
Для изменения структуры таблицы используется оператор alter table
. С его помощью можно вставить новый столбец, удалить существующий, переименовать столбец и пр.
Для **вставки **нового столбца используются следующие SQL-запросы:
- Вставка столбца после последнего
alter table table_name1
add attribute1 type(int, varchar ...)
- Вставка столбца перед первым
alter table table_name1
add attribute1 type(int, varchar...) first
- Вставка столбца после указанного
alter table table_name1
add attribute1 type(int, varchar...) after attribute2
Для удаления столбца используются запросы:
- Удаление столбца с заданным именем
alter table table_name1
drop column attribute1
- Удаление нескольких столбцов
alter table table_name1
drop column attribute1,
drop column attribute2;
Для переименования столбца используется запрос:
alter table table_name1
change old_attribute1 new_attribute1 type(int, varchar)
Для изменения типа столбца используется запрос:
alter table table_name1
change old_attribute1 new_attribute1 new_type(int, varchar)
Номер строки в таблице или запросе в некоторых версиях SQL можно получить с помощью оконной функции row_number()
.
Нумерацию строк также можно реализовать с помощью переменных.
Пример:
Пронумеруем записи в таблице application_order
set @row_num := 0;
select *, (@row_num := @row_num + 1) as str_num
from applicant_order;
Пример:
Создадим нумерацию, которая начинается заново для каждой образовательной программы. Для этого можно использовать алгоритм, в котором в переменную @row_num
заносится 1, если id
программы в предыдущей записи не равен id
программы в текущей.
set @num_pr := 0;
set @row_num := 1;
select *,
if(program_id = @num_pr, @row_num := @row_num + 1, @row_num := 1) as str_num,
@num_pr := program_id as add_var
С помощью оператора case
можно в зависимости от нескольких условий получить один из нескольких результатов.
Оператор case
записывается в виде:
case
when logical_expression1 then expression1
when logical_expression2 then expression2
...
else else_expression
end
else
является необязательной частью.
Выполнение case
происходит так:
-
Вычисляется
logical_expression1
, если оно истинно, то результатом являетсяexpression1
, если ложно, то выполнение оператора продолжается. -
Вычисляется
logical_expression2
, если оно истинно, то результатом являетсяexpression2
, если ложно, то выполнение оператора продолжается. -
Если все логические выражения оказались ложными, то результат оператора -
else_expression
Табличное выражение определяется с помощью оператора WITH
и является частью запроса. Его синтаксис:
with expression_name (name_1, name_2, ...)
as(
select att1, att2
from table_name1
...
)
select ...
from expression_name
...
В табличном выражении определяется запрос, результат которого нужно использовать в основной части запроса после select
. При этом основной запрос может обратиться к столбцам результата табличного выражения через имена, заданные в заголовке with
. Количество имен должно совпадать с количеством результирующих столбцов табличного выражения.
В одном запросе может быть несколько табличных выражений. При этом в каждом табличном выражении можно использовать все предшествующие ему табличные выражения.
В табличном выражении необязательно давать имена столбцам результата. В этом случае в основном запросе можно использовать имена столбцов, указанных после select
в табличном выражении. При наличии одинаковых имен в нескольких табличных выражениях необходимо использовать полное имя столбца.
Оконные функции позволяют получить некоторую дополнительную информацию о выборке данных. С помощью оконных функций можно реализовать вычисления для набора строк, некоторым образом связанных с текущей строкой. При этом использование оконной функции не группирует несколько строк в одну, а сохраняет все строки запроса. Синтаксис оконных функций:
function_name(expression)
over(
partition by столбец_1, столбец_2, ... - это окно
order by ... - сортировка
rows between - границы окна
...
)
Причем все разделы over
являются не обязательным, но обязательно нужно указать либо окно, либо сортировку.
Самый простой синтаксис оконного выражения:
function_name(expression)
over(
order by ...
)
Такое оконное выражение позволяет выполнять одинаковые действия над всеми записями таблицы (здесь окно - вся таблица). В качестве функций могут быть:
-
row_number()
- нумерация строк -
rank()
- ранжирование строк - при одинаковом значении строкам присваивается один номер, с пропуском номеров -
dense_rank()
- ранжирование строк без пропуска номеров -
lag()
- выбирает строку, предшествующую текущей, если такой нет - выводитnull
-
lead()
- выбирает строку, следующую за текущей, если таковой нет - выводитnull
Пример:
Вычислить, сколько шагов прошел пользователь и ранжировать пользователей по убыванию результата.
select
student_name,
count(distinct step_id) AS Kоличество,
ROW_NUMBER() over (order by count(distinct step_id) desc) as Номер
from
student inner join step_student using (student_id)
where
result = "correct"
group by
student_name
Результат:
+--------------+------------+-------+
| student_name | Kоличество | Номер |
+--------------+------------+-------+
| student_60 | 32 | 1 |
| student_15 | 30 | 2 |
| student_18 | 30 | 3 |
| student_27 | 30 | 4 |
| student_30 | 30 | 5 |
| student_31 | 30 | 6 |
| student_36 | 30 | 7 |
...
| student_5 | 9 | 61 |
| student_63 | 9 | 62 |
| student_29 | 8 | 63 |
| student_47 | 8 | 64 |
+--------------+------------+-------+
В этом запросе после того, как были выбраны все студенты, посчитаны их шаги с правильными ответами, с помощью оконной функции была выполнена сортировка по количеству верных шагов (count(distinct step_id)
) и пронумерованы строки (функция row_number()
).
Пример:
Дополнительно ранжируем студентов:
select
student_name,
count(distinct step_id) as Kоличество,
ROW_NUMBER() over (order by count(distinct step_id) desc) as Номер,
RANK() over (order by count(distinct step_id) desc) as Ранг,
DENSE_RANK() over (order by count(distinct step_id) desc) as Рейтинг
from
student inner join step_student using (student_id)
where
result = "correct"
group by
student_name
Результат:
+--------------+------------+-------+------+---------+
| student_name | Kоличество | Номер | Ранг | Рейтинг |
+--------------+------------+-------+------+---------+
| student_60 | 32 | 1 | 1 | 1 |
| student_15 | 30 | 2 | 2 | 2 |
| student_18 | 30 | 3 | 2 | 2 |
| student_27 | 30 | 4 | 2 | 2 |
| student_30 | 30 | 5 | 2 | 2 |
| student_31 | 30 | 6 | 2 | 2 |
| student_36 | 30 | 7 | 2 | 2 |
| student_39 | 30 | 8 | 2 | 2 |
| student_4 | 30 | 9 | 2 | 2 |
| student_43 | 30 | 10 | 2 | 2 |
| student_44 | 30 | 11 | 2 | 2 |
| student_46 | 30 | 12 | 2 | 2 |
| student_49 | 30 | 13 | 2 | 2 |
| student_51 | 30 | 14 | 2 | 2 |
| student_53 | 30 | 15 | 2 | 2 |
| student_59 | 29 | 16 | 16 | 3 |
| student_9 | 29 | 17 | 16 | 3 |
| student_23 | 28 | 18 | 18 | 4 |
| student_50 | 27 | 19 | 19 | 5 |
...
| student_5 | 9 | 61 | 60 | 15 |
| student_63 | 9 | 62 | 60 | 15 |
| student_29 | 8 | 63 | 63 | 16 |
| student_47 | 8 | 64 | 63 | 16 |
+--------------+------------+-------+------+---------+
С помощью функции RANK()
и DENSE_RANK()
все студенты, имеющие 30 верно пройденных шагов, получили ранг 2 и рейтинг 2. Студентам с 29 балами присвоен ранг 16 и рейтинг 3.
Пример:
Для каждого студента указать, на сколько меньше он прошел шагов, чем идущий перед ним по рейтингу студент.
select
student_name,
count(distinct step_id) as Количество,
LAG(count(distinct step_id))
over (order by count(distinct step_id) desc) - count(distinct step_id) as Разница
from
student inner join step_student using (student_id)
where
result = "correct"
group by
student_name
Результат:
+--------------+------------+---------+
| student_name | Количество | Разница |
+--------------+------------+---------+
| student_60 | 32 | Null |
| student_15 | 30 | 2 |
| student_18 | 30 | 0 |
| student_27 | 30 | 0 |
| student_30 | 30 | 0 |
| student_31 | 30 | 0 |
| student_36 | 30 | 0 |
| student_63 | 9 | 0 |
| student_29 | 8 | 1 |
| student_47 | 8 | 0 |
+--------------+------------+---------+
Для замены значения null
на что-то другое можно использовать функцию ifnull(expression, result)
, которая возвращает результат, если выражение равно null
и само выражение в противном случае.
Оконная функция, включающая два раздела:
function_name(expression)
over(
partition by ...
order by...
)
Такое оконное выражение позволяет выполнять одинаковые действия над всеми записями таблицы, ограниченными "окном". Столбцы, образующие окно записываются после PARTITION BY. Окном считается совокупность записей, имеющих в столбцах, указанных после PARTITION BY, одинаковые значения. В качестве функций могут выступать: row_number(), rank(), dense_rank(), lag(), lead()
.