Главная страница

Учебнопрактическое пособие москва 2017 удк 004. 655 Ббк 32. 973. 26018. 2 М79 Моргунов, Е. П


Скачать 0.9 Mb.
НазваниеУчебнопрактическое пособие москва 2017 удк 004. 655 Ббк 32. 973. 26018. 2 М79 Моргунов, Е. П
Дата14.11.2022
Размер0.9 Mb.
Формат файлаpdf
Имя файла4_1_1_SQL_Postgres_Bazovy_kurs_2017.pdf
ТипУчебно-практическое пособие
#787182
страница19 из 28
1   ...   15   16   17   18   19   20   21   22   ...   28

SELECT city
FROM airports
WHERE city <> 'Москва'
?
SELECT arrival_city
FROM routes
WHERE departure_city = 'Москва'
ORDER BY city;
22. В тексте главы мы рассматривали такой запрос: получить перечень аэропортов в тех городах, в которых больше одного аэропорта.
SELECT aa.city, aa.airport_code, aa.airport_name
FROM (
SELECT city, count( * )
FROM airports
GROUP BY city
HAVING count( * ) > 1
) AS a
JOIN airports AS aa ON a.city = aa.city
ORDER BY aa.city, aa.airport_name;
Как вы думаете, обязательно ли наличие функции count в подзапросе в предло- жении SELECT или можно написать просто
...
FROM ( SELECT city FROM airports
...
Сначала попробуйте дать ответ теоретически, а потом проверьте вашу гипотезу на компьютере.
158

23. Предположим, что департамент развития нашей авиакомпании задался вопро- сом: каким будет общее число различных маршрутов, которые теоретически можно проложить между всеми городами?
Если в каком-то городе имеется более одного аэропорта, то это учитывать не будем, т. е. маршрутом будем считать путь между городами, а не между аэро-
портами
. Здесь мы используем соединение таблицы с самой собой на основе неравенства значений атрибутов.
SELECT count( * )
FROM ( SELECT DISTINCT city FROM airports ) AS a1
JOIN ( SELECT DISTINCT city FROM airports ) AS a2
ON a1.city <> a2.city;
count
-------
10100
(1 строка)
Задание.
Перепишите этот запрос с использованием общего табличного выра- жения.
24. В тексте главы мы рассмотрели использование подзапросов в предикатах
EXISTS и IN. Существуют также предикаты многократного сравнения ANY и ALL.
Они представлены в документации в разделе 9.22 «Выражения подзапросов».
Самостоятельно ознакомьтесь с этими предикатами и напишите несколько за- просов с их применением.
Предикаты ANY и ALL имеют некоторую связь с предикатом IN. В частности,
использование IN эквивалентно использованию конструкции = ANY, а исполь- зование NOT IN эквивалентно использованию конструкции <> ALL.
Пример двух эквивалентных запросов, выбирающих аэропорты в часовых поя- сах «Asia/Novokuznetsk» и «Asia/Krasnoyarsk»:
SELECT * FROM airports
WHERE timezone IN
( 'Asia/Novokuznetsk', 'Asia/Krasnoyarsk' );
SELECT * FROM airports
WHERE timezone = ANY (
VALUES ( 'Asia/Novokuznetsk' ),
( 'Asia/Krasnoyarsk' )
);
Еще один пример. В тексте главы мы рассматривали запрос, подсчитывающий количество маршрутов, проложенных из самых восточных аэропортов.
SELECT departure_city, count( * )
FROM routes
GROUP BY departure_city
HAVING departure_city IN (
SELECT city
FROM airports
WHERE longitude > 150
159

)
ORDER BY count DESC;
В этом запросе можно заменить IN на ANY таким образом:
...
HAVING departure_city = ANY ( SELECT city
...
25.* При планировании новых маршрутов и оценке экономической эффективности уже существующих может потребоваться информация о том, какова усреднен- ная степень заполнения самолетов на всех направлениях.
Будем учитывать только уже прибывшие рейсы.
WITH tickets_seats AS
( SELECT f.flight_id, f.flight_no, f.departure_city,
f.arrival_city, f.aircraft_code,
count( tf.ticket_no ) AS fact_passengers,
( SELECT count( s.seat_no )
FROM seats s
WHERE s.aircraft_code = f.aircraft_code
) AS total_seats
FROM flights_v f
JOIN ticket_flights tf ON f.flight_id = tf.flight_id
WHERE f.status = 'Arrived'
GROUP BY 1, 2, 3, 4, 5
)
SELECT ts.departure_city, ts.arrival_city,
sum( ts.fact_passengers ) AS sum_fact_passengers,
sum( ts.total_seats ) AS sum_total_seats,
round( sum( ts.fact_passengers )::numeric /
sum( ts.total_seats )::numeric, 2 ) AS fraction
FROM tickets_seats ts
GROUP BY ts.departure_city, ts.arrival_city
ORDER BY ts.departure_city;
--[ RECORD 1 ]-------+------------------------- departure_city
| Абакан arrival_city
| Москва sum_fact_passengers | 466
sum_total_seats
| 1044
fraction
| 0.45
--[ RECORD 2 ]-------+------------------------- departure_city
| Абакан arrival_city
| Новосибирск sum_fact_passengers | 217
sum_total_seats
| 348
fraction
| 0.62
--[ RECORD 3 ]-------+------------------------- departure_city
| Абакан arrival_city
| Томск sum_fact_passengers | 258
sum_total_seats
| 360
fraction
| 0.72 160

--[ RECORD 361 ]-----+------------------------- departure_city
| Якутск arrival_city
| Санкт-Петербург sum_fact_passengers | 352
sum_total_seats
| 3596
fraction
| 0.10
Для того чтобы лучше уяснить, как работает запрос в целом, вычлените из него отдельные подзапросы и выполните их, посмотрите, что они выводят.
Как вы считаете, равносильно ли в данном запросе
SELECT count( s.seat_no )
и
SELECT count( s.* )
Почему?
Задание. Модифицируйте этот запрос, чтобы он выводил те же отчетные дан- ные, но с учетом классов обслуживания, т. е. Business, Comfort и Economy.
26.* Предположим, что некая контролирующая организация потребовала информа- цию о размещении пассажиров одного из рейсов Кемерово — Москва в салоне самолета. Для определенности выберем конкретный рейс из тех рейсов, кото- рые уже прибыли на момент времени, соответствующий текущему моменту. Те- кущий момент времени в базе данных «Авиаперевозки» определяется с помо- щью функции bookings.now.
Выполним запрос
SELECT *
FROM flights_v
WHERE departure_city = 'Кемерово'
AND arrival_city
= 'Москва'
AND actual_arrival < bookings.now();
Выберем для дальнейшей работы рейс, у которого значения атрибутов flight_id — 27584, aircraft_code — SU9.
Получим список пассажиров этого рейса с местами, которые им были назначе- ны в салоне самолета.
SELECT t.passenger_name, b.seat_no
FROM (
ticket_flights tf
JOIN tickets t ON tf.ticket_no = t.ticket_no
)
JOIN boarding_passes b
ON tf.ticket_no = b.ticket_no
AND tf.flight_id = b.flight_id
WHERE tf.flight_id = 27584
ORDER BY t.passenger_name;
161
passenger_name
| seat_no
---------------------+---------
ALEKSANDR ABRAMOV
| 1A
ALEKSANDR GRIGOREV | 5C
ALEKSANDR SERGEEV
| 6F
ALEKSEY FEDOROV
| 11D
ALEKSEY MELNIKOV
| 18A
VLADIMIR POPOV
| 11A
YAROSLAV KUZMIN
| 18F
YURIY ZAKHAROV
| 10F
(44 строки)
Отсортируем строки по фамилиям пассажиров:
SELECT t.passenger_name,
substr(
t.passenger_name,
strpos( t.passenger_name, ' ' ) + 1
) AS lastname,
left(
t.passenger_name,
strpos( t.passenger_name, ' ' ) - 1
) AS firstname,
b.seat_no
FROM (
ticket_flights tf
JOIN tickets t ON tf.ticket_no = t.ticket_no
)
JOIN boarding_passes b
ON tf.ticket_no = b.ticket_no
AND tf.flight_id = b.flight_id
WHERE tf.flight_id = 27584
ORDER BY 2, 3;
passenger_name
| lastname | firstname | seat_no
---------------------+-----------+-----------+---------
ALEKSANDR ABRAMOV
| ABRAMOV
| ALEKSANDR | 1A
NIKITA ANDREEV
| ANDREEV
| NIKITA
| 6D
ANTONINA ANISIMOVA | ANISIMOVA | ANTONINA | 11F
YURIY ZAKHAROV
| ZAKHAROV | YURIY
| 10F
ELENA ZOTOVA
| ZOTOVA
| ELENA
| 20E
(44 строки)
Получим список мест в салоне самолета и пассажиров, которые сидели на этих местах. При этом незанятые места также должны быть выведены (поэтому ис- пользуем левое внешнее соединение ... FROM seats s LEFT OUTER JOIN ...).
SELECT s.seat_no, p.passenger_name
FROM seats s
LEFT OUTER JOIN (
SELECT t.passenger_name, b.seat_no
FROM (
ticket_flights tf
JOIN tickets t ON tf.ticket_no = t.ticket_no
)
162

JOIN boarding_passes b
ON tf.ticket_no = b.ticket_no
AND tf.flight_id = b.flight_id
WHERE tf.flight_id = 27584
) AS p
ON s.seat_no = p.seat_no
WHERE s.aircraft_code = 'SU9'
ORDER BY s.seat_no;
seat_no | passenger_name
---------+---------------------
10A
|
10C
|
10D
| NATALYA POPOVA
10E
|
10F
| YURIY ZAKHAROV
11A
| VLADIMIR POPOV
11C
| ANNA KUZMINA
11D
| ALEKSEY FEDOROV
11E
|
11F
| ANTONINA ANISIMOVA
8F
|
9A
| MAKSIM CHERNOV
9C
|
9D
| LYUDMILA IVANOVA
9E
|
9F
| SOFIYA KULIKOVA
(97 строк)
Предположим, что нас попросили отсортировать места в порядке их расположе- ния в салоне самолета и вывести также адреса электронной почты пассажиров
(у кого они были указаны при бронировании). Для выполнения второго требо- вания воспользуемся столбцом contact_data. В нем содержатся JSON-объекты,
содержащие контактные данные пассажиров. Ряд из них имеет ключ «email».
Модифицированный запрос будет таким:
SELECT s.seat_no, p.passenger_name, p.email
FROM seats s
LEFT OUTER JOIN (
SELECT t.passenger_name, b.seat_no,
t.contact_data->'email' AS email
FROM (
ticket_flights tf
JOIN tickets t ON tf.ticket_no = t.ticket_no
)
JOIN boarding_passes b
ON tf.ticket_no = b.ticket_no
AND tf.flight_id = b.flight_id
WHERE tf.flight_id = 27584
) AS p
ON s.seat_no = p.seat_no
WHERE s.aircraft_code = 'SU9'
ORDER BY
163

left( s.seat_no, length( s.seat_no ) - 1 )::integer,
right( s.seat_no, 1 );
seat_no |
passenger_name | email
---------+-------------------+------------------------------------
1A
| ALEKSANDR ABRAMOV |
1C
|
|
1D
| DENIS PETROV
|
1F
| LEONID BARANOV
| "baranov.l.1967@postgrespro.ru"
2A
|
|
2C
|
|
2D
|
|
2F
| FEDOR TIKHONOV
| "tikhonov_fedor_1957@postgres... "
9F
| SOFIYA KULIKOVA
| "sofiya.kulikova_041963@postgre..."
10A
|
|
10C
|
|
10D
| NATALYA POPOVA
| "popova.n_13031976@postgrespro.ru"
20E
| ELENA ZOTOVA
|
20F
| LILIYA OSIPOVA
|
(97 строк)
Задание. Перепишите последний запрос с использованием общего табличного выражения и добавьте столбец «Класс обслуживания» (fare_conditions).
164

7 Изменение данных
Эта глава будет посвящена операциям изменения данных: вставке новых строк в таблицы, об- новлению уже существующих строк и их удалению. С простыми приемами использования ко- манд INSERT, UPDATE и DELETE, предназначенных для выполнения указанных операций, вы уже познакомились, поэтому мы расскажем о некоторых более интересных способах приме- нения этих команд.
7.1 Вставка строк в таблицы
Для работы нам потребуется создать еще две таблицы в базе данных «Авиаперевоз- ки» (demo). Мы будем создавать их как временные таблицы, которые будут удаляться при отключении от базы данных. Использование временных таблиц позволит нам проводить эксперименты, будучи уверенными в том, что данные в постоянных таб- лицах модифицированы не будут, поэтому все запросы, которые вы выполняли ра- нее, будут работать так, как и работали.
Итак, создадим две копии таблицы «Самолеты» (aircrafts). Первая таблица-копия бу- дет предназначена для хранения данных, взятых из таблицы-прототипа, а вторая таблица-копия будет использоваться в качестве журнальной таблицы: будем запи- сывать в нее все операции, проведенные с первой таблицей.
Создадим первую таблицу, причем, копировать данные из постоянной таблицы aircrafts не будем, о чем говорит предложение WITH NO DATA. Если бы мы решили скопировать в новую таблицу и все строки, содержащиеся в таблице-прототипе, то- гда в команде CREATE TABLE мы могли бы использовать предложение WITH DATA
или вообще не указывать его: по умолчанию строки копируются в создаваемую таб- лицу.
CREATE TEMP TABLE aircrafts_tmp AS
SELECT * FROM aircrafts WITH NO DATA;
Наложим на таблицу необходимые ограничения: они не создаются при копировании таблицы. При массовом вводе данных гораздо более эффективным с точки зрения производительности было бы сначала добавить строки в таблицу, а уже потом накла- дывать ограничения на нее. Однако в нашем случае речь о массовом вводе не идет,
поэтому мы начнем с наложения ограничений, а уже потом добавим строки с табли- цу.
ALTER TABLE aircrafts_tmp
ADD PRIMARY KEY ( aircraft_code );
ALTER TABLE aircrafts_tmp
ADD UNIQUE ( model );
Теперь создадим вторую таблицу, копировать данные из постоянной таблицы aircrafts в нее также не будем.
CREATE TEMP TABLE aircrafts_log AS
SELECT * FROM aircrafts WITH NO DATA;
165

Ограничения в виде первичного и уникального ключей этой таблице не требуются, но потребуются еще два столбца: первый будет содержать дату/время выполнения опе- рации над таблицей aircrafts_tmp, а второй — наименование этой операции (INSERT,
UPDATE или DELETE).
ALTER TABLE aircrafts_log
ADD COLUMN when_add timestamp;
ALTER TABLE aircrafts_log
ADD COLUMN operation text;
Поскольку в рассматриваемой ситуации копировать данные из постоянных таблиц во временные не требуется, то в качестве альтернативного способа создания временных таблиц можно было бы воспользоваться командой CREATE TEMP TABLE с предложе- нием LIKE. Например:
CREATE TEMP TABLE aircrafts_tmp
( LIKE aircrafts INCLUDING CONSTRAINTS INCLUDING INDEXES );
Но так как уникального индекса по столбцу model в таблице aircrafts нет, то для вре- менной таблицы его пришлось бы сформировать с помощью команды ALTER TABLE,
как и при использовании первого способа ее создания. Добавим, что предложение
LIKE можно применять для создания не только временных таблиц, но и постоян- ных.
Поскольку у нас есть журнальная таблица aircrafts_log, мы можем все операции с таб- лицей aircrafts_tmp записывать в таблицу aircrafts_log, т. е. вести историю изменений данных в таблице aircrafts_tmp.
Начнем работу с того, что скопируем в таблицу aircrafts_tmp все данные из таблицы aircrafts. Для выполнения не только «полезной» работы, но и ведения журнала изме- нений воспользуемся командой INSERT с общим табличным выражением.
Вообще, при классическом подходе для ведения учета изменений, внесенных в таб- лицы, используют триггеры или правила (rules), но их рассмотрение выходит за рам- ки настоящего пособия. Поэтому наш пример нужно рассматривать как иллюстрацию возможностей общих табличных выражений (CTE), а не как единственно верный под- ход.
WITH add_row AS
( INSERT INTO aircrafts_tmp
SELECT * FROM aircrafts
RETURNING *
)
INSERT INTO aircrafts_log
SELECT add_row.aircraft_code, add_row.model, add_row.range,
CURRENT_TIMESTAMP, 'INSERT'
FROM add_row;
INSERT 0 9
Давайте рассмотрим эту команду более подробно. Обратите внимание, что вся «по- лезная» работа выполняется в рамках конструкции WITH add_row AS (...). Здесь строки
166
с помощью команды SELECT выбираются из таблицы aircrafts и вставляются в табли- цу aircrafts_tmp. При вставке строк, выбранных из одной таблицы, в другую табли- цу необходимо, чтобы число атрибутов и их типы данных во вставляемых строках были согласованы с числом столбцов и их типами данных в целевой таблице. За- вершается конструкция WITH add_row AS (...) предложением RETURNING *, которое просто возвращает внешнему запросу все строки, успешно добавленные в таблицу aircrafts_tmp. Конечно же, при этом из таблицы aircrafts_tmp добавленные строки ни- куда не исчезают. Запрос получает имя add_row, на которое может ссылаться внешний запрос, когда он «хочет» обратиться к строкам, возвращенным с помощью предложе- ния RETURNING *.
Теперь обратимся к внешнему запросу. В нем также присутствует команда INSERT,
которая получает данные для ввода в таблицу aircrafts_log от запроса SELECT. А
этот запрос, в свою очередь, получает данные от временной таблицы add_row, ука- занной в предложении FROM. Поскольку в предложении RETURNING мы указали в качестве возвращаемого значения «*», то будут возвращены все столбцы таблицы aircrafts_tmp, т. е. той таблицы, в которую строки были вставлены. Следовательно,
в команде SELECT внешнего запроса можно ссылаться на имена этих столбцов:
SELECT add_row.aircraft_code, add_row.model, add_row.range, ...
Поскольку в таблице aircrafts_log существует еще два столбца, то для них мы допол- нительно передаем значения CURRENT_TIMESTAMP и 'INSERT'.
Проверим, что получилось:
SELECT * FROM aircrafts_tmp ORDER BY model;
aircraft_code |
model
| range
---------------+---------------------+-------
319
| Airbus A319-100
| 6700 320
| Airbus A320-200
| 5700 321
| Airbus A321-200
| 5600 733
| Boeing 737-300
| 4200 763
| Boeing 767-300
| 7900 773
| Boeing 777-300
| 11100
CR2
| Bombardier CRJ-200 | 2700
CN1
| Cessna 208 Caravan | 1200
SU9
| Sukhoi SuperJet-100 | 3000
(9 строк)
Проверим также и содержимое журнальной таблицы:
SELECT * FROM aircrafts_log ORDER BY model;
--[ RECORD 1 ]-+--------------------------- aircraft_code | 319
model
| Airbus A319-100
range
| 6700
when_add
| 2017-01-31 18:28:49.230179
operation
| INSERT
--[ RECORD 2 ]-+--------------------------- aircraft_code | 320
model
| Airbus A320-200
range
| 5700 167
when_add
| 2017-01-31 18:28:49.230179
operation
| INSERT
При вставке новых строк могут возникать ситуации, когда нарушается ограничение первичного или уникального ключей, поскольку вставляемые строки могут иметь значения ключевых атрибутов, совпадающие с теми, что уже имеются в таблице. Для таких случаев предусмотрено специальное средство — предложение ON CONFLICT,
оно предусматривает два варианта действий на выбор программиста. Первый вари- ант — отменять добавление новой строки, для которой имеет место конфликт зна- чений ключевых атрибутов, и при этом не порождать сообщения об ошибке. Второй вариант заключается в замене операции добавления новой строки операцией обнов- ления существующей строки, с которой конфликтует добавляемая строка.
Начнем с первого варианта. Попробуем добавить строку, которая гарантированно бу- дет конфликтовать с уже существующей строкой, причем, как по первичному ключу aircraft_code, так и по уникальному ключу model.
WITH add_row AS
( INSERT INTO aircrafts_tmp
VALUES ( 'SU9', 'Sukhoi SuperJet-100', 3000 )
ON CONFLICT DO NOTHING
RETURNING *
)
INSERT INTO aircrafts_log
SELECT add_row.aircraft_code, add_row.model, add_row.range,
CURRENT_TIMESTAMP, 'INSERT'
FROM add_row;
Обратите внимание, что не будет выведено никаких сообщений об ошибках, как это и предполагалось. Строка добавлена не будет:
INSERT 0 0
Нужно учитывать, что это сообщение относится к таблице aircrafts_log, т. е. к команде в главном запросе, а не в общем табличном выражении WITH add_row AS (...), в кото- ром мы работаем с таблицей aircrafts_tmp. Проверьте, не была ли добавлена строка в таблицу aircrafts_tmp.
В том случае, когда в предложении ON CONFLICT не указана дополнительная инфор- мация об именах столбцов или ограничений, по которым предполагается возможный конфликт, проверка выполняется по первичному ключу и по уникальным ключам.
Давайте укажем конкретный столбец для проверки конфликтующих значений. Пусть это будет столбец aircraft_code, т. е. первичный ключ. Для упрощения команды не бу- дем использовать общее табличное выражение. Добавляемая строка будет иметь кон- фликт с существующей строкой как по столбцу aircraft_code, так и по столбцу model.
INSERT INTO aircrafts_tmp
VALUES ( 'SU9', 'Sukhoi SuperJet-100', 3000 )
ON CONFLICT ( aircraft_code ) DO NOTHING
RETURNING *;
Получим только такое сообщение:
168
aircraft_code | model | range
---------------+-------+-------
(0 строк)
INSERT 0 0
Это сообщение было выведено потому, что в команду включено предложение
RETURNING *. Сообщение о дублировании значений столбца model не выводится.
Давайте в команде INSERT изменим значение столбца aircraft_code, чтобы оно стало уникальным:
INSERT INTO aircrafts_tmp
VALUES ( 'S99', 'Sukhoi SuperJet-100', 3000 )
ON CONFLICT ( aircraft_code ) DO NOTHING
RETURNING *;
Поскольку конфликта по столбцу aircraft_code нет, то далее проверяется выполнение требования уникальности по столбцу model. В результате мы получим традиционное сообщение об ошибке, относящееся к столбцу model:
ОШИБКА: повторяющееся значение ключа нарушает ограничение уникальности "aircrafts_tmp_model_key"
,→
ПОДРОБНОСТИ: Ключ "(model)=(Sukhoi SuperJet-100)" уже существует.
Теперь рассмотрим второй вариант обработки предложения ON CONFLICT, когда опе- рация вставки новой строки заменяется операцией обновления существующей стро- ки, с которой и возник конфликт значений столбцов. Для реализации этой возмож- ности служит предложение DO UPDATE.
Давайте модифицируем команду и добавим предложение DO UPDATE. Выберем та- кую политику для работы с таблицей aircrafts_tmp: если при вставке новой строки имеет место дублирование по атрибутам первичного ключа со строкой, находящей- ся в таблице, тогда мы будем обновлять значения всех остальных атрибутов в этой строке, независимо от того, совпадают ли они со значениями в новой строке или нет.
В качестве примера сделаем так: в добавляемой строке значение атрибута model сде- лаем отличающимся от того, которое уже есть в таблице (вместо «Sukhoi SuperJet-100»
будет «Sukhoi SuperJet»), а значение атрибута range оставим без изменений (3000).
Внесем в команду еще одно изменение, а именно: вместо имени столбца, образую- щего первичный ключ, с помощью предложения ON CONSTRAINT укажем наимено- вание ограничения первичного ключа.
Вот так выглядит команда с предложением DO UPDATE:
INSERT INTO aircrafts_tmp
VALUES ( 'SU9', 'Sukhoi SuperJet', 3000 )
ON CONFLICT ON CONSTRAINT aircrafts_tmp_pkey
DO UPDATE SET model = excluded.model,
range = excluded.range
RETURNING *;
Поскольку мы включили в команду предложение RETURNING *, то СУБД сообщит о том, какие значения получат атрибуты обновленной строки. Как и планировалось,
изменилось только значение атрибута model.
169
aircraft_code |
model
| range
---------------+-----------------+-------
SU9
| Sukhoi SuperJet | 3000
(1 строка)
В случае конфликта по столбцу aircraft_code будет обновлена та строка в таблице aircrafts_tmp, с которой конфликтовала вновь добавляемая строка. В результате новая строка добавлена не будет, а будет обновлено значение столбца model в строке, уже находящейся в таблице. А где PostgreSQL возьмет значение для использования в ко- манде UPDATE? Это значение будет взято из специальной таблицы excluded, которая поддерживается самой СУБД. В этой таблице хранятся все строки, предлагаемые для вставки в рамках текущей команды INSERT. Вот это значение — excluded.model. Зна- чение столбца range также будет обновлено, но его новое значение — excluded.range —
совпадает со старым.
Обратите внимание, что в предложении DO UPDATE не указывается имя таблицы, т. к.
таблица будет та же самая, которая указана в предложении INSERT.
Предложение ON CONFLICT DO UPDATE гарантирует атомарное выполнение опера- ции вставки или обновления строк. Атомарность означает, что проверка наличия конфликта и последующее обновление выполняются как неделимая операция, т. е.
другие транзакции не могут изменить значение столбца, вызывающее конфликт, так,
чтобы в результате конфликт исчез и уже стало возможным выполнить операцию
INSERT, а не UPDATE, или, наоборот, в случае отсутствия конфликта он вдруг появил- ся, и уже операция INSERT стала бы невозможной. Такая атомарная операция даже имеет название UPSERT — «UPDATE или INSERT».
Для массового ввода строк в таблицы используется команда COPY. Эта команда может копировать данные из файла в таблицу. Причем, в качестве файла может служить и стандартный ввод. Хотя в этом разделе пособия мы, в основном, говорим о вставке строк в таблицы, но нужно сказать и о том, что эта команда может также выводить данные из таблиц в файлы и на стандартный вывод.
В качестве примера ввода данных из файла давайте добавим две строки в таблицу aircrafts_tmp. Сначала необходимо подготовить текстовый файл, содержащий новые данные. В этом файле каждая строка соответствует одной строке таблицы. Значения атрибутов разделяются символами табуляции, поэтому пробелы, которые в нашем примере есть в столбце model, можно вводить в файл без каких-либо дополнительных экранирующих символов. Заключать строковые значения в одинарные кавычки не нужно, иначе они также будут введены в таблицу. Завершить файл нужно строкой,
содержащей только символы «\.». Получим файл следующего содержания:
IL9
Ilyushin IL96 9800
I93
Ilyushin IL96-300 9800
\.
Теперь нужно ввести команду COPY, указав полный путь к вашему файлу:
COPY aircrafts_tmp FROM '/home/postgres/aircrafts.txt';
В результате будет выведено сообщение об успешном добавлении двух строк:
COPY 2
Давайте проверим, что получилось:
170

SELECT * FROM aircrafts_tmp;
Вы увидите, что новые строки были добавлены, но все те, что уже находились в таб- лице, удалены не были.
При добавлении строк с помощью команды COPY выполняются проверки всех огра- ничений, наложенных на таблицу, поэтому ввести дублирующие данные не получит- ся.
Эту команду можно использовать и для вывода данных из таблицы в файл:
COPY aircrafts_tmp TO '/home/postgres/aircrafts_tmp.txt'
WITH ( FORMAT csv );
Предложение FORMAT csv говорит о том, что при выводе данных значения столбцов разделяются запятыми (CSV — Comma Separated Values).
Получим файл такого вида:
773,Boeing 777-300,11100 763,Boeing 767-300,7900
SU9,Sukhoi SuperJet-100,3000
Если формат не указывать, то данные будут выведены с использованием символов табуляции в качестве разделителей значений атрибутов.
7.2 Обновление строк в таблицах
Команда UPDATE предназначена для обновления данных в таблицах. Начнем с того,
что покажем, как и при изучении команды INSERT, как можно организовать запись выполненных операций в журнальную таблицу. Эта команда аналогична команде,
уже рассмотренной в предыдущем разделе. В ней также «полезная» работа выполня- ется в общем табличном выражении, а запись в журнальную таблицу — в основном запросе.
WITH update_row AS
( UPDATE aircrafts_tmp
SET range = range * 1.2 WHERE model

'^Bom'
RETURNING *
)
INSERT INTO aircrafts_log
SELECT ur.aircraft_code, ur.model, ur.range,
CURRENT_TIMESTAMP, 'UPDATE'
FROM update_row ur;
Выполнив команду, в ответ получим сообщение
INSERT 0 1 171

Напомним, что выведенное сообщение относится непосредственно к внешнему за- просу, в котором выполняется операция INSERT, добавляющая строку в журнальную таблицу. Конечно, если бы строка в таблице aircrafts_tmp не была успешно обновле- на, тогда предложение RETURNING * не возвратило бы внешнему запросу ни одной строки, и, следовательно, тогда просто не было бы данных для формирования новой строки в таблице aircrafts_log.
При использовании команды UPDATE в общем табличном выражении нужно учиты- вать, что главный запрос может получить доступ к обновленным данным только че-
рез временную таблицу
, которую формирует предложение RETURNING:
...
FROM update_row ur;
...
Можно выполнить выборку из журнальной таблицы aircrafts_log, чтобы посмотреть —
правда, не очень длинную — историю изменений строки с описанием самолета
Bombardier CRJ-200.
SELECT * FROM aircrafts_log
WHERE model '^Bom' ORDER BY when_add;
--[ RECORD 1 ]-+--------------------------- aircraft_code | CR2
model
| Bombardier CRJ-200
range
| 2700
when_add
| 2017-02-05 00:27:38.591958
operation
| INSERT
--[ RECORD 2 ]-+--------------------------- aircraft_code | CR2
model
| Bombardier CRJ-200
range
| 3240
when_add
| 2017-02-05 00:27:56.688933
operation
| UPDATE
Представим себе такую ситуацию: руководство компании хочет иметь возможность видеть динамику продаж билетов по всем направлениям, а именно: общее число про- данных билетов и дату/время последнего увеличения их числа для конкретного на- правления.
Создадим временную таблицу, которую назовем tickets_directions. В ней будет четыре столбца:
– город отправления — departure_city;
– город прибытия — arrival_city;
– дата/время последнего увеличения числа проданных билетов — last_ticket_time;
– число проданных билетов на этот момент времени по данному направлению —
tickets_num.
Создадим таблицу с помощью запроса к представлению «Маршруты» (routes) и за- полним данными, однако в ней сначала будет только два первых столбца.
172

CREATE TEMP TABLE tickets_directions AS
SELECT DISTINCT departure_city, arrival_city
FROM routes;
Ключевое слово DISTINCT является здесь обязательным: ведь нам нужны только уни- кальные пары городов отправления и прибытия.
Добавим еще два столбца и заполним столбец-счетчик нулевыми значениями.
ALTER TABLE tickets_directions
ADD COLUMN last_ticket_time timestamp;
ALTER TABLE tickets_directions
ADD COLUMN tickets_num integer DEFAULT 0;
Поскольку PostgreSQL не требует обязательного создания первичного ключа, то не будем создавать его. Это не помешает нам однозначно идентифицировать строки в таблице tickets_directions.
Поскольку в команде UPDATE нет предложения WHERE, в котором было бы условие,
ограничивающее множество обновляемых строк, то будут обновлены все строки таб- лицы — во все будет записано значение 0 в столбец tickets_num.
Для того чтобы не усложнять изложение материала, создадим временную таблицу,
являющуюся аналогом таблицы «Перелеты» (ticket_flights), однако без внешних клю- чей. Поэтому мы сможем добавлять в нее строки, не заботясь о добавлении строк в таблицы «Билеты» (tickets) и «Бронирования» (bookings). Тем не менее, первич- ный ключ все же создадим, чтобы продемонстрировать, что в случае попытки ввода строк с дубликатными значениями первичного ключа значения счетчиков в таблице tickets_directions наращиваться не будут.
CREATE TEMP TABLE ticket_flights_tmp AS
SELECT * FROM ticket_flights WITH NO DATA;
ALTER TABLE ticket_flights_tmp
ADD PRIMARY KEY ( ticket_no, flight_id );
Теперь представим команду, которая и будет добавлять новую запись о про- даже билета и увеличивать значение счетчика проданных билетов в таблице tickets_directions.
WITH sell_ticket AS
( INSERT INTO ticket_flights_tmp
( ticket_no, flight_id, fare_conditions, amount )
VALUES ( '1234567890123', 30829, 'Economy', 12800 )
RETURNING *
)
UPDATE tickets_directions td
SET last_ticket_time = CURRENT_TIMESTAMP,
tickets_num = tickets_num + 1
WHERE ( td.departure_city, td.arrival_city ) =
( SELECT departure_city, arrival_city
FROM flights_v
WHERE flight_id = ( SELECT flight_id FROM sell_ticket )
);
173

UPDATE 1
Этот запрос работает следующим образом. Добавление новой записи о бронировании авиаперелета производится в общем табличном выражении, а наращивание соответ- ствующего счетчика — в главном запросе. Поскольку в общем табличном выраже- нии присутствует предложение RETURNING *, значения атрибутов добавленной стро- ки будут доступны в главном запросе посредством обращения к временной таблице sell_ticket. Конечно, если строка фактически не будет добавлена из-за дублирования значения первичного ключа, тогда будет сгенерировано сообщение об ошибке, в ре- зультате главный запрос выполнен не будет, следовательно, таблица tickets_directions не будет обновлена.
В главном запросе мы обновляем всего два атрибута, причем, значение атри- бута tickets_num может увеличиться только на единицу, поскольку в таблицу ticket_flights_tmp добавляется одна строка. Остается выяснить, каким образом мы сможем определить ту строку в таблице tickets_directions, атрибуты которой нуж- но обновить. Нам требуется на основе значения идентификатора рейса flight_id, на который был забронирован билет (перелет), определить города отправления и при- бытия, которые как раз и идентифицируют строку в таблице tickets_directions. Эти три атрибута присутствуют в представлении flights_v. Подзапрос обращается к этому представлению, а вложенный подзапрос возвращает значение идентификатора рей- са flight_id, на который был забронирован билет (перелет). Назначение вложенного подзапроса в том, чтобы в условии WHERE flight_id = ... не дублировать значение ат- рибута flight_id, использованное в команде INSERT (в данном примере это 30829). Тем самым должен быть снижен риск ошибки при вводе данных.
Обратите внимание, что подзапрос в предложении WHERE возвращает два столбца,
и сравнение выполняется также сразу с двумя столбцами.
Посмотрим, что получилось:
SELECT * FROM tickets_directions WHERE tickets_num > 0;
--[ RECORD 1 ]----+--------------------------- departure_city
| Сочи arrival_city
| Красноярск last_ticket_time | 2017-02-04 21:15:32.903687
tickets_num
| 1
Представим другой вариант этой команды. Его принципиальное отличие от перво- го варианта заключается в том, что для определения обновляемой строки в таблице tickets_directions используется операция соединения таблиц. Здесь в главном за- просе UPDATE присутствует предложение FROM, однако в этом предложении указы- вается только таблица (представление) flights_v, а таблицу tickets_directions в пред- ложение FROM включать не нужно, хотя она и участвует в выполнении соединения таблиц. Конечно, в предложении SET присваивать новые значения можно только ат- рибутам таблицы tickets_directions, поскольку именно она приведена в предложении
UPDATE.
WITH sell_ticket AS
( INSERT INTO ticket_flights_tmp
(ticket_no, flight_id, fare_conditions, amount )
VALUES ( '1234567890123', 7757, 'Economy', 3400 )
RETURNING *
174

)
UPDATE tickets_directions td
SET last_ticket_time = CURRENT_TIMESTAMP,
tickets_num = tickets_num + 1
FROM flights_v f
WHERE td.departure_city = f.departure_city
AND td.arrival_city
= f.arrival_city
AND f.flight_id = ( SELECT flight_id FROM sell_ticket );
UPDATE 1
Посмотрим, что получилось.
SELECT * FROM tickets_directions WHERE tickets_num > 0;
--[ RECORD 1 ]----+--------------------------- departure_city
| Сочи arrival_city
| Красноярск last_ticket_time | 2017-02-04 21:15:32.903687
tickets_num
| 1
--[ RECORD 2 ]----+--------------------------- departure_city
| Москва arrival_city
| Сочи last_ticket_time | 2017-02-04 21:18:40.353408
tickets_num
| 1
Чтобы увидеть комбинированную строку, которая получилась при соединении таб- лиц tickets_directions и flights_v, можно включить в команду UPDATE предложение
RETURNING *.
7.3 Удаление строк из таблиц
Начнем рассмотрение команды DELETE, предназначенной для удаления данных из таблиц, с того, что, как и при изучении команды INSERT, покажем, как можно орга- низовать запись выполненных операций в журнальную таблицу. Эта команда анало- гична команде, уже рассмотренной в предыдущем разделе. В ней также «полезная»
работа выполняется в общем табличном выражении, а запись в журнальную табли- цу — в основном запросе.
WITH delete_row AS
( DELETE FROM aircrafts_tmp
WHERE model '^Bom'
RETURNING *
)
INSERT INTO aircrafts_log
SELECT dr.aircraft_code, dr.model, dr.range,
CURRENT_TIMESTAMP, 'DELETE'
FROM delete_row dr;
Выполнив команду, в ответ получим сообщение
INSERT 0 1 175

Напомним, что выведенное сообщение относится непосредственно к внешнему за- просу, в котором выполняется операция INSERT, добавляющая строку в журнальную таблицу.
Посмотрим историю изменений строки с описанием самолета Bombardier CRJ-200:
SELECT * FROM aircrafts_log
WHERE model '^Bom' ORDER BY when_add;
--[ RECORD 1 ]-+--------------------------- aircraft_code | CR2
model
| Bombardier CRJ-200
range
| 2700
when_add
| 2017-02-05 00:27:38.591958
operation
| INSERT
--[ RECORD 2 ]-+--------------------------- aircraft_code | CR2
model
| Bombardier CRJ-200
range
| 3240
when_add
| 2017-02-05 00:27:56.688933
operation
| UPDATE
--[ RECORD 3 ]-+--------------------------- aircraft_code | CR2
model
| Bombardier CRJ-200
range
| 3240
when_add
| 2017-02-05 00:34:59.510911
operation
| DELETE
Для удаления конкретных строк из данной таблицы можно использовать информа- цию не только из нее, но также и из других таблиц. Выбирать строки для удаления можно двумя способами: использовать подзапросы к этим таблицам в предложении
WHERE или указать дополнительные таблицы в предложении USING, а затем в пред- ложении WHERE записать условия соединения таблиц. Поскольку первый способ яв- ляется традиционным, то мы покажем второй из них.
Предположим, что руководство авиакомпании решило удалить из парка самолетов машины компаний Boeing и Airbus, имеющие наименьшую дальность полета.
Решим эту задачу следующим образом. В общем табличном выражении с помощью условия model '^Airbus' OR model '^Boeing' в предложении WHERE отберем моде- ли только компаний Boeing и Airbus. Затем воспользуемся оконной функцией rank и произведем ранжирование моделей каждой компании по возрастанию дальности полета. Те модели, ранг которых окажется равным 1, будут иметь наименьшую даль- ность полета.
В предложении USING сформируем соединение таблицы aircrafts_tmp с временной таблицей min_ranges, а затем в предложении WHERE зададим условия для отбора строк.
WITH min_ranges AS
( SELECT aircraft_code,
rank() OVER (
1   ...   15   16   17   18   19   20   21   22   ...   28


написать администратору сайта