Введение в базы данных

Введение

Базы данных являются основой практически всех современных информационных систем, и без них невозможно представить работу уже ни одной организации. Развитие СУБД началось еще в 60-е годы, когда разрабатывался проект полета корабля Apollo на Луну.

В середине 60-х годов корпорация IBM совместно с фирмой NAA (North American Aviation) разработали первую СУБД - иерархическую систему IMS (Information Management System).

Другим заметным достижением середины 60-х годов было появление системы IDS (Integrated Data Store) фирмы General Electric. Развитие этой системы привело к созданию нового типа систем управления базами данных – сетевых СУБД, что оказало существенное влияние на информационные системы того поколения. Сетевая СУБД создавалась для представления более сложных взаимосвязей между данными, чем те, которые можно было моделировать с помощью иерархических структур, и послужили основой для разработки первых стандартов БД.

В этом курсе мы рассмотрим базовые понятия управления данными, историю развития СУБД. Обзорно узнаем какие бывают модели данных. Основное внимание будет уделено реляционной модели данных, как наиболее распространенной и языку SQL, как наиболее стандартизированному.

Кроме этого, в учебном пособии уделяется внимание базовым методам проектирования БД и рассматриваются практические примеры разработки приложений.

Содержатся практические задания для закрепления материала.

Понятия предметной области

Система управления базами данных (СУБД) – системное программное обеспечения, для создания и использования баз данных (БД), добавления, изменения и удаления данных в них, выполнения запросов к этим данным и их обработки.

Основные функции СУБД:

  1. Хранение данных.
  2. Управление данными.
  3. Резервное копирование и восстановление данных.
  4. Поддержка языков запросов (язык определения данных, язык манипулирования данными).
  5. Обеспечение и контроль многопользовательского доступа к данным.
  6. Обеспечение целостности.

Модели данных

Длительное время термин «модель данных» использовался без формального определения. Одним из первых специалистов, который достаточно формально определил это понятие, был Э. Кодд. В статье «Модели данных в управлении базами данных» он определил модель данных как комбинацию трёх компонентов [REF]:

  1. Коллекции типов объектов данных, образующих базовые строительные блоки для любой базы данных, соответствующей модели.
  2. Коллекции общих правил целостности, ограничивающих набор экземпляров тех типов объектов, которые законным образом могут появиться в любой такой базе данных.
  3. Коллекции операций, применимых к таким экземплярам объектов для выборки и других целей.

Классификации СУБД по моделям данных:

  1. Иерархические.
  2. Сетевые.
  3. Реляционные.
  4. Ключ-значение.
  5. Документоориентированные.

Иерархические

В иерархической модели, данные организованы в виде древовидной структуры, подразумевая одного родителя для каждой записи. Поле сортировки сохраняет одноуровневые записи в определенном порядке. Иерархические структуры широко использовались в ранних системах управления базами данных мейнфреймов, таких как IBM IMS. Эта структура допускает одно взаимное отношение между двумя типами данных и очень эффективна для описания многих взаимосвязей в реальном мире: рецепты, оглавление, любую вложенную и отсортированную информацию. Файловая система может служить хорошим примером иерархической организации данных.

Эта иерархия используется как физический порядок записей в хранилище. Доступ к записи осуществляется путем навигации вниз по структуре данных с помощью указателей в сочетании с последовательным доступом. Из-за этого иерархическая структура неэффективна для некоторых операций с базой данных, когда полный путь (в отличие от восходящей линии связи и поля сортировки) также не включен для каждой записи. Такие ограничения были компенсированы в более поздних версиях IMS дополнительными логическими иерархиями, наложенными на базовую физическую иерархию.

Схематически иерархическая модель данных показана на рисунке.

Основными операциями над данными в иерархических СУБД являются:

  1. Поиск указанного объекта. Например, сотрудника с указанным номером.
  2. Переход от одного объекта к другому – к дочерней или родительской.
  3. Вставка записи в указанную позицию.
  4. Обновление текущей записи.
  5. Удаление текущей записи.

Основным правилом контроля целостности является необходимость существования родителей для всех потомков. Другими словами, при добавлении элемента, необходимо указывать родительский элемент, а при удалении, следить чтобы не было потомков.

Плюсами иерархических СУБД является быстродействие в случае иерархически упорядоченных данных и эффективное использование памяти.

В тоже время модель данных является сложной для понимания пользователей.

Сетевые

Сетевая модель расширяет иерархическую структуру, позволяя отношения «многие ко многим». Модель определялась спецификацией CODASYL.

Сетевая модель организует данные с использованием двух фундаментальных понятий, называемых записями и наборами. Записи содержат поля (которые могут быть организованы иерархически, как в языке программирования COBOL). Наборы (не путать с математическими наборами) определяют отношения «один ко многим» между записями: один владелец, многие члены. Запись может быть владельцем в любом количестве наборов и членом в любом количестве наборов.

Набор состоит из круговых связанных списков, где один тип записи, владелец набора или родительский элемент, появляется один раз в каждом круге, а второй тип записи, подчиненный или дочерний, может появляться несколько раз в каждом круге. Таким образом, между любыми двумя типами записей может быть установлена иерархия, например, тип A является владельцем B. В то же время может быть определен другой набор, где B является владельцем A. Таким образом, все множества содержат общий ориентированный граф (Право собственности определяет направление) или сетевую конструкцию. Доступ к записям является либо последовательным (обычно в каждом типе записи), либо навигацией в круговых связанных списках.

Сетевая модель способна представлять избыточность данных более эффективно, чем в иерархической модели, и может быть более одного пути от узла-предка до потомка. Операции сетевой модели являются навигационными по стилю: программа поддерживает текущую позицию и перемещается из одной записи в другую, следуя отношениям, в которых участвует запись. Записи также можно найти, указав значения ключа. Хотя это не является существенной особенностью модели, сетевые базы данных обычно реализуют установленные отношения с помощью указателей, которые непосредственно адресуют местоположение записи на диске. Это дает отличную производительность поиска за счет таких операций, как загрузка базы данных и реорганизация.

Популярными СУБД, которые его использовали, были IDMS Cincom Systems и Total ID Cullinet. IDMS получила значительную клиентскую базу; В 1980-х годах приняла реляционную модель и SQL в дополнение к своим оригинальным инструментам и языкам.

Схематически сетевая модель данных показана на рисунке.

Основными операциями над данными в сетевой модели являются:

  1. Поиск записи.
  2. Переход с связному элементу.
  3. Создание новой записи.
  4. Удаление текущей записи.
  5. Обновление текущей записи.
  6. Добавление связи элементов.
  7. Удаление связи элементов.

Положительными качествами сетевой модели данных является зффективная работа с памятью, меньшая избыточность.

К недостаткам можно отнести сложность схемы БД и операций обработки данных. Кроме того из-за допустимости произвольных связей, сложно контролировать их целостность.

Несмотря на то, что сетевые модели появились в 80-х годах прошлого века, а потом были практически полностью вытеснены реляционными, в последние годы активно развиваюся графовые СУБД, в основе которых тоже лежит сетевая модель в виде графа.

Основными элементами графовой модели являются узлы и связи.

В графовых СУБД, как правило, разделяют подсистему хранения и механизм обработки.

Для аналитической работы с большими объёмами данных в глобальных графах применяются специализированные механизмы графовых вычислений.

Графовые базы данных применяются для моделирования социальных графов, в биоинформатике, а также для семантической паутины. Для задач с естественной графовой структурой данных графовые СУБД могут существенно превосходить реляционные по производительности, а также иметь преимущества в наглядности представления и простоте внесения изменений в схему базы данных.

Примерами графовых СУБД выступают: Neo4j, ArangoDB, Memgraph, OrientDB.

Реляционные

Реляционная модель данных предложена сотрудником фирмы IBM Эд­гаром Коддом и основывается на понятии отношение (relation).

Отношение представляет собой множество элементов, называемых кор­тежами (записями). Реляционная модель данных является наиболее популярной в настоящее время и ей посвящена оставшаяся часть книги. Наиболее привычным восприятием для человека является двумерная таблица.

Таблица имеет строки (записи) и столбцы (колонки). Каждая строка таб­лицы имеет одинаковую структуру и состоит из полей. Строкам таблицы соответствуют кортежи, а столбцам — атрибуты отношения.

Физическое размещение данных в реляционных базах на внешних но­сителях легко осуществляется с помощью обычных файлов.

Достоинство реляционной модели данных заключается в простоте, понят­ности и удобстве физической реализации на ЭВМ. Именно простота и по­нятность для пользователя явились основной причиной их широкого исполь­зования. Проблемы же эффективности обработки данных этого типа оказались технически вполне разрешимыми.

Основными недостатками реляционной модели являются следующие: отсутствие стандартных средств идентификации отдельных записей и слож­ность описания иерархических и сетевых связей.

Примерами реляционных СУБД являются: DB2 (IBM), Access (Microsoft), Firebird, RedDatabase (RED SOFT), MySQL и Oracle (Oracle), PostgreSQL.

Ключ-значение

Это простейшая разновидность нереляционных БД. Данные хранятся в виде словаря, где указателем выступает ключ.

Такая БД хранит и обрабатывает разных по типу и содержанию данные. В одном хранилище под разными ключами могут находиться файлы, строки, текст, числа, JSON-объекты и другие типы данных. Эта модель обеспечивает высокую скорость доступа к данным за счет адресного хранения и легко масштабируется. Можно создать правила шардирования по определенным ключам – например, сессии пользователей разных сайтов хранятся в различных сегментах БД.

Поскольку подход не предполагает жесткой типизации и структуризации данных, то контроль их валидности, а также наименования ключей должен реализовывать разработчик.

Примеры СУБД “ключ-значение”: Amazon, DynamoDB, Redis, Riak, LevelDB, различные хранилища кэша – например, Memcached и пр.

Документоориентированные

В отличие от баз типа «Ключ-значение» данные здесь хранятся в структурированных форматах – XML, JSON, BSON. Однако, сохраняется адресный доступ к данным по ключу. При этом содержимое документа может иметь различный набор свойств.

Например, каталог профилей пользователей: один в качестве предпочтений указал любимое блюдо, а другой – видеоигру. Поскольку эти сведения нельзя хранить в одном поле ввиду логической и структурной разобщенности, они записываются в отдельные свойства отдельных документов. При необходимости можно добавить в документы новые свойства, не нарушив при этом общей целостности данных.

Такие СУБД хорошо подходят для быстрой разработки систем и сервисов, работающих с данными, структурированными по разному, легко масштабируются и меняют структуру при необходимости.

Примеры: MongoDB, RethinkDB, CouchDB, DocumentDB.

Распределенные СУБД

СУБД можно классифицировать по степени распределённости:

  1. Локальные СУБД (все части локальной СУБД размещаются на одном компьютере);
  2. Распределённые СУБД (части СУБД могут размещаться на двух и более компьютерах).

Локальные СУБД

Локальная база данных – это база данных, которая полностью располагается на одном компьютере. Это может быть компьютер пользователя или сервер. Соединение с файлом базы данных происходит через спец драйвер или напрямую. Если эта вычислительная система является компонентом компьютерной сети, то возможен доступ к такой базе с других компьютеров, подключенных к сети.

В архитектуре клиент-сервер сервер базы данных, помимо хранения централизованной базы данных, должен обеспечивать выполнение основного объема обработки данных. Запрос на данные, формируется рабочей станцией и передается на сервер. Процесс обработки и выполнения запроса происходит на сервере. Результат выполнения запроса (извлеченные из базы данные, соответствующие запросу) передаются по сети от сервера к клиенту (рабочей станции).

Распределённые СУБД

Распределенная база данных может размещаться на нескольких компьютерах (серверах, узлах).

Каждый узел — это практически полноценная СУБД сама по себе, но узлы взаимодействуют между собой таким образом, что пользователь любого из них может получить доступ к любым данным в сети так, как будто они находятся на его собственном узле.

Из этого определения следует, что так называемая распределенная база данных в действительности представляет собой виртуальную базу данных, компоненты которой физически хранятся в нескольких различных реальных базах данных на нескольких различных узлах (в сущности, являясь логическим объединением этих реальных баз данных).

Доступ к СУБД

По способу доступа различают следующий виды СУБД:

  1. Файл-серверные.
  2. Клиент-серверные.
  3. Встраиваемые.

Файл-серверные

В файл-серверных СУБД файлы данных располагаются централизованно на файл-сервере. СУБД располагается на каждом клиентском компьютере (рабочей станции). Доступ СУБД к данным осуществляется через локальную сеть. Синхронизация чтений и обновлений осуществляется посредством файловых блокировок.

Преимуществом этой архитектуры является низкая нагрузка на процессор файлового сервера.

Недостатки: потенциально высокая загрузка локальной сети; затруднённость или невозможность централизованного управления; затруднённость или невозможность обеспечения таких важных характеристик, как высокая надёжность, высокая доступность и высокая безопасность. Применяются чаще всего в локальных приложениях, которые используют функции управления БД; в системах с низкой интенсивностью обработки данных и низкими пиковыми нагрузками на БД.

На данный момент файл-серверная технология считается устаревшей, а её использование в крупных информационных системах – недостатком.

Примеры: Microsoft Access, Paradox, dBase, FoxPro, Visual FoxPro.

Клиент-серверные

Клиент-серверная СУБД располагается на сервере вместе с БД и осуществляет доступ к БД непосредственно, в монопольном режиме. Все клиентские запросы на обработку данных обрабатываются клиент-серверной СУБД централизованно.

Недостаток клиент-серверных СУБД состоит в повышенных требованиях к серверу.

Достоинства: потенциально более низкая загрузка локальной сети; удобство централизованного управления; удобство обеспечения таких важных характеристик, как высокая надёжность, высокая доступность и высокая безопасность.

Примеры: Oracle, Firebird, RedDatabase, IBM DB2, MS SQL Server,PostgreSQL, MySQL.

Встраиваемые

Встраиваемая СУБД – СУБД, которая может поставляться как составная часть некоторого программного продукта, не требуя процедуры самостоятельной установки. Встраиваемая СУБД предназначена для локального хранения данных своего приложения и не рассчитана на коллективное использование в сети. Физически встраиваемая СУБД чаще всего реализована в виде подключаемой библиотеки. Доступ к данным со стороны приложения может происходить через SQL либо через специальные программные интерфейсы.

Достоинствами является отсутствие накладных расходов на сетевое взаимодействие, простота распространения прикладных программ.

Примеры: Firebird, RedDatabase, SQLite.

Контрольные вопросы

  1. Что такое СУБД?
  2. Какие основные функции СУБД?
  3. Чем определяется модель данных?
  4. Особенности иерархической модели данных?
  5. Основные операции в иерархических БД?
  6. Особенности сетевой модели данных?
  7. Основные операции в сетевых БД?
  8. Особенности графовых СУБД?
  9. В каком виде хранятся данные в реляционных СУБД?
  10. Преимущества баз данных “Ключ-значение”?
  11. Чем отличаются документоориентированные БД от БД “Ключ-значение”?
  12. Различия локальных и распределенных СУБД?
  13. Недостатки файл-серверных СУБД?
  14. Преимущества клиент-серверных СУБД?
  15. Особенности встраиваемых СУБД?

История СУБД

В истории вычислительной техники можно проследить развитие двух основных областей ее использования:

Первая область – применение вычислительной техники для выполнения численных расчетов, сложных алгоритмов обработки с помощью алгоритмических языков, но все они имеют дело с простыми структурами данных, объем которых невелик.

Вторая область – это использование средств вычислительной техники в автоматических или автоматизированных информационных системах.

Информационная система представляет собой программно-аппаратный комплекс, обеспечивающий выполнение следующих функций:

  1. Надежное хранение информации в памяти компьютера.
  2. Выполнение специфических для данного приложения преобразований информации и вычислений.
  3. Предоставление пользователям удобного и легко осваиваемого интерфейса.

Важным шагом в развитии именно информационных систем явился переход к использованию централизованных систем управления файлами.

Файл – это именованная область внешней памяти, в которую можно записывать и из которой можно считывать данные.

Правила именования файлов, способ доступа к данным, хранящимся в файле, и структура этих данных зависят от конкретной системы управления файлами и, возможно, от типа файла. Система управления файлами берет на себя распределение внешней памяти, отображение имен файлов в соответствующие адреса во внешней памяти и обеспечение доступа к данным. Пользователь может выполнить ряд стандартных операций:

  1. Создать файл (требуемого типа и размера).
  2. Открыть ранее созданный файл.
  3. Прочитать из файла некоторую запись (текущую, следующую, предыдущую, первую, последнюю).
  4. Записать в файл на место текущей записи новую, добавить новую запись в конец файла.

Структура записи файла была известна только программе, которая с ним работала. Каждая программа, работающая с файлом, должна была иметь у себя внутри структуру данных, соответствующую структуре этого файла. Поэтому при изменении структуры файла требовалось изменять структуру программы, а это требовало новой компиляции. То есть это означает зависимость программ от данных. Информационные системы используются многими пользователями одновременно. При изменении структуры файлов необходимо изменять программы всех пользователей. А ведет дополнительные затраты на разработку.

Это было первым существенным недостатком файловых систем, который явился толчком к созданию новых систем хранения и управления информацией.

Так как файлы являются общим хранилищем данных, то система управления файлами должна обеспечить авторизацию доступа к файлам. Для каждого существующего файла указываются действия, которые разрешены или запрещены данному пользователю. Каждому зарегистрированному пользователю соответствует пара целочисленных идентификаторов: идентификатор группы, к которой относится этот пользователь, и его собственный идентификатор в группе . Для каждого файла должен храниться полный идентификатор пользователя, который создал этот файл, и фиксироваться, какие действия ему доступы и доступны для других пользователей группы.

Администрирование режимов доступа к файлу в основном выполняется его создателем-владельцем. Для множества файлов, отражающих информационную модель одной предметной области, такой децентрализованный принцип управления доступом вызывал дополнительные трудности. Отсутствие централизованных методов управления доступом к информации послужило еще одной причиной разработки СУБД.

Одновременная работа нескольких пользователей во многопользовательских ОС, связанная с модификацией данных в файле, либо вообще не реализовывалась, либо очень замедлена.

Все эти недостатки послужили развитию нового подхода к управлению информацией. Этот подход был реализован в СУБД. История развития СУБД насчитывает около 60 лет. В 1968 году была введена в эксплуатацию первая промышленная СУБД система IMS фирмы IBM. В 1975 году появился первый стандарт ассоциации по языкам систем обработки данных – Conference of Data System Languages (CODASYL), который определил ряд фундаментальных понятий в теории систем баз данных, которые и до сих пор являются основополагающими для сетевой модели данных. В дальнейшее развитие теории баз данных большой вклад был сделан американским математиком Э. Ф. Коддом, который является создателем реляционной модели данных. В 1981 году Э. Ф. Кодд получил за создание реляционной модели и реляционной алгебры престижную премию Тьюринга Американской ассоциации по вычислительной технике.

Развитие вычислительной техники повлияло также и на развитие технологии баз данных. Можно выделить четыре этапа в развитии данного направления в обработке данных.

Первый этап

Первый этап развития СУБД связан с организацией БД на больших машинах типа IBM 360/370, ЕС-ЭВМ и мини-ЭВМ типа PDP11 (фирмы DEC), разных моделях HP.

Базы данных хранились во внешней памяти центральной ЭВМ, пользователями этих баз данных были задачи, запускаемые в основном в пакетном режиме. Интерактивный режим доступа обеспечивался с помощью консольных терминалов, которые не обладали собственными вычислительными ресурсами (процессором, внешней памятью) и служили только устройствами ввода-вывода для центральной ЭВМ. Программы доступа к БД писались на различных языках и запускались как обычные числовые программы.

Особенности этого этапа развития выражаются в следующем:

  1. Все СУБД базируются на мощных мультипрограммных операционных системах (MVS, SVM, RTE, OSRV, RSX, UNIX), поэтому в основном поддерживается работа с централизованной базой данных в режиме распределенного доступа.
  2. Функции управления распределением ресурсов в основном осуществляются операционной системой (ОС).
  3. Поддерживаются языки низкого уровня манипулирования данными, ориентированные на навигационные методы доступа к данным.
  4. Значительная роль отводится администрированию данных.

Проводятся серьезные работы по обоснованию и формализации реляционной модели данных, и была создана первая система (System R), реализующая идеологию реляционной модели данных.

Проводятся теоретические работы по оптимизации запросов и управлению распределенным доступом к централизованной БД, было введено понятие транзакции. Результаты научных исследований открыто обсуждаются в печати, идет поток общедоступных публикаций, касающихся всех аспектов теории и практики баз данных, и результаты теоретических исследований активно внедряются в коммерческие СУБД. Появляются первые языки высокого уровня для работы с реляционной моделью данных. Однако отсутствуют стандарты для этих первых языков.

Второй этап

Второй этап – это этап развития персональных компьютеров.

Особенности этого этапа следующие:

  1. Все СУБД были рассчитаны на создание БД в основном с монопольным доступом.
  2. Большинство СУБД имели развитый и удобный пользовательский интерфейс.
  3. Существовал интерактивный режим работы с БД как в рамках описания БД, так и в рамках проектирования запросов. Кроме того, большинство СУБД предлагали развитый и удобный инструментарий для разработки готовых приложений без программирования (на основе готовых шаблонов форм, конструкторов запросов).
  4. Во всех СУБД поддерживался только внешний уровень представления реляционной модели, то есть только внешний табличный вид структур данных.
  5. При наличии высокоуровневых языков манипулирования данными типа реляционной алгебры и SQL в настольных СУБД поддерживались низкоуровневые языки манипулирования данными на уровне отдельных строк таблиц.
  6. В настольных СУБД отсутствовали средства поддержки ссылочной и структурной целостности базы данных. Эти функции должны были выполнять приложения.

Наличие монопольного режима работы фактически привело к вырождению функций администрирования БД и в связи с этим – к отсутствию инструментальных средств администрирования БД. Сравнительно скромные требования к аппаратному обеспечению со стороны настольных СУБД.

Третий этап

Третий этап – базы данных “клиент-сервер”.

Особенности этого этапа:

Практически все современные СУБД обеспечивают поддержку полной реляционной модели, а именно:

  1. Допустимыми являются только данные, представленные в виде отношений реляционной модели.
  2. Поддерживаются только языки манипулирования данными высокого уровня (в основном SQL).
  3. Контроль за соблюдением ссылочной целостности в течение всего времени функционирования системы, и гарантии невозможности со стороны СУБД нарушить эти ограничения.

Большинство современных СУБД рассчитаны на многоплатформенную архитектуру, то есть они могут работать на компьютерах с разной архитектурой и под разными операционными системами.

Необходимость поддержки многопользовательской работы с базой данных и возможность децентрализованного хранения данных потребовали развития средств администрирования БД с реализацией общей концепции средств защиты данных.

Для того чтобы не потерять клиентов, которые ранее работали на настольных СУБД, практически все современные СУБД имеют средства подключения клиентских приложений, разработанных с использованием настольных СУБД, и средства экспорта данных из форматов настольных СУБД второго этапа развития.

Разработка стандартов языков описания и манипулирования данными SQL89, SQL92, SQL99 и технологий по обмену данными между различными СУБД.

Четвертый этап

Четвертый этап характеризуется появлением новой технологии доступа к данным – интранет.

Основное отличие этого подхода от технологии клиент-сервер состоит в том, что отпадает необходимость использования специализированного клиентского программного обеспечения. Для работы с удаленной базой данных используется стандартный браузер.

При этом встроенный в загружаемые пользователем HTML-страницы код, написанный обычно на языке Java, Java-script, Perl и других, отслеживает все действия пользователя и транслирует их в низкоуровневые SQL-запросы к базе данных, выполняя, таким образом, ту работу, которой в технологии клиент-сервер занимается клиентская программа. Сложные задачи реализованы в архитектуре “клиент-сервер” с разработкой специального клиентского программного обеспечения.

Контрольные вопросы

  1. Назовите области применения вычислительной техники?
  2. Дайте определение информационной системы?
  3. Особенности работы централизованных систем управления файлами?
  4. Первый этап развития СУБД?
  5. Второй этап развития СУБД?
  6. Третий этап развития СУБД?
  7. Четвертый этап развития СУБД?

Реляционная модель данных

Базовые понятия

Реляционная модель данных — это подход к организации и управлению данными, основанный на математической теории множеств и логике первого порядка. Она была предложена Эдгардом Коддом в 1970 году и стала основой для современных систем управления базами данных (СУБД).

Реляционная модель данных описывает:

  • Структуры данных в виде (изменяющихся во времени) наборов отношений.
  • Теоретико-множественные операции над данными: объединение, пересечение, разность и декартово произведение.
  • Специальные реляционные операции: селекция, проекция, соединение и деление.
  • Специальные правила, обеспечивающие целостность данных.

Основными понятиями реляционных баз данных являются:

  • тип данных,
  • домен,
  • атрибут,
  • кортеж,
  • первичный ключ
  • и отношение.

Покажем смысл этих понятий на примере отношения СОТРУДНИКИ, содержащего информацию о сотрудниках некоторой организации:

Тип данных

Понятие типа данных в реляционной модели данных во многом аналогично понятию типа данных в языках программирования. Типы данных в реляционных базах данных определяют, какие значения могут храниться в атрибутах (столбцах) таблиц, и обеспечивают корректность операций с данными.

Типы данных в реляционных базах данных можно разделить на несколько категорий:

  1. Символьные данные (строки):

    • CHAR(n) — строка фиксированной длины (n символов).
    • VARCHAR(n) — строка переменной длины (до n символов).
    • TEXT — строка произвольной длины.
  2. Числовые данные:

    • INTEGER (или INT) — целое число.
    • SMALLINT — малое целое число.
    • BIGINT — большое целое число.
    • DECIMAL(p, s) или NUMERIC(p, s) — точные числа с фиксированной точностью (p — общее количество цифр, s — количество цифр после запятой).
    • FLOAT или REAL — числа с плавающей точкой.
  3. Специализированные числовые данные:

    • MONEY — тип для хранения денежных значений (обычно с фиксированной точностью).
  4. Темпоральные данные (временные):

    • DATE — дата (год, месяц, день).
    • TIME — время (часы, минуты, секунды).
    • TIMESTAMP — дата и время.
    • INTERVAL — временной интервал.
  5. Другие типы:

    • BOOLEAN — логический тип (TRUE/FALSE).
    • BLOB (Binary Large Object) — для хранения бинарных данных (например, изображений).
    • JSON или XML — для хранения структурированных данных в форматах JSON или XML.

Типы данных в реляционных БД действительно похожи на типы данных в языках программирования, но есть и отличия:

  • В БД типы данных часто имеют более строгие ограничения (например, фиксированная длина строки CHAR(n)).
  • В БД типы данных могут быть оптимизированы для хранения и обработки больших объемов данных.
  • В БД поддерживаются специализированные типы (например, DATE, MONEY), которые редко встречаются в языках программирования в явном виде.

Таким образом, типы данных в реляционных БД обеспечивают гибкость и надежность при работе с информацией, а также позволяют адаптировать систему под конкретные задачи.

Домен

Понятие домена более специфично для баз данных, хотя и имеет некоторые аналогии с подтипами в некоторых языках программирования. Оно играет ключевую роль в обеспечении семантической целостности данных и их корректной интерпретации.

Домен — это множество допустимых значений для атрибута (столбца) таблицы, которое определяется на основе:

  1. Базового типа данных (например, целые числа, строки, даты).
  2. Ограничений (логических выражений), которые задают дополнительные правила для значений.

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

Например, домен «Имена» может быть определен следующим образом:

  • Базовый тип данных: VARCHAR(50) (строка символов длиной до 50 символов).
  • Ограничения:
    • Строка должна начинаться с заглавной буквы.
    • Строка не может начинаться с мягкого знака.
    • Строка может содержать только буквы и дефисы.

Таким образом, домен «Имена» не только определяет тип данных, но и задает правила, которые должны соблюдаться для всех значений этого домена.

Домен несет в себе семантическую нагрузку, то есть определяет смысл данных. Это означает, что данные, принадлежащие разным доменам, даже если они имеют одинаковый базовый тип, не являются сравнимыми или взаимозаменяемыми.

Например,

  • Домен «Номера пропусков»:
    • Базовый тип: INTEGER.
    • Ограничения: значения должны быть положительными и уникальными.
  • Домен «Номера групп»:
    • Базовый тип: INTEGER.
    • Ограничения: значения должны быть в диапазоне от 1 до 100.

Хотя оба домена используют базовый тип INTEGER, они представляют разные сущности, и их значения не являются сравнимыми. Например, номер пропуска «123» и номер группы «123» — это разные данные, даже если они имеют одинаковое числовое значение.

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

  • В языке Pascal можно определить тип TName = string[50], который ограничивает строки длиной 50 символов.
  • В языке Python можно использовать классы для создания пользовательских типов с дополнительными проверками.

Однако в базах данных домены имеют более строгую семантическую привязку и используются для обеспечения целостности данных на уровне всей системы.

Преимущества использования доменов:

  1. Семантическая ясность: домены делают данные более понятными, так как они отражают их смысл.
  2. Целостность данных: ограничения домена предотвращают попадание некорректных значений в базу данных.
  3. Упрощение проектирования: домены позволяют повторно использовать типы данных с одинаковыми ограничениями.
  4. Сравнимость данных: данные считаются сравнимыми только в пределах одного домена, что предотвращает логические ошибки.

Атрибут

Понятие атрибута в базах данных связано с полями записей и их характеристиками.

Атрибут — это свойство или характеристика сущности (объекта), которая описывается в таблице базы данных. В реляционной модели данных атрибут соответствует столбцу таблицы и характеризует:

  • Имя атрибута — уникальное название столбца (например, FirstName, Age, Salary).
  • Тип данных или домен — определяет, какие значения могут храниться в атрибуте (например, целые числа, строки, даты).

В нашем примере атрибутами являются:

  • СОТР_НОМЕР — тип данных: целое число.
  • СОТР_ИМЯ — тип данных: строка.
  • СОТР_ЗАРП — тип данных: число с плавающей точкой.
  • СОТР_ОТД_НОМЕР — тип данных: целое число.

Запись

Понятие записи в реляционных базах данных связано со строками таблицы, которые содержат данные в виде набора полей.

Запись (или кортеж) — это строка в таблице базы данных, которая представляет собой набор значений, соответствующих атрибутам (столбцам) таблицы. Каждая запись описывает один экземпляр сущности (объекта), например, одного сотрудника, один заказ или один товар.

Запись состоит из полей, каждое из которых соответствует определенному атрибуту таблицы. Поля могут содержать данные разных типов, таких как:

  • Целые числа (INT).
  • Строки (VARCHAR, TEXT).
  • Даты (DATE).
  • Числа с плавающей точкой (DECIMAL, FLOAT).
  • Логические значения (BOOLEAN).

В нашем примере в отношениие СОТРУДНИКИ:

  • Каждая строка (например, 2934, "Иванов", 112.00 , 310) — это запись.
  • Поля записи:
    • СОТР_НОМЕР = 2934 (целое число).
    • СОТР_ИМЯ = "Иванов" (строка).
    • СОТР_ЗАРП = 112.00 (строка).
    • СОТР_ОТД_НОМЕР = 310 (целое число).

Каждая запись в таблице должна быть уникальной. Это обеспечивается с помощью первичного ключа (Primary Key), например, атрибута СОТР_НОМЕР. Поля в записи соответствуют порядку атрибутов в таблице.

Первичный ключ

Понятие ключа отношения (или просто ключа) в реляционных базах данных является фундаментальным для обеспечения уникальности и целостности данных.

Ключ отношения — это атрибут (или набор атрибутов) таблицы, который однозначно идентифицирует каждый кортеж (запись) в этой таблице. Ключ гарантирует, что никакие две записи в таблице не могут иметь одинаковые значения для этого атрибута (или набора атрибутов).

Бывают ключи двух типов.

  1. Простой ключ:

    • Состоит из одного атрибута.
    • Пример: В отношении СОТРУДНИКИ атрибут СОТР_НОМЕР может быть простым ключом, если каждый сотрудник имеет уникальный (например, табельный) номер.
  2. Составной ключ:

    • Состоит из нескольких атрибутов.
    • Пример: Номер паспорта может состоять из атрибутов СЕРИЯ и НОМЕР.

Отношение

Понятие отношение в реляционных базах данных связано с двумерными таблицами, которые используются для хранения и организации данных.

Отношение — это двумерная таблица, которая состоит из:

  • Строк (записей, кортежей): каждая строка представляет собой отдельный экземпляр сущности (например, одного сотрудника, один заказ).
  • Столбцов (атрибутов): каждый столбец описывает определенное свойство сущности (например, имя, возраст, зарплата).

Отношение является основным способом представления данных в реляционной модели.

Реляционная алгебра

Отношения являются основой для операций реляционной алгебры, которые используются для манипуляции данными в реляционных базах данных. Основные операции включают:

  • Выборка (SELECT) — извлечение строк, удовлетворяющих условию.
  • Проекция (PROJECT) — извлечение определенных столбцов.
  • Объединение (JOIN) — комбинирование данных из нескольких отношений.
  • Декартово произведение (CARTESIAN PRODUCT) — создание всех возможных комбинаций строк из двух отношений.

Фундаментальные свойства отношений

Рассмотрим некоторые важные свойства отношений:

  1. Отсутствие кортежей-дубликатов. То свойство, что отношения не содержат кортежей-дубликатов, следует из определения отношения как множества кортежей. В классической теории множеств по определению каждое множество состоит из различных элементов. Для каждого отношения по крайней мере полный набор его атрибутов обладает этим свойством. Однако при формальном определении первичного ключа требуется обеспечение его «минимальности», т.е. в набор атрибутов первичного ключа не должны входить такие атрибуты, которые можно отбросить без ущерба для основного свойства - однозначно определять кортеж. Понятие первичного ключа является исключительно важным в связи с понятием целостности баз данных. Заметим, что во многих практических реализациях реляционных СУБД допускается нарушение свойства уникальности кортежей для промежуточных отношений, порождаемых неявно при выполнении запросов. Такие отношения являются не множествами, а мультимножествами, что в ряде случаев позволяет добиться определенных преимуществ, но иногда приводит к серьезным проблемам.
  2. Отсутствие упорядоченности кортежей. Свойство отсутствия упорядоченности кортежей отношения также является следствием определения отношения-экземпляра как множества кортежей. Отсутствие требования к поддержанию порядка на множестве кортежей отношения дает дополнительную гибкость СУБД при хранении баз данных во внешней памяти и при выполнении запросов к базе данных. Это не противоречит тому, что при формулировании запроса к БД, например, на языке SQL можно потребовать сортировки результирующей таблицы в соответствии со значениями некоторых столбцов. Такой результат, вообще говоря, не отношение, а некоторый упорядоченный список кортежей.
  3. Отсутствие упорядоченности атрибутов. Атрибуты отношений не упорядочены. Для ссылки на значение атрибута в кортеже отношения всегда используется имя атрибута. Это свойство теоретически позволяет, например, модифицировать схемы существующих отношений не только путем добавления новых атрибутов, но и путем удаления существующих атрибутов.
  4. Атомарность значений атрибутов. Значения всех атрибутов являются атомарными. Это следует из определения домена как потенциального множества значений простого типа данных, т.е. среди значений домена не могут содержаться множества значений (отношения).

Целостность сущности и ссылок

Наконец, в целостной части реляционной модели данных фиксируются два базовых требования целостности, которые должны поддерживаться в любой реляционной СУБД. Первое требование называется требованием целостности сущностей. Объекту или сущности реального мира в реляционных БД соответствуют кортежи отношений. Конкретно требование состоит в том, что любой кортеж любого отношения отличим от любого другого кортежа этого отношения, т.е. другими словами, любое отношение должно обладать первичным ключом. Как мы видели в предыдущем разделе, это требование автоматически удовлетворяется, если в системе не нарушаются базовые свойства отношений.

Второе требование называется требованием целостности по ссылкам и является несколько более сложным. Очевидно, что при соблюдении нормализованности отношений сложные сущности реального мира представляются в реляционной БД в виде нескольких кортежей нескольких отношений. Например, представим, что нам требуется представить в реляционной базе данных сущность ОТДЕЛ с атрибутами ОТД_НОМЕР (номер отдела), ОТД_КОЛ (количество сотрудников) и ОТД_СОТР (набор сотрудников отдела). Для каждого сотрудника нужно хранить СОТР_НОМЕР (номер сотрудника), СОТР_ИМЯ (имя сотрудника) и СОТР_ЗАРП (заработная плата сотрудника). При правильном проектировании соответствующей БД в ней появятся два отношения: ОТДЕЛЫ ( ОТД_НОМЕР, ОТД_КОЛ ) (первичный ключ -ОТД_НОМЕР) и СОТРУДНИКИ (СОТР_НОМЕР, СОТР_ИМЯ, СОТР_ЗАРП, СОТР_ОТД_НОМ ) (первичный ключ - СОТР_НОМЕР).

Как видно, атрибут СОТР_ОТД_НОМ появляется в отношении СОТРУДНИКИ не потому, что номер отдела является собственным свойством сотрудника, а лишь для того, чтобы иметь возможность восстановить при необходимости полную сущность ОТДЕЛ. Значение атрибута СОТР_ОТД_НОМ в любом кортеже отношения СОТРУДНИКИ должно соответствовать значению атрибута ОТД_НОМ в некотором кортеже отношения ОТДЕЛЫ. Атрибут такого рода называется внешним ключом, поскольку его значения однозначно характеризуют сущности, представленные кортежами некоторого другого отношения (т.е. задают значения их первичного ключа). Говорят, что отношение, в котором определен внешний ключ, ссылается на соответствующее отношение, в котором такой же атрибут является первичным ключом.

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

Ограничения целостности сущности и по ссылкам должны поддерживаться СУБД. Для соблюдения целостности сущности достаточно гарантировать отсутствие в любом отношении кортежей с одним и тем же значением первичного ключа. С целостностью по ссылкам дела обстоят несколько более сложно.

Понятно, что при обновлении ссылающегося отношения (вставке новых кортежей или модификации значения внешнего ключа в существующих кортежах) достаточно следить за тем, чтобы не появлялись некорректные значения внешнего ключа. Но как быть при удалении кортежа из отношения, на которое ведет ссылка? Здесь существуют три подхода, каждый из которых поддерживает целостность по ссылкам. Первый подход заключается в том, что запрещается производить удаление кортежа, на который существуют ссылки (т.е. сначала нужно либо удалить ссылающиеся кортежи, либо соответствующим образом изменить значения их внешнего ключа). При втором подходе при удалении кортежа, на который имеются ссылки, во всех ссылающихся кортежах значение внешнего ключа автоматически становится неопределенным. Наконец, третий подход (каскадное удаление) состоит в том, что при удалении кортежа из отношения, на которое ведет ссылка, из ссылающегося отношения автоматически удаляются все ссылающиеся кортежи.

В развитых реляционных СУБД обычно можно выбрать способ поддержания целостности по ссылкам для каждой отдельной ситуации определения внешнего ключа. Для принятия такого решения необходимо анализировать требования конкретной прикладной области.

Преимущества реляционного подхода

Преимущества реляционного подхода достаточно очевидны:

  1. Предсказуемость результатов работы с данными. В основе реляционной модели лежит математическая модель, следовательно, любой запрос к базе данных, составленный на корректном языке влечет ответ, однозначно определяемый схемой БД и конкретными данными. При этом пользователю не требуется информация о физической организации данных.
  2. Предметная область часто достаточно естественно описывается в терминах таблиц.

По этим причинам идея создания реляционной СУБД стала популярна среди разработчиков вскоре после ее появления. Сейчас существует множество коммерческих и некоммерческих систем, создатели которых заявляют об их «реляционности». Для того, чтобы более определенно сформулировать цель, к которой разработчикам нужно стремится, Е.Кодд в конце 70-х годов опубликовал 12 правил соответствия реляционной модели, которые опираются на основное (подразумеваемое) правило:

Система, которая провозглашается поставщиком как реляционная СУБД, должна управлять базами данных исключительно способами, соответствующими реляционной модели.

Конкретные требования к реляционной СУБД раскрываются в следующих правилах:

  1. Информационное правило. Вся информация, хранимая в базе данных, должна быть представлена единственным образом: в виде значений в реляционных таблицах.

  2. Правило гарантированного логического доступа. К каждому имеющемуся в реляционной базе атомарному значению должен быть гарантирован доступ с помощью указания имени таблицы, значения первичного ключа и имени атрибута.

  3. Правило наличия значения (missing information). В полностью реляционной СУБД должны иметься специальные индикаторы (отличные от пустой символьной строки или строки из одних пробелов и отличные от нуля или какого-то другого числового значения) для выражения (на логическом уровне, не зависимо от типа данных) того факта, что значение отсутствует по меньшей мере по двум различным причинам: его действительно нет, либо оно не применимо к данной позиции. СУБД должна не только отражать этот факт, но и распространять на такие индикаторы свои функции манипулирования данными не зависимо от типа данных. Как правило это значение обозначается NULL.

  4. Правило динамического реляционного каталога. Описание базы данных выглядит логически как обычные данные, так что авторизованные пользователи и прикладные программы могут употреблять для работы с этим описанием тот же реляционный язык, что и при работе с обычными данными.

  5. Правило полноты языка работы с данными. Сколько бы много в СУБД ни поддерживалось языков и режимов работы с данными, должен иметься по крайней мере один язык, выразимый в виде командных строк в некотором удобном синтаксисе, который бы позволял формулировать:

    1. определение данных;
    2. определение правил целостности;
    3. манипулирование данными (в диалоге и из программы);
    4. определение таблиц-представлений (в том числе и возможности их модификации);
    5. определение правил авторизации;
    6. границы транзакций.
  6. Правило модификации таблиц-представлений. В СУБД должен существовать корректный алгоритм, позволяющий автоматически для каждой таблицы-представления определять во время ее создания, может ли она использоваться для вставки и удаления строк и какие из столбцов допускают модификацию, и заносящий полученную таким образом информацию в системный каталог.

  7. Правило множественности операций. Возможность оперирования базовыми таблицами или таблицами-представлениями распространяется полностью не только на выдачу информации из БД, но и на вставку, модификацию и удаление данных.

  8. Правило физической независимости. Диалоговые операторы и прикладные программы на логическом уровне не должны страдать от каких-либо изменений во внутреннем хранении данных или методах доступа СУБД

  9. Правило логической независимости. Диалоговые операторы и прикладные программы на логическом уровне не должны страдать от таких изменений в базовых таблицах, которые сохраняют информацию и теоретически допускают неизменность этих операторов и программ.

  10. Правило сохранения целостности. Диалоговые операторы и прикладные программы не должны изменяться при изменении правил целостности в БД, задаваемых языком работы с данными и хранимых в системном каталоге.

  11. Правило независимости от распределенности. Диалоговые операторы и прикладные программы на логическом уровне не должны зависеть от совершаемого физического разнесения данных (если первоначально СУБД работала с нераспределенными данными) или перераспределения (если СУБД распределенная).

  12. Правило ненарушения реляционного языка. Если в реляционной СУБД имеется язык низкого уровня (для работы с отдельными строками), он не должен позволять нарушать или «обходить» правила, сформулированные на языке высокого уровня (множественном) и занесенные в системный каталог.

Контрольные вопросы

  1. Дайте определение реляционной модели данных?
  2. Что описывает реляционная модель данных?
  3. Назовите основные понятия реляционной БД?
  4. Чем тип данных отличается от домена?
  5. Назовите особенности типов данных в базах данных?
  6. Что лежит в основе определения домена?
  7. Назовите преимущества использования доменов?
  8. Что такое атрибут?
  9. Дайте определение записи?
  10. Дайте определение первичного ключа?
  11. Что такое отношение?
  12. Назовите основные операции реляционной алгебры?
  13. Назовите фундаментальные свойства отношений и объясните их смысл?
  14. В чем заключается целостность сущности?
  15. В чем состоит требования целостности по ссылкам?
  16. Как поддерживается целостность по ссылкам?
  17. Объясните преимущества реляционного подхода?
  18. Какие требования предъявляются к реляционным СУБД?

Модель “Сущность—Связь”

Семантическое моделирование, основанное на диаграммах сущность-связь (Entity-Relationship, ER-диаграммы), является важным этапом проектирования базы данных. Оно позволяет визуализировать структуру данных, их взаимосвязи и атрибуты, что делает процесс проектирования более понятным и эффективным.

Нотации ER-диаграмм:

Различные нотации предлагают свои способы визуализации элементов ER-модели. Вот некоторые из них:

  1. Нотация Чена (Chen Notation):

    • Первая и классическая нотация, предложенная Питером Ченом в 1976 г.
    • Сущности изображаются прямоугольниками, атрибуты — овалами, связи — ромбами.
  2. Нотация Мартина (Crow’s Foot Notation):

    • Использует специальные символы для обозначения модальности (например, “воронья лапка” для связи “многие”).
    • Популярна благодаря своей наглядности.
  3. Нотация IDEF1X:

    • Используется для моделирования реляционных баз данных.
    • Акцент делается на строгости и детализации.
  4. Нотация Баркера (Barker Notation):

    • Разработана для Oracle.
    • Использует простые и понятные обозначения.

По сути, все варианты диаграмм сущность-связь исходят из одной идеи - рисунок всегда нагляднее текстового описания. Все такие диаграммы используют графическое изображение сущностей предметной области, их свойств (атрибутов), и взаимосвязей между сущностями.

Мы опишем работу с ER-диаграммами близко к нотации Баркера, как довольно легкой в понимании основных идей.

Понятия ER-диаграмм

Сущность (Entity)

Сущность – это класс однотипных объектов, информация о которых должна быть учтена в модели. Каждая сущность должна иметь наименование, выраженное существительным в единственном числе. Примерами сущностей могут быть такие классы объектов как “Поставщик”, “Сотрудник”, “Накладная”.

Каждая сущность в модели изображается в виде прямоугольника с наименованием:

Экземпляр сущности

Экземпляр сущности – это конкретный представитель данной сущности. Например, представителем сущности “Сотрудник” может быть “Сотрудник Иванов”. Экземпляры сущностей должны быть различимы, т.е. сущности должны иметь некоторые свойства, уникальные для каждого экземпляра этой сущности.

Атрибут сущности

Атрибут сущности – это именованная характеристика, являющаяся некоторым свойством сущности. Наименование атрибута должно быть выражено существительным в единственном числе (возможно, с характеризующими прилагательными). Примерами атрибутов сущности “Сотрудник” могут быть такие атрибуты как “Табельный номер”, “Фамилия”, “Имя”, “Отчество”, “Должность”, “Зарплата” и т.п.

Атрибуты изображаются в пределах прямоугольника, определяющего сущность:

Ключ сущности

Ключ сущности – это неизбыточный набор атрибутов, значения которых в совокупности являются уникальными для каждого экземпляра сущности. Неизбыточность заключается в том, что удаление любого атрибута из ключа нарушается его уникальность. Сущность может иметь несколько различных ключей.

Ключевые атрибуты изображаются на диаграмме подчеркиванием:

Связь (Relationship)

Связь – это некоторая ассоциация между двумя сущностями. Одна сущность может быть связана с другой сущностью или сама с собою. Связи позволяют по одной сущности находить другие сущности, связанные с нею. Например, связи между сущностями могут выражаться следующими фразами - “СОТРУДНИК может иметь несколько ДЕТЕЙ”, “каждый СОТРУДНИК обязан числиться ровно в одном ОТДЕЛЕ”.

Графически связь изображается линией, соединяющей две сущности:

Каждая связь имеет два конца и одно или два наименования. Наименование обычно выражается в неопределенной глагольной форме: “иметь”, “принадлежать” и т.п. Каждое из наименований относится к своему концу связи. Иногда наименования не пишутся ввиду их очевидности.

Каждая связь может иметь один из следующих типов связи:

Связь типа один-к-одному означает, что один экземпляр первой сущности (левой) связан с одним экземпляром второй сущности (правой). Связь один-к-одному чаще всего свидетельствует о том, что на самом деле мы имеем всего одну сущность, неправильно разделенную на две.

Связь типа один-ко-многим означает, что один экземпляр первой сущности (левой) связан с несколькими экземплярами второй сущности (правой). Это наиболее часто используемый тип связи. Левая сущность (со стороны “один”) называется родительской, правая (со стороны “много”) - дочерней.

Связь типа много-ко-многим означает, что каждый экземпляр первой сущности может быть связан с несколькими экземплярами второй сущности, и каждый экземпляр второй сущности может быть связан с несколькими экземплярами первой сущности. Тип связи много-ко-многим является временным типом связи, допустимым на ранних этапах разработки модели. В дальнейшем этот тип связи должен быть заменен двумя связями типа один-ко-многим путем создания промежуточной сущности.

Каждая связь может иметь одну из двух модальностей связи:

Модальность “может” означает, что экземпляр одной сущности может быть связан с одним или несколькими экземплярами другой сущности, а может быть и не связан ни с одним экземпляром.

Модальность “должен” означает, что экземпляр одной сущности обязан быть связан не менее чем с одним экземпляром другой сущности.

Связь может иметь разную модальность с разных концов.

Описанный графический синтаксис позволяет однозначно читать диаграммы, пользуясь следующей схемой построения фраз:

<Каждый экземпляр СУЩНОСТИ 1> <МОДАЛЬНОСТЬ СВЯЗИ> <НАИМЕНОВАНИЕ СВЯЗИ> <ТИП СВЯЗИ> <экземпляр СУЩНОСТИ 2>

Каждая связь может быть прочитана как слева направо, так и справа налево. Связь на рисунке читается так:

  • Слева направо: “каждый сотрудник может иметь несколько детей”.
  • Справа налево: “Каждый ребенок обязан принадлежать ровно одному сотруднику”.

Справедливо и обратное. В ходе анализа предметной области многие фразы можно привести к такому эталонному виду, а затем, выделить из них сущности, связи и их модальности. Как это делать мы увидим в следующей главе.

Преимущества семантического моделирования:

  • Наглядность: Диаграммы позволяют быстро понять структуру данных и их взаимосвязи.
  • Упрощение проектирования: Помогает выявить ошибки и противоречия на ранних этапах.
  • Документирование: ER-диаграммы служат документацией для разработчиков и аналитиков.
  • Коммуникация: Упрощает обсуждение структуры данных между заинтересованными сторонами.

Контрольные вопросы

  1. В чем ценность и смысл диаграмм “Сущность-связь”?
  2. Кто и когда предложил использовать ER-диаграммы?
  3. В чем разница сущности и экземпляра сущности?
  4. Приведите примеры атрибутов сущности?
  5. Что такое ключ сущности и каковы его свойства?
  6. Что характеризует связь двух сущностей?
  7. Какие типы связей существуют?
  8. Что характеризует модальность связи?
  9. Назовите преимущества семантического моделирования?

Пример разработки ER-модели

Рассмотрим применение ER-диаграмм на практике. При разработке ER-моделей мы должны получить следующую информацию о предметной области:

  1. Список сущностей предметной области.
  2. Список атрибутов сущностей.
  3. Описание взаимосвязей между сущностями.

Процесс разработки ER-диаграммы является итерационным. Первый вариант диаграммы может быть не полным и ставить вопросы для уточнения с экспертами предметной области. Результатом уточнений является следующий вариант ER-диаграммы и т.д. пока не будут сняты все вопросы и уточнены все детали. ER-диаграммы в тоже время будут выполнять роль документации. Список сущностей как правило определяется всегда в первую очередь. Атрибуты и сущности могут определятся и дополнятся в любом порядке, в зависимости от доступности информации.

В качестве примера рассмотрим процесс разработки ER-диаграммы для проектирования информационной системы оптовой фирмы. В первую очередь мы должны изучить предметную область. Опросим сотрудников фирмы, изучим документацию, формы заказов, накладных и т.п.

Например, в ходе беседы с менеджером по продажам, выяснилось, что он (менеджер) считает, что проектируемая система должна выполнять следующие действия:

  1. Хранить информацию о покупателях.
  2. Печатать накладные на отпущенные товары.
  3. Следить за наличием товаров на складе.

Мы помним, что сущности и атрибуты именуются существительными. Таким образом, чтобы выделить потенциальных кандидатов на роль сущностей и атрибутов, выпишем все существительные из заметок по итогам общения с менеджером. Непонятные термины будем выделять знаком вопроса.

  1. Покупатель - явный кандидат на сущность.
  2. Накладная - явный кандидат на сущность.
  3. Товар - явный кандидат на сущность.
  4. (?)Склад - а вообще, сколько складов имеет фирма? Если несколько, то это будет кандидатом на новую сущность.
  5. (?)Наличие товара - это, скорее всего, атрибут, но атрибут какой сущности?

Сразу возникает очевидная связь между сущностями - “покупатели могут покупать много товаров” и “товары могут продаваться многим покупателям”. Первый вариант диаграммы выглядит так:

Для уточнения поставленных вопросов мы вновь общаемся с менеджером и выясняем, что фирма имеет несколько складов. Причем, каждый товар может храниться на нескольких складах и быть проданным с любого склада.

Куда поместить сущности “Накладная” и “Склад” и с чем их связать? Спросим себя, как связаны эти сущности между собой и с сущностями “Покупатель” и “Товар”? Покупатели покупают товары, получая при этом накладные, в которые внесены данные о количестве и цене купленного товара. Каждый покупатель может получить несколько накладных. Каждая накладная обязана выписываться на одного покупателя. Каждая накладная обязана содержать несколько товаров (не бывает пустых накладных). Каждый товар, в свою очередь, может быть продан нескольким покупателям через несколько накладных. Кроме того, каждая накладная должна быть выписана с определенного склада, и с любого склада может быть выписано много накладных. Таким образом, после уточнения, диаграмма будет выглядеть следующим образом:

На данном этапе можно считать, что мы определили сущности и связи между ними. У нас не осталось вопросов, относительно них. Пока не осталось. На следующей итерации необходимо уточнить атрибуты сущностей. Беседуя с сотрудниками фирмы, мы выяснили следующее:

  1. Каждый покупатель является юридическим лицом и имеет наименование, адрес, банковские реквизиты.
  2. Каждый товар имеет наименование, цену, а также характеризуется единицами измерения.
  3. Каждая накладная имеет уникальный номер, дату выписывания, список товаров с количествами и ценами, а также общую сумму накладной.
  4. Накладная выписывается с определенного склада и на определенного покупателя.
  5. Каждый склад имеет свое наименование.

Снова выпишем все существительные, которые будут потенциальными атрибутами, и проанализируем их:

  1. Юридическое лицо - термин риторический, мы не работаем с физическими лицами. Не обращаем внимания.
  2. Наименование покупателя - явная характеристика покупателя.
  3. Адрес - явная характеристика покупателя.
  4. Банковские реквизиты - явная характеристика покупателя.
  5. Наименование товара - явная характеристика товара.
  6. (?)Цена товара - похоже, что это характеристика товара. Отличается ли эта характеристика от цены в накладной?
  7. Единица измерения - явная характеристика товара.
  8. Номер накладной - явная уникальная характеристика накладной.
  9. Дата накладной - явная характеристика накладной.
  10. (?)Список товаров в накладной - список не может быть атрибутом. Вероятно, нужно выделить этот список в отдельную сущность.
  11. (?)Количество товара в накладной - это явная характеристика, но характеристика чего? Это характеристика не просто “товара”, а “товара в накладной”.
  12. (?)Цена товара в накладной - опять же это должна быть не просто характеристика товара, а характеристика товара в накладной. Но цена товара уже встречалась выше - это одно и то же?
  13. Сумма накладной - явная характеристика накладной. Эта характеристика не является независимой. Сумма накладной равна сумме стоимостей всех товаров, входящих в накладную.
  14. Наименование склада - явная характеристика склада.

В ходе дополнительной беседы с менеджером удалось прояснить различные понятия цен. Оказалось, что каждый товар имеет некоторую текущую цену. Эта цена, по которой товар продается в данный момент. Естественно, что эта цена может меняться со временем. Цена одного и того же товара в разных накладных, выписанных в разное время, может быть различной. Таким образом, имеется две цены - цена товара в накладной и текущая цена товара.

С возникающим понятием “Список товаров в накладной” все довольно ясно. Сущности “Накладная” и “Товар” связаны друг с другом отношением типа много-ко-многим. Такая связь, должна быть расщеплена на две связи типа один-ко-многим. Для этого требуется дополнительная сущность. Этой сущностью и будет сущность “Список товаров в накладной”. Связь ее с сущностями “Накладная” и “Товар” характеризуется следующими фразами - “каждая накладная обязана иметь несколько записей из списка товаров в накладной”, “каждая запись из списка товаров в накладной обязана включаться ровно в одну накладную”, “каждый товар может включаться в несколько записей из списка товаров в накладной”, “каждая запись из списка товаров в накладной обязана быть связана ровно с одним товаром”. Атрибуты “Количество товара в накладной” и “Цена товара в накладной” являются атрибутами сущности “Список товаров в накладной”.

Точно также поступим со связью, соединяющей сущности “Склад” и “Товар”. Введем дополнительную сущность “Товар на складе”. Атрибутом этой сущности будет “Количество товара на складе”. Таким образом, товар будет числиться на любом складе и количество его на каждом складе будет свое.

Теперь можно внести все это в диаграмму:

Мы учли все атрибуты, у нас не осталось связей “много-ко-многим”, а следовательно можно считать что на данном этапе ER-диаграмма разработана в соответствие с требованиями. В последующих версиях системы или при возникновении новых требований, эту диаграмму можно также дополнять или модифицировать в рамках подготовки новых версий разрабатываемой информационной системы.

Концептуальные и физические ER-модели

Разработанный выше пример ER-диаграммы является примером концептуальной диаграммы. Это означает, что диаграмма не учитывает особенности конкретной СУБД. По данной концептуальной диаграмме можно построить физическую диаграмму, которая уже будут учитываться такие особенности СУБД, как допустимые типы и наименования полей и таблиц, ограничения целостности и т.п. Физический вариант диаграммы может выглядеть, например, следующим образом:

На данной диаграмме каждая сущность представляет собой таблицу базы данных, каждый атрибут становится колонкой соответствующей таблицы. Обращаем внимание на то, что во многих таблицах, например, “CUST_DETAIL” и “PROD_IN_SKLAD”, соответствующих сущностям “Запись списка накладной” и “Товар на складе”, появились новые атрибуты, которых не было в концептуальной модели - это ключевые атрибуты родительских таблиц, мигрировавших в дочерние таблицы для того, чтобы обеспечить связь между таблицами посредством внешних ключей.

Контрольные вопросы

Спроектировать структуру БД “Деканат”.

Необходимо учесть информацию о:

  • Группах
  • Студентах
  • Преподавателях
  • Дисциплинах

Можно разделиться на две команды и в последующем презентовать свои варианты диаграмм. Команды могут задавать друг другу вопросы и указывать недостатки. Преподаватель выступает в роли арбитра и решает кто выиграл.

Теория нормальных форм

Нормальная форма – свойство отношения в реляционной модели данных, характеризующее его с точки зрения избыточности, потенциально приводящей к логически ошибочным результатам выборки или изменения данных. Нормальная форма определяется как совокупность требований, которым должно удовлетворять отношение.

Процесс преобразования отношений базы данных к виду, отвечающему нормальным формам, называется нормализацией. Нормализация предназначена для приведения структуры БД к виду, обеспечивающему минимальную логическую избыточность, и не имеет целью уменьшение или увеличение производительности работы или же уменьшение или увеличение физического объёма базы данных. Конечной целью нормализации является уменьшение потенциальной противоречивости хранимой в базе данных информации. Общее назначение процесса нормализации заключается в следующем:

  1. Исключение некоторых типов избыточности.
  2. Устранение некоторых аномалий обновления.
  3. Разработка проекта базы данных, который является достаточно «качественным» представлением реального мира, интуитивно понятен и может служить хорошей основой для последующего расширения.
  4. Упрощение процедуры применения необходимых ограничений целостности.

Устранение избыточности производится, как правило, за счёт декомпозиции отношений таким образом, чтобы в каждом отношении хранились только первичные факты (то есть факты, не выводимые из других хранимых фактов).

При том, что идеи нормализации весьма полезны для проектирования баз данных, они отнюдь не являются универсальным или исчерпывающим средством повышения качества проекта БД. Это связано с тем, что существует слишком большое разнообразие возможных ошибок и недостатков в структуре БД, которые нормализацией не устраняются. Несмотря на эти рассуждения, теория нормализации является очень ценным достижением реляционной теории и практики, поскольку она даёт научно строгие и обоснованные критерии качества проекта БД и формальные методы для усовершенствования этого качества. Этим теория нормализации резко выделяется на фоне чисто эмпирических подходов к проектированию, которые предлагаются в других моделях данных. Более того, можно утверждать, что во всей сфере информационных технологий практически отсутствуют методы оценки и улучшения проектных решений, сопоставимые с теорией нормализации реляционных баз данных по уровню формальной строгости.

Всего выделяют 6 нормальных форм, но мы рассмотрим только первые 3.

Первая нормальная форма (1НФ)

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

В реляционной модели отношение всегда находится в первой нормальной форме по определению понятия отношение.

Что же касается различных таблиц, то они могут не быть правильными представлениями отношений и, соответственно, могут не находиться в 1НФ. В соответствии с определением Кристофера Дейта для такого случая таблица нормализована (т.е. находится в первой нормальной форме) тогда и только тогда, когда она является прямым и верным представлением некоторого отношения. Конкретнее, рассматриваемая таблица должна удовлетворять следующим пяти условиям:

  1. Нет упорядочивания строк сверху вниз (другими словами, порядок строк не несет в себе никакой информации).
  2. Нет упорядочивания столбцов слева направо (другими словами, порядок столбцов не несет в себе никакой информации).
  3. Нет повторяющихся строк.
  4. Каждое пересечение строки и столбца содержит ровно одно значение из соответствующего домена (и больше ничего).
  5. Все столбцы являются обычными.

«Обычность» всех столбцов таблицы означает, что в таблице нет «скрытых» компонентов, которые могут быть доступны только в вызове некоторого специального оператора взамен ссылок на имена регулярных столбцов, или которые приводят к побочным эффектам для строк или таблиц при вызове стандартных операторов. Таким образом, например, строки не имеют идентификаторов кроме обычных значений потенциальных ключей (без скрытых «идентификаторов строк» или «идентификаторов объектов»). Они также не имеют скрытых временных меток.

Пример

Исходная ненормализованная (то есть не являющаяся правильным представлением некоторого отношения) таблица:

СотрудникНомер телефона
Иванов И.И.283-56-82
390-57-34
Петров П.П.708-62-34

Таблица, приведённая к 1НФ, являющаяся правильным представлением некоторого отношения:

СотрудникНомер телефона
Иванов И.И.283-56-82
Иванов И.И.390-57-34
Петров П.П.708-62-34

Атомарность

Многие авторы дополняют определение первой нормальной формы требованием атомарности (неделимости) значений. Однако концепция «атомарности» является слишком неясной. Например, многие типы данных (строки, даты, числа с фиксированной точкой и т. д.) при необходимости легко могут быть декомпозированы на составляющие элементы с помощью стандартных операций, предоставляемых СУБД. К. Дейт заключает, что «понятие атомарности не имеет абсолютно никакого смысла».

Исторически концепция «атомарности» берёт начало от «простых доменов» (англ. simple domains), предложенных автором реляционной модели данных Э. Ф. Коддом. Цель «нормальной формы», которую предложил Кодд в статье «Реляционная модель данных для больших совместно используемых банков данных», не была связана с каким-либо теоретическим аспектом, например, с борьбой с аномалиями или избыточностью. Кодд предложил использовать «простые домены» только для облегчения будущей программной реализации, а именно:

  1. Для облегчения хранения отношений в виде двумерных массивов. Отношение, все домены которого являются простыми, может быть представлено при хранении двухмерным массивом с однородными столбцами.
  2. Для облегчения передачи данных в гетерогенных системах. Простота представления отношений массивами, осуществимая в случае приведения всех отношений в нормальную форму, предоставляет преимущества не только при хранении, но также при передаче больших объёмов данных между системами, использующими во многом отличные представления данных.

Вторая нормальная форма (2НФ)

Отношение находится во второй нормальной форме тогда и только тогда, когда оно находится в первой нормальной форме и каждый неключевой атрибут функционально полно зависит от его потенциального ключа.

Полная функциональная зависимость означает, что каждый неключевой атрибут зависит от всего потенциального ключа, а не от его части.

Если потенциальный ключ является простым, то есть состоит из единственного атрибута, то любая функциональная зависимость от него является полной. Если потенциальный ключ является составным, то, согласно определению второй нормальной формы, в отношении не должно быть неключевых атрибутов, зависящих от части составного потенциального ключа.

Вторая нормальная форма по определению запрещает наличие неключевых атрибутов, которые вообще не зависят от потенциального ключа. Таким образом, вторая нормальная форма в том числе запрещает создавать отношения как несвязанные (случайные) наборы атрибутов.

Пример

Пусть в следующем отношении первичный ключ образует пара атрибутов {Филиал, Должность}:

ФилиалДолжностьЗарплатаКомпьютер
МуромУборщик20000Нет
КазаньПрограммист40000Есть
МуромПрограммист75000Есть

Допустим, что зарплата зависит от филиала и должности, а наличие компьютера зависит только от должности.

Существует функциональная зависимость Должность → Наличие компьютера, в которой левая часть (детерминант) является лишь частью первичного ключа, что нарушает условие второй нормальной формы.

Для приведения к 2NF исходное отношение следует декомпозировать на два отношения:

ФилиалДолжностьЗарплата
МуромУборщик20000
КазаньПрограммист40000
МуромПрограммист75000
ДолжностьКомпьютер
УборщикНет
ПрограммистЕсть

Третья нормальная форма (3НФ)

Отношение находится в третьей нормальной форме тогда и только тогда, когда оно находится во второй нормальной форме и ни один неключевой атрибут не зависит от другого неключевого атрибута.

Пример

Рассмотрим в качестве примера отношение:

СотрудникОтделТелефон
ИвановБухгалтерия3-27-58
ПетровБухгалтерия3-27-58
СидоровСнабжение2-28-42

Каждый сотрудник относится исключительно к одному отделу; каждый отдел имеет единственный телефон. Атрибут Сотрудник является первичным ключом. Личных телефонов у сотрудников нет, и телефон сотрудника зависит исключительно от отдела.

В примере:

  • Отдел зависит от Сотрудника.
  • Телефон зависит от Отдела.
  • Телефон зависит от Сотрудника (транзитивно).

Отношение находится во второй нормальной форме, поскольку каждый атрибут функционально полно зависит от потенциального ключа Сотрудник.

Телефон зависит от неключевого атрибута Отдел следовательно, отношение не находится в третьей нормальной форме.

Кроме этого, очевидно, что присутствует избыточность, т.к. телефоны повторяются для всех сотрудников одного отдела.

Для приведения отношения к третьей нормальной форме, необходимо разделить его на два:

ОтделТелефон
Бухгалтерия3-27-58
Снабжение2-28-42
СотрудникОтдел
ИвановБухгалтерия
ПетровБухгалтерия
СидоровСнабжение

Упражнение

Рассмотрим упражнение на нормальные формы, чтобы закрепить понимание. Мы разберем пример таблицы, выявим нарушения нормальных форм и приведем ее к каждой из них.

Исходная таблица таблица Orders (Заказы):

OrderIDCustomerIDCustomerNameProductIDProductNameQuantityPrice
1101Иванов201Ноутбук21200
1101Иванов202Мышь320
2102Кузнецов201Ноутбук11200
3101Иванов203Клавиатура150

В таблице Orders все атрибуты содержат только одно значение. Следовательно, таблица уже находится в первой нормальной форме.

Чтобы быть во второй нормальной форме, все неключевые атрибуты должны полностью зависеть от всего потенциального ключа.

  • Потенциальный ключ: (OrderID, ProductID) (составной ключ).
  • Неключевые атрибуты: CustomerID, CustomerName, ProductName, Quantity, Price.

При этом есть проблемы:

  • CustomerID и CustomerName зависят только от OrderID, а не от всего ключа (OrderID, ProductID).
  • ProductName и Price зависят только от ProductID, а не от всего ключа.

Кроме этого, CustomerName и ProductName дублируются для каждого заказа.

Для приведения отношения ко второй нормальной форме декомпозируем таблицу на три отношения:

  1. Orders (Заказы):

    OrderIDCustomerIDCustomerName
    1101Иванов
    2102Кузнецов
    3101Иванов
  2. OrderDetails (Детали заказов):

    OrderIDProductIDQuantity
    12012
    12023
    22011
    32031
  3. Products (Товары):

    ProductIDProductNamePrice
    201Ноутбук1200
    202Мышь20
    203Клавиатура50

Чтобы быть в третьей нормальной форме неключевые атрибуты не должны зависеть от других неключевых атрибутов. Однако, в таблице Orders CustomerID зависит от OrderID, а CustomerName зависит от CustomerID.

Для устранения этого, декомпозируем таблицу Orders:

  1. Orders (Заказы):

    OrderIDCustomerID
    1101
    2102
    3101
  2. Customers (Клиенты):

    CustomerIDCustomerName
    101Иванов
    102Кузнецов

Таким образом, в результате нормализации мы получаем базу данных из 4-х таблиц, находящихся в 3-й нормальной форме.

  1. Orders (Заказы):

    OrderIDCustomerID
    1101
    2102
    3101
  2. Customers (Клиенты):

    CustomerIDCustomerName
    101Иванов
    102Кузнецов
  3. OrderDetails (Детали заказов):

    OrderIDProductIDQuantity
    12012
    12023
    22011
    32031
  4. Products (Товары):

    ProductIDProductNamePrice
    201Ноутбук1200
    202Мышь20
    203Клавиатура50

Контрольные вопросы

  1. Для чего используется нормализация?
  2. Приведите пример нарушения первой нормальной формы?
  3. Приведите пример нарушения второй нормальной формы?
  4. Что такое полная функциональная зависимость?
  5. Приведите пример нарушения третьей нормальной формы?

Язык SQL

Введение в язык SQL

SQL (Structured Query Language) — это язык запросов, используемый для работы с реляционными базами данных. Он позволяет создавать, изменять, извлекать и управлять данными в базах данных. Все операторы языка SQL можно разделить на две большие группы:

  • Операторы DDL (Data Definition Language) - язык определения данных. Позволяет создавать, изменять и удалять объекты БД, такие как таблицы, домены, хранимые представления и т.п.

  • Операторы DML (Data Manipulation Language) - язык манипулирования данными. Позволяет добавлять, модифицировать и удалять данные в объектах БД. Одним из самых сложных операторов DML является оператор выборки данных. С его помощью можно строить самые разнообразные запросы и производить вычисления.

Язык SQL является декларативным языком. С его помощью описывается результат, который необходимо получить, а процессор запросов и оптимизатор определяют оптимальный способ получить такой результат.

Для получения общего представления о языке и дальнейшего более глубокого его изучения, давайте рассмотрим простейшие примеры самых распространенных операторов.

Основные команды DDL

  • CREATE TABLE — создание новой таблицы.

    CREATE TABLE table_name (
        column1 datatype,
        column2 datatype,
        ...
    );
    

    Пример:

    CREATE TABLE users (
        id INT PRIMARY KEY,
        name VARCHAR(50),
        age INT
    );
    
  • ALTER TABLE — изменение структуры таблицы (добавление, удаление или изменение столбцов).

    ALTER TABLE table_name ADD column_name datatype;
    

    Пример:

    ALTER TABLE users ADD email VARCHAR(100);
    
  • DROP TABLE — удаление таблицы.

    DROP TABLE table_name;
    

    Пример:

    DROP TABLE users;
    

Пример базы данных

CREATE TABLE users (
    id INT NOT NULL PRIMARY KEY,
    name VARCHAR(50),
    age INT
);

-- Мужские имена
INSERT INTO users (id, name, age) VALUES (1, 'Александр', 28);
INSERT INTO users (id, name, age) VALUES (2, 'Дмитрий', 34);
INSERT INTO users (id, name, age) VALUES (3, 'Иван', 22);
INSERT INTO users (id, name, age) VALUES (4, 'Сергей', 30);

-- Женские имена
INSERT INTO users (id, name, age) VALUES (5, 'Анна', 25);
INSERT INTO users (id, name, age) VALUES (6, 'Екатерина', 29);
INSERT INTO users (id, name, age) VALUES (7, 'Ольга', 31);
INSERT INTO users (id, name, age) VALUES (8, 'Мария', 19);

В результате выполнения этих команд, таблица users будет содержать следующие данные.

idnameage
1Александр28
2Дмитрий34
3Иван22
4Сергей30
5Анна25
6Екатерина29
7Ольга31
8Мария19

Основные команды DML

  • SELECT — извлечение данных из таблицы.

    SELECT column1, column2 FROM table_name;
    

    Пример:

    SELECT name, age FROM users;
    

    вернет

    nameage
    Александр28
    Дмитрий34
    Иван22
    Сергей30
    Анна25
    Екатерина29
    Ольга31
    Мария19
  • INSERT INTO — добавление новых данных в таблицу.

    INSERT INTO table_name (column1, column2) VALUES (value1, value2);
    

    Пример:

    INSERT INTO users (id, name, age) VALUES (9, 'Алиса', 25);
    
    idnameage
    1Александр28
    2Дмитрий34
    3Иван22
    4Сергей30
    5Анна25
    6Екатерина29
    7Ольга31
    8Мария19
    9Алиса25
  • UPDATE — обновление существующих данных.

    UPDATE table_name SET column1 = value1 WHERE condition;
    

    Пример:

    UPDATE users SET age = 26 WHERE name = 'Алиса';
    
    idnameage
    1Александр28
    2Дмитрий34
    3Иван22
    4Сергей30
    5Анна25
    6Екатерина29
    7Ольга31
    8Мария19
    9Алиса26
  • DELETE — удаление данных из таблицы.

    DELETE FROM table_name WHERE condition;
    

    Пример:

    DELETE FROM users WHERE name = 'Алиса';
    
    idnameage
    1Александр28
    2Дмитрий34
    3Иван22
    4Сергей30
    5Анна25
    6Екатерина29
    7Ольга31
    8Мария19

Фильтрация данных

  • WHERE — фильтрация строк по условию.

    SELECT * FROM users WHERE age > 25;
    
    idnameage
    1Александр28
    2Дмитрий34
    4Сергей30
    6Екатерина29
    7Ольга31
  • AND, OR, NOT — логические операторы для комбинирования условий.

    SELECT * FROM users WHERE age > 20 AND name = 'Алиса';
    
    idnameage
    7Ольга31
  • LIKE — поиск по шаблону.

    SELECT * FROM users WHERE name LIKE 'А%';  -- Начинается с "А"
    
    idnameage
    1Александр28
    5Анна25
  • IN — проверка наличия значения в списке.

    SELECT * FROM users WHERE age IN (20, 25, 30);
    
    idnameage
    4Сергей30
    5Анна25
  • BETWEEN — выбор значений в диапазоне.

    SELECT * FROM users WHERE age BETWEEN 20 AND 30;
    
    idnameage
    1Александр28
    3Иван22
    4Сергей30
    5Анна25
    6Екатерина29

Сортировка и группировка

  • ORDER BY — сортировка результатов.

    SELECT * FROM users ORDER BY age DESC;  -- По убыванию
    
    idnameage
    2Дмитрий34
    7Ольга31
    4Сергей30
    6Екатерина29
    1Александр28
    9Алиса25
    5Анна25
    3Иван22
    8Мария19
  • GROUP BY — группировка данных.

    SELECT age, COUNT(*) FROM users GROUP BY age;
    

    Для таблицы

    idnameage
    2Дмитрий34
    7Ольга34
    4Сергей29
    6Екатерина29
    1Александр29
    9Алиса25
    5Анна25
    3Иван22
    8Мария19

    запрос вернет

    agecount
    342
    293
    252
    221
    191
  • HAVING — фильтрация результатов группировки.

    SELECT age, COUNT(*) FROM users GROUP BY age HAVING COUNT(*) > 1;
    

    Для таблицы

    idnameage
    2Дмитрий34
    7Ольга34
    4Сергей29
    6Екатерина29
    1Александр29
    9Алиса25
    5Анна25
    3Иван22
    8Мария19

    запрос вернет

    agecount
    342
    293
    252

Агрегатные функции

  • COUNT() — подсчет строк.

    SELECT COUNT(*) FROM users;
    

    Для таблицы

    idnameage
    2Дмитрий34
    7Ольга34
    4Сергей29
    6Екатерина29
    1Александр29
    9Алиса25
    5Анна25
    3Иван22
    8Мария19

    запрос вернет

    count
    9
  • SUM() — сумма значений.

    SELECT SUM(age) FROM users;
    

    Для таблицы

    idnameage
    2Дмитрий34
    7Ольга34
    4Сергей29
    6Екатерина29
    1Александр29
    9Алиса25
    5Анна25
    3Иван22
    8Мария19

    запрос вернет

    count
    246
  • AVG() — среднее значение.

    SELECT AVG(age) FROM users;
    

    Для таблицы

    idnameage
    2Дмитрий34
    7Ольга34
    4Сергей29
    6Екатерина29
    1Александр29
    9Алиса25
    5Анна25
    3Иван22
    8Мария19

    запрос вернет

    count
    27,33
  • MIN() и MAX() — минимальное и максимальное значение.

    SELECT MIN(age), MAX(age) FROM users;
    

    Для таблицы

    idnameage
    2Дмитрий34
    7Ольга34
    4Сергей29
    6Екатерина29
    1Александр29
    9Алиса25
    5Анна25
    3Иван22
    8Мария19

    запрос вернет

    minmax
    1934

Контрольные вопросы

  1. Для чего предназначены операторы DDL?
  2. Для чего предназначены операторы DML?
  3. Назовите основные команды DDL и приведите примеры?
  4. Назовите основные команды DML и приведите примеры?
  5. Приведите примеры запросов для фильтрации данных?
  6. Приведите примеры агрегирующих операторов?

Типы данных

Тип данных определяет множество значений, которые может содержать столбец таблицы, переменная или параметр хранимой процедуры или триггера. Тип данных определяет также допустимые для соответствующего столбца (переменной, параметра) операции.

Рассмотрим стандартные типы данных языка SQL.

Числовые типы данных

В SQL существует достаточно большое количество числовых типов данных. В эту категорию типов данных входят числа с фиксированной точкой (целочисленные и дробные) и числа с плавающей точкой.

Целочисленные

Позволяют хранить целые числа по аналогии с типизированными языками программирования.

SMALLINT - 2 байта. Целые числа от –32 768 до +32 767

INTEGER - 4 байта. Целые числа от −231 до +231−1

BIGINT - 8 байт. Целые числа от −263 до +263−1

INT128 - 16 байт. Целые числа от -2127 до 2127−1

Числа с фиксированной точкой

Тип данных DECIMAL (или NUMERIC) в SQL используется для хранения точных числовых значений с фиксированной точностью и масштабом. Он особенно полезен для работы с финансовыми данными, где важна точность вычислений (например, деньги, проценты, курсы валют).

DECIMAL(n, m) - 2, 4, 8 или 16 байт.

  • n (от 1 до 38) - общее количество знаков, которые могут быть сохранены, включая дробные.
  • m (от 0 до 38) - количество знаков после десятичной точки

Например, для числа 123.45 точность равна 5, а масштаб 2.

NUMERIC(n, m) - аналогичен DECIMAL.

Размер памяти, используемый для физического хранения значений такого типа,зависит от точности. Например, в РЕД База Данных:

ТочностьРазмер
1 - 42 байта
5 - 94 байта
10 - 188 байт
19 - 3816 байт

Числа с плавающей точкой

FLOAT - 4 байта. Числа от 1.175×10−38 до 3.402×1038

DOUBLE PRECISION - 8 байт. Числа от 2.225×10−308 до 1.179×10308

DECFLOAT является типом данных из стандарта SQL:2016, который точно хранит числа с плавающей запятой, в отличие от FLOAT или DOUBLE PRECISION, которые обеспечивают двоичное приближение предполагаемой точности. РЕД База Данных в соответствии со стандартом IEEE 754-2008 реализует типы DECIMAL64 и DECIMAL128, что обеспечивает точность DECFLOAT 16 и 34 значащих цифр, и занимает 8 и 16 байт памяти соответственно.

Если точность не указана, то по умолчанию используется точность 34 значащих цифры.

Все промежуточные вычисления осуществляются с использованием 34-значными значениями.

Операции для числовых типов данных

Для числовых типов данных определены четыре арифметические операции – сложение, вычитание, умножение и деление.

Операции сложения и вычитания для всех числовых типов данных выполняются обычным образом.

При выполнении операций умножения и деления чисел с фиксированной точкой (SMALLINT, INTEGER, BIGINT, DECIMAL и NUMERIC) результат будет иметь количество дробных знаков, равное сумме дробных знаков обоих операндов.

Например, результатом деления 1/3 будет 0, потому что сумма дробных знаков операндов равна нулю, а целая часть в результате выполнения операции деления будет равна нулю. Результат является целочисленным. Чтобы получить значение с заданной точностью, необходимо у одного или у обоих операндов явно указать нужное количество нулевых дробных знаков.

Например, операция 1.00 / 3 вернет уже более верное число – 0.33. Тот же результат можно получить, если записать операцию деления в следующем виде: 1.0 / 3.0

Для чисел с плавающей точкой все операции выполняются таким же образом, как принято во всех языках программирования.

Строковые типы данных

К строковым (символьным) типам данных относятся следующие типы данных, определенные в SQL:

CHAR(n), CHARACTER(n) – символьный тип данных фиксированной длины. Число n задает максимальное количество символов. Конечные пробелы в базе данных не хранятся, а восстанавливаются до указанного размера при отображении такого столбца. Максимальный размер столбца 32767 байтов. Если количество символов n не указано, принимается 1.

VARCHAR(n), VARYING CHARACTER (n) – символьный тип данных переменной длины. Максимальный размер 32765 байтов. Если количество символов n не указано, то предполагается 1024.

NCHAR(n), NATIONAL CHARACTER (n), NCHAR VARYING (n), NATIONAL CHARACTER VARYING (n) – символьные типы данных длины. Отличается от типов данных CHARACTER и VARYING CHARACTER только тем, что для них предопределен набор символов ISO8859_1 (кодировка, предназначенная для западноевропейских языков). Другие наборы символов для этих типов данных задавать нельзя, поэтому в России они применяются редко.

BINARY(n) является типом данных с фиксированной длиной для хранения двоичных данных. Если переданное количество байт меньше объявленной длины n, то значение будет дополнено нулями. В случае если не указана длина, то считается, что она равна единице.

Этот тип является псевдонимом типа

CHAR [(<длина>)] CHARACTER SET OCTETS

VARBINARY (n), BINARY VARYING (n) – является типом для хранения двоичных данных переменной длины n. Реальный размер хранимой структуры равен фактическому размеру данных плюс 2 байта, в которых задана длина поля.

Этот тип является псевдонимом типа

VARCHAR (<длина>) CHARACTER SET OCTETS

Строковые константы заключаются в апострофы. Если в строковой константе присутствует символ апостроф, то он должен быть представлен двумя подряд идущими апострофами.

Для строковых типов данных определена только одна операция конкатенации – соединения двух строк в одну. Для обозначения этой операции применяются два подряд идущих символа вертикальной черты ||.

Результатом этой операции является строка, которая представляет собой соединение двух операндов. Операция всегда возвращает тип данных CHAR (а не VARCHAR), независимо от того, какой именно строковый тип данных имеют исходные строки. Это означает, что в результате конкатенации сохраняются конечные пробелы. Размер (количество символов) результирующей строки равен сумме размеров исходных строк конкатенации. Например, можно записать следующую операцию:

’Руководство ’ || ’по SQL ’

Результатом будет одна строка: «Руководство по SQL».

Для строковых и ряда других типов данных применимо множество встроенных функций SQL.

Логический тип данных

РЕД База Данных предоставляет стандартный SQL тип BOOLEAN, у которого может быть 3 значения:

  • TRUE (истина)
  • FALSE (ложь)
  • UNKNOWN (неизвестно) представляется SQL значением NULL.

Спецификация не делает различия между значением NULL этого типа и значением истинности UNKNOWN, которое является результатом SQL предиката, поискового условия или выражения логического типа. Эти значения взаимозаменяемы и обозначают одно и то же.

Значения типа BOOLEAN могут быть проверены в неявных значениях истинности. Например, field1 OR field2 или NOT field1 являются допустимыми выражениями.

Предикаты могут использовать оператор IS [NOT] для проверки соответствия. Например, field1 IS FALSE или field1 IS NOT TRUE.

Тип данных BOOLEAN не преобразуется неявно ни к одному типу, но возможно явное преобразование к строке с помощью функции CAST.

Покажем значения выражений на примере.

ВыражениеЗначение
1 <> 4TRUE
1 == 4FALSE
FALSEFALSE
NULL - 1NULL
NULL == 1NULL
NULL == NULLNULL

Тип данных даты и времени

Существует три типа данных для представления даты и времени – DATE, TIME и TIMESTAMP, позволяющие хранить, соответственно, дату, время и объединение даты и времени.

Тип данных DATE

Этот тип данных позволяет хранить только дату в диапазоне от 1 января 1 года до 31 декабря 9999 года.

Для литералов, представляющих дату, в SQL существует много форматов. При описании синтаксиса для формата типа DATE для указания номера дня в месяце используются символы «dd» (число от 1 до 31), для месяца в году — «mm» (число от 1 до 12), для номера года — «yyyy» (число от 1 до 9999). Для номера дня и номера месяца ведущий ноль можно не указывать. Год может быть задан и числом с меньшей, чем четыре, значимостью. Вот основные форматы даты, используемые в РЕД Базе Данных:

dd.mm.yyyy 
mm-dd-yyyy 
mm/dd/yyyy 
yyyy-mm-dd 
yyyy/mm/dd 
yyyy.mm.dd 
dd-MON-yyyy

MON – трехсимвольное сокращенное название месяца (английское). Может принимать значения (в любом регистре): jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec – месяцы с января по декабрь.

Например, дату 14 июля 2025 года можно записать в следующем виде: '14.07.2025', '14-JUL-2025', '07-12-2025', '07/12/2025' и т.д.

Тип данных TIME

Позволяет хранить время дня в диапазоне от 00:00:00.0000 до 23:59:59.9999. По умолчанию тип TIME не содержит информацию о часовом поясе. Для того чтобы тип TIME включал информацию о часовом поясе необходимо использовать его с модификатором WITH TIME ZONE.

   TIME [{WITH | WITHOUT} TIME ZONE]

При преобразовании из/в TIME WITH TIME ZONE учитывайте, что тип TIME WITHOUT TIME ZONE определен для использования в часовом поясе сеанса.

Часовой пояс сеанса как следует из названия может быть разным для каждого соединения с базой данных. Если не установлен, то часовой пояс сеанса будет тем же, что используется операционной системой, в которой запущен процесс СУБД.

Часовой пояс сеанса может быть изменён с помощью оператора

SET TIME ZONE

или сброшен в исходное значение с помощью

SET TIME ZONE LOCAL

Часовой пояс может быть задан строкой с регионом часового пояса (например, America/ Sao_Paulo), или в виде смещения часов:минут относительно GMT (например, -03:00). Список региональных часовых поясов и их идентификаторов можно найти в документации к СУБД РЕД База Данных.

TIME WITH TIME ZONE хранится так же как TIME WITHOUT TIME ZONE плюс 2 байта для идентификации часового пояса или смещения. TIME часть хранится в UTC (и переводится в сохранённый часовой пояс).

Тип данных TIMESTAMP

Тип данных дата и время (TIMESTAMP) представляет собой соединение даты и времени, которые в литералах просто разделяются любым количеством пробелов.

Например, для задания 12 часов 30 минут 14 июня 2025 года можно записать: '14.07.2025 12:30'

Для того чтобы тип TIMESTAMP включал информацию о часовом поясе необходимо использовать его с модификатором WITH TIME ZONE.

   TIMESTAMP [{WITH | WITHOUT} TIME ZONE]

В РЕД База Данных можно получить текущие дату и время с помощью встроенных функций:

NOW - текущая дата и время в момент обращения к функции.

CURRENT_TIMESTAMP типа TIMESTAMP WITH TIME ZONE – метка времени старта запроса с временной зоной. Не меняется в процессе выполнения запроса.

LOCALTIMESTAMP типа TIMESTAMP WITHOUT TIME ZONE – метка времени старта запроса без временной зоны. Не меняется в процессе выполнения запроса.

CURRENT_DATE типа DATE – текущая дата.

Арифметические операции для типов данных даты и времени

Для типов данных даты (DATE) и времени (TIME) определены операции сложения и вычитания.

Операция сложения для типа DATE и целого числа дает дату, увеличенную на заданное количество дней. Вычитание из типа данных DATE целого числа возвращает дату, уменьшенную на указанное количество дней. В операции можно указать и дробное число. К ошибке это не приводит, происходит правильное округление числа до ближайшего целого.

Например, следующая операция дает завтрашнюю дату: CURRENT_DATE + 1 Чтобы получить вчерашнюю дату, нужно записать: CURRENT_DATE - 1

Вычитание двух дат дает количество дней в интервале. Например, чтобы узнать, сколько дней осталось до Нового Года 2030, нужно записать: CAST('31.12.2029' AS DATE) - CURRENT_DATE

Сложение «TIME + число» дает указанное время, увеличенное на заданное число секунд, включая десятитысячные доли секунды. Здесь в операции можно использовать дробное число.

Соответственно, вычитание «TIME − число» дает время, уменьшенное на заданное число секунд, включая десятитысячные доли секунды.

Вычитание двух переменных типа TIME дает интервал времени в секундах (включая десятитысячные доли секунды).

Для типа данных TIMESTAMP ни одна из перечисленных операций недопустима.

Тип данных BLOB

Тип данных BLOB называется большим двоичным объектом (Binary Large OBject). Этот тип данных позволяет хранить любые большие по объему данные – форматированные тексты, графику, звуки, видео.

Синтаксис объявления типа BLOB

    BLOB [SUB_TYPE <имя подтипа>]
        [SEGMENT SIZE <размер сегмента>] 
        [CHARACTER SET <набор символов>]

Поля BLOB не хранятся непосредственно в самой записи вместе с другими данными строки. Запись содержит только ссылку (идентификатор, указатель) на страницу базы данных, где располагаются данные BLOB. Тип данных BLOB характеризуется размером сегмента, в котором размещаются данные. Размер сегмента задает в байтах размер полей в базе данных, которые будут использованы для хранения данных типа BLOB. По умолчанию принимается 80.

Максимальный размер сегмента не может превышать 64Кб – 1, то есть числа 65535. За одно обращение к базе данных система всегда считывает один сегмент. Если в поле BLOB хранятся данные, занимающие менее 32765 байтов, то хранение и работа с этим полем осуществляется так же, как и с полем, имеющим тип данных VARCHAR.

Объем данных, которые могут храниться в этом типе данных, зависит от размера страницы базы данных:

Размер страницыМакс. размер BLOB
4K4ГБ
8K32ГБ
16K256ГБ
32K2TB

При объявлении столбца или домена типа BLOB можно указать его подтип (предложение SUB_TYPE), а также размер сегмента, используемый при хранении данных (предложение SEGMENT SIZE). Значение подтипа может быть целым числом в диапазоне от –32768 до +32767.

Широко используемыми подтипами являются:

  • 0 (BINARY) - неструктурированные двоичные данные.
  • 1 (TEXT) - текстовые данные. К полями такого типа может применяться большинство строковых функций.

Кроме этих двух в РЕД База Данных существует еще семь (от 2 до 8 включительно) заранее предопределенных подтипов. Их не следует использовать для каких-то своих внутренних целей.

Подтипы могут использоваться в случае, когда в базе данных описаны стандартные BLOB-фильтры. Фильтры – это программы, которые выполняют преобразования между данными BLOB разных подтипов на серверной и клиентской стороне. Такие преобразования связаны, как правило, с упаковкой и, соответственно, распаковкой данных.

Контрольные вопросы

  1. Дайте определение типа данных?
  2. Назовите основные целочисленные типы данных?
  3. В чем отличие чисел с фиксированной точкой от чисел с плавающей точкой?
  4. Какие типы данных могут применяться для хранения чисел с плавающей точкой?
  5. В чем особенность операций умножения и деления для чисел с фиксированной точкой?
  6. В чем отличие символьных типов данных с фиксированной и переменной длиной?
  7. Какие операции определены для символьных типов данных?
  8. Какие значения могут быть у логического типа данных?
  9. В каком диапазоне может хранить даты переменная типа DATE?
  10. Как сравнить два значения времени в разных часовых поясах?
  11. Что получится если сложить две переменные типа DATE?
  12. Что получится если из одной переменной типа TIME вычесть значение другой?
  13. Для чего предназначен типа данных BLOB?
  14. Какие подтипы BLOB известны?

Домены

Создание домена

Для создания нового домена в базе данных используется оператор CREATE DOMAIN. Синтаксис оператора:

    CREATE DOMAIN <имя домена> [AS] <тип данных>
        [DEFAULT {<литерал> | NULL | <контекстная переменная>}]
        [NOT NULL]
        [CHECK (<условие домена>)]
        [COLLATE <порядок сортировки>];

Предложение DEFAULT задает значение по умолчанию – что должно быть помещено в столбец таблицы, основанный на этом домене, если пользователь в операторе INSERT, добавляющем данные в таблицу, не укажет значение для этого столбца. Значение по умолчанию не используется при выполнении оператора изменения данных в таблице UPDATE. Если в этом операторе пользователь не укажет значение для конкретного столбца, то это значение просто не изменяется.

Значением по умолчанию может быть литерал или пустое значение NULL. Литералом может быть любая самоопределенная константа соответствующего типа, предварительно определенный литерал или контекстная переменная. Если значение по умолчанию не устанавливается, то подразумевается пустое значение NULL. В значении по умолчанию нельзя задавать выражения.

Предложение NOT NULL указывает, что столбцу, основанному на этом домене, не может присваиваться пустое значение ни в операторе INSERT, ни в операторе UPDATE. Это предложение является обязательным, если домен будет использован для создания столбца, входящего в состав первичного ключа таблицы.

Предложение CHECK, являясь ограничением домена, задает некоторое условие, которому должно удовлетворять значение (VALUE), помещаемое поле, основанные на этом домене.

Условие в предложении CHECK также иногда называется предикатом. Это логическое выражение, которое может возвращать значения TRUE (истина), FALSE (ложь) и UNKNOWN (неопределенное, неизвестное значение). Условие считается выполненным, если этот предикат возвращает значение TRUE.

Условие домена может быть достаточно сложным. Полный его синтаксис подробно описан в документации к СУБД. Рассмотрим примеры:

CHECK (VALUE >= 18)

Значениям такого домена можно присваивать только числа, которые не меньше 18.

CHECK (SUBSTRING(VALUE FROM 1 FOR 1) = SUBSTRING(VALUE FROM 2 FOR 1))

Данное ограничение требует чтобы первые две буквы строкового значения домена были равны.

CHECK ((VALUE IS NULL) OR (VALUE BETWEEN '0' AND '9'))

Такое ограничение разрешает использовать или только символьные представления цифр или значение NULL.

CHECK (VALUE IN ('М', 'Ж'))

В таком домене можно хранить пол человека.

CREATE DOMAIN D_SELECT AS CHAR(3) 
    CHECK(VALUE IN (SELECT CODCOUNTRY FROM COUNTRY))

В данном ограничении демонстрируется использование подзапроса. Присваимое значение обязано присутствовать среди значений поля CODCOUNTRY таблицы COUNTRY. Обратите внимание, это не полноценная поддержка ссылочной целостности, потому что требуется еще обработка случаев удаления или изменения записей таблицы COUNTRY.

CHECK (UPPER(VALUE) LIKE '%МИР%')

Такое ограничение требует наличия слова “МИР” в присваиваемом значении, причем регистр не имеет значения.

CHECK (VALUE > ALL (SELECT BIRTHDAY FROM PEOPLE))

Данное ограничение будет проверят что каждое новое значение домена больше каждого значения поля BIRTHDAY таблицы PEOPLE.

CHECK (VALUE = ANY (SELECT CODCOUNTRY FROM COUNTRY))

Данное ограничение проверит что присваиваемое значение домена совпадает хотя бы с одним значением поля CODCOUNTRY таблицы COUNTRY.

CHECK (EXISTS (SELECT CODCOUNTRY FROM COUNTRY WHERE CODCOUNTRY = VALUE))

В данном примере обратите внимание на использование ключевого слова VALUE внутри запроса в условии CHECK. Ограничение требует наличия записей в таблице COUNTRY где значение поля CODCOUNTRY равен присваиваемому значению.

CHECK (SINGULAR(SELECT CODCOUNTRY FROM COUNTRY WHERE CODCOUNTRY = VALUE))

Это условие практически аналогично предыдущему, но требует чтобы такая строка была ровно одна.

Как видно из примеров, одну и ту же задачу можно решить с помощью разных синтаксических конструкций и выражений.

Изменение домена

Для изменения характеристик существующего в базе данных домена используется оператор ALTER DOMAIN. Синтаксис оператора:

ALTER DOMAIN <имя>
    [TO <новое имя>]
    [{SET DEFAULT {<литерал> | NULL | <контекстная переменная>}
    | DROP DEFAULT}]
    [{SET | DROP} NOT NULL]
    [{ADD [CONSTRAINT] CHECK (<условие домена>)
    | DROP CONSTRAINT}]
    [TYPE <тип данных>];

Предложение SET DEFAULT позволяет установить новое значение по умолчанию.

Предложение DROP DEFAULT удаляет существующее значение по умолчанию. Значением по умолчанию в этом случае неявно становится пустое значение.

Предложение ADD [CONSTRAINT] CHECK добавляет условие домена. Если у домена уже существует условие, то вначале его нужно удалить при помощи предложения DROP CONSTRAINT иначе будет ошибка.

Предложение DROP CONSTRAINT удаляет существующее ограничение CHECK домена. Если домен не содержит ограничения с таким именем, то выполнение подобного оператора не вызовет сообщения об ошибке.

Можно также поменять тип данных домена при помощи предложения TYPE. Если существуют таблицы, содержащие значения изменяемого домена, то они будут или преобразованы в новый тип данных, или будет выдана ошибка невозможности такого преобразования.

Предложение SET NOT NULL устанавливает ограничение NOT NULL для домена. Если в таблице уже есть данные, содержащие NULL, будет выдана ошибка. В этом случае для переменных и столбцах базирующихся на домене значение NULL не допускается. Предложение DROP NOT NULL удаляет ограничение NOT NULL для домена.

Например,

    ALTER DOMAIN D099
        DROP DEFAULT
        SET DEFAULT CURRENT_USER
        DROP CONSTRAINT
        ADD CONSTRAINT
        CHECK (SUBSTRING(UPPER(VALUE) FROM 1 FOR 1) = 
                SUBSTRING(UPPER(VALUE) FROM 2 FOR 1));

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

Удаление домена

Для удаления домена используется оператор DROP DOMAIN. Синтаксис оператора удаления домена:

    DROP DOMAIN <имя домена>;

Нельзя удалить домен, на который ссылаются столбцы существующих таблиц базы данных или внутренние переменные хранимых процедур и триггеров. Предварительно нужно удалить все столбцы и переменные, ссылающиеся на этот домен. Удаление доменов для заполненной данными базы данных не является хорошей практикой.

Например, чтобы удалить домен CODCOUNTRY, нужно выполнить оператор:

    DROP DOMAIN CODCOUNTRY;

Контрольные вопросы

  1. В каких случаях применяется значение по умолчанию?
  2. Как можно ограничить возможные значения домена?
  3. Что произойдет, если добавить домену ограничение NOT NULL для таблицы, в которой уже содержатся значения NULL?
  4. Приведите пример удаления домена?

Таблицы

Таблица – наиболее важный и сложный объект реляционной базы данных. В таблицах хранятся все обрабатываемые клиентскими программами данные базы данных. Строки одной таблицы имеют одинаковую структуру и их количество не ограничено. Таблица должна содержать не менее одного столбца. Пользовательские данные хранятся в таблицах, создаваемых пользователем при помощи оператора CREATE TABLE и изменяемых оператором ALTER TABLE. Системные данные (метаданные, описывающие объекты базы данных) хранятся в системных таблицах, которые создаются автоматически при первоначальном создании базы данных.

Создание таблиц

Синтаксис оператора создания таблицы:

CREATE TABLE <имя таблицы>
    (<определение столбца> [, <определение столбца>, ...]
    [<ограничение таблицы> ...]);

Имя таблицы должно быть уникальным среди имен таблиц базы данных, хранимых процедур и представлений, описанных в этой базе данных.

Определение обычного столбца таблицы

Синтаксис описания столбца таблицы:

<определение обычного столбца> ::=
    <имя столбца> { <тип данных> | <имя домена>}
    [DEFAULT {<литерал> | NULL | <контекстная переменная>}]
    [NOT NULL]
    [<ограничение столбца>]

Столбец должен иметь имя, уникальное только в данной таблице. В других таблицах могут присутствовать столбцы с тем же именем.

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

Если при создании столбца таблицы осуществляется ссылка на домен, содержащий предложение CHECK, а при описании столбца также задается ограничение CHECK, то в результате для столбца будет сформировано условие, являющееся конъюнкцией (логическое И) обоих условий.

Если при описании столбца таблицы не задается базовый домен, а указывается тип данных, то для такого столбца система все равно создаст системный домен с уникальным именем, поместив в него все описанные для столбца характеристики.

Необязательное предложение DEFAULT определяет значение по умолчанию для столбца – это то значение, которое будет присвоено столбцу, если при добавлении новой строки в таблицу в операторе INSERT не указан данный столбец и его значение. Значение по умолчанию применяется только при выполнении оператора добавления данных INSERT и не оказывает никакого влияния на выполнение оператора изменения существующих в таблице данных (UPDATE). Если в операторе изменения данных не указан какой-либо столбец, то его значение просто не изменяется.

Значением по умолчанию может быть литерал, пустое значение NULL или контекстная переменная. Если значение по умолчанию в операторе описания столбца явно не устанавливается, то подразумевается пустое значение NULL. Использование каких-либо выражений в значении по умолчанию недопустимо.

Например. В следующей таблице CALLS для ведения журнала звонкой, поле CALL_DATE имеет значением по умолчанию дату на серверном компьютере в то время, когда выполняется данный оператор INSERT:

CREATE TABLE CALLS (
    CALL_CODE VARCHAR(10),
    CALL_NUMBER VARCHAR(10),
    CALL_DATE DATE DEFAULT CURRENT_DATE)

Если пользователь при помещении новой строки в эту таблицу не задаст значение даты, то система поместит туда текущую дату с сервера.

Необязательное предложение NOT NULL указывает, что столбцу не может быть присвоено пустое значение в операторе INSERT или UPDATE. Это предложение является обязательным для столбца, входящего в состав первичного ключа таблицы.

Например. В справочной таблице стран (COUNTRY) в демонстрационной базе данных employee код страны является первичным ключом. По этой причине он объявлен с предложением NOT NULL:

CREATE TABLE COUNTRY (
    COUNTRY COUNTRYNAME NOT NULL PRIMARY KEY,
    CURRENCY VARCHAR(10) NOT NULL

После определения характеристик столбца можно записать ограничение столбца. Существует четыре вида ограничений столбца.

  • первичный ключ (PRIMARY KEY)
    <ограничение столбца> ::=
        [CONSTRAINT <имя ограничения>]
        PRIMARY KEY 
        [USING [ASC | DESC] INDEX <имя индекса>]
  • уникальный ключ (UNIQUE)
    <ограничение столбца> ::=
        [CONSTRAINT <имя ограничения>]
        UNIQUE
        [USING [ASC | DESC] INDEX <имя индекса>]
  • внешний ключ (REFERENCES)
    <ограничение столбца> ::=
        [CONSTRAINT <имя ограничения>]
        REFERENCES <имя таблицы> [(<имя столбца>)]
            [ON DELETE { NO ACTION | CASCADE | SET DEFAULT | SET NULL }]
            [ON UPDATE { NO ACTION | CASCADE | SET DEFAULT | SET NULL }]
        [USING [ASC | DESC] INDEX <имя индекса>]
  • ограничение значения столбца (CHECK)
    <ограничение столбца> ::=
        [CONSTRAINT <имя ограничения>] CHECK (<условие столбца>)

Необязательное предложение CONSTRAINT задает имя ограничения столбца. Если имя не указано, система присваивает ограничению системное имя, например, INTEG_28. Рекомендуется задавать осмысленные имена ограничениям столбца. В дальнейшем при изменении характеристик таблицы к таким ограничениям будет проще обращаться по заданному имени. Имя ограничения должно быть уникальным среди имен всех ограничений столбцов и/или имен ограничений таблиц во всех таблицах базы данных, а также среди имен созданных пользователем индексов.

Предложение USING позволяет задать имя индекса для поддержания соответствующего ограничения первичного, уникального или внешнего ключа и указать его упорядоченность – по возрастанию значений реквизитов ключа (ASC) или по убыванию их значений (DESC). Если упорядоченность не задана, то предполагается ASC, по возрастанию. Если индекс не указан (не задано предложение USING), то автоматически будет создан индекс с именем этого ограничения, если указано имя ограничения, или с системным именем, если не было задано имени ограничения в предложении CONSTRAINT.

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

Ограничение PRIMARY KEY определяет первичный ключ. В отличие от уникального ключа в таблице может быть только один первичный ключ.

Первичный ключ является уникальным – в таблице не может существовать двух разных строк с одним и тем же значением первичного ключа.

Столбец, являющийся первичным ключом, должен быть описан с указанием NOT NULL – он не может иметь пустое значение.

Для уникального и первичного ключа система автоматически строит соответствующий индекс. Если в описании уникального ключа было указано имя ограничения в предложении CONSTRAINT, то это имя будет присвоено индексу (если в предложении USING не было задано другого имени), иначе индекс получит системное имя.

Ограничение REFERENCES определяет внешний ключ. Внешний ключ должен иметь пустое значение NULL или же он должен ссылаться на первичный или уникальный ключ другой или той же самой таблицы. Понятие «ссылается» означает только лишь, что в родительской таблице должна присутствовать строка, имеющая такое же значение первичного или уникального ключа, что и внешний ключ дочерней таблицы.

Первичный или уникальный ключ часто называют родительскими ключами.

В предложении REFERENCES указывается имя родительской таблицы, на первичный/уникальный ключ которой ссылается внешний ключ дочерней таблицы, и имя первичного/уникального ключа в родительской таблице, на которую ссылается соответствующий ключ дочерней таблицы.

Предложения ON DELETE и ON UPDATE определяют, что произойдет с записями дочерней таблицы при удалении или обновлении соответствующих строк родительской таблицы:

  • NO ACTION – не будет выполнено никаких действий. Будет выдано сообщение об ошибке. Клиентское приложение должно самостоятельно исправить ситуацию и повторить попытку.
  • CASCADE – в дочерней таблице должны быть автоматически удалены все записи, имеющие те же значения внешнего ключа, что и значение первичного (уникального) родительского ключа удаляемой строки родительской таблицы.
  • SET DEFAULT – столбец внешнего ключа всех соответствующих строк в дочерней таблице устанавливается в значение по умолчанию, определенное в предложении DEFAULT этого столбца, описанного как внешний ключ. В подобной ситуации, как правило, в клиентской программе следует предпринять дополнительные меры по обеспечению непротиворечивости данных. Если значение по умолчанию для столбца внешнего ключа не задано, то столбцу присваивается значение NULL.
  • SET NULL – значения внешнего ключа всех соответствующих строк в дочерней таблице устанавливаются в пустое значение NULL. Это не приведет к нарушению целостности данных, так как для внешнего ключа допустимо пустое значение.

Для внешнего ключа система также автоматически строит индекс. Если в описании внешнего ключа было указано имя ограничения в предложении CONSTRAINT, то это имя будет присвоено автоматически создаваемому индексу. Рекомендуется каждому такому ограничению явно присваивать имя.

Предложение USING позволяет задать иное имя индекса для поддержания ограничения внешнего ключа.

Ограничение CHECK для столбца аналогично домену.

При создании таблицы происходит неявная нумерация столбцов. Первый создаваемый столбец получает номер один, следующий – номер два и т.д. Вообще говоря, порядок столбцов в таблице особого значения не имеет за исключением случая, когда в операторе добавления данных INSERT не задан явно список столбцов. Тем не менее, для любого столбца таблицы можно изменить номер – переместить его с одной позиции на другую.

Определение вычисляемого столбца

Вычисляемый столбец задается предложением COMPUTED BY.

COMPUTED [BY] (<выражение>)

Другой вариант задания вычисляемого столбца:

GENERATED ALWAYS AS (<выражение>)

Значение такого столбца не хранится в таблице, а вычисляется, при выборке данных из таблицы. Термин «вычисляемый» не обязательно означает только лишь арифметическое вычисление. Для строковых данных, например, может применяться операция конкатенации, вызов функции получения подстроки и ряда других встроенных функций.

Выражение в этом предложении – выражение, возвращающее ровно одно значение любого типа данных, кроме BLOB или массива. Выражение может содержать любые допустимые операции, обращение к встроенным функциям и/или к функциям, определенным пользователем. Среди значений выражения допустимо использование и оператора SELECT, заключенного в круглые скобки, который при обращении к таблице (это может быть другая или та же самая таблица), представлению или хранимой процедуре выбора возвращает единственное значение или NULL. Операндами используемых в выражении операторов и функций могут быть различные константы, контекстные переменные и имена столбцов этой же таблицы. Все столбцы, используемые в выражении, должны быть определены ранее в этой таблице. Все таблицы, представления и хранимые процедуры, к которым обращаются операторы SELECT, должны уже существовать в базе данных. По этой причине вычисляемые столбцы обычно описывают в самом конце таблицы после ограничений таблицы или непосредственно перед ними.

Вычисляемому столбцу система присваивает соответствующий тип данных, рассчитанный, исходя из вида операций и характеристик операндов в выражении вычисления.

Пример 1. Пусть в таблице существует столбец «оклад человека», SALARY. Можно создать вычисляемый столбец с именем NET_SALARY, который будет иметь значение на 13% меньше, чем оклад (вычеты из заработной платы).

CREATE TABLE STAFF (
    NAME VARCHAR(32),
    SALARY NUMERIC(18, 4),
    NET_SALARY GENERATED ALWAYS AS (SALARY * 0.87)
)

Вычисляемому столбцу NET_SALARY системой будет присвоен тип данных NUMERIC(18, 4). При выборке данных из этой таблицы оператором SELECT будет возвращаться и значение вычисляемого столбца, на 13 процентов меньшее, чем указанный оклад.

Пример 2. Пусть в базе данных существует справочная таблица, содержащая сведения о странах:

CREATE TABLE COUNTRY (
    CODCOUNTRY CHAR(3) NOT NULL,     /* Код страны */
    NAME CHAR(30),                   /* Краткое название страны */
    FULLNAME CHAR(60),               /* Полное название страны */
    CAPITAL CHAR(15),                /* Название столицы */
    DESCR BLOB,                      /* Дополнительное описание */
CONSTRAINT PK_COUNTRY PRIMARY KEY (CODCOUNTRY) )

Другая таблица, описывающая различные организации, содержит код страны, в которой располагается (зарегистрирована) данная организация.

CREATE TABLE FIRM (
    COD INTEGER NOT NULL,
    NAME CHAR(50),
    CODCOUNTRY CHAR(3),
    COUNTRYNAME GENERATED ALWAYS AS (
        (SELECT NAME FROM COUNTRY
         WHERE COUNTRY.CODCOUNTRY = FIRM.CODCOUNTRY))
)

В этой таблице присутствует вычисляемый столбец с типом данных VARCHAR(30), поскольку отыскиваемый при использовании оператора SELECT столбец из справочной таблицы имеет тип данных VARCHAR(30). Обратите внимание, что оператор SELECT заключен в двойную пару круглых скобок. Во всех синтаксических конструкциях, где присутствует одиночный оператор SELECT (оператор, возвращающий ровно одно значение одного столбца или пустое значение NULL), этот оператор должен быть заключен в круглые скобки. Внешняя пара скобок требуется, потому что выражение для любого вычисляемого столбца по правилам синтаксиса также должно заключаться в круглые скобки.

В предложении WHERE именам столбцов предшествует имя соответствующей таблицы и точка. Это так называемые уточненные имена. Имя таблицы здесь требуется, чтобы устранить возникающую неопределенность, поскольку столбец с именем CODCOUNTRY присутствует в обеих таблицах — и в FIRM, и в COUNTRY. Для уточненных имен возможно использование и псевдонимов (или алиасов, alias) таблиц. Использование псевдонимов может несколько сократить количество символов, набираемых для выполнения оператора, однако их применение имеет больший смысл, когда в сложном запросе одна и та же таблица встречается в нескольких различных конструкциях оператора SELECT. Если для таблицы задан псевдоним, то во всех уточненных именах столбцов можно использовать только псевдонимы, использование имени таблицы в этом случае недопустимо. При отсутствии псевдонима используется имя таблицы. Для главной таблицы, таблицы самого верхнего уровня, используемой в первом операторе SELECT, уточняющее имя можно не указывать.

CREATE TABLE FIRM (
    COD INTEGER NOT NULL,
    NAME CHAR(50),
    CODCOUNTRY CHAR(3),
    COUNTRYNAME GENERATED ALWAYS AS (
        (SELECT NAME FROM COUNTRY
         WHERE CODCOUNTRY = FIRM.CODCOUNTRY))
)

Для таблицы же FIRM псевдоним или имя таблицы обязательно должно быть указано.

Определение столбца идентификации

Столбцы идентификации могут быть определены с помощью предложения

GENERATED BY DEFAULT AS IDENTITY

Столбец идентификации представляет собой столбец, связанный с внутренним генератором последовательностей. Его значение устанавливается автоматически каждый раз, когда оно не указано в операторе INSERT. Необязательное предложение START WITH позволяет указать начальное значение отличное от нуля. Идентификационные столбцы неявно являются NOT NULL столбцами.

Тип данных столбца идентификации должен быть целым числом с нулевым масштабом. Допустимыми типами являются SMALLINT, INTEGER, BIGINT, NUMERIC(x,0) и DECIMAL(x,0).

Идентификационный столбец не может иметь значений по умолчанию и вычисляемых значений, а также не может быть изменён в обычный столбец. И наоборот.

Определение ограничений таблицы

Ограничения таблицы являются более универсальным, удобным и наглядным способом описания ограничений таблицы, чем ограничения столбца. Они применяются не только к одному столбцу, но и к группе столбцов создаваемой (изменяемой) таблицы. Рассмотрим синтаксис описания различных ограничений на уровне таблицы:

  • первичный ключ (PRIMARY KEY)
<ограничение таблицы> ::= 
    [CONSTRAINT <имя ограничения>]
    PRIMARY KEY (<имя столбца> [, <имя столбца> ...])
    [USING [ASC | DESC] INDEX <имя индекса>]
  • уникальный ключ (UNIQUE)
<ограничение таблицы> ::= 
    [CONSTRAINT <имя ограничения>]
    UNIQUE (<имя столбца> [, <имя столбца> ...])
    [USING [ASC | DESC] INDEX <имя индекса>]
  • внешний ключ (FOREIGN KEY)
<ограничение таблицы> ::= 
    [CONSTRAINT <имя ограничения>]
    FOREIGN KEY (<имя столбца> [, <имя столбца> ...])
    REFERENCES <имя таблицы> (<имя столбца> [, <имя столбца> ...])
    [USING [ASC | DESC] INDEX <имя индекса>]
    [ON DELETE { NO ACTION | CASCADE | SET DEFAULT | SET NULL }]
    [ON UPDATE { NO ACTION | CASCADE | SET DEFAULT | SET NULL }]
  • ограничения записей таблицы
<ограничение таблицы> ::= 
    [CONSTRAINT <имя ограничения>] CHECK (<условие>)

Ограничение таблицы, в отличие от ограничения столбца, может относиться как к одному отдельному столбцу, так и к группе столбцов этой таблицы. Столбцы, задаваемые в ограничении, должны быть уже описаны при создании или изменении таблицы.

Ограничению таблицы также можно присвоить имя, используя предложение CONSTRAINT. Имя ограничения должно быть уникальным среди имен всех ограничений всех таблиц, ограничений столбцов таблиц и имен всех индексов базы данных. Если не указано предложение USING, то соответствующий данному ограничению индекс получит имя создаваемого ограничения. Иначе будет присвоено системное имя.

В случае задания предложения USING при описании первичного, уникального или внешнего ключа можно указать имя создаваемого индекса, поддерживающего соответствующее ограничение, а также и его упорядоченность – по возрастанию (ASC – по умолчанию) или по убыванию (DESC).

Предложение PRIMARY KEY задает ограничение первичного ключа. В состав первичного ключа в ограничении таблицы может входить один или более столбцов данной таблицы. Имена столбцов перечисляются в круглых скобках. Каждый столбец первичного ключа должен быть явно описан с указанием атрибута NOT NULL. Таблица может иметь не более одного первичного ключа. Для первичного ключа система автоматически создает индекс.

Предложение UNIQUE задает ограничение уникального ключа. В отличие от первичного ключа отдельные столбцы уникального ключа могут иметь пустое значение NULL. При этом комбинация значений столбцов, входящих в состав уникального ключа, должна оставаться уникальной, исходя из того, что в подобной ситуации два пустых значения NULL считаются одинаковыми. Исключением является лишь случай, когда все столбцы уникального ключа имеют пустое значение. Таких строк с полностью «пустым» значением уникального ключа в одной таблице может быть произвольное количество.

Пусть, например, уникальный ключ таблицы состоит из двух столбцов K1 и K2, и для них не указано предложение NOT NULL. Тогда присутствие в такой таблице следующих строк является допустимым:

K1K2
11
12
NULLNULL
NULLNULL
1NULL
NULL2
NULLNULL

Однако попытка записать в эту таблицу еще и любую из следующих строк приведет к нарушению уникальности значения ключа:

K1K2
1NULL
NULL2

Эти значения уникального ключа уже присутствуют в таблице. Такие действия вызовут исключение базы данных, строки в таблицу помещены не будут. Таблица может иметь произвольное количество уникальных ключей. Для уникального ключа система автоматически создает индекс.

Предложение FOREIGN KEY задает ограничение внешнего ключа. Синтаксис этого ограничения на уровне таблицы несколько отличается от синтаксиса определения внешнего ключа на уровне столбца. После ключевых слов FOREIGN KEY в скобках указывается список столбцов создаваемой (изменяемой) таблицы, которые включены в состав внешнего ключа. Может быть задан и один единственный столбец. Все столбцы, входящие в состав внешнего ключа, должны быть описаны в таблице ранее.

После ключевого слова REFERENCES указывается имя родительской таблицы, на первичный или уникальный ключ которой ссылается описываемый внешний ключ. Список имен столбцов родительской таблицы, входящих в состав первичного (уникального) ключа помещается сразу после имени таблицы в этом предложении и заключается в круглые скобки. Сама родительская таблица уже должна быть создана в базе данных. Если происходит ссылка именно на первичный ключ родительской таблицы, то список столбцов, включенных в состав первичного ключа этой таблицы, можно не указывать. Для внешнего ключа система автоматически создает индекс.

Структура внешнего ключа дочерней таблицы по количеству столбцов и по типам данных, включая размерность символьных данных, должна полностью соответствовать структуре первичного (уникального) ключа родительской таблицы, на который ссылается данный внешний ключ. Совпадения имен не требуется.

Необязательные предложения ON DELETE и ON UPDATE определяют, что будет происходить с дочерней таблицей, соответственно, при удалении строки родительской таблицы или при изменении ключевых данных в родительской таблице. Возможные опции аналогичны соответствующему параметру ограничения столбца, рассмотренному ранее.

Ограничение CHECK задает условия, которым должны удовлетворять значения столбцов данной таблицы при помещении в таблицу новой строки (оператор INSERT) или при изменении (оператор UPDATE) отдельных столбцов существующей строки таблицы. Варианты и правила использования условий таблицы полностью совпадают с условиями CHECK столбца, только вместо VALUE нужно использовать имена столбцов.

Изменение таблицы

Описание существующих в базе данных таблиц (характеристик столбцов, их порядка, наличие различных ключей или ограничения CHECK) можно изменять после создания таблиц. Для изменения структуры существующих таблиц используется оператор ALTER TABLE.

Синтаксис оператора ALTER TABLE:

ALTER TABLE <имя таблицы> <операция изменения> [, <операция изменения>...]

В одном операторе можно выполнить произвольное количество изменений в таблице. Различные операции по изменению существующей таблицы отделяются друг от друга запятыми.

Синтаксис такой операции изменения существующей таблицы:

<операция изменения> ::= 
    { ADD <определение столбца> 
    | ADD <ограничение таблицы>
    | DROP <имя столбца>
    | DROP CONSTRAINT <ограничение столбца или таблицы>
    | ALTER [COLUMN] <имя столбца> <модификация столбца>}

При добавлении нового столбца или ограничения в существующую в базе данных таблицу используются уже рассмотренные конструкции определение столбца и ограничение таблицы.

Операция удаления столбцов (DROP <имя столбца>) проверяет зависимости. Прежде чем удалять столбец, нужно удалить все зависимости в базе данных, связанные с этим столбцом. Такие зависимости могут присутствовать:

  • в ограничениях столбцов и таблицы. Такие ограничения могут существовать как в текущей, корректируемой, таблице, так и в любой другой существующей таблице базы данных. В первую очередь это могут быть ограничения первичного, уникального или внешнего ключа, в состав которого входит удаляемый столбец. Затем это могут быть ограничения внешних ключей в других таблицах, которые ссылаются на первичный или уникальный ключ корректируемой таблицы, если удаляемый столбец входит в состав первичного (уникального) ключа. Наконец, это могут быть ограничения CHECK данной или иных таблиц, в условиях которых присутствует удаляемый столбец.
  • в индексах, когда удаляемый столбец входит в состав каких-либо индексов базы данных, созданных пользователем для изменяемой таблицы.
  • в хранимых процедурах, функциях и триггерах, где присутствуют обращения к значениям удаляемого столбца.
  • в представлениях, где удаляемый столбец может присутствовать в списке выбора, а также в предложении ON соединяемых таблиц, определяющем условие соединения, в предложении WHERE, определяющем условие выборки, или в предложениях ORDER BY, существующих в представлениях, задающих упорядоченность результата выборки данных.

Часть DROP CONSTRAINT удаляет существующее ограничение столбца или ограничение таблицы.

Часть ALTER [COLUMN] позволяет изменить характеристики существующих столбцов и похожа на изменение домена. Рассмотрим как изменять разные типы столбцов.

Изменение столбца таблицы

Синтаксис соответствующей части команды ALTER TABLE выглядит следующим образом:

ALTER [COLUMN] <имя столбца>
    | TO <новое имя столбца>
    | POSITION <новая позиция>
    | TYPE { <тип данных> | <имя домена> }
    | SET DEFAULT { <литерал> | NULL | <контекстная переменная>}
    | DROP DEFAULT
    | SET NOT NULL
    | DROP NOT NULL

Невозможно изменение имени столбца, если этот столбец включен в какое-либо ограничение – первичный или уникальный ключ, внешний ключ, ограничение столбца или ограничение таблицы CHECK. Имя столбца также нельзя изменить, если этот столбец таблицы используется в каком-либо триггере, в хранимой процедуре или в представлении, созданных пользователем. Если для таблицы был автоматически создан триггер для поддержания какого-либо ограничения, то соответствующее имя в триггере будет автоматически изменено при изменении имени столбца.

Нельзя изменить тип данных у столбца, который принимает участие в связке внешний ключ / первичный (уникальный) ключ. В остальных случаях изменение типа данных возможно.

При таком изменении таблица будет содержать все существовавшие на момент изменения типа данных строки в одной структуре, а вновь добавляемые строки будут иметь иную структуру.

При изменении позиции столбца нужно помнить про случаи, когда существуют операторы INSERT, в которых явно не указан список имен добавляемых столбцов. При изменении позиции столбца такие операторы станут работать неправильно. Также это может привести к неверному выполнению оператора SELECT, если в списке выбора был указан выбор всех столбцов таблицы (символ *) , а в предложении ORDER BY был задан номер столбца, по которому выполняется упорядочивание полученных данных. При разработке приложений такие конструкции лучше не использовать.

Номер позиции – это положительное число. Столбцы в таблицах нумеруются, начиная с позиции один. Если указать номер позиции, превышающий количество столбцов в таблице, то никакие изменения в таблице выполнены не будут, исключений базы данных не возникнет. При задании же числа меньше единицы будет выдано сообщение об ошибке, такое изменение не будет выполнено.

При удалении значения по умолчанию для столбца, доменное значение перекроет удаляемое, если задан домен.

Если удаление значения по умолчанию производится над столбцом, у которого нет значения по умолчанию, или чьё значение по умолчанию основано на домене, то это приведёт к ошибке выполнения данного оператора.

При добавлении значения по умолчанию, если столбец уже имел значение по умолчанию, то оно будет заменено новым. Значение по умолчанию для столбца всегда перекрывает доменное значение по умолчанию.

Предложение SET NOT NULL добавляет ограничение NOT NULL для столбца таблицы. Успешное добавление ограничения NOT NULL происходит, только после полной проверки данных таблицы, для того чтобы убедится что столбец не содержит значений NULL.

Явное ограничение NOT NULL на столбце, базирующегося на домене, преобладает над установками домена. В этом случае изменение домена для допустимости значения NULL, не распространяется на столбец таблицы.

Предложение DROP NOT NULL удаляет ограничение NOT NULL для столбца таблицы. Если столбец основан на домене с ограничением NOT NULL, то ограничение домена перекроет это удаление.

Для вычисляемых столбцов допустимо изменить тип и выражение вычисляемого столбца

ALTER [COLUMN] <имя столбца>
    [TYPE <тип данных>]
    {GENERATED ALWAYS AS | COMPUTED [BY]} (<выражение>)

Невозможно изменить обычный столбец на вычисляемый и наоборот.

Для столбцов идентификации позволено изменять начальное значение. Если указано только предложение RESTART, то происходит сброс значения генератора в ноль. Необязательное предложение WITH позволяет указать для нового значения внутреннего генератора отличное от нуля значение. Невозможно изменить обычный столбец на столбец идентификации и наоборот.

Удаление таблицы

Для удаления существующей таблицы используется оператор DROP TABLE.

Синтаксис оператора:

DROP TABLE <имя таблицы>

Нельзя удалить таблицу, которая является родительской в связке внешний ключ / первичный (уникальный) ключ. Нельзя также удалить таблицу, на которую существуют ссылки в триггерах (за исключением триггеров, написанных пользователем именно для этой таблицы), и таблицу, которая используется в хранимой процедуре или в представлении.

Таблица, используемая в какой-либо активной транзакции, не будет удалена до завершения (подтверждения или отмены) этой транзакции.

При успешном удалении таблицы автоматически будут удалены все ее данные, триггеры, созданные для этой таблицы (пользователем или автоматически системой), а также все индексы, построенные автоматически системой управления базами данных или пользователем для такой таблицы.

Контрольные вопросы

  1. Какие ограничения существуют для строк и столбцов таблицы?
  2. Приведите пример оператора создания таблицы?
  3. В какой момент используется значение по умолчанию для столбца?
  4. В чем различия первичного и уникального ключа?
  5. Назовите способы поддержания ссылочной целостности?
  6. Приведите пример использования вычисляемого столбца?
  7. Какой тип данных допустим для столбца идентификации?
  8. Чем ограничения таблицы отличаются от ограничений столбца?
  9. Что можно или нельзя изменять в уже созданной таблице?

Транзакции

Понятие транзакции и ее свойства

Транзакции - это фундаментальная концепция баз данных, обеспечивающая целостность данных.

Транзакция - это последовательность операций с базой данных, которая должна быть выполнена как единое целое по принципу “все или ничего”.

Стандартно принято выделять 4 базовых свойства транзакций, сокращенно ACID-свойства:

  • Атомарность (Atomicity) - транзакция выполняется полностью или не выполняется вообще
  • Согласованность (Consistency) - транзакция переводит БД из одного согласованного состояния в другое
  • Изолированность (Isolation) - параллельные транзакции не мешают друг другу
  • Долговечность (Durability) - результаты завершенной транзакции сохраняются даже после сбоев

Представьте, что вы разрабатываете информационную систему для банка. Основной сущностью является счет, а основной операцией – перевод денег между счетами. Перевод заключается в том, чтобы уменьшить баланс одного счета и увеличить баланс другого счета на такую же сумму. В терминах SQL это означает, что нужно выполнить два запроса UPDATE. В простейшем случае, база данных и запросы к ней могли бы выглядеть так.

CREATE TABLE ACCOUNTS (
   ACCOUNT_ID VARCHAR(20) PRIMARY KEY,
   OWNER_NAME VARCHAR(100) NOT NULL,
   BALANCE DECIMAL(15,2) NOT NULL CHECK (BALANCE >= 0)
);

-- Инициализируем тестовые данные
INSERT INTO ACCOUNTS VALUES ('10000001', 'Иванов И.И.', 5000.00);
INSERT INTO ACCOUNTS VALUES ('10000002', 'Петров П.П.', 3000.00);

CREATE PROCEDURE TRANSFER (
    FROM_ACCOUNT VARCHAR(20),
    TO_ACCOUNT VARCHAR(20),
    AMOUNT DECIMAL(15,2))
RETURNS (RES BOOLEAN)
AS
BEGIN
    RES = FALSE;
    UPDATE ACCOUNTS 
        SET BALANCE = BALANCE - :AMOUNT 
        WHERE ACCOUNT_ID = :FROM_ACCOUNT AND BALANCE >= :AMOUNT;
    IF (ROW_COUNT = 0) THEN
    BEGIN
        SUSPEND;
        EXIT;
    END
    -- а что если здесь отключат свет?
    UPDATE ACCOUNTS SET BALANCE = BALANCE + :AMOUNT WHERE ACCOUNT_ID = :TO_ACCOUNT;
    RES = TRUE;
    SUSPEND;
END;

SELECT * FROM TRANSFER('10000001', '10000002', 10000.00);

SELECT * FROM ACCOUNTS;

В примере мы видим комментарий. А что случится, если в этом месте действительно отключат электричество, произойдет какой-либо сбой, аппаратная ошибка? В промышленных транзакционных СУБД эти ситуации и обрабатывает сервер. Если в этом месте произойдет сбой, то в дальнейшем, когда БД снова станет доступна, она будет содержать данные, как если бы процедуру TRANSFER не вызывали.

Кроме этой проблемы, транзакции играют важную роль в изоляции работы множества пользователей в одной базе данных, с целью исключения аномалий параллельного доступа.

Аномалии параллельного доступа

  1. Потерянное обновление. Возникает при одновременном изменении одного блока данных разными транзакциями. При этом теряются все изменения, кроме последнего.
Транзакция 1Транзакция 2
UPDATE T SET F=F+10 WHERE K=1UPDATE T SET F=F+15 WHERE K=1

При последовательном выполнении транзакций, значение поля F изменилось бы на 25. Но при параллельном доступе, обе транзакции могут прочитать старое значение F, добавить свое слагаемое и записать новый результат. В результате, новое значение F будет увеличено или на 10 или на 15, в зависимости от того, какая из транзакций будет подтверждена последней и затрет изменения параллельной транзакции.

  1. Грязное чтение (Dirty Read). Чтение незафиксированных изменений другой транзакции.
Транзакция 1Транзакция 2
UPDATE T SET F=F+1 WHERE K=1
SELECT F FROM T WHERE K=1
ROLLBACK

Транзакция 1 изменяет данные поля F, после транзакция 2 его читает, использует далее в своей работе, а после транзакция 1 не подтверждается и получится, что прочтенные транзакцией 1 незафиксированные данные никогда не существовали, но были использованы при работе.

  1. Неповторяющееся чтение (Non-repeatable Read). Разные значения при повторном чтении одной строки.
Транзакция 1Транзакция 2
SELECT F FROM T WHERE K=1
UPDATE T SET F=F+2 WHERE K=1
COMMIT
SELECT F FROM T WHERE K=1

В транзакции 2 выбирается значение поля F, затем в транзакции 1 изменяется значение поля F. При повторной попытке выбора значения из поля F в транзакции 2 будет получен другой результат.

  1. Фантомное чтение (Phantom Read). Появление новых строк при повторном чтении диапазона.
Транзакция 1Транзакция 2
SELECT SUM(F) FROM T
INSERT INTO T (K, F) VALUES (2, 30)
COMMIT
SELECT SUM(F) FROM T

Транзакция 2 выполняет запрос, использующий все значения поля F. Затем транзакция 1 вставляет новую строку. Повторное выполнение запроса в транзакции 2 выдаст другой результат. От неповторяющегося чтения оно отличается тем, что данные, к которым было обращение, не изменились, но изменилось количество строк с данными.

Уровни изоляции

Уровень изоляции определяет, насколько транзакции изолированы друг от друга и какие аномалии возможны при параллельном выполнении.

Стандарт SQL определяет 4 основные уровни изоляции:

  1. READ UNCOMMITTED (Чтение незафиксированных данных)

    • Самый низкий уровень изоляции
    • Возможны все типы аномалий:
  2. READ COMMITTED (Чтение зафиксированных данных)

    • По умолчанию в большинстве СУБД
    • Запрещает:
      • Потерянное обновление
      • Грязное чтение
    • Разрешает:
      • Неповторяющееся чтение
      • Фантомное чтение
  3. REPEATABLE READ (Повторяемое чтение)

    • Запрещает:
      • Грязное чтение
      • Неповторяющееся чтение
      • Потерянное обновление
    • Разрешает:
      • Фантомное чтение
  4. SERIALIZABLE (Упорядочиваемый)

    • Самый строгий уровень
    • Запрещает все аномалии
    • Эквивалентен последовательному выполнению транзакций

Исторически Firebird и РЕД База Данных реализует следующие уровни изоляции:

  1. READ COMMITTED. В полном объеме соответствует стандартному.

  2. SNAPSHOT. Практически аналог REPEATABLE READ, но запрещает также и фантомное чтение. Все операции видят согласованный снимок данных на момент начала транзакции.

  3. SNAPSHOT TABLE STABILITY. Практически аналог SERIALIZABLE. Блокирует читаемые таблицы от изменений другими транзакциями.

Старт транзакции

Для старта транзакции с необходимыми параметрами используется оператор SET TRANSACTION.

Запуск транзакций на выполнение осуществляется только клиентскими приложениями, но не сервером.

SET TRANSACTION
    [READ WRITE | READ ONLY]
    [ISOLATION LEVEL] [SNAPSHOT [TABLE STABILITY] | READ COMMITTED]

Все предложения в операторе SET TRANSACTION являются необязательными. Если в операторе запуска транзакции на выполнение не задано никакого предложения, то предполагается старт транзакции со значениями всех характеристик по умолчанию.

SET TRANSACTION
    READ WRITE
    ISOLATION LEVEL SNAPSHOT

Транзакция с такими же характеристиками по умолчанию автоматически запускается системой, если пользователь, выполняя обращение к данным базы данных, не запустил никакой транзакции.

При старте транзакции сервер передает клиенту дескриптор транзакции (целое число). Значение этого дескриптора средствами SQL можно получить, используя контекстную переменную CURRENT_TRANSACTION.

Для транзакций существует два режима доступа к данным базы данных: READ WRITE (по умолчанию) и READ ONLY.

В режиме READ ONLY в контексте данной транзакции могут выполняться только операции выборки данных SELECT. Любая попытка изменения данных в контексте такой транзакции приведет к исключениям базы данных.

Уровень изоляции запускаемой транзакции задается необязательным предложением ISOLATION LEVEL. Это самая важная характеристика транзакции, которая определяет ее основное поведение по отношению к другим одновременно выполняющимся транзакциям.

Подтверждение транзакции

Чтобы подтвердить текущую транзакцию используется оператор COMMIT.

COMMIT;

При выполнении этого оператора подтверждаются все изменения в данных, выполненные в контексте данной транзакции (добавления, изменения, удаления). Новые версии записей становятся доступными для других процессов. Если в процессе подтверждения транзакции возникли ошибки в базе данных, то транзакция не подтверждается. Пользовательская программа должна обработать ошибочную ситуацию и заново подтвердить транзакцию или выполнить ее откат.

Отмена транзакции

Для отмены всех изменений, выполненных в контексте текущей транзакции, используется оператор ROLLBACK.

ROLLBACK;

При выполнении оператора отменяются все изменения данных базы данных (добавление, изменение, удаление), выполненные под управлением этой транзакции. Оператор ROLLBACK никогда не вызывает ошибок.

Контрольные вопросы

  1. Что такое транзакция?
  2. Назовите свойства транзакций?
  3. Назовите аномалии параллельного доступа к данным?
  4. Чем фантомное чтение отличается от неповторяющегося чтения?
  5. Назовите уровни изоляции транзакций?
  6. Какие уровни изоляции транзакций поддерживает РЕД База Данных?
  7. Какие аномалии параллельного доступа разрешены на каждом из них?
  8. Какие аномалии параллельного доступа запрещены на каждом из них?
  9. Какие параметры транзакций допустимы?
  10. Что произойдет, если при подтверждении транзакции возникнут ошибки?
  11. Что произойдет, если при отмене транзакции возникнут ошибки?

Последовательности и генераторы

Последовательность (sequence) или генератор (generator) — это самый простой объект базы данных. Он позволяет хранить и генерировать 8 байтные целые числа в диапазоне значений: от −263 до263−1.­ Обычно используются для формирования значений искусственных первичных ключей.

Важной особенностью генераторов является то, что работа с ними выполняется вне контекста какой-либо транзакции. Это означает, что при одновременном обращении к одному и тому же генератору разных конкурирующих транзакций никогда не возникнет конфликта блокировки, и каждый параллельный процесс получит уникальное новое числовое значение. Тем не менее, генераторы могут использоваться и для получения последовательностей неповторяющихся целых чисел для любых других целей.

Исторически в Interbase/Firebird/RedDatabase были генераторы. Позднее, когда последовательности вошли в стандарт SQL грамматика языка была доработана, но внутри СУБД используются те же генераторы. Таким образом, это синонимы, а все функции и операторы языка SQL могут применятся как к последовательностям, так и к генераторам. При дальнейшем изложении будем стараться использовать термин последовательность, т.к. он больше соответствует стандарту языка SQL.

Создание последовательности

Синтаксис оператора создания последовательности:

CREATE {GENERATOR | SEQUENCE} <имя последовательности>
    [START WITH <начальное значение>] [INCREMENT [BY] <приращение>];

Ключевые слова GENERATOR и SEQUENCE являются синонимами.

В момент создания последовательности ей устанавливается значение, указанное в необязательном предложении START WITH или 0.

Необязательное предложение INCREMENT [BY] позволяет задать шаг приращения для оператора NEXT VALUE FOR. По умолчанию шаг приращения равен единице.

Изменение последовательности

Можно явно в любой момент времени установить новое значение последовательности, выполнив оператор:

ALTER SEQUENCE <имя генератора> 
    [START WITH <значение>]
    [RESTART [WITH <значение>]]
    [INCREMENT [BY] <приращение>]

Устаревший синтаксис для генераторов также доступен и будет работать для этих объектов.

SET GENERATOR <имя генератора> TO <значение>

Предложение START WITH позволяет установить новое начальное значение последовательности.

Предложение RESTART без WITH используется для перезапуска последовательности с начального значения. Возможно уже с нового, если в команде также использован START WITH.

Предложение RESTART WITH позволяет просто перезапустить последовательность с указанного значения, не меняя его сохраненное начальное значение.

Предложение INCREMENT [BY] позволяет изменить шаг приращения последовательности для оператора NEXT VALUE FOR.

Разработчикам прикладных систем следует аккуратно использовать эти операторы, поскольку изменение значений последовательностей может привести к нарушениям в базе данных из-за помещения в разные строки первичного ключа таблицы одинаковых значений. Используйте их только когда ясно осознаете необходимость. Возможно в случаях, когда последовательности используются не для генерации первичных ключей.

Когда значение генератора достигает максимальной величины, то все новые обращения к нему переводят его значение в отрицательную величину. В этот момент при каждом новом обращении к генератору начинается отрицательный отсчет от максимальной его величины (–263) к нулю.

С помощью оператора CREATE OR ALTER GENERATOR (SEQUENCE) можно создать новую или изменить существующую последовательность:

CREATE OR ALTER {GENERATOR | SEQUENCE} <имя последовательности>
    [{START WITH <начальное значение> | RESTART}]
    [INCREMENT [BY] <приращение>]

Если последовательности не существует, то она будет создана. Уже существующая последовательность будет изменена, при этом существующие зависимости последовательности будут сохранены.

Удаление последовательности

Последовательность можно удалить, используя оператор:

DROP {GENERATOR | SEQUENCE) <имя генератора>

Удалять последовательность следует только после того, как будут удалены из базы данных все триггеры и хранимые процедуры и функции, ссылающиеся на этот генератор.

Работа с последовательностями и генераторами

Основная функция для работы с последовательностями это NEXT VALUE FOR. Конструкция (функция) NEXT VALUE FOR позволяет получить значение указанного генератора, увеличенное на размер инкремента (по умолчанию на единицу).

Синтаксис для получения значения последовательности:

NEXT VALUE FOR <имя генератора>

Точно такой же результат можно получить, вызвав функцию:

GEN_ID(<имя генератора>, <инкремент>)

Отличие в том, что при выполнении функции GEN_ID текущее значение указанного генератора, изменяется на указанную в качестве параметра величину приращения (это может быть положительное, отрицательное число или ноль). Обычно функция используется в операторах INSERT для получения уникальных числовых значений для искусственного первичного ключа. Вместо функции GEN_ID рекомендуется использовать конструкцию NEXT VALUE FOR.

Контрольные вопросы

  1. Чем последовательность отличается от генератора?
  2. Для каких целей чаще всего используют последовательности?
  3. Приведите пример оператора изменения последовательности?
  4. Назовите основные различия функции GEN_ID и конструкции NEXT VALUE FOR?

Оператор SELECT

Оператор SELECT относится к подразделу языка манипулирования данными DML (Data Manipulation Language). Его можно назвать самым сложным и мощным оператором SQL. Он позволяет выбирать данные из одной или более таблиц на основании условий соединения, фильтровать данные, группировать их и вычислять агрегатные функции по группам, упорядочивать данные по различным критериям, вычислять и комбинировать данные.

Результатом выборки является выходной набор данных — множество строк одинаковой структуры, состав которых задан в списке выбора оператора SELECT.

Синтаксис оператора достаточно сложный. В данной главе дадим его краткий обзор, а последующие главы будут посвящены более детальному изучению некоторых частей.

SELECT [DISTINCT | ALL] <выходное поле> [, <выходное поле>]
    FROM [<соединяемые источники>]
    [WHERE <условие выборки>]
    [GROUP BY <условие группировки>
    [HAVING <условие выборки>]]
    [UNION [DISTINCT | ALL] <другой набор данных>]
    [ORDER BY <выражение для порядка выборки>]
    [OFFSET <n> {ROW | ROWS}] [FETCH {FIRST | NEXT} [<m>] {ROW | ROWS} ONLY]]

Ключевое слово DISTINCT указывает, что в выходной набор данных не помещаются дубликаты строк.

Далее идет список выбора, указывающий, какие столбцы из каких таблиц, участвующих в операции выборки, помещаются в выходной набор данных. Здесь могут располагаться константы, контекстные переменные и операторы SELECT, выбирающие из произвольных таблиц одно значение одного столбца.

Предложение FROM содержит список таблиц, из которых осуществляется выбор данных. В этом предложении может содержаться описание соединения (JOIN) нескольких таблиц для получения выходного набора данных.

Необязательное предложение WHERE задает условия выборки данных — те условия, которым должны удовлетворять строки исходной таблицы (исходных таблиц), для того, чтобы они попали в результирующий набор данных.

Предложения GROUP BY и HAVING позволяют сгруппировать выбранные данные, если в списке выбора присутствуют агрегатные функции, обобщающие данные из нескольких строк исходной таблицы.

Предложение UNION дает возможность объединить в выходном наборе данных несколько таблиц с одинаковой структурой.

Предложение ORDER BY задает упорядоченность выходного набора данных. Здесь также можно указать количество строк, которое должно быть помещено в результирующий набор данных (OFFSET, FETCH).

Рассмотрим несколько простых примеров оператора SELECT.

SELECT * FROM COUNTRY;

Выбирает все записи и все столбцы из таблицы COUNTRY.

SELECT CURRENCY FROM COUNTRY WHERE COUNTRY='Russia';

Выбирает только поле CURRENCY (валюта) из таблицы стран COUNTRY, причем только те строки, название стран в которых удовлетворяют условию в предложении WHERE. В данном случае будет выбрана единственная строка и единственное поле, т.е. фактически одно значение.

SELECT COUNT(*) FROM CUSTOMER;

Возвращает количество записей в таблице CUSTOMER. COUNT является агрегатной функцией, которая производит вычисления в группе значений и возвращает одно значение для каждой группы. В данном случае группой является вся таблица, а вычисления подсчитывают количество значение в группе.

SELECT E.FULL_NAME FROM EMPLOYEE E ORDER BY E.FULL_NAME;

Возвращает список полных имен сотрудников, упорядоченный по алфавиту. Обратите внимание на использование псевдонима таблицы E.

Список выбора

Список полей содержит одно или более выражений, разделённых запятыми. Результатом каждого выражения является значение соответствующего поля в наборе данных команды SELECT. Исключением является выражение * («звёздочка»), которое возвращает все поля отношения.

Синтаксис:

SELECT [ALL | DISTINCT] <поле>, <поле>, ... FROM T

, где <поле> может быть:

  • ТАБЛИЦА.ПОЛЕ
  • ПРОЦЕДУРА.ВЫХОДНОЕ_ПОЛЕ
  • NULL
  • NEXT VALUE FOR
  • Конструкция CASE
  • Выражение
  • Константа
  • Любое выражение, возвращающее единственное значение

Вместо ТАБЛИЦА можно указать псевдоним, представление.

Хорошим тоном является указание полного имени поля вместе с именем псевдонима или таблицы/представления/хранимой процедуры, к которой это поле принадлежит.

Указание полного имени становится обязательным в случае, если поле с одним и тем же именем находится в более чем одной таблице, участвующей в объединении.

Псевдонимы заменяют оригинальное имя таблицы, представления или хранимой процедуры: как только определён псевдоним для соответствующего отношения, использовать оригинальное имя нельзя.

В начало списка полей могут быть добавлены ключевые слова DISTINCT или ALL:

  • DISTINCT удаляет дубликаты строк: то есть, если две или более записи содержат одинаковые значения во всех соответствующих полях, только одна из этих строк будет включена в результирующий набор данных.
  • ALL включает все строки в результирующий набор данных. ALL включено по умолчанию и поэтому редко используется: явное указание поддерживается для совместимости со стандартом SQL.

Примеры списков выбора:

SELECT * FROM RDB$DATABASE;
SELECT CURRENT_USER FROM RDB$DATABASE;

Выбирает все поля из таблицы RDB$DATABASE. Особенность данной таблицы в том, что в ней существует всегда одна единственная запись. Часто ее используют чтобы просто вычислить значения какой-либо функции. При этом сама запись фактически не нужна, как показано во втором случае.

SELECT CUSTOMER.CUSTOMER, CUSTOMER.PHONE_NO, CITY FROM CUSTOMER;

Выбирает поля из таблицы CUSTOMER используя полное имя поля.

SELECT LAST_NAME, SALARY * 12 AS ANNUAL_SALARY FROM EMPLOYEE;

Запрос, в котором используется выражение в списке выбора. При этом для каждой строки результата столбец SALARY будет умножен на 12.

SELECT iif(CITY IS NULL, 'Муром', CITY) FROM CUSTOMER;

Тоже выражение, но с использованием встроенной функции iif, которая имеет три аргумента: логический и два значения. Если первый аргумент равен TRUE, то функция возвращает второй аргумент, в противном случае третий. Может быть использована любая функция, в том числе определенная пользователем.

SELECT UPPER(COUNTRY) FROM COUNTRY;

Пример приведения строкового поля к верхнему регистру. Это поле в результатирующем наборе данных будет иметь значения только в верхнем регистре.

Ограничения выборки

Позволяет получить части строк из упорядоченного набора.

Синтаксис:

SELECT ... FROM ...
    [ORDER BY <список сортировки>]
    [OFFSET <число строк> {ROW | ROWS}]
    [FETCH {FIRST | NEXT} [<число строк>] {ROW | ROWS} ONLY]

<число строк> - может быть выражением, возвращающим целое число, в том числе подзапросом.

Предложение OFFSET указывает, какое количество строк необходимо пропустить. Предложение FETCH указывает, какое количество строк необходимо получить.

Предложения OFFSET и FETCH могут применяться независимо уровня вложенности выражений запросов.

Рассмотрим несколько примеров.

Следующий запрос возвращает все строки кроме первых 10, упорядоченных по столбцу COL1:

SELECT * FROM T1 ORDER BY COL1 OFFSET 10 ROWS

В этом примере возвращается первые 10 строк, упорядоченных по столбцу COL1:

SELECT * FROM T1 ORDER BY COL1 FETCH FIRST 10 ROWS ONLY

Использование предложений OFFSET и FETCH в производной таблице, результат которой ограничивается ещё раз во внешнем запросе.

SELECT * FROM (
        SELECT * FROM T1
        ORDER BY COL1 DESC 
        OFFSET 1 ROW FETCH NEXT 10 ROWS ONLY
    ) a
ORDER BY a.COL1 FETCH FIRST ROW ONLY

Выражение FROM

Выражение FROM определяет источники, из которых будут отобраны данные. В простейшей форме, это может быть единственная таблица или представление. Однако источниками также могут быть хранимая процедура, производная таблица или общее табличное выражение (CTE). Различные виды источников могут комбинироваться с использованием разнообразных видов соединений (JOIN).

Синтаксис использования:

SELECT ... FROM <источник> [[AS] <псевдоним>] [<joins>] [...]

где <источник>:

  • таблица
  • представление
  • селективная хранимая процедура
  • производная таблица
  • общее табличное выражение

При выборке из таблицы или представления предложение FROM не требует ничего кроме его имени. Псевдоним может быть полезен или даже необходим при использовании подзапросов, которые соотнесены с главным запросом.

Если вы дадите таблице или представлению псевдоним, то вы должны везде использовать этот псевдоним, а не имя таблицы, при обращении к именам столбцов.

Например, следующие запросы корректны:

SELECT LAST_NAME FROM CUSTOMER;
SELECT CUSTOMER.LAST_NAME FROM CUSTOMER;
SELECT LAST_NAME FROM CUSTOMER C;
SELECT C.LAST_NAME FROM CUSTOMER C;

А этот нет, т.к. таблице назначен псевдоним, а в списке выбора поле указано с использованием имени таблицы:

SELECT CUSTOMER.LAST_NAME FROM CUSTOMER C;

Селективная хранимая процедура — это объект БД, записанный на языке PSQL и возвращающий записи с одним или несколькими столбцами. Выходные параметры селективной хранимой процедуры с точки зрения команды SELECT соответствуют полям обычной таблицы.

Выборка из хранимой процедуры без входных параметров осуществляется точно так же, как обычная выборка из таблицы. Например,

SELECT name, az, alt 
    FROM visible_stars('Brugge', current_date, '22:30')
    WHERE alt >= 20

Производная таблица — это корректная команда SELECT, заключённая в круглые скобки, опционально обозначенная псевдонимом таблицы и псевдонимами полей.

Синтаксис использования производной таблицы:

(запрос) [[AS] <псевдоним производной таблицы>] [(<псевдоним столбца 1>, ...)]

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

Пример, демонстрирующий использование псевдонима производной таблицы и списка псевдонимов столбцов (оба опциональные):

SELECT DBINFO.DESCR, DBINFO.DEF_CHARSET 
    FROM (SELECT * FROM RDB$DATABASE) 
        DBINFO (DESCR, REL_ID, SEC_CLASS, DEF_CHARSET)
  • Производные таблицы могут быть вложенными;

  • Производные таблицы могут быть объединениями и использоваться в объединениях. Они могут содержать агрегатные функции, подзапросы и соединения, и сами по себе могут быть использованы в агрегатных функциях, подзапросах и соединениях. Они также могут быть хранимыми процедурами или запросами из них. Они могут иметь предложения WHERE, ORDER BY и GROUP BY, указания FIRST, SKIP или ROWS и т.д.;

  • Каждый столбец в производной таблице должен иметь имя. Если этого нет по своей природе (например, потому что это — константа), то надо в обычном порядке присвоить псевдоним или добавить список псевдонимов столбцов в спецификации производной таблицы;

  • Список псевдонимов столбцов опциональный, но если он присутствует, то должен быть полным (т.е. он должен содержать псевдоним для каждого столбца производной таблицы);

  • Оптимизатор может обрабатывать производные таблицы очень эффективно. Однако если производная таблица включена во внутреннее соединение и содержит подзапрос, то никакой порядок соединения не может быть использован оптимизатором.

Приведём пример того, как использование производных таблиц может упростить решение некоторой задачи.

Предположим, что у нас есть таблица COEFFS, которая содержит коэффициенты для ряда квадратных уравнений, которые мы собираемся решить. Она может быть определена примерно так:

CREATE TABLE coeffs (
    a DOUBLE PRECISION NOT NULL,
    b DOUBLE PRECISION NOT NULL,
    c DOUBLE PRECISION NOT NULL,
    CONSTRAINT chk_a_not_zero CHECK (a <> 0)
)

В зависимости от значений коэффициентов a, b и c, каждое уравнение может иметь ноль, одно или два решения. Мы можем найти эти решения с помощью одноуровневого запроса к таблице COEFFS, однако код такого запроса будет громоздким, а некоторые значения (такие, как дискриминанты) будут вычисляться несколько раз в каждой строке.

SELECT
    IIF ((B*B - 4*A*C) >= 0, (-B - SQRT(B*B - 4*A*C)) / (2*A), NULL) AS SOL_1,
    IIF ((B*B - 4*A*C) > 0, (-B + SQRT(B*B - 4*A*C)) / (2*A), NULL) AS SOL_2
FROM COEFFS

Если использовать производную таблицу, то запрос можно сделать гораздо более элегантным:

SELECT
    IIF (D >= 0, (-B - SQRT(D)) / DENOM, NULL) AS SOL_1,
    IIF (D > 0, (-B + SQRT(D)) / DENOM, NULL) AS SOL_2
FROM
    (SELECT B, B*B - 4*A*C, 2*A FROM COEFFS) (B, D, DENOM)

Если мы захотим показывать коэффициенты рядом с решениями уравнений, то мы можем модифицировать запрос следующим образом:

SELECT
    A, B, C,
    IIF (D >= 0, (-B - SQRT(D)) / DENOM, NULL) SOL_1,
    IIF (D > 0, (-B + SQRT(D)) / DENOM, NULL) SOL_2
FROM
    (SELECT A, B, C, B*B - 4*A*C AS D, 2*A AS DENOM FROM COEFFS)

Обратите внимание, что в первом запросе мы назначили псевдонимы для всех полей производной таблицы в виде списка после таблицы, а во втором, по мере необходимости, добавляем псевдонимы внутри запроса производной таблицы. Оба этих метода корректны, так как при правильном применении гарантируют, что каждое поле производной таблицы имеет уникальное имя.

Контрольные вопросы

  1. Как исключить дубликаты строк результата SELECT?
  2. Что может указываться в списке выбора?
  3. Каким образом можно ограничить выбираемые строки?
  4. Для чего применяются псевдонимы источник выборки?
  5. Что такое “производная таблица”?

Соединение наборов данных (JOIN)

Соединения объединяют данные из двух источников (которые обычно называют “левый” и “правый”) в один набор данных. Соединение данных осуществляется для каждой строки и обычно включает в себя проверку условия соединения (join condition) для того, чтобы определить, какие строки должны быть объединены и оказаться в результирующем наборе данных.

Результат соединения также может быть соединён с другим набором данных с помощью следующего соединения.

Существует несколько типов (INNER, OUTER) и классов (именованные, натуральные, и др.) соединений, каждый из которых имеет свой синтаксис и правила.

Для примеров мы будем использовать две таблицы:

Таблица PROGRAMMER

LANG_IDPNAME
1Андрей
2Леонид
1Сергей
4Григорий

Таблица LANG

LANG_IDLNAME
1C/C++
2Java
3Python

Общий синтаксис использования операторов соединения нескольких источников можно представить в следующем виде:

SELECT ... FROM <source1> <join1> <source2> <join2> <source3> ...

где <source>:

  • таблица
  • представление
  • селективная хранимая процедура, с возможными аргументами
  • производный запрос
  • общее табличное выражение

а <join>:

  • [INNER] JOIN {ON condition | USING (column-list)}
  • {LEFT | RIGHT | FULL} [OUTER] JOIN {ON condition | USING (column-list)}
  • NATURAL [INNER] JOIN
  • NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN
  • CROSS JOIN

Рассмотрим виды соединений более подробно и приведем примеры.

Внутреннее соединение (INNER JOIN)

Данный тип соединяет только строки, которые удовлетворяет условию соединения. Порядок соединения таблиц неважен.

Самый очевидный, но наиболее понятный алгоритм работы такого соединения заключается в том, что каждая строка левого источника сопоставляется с каждой строкой правого источника и происходит проверка условия. Если условие выполняется, то строки соединяются и включаются в результирующий набор. Такой алгоритм универсален, но не оптимален и существует множество других реализаций такого типа соединения. Оптимизатор СУБД выбирает наиболее подходящий в зависимости от множества факторов. Какую бы реализацию не выбрал оптимизатор, результат всегда должен быть одинаков.

Например, запрос

SELECT * FROM PROGRAMMER P INNER JOIN LANG L ON P.LANG_ID = L.LANG_ID

Выдаст результат

LANG_IDPNAMELANG_ID1LNAME
1Андрей1C/C++
2Леонид2Java
1Сергей1C/C++

Ключевое слово INNER является необязательным.

Обратите внимание, что для каждой записи возможны несколько подходящих строк из правого источника. Также обратите внимание, что поле LANG_ID фактические присутствует дважды: из левого источника и из правого. Если условие будет не простым равенством, значения в этих полях не обязательно будут совпадать.

Левое внешнее соединение (LEFT OUTER JOIN)

Оператор левого внешнего соединения LEFT OUTER JOIN соединяет две таблицы. Порядок таблиц для оператора важен.

В результат включается внутреннее соединение (INNER JOIN) левой и правой таблиц, а после добавляются те строки левой таблицы, которые не вошли во внутреннее соединение. Для таких строк столбцы, соответствующие правой таблице, заполняются значениями NULL.

Например, запрос

SELECT * FROM PROGRAMMER P LEFT OUTER JOIN LANG L ON P.LANG_ID = L.LANG_ID

Выдаст результат

LANG_IDPNAMELANG_ID1LNAME
1Андрей1C/C++
2Леонид2Java
1Сергей1C/C++
4ГригорийNULLNULL

Ключевое слово OUTER является необязательным.

Правое внешнее соединение (RIGHT OUTER JOIN)

Оператор правого внешнего соединения RIGHT OUTER JOIN соединяет две таблицы. Порядок таблиц для оператора важен.

В результат включается внутреннее соединение (INNER JOIN) левой и правой таблиц, а после добавляются те строки правой таблицы, которые не вошли во внутреннее соединение. Для таких строк столбцы, соответствующие левой таблице, заполняются значениями NULL.

Например, запрос

SELECT * FROM PROGRAMMER P RIGHT OUTER JOIN LANG L ON P.LANG_ID = L.LANG_ID

Выдаст результат

LANG_IDPNAMELANG_ID1LNAME
1Андрей1C/C++
2Леонид2Java
1Сергей1C/C++
NULLNULL3Python

Ключевое слово OUTER является необязательным.

Полное внешнее соединение (FULL OUTER JOIN)

Оператор полного внешнего соединения FULL OUTER JOIN соединяет две таблицы. Порядок таблиц для оператора неважен.

В результат включается внутреннее соединение (INNER JOIN) левой и правой таблиц. Затем добавляются те строки левой таблицы, которые не вошли во внутреннее соединение. Для таких строк столбцы, соответствующие правой таблице, заполняются значениями NULL. Затем добавляются те строки правой таблицы, которые не вошли во внутреннее соединение. Для таких строк столбцы, соответствующие левой таблице, заполняются значениями NULL.

Например, запрос

SELECT * FROM PROGRAMMER P FULL OUTER JOIN LANG L ON P.LANG_ID = L.LANG_ID

Выдаст результат

LANG_IDPNAMELANG_ID1LNAME
1Андрей1C/C++
2Леонид2Java
1Сергей1C/C++
4ГригорийNULLNULL
NULLNULL3Python

Ключевое слово OUTER является необязательным.

Явные условия соединения

В синтаксисе явного соединения есть предложение ON, с условием соединения, в котором может быть указано любое логическое выражение, но, как правило, оно содержит условие сравнения между двумя участвующими источниками. Довольно часто, это условие — проверка на равенство (или ряд проверок на равенство объединённых оператором AND) использующая оператор “=”.

Такие соединения называются эквисоединениями. Рассмотренные выше соединения являлись таковыми.

Еще примеры соединений с явными условиями:

Выборка всех заказчиков из города Муром, которые сделали покупку.

SELECT * FROM customers c
  JOIN sales s ON s.cust_id = c.id
  WHERE c.city = 'Муром'

Тоже самое, но включает в выборку заказчиков, которые не совершали покупки.

SELECT * FROM customers c
  LEFT JOIN sales s ON s.cust_id = c.id
  WHERE c.city = 'Муром'

Для каждого мужчины выбрать женщин, которые выше него. Мужчины, для которых такой женщины не существуют, не будут выключены в выборку.

SELECT m.fullname AS man, f.fullname AS woman
  FROM males m
  JOIN females f ON f.height > m.height

Соединение именованными столбцами

Эквисоединения часто сравнивают столбцы, которые имеют одно и то же имя в обеих таблицах. Для таких соединений мы можем использовать второй тип явных соединений, называемый соединением именованными столбцами (Named Columns Joins). Соединение именованными столбцами осуществляются с помощью предложения USING, в котором перечисляются только имена столбцов.

Таким образом, один из запросов выше

SELECT * FROM PROGRAMMER P INNER JOIN LANG L ON P.LANG_ID = L.LANG_ID

можно переписать следующим образом

SELECT * FROM PROGRAMMER P INNER JOIN LANG L USING (LANG_ID)

что значительно короче.

Результирующий набор несколько отличается, по крайней мере, при использовании SELECT *.

LANG_IDPNAMELNAME
1АндрейC/C++
2ЛеонидJava
1СергейC/C++
  • Результат соединения с явным условием соединения в предложении ON будет содержать каждый из столбцов дважды: один раз для левой таблицы и один раз для правой. Очевидно, что они будут иметь они и те же значения.
  • Результат соединения именованными столбцами, с помощью предложения USING, будет содержать эти столбцы один раз.

Если вы хотите получить в результате соединения именованными столбцами все столбцы, перепишите запрос следующим образом:

SELECT P.*, L.* FROM PROGRAMMER P 
  INNER JOIN LANG L USING (LANG_ID)

Естественное соединение (NATURAL JOIN)

Взяв за основу соединения именованными столбцами, следующим шагом будет естественное соединение, которое выполняет эквисоединение по всем одноименным столбцам правой и левой таблицы. Типы данных этих столбцов должны быть совместимыми. Важно помнить, что результат такого соединения может быть не предсказуем, поэтому его применение рекомендуется ограничить случаями, когда структура таблиц понятна и не изменяется.

Для нашего примера запрос,

SELECT * FROM PROGRAMMER P NATURAL JOIN LANG L

эквивалентен запросу

SELECT * FROM PROGRAMMER P  INNER JOIN LANG L USING (LANG_ID)

Легко заменить, что если бы поля PNAME и LNAME назывались просто NAME, то результат был бы совершенно другим и не вернул бы ни одной записи.

Как и все соединения, естественные соединения являются внутренними соединениями по умолчанию, но вы можете превратить их во внешние соединения, указав LEFT, RIGHT или FULL перед ключевым словом JOIN.

Если в двух исходных таблицах не будут найдены одноименные столбцы, то будет выполнен CROSS JOIN.

Неявное соединение

В стандарте SQL-89 таблицы, участвующие в соединении, задаются списком с разделяющими запятыми в предложении FROM. Условия соединения задаются в предложении WHERE среди других условий поиска. Такие соединения называются неявными.

Синтаксис неявного соединения может осуществлять только внутренние соединения.

Пример неявного соединения:

SELECT * FROM PROGRAMMER P, LANG L WHERE P.LANG_ID = LANG.LANG_ID

Фактически данный запрос внутри СУБД будет преобразован ко внутреннему соединению, по условию после WHERE.

SELECT * FROM PROGRAMMER P INNER JOIN LANG L ON P.LANG_ID = L.LANG_ID

В настоящее время неявные соединения не рекомендованы для использования.

Перекрестные соединения (CROSS JOIN)

Такое соединение еще называют декартовым произведением двух отношений. Каждая строка левой таблицы соединяется с каждой строкой правой таблицы. Условий при этом никаких нет. Можно добавить условия в части WHERE по аналогии с неявным соединением.

Порядок таблиц для оператора неважен.

Перекрёстное соединение двух наборов эквивалентно их соединению по условию тавтологии (условие, которое всегда верно).

Следующие два запроса дадут один и тот же результат:

SELECT * FROM PROGRAMMER CROSS JOIN LANG;
SELECT * FROM PROGRAMMER JOIN LANG ON 1 = 1;

Перекрёстные соединения являются внутренними соединениями, потому что они отбирают строки, для которых есть соответствие, а соответствие есть для каждой строки. Внешнее перекрёстное соединение, если бы оно существовало, ничего не добавило бы к результату.

Для нашего примера запрос

SELECT * FROM PROGRAMMER CROSS JOIN LANG

вернет результат

LANG_IDPNAMELANG_ID1LNAME
1Андрей1C/C++
1Андрей2Java
1Андрей3Python
2Леонид1C/C++
2Леонид2Java
2Леонид3Python
1Сергей1C/C++
1Сергей2Java
1Сергей3Python
4Григорий1C/C++
4Григорий2Java
4Григорий3Python

Перекрёстные соединения редко полезны, кроме случаев, когда вы хотите получить список всех возможных комбинаций двух или более переменных. Предположим, вы продаёте продукт, который поставляется в различных размерах, различных цветов и из различных материалов. Если для каждой переменной значения перечислены в собственной таблице, то этот запрос будет возвращать все комбинации:

SELECT
    m.name,
    s.size,
    c.name
FROM materials m
    CROSS JOIN sizes s
    CROSS JOIN colors c

Контрольные вопросы

  1. Какие типы соединений вы знаете?
  2. Какие классы соединений вы знаете?
  3. Можно ли левое соединение заменить правым и что для этого нужно сделать?
  4. Опишите простейший алгоритм работы внутреннего соединения?
  5. Что такое эквисоединения?
  6. Чем соединение именованными столбцами отличается от соответствующего эквисоединения?
  7. В чем опасность применения естественных соединений?
  8. Что такое неявные соединения?
  9. Что такое перекрестные соединения?

Фильтрация записей

Предложение WHERE

Предложение WHERE предназначено для ограничения количества возвращаемых строк, теми которые нас интересуют. Условие после ключевого слова WHERE может быть простым, как проверка “AMOUNT = 3”, так и сложным, запутанным выражением, содержащим подзапросы, предикаты, вызовы функций, математические и логические операторы, контекстные переменные и многое другое.

Условие в предложении WHERE часто называют условием поиска, выражением поиска или просто поиском. В DSQL выражение поиска может содержать параметры. Это полезно, если запрос должен быть повторен несколько раз с разными значениями входных параметров. В строке SQL запроса, передаваемого на сервер, вопросительные знаки используются как заполнители для параметров. Их называют позиционными параметрами, потому что они не могут сказать ничего кроме как о позиции в строке. Библиотеки доступа часто поддерживают именованные параметры в виде :id, :amount, :a и т.д. Это более удобно для пользователя, библиотека заботится о трансляции именованных параметров в позиционные параметры, прежде чем передать запрос на сервер.

Синтаксис:

SELECT ... FROM ... WHERE <условие поиска>

<условие поиска> - логическое выражение возвращающее TRUE, FALSE и возможно UNKNOWN (NULL).

Только те строки, для которых условие поиска истинно, будут включены в результирующий набор. Будьте осторожны с возможными получаемыми значениями NULL: если вы отрицаете выражение, дающее NULL с помощью NOT, то результат такого выражения все равно будет NULL и строка не пройдёт.

Примеры:

SELECT PROJ_NAME FROM PROJECT WHERE PRODUCT='software';

SELECT * FROM SALES WHERE ORDER_DATE BETWEEN '1992-01-01' AND '1993-01-01';

SELECT * FROM EMPLOYEES WHERE SALARY >= 10000 AND POSITION <> 'MANAGER';

SELECT PROJ_NAME FROM PROJECT P WHERE EXISTS (SELECT * FROM PROJ_DEPT_BUDGET B WHERE P.PROJ_ID=B.PROJ_ID AND B.PROJECTED_BUDGET > 200000);

SELECT FULL_NAME FROM EMPLOYEE WHERE JOB_COUNTRY ='England' AND SALARY > ALL (SELECT SALARY FROM EMPLOYEE WHERE JOB_COUNTRY = 'Canada');

SELECT * FROM CUSTOMER WHERE CUSTOMER LIKE '%Inc%' AND CITY CONTAINING 'san';

SELECT SEC$DESCRIPTION FROM SEC$USERS WHERE SEC$USER_NAME = CURRENT_USER;

Следующий пример показывает, что может быть, если условие поиска вычисляется как NULL. Предположим у вас есть таблица, в которой находятся несколько детских имён и количество шариков, которыми они обладают.

Таблица MARBLETABLE

CHILDMARBLES
Маша5
Миша15
ТаняNULL
Ваня0
Коля7
Даша17

Первое, обратите внимание на разницу между NULL и 0. Известно, что Ваня не имеет шариков вовсе, однако неизвестно количество шариков у Тани.

Теперь, если ввести этот SQL оператор:

SELECT CHILD FROM MARBLETABLE WHERE MARBLES > 10

запрос вернет

CHILDMARBLES
Миша15
Даша17

Если добавить отрицание NOT:

SELECT CHILD FROM MARBLETABLE WHERE NOT MARBLES > 10

запрос вернёт

CHILDMARBLES
Маша5
Ваня0
Даша17

Таня не попадет ни в одну из выборок, т.к. не известно сколько у нее шариков. Если изменить последний запрос так:

SELECT CHILD FROM MARBLETABLE WHERE MARBLES <= 100

Таня все равно не попадет в результат поскольку выражение NULL <= 100 даёт UNKNOWN. Это не тоже самое что TRUE, поэтому Таня не отображена. Если предполагается что NULL равносильно 0, то запрос можно изменить следующим образом:

SELECT CHILD FROM MARBLETABLE WHERE MARBLES <= 10 OR MARBLES IS NULL

Теперь условие поиска становится истинным для Тани, потому что условие MARBLES is NULL возвращает TRUE в этом случае.

Рассмотрим возможности для построения выражений более подробно.

Выражения

Выражения SQL представляют формальные методы для вычисления, преобразования и сравнения значений. Выражения SQL могут включать в себя столбцы таблиц, переменные, константы, литералы, различные операторы и предикаты, а так же другие выражения.

Подзапросы

Подзапрос — это специальный вид выражения, которое фактически является запросом SELECT к другой таблице, включённый в спецификацию основного запроса. Подзапросы пишутся как обычные SELECT запросы, но должны быть заключены в круглые скобки.

Подзапрос может быть коррелированным, когда оба, и внутренний, и внешний, запросы взаимозависимы. Это означает, что для обработки каждой записи внутреннего запроса, должна быть получена также запись внешнего запроса, т.е. внутренний запрос всецело зависит от внешнего.

SELECT * FROM CUSTOMERS C
  WHERE EXISTS
        (SELECT * FROM ORDERS O
          WHERE C.CNUM = O.CNUM
          AND O.ADATE = DATE '10.03.1990');

Подзапросы, используемые в предикатах поиска, кроме предикатов существования и количественных предикатов, должны возвращать скалярный результат, то есть не более чем один столбец из одной отобранной строки или одно агрегированное значение, в противном случае, произойдёт ошибка времени выполнения ("Multiple rows in a singleton select...").

SELECT
    E.FIRST_NAME,
    E.LAST_NAME,
    E.SALARY
FROM EMPLOYEE E
WHERE
    E.SALARY = (SELECT MAX(IE.SALARY)
                FROM EMPLOYEE IE)

Предикаты

Предикат — это простое выражение, утверждающее некоторый факт. Предикат может быть истинным (TRUE), ложным (FALSE) и неопределённым (UNKNOWN). В SQL ложный и неопределённый результаты трактуются как ложь.

В SQL предикаты проверяют в ограничении CHECK, предложении WHERE, выражении CASE, условии соединения во фразе ON для предложений JOIN, а также в предложении HAVING. В PSQL операторы управления потоком выполнения проверяют предикаты в предложениях IF, WHILE и WHEN.

Проверяемые условия могут состоять из одного или нескольких предикатов, связанных логическими операторами AND, OR и NOT.

Предикат сравнения представляет собой два выражения, соединяемых оператором сравнения. К операторам сравнения относятся традиционные операторы (< , > , =, ...), а также другие перечисленные далее операторы.

Оператор BETWEEN проверяет, попадает (или не попадает при использовании NOT) ли значение во включающий диапазон значений (включая границы).

<значение> [NOT] BETWEEN <значение 1> AND <значение 2>

Условие будет истинным, если значение присутствует в указанном диапазоне (от <значение 1> до <значение 2> включительно) при отсутствии ключевого слова NOT. При наличии ключевого слова NOT условие будет истинным, если значение отсутствует в указанном диапазоне, включая граничные значения.

Оператор BETWEEN использует два аргумента совместимых типов. В отличие от некоторых других СУБД в Ред Базе Данных оператор BETWEEN не является симметричным. Меньшее значение должно быть первым аргументом, иначе предикат BETWEEN всегда будет ложным.

Например, если в таблице сотрудников EMPLOYEES нужно выбрать только тех сотрудников, которые имеют оклады (столбец SALARY) в соответствующем диапазоне, включая граничные значения, то следует выполнить следующий оператор:

SELECT * FROM EMPLOYEES WHERE SALARY BETWEEN 10000 AND 20000;

Предикат LIKE сравнивает выражение символьного типа с шаблоном.

<значение> [NOT] LIKE <шаблон> [ESCAPE <символ экранирования>]

Этот оператор является чувствительным к регистру (за исключением случаев, когда само поле определено с сортировкой (COLLATION) нечувствительной к регистру).

В шаблоне можно указать трафаретные символы % и _. Символ процента задает произвольное количество, в том числе и нулевое, любых символов. Знак подчеркивания задает ровно один любой символ.

SELECT DEPT_NO FROM DEPT WHERE DEPT_NAME LIKE 'Software%' ;

Необязательное ключевое слово ESCAPE позволяет в шаблон включить и сами трафаретные символы % и _. После ESCAPE нужно указать символ, который должен предшествовать в шаблоне трафаретному символу, когда такой трафаретный символ должен рассматриваться как обычный символ, присутствующий в строке.

SELECT RDB$RELATION_NAME FROM RDB$RELATIONS
  WHERE RDB$RELATION_NAME LIKE '%#_%'' ESCAPE '#'

Для того, чтобы в строке также можно было использовать обычным образом и символ, заданный после ключевого слова ESCAPE, необходимо задать его в этой строке дважды.

Оператор STARTING WITH ищет строку, которая начинается с символов в его аргументе.

<значение> [NOT] STARTING [WITH] <значение 1>

Оператор чувствителен к регистру, однако такое ограничение также можно легко обойти, используя функцию UPPER или функцию LOWER.

При использовании предиката STARTING WITH в поисковых условиях DML запросов, оптимизатор может использовать индекс по искомому столбцу, если он определён.

SELECT LAST_NAME, FIRST_NAME FROM EMPLOYEE
  WHERE LAST_NAME STARTING WITH 'Jo';

Оператор CONTAINING ищет строку, отыскивая последовательность символов, которая соответствует его аргументу.

<значение> [NOT] CONTAINING <значение 1>

Оператор может быть использован для алфавитно-цифрового (подобного строковому) поиска в числах и датах.

Поиск CONTAINING не чувствителен к регистру.

При использовании оператора CONTAINING во внимание принимаются все символы строки. Это касается так же начальных и конечных пробелов. Если операция сравнения в запросе должна вернуть все строки, содержащие строки CONTAINING 'абв ' (с символом пробела на конце), то строка, содержащая 'абв' (без пробела), не будет возвращена.

При использовании предиката CONTAINING в поисковых условиях DML запросов, оптимизатор не может использовать индекс по искомому столбцу.

SELECT * FROM PROJECT
  WHERE PROJ_NAME CONTAINING 'map';

Оператор SIMILAR TO используется для сопоставления строк с шаблоном на основе регулярных выражений. Он похож на оператор LIKE, но предоставляет более мощные возможности, так как поддерживает синтаксис регулярных выражений в стиле стандарта SQL.

По умолчанию SIMILAR TO регистрозависим. Для регистронезависимого поиска можно использовать функции, такие как LOWER() или UPPER().

SIMILAR TO использует символы подстановки, такие как % (любое количество символов) и _ (один символ), как в LIKE. Также поддерживаются регулярные выражения.

  • [] — класс символов.

Символы, заключенные в квадратные скобки [], определяют класс символов. Если символ в строке соответствует классу, то символ является элементом класса. Причем классу соответствует единственный символ строки.

Два символа, соединенные дефисом -, в определении класса определяют диапазон. Диапазон для сопоставления включает в себя эти два конечных символа и все символы, находящиеся между ними

'Class' SIMILAR TO 'Cla[o-y]s',   -- <true>
'Class' SIMILAR TO 'C[la]ss',     -- <false>
'Class' SIMILAR TO 'C[abd-sx]ass' -- <true>

Если определение класса запускается со знаком вставки ^, то все, что следует за ним, исключается из класса. Все остальные символы проверяются. Если знак вставки ^ находится не в начале последовательности, то класс включает в себя все символы до него и исключает символы после него.

'Error' SIMILAR TO 'Er[^a-g]or',      -- <true>
'Error' SIMILAR TO 'Err[^e-p][^a-g]', -- <false>
'Error' SIMILAR TO 'Er[wrt^a-d]or'    -- <true>
  • *, +, ? — кванторы.

Квантор после символа, символьного класса или группы определяет, сколько раз предшествующее выражение может встречаться. Вопросительный знак ? сразу после символа, класса или группы указывает на то, что для соответствия предыдущий элемент может встретиться 0 или 1 раз. Звёздочка * сразу после символа, класса или группы указывает на то, что для соответствия предыдущий элемент может встретиться 0 или более раз. Знак плюс + сразу после символа, класса или группы указывает на то, что для соответствия предыдущий элемент может встретиться 1 или более раз.

'Question' SIMILAR TO 'Questt?ion',  -- <true>
'Asterisk' SIMILAR TO 'Ast[c-s]*sk', -- <true>
'Plus' SIMILAR TO 'Plus[[:DIGIT:]]+' -- <false>

Если символ или класс сопровождаются числом, заключённым в фигурные скобки {n}, то для соответствия нужно повторение элемента точно это число раз. Если число сопровождается запятой {n,}, то для соответствия нужно повторение элемента как минимум это число раз. Если фигурные скобки содержат два числа {m,n} и второе число больше первого, то для соответствия элемент должен быть повторен как минимум m раз и не больше n раз.

'Braces' SIMILAR TO 'Bra{2}ces',      -- <false>
'Braces' SIMILAR TO 'Bra[aceg]{2,}s', -- <true>
'Braces' SIMILAR TO 'Br[aceg]{1,2}s'  -- <false>
  • | — логическое ИЛИ.

В условиях регулярных выражений можно использовать оператор ИЛИ |. Соответствие произошло, если строка параметра соответствует по крайней мере одному из условий.

'Condition' SIMILAR TO 'Condi|tion',              -- <false>
'Condition' SIMILAR TO 'Condition|Statement',     -- <true>
'Condition' SIMILAR TO 'Condi_+|Kondi_+|Ckondi_+' -- <true>
  • () — группировка.

Одна или более частей регулярного выражения могут быть сгруппированы в подвыражения. Для этого их нужно заключить в круглые скобки

'Groups' SIMILAR TO 'G(ru|ro|ra)ups' -- <true>

Для исключения из процесса сопоставления специальных символов (которые используются для записи самих регулярных выражений) их надо экранировать. Специальных символов экранирования по умолчанию нет — их при необходимости определяет пользователь.

'Russia(RU)' SIMILAR TO 'R[a-z]+\(R[A-Z]+\)' ESCAPE '\', -- <true>
'France[FR]' SIMILAR TO 'Fr[a-z]+#[F%#]' ESCAPE '#',     -- <true>
'Puerto-Rico' SIMILAR TO 'P%$-R%' ESCAPE '$'             -- <true>

Рассмотрим еще несколько примеров.

Найдем все строки, которые начинаются с “А” и заканчиваются на “й”:

SELECT * FROM users WHERE name SIMILAR TO 'А%й';

Найдем имена, которые начинаются на “А” или “М”:

SELECT * FROM users WHERE name SIMILAR TO 'А%|М%';

Найдем имена, которые начинаются с “А”, “Б” или “В”:

SELECT * FROM users WHERE name SIMILAR TO '[АБВ]%';

Найдем имена, в которых есть две буквы “а” подряд:

SELECT * FROM users WHERE name SIMILAR TO '%а+а%';

Найдем имена, которые начинаются с “Ан” или “Ма”:

SELECT * FROM users WHERE name SIMILAR TO '(Ан|Ма)%';

Оператор IS DISTINCT FROM используется для проверки на неравенство (равенство, если задано NOT) двух значений.

<значение 1> IS [NOT] DISTINCT FROM <значение 2>

Два операнда считают различными (DISTINCT), если они имеют различные значения, или если одно из них — NULL, и другое нет. Они считаются равными (NOT DISTINCT), если имеют одинаковые значения или оба имеют значение NULL.

SELECT ID, NAME, TEACHER FROM COURSES
  WHERE START_DAY IS NOT DISTINCT FROM END_DAY;

В отличие от операторов равно (=) и не равно (!=, <>) этот оператор трактует два сравниваемых пустых значения NULL как равные друг другу. Как и в случае оператора IS [NOT] NULL данный оператор всегда возвращает либо TRUE, либо FALSE.

Таблица истинности для разных операторов представлена в таблице.

=IS NOT DISTINCT FROM!=, <>IS DISTINCT FROM
Одинаковые значенияTRUETRUEFALSEFALSE
Различные значенияFALSEFALSETRUETRUE
Оба NULLUNKNOWNTRUEUNKNOWNFALSE
Одно NULLUNKNOWNFALSEUNKNOWNTRUE

Оператор IS проверяет, что выражение в левой части является псевдозначением NULL или соответствует логическому значению в правой части.

<значение> IS [NOT] {TRUE | FALSE | UNKNOWN | NULL}

Если в правой части предиката использованы литерал TRUE, FALSE или UNKNOWN, то выражение в левой части должно быть логического типа, иначе будет выдана ошибка.

Оператор может вернуть только истинное значение TRUE или ложное FALSE, значение UNKNOWN невозможно.

Предикаты существования

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

Предикат EXISTS используется, чтобы определить, что результат подзапроса содержит хотя бы одну запись. В таком случае предикат EXISTS возвращает TRUE, а иначе FALSE.

[NOT] EXISTS (<оператор SELECT>)

Аргументом предиката EXISTS является оператор SELECT, возвращающий произвольное количество любых столбцов таблицы.

SELECT * FROM employee 
  WHERE NOT EXISTS (SELECT * FROM employee_project ep
                      WHERE ep.emp_no = employee.emp_no);

Предикат IN проверяет, присутствует ли значение выражения в указанном справа наборе значений.

<значение> [NOT] IN (<n1, n2, ... ,nN>)

Например,

SELECT * FROM EMPLOYEE WHERE FIRST_NAME IN ('Pete', 'Ann', 'Roger');

SELECT * FROM EMPLOYEE  WHERE FIRST_NAME = 'Pete' OR 
                              FIRST_NAME =  'Ann' OR 
                              FIRST_NAME =  'Roger';

Во второй форме предикат IN проверяет, присутствует (или отсутствует, при использовании NOT IN) ли значение выражения слева в результате выполнения подзапроса справа. Результат подзапроса должен содержать только один столбец, иначе будет выдана ошибка "count of column list and variable list do not match".

Например,

SELECT model, speed, hd FROM PC
  WHERE model IN (SELECT model FROM product WHERE maker = 'A');

Предикат SINGULAR использует подзапрос в качестве аргумента и оценивает его как истинный, если подзапрос возвращает одну и только одну строку результата, в противном случае предикат оценивается как ложный.

[NOT] SINGULAR (<оператор SELECT>)

Аргументом предиката SINGULAR является оператор SELECT, возвращающий произвольное количество любых столбцов таблицы (обычно это *). Данный предикат может возвращать только два значения: TRUE или FALSE.

Квантором называется логический оператор, задающий количество объектов, для которых данное утверждение истинно. Это логическое количество, а не числовое; оно связывает утверждение с полным множеством возможных объектов.

При использовании квантора ALL, предикат является истинным, если каждое значение выбранное подзапросом удовлетворяет условию.

<значение> <оператор сравнения> ALL (<оператор SELECT>)

Здесь используется операторы сравнения, описанные выше в этом разделе, а также оператор IS [NOT] DISTINCT FROM.

Если подзапрос не возвращает ни одной строки, то предикат автоматически считается верным.

SELECT * FROM CUSTOMERS 
  WHERE RATING > ALL (SELECT RATING
                    FROM CUSTOMERS
                    WHERE CITY = 'PARIS')

При использовании кванторов SOME или ANY, предикат является истинным, если хотя бы одно значение выбранное подзапросом удовлетворяет условию.

<значение> <оператор сравнения> { SOME | ANY } (<оператор SELECT>)

Ключевые слова SOME и ANY являются синонимами. Если подзапрос не возвращает ни одной строки, то предикат автоматически считается ложным.

Контрольные вопросы

  1. Как можно ограничить количество возвращаемых строк?
  2. Что произойдет, если условие поиска вернет NULL?
  3. Каким образом в выражении можно использовать результат SQL запроса?
  4. Какие операторы могут быть использованы в предикатах сравнения?
  5. Какие трафаретные символы применяются в операторе LIKE?
  6. Что делать, если трафаретный символ нужно использовать как обычный?
  7. Как обойти ограничение операторов, которые чувствительны к регистру?
  8. Для чего служат кванторы в операторе SIMILAR TO?
  9. Как указываются классы символов оператора SIMILAR TO?
  10. В чем заключается различие оператора IS DISTINCT FROM и оператора неравенства?
  11. Что является результатом предиката SINGULAR?
  12. В различие квантора ALL и ANY?
  13. В различие квантора SOME и ANY?

Встроенные функции

В этой главе мы рассмотрим некоторые, наиболее широко используемые встроенные скалярные функции, которые могут использоваться в выражениях для фильтрации записей, в списках выбора, в выражениях построения индекса и так далее.

Функции для работы с контекстными переменными

Контекстные переменные позволяют сохранять или получать множество разных значений, как системных, так и специально созданных для нужд конкретной базы данных.

Контекстные переменные принадлежат различным пространствам имен.

Функция RDB$GET_CONTEXT возвращает значение типа VARCHAR контекстной переменной одного из пространств имен:

  • SYSTEM — предоставляет доступ к системным контекстным переменным. Эти переменные доступны только для чтения;
  • USER_SESSION — предоставляет доступ к пользовательским контекстным переменным, заданным через функцию RDB$SET_CONTEXT. Переменные существуют в течение подключения;
  • USER_TRANSACTION — предоставляет доступ к пользовательским контекстным переменным, заданным через функцию RDB$SET_CONTEXT. Переменные существуют в течение транзакции;
  • DDL_TRIGGER — предоставляет доступ к системным контекстным переменным, доступным только во время выполнения DDL триггера. Эти переменные доступны только для чтения;
  • AUTHDATA — предоставляет доступ к информации об аутентификации и ФИО пользователя.

Длина возвращаемого значения определяется исходя из размера фактических данных. По умолчанию используется VARCHAR(8192).

RDB$GET_CONTEXT('пространство имен', '<имя переменной>')

Пространство имен и имя переменной чувствительны к регистру, должны быть непустыми строками и заключены в кавычки.

Пространства имен USER_SESSION и USER_TRANSACTION изначально пусты. Пользователь может создать и установить значение переменных в них функцией RDB$SET_CONTEXT и получить их значения из функции RDB$GET_CONTEXT.

Пространство имен SYSTEM доступно только для чтения. Оно содержит много предопределенных переменных. Полный список можно найти в документации. Рассмотрим некоторые из них.

Имя переменнойОписание
FULL_VERSIONПолная версия сборки СУБД (например, WI-V3.0.11.0 RedDatabase 5.0 SNAPSHOT.16 (9ec7320661241a96270a45741e9aae609d024ade))
EDITIONУстановленная редакция СУБД Ред База Данных: Open, Standard или Enterprise
DB_NAMEПолный путь к базе данных
CLIENT_ADDRESSДля TCPv4 — IP адрес, для XNET — локальный ID процесса. Для всех остальных протоколов переменная имеет значение NULL
CLIENT_PROCESSПолный путь к клиентскому приложению, подключившемуся к базе данных. Позволяет не использовать системную таблицу MON$ATTACHMENTS (поле MON$REMOTE_PROCESS)
CURRENT_USERГлобальная переменная CURRENT_USER
CURRENT_ROLEГлобальная переменная CURRENT_ROLE
SESSION_TIMEZONEЧасовой пояс текущего соединения.

Обращение к несуществующему пространству имён или несуществующей переменной в пространстве имен SYSTEM приведёт к ошибке, а в пространстве имен USER_SESSION и USER_TRANSACTION, функция вернёт NULL.

Использование пространства имён DDL_TRIGGER допустимо, только во время работы DDL триггера. Его использование также допустимо в хранимых процедурах и функциях, вызванных триггерами DDL.

Контекст DDL_TRIGGER работает как стек. Перед вызовом DDL триггера, значения, относящиеся к выполняемой команде, помещаются в этот стек. После завершения работы триггера значения выталкиваются. Таким образом, значения переменных в пространстве имён DDL_TRIGGER будут соответствовать команде, которая вызвала последний DDL триггер в стеке вызовов.

Функция RDB$SET_CONTEXT создает переменную, устанавливает ее значение или обнуляет в одном из используемых пользователями для записи пространстве имён: USER_SESSION, USER_TRANSACTION.

В рамках одного соединения может быть максимум 1000 переменных.

RDB$SET_CONTEXT ('USER_SESSION' | 'USER_TRANSACTION', '<имя переменной>', '<значение переменной>' | NULL)

Параметр <имя переменной> — это строка, чувствительная к регистру. Параметр <значение переменной> — значение любого типа, приводимое к типу VARCHAR.

Функция возвращает только два значения типа INTEGER: 1 — если переменная уже существовала и 0 — если не существовала.

Для удаления переменной надо установить её значение в NULL. Если данное пространство имен не существует, то функция вернёт ошибку.

Математические функции

В SQL доступны обычные математическая функции, смысл которых такой же как и в любом языке программирования. Перечислим их в виде таблицы с указанием некоторых особенностей. Для обработки NULL на входе, многие возвращают также NULL. Если не указано иное.

Имя функцииЗначениеОсобенности
ABS(x)Модуль числаТип результата как у x. Возвращает 0, если x NULL.
SIN(x)Синус
COS(x)Косинусx в радианах
TAN(x)Тангенс x
COT(x)Котангенс x
ACOS(x)Арккосинус-1<=x<=1. Результат — угол в радианах.
ASIN(x)Арксинус-1<=x<=1. Результат — угол в радианах.
ATAN(x)АрктангенсРезультат — угол в радианах.
CEIL(x)Наименьшее целое больше x
FLOOR(x)Наибольшее целое меньше x
LN(x)Натуральный логарифм x
LOG(x, b)Логарифм x по основанию b
LOG10(x)Десятичный логарифм
MOD(x, y)Остаток от деления x на y
PI()Число Pi
POWER(x, y)Возводит x в степень y
RAND()Случайное число от 0 до 1Результат DOUBLE PRECISION
ROUND(x, p)Округление числа с точностью p
SQRT(x)Квадратный корень x

Функции для работы со строками

ASCII_CHAR

Возвращает символ ASCII по его коду.

ASCII_CHAR(<числовое значение>)

Входной параметр может принимать значения в диапазоне от 0 до 255, иначе выдается сообщение об арифметическом переполнении. Если входной параметр имеет значение NULL, то возвращается пустое значение NULL.

ASCII_VAL

Возвращает число, соответствующее коду ASCII символа заданного строкового параметра.

ASCII_VAL(<строка>)

Возвращается NULL, если входной параметр имеет значение NULL. Возвращается 0, если входной параметр является строкой, не содержащей ни одного символа.

OCTET_LENGTH

Возвращает количество байтов, занимаемых входным параметром функции.

OCTET_LENGTH(<строка>)

Следует помнить, что не во всех наборах символов количество байт, занимаемых строкой, равно количеству символов.

CHARACTER_LENGTH

Функция CHARACTER_LENGTH (сокращенное название CHAR_LENGTH) возвращает количество символов, занимаемых входным параметром функции.

CHARACTER_LENGTH_LENGTH(<строка>)

Для строки функция возвращает именно количество символов, а не байтов, отводимых под исходную строку. Если функция применяется к столбцу, который имеет набор символов, в котором для каждого символа используется более одного байта, то количество байтов этого столбца (функция OCTET_LENGTH) будет больше, чем количество символов.

LOWER/UPPER

Переводит все буквы строки в нижний/верхний регистр.

LOWER (<строка>)
UPPER(<строка>)

Если исходное значение не содержит букв, то будет возвращаться само исходное значение.

Точный результат зависит от набора символов входной строки. Например, для наборов символов NONE и ASCII только ASCII символы переводятся в нижний регистр; для OCTETS — вся входная строка возвращается без изменений.

POSITION

Отыскивает позицию подстроки в исходной строке. Существует два варианта синтаксиса:

POSITION(<подстрока> IN <строка>)
POSITION(<подстрока>, <строка> [, <начальная позиция> [, <номер вхождения подстроки>]])

Функция возвращает целое число — позицию подстроки в исходной строке. Если подстрока отсутствует в исходной строке, то функция возвращает 0.

Третий аргумент (во втором варианте синтаксиса) задаёт позицию в строке, с которой начинается поиск подстроки, тем самым игнорируются любые вхождения подстроки в исходную строку до этой позиции.

Четвёртый аргумент определяет, какое по счету вхождение подстроки нужно искать. Если аргумент не задан, то по умолчанию равен 1, то есть результатом будет позиция первого вхождения подстроки в строку.

REPLACE

Отыскивает подстроку в исходной строке и заменяет на другую.

REPLACE(<исходная строка>, <подстрока>, <строка замены>)

Функция выполняет замену в исходной строке всех найденных подстрок на заданную третьим параметром строку замены.

Если любой из аргументов равен NULL, то результатом всегда будет NULL, даже если не было произведено ни одной замены

SUBSTRING

Функция SUBSTRING возвращает подстроку исходной строки.

SUBSTRING( <строка> FROM <начальная позиция> [FOR <длина подстроки>])

Начальная позиция — номер позиции в строке, начиная с которой выделяется подстрока. Нумерация символов в строке начинается с единицы. Если начальная позиция подстроки превышает количество символов в строке, то будет выделена пустая подстрока, т.е. строка, содержащая ноль символов.

Длина подстроки — количество символов, которые выбираются в результирующую строку. Должно быть положительным числом. Если задать количество символов, которое выходит за границы исходной строки, то результат будет усечен до размера, соответствующего положению последнего символа исходной строки. При этом не будет выдано никаких диагностических сообщений. Если ключевое слово FOR не указано, то в подстроку помещаются все оставшиеся до конца исходной строки символы.

TRIM

Встроенная функция TRIM удаляет начальные и/или конечные указанные символы (по умолчанию пробелы) в исходной строке, передаваемой функции в виде входного параметра.

TRIM ([LEADING | TRAILING | BOTH] [<удаляемые символы>] FROM <строка>)
TRIM (<строка>)

Внутри функции указывается спецификатор удаления и он может иметь следующие значения:

  • LEADING — символы удаляются из начальной части строки.
  • TRAILING — удаляются конечные символы строки.
  • BOTH (значение по умолчанию) — символы одновременно удаляются как из начальной, так и из конечной части стоки.

Удаляемые символы — строка, содержащая произвольное количество символов. Эта строка заключается в апострофы. Если параметр опущен, предполагаются пробелы.

Строкой в функции может быть строковый столбец, домен, которому был задан строковый тип данных, строковый литерал, заключенный в апострофы, входной или выходной параметр хранимой процедуры, локальная переменная строкового типа данных.

Например,

select TRIM ('  Руководство ' || 'по SQL  ') from rdb$database;

вернет Руководство по SQL. Здесь по умолчанию убираются символы пробелов.

TRIM (LEADING '*' FROM '***********Руководство ' || 'по SQL*******')

вернет Руководство по SQL*******. В результате выполнения функции будут удалены только начальные символы “*”.

TRIM (TRAILING '*' FROM '***********Руководство ' || 'по SQL*******')

вернет ***********Руководство по SQL. В результате выполнения функции будут удалены только конечные символы “*”.

TRIM (BOTH '*' FROM '***********Руководство ' || 'по SQL*******')

вернет Руководство по SQL. В результате выполнения функции будут удалены все символы “*”.

Ключевое слово BOTH можно не задавать. В этом случае удаляются указанные символы как с начала, так и с конца строки.

Функции для работы с датой и временем

DATEADD

Возвращает значение типа данных DATE, TIME или TIMESTAMP в зависимости от типа данных входного параметра. Возвращаемое значение параметра увеличивается (уменьшается, если задано отрицательное значение параметра) на соответствующее количество элементов (секунд, миллисекунд, минут, часов, дней, месяцев, лет), заданных параметром . У функции есть два формата.

DATEADD(<количество> <элемент даты/времени> TO <входной параметр>)
DATEADD(<элемент даты/времени>, <количество>, <входной параметр>)

Элемент даты/времени - это YEAR, MONTH, WEEK, DAY, WEEKDAY, YEARDAY, HOUR, MINUTE, SECOND, MILLISECOND.

С типом данных, содержащим только время, не могут использоваться элементы, относящиеся к дате, с типом данных DATE не могут использоваться элементы времени. Для типа данных TIMESTAMP допустимы любые варианты.

При задании элементов, указывающих месяц, день, час, минуту, секунду или миллисекунду, происходит естественное изменение значений всех вышележащих элементов, составляющих дату и время. Например, вызов следующей функции вернет дату и время на 10 лет ранее текущей даты.

DATEADD(SECOND, -60*60*24*365*10, CURRENT_TIMESTAMP)

DATEDIFF

Возвращает целое число, задающее интервал в соответствии с указанным выделяемым элементом между двумя значениями типа данных DATE, TIME или TIMESTAMP. У функции есть два формата.

DATEDIFF(<элемент даты/времени> FROM <параметр 1> TO <параметр 2>)
DATEDIFF(<элемент даты/времени>, <параметр 1>, <параметр 2>)

Например, чтобы определить, сколько лет осталось до 2050 года, нужно выполнить функцию:

DATEDIFF (YEAR, CURRENT_DATE, CAST('01.01.2050' AS DATE))

EXTRACT

Функция позволяет выделять различные элементы даты и времени.

EXTRACT (<выделяемый элемент> FROM <дата>)

В следующем операторе из переменной DATE_C типа DATE выделяются день, месяц и год. Полученные данные при помощи операции конкатенации приводятся к виду, принятому в нашей стране:

EXTRACT (DAY FROM DATE_C) || '.' ||
EXTRACT (MONTH FROM DATE_C) || '.' ||
EXTRACT (YEAR FROM DATE_C)

Функции преобразования типов

CAST

Функция CAST позволяет преобразовывать исходные данные из одного типа данных в другой, допустимый для исходного значения.

CAST ({<значение> | NULL} AS <тип данных>)

Преобразование NULL в любой тип данных всегда дает тот же NULL.

В целочисленные типы данных (SMALLINT, INTEGER, BIGINT) можно выполнять преобразование числовых данных и констант с фиксированной точкой (DECIMAL, NUMERIC), с плавающей точкой (FLOAT, DOUBLE PRECISION), данных текстового BLOB и строковых данных (CHAR, VARCHAR, NCHAR и NCHAR VARYING), содержащих только цифры и десятичную точку.

В дробные числа с фиксированной точкой (DECIMAL, NUMERIC) можно преобразовывать все целочисленные данные и данные с фиксированной или плавающей точкой, данные типа BLOB подтипа TEXT, а также строки, содержащие данные, по форме соответствующие числам.

В строковые типы данных (CHAR, VARCHAR, NCHAR и NCHAR VARYING) можно преобразовывать любой тип данных. Необходимо лишь указать размер строкового типа, достаточный для того, чтобы в него поместился результат преобразования.

В типы данных DATE, TIME и TIMESTAMP можно преобразовать любую строку, содержащую дату в одном из допустимых форматов.

Функции побитовых операций

Выходные и выходные параметры побитовых операций - целые числа. Если любой из входных параметров имеет значение NULL, то возвращается также NULL.

Функции побитовых операций представлены в таблице.

Название функцииОписание
BIN_AND(x1,x2,…)Логическое И
BIN_NOT(x)Логическое отрицание
BIN_OR(x1,x2,…)Логическое ИЛИ
BIN_SHL(x, n)Сдвиг влево двоичных знаков x на n
BIN_SHR(x, n)Сдвиг вправо двоичных знаков x на n
BIN_XOR(x1,x2,…)Исключающее ИЛИ

Функции для работы с UUID

CHAR_TO_UUID

Данная функция преобразует переданное в качестве параметра 32-х символьное ASCII представление UUID (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) в восьмеричное представление, оптимизированное для хранения.

CHAR_TO_UUID(<string>)

Например, запрос

select CHAR_TO_UUID('93519227-8D50-4E47-81AA-8F6678C096A1') from rdb$database

вернет 935192278D504E4781AA8F6678C096A1

GEN_UUID

Позволяет получить уникальное символьное значение в текущей базе данных, не повторяющееся ни при каких обращениях к этой функции.

GEN_UUID()

Возвращаемое значение содержит 16 символов.

UUID_TO_CHAR

Данная функция преобразует переданное в качестве параметра восьмеричное представление UUID CHAR(16) в 32-х символьное ASCII представление (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).

UUID_TO_CHAR(<uuid>)

Условные функции

CASE

Условное выражение. Дает возможность выбрать результирующее значение из множества различных выражений.

CASE <исходное выражение>
    WHEN <выражение 1> THEN {<результат 1> | NULL}
    [WHEN <выражение 2> THEN {<результат 2> | NULL}] 
    ...
    [ELSE {<значение по умолчанию> | NULL}]
END

Исходное выражение возвращает значение, с которым сравниваются выражения N в последующих предложениях WHEN. При совпадении функция возвращает результат N или NULL, если пустое значение указано в этом предложении.

Если ни одно выражение N в списке предложений WHEN не равно исходному выражению, то, в случае присутствия предложения ELSE, возвращается значение по умолчанию (или NULL, если именно оно указано в этом предложении). Если же в этом случае отсутствует предложение ELSE, то функция возвращает значение NULL.

Если исходное выражение имеет значение NULL, то оно не будет соответствовать ни одному из выражений N, даже тем, которые имеют значение NULL.

Есть еще один синтаксический вариант функции CASE - поисковый CASE:

CASE
    WHEN <логическое выражение> THEN {<результат> | NULL}
    [WHEN <логическое выражение> THEN {<результат> | NULL}] 
    ...
    [ELSE {<выражение по умолчанию> | NULL}]
END

Здесь <логическое выражение> даёт тройной логический результат: TRUE, FALSE или NULL. Первое выражение, которое вернет TRUE, определяет результат. Если нет выражений, возвращающих TRUE, то в качестве результата берётся <выражение по умолчанию> из ветви ELSE. Если нет выражений, возвращающих TRUE, и ветвь ELSE отсутствует, результатом будет NULL.

COALESCE

Возвращает первое по порядку непустое значение в списке.

COALESCE (<выражение 1>, <выражение 2> [, <выражение 3>]...)

Выполняется просмотр выражений в списке слева направо. Функция возвращает первое встретившееся непустое значение (NOT NULL). Если все выражения в списке имеют пустое значение, то функция возвращает NULL.

IIF

Проверяет условие, если оно истинно, то возвращает первое значение, иначе - второе.

IIF (<условие>, <значение 1> <значение 2>)

Контрольные вопросы

  1. Как можно узнать адрес подключившегося клиента внутри хранимой процедуры?
  2. Как сохранить какое-либо значение для использования в последующих запросах?
  3. Сколько времени будет храниться такое значение?
  4. В чем разница функций CEIL и ROUND?
  5. В чем разница функций OCTET_LENGTH и CHAR_LENGTH?
  6. Как из строки выделить подстроку?
  7. Как удалить заданные символы из начала или конца строки?
  8. Как вычислить дату через 3 года от текущей?
  9. Как вычислить какой день недели будет через год?
  10. Как преобразовать значение одного типа данных, в значение другого?
  11. Как осуществить побитовый сдвиг числа вправо?
  12. Как получить глобальный уникальный идентификатор?
  13. Как обеспечить, чтобы переменная никогда не была NULL?
  14. Как преобразовать символы строки к верхнему регистру?

Сортировка

Результат выборки данных при выполнении оператора SELECT по умолчанию никак не упорядочивается. Предложение ORDER BY позволяет указать необходимый порядок при выборке данных.

Синтаксис:

SELECT ... FROM ...
...
ORDER BY <элемент сортировки> [, <элемент сортировки> ...]

где <элемент сортировки> состоит из

    {имя столбца | псевдоним столбца | позиция столбца | выражение}
    [ASC | DESC] [NULLS FIRST | NULLS LAST]

В предложении через запятую перечисляются элементы сортировки, по которым нужно упорядочить результирующий набор данных. Можно задавать имя столбца, псевдоним, присвоенный столбцу в списке выбора при помощи ключевого слова AS, или порядковый номер столбца в списке выбора. В одном предложении можно для разных столбцов смешивать форму записи. Например, один столбец в списке сортировки может быть задан своим именем, а другой порядковым номером.

Ключевое слово ASC задаёт упорядочение по возрастанию значений. Применяется по умолчанию. Ключевое слово DESC задаёт упорядочение по убыванию значений. В одном предложении упорядочение по одному столбцу может идти по возрастанию значений, а по другому — по убыванию.

Ключевое слово NULLS определяет, где в отсортированном наборе данных будут находиться значения NULL соответствующего столбца - в начале выборки (FIRST) или в конце (LAST). По умолчанию принимается NULLS FIRST.

Рассмотрим несколько примеров.

Сортировка сотрудников по фамилии (last_name) в алфавитном порядке:

SELECT EMP_NO, FIRST_NAME, LAST_NAME
FROM EMPLOYEE
ORDER BY LAST_NAME ASC;

Сортировка сотрудников по зарплате (salary) от самой высокой к самой низкой:

SELECT EMP_NO, FIRST_NAME, LAST_NAME, SALARY
FROM EMPLOYEE
ORDER BY SALARY DESC;

Сортировка сотрудников сначала по отделу (dept_no), а затем по фамилии (last_name):

SELECT EMP_NO, FIRST_NAME, LAST_NAME, DEPT_NO
FROM EMPLOYEE
ORDER BY DEPT_NO ASC, LAST_NAME ASC;

Сортировка сотрудников по длине их имени (first_name):

SELECT EMP_NO, FIRST_NAME, LAST_NAME
FROM EMPLOYEE
ORDER BY CHAR_LENGTH(FIRST_NAME) DESC;

Сортировка сотрудников по дате приема на работу (hire_date):

SELECT EMP_NO, FIRST_NAME, LAST_NAME, HIRE_DATE
FROM EMPLOYEE
ORDER BY HIRE_DATE ASC;

Сортировка по второму столбцу в результате выборки (в данном случае first_name):

SELECT EMP_NO, FIRST_NAME, LAST_NAME
FROM EMPLOYEE
ORDER BY 2 ASC;

Сортировка сотрудников по зарплате, но сначала выводятся те, у кого зарплата не указана (NULL):

SELECT EMP_NO, FIRST_NAME, LAST_NAME, SALARY
FROM EMPLOYEE
ORDER BY SALARY NULLS FIRST;

Контрольные вопросы

  1. Надо ли использовать упорядочивание, если мы добавляли записи в правильном порядке?
  2. Что произойдет, если указать несколько полей для сортировки?
  3. Можно ли упорядочивать по одному полю по возрастанию, а по другому - по убыванию?
  4. Куда будут помещаться значения NULL при сортировке?
  5. Что можно указывать в качестве элемента сортировки?

Индексы

Понятие индекса

Индекс - это объект БД, содержащий упорядоченные значения указанных столбцов таблицы и ссылки на физическое размещение записи с данными значениями.

Индексы ускоряют поиск и извлечение данных из таблиц. Они работают аналогично указателю в книге: вместо того чтобы перелистывать всю книгу (таблицу) для поиска нужной информации, вы можете сразу перейти к нужной странице (строке) с помощью указателя (индекса).

  1. Ускорение поиска:

    • Индексы позволяют быстро находить строки, удовлетворяющие условиям запроса, без необходимости полного сканирования таблицы.
  2. Оптимизация запросов:

    • Они помогают СУБД (системе управления базами данных) эффективно выполнять запросы, особенно при работе с большими объемами данных.
  3. Ускорение сортировки и группировки:

    • Если данные уже отсортированы в индексе, запросы с ORDER BY или GROUP BY выполняются быстрее.
  4. Обеспечение уникальности и ограничений целостности:

    • Уникальные индексы гарантируют, что в столбце или группе столбцов не будет дубликатов. Для обеспечения ограничений целостности – PRIMARY KEY, FOREIGN KEY, UNIQUE, СУБД может создавать индексы автоматически.

Индексы обычно реализуются с помощью сбалансированных деревьев (B-деревьев) или хэш-таблиц. Рассмотрим их подробнее:

B-дерево (B-tree):

  • Это наиболее распространенная структура для индексов.
  • B-дерево — это сбалансированное дерево, где каждый узел содержит несколько ключей и ссылок на дочерние узлы.
  • Преимущества:
    • Поддерживает быстрый поиск, вставку и удаление данных.
    • Эффективно работает для диапазонных запросов (например, WHERE column BETWEEN value1 AND value2).
  • Пример:
    • Если у вас есть индекс на столбце ID, то поиск всех строк с ID = 25 будет выполняться за время O(log n), где n — количество строк.

Хэш-индекс:

  • Использует хэш-таблицы для быстрого поиска по точному совпадению.
  • Преимущества:
    • Очень быстрый поиск по точному значению (O(1) в среднем случае).
  • Недостатки:
    • Не поддерживает диапазонные запросы.
    • Менее эффективен при частых изменениях данных.

Другие типы индексов:

  • Bitmap-индексы: Используются для столбцов с небольшим количеством уникальных значений (например, пол).
  • Полнотекстовые индексы: Для поиска по текстовым данным.
  • Пространственные индексы: Для работы с геоданными.

Создание индекса

Для создания индекса используется следующие команды:

CREATE [UNIQUE] [ASC | DESC] INDEX <имя индекса> ON <таблицы> (<поле>, <поле>, ...);

CREATE [UNIQUE] [ASC | DESC] INDEX <имя индекса> ON <таблицы> COMPUTED BY (<выражение>);

Если при создании индекса указано ключевое слово UNIQUE, то индекс гарантирует уникальность значений ключей. Такой индекс называется уникальным.

При создании индекса вместо одного или нескольких столбцов вы также можете указать одно выражение, используя предложение COMPUTED BY. Такой индекс называется вычисляемым или индексом по выражению. Вычисляемые индексы используются в запросах, в которых условие в предложениях WHERE, ORDER BY или GROUP BY в точности совпадает с выражением в определении индекса. Выражение в вычисляемом индексе может использовать несколько столбцов таблицы.

Примеры:

Создание индекса

CREATE INDEX IDX_UPDATER ON SALARY_HISTORY (UPDATER_ID);

Создание индекса с сортировкой ключей по убыванию

CREATE DESCENDING INDEX IDX_CHANGE ON SALARY_HISTORY (CHANGE_DATE);

Создание многосегментного индекса

CREATE INDEX IDX_SALESTAT ON SALES (ORDER_STATUS, PAID);

Создание индекса, не допускающего дубликаты значений

CREATE UNIQUE INDEX UNQ_COUNTRY_NAME ON COUNTRY (NAME);

Создание вычисляемого индекса

CREATE INDEX IDX_NAME_UPPER ON PERSONS COMPUTED BY (UPPER (NAME));

Такой индекс может быть использован для регистронезависимого поиска.

SELECT * FROM PERSONS WHERE UPPER(NAME) STARTING WITH UPPER('Iv');

Изменение и удаление индекса

Индекс можно перевести в неативное состояние или снова активировать.

Для этого используется оператор

ALTER INDEX <имя индекса> {ACTIVE | INACTIVE};

При выборе опции INACTIVE, индекс переводится из активного в неактивное состояние. Перевод индекса в неактивное состояние по своему действию похоже на команду DROP INDEX за исключением того, что определение индекса сохраняется в базе данных. Невозможно перевести в неактивное состояние индекс участвующий в ограничении.

Активный индекс может быть отключен, только если отсутствуют запросы использующие этот индекс, иначе будет возвращена ошибка «object in use».

Перевод индекса в неактивное состояние может быть полезен при массовой вставке, модификации или удалении записей из таблицы, для которой этот индекс построен.

При выборе альтернативы ACTIVE индекс переводится из неактивного состояния в активное. При переводе индекса из неактивного состояния в активное индекс перестраивается.

Для удаления индекса используется оператор

DROP INDEX <имя индекса>;

Пример запроса с использованием индекса

Предположим, у нас есть таблица EMPLOYEES:

CREATE TABLE EMPLOYEES (
    ID INT PRIMARY KEY,
    FIRST_NAME VARCHAR(50),
    LAST_NAME VARCHAR(50),
    DEPT_NO CHAR(3),
    SALARY DECIMAL(10, 2)
);

Создадим индекс на столбце LAST_NAME:

CREATE INDEX IDX_LAST_NAME ON EMPLOYEES (LAST_NAME);

Теперь запрос:

SELECT * FROM EMPLOYEES WHERE LAST_NAME = 'Иванов';

будет выполняться быстрее, так как СУБД использует индекс IDX_LAST_NAME для поиска.

Достоинства и недостатки индексов

Использование индексов дает следующие преимущества:

  • Ускорение поиска: Индексы значительно сокращают время выполнения запросов.
  • Эффективность: Уменьшают нагрузку на сервер за счет сокращения количества операций ввода-вывода.

Однако и сопряжено с рядом недостатков:

  1. Затраты на хранение:
    • Индексы занимают дополнительное место на диске.
  2. Затраты на обновление:
    • При вставке, обновлении или удалении данных индекс должен быть обновлен, что может замедлить эти операции.
  3. Не всегда используются:
    • Индексы полезны только для определенных типов запросов. Например, они не помогут, если запрос использует функции или операции, которые не могут быть оптимизированы с помощью индекса.

Контрольные вопросы

  1. Для чего применяются индексы?
  2. Какие структуры данных лежат в основе индексов?
  3. Назовите известные типы индексов?
  4. Какие команды используются для создания индексов?
  5. Как можно изменять уже созданный индекс?
  6. Назовите преимущества использования индексов?
  7. Назовите недостатки применения индексов?

Объединение

Предложение UNION объединяет два и более набора данных, тем самым увеличивая общее количество строк, но не столбцов. Наборы данных, принимающие участие в UNION, должны иметь одинаковое количество столбцов. Однако столбцы в соответствующих позициях не обязаны иметь один и тот же тип данных, они могут быть абсолютно не связанными. По умолчанию, объединение подавляет дубликаты строк. UNION ALL отображает все строки, включая дубликаты. Необязательное ключевое слово DISTINCT делает поведение по умолчанию явным.

Синтаксис

    SELECT ... FROM ...
    UNION [DISTINCT | ALL]
    SELECT ... FROM ...
    ...
    [UNION [DISTINCT | ALL]
    SELECT ... FROM ...]
    [...]

    [ORDER BY <элементы сортировки>]

Объединения получают имена столбцов из первого запроса на выборку. Если требуется дать псевдонимы объединяемым столбцам, то это необходимо делать для списка столбцов в самом верхнем запросе на выборку. Псевдонимы в других участвующих в объединении выборках разрешены, и могут быть даже полезными, но они не будут распространяться на уровне объединения. Если объединение имеет предложение ORDER BY, то единственно возможными элементами сортировки являются целочисленные литералы, указывающие на позиции столбцов, необязательно сопровождаемые ASC или DESC и NULLS FIRST и NULLS LAST директивами. Это так же означает, что нельзя упорядочить объединение ничем, что не является столбцом объединения. (Однако можно завернуть его в производную таблицу, которая даст возможность использовать все обычные параметры сортировки.) Объединения позволены в подзапросах любого вида и могут самостоятельно содержать подзапросы. Они также могут содержать соединения (joins), и могут принимать участие в соединениях, если завёрнуты в производную таблицу.

Например, запрос представляет информацию из различных музыкальных коллекций в одном наборе данных с помощью объединений:

SELECT id, title, artist, len, 'CD' AS medium
FROM cds
UNION
SELECT id, title, artist, len, 'LP'
FROM records
UNION
SELECT id, title, artist, len, 'MC'
FROM cassettes
ORDER BY 3, 2 -- artist, title

Следующий запрос получает имена и телефонные номера переводчиков и корректоров. Те переводчики, которые также работают корректорами, будут отображены только один раз в результирующем наборе, если номера их телефонов одинаковые в обеих таблицах. Тот же результат может быть получен без ключевого слова DISTINCT. Если вместо ключевого слова DISTINCT, будет указано ключевое слово ALL, эти люди будут отображены дважды.

SELECT name, phone FROM translators
UNION DISTINCT
SELECT name, telephone FROM proofreaders 

Пример использования UNION в подзапросе:

SELECT name, phone, hourly_rate FROM clowns
    WHERE hourly_rate < ALL 
    (SELECT hourly_rate FROM jugglers
        UNION
    SELECT hourly_rate FROM acrobats)
ORDER BY hourly_rate

Контрольные вопросы

  1. Что будет, если объединяемые запросы будут содержать разное количество столбцов?
  2. Что будет, если соответствующие столбцы объединяемых запросов будут иметь разные названия?
  3. Что будет, если соответствующие столбцы объединяемых запросов будут иметь разные типы данных?
  4. Можно ли упорядочить объединение нескольких запросов и как?
  5. Как управлять подавлением дубликатов в результате объединения и что является поведением по умолчанию?

Группировка

Предложение GROUP BY

Предложение GROUP BY используется для объединения строк в группы на основе одинаковых значений в указанных столбцах. Это особенно полезно для выполнения агрегатных операций (например, подсчет, суммирование, нахождение среднего значения) над каждой группой.

Синтаксис:

SELECT <поле1>, <поле2>, <агрегатная функция>(<поле3>)
    FROM <источник>
    WHERE <условие>
    GROUP BY <поле1>, <поле2>
    HAVING <условие>
    ORDER BY <поле1>;

Каждый не агрегированный столбец в SELECT списке, должен быть так же включён в GROUP BY список.

Рассмотрим несколько примеров использования группировки. Для этого допустим у нас есть таблица сотрудников (WORKERS).

NAMEPROJECT_IDAGEGENDERSHIFT
Иванов120М1
Петров122М2
Виноградова121Ж1
Сидоров218М2
Кузнецова226Ж2
Дмитриева321Ж1
Ершова319Ж2

Когда в списке выбора SELECT содержатся только агрегатные столбцы, предложение GROUP BY необязательно:

SELECT COUNT(*), AVG(AGE) FROM WORKERS;

Этот запрос вернёт одну строку с указанием количества сотрудников и их средний возраст.

COUNTAVG
721

Добавление выражения в список выбора, которое не зависит от строк таблицы WORKERS, ничего не меняет:

SELECT COUNT(*), AVG(AGE), CURRENT_DATE FROM WORKERS;
COUNTAVGCURRENT_DATE
7212025-05-29

Теперь строка результата будет иметь дополнительный столбец, отображающий текущую дату, но кроме этого, ничего фундаментального не изменилось. Группировка по-прежнему не требуется.

Тем не менее, в обоих приведённых выше примерах это разрешено. Например, Можно добавить группировку по проектам:

SELECT COUNT(*), AVG(AGE) FROM WORKERS GROUP BY PROJECT_ID;

Этот запрос разобьет все записи таблицы на группы с одинаковыми значениями поля PROJECT_ID.

В нашем случе это будет.

NAMEPROJECT_IDAGEGENDERSHIFT
Иванов120М1
Петров122М2
Виноградова121Ж1
=============================================
Сидоров218М2
Кузнецова226Ж2
=============================================
Дмитриева321Ж1
Ершова319Ж2

Для вычисления агрегатных значений будут использованы поля, указанные в параметре агрегатной функции. В нашем случае это AGE. Другими словами:

  • для группы записей где PROJECT_ID=1 будут использованы значения 20, 22, 21.
  • для группы записей где PROJECT_ID=2 будут использованы значения 18, 24.
  • для группы записей где PROJECT_ID=3 будут использованы значения 21, 21.

Агрегатная функция COUNT считает количество значений и сами значения не важны. Подробнее функцию COUNT и ее особенности мы изучим позднее.

После группировки и вычисления агрегатных значений, мы в результате получим столько записей, сколько групп у нас получилось.

COUNTAVG
321
222
220

Обратите внимание, что значения агрегатных функций тоже изменились, т.к. они вычисляются уже по другим наборам данных.

Однако, такой запрос не говорит, какая запись к какому проекту относится. Для того чтобы получить эту дополнительную часть информации, не агрегатный столбец PROJECT_ID должен быть добавлен в список выборки SELECT. И он может быть добавлен, потому что входит в список группировки после GROUP BY.

SELECT PROJECT_ID, COUNT(*), AVG(AGE) FROM WORKERS GROUP BY PROJECT_ID;

При этом полученная информация уже связана с проектом, для которого она вычисляется.

PROJECT_IDCOUNTAVG
1321
2222
3220

Допустим, что кроме этого мы хотим знать еще смену, в которую работает сотрудник. Просто добавить смену в список выбора нельзя. Требуется обязательно добавить это поле в список группировки.

SELECT PROJECT_ID, SHIFT, COUNT(*), AVG(AGE) FROM WORKERS
    GROUP BY PROJECT_ID, SHIFT

Такой запрос изменит состав групп и вернет следующий результат:

PROJECT_IDSHIFTCOUNTAVG
11220,5
12122
22222
31121
32119

Предложение GROUP BY работает после фильтрации записей WHERE и, следовательно, уже не учитывает данные, которые были отфильтрованы. Например,

SELECT COUNT(*), AVG(AGE) FROM WORKERS WHERE GENDER='М';

Этот запрос вернёт одну строку с указанием количества сотрудников мужского пола и их средний возраст.

COUNTAVG
320

Предложение HAVING

Так же, как и предложение WHERE ограничивает строки в наборе данных, теми которые удовлетворяют условию поиска, с той разницей, что предложение HAVING накладывает ограничения на агрегированные строки сгруппированного набора. Предложение HAVING не является обязательным и может быть использовано только в сочетании с предложением GROUP BY.

Условие в предложении HAVING может ссылаться на любой агрегированный столбец в списке выбора SELECT или любой столбец в списке GROUP BY. В последнем случае эффективнее фильтровать не агрегированные данные на более ранней стадии в предложении WHERE.

Например, мы можем использовать предложение HAVING для исключения проектов с малым составом:

SELECT PROJECT_ID, COUNT(*), AVG(AGE) FROM WORKERS 
    GROUP BY PROJECT_ID HAVING COUNT(*) > 2
PROJECT_IDCOUNTAVG
1321

Либо выбрать проекты, где разброс возраста участников больше 2-х лет.

SELECT PROJECT_ID, COUNT(*), AVG(AGE) FROM WORKERS
    GROUP BY PROJECT_ID HAVING MAX(AGE) - MIN(AGE) > 2
PROJECT_IDCOUNTAVG
1321
2222
3220

Обратите внимание, что в этом запросе мы не указываем MIN и MAX в списке выбора, однако используем их в предложении HAVING. Фактически они вычисляются, но не выводятся. Если бы они были, результат запроса был бы следующий.

SELECT PROJECT_ID, COUNT(*), AVG(AGE), MAX(AGE), MIN(AGE) FROM WORKERS
    GROUP BY PROJECT_ID HAVING MAX(AGE) - MIN(AGE) > 2
PROJECT_IDCOUNTAVGMAXMIN
13212220
22222618
32202119

Контрольные вопросы

  1. Опишите алгоритм работы группировки записей?
  2. Какие ограничения накладывает группировка записей на список выбора?
  3. Что допускается использовать в списке выбора при группировке записей?
  4. Чем определяется количество записей, возвращаемых запросами с группировкой?
  5. Для чего используется предложение HAVING?
  6. Чем HAVING отличается от WHERE?
  7. В каких случаях рекомендуется использовать WHERE, а не HAVING?

Агрегатные функции

Агрегатные функции выполняют вычисление на наборе значений и возвращают одиночное значение. Агрегатные функции, за исключением COUNT, не учитывают значения NULL и часто используются совместно с предложением GROUP BY в сложных аналитических запросах.

Общий синтаксис для использования агрегатных функций

<агрегатная функция>([ALL | DISTINCT] <выражение>)

Выражение может содержать столбец таблицы, константу, переменную, выражение, неагрегатную или пользовательскую функцию, которая возвращает числовой тип данных. Агрегатные функции в качестве выражения не допускаются.

Параметр ALL применяет агрегатную функцию ко всем значениям. ALL является параметром по умолчанию. Параметр DISTINCT указывает на то, что функция AVG будет выполнена только для одного экземпляра каждого уникального значения, независимо от того, сколько раз встречается это значение.

Основные агрегатные функции:

  • COUNT() — подсчитывает количество строк.
  • SUM() — вычисляет сумму значений.
  • AVG() — вычисляет среднее значение.
  • MIN() — находит минимальное значение.
  • MAX() — находит максимальное значение.
  • LIST() — выполняет конкатенацию строк аргументов.

Рассмотрим примеры использования агрегатных функций.

COUNT()

Подсчитывает количество строк группы.

Например, найдем общее количество сотрудников.

SELECT COUNT(*) AS TOTAL_EMPLOYEES
  FROM EMPLOYEES;

Результат:

total_employees
5

Найдем количество сотрудников в отделе D01.

SELECT COUNT(*) AS EMPLOYEES_IN_D01
  FROM EMPLOYEES
  WHERE DEPT_NO = 'D01';

Результат:

employees_in_d01
2

SUM()

Вычисляет сумму значений в столбце.

Найдем общую сумму зарплат всех сотрудников.

SELECT SUM(SALARY) AS TOTAL_SALARY
  FROM EMPLOYEES;

Результат:

total_salary
280000.00

Найдем сумму зарплат в отделе D02.

SELECT SUM(SALARY) AS TOTAL_SALARY_D02
  FROM EMPLOYEES
  WHERE DEPT_NO = 'D02';
total_salary_d02
130000.00

AVG()

Вычисляет среднее значение в столбце.

Найдем среднюю зарплату всех сотрудников.

SELECT AVG(SALARY) AS AVG_SALARY
  FROM EMPLOYEES;

Результат:

avg_salary
56000.00

Найдем среднюю зарплату в отделе D01.

SELECT AVG(SALARY) AS AVG_SALARY_D01
  FROM EMPLOYEES
  WHERE DEPT_NO = 'D01';

Результат:

avg_salary_d01
52500.00

MIN()

Находит минимальное значение в столбце.

Найдем минимальную зарплату среди всех сотрудников.

SELECT MIN(SALARY) AS MIN_SALARY
  FROM EMPLOYEES;

Результат:

min_salary
45000.00

Найдем минимальную зарплату в отделе D02.

SELECT MIN(SALARY) AS MIN_SALARY_D02
  FROM EMPLOYEES
  WHERE DEPT_NO = 'D02';

Результат:

min_salary_d02
60000.00

MAX()

Находит максимальное значение в столбце.

Найдем максимальную зарплату среди всех сотрудников.

SELECT MAX(SALARY) AS MAX_SALARY
  FROM EMPLOYEES;

Результат:

max_salary
70000.00

Найдем максимальную зарплату в отделе D01.

SELECT MAX(SALARY) AS MAX_SALARY_D01
  FROM EMPLOYEES
  WHERE DEPT_NO = 'D01';

Результат:

max_salary_d01
55000.00

LIST

Функция имеет параметр, который можно указать через запятую

LIST([ALL | DISTINCT] <выражение> [, <разделитель>])

Разделитель - выражение строкового типа. По умолчанию разделителем является запятая.

Функция LIST возвращает строку, содержащую значения элементов выборки, которые не равны NULL. При пустой выборке функция возвратит NULL. Порядок конкатенации строк определяется порядком чтения записей из источников, который в общем случае не определён. Для придания списку необходимого порядка вы можете предварительно упорядочить источник данных, например с помощью производной таблицы.

Получим все страны в одной строке.

SELECT LIST(COUNTRY)
  FROM COUNTRY;

Результат:

LIST
‘USA,England,Canada,Switzerland,Japan,Italy,France,Germany,Australia,Hong Kong,Netherlands,Belgium,Austria,Fiji,Russia,Romania’

Добавим пробел после разделяющей запятой.

SELECT LIST(COUNTRY)
  FROM COUNTRY;

Результат:

LIST
‘USA, England, Canada, Switzerland, Japan, Italy, France, Germany, Australia, Hong Kong, Netherlands, Belgium, Austria, Fiji, Russia, Romania’

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

SELECT LIST(DISTINCT CURRENCY, ', ')
  FROM COUNTRY;

Результат:

LIST
‘ADollar, CdnDlr, Dollar, Euro, FDollar, HKDollar, Pound, RLeu, Ruble, SFranc, Yen’

Примеры с GROUP BY

Агрегатные функции часто используются вместе с предложением GROUP BY. Они вычисляют значения не на основании значений столбца из всех строк таблицы, а на основании значений столбца для записей, входящих в определенную группу. Напомним, что в группу входят записи, в одинаковым значением столбцов из списка группировки.

Количество сотрудников в каждом отделе:

SELECT DEPT_NO, COUNT(*) AS EMPLOYEE_COUNT
  FROM EMPLOYEES
  GROUP BY DEPT_NO;

Результат:

DEPT_NOEMPLOYEE_COUNT
D012
D022
D031

Средняя зарплата в каждом отделе:

SELECT DEPT_NO, AVG(SALARY) AS AVG_SALARY
  FROM EMPLOYEES
  GROUP BY DEPT_NO;

Результат:

DEPT_NOAVG_SALARY
D0152500.00
D0265000.00
D0345000.00

Максимальная зарплата по должностям:

SELECT JOB_TITLE, MAX(SALARY) AS MAX_SALARY
  FROM EMPLOYEES
  GROUP BY JOB_TITLE;

Результат:

JOB_TITLEMAX_SALARY
МЕНЕДЖЕР50000.00
АНАЛИТИК70000.00
РАЗРАБОТЧИК55000.00

Контрольные вопросы

  1. Что представляют собой агрегатные функции?
  2. Как обрабатываются NULL значения в агрегатных функциях?
  3. Для чего используется опция DISTINCT?
  4. Назовите основные агрегатные функции?
  5. Какие особенности есть у агрегатной функции COUNT?
  6. Какие особенности есть у агрегатной функции LIST?

Оконные (аналитические) функции

Оконная функция выполняет вычисления над списком строк в таблице, которые как-то относятся к текущей строке. Это сравнимо с типом вычислений, которые могут быть выполнены с помощью какой-либо агрегатной функции. Но в отличие от обычных агрегатных функций, использование оконной функции не заставляет строки группироваться в одну; строки сохраняют свои отдельные значения. Другими словами, оконная функция позволяет получить доступ более чем только к текущей строке результата запроса.

Синтаксически вызов оконной функции есть указание её имени, за которым всегда следует ключевое слово OVER() с возможными аргументами внутри скобок. В этом и заключается её синтаксическое отличие от обычной функции или агрегатной функции. Оконные функции могут находиться только в списке SELECT и предложении ORDER BY.

Предложение OVER может содержать секционирование, сортировку и рамку окна.

Простой пример

Начнем с простого примера, который показывает как меняются результаты в зависимости от указаний внутри OVER.

SELECT A, B, C,
    SUM(C) OVER(),
    SUM(C) OVER(ORDER BY A, B) AS S_O_AB,
    SUM(C) OVER(PARTITION BY A) AS S_P_A,
    SUM(C) OVER(PARTITION BY A ORDER BY B) AS S_P_A_O_B
FROM T

Потенциальные результаты могут быть следующими.

ABCSUMS_O_ABS_P_AS_P_A_O_B
1130141306030
1220141506050
1310141606060
2125141854025
22151411004040
31411411414141

Столбцы A, B, C содержат некие данные. В запросе использована оконная функция SUM по столбцу C, но с разными указаниям к выполнению суммирования. Посмотрим на них более внимательно.

SUM(C) OVER() не содержит никаких указаний и для каждой строки выдает один и тот же результат (столбец SUM в результате): сумму всех значений столбца C. Такое же значение мы бы получили при простом использовании функции SUM как агрегатной и без предложения GROUP BY. Но заметьте что здесь количество строк не уменьшается и не равно количеству групп. Значение выдается для каждом строки.

ABCSUM
1130141
1220141
1310141
2125141
2215141
3141141

SUM(C) OVER(ORDER BY A, B) (столбец S_O_AB) содержит указание к порядку использования значений C в соответствие со значениями столбцов A и B. В таком случае для каждой строки результат SUM(C) будет выдавать сумму значений столбца C с первого до текущего по указанному порядку суммирования. Последнее значение как и следует ожидать будет равно сумме всех значений C.

ABCS_O_AB
113030
122050
131060
212585
2215100
3141141

SUM(C) OVER(PARTITION BY A) (столбец S_P_A) снова не содержит указания к порядку выполнения и там мы видим снова одинаковые суммы, но разбитые на группы. Это потому что есть указания к сегментированию, разделению общего набора данных на части. Суммируются значения столбца C всех строк, в которых значения поля A равно значению поля A текущей строки.

ABCS_P_A
113060
122060
131060
212540
221540
314141

SUM(C) OVER(PARTITION BY A ORDER BY B) (столбец S_P_A_O_B) содержит указания и к сегментированию, и к порядку. В таком случае сумма вычисляется по значениям, входящим в указанную группу начиная с первого до текущего, в указанном порядке.

ABCS_P_A_O_B
113030
122050
131060
212525
221540
314141

Суммы, вычисленные по порядку еще называют сумма, вычисленная нарастающим итогом.

Агрегатные функции

Все агрегатные функции могут быть использованы в качестве оконных функций, при добавлении предложения OVER.

Допустим, у нас есть таблица EMPLOYEE со столбцами:

  • DEPT_NO,
  • FULL_NAME
  • SALARY.

Если необходимо показать для каждого сотрудника, соответствующую ему заработную плату и процент от фонда заработной платы, то это решается следующим запросом:

SELECT FULL_NAME, DEPT_NO, 
  SALARY,
  100 * SALARY / (SELECT SUM(SALARY) FROM EMPLOYEE) PERCENTAGE
FROM EMPLOYEE
ORDER BY FULL_NAME;

Результат:

FULL_NAMEDEPT_NOSALARYPERCENTAGE
Baldwin, Janet11061637.810.38
Bender, Oliver H.000212850.001.31
Bennet, Ann12022935.000.14
Bishop, Dana62162550.000.38
Brown, Kelly60027000.000.16

Запрос повторяется и может работать довольно долго, особенно если EMPLOYEE является сложным представлением.

Этот запрос может быть переписан в более быстрой и элегантной форме с использованием оконных функций:

SELECT FULL_NAME, DEPT_NO,
  SALARY,
  100 * SALARY / SUM(SALARY) OVER () PERCENTAGE
FROM EMPLOYEE
ORDER BY FULL_NAME;

Здесь SUM(SALARY) OVER () вычисляет сумму всех зарплат из запроса.

Секционирование

Как и для агрегатных функций, которые могут работать отдельно или по отношению к группе, оконные функции тоже могут работать для групп, которые называются секциями (partition).

Синтаксис:

<оконная функция>(...) OVER (PARTITION BY <выражение> [, <выражение> ...])

Для каждой строки, оконная функция обсчитывает только строки, которые попадают в то же самую секцию, что и текущая строка.

Результаты оконных функций выводятся вместе со строками, для которых они вычисляются и поэтому не уменьшают мощность выборки. Продолжая пример с сотрудниками, вместо того чтобы считать процент зарплаты каждого сотрудника от суммарной зарплаты сотрудников, посчитаем процент от суммарной зарплаты сотрудников того же отдела:

SELECT FULL_NAME, DEPT_NO,
  SALARY,
  SALARY / SUM(SALARY) OVER (PARTITION BY DEPT_NO) PERCENTAGE
FROM EMPLOYEE
ORDER BY FULL_NAME;

Результат:

FULL_NAMEDEPT_NOSALARYPERCENTAGE
Reeves, Roger12033620.6335.10
Stansbury, Willie12039224.0640.95
Steadman, Walter900116100.0062.55
Sutherland, Claudia140100914.00100.0
Weston, K. J.13086292.9445.64

Сортировка

Предложение ORDER BY может быть использовано с секционированием или без него. Предложение ORDER BY внутри OVER задаёт порядок, в котором оконная функция будет обрабатывать строки. Этот порядок не обязан совпадать с порядком вывода строк.

Синтаксис:

<оконная функция>(...) OVER (ORDER BY <выражение> [, <выражение> ...])

Есть ещё одно важное понятие, связанное с оконными функциями: для каждой строки существует набор строк в её секции, называемый рамкой окна (кадры окна, frame). По умолчанию, с указанием ORDER BY рамка состоит из всех строк от начала секции до текущей строки и строк, равных текущей по значению выражения ORDER BY. Без ORDER BY рамка по умолчанию состоит из всех строк секции.

Таким образом, для стандартных агрегатных функций, предложение ORDER BY заставляет возвращать частичные результаты агрегации по мере обработки записей.

Пример:

SELECT EMP_NO, SALARY,
  SUM(SALARY) OVER (ORDER BY SALARY) AS CUMUL_SALARY
FROM EMPLOYEE
ORDER BY SALARY;

Результат:

EMP_NOSALARYCUMUL_SALARY
282293522935
1092700049935
653127581210
14532000113210
12133000146210
3633620179830
11435000249830
14435000249830
13836000285830

В этом случае CUMUL_SALARY возвращает частичную/накопительную агрегацию (функции SUM). Может показаться странным, что значение 249830 повторяется для идентификаторов 114 и 144, но так и должно быть. Сортировка (ORDER BY) ключей группирует их вместе, и агрегат вычисляется единожды (но суммируя сразу два значения 35000). Чтобы избежать этого, вы можете добавить поле EMP_NO в конце предложения ORDER BY.

SELECT EMP_NO, SALARY,
    SUM(SALARY) OVER (ORDER BY SALARY, EMP_NO) AS CUMUL_SALARY
FROM EMPLOYEE
ORDER BY SALARY;

И тогда результат будет отличаться.

EMP_NOSALARYCUMUL_SALARY
282293522935
1092700049935
653127581210
14532000113210
12133000146210
3633620179830
11435000214830
14435000249830
13836000285830

Вы можете использовать несколько окон с различными сортировками, и дополнять предложение ORDER BY опциями ASCи DESC, а также NULLS FIRST и NULLS LAST.

С секциями предложение ORDER BY работает таким же образом, но на границе каждой секции агрегаты сбрасываются.

Рамка окна

Набор строк внутри секции, которым оперирует оконная функция называется рамкой окна. Рамка окна состоит из трёх частей: единица (unit), начальная граница и конечная граница. В качестве единицы может быть использовано ключевые слова RANGE или ROWS, которые указывают каким образом определены границы окна. Границы окна определяются следующими выражениями:

  • <значение> PRECEDING
  • <значение> FOLLOWING
  • CURRENT ROW

Предложения ROWS и RANGE требуют, чтобы было указано предложение ORDER BY, а иначе для агрегатных функций рамка окна состоит из всех строк секции.

Синтаксис:

Стандартное, для оконной функции, начало:

<оконная функция>(...) OVER (ORDER BY <выражение> [, <выражение> ...] <описание рамки окна>)

Рамка окна описывается: одним из следующих способов.

С помощью описания предшествующих строк:

    ROWS UNBOUNDED PRECEDING
    ROWS <значение> PRECEDING
    ROWS CURRENT ROW

С помощью описания предшествующих значений:

    RANGE UNBOUNDED PRECEDING
    RANGE <значение> PRECEDING
    RANGE CURRENT ROW

С помощью описание диапазона строк:

    ROWS BETWEEN 
          { UNBOUNDED PRECEDING | 
          <значение> PRECEDING |
          <значение> FOLLOWING | 
          CURRENT ROW }
      AND 
          { UNBOUNDED FOLLOWING |
          <значение> PRECEDING |
          <значение> FOLLOWING | 
          CURRENT ROW })

С помощью описание диапазона значений:

    2) RANGE BETWEEN
          { UNBOUNDED PRECEDING | 
          <значение> PRECEDING |
          <значение> FOLLOWING | 
          CURRENT ROW }
      AND 
          { UNBOUNDED FOLLOWING |
          <значение> PRECEDING |
          <значение> FOLLOWING | 
          CURRENT ROW })

Предложение ROWS ограничивает строки внутри секции путем указания фиксированного числа строк, предшествующих или следующих после текущей строки, а предложение RANGE логически ограничивает строки внутри секции путем указания диапазона значений в отношении к значению текущей строки. Предшествующие и последующие строки определяются на основании порядка, заданного в предложении ORDER BY.

Если рамка окна задаётся с помощью предложения RANGE, то предложение ORDER BY может содержать только одно выражение числового типа или типа DATE, TIME или TIMESTAMP. Для границ <значение> PRECEDING и <значение> FOLLOWING значения вычитаются и добавляются соответственно к значению указанному в ORDER BY, таким образом получаются границы значений для рамки. Затем все строки внутри секции между границам считаются частью результирующей рамки окна и по ним вычисляется оконная функция.

Если рамка окна задаётся с помощью предложения ROWS, то на предложение ORDER BY не накладывается ограничений на количество и типы выражений. В этом случае значение в <значение> PRECEDING указывает количество строк предшествующее текущей строке, а соответственно в <значение> FOLLOWING указывает количество строк после текущей строки.

Например, следующий запрос вычисляет значения оконных функций используя разные рамки окон

SELECT I, J,
  SUM(J) OVER( ORDER BY I RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS S1,
  SUM(J) OVER( ORDER BY J RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS S2,
  SUM(J) OVER( ORDER BY J RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING ) AS S22,
  SUM(J) OVER( ORDER BY I ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS S3,
  SUM(J) OVER( ORDER BY J ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS S4
FROM T

И выдает следующий результат.

IJS1S2S22S3S4
1103010303030
2206020606060
3309030909090
44012040120120120
5509050909090

Фраза UNBOUNDED PRECEDING указывает, что окно начинается с первой строки секции. UNBOUNDED PRECEDING может быть указано только как начальная точка окна.

Фраза UNBOUNDED FOLLOWING указывает, что окно заканчивается последней строкой секции. UNBOUNDED FOLLOWING может быть указано только как конечная точка окна.

Фраза CURRENT ROW указывает, что окно начинается или заканчивается на текущей строке при использовании совместно с предложением ROWS или что окно заканчивается на текущем значении при использовании с предложением RANGE. CURRENT ROW может быть задана и как начальная, и как конечная точка.

Предложение BETWEEN используется совместно с ключевым словом ROWS или RANGE для указания нижней (начальной) или верхней (конечной) граничной точки окна. Верхняя граница не может быть меньше нижней границы.

Если указана только начальная точка окна, то конечной точкой окна считается CURRENT ROW. Например, если указано ROWS 1 PRECEDING, то это аналогично указанию ROWS BETWEEN 1 PRECEDING AND CURRENT ROW.

Некоторые оконные функции игнорируют выражение рамки, например, ROW_NUMBER.

Таким образом, предложения ROWS и RANGE позволяют довольно гибко настроить размер плавающего окна. Чаще всего встречаются следующие варианты:

  • Нижняя граница фиксирована (совпадает с первой строкой упорядоченной группы), а верхняя граница ползёт (совпадает с текущей строкой упорядоченной группы). В этом случае получаем нарастающий итог (кумулятивный агрегат). В этом случае размер окна меняется (расширяется в одну сторону) и само окно движется за счёт расширения. Возможна и обратная ситуация, когда нижняя граница ползёт, а верхняя зафиксирована. В этом случае окно будет сужаться.

  • Если верхняя и нижняя границы фиксированы относительно текущей строки, например 1 строка до текущей и 2 после текущей, то получаем скользящий агрегат. В этом случае размер окна фиксирован, а само окно скользит.

Именованные окна

Для того чтобы не писать каждый раз сложные выражения для задания окна, имя окна можно задать в предложении WINDOW. Имя окна может быть использовано в предложении OVER для ссылки на определение окна. Кроме того, оно может быть использовано в качестве базового окна для другого именованного или встроенного (в предложении OVER) окна.

Окна с предложениями RANGE или ROWS не могут быть использованы в качестве базового окна (но могут быть использованы в предложении OVER <имя окна>). Окно, которое использует ссылку на базовое окно, не может иметь предложение PARTITION BY и переопределять сортировку с помощью предложения ORDER BY.

Другими словами, базовое окно может определять секции и сортировку, а наследующие окна могут добавлять рамки в виде предложений RANGE и ROWS. Сортировку они могут добавлять только если ее нет в базовом окне.

Рассмотрим следующий запрос.

SELECT
    EMP_NO,
    DEPT_NO,
    SALARY,
    COUNT(*) OVER W1,
    FIRST_VALUE(SALARY) OVER W2,
    LAST_VALUE(SALARY) OVER W2,
    SUM(SALARY) OVER (W2 ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
FROM EMPLOYEE
WINDOW 
    W1 AS (PARTITION BY DEPT_NO), 
    W2 AS (W1 ORDER BY SALARY)
ORDER BY DEPT_NO, SALARY

В этом запросе используются четыре оконные функции и два окна:

  • W1 определяет секции по полю DEPT_NO
  • W2 унаследовано от W1 и, следовательно, тоже определяет секции по полю DEPT_NO, но дополняет его сортировкой по полю SALARY.

Функция COUNT использует окно W1 и по вычисляет общее количество записей для каждого отдела. Функции FIRST_VALUE и LAST_VALUE фактически показывают рамки окна W2 внутри секций. Рамками в данном случае будут являться строки от первой до текущей, так как указана сортировка. Функция SUM определяет сумма зарплат из строк рамки, полученной из окна W2, но с переопределенными границами окна. Эта функция суммирует записи от текущей до следующей (если она есть).

Запрос выдаст следующий результат.

EMP_NODEPT_NOSALARYCOUNTFIRST_VALUELAST_VALUESUM
1205374325374353743266543
1050212800253743212800212800
1271004397024397043970155202.5
85100111232.5243970111232.5111232.5
3411061607.81261607.8161607.81130382.81
6111068775261607.816877568775
110115599997025999970599997013479940
11811574799702599997074799707479970
28120229053229052290556495.63
3612033590.6332290533590.6372784.69
3712039194.0632290539194.0639194.06

Ниже приведены три эквивалентных варианта определения окна из примеры выше. Первый с использованием именованных окон и наследования, второй с использованием окна и явно указанной сортировкой и третий с полностью явным определение окна.

1) OVER (W2 ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
2) OVER (W1 ORDER BY SALARY ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
3) OVER (PARTITION BY DEPT_NO ORDER BY SALARY ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)

Ранжирующие функции

Ранжирующие функции вычисляют некоторый ранг или относительный ранг внутри секции окна.

ROW_NUMBER

Возвращает последовательные номера строк в каждой секции, результатирующего набора.

Например запрос,

SELECT EMP_NO, DEPT_NO, SALARY,
    ROW_NUMBER() OVER (ORDER BY SALARY)
FROM EMPLOYEE

присвоит номер каждой строке

EMP_NODEPT_NOSALARYROW_NUMBER
28120229351
109600270002
65670312753
145622320004
121125330005

Тот же запрос, но с секционированием по номерам отделов.

SELECT EMP_NO, DEPT_NO, SALARY,
    ROW_NUMBER() OVER (PARTITION BY DEPT_NO ORDER BY SALARY)
FROM EMPLOYEE

присвоит номера каждой строке внутри секции с одним отделом.

EMP_NODEPT_NOSALARYROW_NUMBER
120537931
10502128502
127100440001
85100111262.52
3411061637.811
61110688052

RANK

Возвращает ранг строки в секции результатирующего набора. Строки с одинаковыми значениями получают одинаковый ранг в пределах секции. Ранг вычисляется как количество рангов до текущей строки плюс единица.

Например, запрос:

SELECT EMP_NO, DEPT_NO, SALARY,
    RANK() OVER (ORDER BY SALARY)
FROM EMPLOYEE
WHERE SALARY > 33000

вернет такой результат:

EMP_NODEPT_NOSALARYRANK
3612033620.631
114623350002
144672350002
138621360004
134123385005

DENSE_RANK

Возвращает ранг строки в секции без промежутков в ранжировании. Строки с одинаковыми значениями получают одинаковый ранг в пределах секции. Ранг строки равен количеству различных значений рангов в секции, предшествующих текущей строке, увеличенному на единицу.

Например, запрос:

SELECT EMP_NO, DEPT_NO, SALARY,
    RANK() OVER (ORDER BY SALARY)
FROM EMPLOYEE
WHERE SALARY > 33000

вернет такой результат:

EMP_NODEPT_NOSALARYRANK
3612033620.631
114623350002
144672350002
138621360003
134123385004

NTILE

Принимает один параметр — количество групп, на которые необходимо разделить каждую секцию.

Функция NTILE распределяет строки упорядоченной секции в заданное количество групп так, чтобы размеры групп были максимально близки. Группы нумеруются, начиная с единицы. Для каждой строки функция NTILE возвращает номер группы, которой принадлежит строка. Если количество строк в секции не делится на необходимое число, то формируются группы двух размеров, отличающихся на единицу. Группы большего размера следуют перед группами меньшего размера в порядке, заданном в предложении OVER.

Например, запрос:

SELECT EMP_NO, DEPT_NO, SALARY,
    NTILE(25) OVER (ORDER BY SALARY)
FROM EMPLOYEE

вернет такой результат:

EMP_NODEPT_NOSALARYNTILE
28120229351
109600270001
65670312752
145622320002
105021285023
110115600000024
118115748000025

Навигационные функции

FIRST_VALUE и LAST_VALUE

Функция FIRST_VALUE возвращает первое значение из упорядоченного набора значений.

Функция LAST_VALUE возвращает последнее значение из упорядоченного набора значений рамки окна.

LAG и LEAD

LAG(<выражение> [, <смещение> [, <default>]]) OVER (...)
LEAD(<выражение> [, <смещение> [, <default>]]) OVER (...)

Эти функции обеспечивают доступ к строке с заданным физическим смещением перед или после текущей строки (LAG и LEAD соответственно). Если смещение указывает за пределы секции, то будет возвращено значение , которое по умолчанию равно NULL. Параметр <выражение> может содержать столбец таблицы, константу, переменную, выражение,неагрегатную функцию. Параметр <смещение> — количество строк до строки перед (LAG) или после (LEAD) текущей строки, из которой необходимо получить значение. Если значение аргумента не указано, то по умолчанию принимается 1. <смещение> может быть столбцом, вложенным запросом или другим выражением, с помощью которого вычисляется целая положительная величина.

NTH_VALUE

NTH_VALUE(<выражение> [, <смещение> ]) [FROM FIRST | FROM LAST]
OVER  (...)

Функция NTH_VALUE возвращает N-ое значение, начиная с первой (опция FROM FIRST) или последней (опция FROM LAST) записи. По умолчанию используется опция FROM FIRST. Смещение 1 от первой записи будет эквивалентно функции FIRST_VALUE, смещение 1 от последней записи будет эквивалентно функции LAST_VALUE. Параметр <выражение> может содержать столбец таблицы, константу, переменную, выражение,неагрегатную функцию. Параметр <смещение> — номер записи, начиная с первой (опция FROM FIRST) или последней (опция FROM LAST) записи.

Пример

В качестве примера, рассмотрим запрос:

SELECT EMP_NO, SALARY,
    FIRST_VALUE(SALARY) OVER (ORDER BY SALARY),
    LAST_VALUE(SALARY) OVER W,
    NTH_VALUE(SALARY, 2) OVER W,
    LAG(SALARY) OVER (ORDER BY SALARY),
    LEAD(SALARY) OVER (ORDER BY SALARY)
  FROM EMPLOYEE
    WINDOW W AS (ORDER BY SALARY ROWS 
      BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
  ORDER BY SALARY

Запрос вернет следующий результат:

EMP_NOSALARYFIRST_VALUELAST_VALUENTH_VALUELAGLEAD
28229352293574800002700027000
10927000229357480000270002293531275
6531275229357480000270002700032000
14532000229357480000270003127533000

Контрольные вопросы

  1. На сколько сократиться количество возвращаемых строк, если использовать оконные функции с секционированием?
  2. Как отличить агрегатную функцию от оконной?
  3. Можно ли использовать разные секции оконных функций в одном запросе?
  4. Что означает враза “нарастающим итогом”?
  5. Для чего используется секционирование?
  6. Какой смысл несет сортировка?
  7. Что называют рамкой окна?
  8. Как можно указать границы рамки?
  9. Чем отличается диапазон значений от диапазона строк?
  10. Для какой цели используются именованные окна?
  11. Какие ограничения есть у базовых окон?
  12. Какие ограничения есть у наследующих окон?
  13. В каком порядке функция ROW_NUMBER присваивает номера строкам?
  14. В чем разница RANK и DENSE_RANK?
  15. Что произойдет, если NTILE не сможет поделить строки секции на группы равного размера?
  16. Если указать функции LAG отрицательное смещение, будет ли она эквивалентна функции LEAD?

Представления

Представление - это виртуальная (реально не существующая) таблица, которая в базе данных не хранится. В БД храниться лишь описание этой виртуальной таблицы. Основой представления является оператор SELECT произвольной сложности, который задает выборку данных из одной или более таблиц, других представлений, а также селективных хранимых процедур. В базе данных хранится оператор SELECT, но не результаты его выполнения. Результат в виде набора данных создается при обращении к представлению. К представлениям можно обращаться в операторах SELECT, представления могут принимать участие в операциях объединения (UNION) и соединения (JOIN), заданных в операторе выборки данных. Другими словами, представление можно использовать практически также, как и реальную таблицу.

Представления, задавая выборку не всех, а только отдельных столбцов и строк исходных таблиц, дают возможность скрыть от рядового пользователя системы некоторые данные, не предназначенные для широкого просмотра. Например, это могут быть оклады сотрудников, пароли, некоторые количественные характеристики деятельности организации и др. В этом отношении представления являются хорошим средством повышения безопасности данных.

Для создания представления используется оператор CREATE VIEW.

CREATE VIEW <имя представления> [(<список столбцов>)]
AS <оператор SELECT>

После имени создаваемого представления может идти список имен полей представления, заключенный в скобки. Если в представлении присутствуют элементы, значения которых получаются из выражений, то такой список столбцов является обязательным. В остальных случаях список можно не указывать. Обращение к столбцам можно осуществлять по именам столбцов, которые указаны в списке выбора главного оператора SELECT в представлении. Однако хорошей практикой является явное задание списка столбцов в самом представлении. Имена в списке могут быть никак не связаны с именами столбцов базовых таблиц. При этом их количество должно точно соответствовать количеству столбцов в списке выбора главного оператора SELECT представления. По этим именам в списке к столбцам полученного набора данных можно обращаться в операторе SELECT, вызывающем данное представление.

После ключевого слова AS следует оператор SELECT. Здесь можно выполнять объединение (UNION) и соединение (JOIN) различных таблиц, использовать предложение WHERE для задания условий выбора строк.

Для изменения существующего представления используется оператор

ALTER VIEW <имя представления> [(<список столбцов>)]
AS <оператор SELECT>

Синтаксис оператора ALTER VIEW полностью аналогичен синтаксису оператора CREATE VIEW.

Для создание нового или изменение существующего представления используется оператор CREATE OR ALTER VIEW.

CREATE OR ALTER VIEW <имя представления> [<список столбцов>]
AS <оператор SELECT>

Оператор CREATE OR ALTER VIEW создаёт представление, если оно не существует. В противном случае он изменит представление с сохранением существующих зависимостей.

Для удаления существующего в базе данных представления используется оператор DROP VIEW.

DROP VIEW <имя представления>;

Представление нельзя удалить, если на него есть ссылки в другом представлении, в хранимой процедуре или в ограничении CHECK столбца таблицы или соответствующего ограничения на уровне таблицы.

В качестве примера рассмотрим следующее представление.

CREATE VIEW SELECT_PEOPLE (C1, C2, C3)
AS
	SELECT
			P.FULLNAME AS FULLNAME,
			PM.FULLNAME AS MOTHER,
			PF.FULLNAME AS FATHER
		FROM PEOPLE P
			LEFT OUTER JOIN PEOPLE PM
				ON P.CODMOTHER = PM.COD
			LEFT OUTER JOIN PEOPLE PF
				ON P.CODFATHER = PF.COD;

В данном случае представление основано на запросе, в котором таблицы PEOPLE соединяется дважды сама с собой. Каждое соединение добавляет к столбцам записи, столбцы из строки с информацией о матери и об отце. В результате, такое представление позволяет делать более простые и понятные выборки из БД. Например,

SELECT * FROM SELECT_PEOPLE;

Контрольные вопросы

  1. Что такое “представление”?
  2. Приведите пример полезного применения представления?
  3. Где происходит обращение к представлению?
  4. Где хранятся данные, которые выбираются из представления?

Изменение данных

Вставка записей

Оператор INSERT используется для добавления новых строк в таблицу. Он позволяет вставлять данные в одну или несколько строк, а также поддерживает вставку данных из других таблиц с помощью подзапросов. Рассмотрим основные варианты использования INSERT.

Явное указание значений

INSERT INTO <таблица> [(<столбцы>)] VALUES(<значения>)

Вставляет строку в таблицу с указанными значениями. В списке VALUES должны быть указаны значения всех столбцов из списка столбцов в соответствующем порядке. Если список столбцов отсутствует, должны быть указаны все столбцы таблицы кроме вычисляемых.

Например, добавим новый проект в таблицу PROJECT, указав все необходимые поля.

INSERT INTO PROJECT VALUES ('RDB', 'RedDatabase', 'The Best Relation Database Management System', NULL, 'software')

Последнее поле — это поле по умолчанию. Его можно было не указывать. Но тогда необходимо указать список вставляемых столбцов. Также можно не указывать поля, обозначенные NOT NULL. Например,

INSERT INTO PROJECT (PROJ_ID, PROJ_NAME) VALUES ('RDB', 'RedDatabase')

Вставка результатов запроса

Можно вставить данные в таблицу из результата подзапроса.

INSERT INTO <таблица> [(<столбцы>)] SELECT <список выбора> ...

В этом случае выходные столбцы оператора SELECT, должны предоставить значения для каждого целевого столбца в списке столбцов, в том же порядке и совместимого типа. Если список столбцов отсутствует, то значения должны быть предоставлены для каждого столбца таблицы или представления (исключая вычисляемые столбцы).

Например, создадим новую таблицу SOFTWARE_PROJECT и вставим в нее все проекты из таблицы PROJECT, где тип продукта software.

CREATE TABLE SOFTWARE_PROJECT (
    PROJ_ID PROJNO NOT NULL,
    PROJ_NAME VARCHAR(20) NOT NULL,
    PROJ_DESC BLOB SUB_TYPE TEXT SEGMENT SIZE 800,
    TEAM_LEADER EMPNO,
    PRODUCT PRODTYPE);

INSERT INTO SOFTWARE_PROJECT SELECT * FROM PROJECT WHERE PRODUCT='software';

Аналогично вставке значений, можно использовать не все поля.

INSERT INTO SOFTWARE_PROJECT (PROJ_ID, PROJ_NAME) 
    SELECT P.PROJ_ID, P.PROJ_NAME FROM PROJECT P WHERE PRODUCT='software'

Конечно, имена столбцов в таблице источнике не обязательно должны быть такими же, как и в таблице приёмнике. Любой тип оператора SELECT разрешён, пока его выходные столбцы точно соответствуют столбцам вставки по числу и типу. Типы не должны быть точно такими же, но они должны быть совместимыми по присваиванию.

Добавление строки со значениями по умолчанию

Предложение DEFAULT VALUES позволяет вставлять записи без указания значений вообще.

INSERT INTO <таблица> DEFAULT VALUES

Это возможно, только все поля NOT NULL, имеют значения по умолчанию, или эти значения устанавливаются в BEFORE INSERT триггере.

Например:

INSERT INTO JOURNAL DEFAULT VALUES

Получение вставленных значений

Оператор INSERT вставляющий только одну строку может включать необязательное предложение RETURNING, которое позволяет вернуть данные вставленной строки. Это полезно, например, для получения значения автоматически генерируемого первичного ключа.

INSERT ... [RETURNING <возвращаемый список выбора>]

Если предложение указано, то оно может содержать любые столбцы, указанные в операторе, или другие столбцы таблицы, в которую выполняется вставка.

Возвращаемые значения содержат все изменения, произведённые в триггерах BEFORE.

Например, создадим таблицу с автоматически генерируемым первичным ключом.

CREATE TABLE EVENT (
    EVENT_ID INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    EVENT_DESCRIPTION VARCHAR(100)
)

Теперь допустим, что в программе нам нужно вставить новую запись и получить значений сгенерированного значения поля EVENT_ID новой записи.

INSERT INTO EVENT (EVENT_DESCRIPTION) VALUES ('Power On') RETURNING EVENT_ID

Этот запрос выполниться как SELECT и вернет одну запись с полем EVENT_ID.

EVENT_ID
20

Обновление записей

Оператор UPDATE используется для изменения существующих данных в таблице. Он позволяет обновлять значения в одной или нескольких строках на основе заданных условий. Рассмотрим основные варианты использования UPDATE.

UPDATE <таблица> SET поле = <новое значение> 
                  [, поле = <новое значение> ...]
    [WHERE <условие поиска>]
    [RETURNING <возвращаемый список выбора>]

Изменяемые столбцы

Изменяемые столбцы указываются в предложении SET. Столбцы и их значения перечисляются через запятую. Слева имя столбца, а справа значение или выражение.

Разрешено использовать имена столбцов в выражениях справа. При этом использоваться будет всегда старое значение столбца, даже если присваивание этому столбцу уже произошло ранее в перечислении SET. Один столбец может быть использован слева только один раз в конструкции SET.

Допустим у нас есть таблица T:

AB
10
20

После выполнения оператора

UPDATE T SET A = 5, B = A
AB
51
52

Обратите внимание, что старые значения 1 и 2 используются для обновления столбца B, даже после того как столбцу A были назначено новое значение 5.

Ограничение записей

Предложение WHERE ограничивает набор обновляемых записей заданным условием. Формат, синтаксис и смысл данной секции совпадает с одноименной секцией оператора SELECT.

Рассмотрим несколько примеров.

UPDATE ADDRESSES
    SET CITY = 'Saint Peterburg', CITYCODE = 'PET'
    WHERE CITY = 'Leningrad'

Здесь мы переименовали город, заменив в соответствующей строке два поля.

UPDATE EMPLOYEES
    SET SALARY = 2.5 * SALARY
    WHERE TITLE = 'CEO'

Здесь мы использовали значение поля SALARY для того, чтобы вычислить новое значение этого же поля.

UPDATE EMPLOYEE E
    SET SALARY = SALARY * 1.05
    WHERE EXISTS(SELECT * FROM EMPLOYEE_PROJECT EP WHERE E.EMP_NO = EP.EMP_NO);

В данном примере всем сотрудникам, которые заняты в каком-либо проекте, мы увеличиваем зарплату на 5%.

Выражение CASE

Выражение CASE можно использовать для условного обновления данных.

Например, увеличим зарплату сотрудникам в зависимости от отдела:

UPDATE EMPLOYEE
SET SALARY = CASE
    WHEN DEPT_NO = '000' THEN SALARY - 50
    WHEN DEPT_NO = '621' THEN SALARY - 70
    ELSE SALARY - 30
END

Получение обновленных значений

Оператор UPDATE, может включать секцию RETURNING для возврата значений из обновляемой строки. В RETURNING могут включаться любые столбцы, необязательно только те, которые обновляются.

UPDATE employee
  SET salary = salary * 1.1
  RETURNING salary

Вставка или обновление записей

Если мы не знаем, существует ли запись, которую нужно обновить, или её требуется добавить, удобно использовать оператор UPDATE OR INSERT.

Его синтаксис:

UPDATE OR INSERT INTO <таблица> [(столбцы>)]
    VALUES (<значения>)
    [MATCHING (<столбцы>)]
    [RETURNING <возвращаемый список выбора>]

Что именно произойдёт, вставка или обновление, зависит от значений столбцов в предложении MATCHING, а в случае, если оно не указано, то от значений столбцов первичного ключа. Если найдены записи, совпадающие с указанными значениями, то они обновятся, а иначе будет добавлена новая запись.

Когда у таблицы нет первичного ключа, то обязательно надо указать MATCHING.

Предложение RETURNING используется так же как и в операторах UPDATE или INSERT.

Пример запроса:

UPDATE OR INSERT INTO PROJECT 
    VALUES ('RDB', 'RedDatabase', 'The Best Relation Database Management System', NULL, 'software')
    MATCHING (PROJ_ID)

Удаление записей

Оператор DELETE в SQL используется для удаления одной или нескольких строк из таблицы. DELETE позволяет удалять данные на основе условий, указанных в предложении WHERE. Если условие не указано, удаляются все строки из таблицы.

Синтаксис оператора DELETE:

DELETE FROM target [[AS] <псевдоним>]
    [WHERE <условие поиска>]
    [RETURNING <возвращаемый список выбора>]

Удалим все записи из таблицы employee:

DELETE FROM EMPLOYEE

Чаще всего DELETE используется с условием, чтобы удалить только определенные строки.

Например, удалим сотрудника с emp_no = 5:

DELETE FROM EMPLOYEE WHERE EMP_NO = 5

Можно использовать подзапросы для удаления данных на основе другой таблицы.

Например, удалим всех сотрудников, которые работают в отделах, расположенных в Бостоне:

DELETE FROM EMPLOYEE WHERE DEPT_NO IN 
    (SELECT DEPT_NO FROM DEPARTMENT WHERE LOCATION = 'Boston')

Предложение RETURNING позволяет выбрать данные удаленных записей.

Например, удалим сотрудника с emp_no = 3 и получим его данные:

DELETE FROM EMPLOYEE WHERE EMP_NO = 3
    RETURNING EMP_NO, FIRST_NAME, LAST_NAME;

Если таблица связана с другими таблицами через внешние ключи с опцией ON DELETE CASCADE, удаление строки из родительской таблицы приведет к автоматическому удалению связанных строк из дочерних таблиц.

Предположим, что у нас есть таблица PROJECTS, связанная с таблицей EMPLOYEE через внешний ключ:

CREATE TABLE PROJECTS (
    PROJECT_ID INT PRIMARY KEY,
    PROJECT_NAME VARCHAR(100),
    EMP_NO INT,
    FOREIGN KEY (EMP_NO) REFERENCES EMPLOYEE(EMP_NO) ON DELETE CASCADE
)

Удаление сотрудника из таблицы EMPLOYEE автоматически удалит все его проекты из таблицы PROJECTS:

DELETE FROM EMPLOYEE WHERE EMP_NO = 1;

Слияние наборов данных

Оператор MERGE производит слияние записей источника в целевую таблицу. Источником может быть таблица, представление, хранимая процедура или производная таблица. Каждая запись источника используется для обновления (предложение UPDATE) или удаления (предложение DELETE) одной или более записей цели, или вставки (предложение INSERT) записи в целевую таблицу, или ни для того, ни для другого. Условие обычно содержит сравнение столбцов в таблицах источника и цели.

Синтаксис оператора MERGE:

MERGE INTO <целевая таблица> [[AS] <псевдоним>]
    USING <источник> [[AS] псевдоним] 
    ON <условие соединения> 
    [WHEN MATCHED [ AND <условие> ]
        THEN { UPDATE SET <присвоение новых значение> | DELETE }]
    ...
    [WHEN NOT MATCHED [BY TARGET] [ AND <условие> ]
        THEN INSERT [ (<список столбцов>) ] VALUES (<список значений>)]
    ...
    [WHEN NOT MATCHED BY SOURCE [ AND <условие> ]
        THEN { UPDATE SET <присвоение новых значение> | DELETE }]
    ...
    [RETURNING <возвращаемый список выбора>]

В списке VALUES предложения INSERT и списке SET предложения UPDATE вместо значения столбца можно использовать ключевое слово DEFAULT. В этом случае столбец получит значение по умолчанию, указанное при определении целевой таблицы. Если значение по умолчанию для столбца отсутствует, то столбец получит значение NULL.

Допускается указывать несколько предложений WHEN MATCHED и WHEN NOT MATCHED.

Предложение WHEN NOT MATCHED BY TARGET вызывается, когда исходная запись не совпадает с ни с одной записью в целевой таблице. INSERT изменит целевую таблицу.

Предложение WHEN NOT MATCHED BY SOURCE вызывается, когда целевая запись не совпадает ни с одной записью в источнике. UPDATE или DELETE изменяют целевую таблицу.

Предложения WHEN проверяются в указанном порядке. Если условие в предложении WHEN не выполняется, то оно пропускается и происходит переход к следующему предложению. Так будет происходить до тех пор, пока условие для одного из предложений WHEN не будет выполнено. В этом случае выполняется действие, связанное с этим предложением WHEN, а затем осуществляется переход на следующую строку источника. Для каждой строки источника выполняется только одно действие.

Примеры использования MERGE.

Обновление и вставка данных

Предположим, у нас есть две таблицы: EMPLOYEE (целевая таблица) и NEW_EMPLOYEES (источник данных). Мы хотим:

  • Обновить данные сотрудников, если они уже существуют в таблице EMPLOYEE.
  • Вставить новых сотрудников, если их нет в таблице EMPLOYEE.

Таблица EMPLOYEE:

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYHIRE_DATE
1ИванИвановD01500002020-01-15
2МарияПетроваD02600002019-05-20

Таблица NEW_EMPLOYEES:

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYHIRE_DATE
2МарияПетроваD02650002019-05-20
3АлексейСидоровD01550002021-03-10

Запрос MERGE:

MERGE INTO EMPLOYEE AS T
    USING NEW_EMPLOYEES AS S
    ON (T.EMP_NO = S.EMP_NO)
    WHEN MATCHED THEN
        UPDATE SET SALARY = S.SALARY, HIRE_DATE = S.HIRE_DATE
    WHEN NOT MATCHED THEN
        INSERT (EMP_NO, FIRST_NAME, LAST_NAME, DEPT_NO, SALARY, HIRE_DATE)
        VALUES (S.EMP_NO, S.FIRST_NAME, S.LAST_NAME, S.DEPT_NO, S.SALARY, S.HIRE_DATE)

Результат в таблице EMPLOYEE:

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYHIRE_DATE
1ИванИвановD01500002020-01-15
2МарияПетроваD02650002019-05-20
3АлексейСидоровD01550002021-03-10

Удаление устаревших данных

Допустим, мы хотим удалить сотрудников, которые больше не работают в компании (их нет в таблице NEW_EMPLOYEES).

MERGE с удалением:

MERGE INTO EMPLOYEE AS T
    USING NEW_EMPLOYEES AS S
    ON (TARGET.EMP_NO = S.EMP_NO)
    WHEN MATCHED THEN
        UPDATE SET SALARY = S.SALARY, HIRE_DATE = S.HIRE_DATE
    WHEN NOT MATCHED BY SOURCE THEN
        DELETE

Результат в таблице EMPLOYEE:

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYHIRE_DATE
2МарияПетроваD02650002019-05-20

Использование подзапроса

Вместо таблицы NEW_EMPLOYEES можно использовать подзапрос.

Запрос:

MERGE INTO EMPLOYEE AS T
    USING (SELECT * FROM NEW_EMPLOYEES WHERE DEPT_NO = 'D01') AS S
    ON (T.EMP_NO = S.EMP_NO)
    WHEN MATCHED THEN
        UPDATE SET SALARY = S.SALARY, HIRE_DATE = S.HIRE_DATE
    WHEN NOT MATCHED THEN
        INSERT (EMP_NO, FIRST_NAME, LAST_NAME, DEPT_NO, SALARY, HIRE_DATE)
        VALUES (S.EMP_NO, S.FIRST_NAME, S.LAST_NAME, S.DEPT_NO, S.SALARY, S.HIRE_DATE)

В результате обновляются или вставляются только сотрудники из отдела D01.

Дополнительные условия

В следующем примере происходит ежедневное обновление таблицы PRODUCTS на основе заказов, обработанных в таблице SALES. Если количество заказов на продукт таково, что уровень запасов продукта опускается до нуля или становится ещё ниже, то строка этого продукта удаляется из таблицы PRODUCTS.

MERGE INTO PRODUCTS AS T
    USING (SELECT ID_PRODUCT, SUM(QUANTITY) 
                FROM SALES WHERE BYDATE = CURRENT_DATE
                GROUP BY 1
    ) AS S(ID_PRODUCT, QUANTITY)
    ON T.ID_PRODUCT = S.ID_PRODUCT
    WHEN MATCHED AND T.QUANTITY - S.QUANTITY <= 0 THEN
        DELETE
    WHEN MATCHED THEN
        UPDATE SET T.QUANTITY = T.QUANTITY - S.QUANTITY, T.BYDATE = CURRENT_DATE

Контрольные вопросы

  1. Как вставить записи в несколько таблиц?
  2. Можно ли вставить запись, не указывая вставляемые столбцы?
  3. Какие значения получат столбцы, если им явно не указали значения?
  4. В каком случае может быть вставлено несколько записей?
  5. Можно ли вставить запись, вообще не указывая значений?
  6. Как получить сгенерированное сервером значение первичного ключа для только что вставленной записи?
  7. Какие записи будут обновлены если не указывать WHERE?
  8. Приведите пример обновления нескольких столбцов таблицы в одном операторе.
  9. Можно ли использовать столбцы, для которых выполняется обновление, для обновления других столбцов?
  10. Как выбрать новое значение столбца в зависимости от некоторого условия?
  11. Как получить новые значения столбцов?
  12. Каким образом проверяется существование записи в таблице, если в операторе UPDATE OR INSERT не указано предложение MATCHING?
  13. Сколько записей удалит оператор DELETE, если не указать условие поиск записей?
  14. Может ли оператор DELETE вернуть записи?
  15. Что произойдет, если на удаляемую запись существуют ссылки в других таблицах?
  16. Приведите примеры слияния наборов данных?
  17. Что произойдет с записями источника, которые не найдут совпадения в целевой таблице?
  18. Что произойдет с записями целевой таблицы, для которых не найдется совпадений в источнике?
  19. Что произойдет с совпавшими записями при слиянии?
  20. Для чего используются дополнительные условия в условиях совпадения?

Процедурный язык PSQL

Элементы языка PSQL

В реляционных базах данных используются в первую очередь декларативные средства, когда пользователь системы в операторах DDL и DML описывает, что он хочет сделать с данными или метаданными базы данных, но не указывает, как это должно быть сделано. Кроме этих возможностей, можно использовать и императивные, процедурные, средства, когда пользователь точно шаг за шагом описывает выполняемые действия как с данными базы данных, так и с любыми другими внутренними данными.

Для этих целей используются программные элементы базы данных — хранимые процедуры, функции и триггеры.

Для описания алгоритмов обработки данных в хранимых процедурах, функциях и в триггерах используется расширение языка SQL. Это расширение называется процедурным SQL (PSQL) или языком хранимых процедур, функций и триггеров. Язык содержит обычные операторы присваивания, операторы ветвления и операторы циклов. В триггерах могут применяться специфические контекстные переменные.

Этот язык является обычным языком программирования, который содержит все основные конструкции классических языков программирования. Кроме того, в нем присутствуют несколько модифицированные операторы добавления, изменения, удаления и выборки существующих данных из таблиц базы данных (INSERT, UPDATE, DELETE и SELECT).

В хранимых процедурах, функциях и триггерах можно использовать средства оповещения клиентов о некоторых событиях и выбрасывать и обрабатывать исключения.

В синтаксисе PSQL можно выделить заголовок и тело. Заголовок содержит имя программного объекта, описание локальных переменных. Для триггеров в заголовке указывается событие базы данных и фаза, при которой автоматически вызывается триггер. В заголовке хранимой процедуры можно указать входные и выходные параметры. В заголовке хранимой функции можно указать входные параметры и тип выходного результата. Тело хранимой процедуры, функции или триггера представляет собой блок операторов, содержащий описание выполняемых программой действий. Блок операторов заключается в операторные скобки BEGIN и END. В самих программах возможно присутствие произвольного количества блоков, как последовательных, так и вложенных друг в друга.

Рассмотрим пример простейшей хранимой процедуры, возвращающей случайное число от 0 до 100.

CREATE OR ALTER PROCEDURE R RETURNS (I INT)
AS
BEGIN
   I = RAND() * 100;
   SUSPEND;
END;

Эта процедура не принимает входных параметров, но возвращает параметр с именем I, целочисленного типа данных. В теле процедуры возвращаемому параметру I присваивается результат и передается вызывающей стороне с помощью оператораSUSPEND.

Созданную хранимую процедуру можно использовать в запросах SELECT. Например, запрос:

SELECT * FROM R;

Может вернуть следующий результат:

I
23

Локальные переменные

Для описания одной локальной переменной используется оператор DECLARE VARIABLE. Синтаксис оператора представлен в листинге:

DECLARE [VARIABLE] <переменная> <тип данных SQL> [NOT NULL] [= <значение>]

Ключевое слово VARIABLE можно не указывать. В одном операторе можно объявить только одну локальную переменную, но можно объявлять произвольное количество локальных переменных, используя для каждой переменной отдельный оператор DECLARE VARIABLE.

Имя переменной должно быть уникальным среди всех имен локальных переменных, имен входных и выходных параметров данного программного объекта.

Типом данных может быть любой тип данных, используемый в SQL.

Вместо типа данных можно указать имя домена. В этом случае переменной присваиваются все характеристики домена — запрет пустого значения (NOT NULL), значение по умолчанию (DEFAULT) и условие (CHECK), которому должно удовлетворять значение, помещаемое в переменную.

Для локальных переменных можно указать ограничение NOT NULL, тем самым запретив передавать в него значение NULL.

Локальной переменной можно устанавливать инициализирующее (начальное) значение. Это значение устанавливается с помощью предложения DEFAULT или оператора =. В качестве значения по умолчанию может быть использовано значение NULL, литерал и любая контекстная переменная совместимая по типу данных.

Например, добавим в наш пример объявление локальной переменной, для замены константы.

CREATE OR ALTER PROCEDURE R RETURNS (I INT)
AS
DECLARE N INT = 100;
BEGIN
   I = RAND() * N;
   SUSPEND;
END;

Входные и выходные параметры

В хранимых процедурах могут быть также описаны входные и выходные параметры, в хранимых функциях только входные параметры. Список входных параметров записывается после имени хранимой процедуры или функции и заключается в круглые скобки:

<имя параметра> <тип данных SQL> [NOT NULL] [= <значение по умолчанию>]

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

Выходные параметры хранимой процедуры описываются в предложении RETURNS:

RETURNS (<параметр> [, <параметр> ...])

Локальные переменные триггеров, функций и процедур, входные и выходные параметры, используемые только в хранимых процедурах и функциях, являются внутренними переменными. Имена внутренних переменных могут совпадать с именами столбцов используемых таблиц базы данных. Это не вызовет проблемы двусмысленности имен. При использовании внутренних переменных в операторах SELECT, INSERT, UPDATE или DELETE именам внутренних переменных всегда должно предшествовать двоеточие, чтобы не спутать их с именами столбцов таблицы. Во всех остальных случаях в любых других операторах имена внутренних переменных записываются обычным образом без двоеточия.

Например, изменим наш пример таким образом, чтобы указывать верхний диапазон значений.

CREATE OR ALTER PROCEDURE R(N INT = 100) RETURNS (I INT)
AS
BEGIN
   I = RAND() * N;
   SUSPEND;
END;

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

SELECT * FROM R 
   UNION ALL 
SELECT * FROM R(1000);

Такой запрос может вернуть

I
80
993

Оператор присваивания

Синтаксис оператора присваивания значения внутренней переменной имеет следующий вид:

<переменная> = <выражение>;

Выражением может быть любое правильное выражение SQL. Оно может содержать константы, контекстные переменные, имена внутренних переменных, арифметические, логические и строковые операции, обращения к встроенным функциям и к функциям, определенным пользователем.

Арифметическое выражение содержит четыре арифметические операции — сложение (+), вычитание (-), умножение (*) и деление (/).

Строковое выражение представлено одной строковой операцией конкатенации (||) — соединения двух строк в одну.

Логическое выражение может содержать операцию отрицания (NOT), дизъюнкции (OR) и конъюнкции (AND).

Конструкция NEXT VALUE FOR <имя генератора> является аналогом функции GEN_ID (<имя генератора>, 1). Значение указанного генератора увеличивается на единицу, и конструкция возвращает новое значение.

В качестве выражения может быть также указано пустое значение NULL.

В ранее рассмотренных примерах оператор присваивания уже использовался.

Оператор ветвления IF

Для выполнения ветвления процесса обработки данных в PSQL используется оператор IF. Его синтаксис:

IF (<условие>)
   THEN <составной оператор>
   [ELSE <составной оператор>];

Условием является обычное условие, принятое в SQL, которое может возвращать значения TRUE, FALSE или UNKNOWN. Если условие возвращает значение TRUE, то выполняется составной оператор после ключевого слова THEN. Иначе (если условие возвращает FALSE или UNKNOWN) выполняется составной оператор после ключевого слова ELSE, если это ключевое слово присутствует. Условие всегда заключается в круглые скобки.

Составной оператор — это одиночный оператор или блок операторов, заключенных в операторные скобки BEGIN и END.

Например, пусть процедура возвращает значения ‘Четное’ или ‘Нечетное’, для сгенерированного случайного числа.

CREATE OR ALTER PROCEDURE R RETURNS (I INT, S VARCHAR(16))
AS
BEGIN
   I = RAND() * 100;
   IF (MOD(I, 2) = 0)
   	THEN
   		S = 'Четное';
   	ELSE
   		S = 'Нечетное';
   SUSPEND;
END;

SELECT * FROM R;

Такой запрос может вернуть

IS
80Четное

Оператор цикла WHILE

Оператор WHILE позволяет организовать в PSQL обычный цикл с условием.

WHILE (<условие>) DO
   <составной оператор>

Составной оператор будет выполняться в цикле, пока условие возвращает значение TRUE.

Циклы могут быть вложенными, глубина вложения не ограничена.

Изменим наш пример таким образом, чтобы он генерировал заданное количество случайных чисел.

CREATE OR ALTER PROCEDURE R(N INT) RETURNS (I INT)
AS
BEGIN
    WHILE (N > 0) DO
    BEGIN
        N = N - 1;
        I = RAND() * 100;
        SUSPEND;
    END
END;

SELECT * FROM R(5);

Такой запрос может вернуть

I
79
36
45
3
47

Операторы перехода

В PSQL не существует оператора GOTO, выполняющего переход на указанную метку в программном тексте, что соответствует правилам структурного программирования. При этом есть операторы, позволяющие выйти из циклов или перейти на начало этого же или другого цикла (LEAVE), перейти на финальный оператор END (EXIT), временно приостановить выполнение хранимой процедуры для передачи вызвавшей стороне полученных данных (SUSPEND) и досрочно начать новую итерацию цикла (CONTINUE).

Оператор EXIT

Оператор EXIT позволяет из любой точки триггера или хранимой процедуры, функции перейти на конечный оператор END, то есть завершить выполнение программы.

EXIT;

Изменим наш пример, чтобы он использовал оператор EXIT.

CREATE OR ALTER PROCEDURE R(N INT) RETURNS (I INT)
AS
BEGIN
    WHILE (TRUE) DO
    BEGIN
        N = N - 1;
        I = RAND() * 100;
        SUSPEND;
        IF (N = 0)
           THEN
               EXIT;
    END
END;

Оператор LEAVE

Этот оператор осуществляет выход из цикла. Если указана метка, то будет осуществлен выход из внешнего цикла, помеченного этой меткой.

LEAVE [<метка>];

Изменим наш пример, чтобы он использовал оператор EXIT.

CREATE OR ALTER PROCEDURE R(N INT) RETURNS (I INT)
AS
BEGIN
   LOOP:
   WHILE (TRUE) DO
   BEGIN
      N = N - 1;
      I = RAND() * 100;
      SUSPEND;
      IF (N = 0)
         THEN
            LEAVE LOOP;
   END
END;

Здесь в операторе WHILE задается бесконечный цикл, поскольку условие выхода из цикла всегда истинно. Фактический выход из цикла осуществляется после соответствующей проверки условия в операторе IF с использованием оператора LEAVE.

Оператор BREAK

Оператор BREAK осуществляет выход из цикла. Код продолжает выполняться с первого оператора после прерванного цикла.

 BREAK;

Оператор BREAK похож на LEAVE, но не поддерживает работу с метками.

Изменим наш пример, чтобы он использовал оператор BREAK.

CREATE OR ALTER PROCEDURE R(N INT) RETURNS (I INT)
AS
BEGIN
    WHILE (TRUE) DO
    BEGIN
        N = N - 1;
        I = RAND() * 100;
        SUSPEND;
        IF (N = 0)
           THEN
               BREAK;
    END
END;

Оператор SUSPEND

Применение оператора SUSPEND допускается только в селективной хранимой процедуры, т.е. такой процедуре, которая должна возвращать записи и применятся в роли источника данных.

SUSPEND;

Этот оператор передает текущие значения возвращаемых значений в буфер и приостанавливает выполнение процедуры, пока вызывающая сторона не обработает эту строку. После этого работа процедуры возобновляется с оператора, следующего непосредственно за оператором SUSPEND, сохраняя весь контекст выполнения.

Все примеры выше использовали оператор SUSPEND.

Оператор CONTINUE

Оператор CONTINUE моментально начинает новую итерацию внутреннего цикла операторов WHILE или FOR. С использованием опционального параметра метки CONTINUE также может начинать новую итерацию для внешних циклов, помеченных меткой.

CONTINUE [<метка>];

Изменим наш пример таким образом, чтобы он генерировал случайные числа, но пропускал нечетные.

CREATE OR ALTER PROCEDURE R(N INT) RETURNS (I INT)
AS
BEGIN
	LOOP:
	WHILE (N > 0) DO
	BEGIN
		N = N - 1;
		I = RAND() * 100;
		IF (MOD(I, 2) <> 0)
			THEN 
				CONTINUE LOOP;
			ELSE
				SUSPEND;
	END
END;

SELECT * FROM R(10);

Такой запрос может вернуть

I
70
46
34
88
30

И обратите внимание, что количество строк результата меньше 10.

Оператор EXECUTE PROCEDURE

Триггеры, хранимые процедуры и функции могут вызывать хранимые процедуры с помощью оператора EXECUTE PROCEDURE. Оператор также может быть использован в подмножестве языка DML.

EXECUTE PROCEDURE <имя процедуры> [(<параметр> [, <параметр>] ...)];

Если процедура получает параметры, то список входных параметров в операторе EXECUTE PROCEDURE является обязательным. При этом требуется полное соответствие количества передаваемых процедуре параметров и их типов данных описанным в процедуре входным параметрам.

Обычные операторы обращения к базе данных

В PSQL можно использовать обычные операторы, выполняющие выборку данных (SELECT), добавление (INSERT), изменение (UPDATE) и удаление (DELETE) данных. Отличительной особенностью использования этих операторов в PSQL является только то, что в них могут использоваться внутренние переменные, перед которыми обязательно должно помещаться двоеточие, чтобы не спутать внутренние переменные с именами столбцов таблицы.

Например, чтобы выполнить удаление регионов страны, чей код задан переменной CODCOUNTRY (в примере имя этой переменной совпадает с именем столбца в таблице регионов REGION), в хранимой процедуре, функции или в триггере нужно выполнить следующий оператор DELETE:

DELETE FROM REGION
   WHERE CODCOUNTRY = :CODCOUNTRY;

Чтобы изменить код страны у всех регионов, относящихся к одной конкретной стране, чей код хранится во внутренней переменной CODCOUNTRY, нужно выполнить оператор UPDATE:

UPDATE REGION
      SET CODCOUNTRY = :NEWCODCOUNTRY
   WHERE CODCOUNTRY = :CODCOUNTRY;

Следующий оператор INSERT добавляет в таблицу стран COUNTRY новую страну. Значения для столбцов новой записи выбираются из внутренних переменных:

INSERT INTO COUNTRY (CODCOUNTRY, NAME, FULLNAME, CAPITAL)
   VALUES (:CODCOUNTRY, :NAME, :FULLNAME, :CAPITAL);

В операторе SELECT, выбирающем данные из таблицы, представления или хранимой процедуры выбора, помимо остальных предложений должно присутствовать предложение INTO, в котором перечисляются имена внутренних переменных, куда будут помещаться значения столбцов считанной строки таблицы. Именам внутренних переменных предшествует двоеточие, чтобы не спутать их с именами столбцов таблицы базы данных. Предложение INTO должно быть последним в этом операторе.

Например, следующий оператор SELECT выбирает строку из таблицы стран COUNTRY с кодом страны, находящимся во внутренней переменной CODCOUNTRY, и помещает краткое название страны и полное название страны в две внутренние переменные NAME и FULLNAME, которые имеют те же имена, что и столбцы таблицы:

SELECT NAME, FULLNAME
   FROM COUNTRY
   WHERE CODCOUNTRY = :CODCOUNTRY
   INTO :NAME, :FULLNAME;

Оператор FOR SELECT

Оператор FOR SELECT является оператором цикла, выбирающим строки из запроса.

FOR
   <оператор SELECT>
   [AS CURSOR <имя курсора>]
   INTO [:]<имя переменной/параметра> [, [:]<имя переменной/параметра> ...]
DO 
   <составной оператор>;

Оператор SELECT выбирает очередную строку из результата запроса, после чего выполняется составной оператор, который может быть одним оператором или блоком операторов, заключенных в операторные скобки BEGIN и END.

Оператор SELECT должен содержать предложение INTO, которое располагается в конце оператора. Цикл повторяется, пока не будут прочитаны все строки. После этого происходит выход из цикла. Цикл также может быть завершен и раньше при использовании оператора LEAVE или BREAK.

Необязательное предложение AS CURSOR создаёт именованный курсор, на который можно ссылаться (с использованием предложения WHERE CURRENT OF) внутри оператора или блока операторов следующего после предложения DO, для того чтобы удалить или модифицировать текущую строку.

Рассмотрим использование именованных курсоров на примере хранимой процедуры, которая удаляет сотрудника с заданным номером.

CREATE PROCEDURE EMP_DEL(EMP_NO SMALLINT)
AS
BEGIN
	FOR SELECT * FROM EMPLOYEE AS CURSOR C DO
      IF (C.EMP_NO = EMP_NO) THEN
         DELETE FROM EMPLOYEE WHERE CURRENT OF C;
END;

Другим примером хранимой процедуры, может быть пример вычисления корней квадратных уравнений, коэффициенты которых хранятся в таблице COEFFS. Мы уже видели этот пример ранее, но сейчас посмотрим как реализовать его с помощью хранимых процедур.

SELECT
    A, B, C,
    IIF (D >= 0, (-B - SQRT(D)) / DENOM, NULL) SOL_1,
    IIF (D > 0, (-B + SQRT(D)) / DENOM, NULL) SOL_2
FROM
    (SELECT A, B, C, B*B - 4*A*C AS D, 2*A AS DENOM FROM COEFFS)

Эту задачу также можно решить с помощью хранимой процедуры.

CREATE PROCEDURE SOLVE 
	RETURNS (A DOUBLE PRECISION, 
				B DOUBLE PRECISION, 
				C DOUBLE PRECISION, 
				X1 DOUBLE PRECISION, 
				X2 DOUBLE PRECISION)
AS
DECLARE D DOUBLE PRECISION;
BEGIN
	FOR SELECT A, B, C FROM COEFFS INTO :A, :B, :C DO
	BEGIN
		X1 = NULL;
		X2 = NULL;
		D = B*B - 4*A*C;
		IF ( D >= 0) THEN
			X1 = ( -B + SQRT(D) ) / ( 2*A );
		IF ( D > 0) THEN
			X2 = ( -B - SQRT(D) ) / ( 2*A );
		SUSPEND;
	END
END;

Пользуясь случаем оцените выразительность декларативного варианта. Вариант на SQL гораздо короче, а главное он может быть подвергнут дополнительной оптимизации, если будет использован в составе более сложного запроса. Однако вариант с хранимой процедурой может быть понятнее и привычнее. Кроме того, это пример использования оператора FOR SELECT и мощности языка PSQL. В реальном проекте это может оказаться решающим аргументов в пользу выбора хранимой процедуры, когда необходимо каждую строку обработать сложным образом, сформировать и выполнить для нее отдельные операторы SQL, вызвать другие хранимые процедуры или функции, в том числе определенные пользователем.

Оператор IN AUTONOMOUS TRANSACTION

Оператор IN AUTONOMOUS TRANSACTION позволяет выполнить оператор или блок операторов в автономной транзакции.

IN AUTONOMOUS TRANSACTION DO 
   <оператор/блок операторов>

Код, работающий в автономной транзакции, будет подтверждаться сразу же после успешного завершения независимо от состояния родительской транзакции. Это бывает нужно, когда определённые действия не должны быть отменены, даже в случае возникновения ошибки в родительской транзакции.

Автономная транзакция имеет тот же уровень изоляции, что и родительская транзакция. Любое исключение, вызванное или появившееся в блоке кода автономной транзакции, приведёт к откату автономной транзакции и отмене всех внесённых изменений. Если код будет выполнен успешно, то автономная транзакция будет подтверждена.

Примером может служить необходимость логирования каких-либо изменений, независимо от основной транзакции.

Допустим у нас есть таблица ACTIONS для хранения событий.

RECREATE TABLE ACTIONS (
	S VARCHAR(64)
)

Добавим в хранимую процедуру для генерации случайного числа логирование ее вызова.

CREATE PROCEDURE R RETURNS (I INT)
AS
BEGIN
	IN AUTONOMOUS TRANSACTION DO
		INSERT INTO ACTIONS VALUES ('CALL OF THE PROCEDURE R');
	I = RAND() * 100;
	SUSPEND;
END

Тогда после выполнения запроса

SELECT * FROM R

В таблице ACTIONS появится запись

S
CALL OF THE PROCEDURE R

Контекстная переменная ROW_COUNT

В триггерах, в хранимых процедурах и функциях могут быть использованы предварительно определенные литералы и контекстные переменные, часть из которых может применяться в любых операторах SQL, другая же часть может быть использована только в триггерах.

Контекстная переменная ROW_COUNT типа INTEGER может быть использована в триггерах и в хранимых процедурах. Она возвращает общее количество строк, которые были прочитаны, добавлены, изменены или удалены в процессе выполнения последнего оператора SQL. Чаще всего эта контекстная переменная используется после оператора SELECT или после оператора FETCH, читающего очередную запись из таблицы, заданной объявленным курсором. В этом случае она содержит количество считанных данным оператором строк. Обычно используется для определения завершения считывания данных из таблицы (представления), определенной объявленным внутренним курсором.

Например, напишем хранимую процедуру, которая выбирает строки из таблицы EMPLOYEE и возвращает количество прочитанных строк. Для наглядности в примере посчитано количество использованных строк, традиционным способом.

CREATE OR ALTER PROCEDURE NUM RETURNS (N INT, R INT)
AS
BEGIN
	N = 0;
	FOR SELECT * FROM EMPLOYEE AS CURSOR C DO
		N = N + 1;
	R = ROW_COUNT;
	SUSPEND;
END;

SELECT * FROM NUM;

Результатом такого запроса будет таблица.

NR
104104

Пользовательские исключения

В триггерах, хранимых процедурах и функциях существует возможность выдачи пользовательских исключений (EXCEPTION). В базе данных создается именованный объект данных, исключение, содержащий текст сообщения вызываемого исключения.

Для вызова в триггере или в хранимой процедуре и функции пользовательского исключения нужно выполнить оператор EXCEPTION

EXCEPTION <имя пользовательского исключения> [<текст сообщения> | USING (<значение> [,<значение>...])];

Оператор EXCEPTION выбрасывает пользовательское исключение с указанным именем. При выбрасывании исключения можно также указать альтернативный текст сообщения, который заменит текст сообщения заданным при создании исключения. Максимальная длина текстового сообщения составляет 1021 байт.

Текст сообщения исключения может содержать слоты для параметров, которые заполняются при выбрасывании исключения. Для передачи значений параметров в исключение используется предложение USING. Параметры рассматриваются слева направо. Каждый параметр передаётся в оператор EXCEPTION как N-ый, начиная с 1:

  • Если N-ый параметр не передан, его слот не заменяется;
  • Если передано значение NULL, слот будет заменён на строку '***null***';
  • Если количество передаваемых параметров будет больше, чем содержится в сообщении исключения, то лишние будут проигнорированы;
  • Максимальный номер параметра равен 9;
  • Общая длина сообщения, включая значения параметров, ограничена 1053 байтами.

Например, в БД employee есть процедура SHIP_ORDER, которая производит необходимые действия по изменению статуса заказа на доставлен (shipped). Однако, если заказ уже доставлен, то будет выброшено пользовательское исключение order_already_shipped.

Приведем фрагмент этого кода.

CREATE OR ALTER PROCEDURE SHIP_ORDER (PO_NUM CHAR(8)) 
AS
...
BEGIN
   SELECT s.order_status, c.on_hold, c.cust_no
      FROM sales s, customer c
      WHERE po_number = :po_num AND s.cust_no = c.cust_no
      INTO :ord_stat, :hold_stat, :cust_no;

   /* This purchase order has been already shipped. */
   IF (ord_stat = 'shipped') THEN
   BEGIN
      EXCEPTION order_already_shipped;
   END
   ...
END;

Обработка исключений

Для обработки ошибочных ситуаций базы данных и пользовательских исключений в языке хранимых процедур, функций и триггеров используется оператор WHEN. Оператор позволяет перехватить любые указанные ошибки базы данных и/или пользовательские исключения (EXCEPTION) при обращении к базе данных. Для определения какая именно ошибка произошла, можно использовать разные контекстные переменные.

WHEN SQLCODE <код ошибки SQLCODE>
   DO <составной оператор>;

SQLCODE это устаревшая классификация ошибок. Одному коду могут соответствовать разные ошибки. В настоящее время этот способ не рекомендован к применению и оставлен для совместимости с существующими БД.

WHEN SQLSTATE <код ошибки SQLSTATE>
   DO <составной оператор>;

SQLSTATE это коды ошибок, определенные стандартом SQL. Он состоит из 5 символов: два первых символа класса ошибки и два символа подкласса. Например, класс 00 - это отсутствие ошибки, 01 - предупреждение, но не ошибка, 02 - отсутствие данных. Полный перечень классов и подклассов приведен в стандарте и в документации к СУБД.

WHEN GDSCODE <код ошибки GDSCODE>
   DO <составной оператор>;

GDSCODE это коды ошибок, определенные в РЕД Базе Данных и Firebird. Наиболее подробная классификация. Все ошибки описаны в документации.

WHEN EXCEPTION <имя пользовательского исключения>
   DO <составной оператор>;

EXCEPTION это имя пользовательского исключения, которое необходимо перехватить и обработать.

WHEN ANY
   DO <составной оператор>;

Ключевое слово ANY означает, что обработка ошибочной ситуации будет выполняться при появлении любой ошибки базы данных и/или любого пользовательского исключения.

Этот оператор должен находиться в самом конце блока, в котором происходят обращения к базе данных, которые могут вызвать ошибки базы данных, непосредственно перед последним оператором END.

После ключевого слова DO помещается оператор или блок операторов, заключенных в операторные скобки BEGIN и END. В этом блоке выполняется обработка возникшей ситуации.

Оператор WHEN вызывается только тогда, когда произошло одно из указанных в его условии событий. В случае выполнения оператора (даже если в нем фактически не было выполнено никаких действий) ошибка или пользовательское исключение считается обработанным. При этом не прерываются и не отменяются действия триггера или хранимой процедуры или функции, где присутствует этот оператор, работа продолжается, как если бы никаких исключительных ситуаций не было.

Оператор перехватывает ошибки и исключения в текущем блоке операторов. Он также перехватывает подобные ситуации во вложенных блоках, если эти ситуации не были в них обработаны.

Следующий пример показывает вариант обработки ошибочной ситуации, возникающей при попытке поместить в таблицу строку, имеющую дублирующее значение первичного или уникального ключа.

INSERT INTO COUNTRY (CODCOUNTRY) VALUES ('USA');

WHEN SQLCODE -803 DO
BEGIN
... 
END;

Однако подобное значение SQLCODE будет сформировано и при попытке поместить в базу данных новой строки, создающей дублирующее значение построенного пользователем индекса. Чтобы более тонко определить данную конкретную ошибочную ситуацию, следует использовать не SQLCODE, а GDSCODE. Например,

INSERT INTO COUNTRY (CODCOUNTRY) VALUES ('USA');

WHEN GDSCODE -335544665 DO 
BEGIN
... 
END;

Этот обработчик срабатывает уже только на дублированное значение первичного ключа помещаемой в базу данных новой записи.

При возникновении ошибок или пользовательских исключений вначале отыскивается оператор WHEN в текущем блоке. Если соответствующий оператор был найден, то выполняется обработка ситуации. Иначе происходит переход на блок операторов выше по иерархии вложенности операторов, пока не будет найден подходящий обработчик возникшей ситуации. Только в том случае, если не будет найден соответствующий оператор обработки данной ситуации, процедура, функция или триггер выдаст сообщение об ошибке.

Использование курсоров

В триггерах, хранимых процедурах и функциях существует возможность использования курсоров — локальных переменных, связанных с оператором SELECT.

Курсор необходимо объявить в коде PSQL как локальную переменную. В процессе выполнения открыть этот курсор (оператор OPEN). После этого можно читать данные при использовании курсора (оператор FETCH). После считывания всех записей курсор нужно закрыть (оператор CLOSE).

Для объявления локальной переменной — курсора используется следующий вариант синтаксиса оператора DECLARE VARIABLE

DECLARE [VARIABLE] <имя курсора> [SCROLL | NO SCROLL] CURSOR 
   FOR (<оператор SELECT>);

Курсор может быть однонаправленным и прокручиваемым. Необязательное предложение SCROLL делает курсор двунаправленным (прокручиваемым), предложение NO SCROLL — однонаправленным. По умолчанию курсоры являются однонаправленными. Однонаправленные курсоры позволяют двигаться по набору данных только вперёд. Двунаправленные курсоры позволяют двигаться по набору данных не только вперёд, но и назад, а также на N позиций относительно текущего положения.

Оператор SELECT задает выборку из базы данных. Это может быть сколь угодно сложный оператор, включающий объединения и соединения таблиц при наличии любых условий выборки данных. Собственно выборка данных осуществляется при выполнении оператора OPEN для этого курсора.

OPEN <имя курсора>;

Этот оператор выполняет оператор SELECT, заданный при объявлении курсора. После выполнения этого оператора возможно получение данных из набора данных указанного курсора.

Данные очередной строки получаются при выполнении оператора FETCH для этого курсора. При этом можно указать какую именно строку необходимо прочитать.

FETCH <имя курсора> [INTO :<переменная> [, :<переменная>]... ];

Оператор FETCH применим только к курсорам, объявленным в операторе DECLARE VARIABLE.

Оператор читает очередную строку, полученную при выполнении оператора SELECT, связанного с данным курсором, и помещает полученные данные во внутренние переменные программы (предложение INTO). Предложение INTO можно не указывать лишь в том случае, если для полученной строки в дальнейшем будет использован только оператор удаления данных DELETE.

В операторе FETCH можно указывать в каком направлении и на сколько записей продвинется позиция курсора.

FETCH NEXT FROM <имя курсора> [INTO ...];
FETCH PRIOR FROM <имя курсора> [INTO ...];
FETCH FIRST FROM <имя курсора> [INTO ...];
FETCH LAST FROM <имя курсора> [INTO ...];
FETCH ABSOLUTE <n> FROM <имя курсора> [INTO ...];
FETCH RELATIVE <n> FROM <имя курсора> [INTO ...];

Предложение NEXT указывает, что указатель курсора должен продвинуться на 1 запись вперёд. Это предложение допустимо использовать как с прокручиваемыми, там и не прокручиваемыми курсорами. Остальные предложения допустимо использовать только с прокручиваемыми курсорами. Предложение PRIOR указывает, что указатель курсора должен продвинуться на 1 запись назад. Предложение FIRST позволяет переместить позицию курсора на первую запись, а предложение LAST – на последнюю. Предложение ABSOLUTE позволяет указать номер позиции, на которую будет установлен курсор. Номер позиции должен быть в диапазоне от 1 до максимального количества записей извлекаемых запросом курсора. Предложение RELATIVE позволяет указать, на какое количество записей относительно текущей позиции необходимо переместить указатель курсора. Если указано положительное число, то курсор перемещает вперёд на N позиций, если отрицательное, то назад.

Для проверки того, что исходные записи для набора данных исчерпаны, используется контекстная переменная ROW_COUNT, которая возвращает количество считанных оператором FETCH строк. Если произошло чтение очередной записи из набора данных, то ROW_COUNT равняется единице, иначе при достижении конца исходных данных эта контекстная переменная будет равна нулю.

После завершения работы с данными, полученными при помощи курсора, необходимо закрыть курсор, используя оператор CLOSE. Вообще говоря, курсор явно можно и не закрывать. Он автоматически будет закрыт по завершении выполнения хранимой процедуры, функции, триггера.

CLOSE <имя курсора>;

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

Следующий фрагмент хранимой процедуры выполняет выборку данных из таблицы стран.

DECLARE VARIABLE NEW_CURSOR
   CURSOR FOR (SELECT CODCOUNTRY, CODREGION, NAMEREG, CENTER
                  FROM VIEW_RUSSIA2);
BEGIN
   OPEN NEW_CURSOR;
   WHILE (1 = 1) DO
      BEGIN
         FETCH NEW_CURSOR
            INTO :CODCOUNTRY, :CODREGION, :NAMEREG, :CENTER;
         IF (ROW_COUNT = 0) THEN
            LEAVE;
         SUSPEND;
      END
   CLOSE NEW_CURSOR;
END

Оператор CALL

Оператор CALL аналогичен EXECUTE PROCEDURE, но позволяет получать определенные выходные параметры или не получать их вовсе.

Важной особенностью оператора CALL является возможность использования позиционной или смешанной передачи параметров. Первыми нужно указать входные параметры, а потом выходные.

Синтаксис оператора CALL:

CALL <имя процедуры> ([<позиционные параметры>][<именованные параметры>])

В качестве примера рассмотрим процедуру вставки нового клиента (CUSTOMER)

CREATE PROCEDURE INSERT_CUSTOMER (LAST_NAME VARCHAR(30), FIRST_NAME VARCHAR(30))
   RETURNS (ID INTEGER, FULL_NAME VARCHAR(62))
AS
BEGIN
   INSERT INTO CUSTOMERS (LAST_NAME, FIRST_NAME)
      VALUES (:LAST_NAME, :FIRST_NAME)
      RETURNING ID, LAST_NAME || ', ' || FIRST_NAME
      INTO :ID, :FULL_NAME;
END

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

Например, если при выполнении запроса в приложении нам необходимо получить только первое поле ID, то можно сделать такой вызов.

CALL INSERT_CUSTOMER('Иванов', 'Иван', ?)

Второй выходной параметр при этом не указан и не будет возвращен.

Если же нам необходимо получить только второй выходной параметр FULL_NAME, а первый ID при этом проигнорировать, то это можно сделать указав NULL вместо игнорируемого параметра.

CALL INSERT_CUSTOMER('Иванов', 'Иван', NULL, ?)

Того же эффекта можно добиться если использовать позиционные параметры.

CALL INSERT_CUSTOMER('Иванов', 'Иван', FULL_NAME => ?)

Можно все параметры передавать как позиционные. Такой запрос выглядел бы следующим образом.

CALL INSERT_CUSTOMER(
   LAST_NAME => 'Иванов', 
   FIRST_NAME => 'Иван',
   FULL_NAME => ?,         -- выходной параметр
   ID => ?                 -- выходной параметр
)

Позиционные параметры полезны в тех случаях, когда у процедуры много аргументов со значениями по умолчанию, но не каждый их них нужен в каждом конкретном вызове.

Контрольные вопросы

  1. Чем отличается PSQL от обычных операторов DML?
  2. Какие объекты БД используют PSQL?
  3. Какие возможности поддерживает PSQL?
  4. Каким образом объявляются локальные переменные?
  5. Приведите пример оператора ветвления?
  6. Какие операторы используются для организации циклов?
  7. Какие операторы перехода доступны в PSQL?
  8. Можно ли досрочно выйти из вложенного цикла?
  9. Чем отличаются операторы LEAVE и BREAK?
  10. Как работает оператор SUSPEND и почему так называется?
  11. В чем особенность применения обычных операторов DML внутри PSQL?
  12. Возможно ли зафиксировать изменения в БД, даже если в последующем транзакция будет отменена?
  13. Какие инструменты существуют для обработки ошибок выполнения операторов?
  14. Для чего используются курсоры?
  15. Какие бывают типы курсоров и чем они отличаются?
  16. Какие особенности предлагает оператор CALL?

Хранимые процедуры (PROCEDURE)

Хранимая процедура является программой, хранящейся в базе данных.

Преимуществами хранимых процедур является то, что они, во-первых, выполняются на стороне сервера, что во многих случаях может резко сократить сетевой трафик, во-вторых, один раз написанная и отлаженная хранимая процедура может использоваться многими программами, в третьих, хранимая процедура позволяет централизованно обновлять или изменять бизнес-логику приложения.

К хранимой процедуре могут обращаться другие хранимые процедуры, триггеры, функции и клиентские программы. При этом допустима рекурсия — хранимая процедура может обращаться сама к себе. Хранимые процедуры выполняются в контексте той же транзакции, что и вызывающие их программы.

Существует два вида хранимых процедур:

  • выполняемые хранимые процедуры (executable stored procedures) и
  • хранимые процедуры выбора или селективные (selectable stored procedures).

Выполняемые хранимые процедуры осуществляют обработку данных, находящихся в базе данных, или вовсе не связанных с базой данных. Эти процедуры могут получать входные параметры и возвращать выходные параметры. Обращение к выполняемым хранимым процедурам осуществляется при выполнении оператора SQL EXECUTE PROCEDURE или CALL.

Селективные хранимые процедуры как правило осуществляют выборку данных из базы данных, возвращая произвольное количество полученных строк. Селективные процедуры также могут получать входные параметры. Значение каждой очередной прочитанной строки возвращается вызвавшей программе в выходных параметрах. Для временной приостановки выполнения такой процедуры и передачи выбранных данных вызвавшей программе в хранимой процедуре используется оператор SUSPEND. Обращение к хранимой процедуре выбора осуществляется при помощи оператора SELECT или CALL.

Создание хранимой процедуры

Для создания хранимой процедуры используется оператор CREATE PROCEDURE.

CREATE PROCEDURE <имя хранимой процедуры>
   [(<входной параметр> [, <входной параметр> ...])]
   [RETURNS (<выходной параметр> [, <выходной параметр> ...])]
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END

CREATE PROCEDURE является составным оператором, состоящим из заголовка и тела. Заголовок определяет имя хранимой процедуры и объявляет входные и выходные параметры, если они должны быть возвращены процедурой. Тело процедуры состоит из необязательных объявлений локальных переменных, подпрограмм и именованных курсоров, и одного или нескольких операторов, или блоков операторов, заключённых во внешнем блоке, который начинается с ключевого слова BEGIN, и завершается ключевым словом END. Объявления локальных переменных и именованных курсоров, а также внутренние операторы должны завершаться точкой с запятой (;).

Хранимой процедуре от вызвавшей программы могут передаваться входные параметры. Параметры передаются по значению, то есть любые изменения значений входных параметров никак не влияют на значения этих параметров в вызвавшей программе. Входным параметрам может присваиваться значение по умолчанию. Параметры, для которых заданы значения по умолчанию, должны располагаться в самом конце списка.

Хранимая процедура может возвращать вызвавшей программе произвольное количество выходных параметров (предложение RETURNS).

Для изменения существующей хранимой процедуры используется оператор ALTER PROCEDURE.

ALTER PROCEDURE <имя хранимой процедуры> [(<входной пар-р>[,<входной пар-р>...])]
[RETURNS (<выходной параметр> [, <выходной параметр> ...])]
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END 

Возможно одновременное применение оператора CREATE OR ALTER PROCEDURE.

CREATE OR ALTER PROCEDURE <имя хранимой процедуры> [(<входной пар-р>[,<входной пар-р>...])]
[RETURNS (<выходной параметр> [, <выходной параметр> ...])]
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END 

При этом если указанная хранимая процедура существует, она будет изменена, а если нет, то создана.

Для удаления существующей хранимой процедуры используется оператор DROP PROCEDURE.

DROP PROCEDURE <имя хранимой процедуры>

Ранее мы уже встречали пример создания селективной хранимой процедуры. Посмотрим на него еще раз.

CREATE OR ALTER PROCEDURE R(N INT) RETURNS (I INT)
AS
BEGIN
    WHILE (N > 0) DO
    BEGIN
        N = N - 1;
        I = RAND() * 100;
        SUSPEND;
    END
END;

SELECT * FROM R(5);

Контрольные вопросы

  1. Какие виды хранимых процедур вы знаете?
  2. Перечислите преимущества применения хранимых процедур?
  3. Кто может вызывать хранимую процедуру?
  4. Сколько строк может вернуть селективная хранимая процедура?
  5. Каким образом вызывается селективная хранимая процедура?

Хранимые функции (FUNCTION)

Хранимая функция подобно хранимой процедуре является программой, хранящаяся в базе данных и выполняющейся на стороне сервера.

В отличие от хранимых процедур хранимые функции всегда возвращают одно скалярное значение. Для возврата значения из хранимой функции используется оператор RETURN, который немедленно прекращает выполнение функции.

Хранимые функции работают практически также, как и обычные встроенные скалярные функции (например, UPPER, SIN, RAND, …). Их можно использовать в списке выбора SELECT, в условиях WHERE или ORDER BY и т.п.

Создание хранимой функции

Для создания хранимой функции используется оператор CREATE FUNCTION.

CREATE FUNCTION <имя хранимой функции>
   [(<входной параметр> [, <входной параметр> ...])]
RETURNS <тип> [DETERMINISTIC]
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END

Хранимой функции от вызвавшей программы могут передаваться входные параметры. Параметры передаются по значению, то есть любые изменения значений входных параметров никак не влияют на значения этих параметров в вызвавшей программе. Входным параметрам может присваиваться значение по умолчанию. Параметры, для которых заданы значения по умолчанию, должны располагаться в самом конце списка.

Предложение RETURNS задаёт тип возвращаемого значения хранимой функции.

Необязательное предложение DETERMINISTIC указывает, что функция детерминированная. Детерминированные функции каждый раз возвращают один и тот же результат, если предоставлять им один и тот же набор входных значений. Недетерминированные функции могут возвращать каждый раз разные результаты, даже если предоставлять им один и тот же набор входных значений. Если для функции указано, что она является детерминированной, то такая функция не вычисляется заново, если она уже была вычислена однажды с данным набором входных аргументов, а берет свои значения из кэша метаданных (если они там есть).

Для изменения существующей хранимой функции используется оператор ALTER FUNCTION.

ALTER FUNCTION <имя хранимой функции>
   [(<входной параметр> [, <входной параметр> ...])]
RETURNS <тип> [DETERMINISTIC]
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END

После выполнения существующие привилегии и зависимости сохраняются.

Параметр DETERMINISTIC можно изменять без указания тела функции:

ALTER FUNCTION <имя хранимой функции> {DETERMINISTIC | NOT DETERMINISTIC}

Допускается также и одновременное создание или обновление хранимой функции в зависимости от того, существует ли уже функция с таким названием или нет.

CREATE OR ALTER FUNCTION <имя хранимой функции>
   [(<входной параметр> [, <входной параметр> ...])]
RETURNS <тип> [DETERMINISTIC]
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END

Для удаления существующей хранимой функции используется оператор DROP FUNCTION.

DROP FUNCTION <имя хранимой процедуры>

Если от хранимой функции существуют зависимости, то при попытке удаления такой функции будет выдана соответствующая ошибка.

В качестве примера рассмотрим создание простой детерминированной хранимой функции, которая вычисляет площадь окружности по ее радиусу.

CREATE FUNCTION CIRCLE_SQUARE(R DOUBLE PRECISION)
   RETURNS DOUBLE PRECISION
   DETERMINISTIC
AS
BEGIN
   RETURN PI() * R * R;
END

Использовать такую функцию можно, например, в таком запросе:

SELECT CIRCLE_SQUARE(10) FROM RDB$DATABASE

Который вернет,

CIRCLE_SQUARE
314.1592653589793

Контрольные вопросы

  1. В чем отличие хранимой функции от хранимой процедуры?
  2. Когда и зачем указывается опция DETERMINISTIC?
  3. Как и где можно вызывать хранимые функции?

Триггеры

Триггер является программой, которая хранится в базе данных и выполняется на стороне сервера. Напрямую обращение к триггеру невозможно. Он вызывается автоматически при наступлении событий, относящихся к таблице или базе данных. Триггер выполняется в контексте той транзакции, в контексте которой выполнялась программа, вызвавшая соответствующее событие. Исключением являются триггеры, реагирующие на события базы данных. Для некоторых из них запускается транзакция по умолчанию.

Триггеры являются полезным инструментом при выполнении различных действий в случае изменения данных базы данных. Назначением триггеров является автоматическое выполнение функций по формированию значений искусственного первичного ключа, выдачи информационных сообщений базы данных, связанных с изменениями данных, специфические способы поддержания декларативной целостности данных, логирование событий и выполнение определенных действий при добавлении (изменении) некоторых данных базы данных.

В СУБД «Ред База Данных» различают три вида триггеров в зависимости от событий, на которые они реагируют:

  • DML триггеры - табличные триггеры.
  • Триггеры базы данных - триггеры на события базы данных.
  • DDL триггеры - триггеры на события изменения метаданных.

DML триггеры

DML триггеры вызываются при изменении состояния данных DML операциями: редактирование, добавление или удаление записей. Они могут быть определены и для таблиц, и для представлений.

CREATE TRIGGER <имя триггера> {
         [ACTIVE | INACTIVE]
         {BEFORE | AFTER} 
         { INSERT | UPDATE | DELETE } [OR { INSERT | UPDATE | DELETE }...]
         [POSITION <порядок срабатывания триггера>]
         ON {<имя таблицы> | <имя представления>}
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END

У DML триггера различают две фазы срабатывания BEFORE и AFTER, перед и после соответствующего события. В фазе BEFORE, если она доступна, можно изменять добавляемые или изменяемые строки, используя значения, которые были переданы в таблицу или представление. В фазе AFTER нельзя изменять значения, но можно использовать все значения, присвоенные записям триггерами на фазе BEFORE.

Также для DML триггера указывается событие, при котором он должен срабатывать (вызываться). Событиями могут быть добавление записи (INSERT), обновление записи (UPDATE) и удаление записи (DELETE).

Таким образом, существует шесть вариантов соотношения событие-фаза для таблицы (представления):

  • перед добавлением новой строки (BEFORE INSERT);
  • после добавления новой строки (AFTER INSERT);
  • перед изменением строки (BEFORE UPDATE);
  • после изменения строки (AFTER UPDATE);
  • перед удалением строки (BEFORE DELETE);
  • после удаления строки (AFTER DELETE).

Существует возможность создавать триггеры, вызываемые автоматически для одной таблицы (представления), для одной фазы и одного события, а также для одной фазы и нескольких событий. В последнем случае необходимые события перечисляются через OR.

Контекстные переменные INSERTING, UPDATING и DELETING логического типа могут быть использованы в теле триггера для определения события, которое вызвало срабатывание триггера.

Например,

CREATE TRIGGER BIU_EMPLOYEE
      BEFORE INSERT OR UPDATE
      ON EMPLOYEE
AS
BEGIN
   -- Общие действия для события
   IF (INSERTING) THEN
      -- Действия при вставке
   ELSE
      -- Действия при обновлении
END

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

Для DML триггеров существуют специфические контекстные переменные OLD.<столбец> и NEW.<столбец> для обращения с старым и новым значениям столбца. В триггерах можно обращаться к значению любого столбца таблицы до его изменения.

Контекстная переменная OLD для всех видов триггеров является переменной только для чтения. Она недоступна в триггерах, вызываемых при добавлении данных, независимо от фазы события.

Контекстная переменная NEW в триггерах для фазы события после (AFTER) также является переменной только для чтения. Она недоступна в триггерах для события удаления данных.

Одно из основных назначений триггеров — формирование значения искусственных первичных ключей в таблицах. Такие триггеры вызываются до помещения новой строки таблицы в базу данных (BEFORE INSERT).

Пусть имеется таблица PEOPLE, описывающая людей в базе данных. В этой таблице присутствует столбец COD, являющийся искусственным первичным ключом. Для получения значения этого ключа в базе данных создан генератор с именем GEN_PEOPLE. Чтобы при операции добавления новой строки в эту таблицу получать уникальное значение искусственного первичного ключа, нужно создать следующий триггер:

CREATE TRIGGER TBI_PEOPLE
   ACTIVE
   BEFORE INSERT
   ON PEOPLE
AS
BEGIN
   IF (NEW.COD IS NULL) THEN
      NEW.COD = NEXT VALUE FOR GEN_PEOPLE;
END

Триггер является активным (ACTIVE), создается для таблицы PEOPLE для фазы до (BEFORE) события добавления новой записи (INSERT).

В теле триггера проверяется, не присвоено ли уже первичному ключу какое-либо значение (стандартная проверка). Для этого используется имя столбца с префиксом NEW. После этой проверки, если первичный ключ не имеет еще никакого значения, значению первичного ключа присваивается уникальное значение, получаемое из генератора GEN_PEOPLE увеличением на единицу значения генератора.

Пример триггера, поддерживающего ссылочную целостность. Если при объявлении внешнего ключа в описании ограничения REFERENCES для операции DELETE используется вариант NO ACTION, то пользователь сам должен обеспечить отсутствие записей дочерней таблицы, ссылающихся на первичный ключ удаляемой записи.

CREATE OR ALTER TRIGGER TAD_COUNTRY
   AFTER DELETE
   ON COUNTRY 
AS BEGIN
   DELETE FROM REGION WHERE REGION.CODCOUNTRY = OLD.CODCOUNTRY;
END

Еще пример триггера, которые определенным образом логирует изменения в БД.

В демонстрационной базе данных EMPLOYEE, присутствуют таблицы EMPLOYEE, описывающая сотрудников организации, и SALARY_HISTORY, хранящая историю окладов сотрудников.

Следующий триггер вызывается после изменения (AFTER UPDATE) таблицы EMPLOYEE. В нем проверяется, не изменился ли оклад сотрудника, и если изменился, в триггере создается новая запись истории сотрудника.

CREATE TRIGGER SAVE_SALARY_CHANGE
   AFTER UPDATE
   ON EMPLOYEE
AS
BEGIN
   IF (OLD.SALARY <> NEW.SALARY) THEN
      INSERT INTO SALARY_HISTORY (EMP_NO, CHANGE_DATE, UPDATER_ID, 
               OLD_SALARY, 
               PERCENT_CHANGE)
            VALUES (OLD.EMP_NO, 'NOW', USER, 
               OLD.SALARY, 
               (NEW.SALARY - OLD.SALARY) * 100 / OLD.SALARY);
END

Триггеры базы данных

CREATE TRIGGER <имя триггера> {
         [ACTIVE | INACTIVE]
         ON {CONNECT | DISCONNECT | TRANSACTION START | TRANSACTION COMMIT | TRANSACTION ROLLBACK}
         [POSITION <порядок срабатывания триггера>]
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END

Триггер, связанный с событиями базы данных, может вызываться при следующих событиях:

  • При соединении с базой данных (CONNECT).
  • При отсоединении от базы данных (DISCONNECT).
  • При старте транзакции (TRANSACTION START).
  • При подтверждении транзакции (TRANSACTION COMMIT).
  • При отмене транзакции (TRANSACTION ROLLBACK).

Триггеры на события CONNECT и DISCONNECT выполняются в специально созданной для этого транзакции. Если при обработке триггера не было вызвано исключение, то транзакция подтверждается. Не перехваченные исключения откатят транзакцию и в случае триггера на событие CONNECT соединение разрывается, а исключения возвращается клиенту. Для триггера на событие DISCONNECT соединение разрывается, как это и предусмотрено, но исключения не возвращается клиенту.

Триггеры на событие TRANSACTION срабатывают при старте транзакции, её подтверждении или отмене. Не перехваченные исключения обрабатываются в зависимости от типа события TRANSACTION:

  • для события START исключение возвращается клиенту, а транзакция отменяется;
  • для события COMMIT исключение возвращается клиенту, действия, выполненные триггером, и транзакция отменяются;
  • для события ROLLBACK исключение не возвращается клиенту, а транзакция, как и предусмотрено, отменяется.

Рассмотрим пример триггера, который логирует подключившихся пользователей.

CREATE TRIGGER TR_LOG_CONNECT ON CONNECT
AS
BEGIN
   INSERT INTO LOG_CONNECT (ID, USERNAME, ATIME)
      VALUES (NEXT VALUE FOR SEQ_LOG_CONNECT, CURRENT_USER, CURRENT_TIMESTAMP);
END

Следующий пример показывает как с помощью триггера на подключение, контролировать доступ пользователей в БД:

CREATE EXCEPTION E_INCORRECT_WORKTIME 'Не рабочее время.';

CREATE TRIGGER TR_LIMIT_WORKTIME ACTIVE ON CONNECT
AS
BEGIN
  IF ((CURRENT_USER <> 'SYSDBA') AND
      NOT (CURRENT_TIME BETWEEN time '9:00' AND time '17:00')) THEN
    EXCEPTION E_INCORRECT_WORKTIME;
END

DDL триггеры

Триггеры на события изменения метаданных (DDL триггеры) предназначены для обеспечения ограничений, которые будут распространены на пользователей, которые пытаются создать, изменить или удалить DDL объект. Другое их назначение — ведение журнала изменений метаданных.

CREATE TRIGGER <имя триггера> {
         [ACTIVE | INACTIVE]
         {BEFORE | AFTER} 
         {ANY DDL STATEMENT | <DDL событие> [OR <DDL событие> ...] }
         [POSITION <порядок срабатывания триггера>]
AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END

При этом возможные события задаются следующим образом.

<DDL событие> ::=
           CREATE|ALTER|DROP TABLE
         | CREATE|ALTER|DROP PROCEDURE
         | CREATE|ALTER|DROP FUNCTION
         | CREATE|ALTER|DROP TRIGGER
         | CREATE|ALTER|DROP EXCEPTION
         | CREATE|ALTER|DROP VIEW
         | CREATE|ALTER|DROP DOMAIN
         | CREATE|ALTER|DROP ROLE
         | CREATE|ALTER|DROP SEQUENCE
         | CREATE|ALTER|DROP USER
         | CREATE|ALTER|DROP INDEX
         | CREATE|DROP COLLATION
         | ALTER CHARACTER SET
         | CREATE|ALTER|DROP PACKAGE
         | CREATE|DROP PACKAGE BODY
         | CREATE|ALTER|DROP MAPPING

Если в качестве события указано предложение ANY DDL STATEMENT, то триггер будет вызван при наступлении любого из DDL событий.

DDL триггеры срабатывают на указанные события изменения метаданных в одной из фаз события. BEFORE триггеры запускаются до изменений в системных таблицах, AFTER триггеры запускаются после изменений в системных таблицах.

Когда оператор DDL запускает триггер, в котором выбрасывается исключение, оператор не будет фиксирован. Т.е. исключения могут использоваться, чтобы гарантировать, что оператор DDL будет отменен, если некоторые условия не будут соблюдены.

Действия DDL триггеров выполняются только при фиксации транзакции, в которой работает затронутая DDL команда.

Чтобы узнать, какое именно событие произошло, объект, над которым производится действие и другую информацию, можно использовать встроенную функцию RDB$GET_CONTEXT.

Для этого в ней доступно пространство имён DDL_TRIGGER.

Контекст DDL_TRIGGER работает как стек. Перед выполнением DDL триггера, значения, относящиеся к выполняемой команде, помещаются в этот стек. После завершения работы триггера значения выталкиваются. Таким образом, в случае каскадных DDL операторов, когда каждая пользовательская DDL команда возбуждает DDL триггер, и этот триггер запускает другие DDL команды, значения переменных в пространстве имен DDL_TRIGGER будут соответствовать команде, которая вызвала последний DDL триггер в стеке вызовов.

Переменные доступные в пространстве имён DDL_TRIGGER:

  • EVENT_TYPE — тип события (CREATE, ALTER, DROP);
  • OBJECT_TYPE — тип объекта (TABLE, VIEW и д.р.);
  • DDL_EVENT — имя события (EVENT_TYPE || ' ' || OBJECT_TYPE);
  • OBJECT_NAME — имя объекта метаданных;
  • SQL_TEXT — текст SQL запроса.

Например, следующий триггер проверяет название создаваемой хранимой процедуры, чтобы гарантировать что все они начинаются с заданного префикса.

CREATE EXCEPTION E_INVALID_SP_NAME 'Имя хранимой процедуры должно начинаться с "SP_")';

CREATE TRIGGER TRIG_DDL_SP BEFORE CREATE PROCEDURE
AS
BEGIN
  IF (RDB$GET_CONTEXT('DDL_TRIGGER', 'OBJECT_NAME') NOT STARTING 'SP_') THEN
    EXCEPTION E_INVALID_SP_NAME;
END;

Триггер может быть активным (ACTIVE) или неактивным (INACTIVE). Если триггер активен (значение по умолчанию), то он автоматически вызывается при наступлении соответствующего события (событий) таблицы или базы данных. Если триггер неактивен, то вызов триггера не происходит.

Для одного и того же события или группы событий может быть создано несколько триггеров. Ключевое слово POSITION позволяет задать порядок, в котором будут выполняться такие триггеры (по умолчанию значение 0). Если позиции для триггеров не заданы или несколько триггеров имеют одно и то же значение позиции, то такие триггеры будут выполняться в алфавитном порядке их имен.

Изменение триггера

Для изменения заголовка и/или тела существующего триггера используется оператор ALTER TRIGGER.

ALTER TRIGGER <имя триггера>
   [ACTIVE | INACTIVE]
   [{BEFORE | AFTER} <список событий>]
   [POSITION <порядок срабатывания триггера>]
[AS
   [<объявление> [<объявление> ...] ]
BEGIN
   <блок операторов>
END]

В операторе изменения триггера можно изменить его состояние активности (ACTIVE / INACTIVE), событие (события) таблицы (представления) и фазу события, позицию триггера, выполняемые триггером действия. Если триггер был создан для события базы данных или для события изменения метаданных, то можно только изменить его активность, позицию и выполняемые действия. Если какой-то элемент не указан, то его первоначальное значение не изменяется.

В этом операторе нельзя изменить ссылку на таблицу или представление, с которым связан существующий триггер, а также событие базы данных.

Удаление триггера

Для удаления существующего триггера используется оператор DROP TRIGGER.

DROP TRIGGER <имя триггера>;

Нельзя удалить триггер, автоматически созданный системой для поддержания ограничений PRIMARY KEY, CHECK и FOREIGN KEY. Остальные триггеры не имеют никаких зависимостей, которые ограничили бы возможности удаления триггеров.

Контрольные вопросы

  1. Как можно вызвать триггер?
  2. Для чего используются триггеры?
  3. Какие виды триггеров бывают?
  4. Что такое “фаза выполнения”?
  5. В каких случаях допустимо изменение столбцов записей?
  6. На какие события в БД допустимо создавать триггеры?
  7. Как узнать какой объект вызвал DDL триггер?
  8. В каком порядке срабатывают триггеры?
  9. Какие есть ограничения при удалении триггера?

Пакеты

Пакет — группа процедур и функций, которая представляет собой единый объект базы данных.

Пакеты состоят из двух частей: заголовка (ключевое слово PACKAGE) и тела (ключевые слова PACKAGE BODY). Сначала создаётся заголовок, а затем — тело.

Пакеты обладают следующими преимуществами:

Модульность

Блоки взаимозависимого кода выделены в логические модули, как это сделано в других языках программирования. В программировании существует множество способов для группировки кода, например с помощью пространств имен (namespaces), модулей (units) и классов. Со стандартными процедурами и функциями базы данных это невозможно.

Упрощение отслеживания зависимостей

Пакеты упрощают механизм отслеживания зависимостей между набором связанных процедур, а также между этим набором и другими процедурами, как упакованными, так и неупакованными.

Каждый раз, когда упакованная подпрограмма определяет, что используется некоторый объект базы данных, информации о зависимости от этого объекта регистрируется в системных таблицах Ред Базы Данных. После этого, чтобы удалить или изменить этот объект, сначала необходимо удалить, то что зависит от него. Поскольку зависимости от других объектов существуют только для тела пакета, это тело пакета может быть легко удалено, даже если какой-нибудь другой объект зависит от этого пакета. Когда тело удаляется, заголовок остаётся, что позволяет пересоздать это тело после того, как сделаны изменения связанные с удалённым объектом.

Упрощение управления разрешениями

Поскольку Ред База Данных выполняет подпрограммы с полномочиями вызывающей стороны, то каждой вызывающей подпрограмме необходимо предоставить полномочия на использования ресурсов, если эти ресурсы не являются непосредственно доступными вызывающей стороне. Использование каждой подпрограммы требует предоставления привилегий на её выполнение для пользователей и/или ролей.

У упакованных подпрограмм нет отдельных привилегий. Привилегии действуют на пакет в целом. Привилегии, предоставленные пакетам, действительны для всех подпрограмм тела пакета, в том числе частных, и сохраняются для заголовка пакета.

Частные области видимости

Некоторые процедуры и функции могут быть частными (private), а именно их использование разрешено только внутри определения пакета.

Все языки программирования имеют понятие области видимости подпрограмм, которое невозможно без какой-либо формы группировки. Пакеты в этом отношении подобны модулям. Если подпрограмма не объявлена в заголовке пакета, но реализована в теле, то такая подпрограмма становится частной. Частную подпрограмму возможно вызвать только из её пакета.

Создание заголовка пакета

Оператор CREATE PACKAGE создаёт новый заголовок пакета.

CREATE PACKAGE <имя пакета>
AS
BEGIN
   [ <объявление константы> ]
   [ <объявление процедуры>]
   [ <объявление функции> ]
   [...]
END

Процедуры, функции и константы, объявленные в заголовке пакета, доступны вне тела пакета через полный идентификатор имён процедур, функций и констант (<имя пакета>.<имя процедуры> и <имя пакета>.<имя функции>). Процедуры, функции и константы, определенные в теле пакета, но не объявленные в заголовке пакета, не видны вне тела пакета.

Значением константы должно быть выражение, результат которого не изменится после перекомпиляции. В качестве значения допустимо указывать скаляры, функции и контекстные переменные. Константы нельзя использовать в запросах, где есть таблица, имя которой совпадает с именем пакета, в котором определена константа. В этом случае необходимо указывать псевдоним таблицы.

Имя пакета должно быть уникальным среди имён всех пакетов. Имена процедур и функций, объявленные в заголовке пакета, должны быть уникальны среди имён процедур и функций, объявленных в заголовке и теле пакета.

Изменение заголовка пакета

Оператор ALTER PACKAGE изменяет заголовок пакета.

ALTER PACKAGE <имя пакета>
AS
BEGIN
   [ <объявление константы> ]
   [ <объявление процедуры>]
   [ <объявление функции> ]
   [...]
END

Удаление заголовка пакета

Оператор DROP PACKAGE удаляет существующий заголовок пакета.

DROP PACKAGE <имя пакета>

Перед удалением заголовка пакета, необходимо выполнить удаление тела пакета (DROP PACKAGE BODY), иначе будет выдана ошибка. Если от заголовка пакета существуют зависимости, то при попытке удаления такого заголовка будет выдана соответствующая ошибка.

Создание тела пакета

Оператор CREATE PACKAGE BODY создаёт новое тело пакета. Тело пакета может быть создано только после того как будет создан заголовок пакета. Если заголовка пакета с именем <имя пакета> не существует, то будет выдана соответствующая ошибка.

CREATE PACKAGE BODY <имя пакета>
AS
BEGIN
   [ <объявление константы> ]
   [ <объявление процедуры>]
   [ <объявление функции> ]
   [ <реализация процедуры>]
   [ <реализация функции> ]
   [...]
END

Все процедуры и функции, объявленные в заголовке пакета, должны быть реализованы в теле пакета с той же сигнатурой. Кроме того, должны быть реализованы и все процедуры и функции, объявленные в теле пакета, с той же сигнатурой. Процедуры и функции, определенные в теле пакета, но не объявленные в заголовке пакета, не видны вне тела пакета.

Значения по умолчанию для параметров процедур не могут быть переопределены. Это означает, что они могут быть в реализации только для частных процедур, которые не были объявлены.

Удаление тела пакета

Оператор DROP PACKAGE BODY удаляет существующее тело пакета.

DROP PACKAGE BODY <имя пакета>

Пример пакета

Рассмотрим простой пример создания пакета. В нем упаковывается две хранимые функции, используемые для получения даты начала и даты завершения какого-либо события, а также хранимая процедура для установки диапазона этих дат.

CREATE PACKAGE APP_VAR
AS
BEGIN
  FUNCTION GET_DATEBEGIN() RETURNS DATE DETERMINISTIC;
  FUNCTION GET_DATEEND() RETURNS DATE DETERMINISTIC;
  PROCEDURE SET_DATERANGE(ADATEBEGIN DATE, ADATEEND DATE DEFAULT CURRENT_DATE);
END

Таким образом, пакет APP_VAR представляет собой некий интерфейс, который можно использовать в других модулях PSQL.

Реализация тела пакета может быть представлена в следующем виде.

CREATE PACKAGE BODY APP_VAR
AS
BEGIN

  -- Возвращает дату начала периода
  FUNCTION GET_DATEBEGIN() RETURNS DATE DETERMINISTIC
  AS
  BEGIN
    RETURN RDB$GET_CONTEXT('USER_SESSION', 'DATEBEGIN');
  END

  -- Возвращает дату конца периода
  FUNCTION GET_DATEEND() RETURNS DATE DETERMINISTIC
  AS
  BEGIN
    RETURN RDB$GET_CONTEXT('USER_SESSION', 'DATEEND');
  END

  -- Устанавливает период
  PROCEDURE SET_DATERANGE(ADATEBEGIN DATE, ADATEEND DATE)
  AS
  BEGIN
    RDB$SET_CONTEXT('USER_SESSION', 'DATEBEGIN', ADATEBEGIN);
    RDB$SET_CONTEXT('USER_SESSION', 'DATEEND', ADATEEND);
  END
END

Для хранения установленных значений применяются функции для работы с контекстными переменными RDB$SET_CONTEXT и RDB$GET_CONTEXT.

Контрольные вопросы

  1. Что такое пакет и из чего он состоит?
  2. Назовите преимущества использования пакетов?
  3. Можно ли объявлять хранимые процедуры и функции только в заголовке пакета?
  4. Можно ли объявлять хранимые процедуры и функции только в теле пакета?

Лабораторные работы

Проектирование БД

Цель

Научится проектировать БД используя РЕД Эксперт и СУБД РЕД База Данных.

Порядок выполнения

  1. Установить РЕД База Данных
  2. Установить РЕД Эксперт
  3. Создать базу данных “телефонный справочник”
  4. Создать таблицу ABONENT c с полями:
    • ABONENT_ID - первичный ключ, автоинкрементное поле.
    • FIRSTNAME - имя абонента.
    • LASTNAME - фамилия абонента.
    • ADDRESS - адрес абонента.
  5. Создать таблицу PHONE c с полями:
    • PHONE_NUMBER - первичный ключ, строковый тип данных.
    • ABONENT_ID - внешний ключ, не автоинкрементное поле.
    • PHONE_TYPE - тип телефона, строковый тип данных.
  6. Внести 3 абонента и у каждого по 3 номера телефона.
  7. Построить ER-диаграмму полученной базы данных.
  8. Написать отчет по лабораторной работе содержащий все этапы выполнения работы и полученные результаты.

Полезная информация

Подготовка

  1. В учебной виртуальной машине РЕД Эксперт и РЕД База Данных уже установлены.
  2. Перед созданием БД подготовьте папку, в которой будет размещаться файл БД. Владельцем папки необходимо сделать пользователя reddatabase (firebird для версий СУБД Ред База Данных ниже 5 версии). Например, следующий образом в терминале от пользователя root:
mkdir /db
chown reddatabase. /db

Создание базы данных

Для создания БД используется кнопка “Создать базу данных” на вкладке “Браузера баз данных” или соответствующий пункт меню “База данных/Создать базу данных”.

Далее необходимо заполнить все обязательные поля. Опишем назначение некоторых полей этой вкладки:

  • Имя подключения - произвольное имя, чтобы обозначить подключение в дереве объектов слева.
  • Имя сервера - доменное имя или IP адрес сервера, на котором запущен сервер БД, к которому мы будем подключаться чтобы создать БД. Для локального компьютера достаточно оставить значение по умолчанию localhost.
  • Файл БД - файл на сервере БД. Обратите внимание. Это файл, к которому будет подключаться сервер СУБД, а не РЕД Эксперт. Если в поле Имя сервера указан удаленный сервер, то с локального компьютера к этому файлу не будет доступа. Но сервер СУБД выполняется на том сервере и, следовательно, доступ у него должен быть. Другими словами, этим значением мы лишь сообщаем серверу, какой файл использовать для БД, но не работаем с этим файлом на нашем компьютере. Это объясняет почему права на каталог с БД должны быть предоставлены пользователю reddatabase (firebird для ранних версий) от которого работает сервер СУБД РЕД База Данных.
  • Пользователь - имя пользователя, от имени которого будет установлено подключение к серверу. sysdba - это имя суперпользователя, который всегда существует в БД и имеет все права. Управлять и создавать новых пользователей также можно с помощью РЕД Эксперт.
  • Пароль - пароль пользователя, который был указан при его создании. Пароль пользователя sysdba указывается при установке сервера СУБД и традиционно для тестовых целей равен masterkey.
  • Сохранить пароль - можно выставить, чтобы не вводить пароль для этого подключения постоянно.

После нажатия кнопки “Создать”, запрос создания БД будет отправлен на сервер и, если не будет ошибок, БД будет успешно создана, а РЕД Эксперт предложит ее зарегистрировать в дереве объектов.

Нажмите “Да” и вновь созданная БД появится в дереве объектов слева.

Дважды нажмите на ней и РЕД Эксперт установит к ней подключение, покажет все объекты, которые в ней есть. После этого, можно с ней работать.

Создание таблиц

Создание всех объектов, кроме выполнения DDL команд в редакторе запросов, можно делать с помощью графических форм. Для этого нажмите правой кнопкой мыши на нужном узле дерева объектов. В нашем случае это “Таблицы” и выберете соответствующий пункт меню “Создать таблицу”.

Теперь необходимо указать ее имя, поля и другие атрибуты. Учтите, что изменять имя таблицы после ее создания невозможно, поэтому сразу укажите необходимое имя. Также добавим первое поле ABONENT_ID и укажем его тип данных (INTEGER). Кроме этого, в столбце PK (первичный ключ) необходимо сделать двойной клик мышкой и появится “ключ”. Это означает что это поле входит в первичный ключ.

Первичный ключ не должен содержать NULL значения, поэтому с помощью горизонтального скроллинга найдем соответствующее поле NOT NULL и поставим в нем галочку.

Еще правее есть атрибут “Автоинкремент”. Поскольку это поле первичный ключ и его значение должно генерироваться автоматически, укажем и эту галочку. Появится окно, где надо выбрать каким образом будет генерироваться значение этого поля. Рекомендованным способом по умолчанию является “Использовать IDENTITY”. Начальное значение оставьте по умолчанию.

Похожим образом добавим остальные поля. Учтите что они не все входят в первичный ключ и имеют разный тип данных.

Учтите также что не все они NOT NULL.

Далее, после нажатия на кнопку “Применить”, появится окно выполнения сгенерированных DDL команд, сам текст соответствующих команд, которые выполняет сервер СУБД и возможность “Подтвердить” или “Откатить” эти изменения (транзакцию). Подтвердите ее и созданная таблица ABONENT появится в дереве объектов слева.

Аналогичным образом делаем таблицу PHONE. Обратите внимание, что ее первичный ключ указывается пользователем и не должен быть автоинкрементным.

Также не все поля должны быть NOT NULL.

Поле ABONENT_ID является внешним ключом, ссылающимся на таблицу ABONENT. Это ограничение можно добавить на вкладке “Ограничения”.

Имя ограничения мы указывать не будем. Необходимо выбрать столбец ограничения (ABONENT_ID), внешнюю таблицу, на которую этот столбец ссылается (ABONENT) и столбец внешней таблицы (ABONENT_ID).

После нажатия кнопки “Применить” также появится окно подтверждения DDL операции. Обратите внимание на команды добавления внешнего ключа.

Добавление данных

Посмотреть и изменить данные таблиц можно на вкладке “Данные”. Поскольку телефон не может существовать без абонента, то сначала нужно добавить запись об абоненте. Обратите внимание, что поле ABONENT_ID заполнять не следует, поскольку его значение генерируется автоматически. Адрес можно оставить незаданным, но для остальных записей попробуйте его указать.

Для внесения изменений в БД нажмите кнопку “Фиксировать”. Это выполнит запрос на вставку данных, зафиксирует транзакцию и данные будут записаны в БД. После этого набор данных будет обновлен и можно будет увидеть сгенерированное значение поля ABONENT_ID.

При внесении данных в таблицу PHONE значение первичного ключа надо указать явно, а вот для указания значения внешнего ключа ABONENT_ID редактор предложит список выбора, поскольку “знает” о том, что значение должно ссылаться на внешнюю таблицу.

Аналогичным образом заполняются остальные данные.

Построение ER-диаграммы

Чтобы построить диаграмму, нужно воспользоваться соотвутствующим пунктом меню “Инструменты/Редактор ER-диаграмм”.

Для построения диаграммы для существующей БД, в редакторе ER-диаграмм нужно выбрать кнопку “Реверс-инжиниринг”.

Появится окно, где можно выбрать какое подключение и какие таблицы из него использовать в диаграмме.

Нажмем кнопку “Выбрать все”.

После нажатия на кнопку “Построить” будет построена ER-диаграмма нашей БД.

Выборка, фильтрация и сортировка

Цель

Изучить основы языка SQL. Научится делать выборку данных, фильтрацию и сортировку.

Порядок выполнения

1. Изучить схему БД employee.fdb и, используя ее, составьте запросы, возвращающие указанные результаты

2. Выбрать все страны и их валюты

COUNTRYCURRENCY
USADollar
EnglandPound
CanadaCdnDlr
SwitzerlandSFranc
JapanYen
ItalyEuro
FranceEuro
GermanyEuro
AustraliaADollar
Hong KongHKDollar
NetherlandsEuro
BelgiumEuro
AustriaEuro
FijiFDollar
RussiaRuble
RomaniaRLeu

3. Отобразить имя клиентов из города “Boston”

CUSTOMER
Buttle, Griffith and Co.

4. Выбрать полное имя и зарплату пяти самых высокооплачиваемых сотрудников

FULL_NAMESALARY
Yamamoto, Takashi7480000
Ichida, Yuki6000000
Bender, Oliver H.212850
Steadman, Walter116100
MacDonald, Mary S.111262.5

5. Выбрать полное имя, страну работы и зарплату сотрудников из отдела 621

FULL_NAMEJOB_COUNTRYSALARY
Young, BruceUSA97500
Ramanathan, AshokUSA80689.5
Bishop, DanaUSA62550
Green, T.J.USA36000

6. Выбрать полное имя, дату найма и зарплату сотрудников с зарплатой от 50000 до 60000

FULL_NAMEHIRE_DATESALARY
Lee, Terri1990-05-01T00:0053793
Phong, Leslie1991-06-03T00:0056034.38
Burbank, Jennifer M.1992-04-15T00:0053167.5
Williams, Randy1992-08-08T00:0056295
Johnson, Scott1993-09-13T00:0060000

7. Выбрать имя и фамилию сотрудников, чья фамилия начинается на букву “S”

FIRST_NAMELAST_NAME
WillieStansbury
WalterSteadman
ClaudiaSutherland

8. Выбрать номер, имя и фамилию сотрудников и отсортировать их по фамилии в алфавитном порядке

EMP_NOFIRST_NAMELAST_NAME
34JanetBaldwin
105Oliver H.Bender
……..………….…………..
127MichaelYanowski
4BruceYoung
15KatherineYoung

9. Выбрать номер, имя, фамилию, номер отдела и зарплату сотрудников

  • Отсортировать их по отделу в порядке возрастания
  • Затем по зарплате в порядке убывания.
EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARY
105Oliver H.Bender0212850
12TerriLee053793
85Mary S.MacDonald100111262.5
127MichaelYanowski10044000
61LukeLeung11068805
34JanetBaldwin11061637.81
118TakashiYamamoto1157480000
110YukiIchida1156000000
37WillieStansbury12039224.06
36RogerReeves12033620.63
28AnnBennet12022935
……..………….…………..……………….

10. Напишите запрос, чтобы выбрать номер, имя, фамилию, номер отдела, зарплату и дату найма сотрудников из отдела 621 с зарплатой больше 30000 и отсортировать их по дате приема на работу (от самых новых к самым старым)

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYHIRE_DATE
138T.J.Green621360001993-11-01T00:00
83DanaBishop621625501992-06-01T00:00
45AshokRamanathan62180689.51991-08-01T00:00
4BruceYoung621975001988-12-28T00:00

11. Напишите запрос, чтобы выбрать сотрудников, принятых на работу в 1989 году

FIRST_NAMELAST_NAMEHIRE_DATE
KimLambert1989-02-06T00:00
LeslieJohnson1989-04-05T00:00
PhilForest1989-04-17T00:00

12. Напишите запрос, чтобы выбрать сотрудников, которые:

  • Работают в отделах 621 или 000.
  • Имеют зарплату больше 50000.
  • Были приняты на работу после 1 января 1989 года.
FIRST_NAMELAST_NAMEDEPT_NOHIRE_DATE
TerriLee01990-05-01T00:00
AshokRamanathan6211991-08-01T00:00
DanaBishop6211992-06-01T00:00
Oliver H.Bender01992-10-08T00:00

13. Напишите запрос, чтобы выбрать всех сотрудников, чья фамилия заканчивается на “en” или “an”, и которые работают в отделе 621

FIRST_NAMELAST_NAMEDEPT_NOHIRE_DATE
AshokRamanathan6211991-08-01T00:00
T.J.Green6211993-11-01T00:00

14. Выбрать все заказы по которым была оплата, но заказ не доставлен

  • Отобразить номер заказа, дату заказа, статус оплаты, статус заказа, количество дней просрочки доставки заказа (DATE_NEEDED).
  • Отсортировать данные в порядке уменьшения количества дней просрочки.
PO_NUMBERORDER_STATUSPAIDORDER_STATUSSUBTRACT
V93H0030openyopen11392.98179
V9456220openyopen11363.98179
V94S6400waitingywaiting11347.98179
V93F2030openyopen

Полезная информация

  1. Для выполнения лабораторной работы использовать материалы лекций 12 и 14 учебника.

Соединения и агрегирование

Цель

Получить практические навыки применения операторов JOIN и агрегатных функций.

Порядок выполнения

1. Используя БД employee.fdb, напишите заданный запрос, возвращающий указанные результат

2. Выбрать всех сотрудников вместе с названиями их отделов и местоположением

EMP_NOFIRST_NAMELAST_NAMEDEPARTMENTLOCATION
2RobertNelsonEngineeringMonterey
4BruceYoungSoftware DevelopmentMonterey
5KimLambertField Office: East CoastBoston
8LeslieJohnsonMarketingSan Francisco
9PhilForestQuality AssuranceMonterey

3. Выбрать всех сотрудников, отсортированных по названию отдела (в алфавитном порядке), а затем по фамилии

EMP_NOFIRST_NAMELAST_NAMEDEPARTMENTLOCATION
107KevinCookConsumer Electronics Div.Burlington, VT
65Sue AnneO’’BrienConsumer Electronics Div.Burlington, VT
105Oliver H.BenderCorporate HeadquartersMonterey
12TerriLeeCorporate HeadquartersMonterey
144JohnMontgomeryCustomer ServicesBurlington, VT
94RandyWilliamsCustomer ServicesBurlington, VT
29RogerDe SouzaCustomer SupportMonterey
136ScottJohnsonCustomer SupportMonterey
114BillParkerCustomer SupportMonterey
44LesliePhongCustomer SupportMonterey
15KatherineYoungCustomer SupportMonterey
109KellyBrownEngineeringMonterey
2RobertNelsonEngineeringMonterey
83DanaBishopSoftware DevelopmentMonterey
138T.J.GreenSoftware DevelopmentMonterey
45AshokRamanathanSoftware DevelopmentMonterey
4BruceYoungSoftware DevelopmentMonterey

4. Найти среднюю зарплату всех сотрудников

AVG_SALARY
385796.85

5. Найти среднюю зарплату по отделу и вывести с названием отдела

DEPARTMENTAVG_SALARY
Consumer Electronics Div.71268,75
Corporate Headquarters133321,5
Customer Services45647,5
Customer Support57551,65
Engineering66450
European Headquarters31926,56
Field Office: Canada100914
Field Office: East Coast94521,47
Field Office: France38500
Field Office: Italy33000
Field Office: Japan6740000
Field Office: Switzerland110000
Finance92791,31
Marketing53688,75
Pacific Rim Headquarters65221,4
Quality Assurance53409,16
Research and Development73155,06
Sales and Marketing77631,25
Software Development69184,87

6. Выбрать сотрудников, с зарплатой выше средней

EMP_NOFIRST_NAMELAST_NAMESALARY
110YukiIchida6000000
118TakashiYamamoto7480000

7. Выбрать сотрудников, чья зарплата выше средней зарплаты в их отделе

EMP_NOFIRST_NAMELAST_NAMEDEPARTMENTSALARY
2RobertNelsonEngineering105900
4BruceYoungSoftware Development97500
5KimLambertField Office: East Coast102750
8LeslieJohnsonMarketing64635
9PhilForestQuality Assurance75060
15KatherineYoungCustomer Support67241.25
20ChrisPapadopoulosResearch and Development89655
24PeteFisherResearch and Development81810.19
29RogerDe SouzaCustomer Support69482.63
36RogerReevesEuropean Headquarters33620.63
37WillieStansburyEuropean Headquarters39224.06
45AshokRamanathanSoftware Development80689.5
46WalterSteadmanFinance116100
61LukeLeungPacific Rim Headquarters68805
85Mary S.MacDonaldSales and Marketing111262.5
94RandyWilliamsCustomer Services56295
105Oliver H.BenderCorporate Headquarters212850
107KevinCookConsumer Electronics Div.111262.5
118TakashiYamamotoField Office: Japan7480000
136ScottJohnsonCustomer Support60000

8. Выбрать всех сотрудников, которые работают в отделах, расположенных в Бостоне

EMP_NOFIRST_NAMELAST_NAMEDEPARTMENTLOCATION
5KimLambertField Office: East CoastBoston
11K. J.WestonField Office: East CoastBoston

9. Найти общую сумму зарплат по каждому местоположению

LOCATIONTOTAL_SALARY
Boston189042.94
Burlington, VT453297.69
Cannes38500
Kuaui130442.81
London95779.69
Milan33000
Monterey1309850.89
San Francisco262640
Tokyo13480000
Toronto100914
Zurich110000

10. Найти средний стаж сотрудников по странам, и упорядочить по возрастанию

JOB_COUNTRYAVG
Canada12014.97179
England12403.59679
France11524.97179
Italy11566.97179
Japan11651.45095
Switzerland11391.93012
USA12238.70664

11. Выбрать участников и проекты, в которых они участвуют. Упорядочить по названию проекта, а потом по полному имени сотрудника

FULL_NAMEPROJ_NAME
Fisher, PeteAutoMap
Johnson, LeslieAutoMap
Page, MaryAutoMap
Papadopoulos, ChrisAutoMap
Fisher, PeteDigiPizza
Montgomery, JohnDigiPizza
Johnson, ScottVideo Database
Phong, LeslieVideo Database
Ramanathan, AshokVideo Database
Young, BruceVideo Database
Young, KatherineVideo Database

12. Выбрать сотрудников департамента с самым большим бюджетом

FIRST_NAMELAST_NAME
Mary S.MacDonald
MichaelYanowski

13. Найти отделы, где средняя зарплата сотрудников больше 50000

DEPARTMENTAVG_SALARY
Consumer Electronics Div.71268.75
Corporate Headquarters133321.5
Customer Support57551.65
Engineering66450
Field Office: Canada100914
Field Office: East Coast94521.47
Field Office: Japan6740000
Field Office: Switzerland110000
Finance92791.31
Marketing53688.75
Pacific Rim Headquarters65221.4
Quality Assurance53409.16
Research and Development73155.06
Sales and Marketing77631.25
Software Development69184.87

Полезная информация

  1. Для выполнения лабораторной работы использовать материалы лекций 13, 18 и 19 учебника.

Оконные функции

Цель

Получить практические навыки применения оконные функций.

Порядок выполнения

1. Используя БД employee.fdb, напишите заданный запрос, возвращающий указанные результат

2. Вывести список сотрудников с их рангом по зарплате внутри каждого отдела

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYSALARY_RANK
105Oliver H.Bender02128501
12TerriLee0537932
85Mary S.MacDonald100111262.51
127MichaelYanowski100440002
61LukeLeung110688051
34JanetBaldwin11061637.812
118TakashiYamamoto11574800001
110YukiIchida11560000002
37WillieStansbury12039224.061

3. Вывести накопленную сумму зарплат для каждого сотрудника в отделе по дате приема на работу

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYCUMULATIVE_SALARY
12TerriLee05379353793
105Oliver H.Bender0212850266643
85Mary S.MacDonald100111262.5111262.5
127MichaelYanowski10044000155262.5
34JanetBaldwin11061637.8161637.81
61LukeLeung11068805130442.81
110YukiIchida11560000006000000
118TakashiYamamoto115748000013480000

4. Вычислить скользящее среднее зарплаты для каждого сотрудника, используя текущую строку и две предыдущие по дате приема на работу

EMP_NOFIRST_NAMELAST_NAMEHIRE_DATESALARYMOVING_AVG
2RobertNelson1988-12-28T00:00105900105900
4BruceYoung1988-12-28T00:0097500101700
5KimLambert1989-02-06T00:00102750102050
8LeslieJohnson1989-04-05T00:006463588295
9PhilForest1989-04-17T00:007506080815
20ChrisPapadopoulos1990-01-01T00:008965576450
11K. J.Weston1990-01-17T00:0086292.9483669.31
12TerriLee1990-05-01T00:005379376580.31
14StewartHall1990-06-04T00:0069482.6369856.19

5. Вывести процент, который составляет зарплата каждого сотрудника от общей суммы зарплат в его отделе

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYSALARY_PERCENT
12TerriLee05379320.17416
105Oliver H.Bender021285079.82583
85Mary S.MacDonald100111262.571.66089
127MichaelYanowski1004400028.3391
34JanetBaldwin11061637.8147.25274
61LukeLeung1106880552.74725
110YukiIchida115600000044.51038
118TakashiYamamoto115748000055.48961
28AnnBennet1202293523.94557

6. Вывести разницу между зарплатой сотрудника и средней зарплатой в его отделе.

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYSALARY_DIFF
12TerriLee053793-79528.5
105Oliver H.Bender021285079528.5
85Mary S.MacDonald100111262.533631.25
127MichaelYanowski10044000-33631.25
34JanetBaldwin11061637.81-3583.59
61LukeLeung110688053583.6
110YukiIchida1156000000-740000
118TakashiYamamoto1157480000740000
28AnnBennet12022935-8991.56
36RogerReeves12033620.631694.07

7. Вывести порядковый номер строки для каждого сотрудника в отделе, отсортированного по фамилии

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOROW_NUM
105Oliver H.Bender01
12TerriLee02
85Mary S.MacDonald1001
127MichaelYanowski1002
34JanetBaldwin1101
61LukeLeung1102
110YukiIchida1151
118TakashiYamamoto1152

8. Вывести зарплату самого высокооплачиваемого сотрудника в отделе для каждого сотрудника

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYMAX_SALARY
12TerriLee053793212850
105Oliver H.Bender0212850212850
85Mary S.MacDonald100111262.5111262.5
127MichaelYanowski10044000111262.5
34JanetBaldwin11061637.8168805
61LukeLeung1106880568805
110YukiIchida11560000007480000
118TakashiYamamoto11574800007480000
28AnnBennet1202293539224.06

9. Вывести разницу между зарплатой сотрудника и максимальной зарплатой в его отделе

EMP_NOFIRST_NAMELAST_NAMEDEPT_NOSALARYDIFFFROMMAX
12TerriLee053793159057
105Oliver H.Bender02128500
85Mary S.MacDonald100111262.50
127MichaelYanowski1004400067262.5
34JanetBaldwin11061637.817167.19
61LukeLeung110688050
110YukiIchida11560000001480000
118TakashiYamamoto11574800000
28AnnBennet1202293516289.06

10. Вывести ранг сотрудников по стажу

EMP_NOFIRST_NAMELAST_NAMEHIRE_DATEEXPERIENCE_RANK
4BruceYoung1988-12-28T00:001
2RobertNelson1988-12-28T00:001
5KimLambert1989-02-06T00:003
8LeslieJohnson1989-04-05T00:004
9PhilForest1989-04-17T00:005
20ChrisPapadopoulos1990-01-01T00:006
11K. J.Weston1990-01-17T00:007
12TerriLee1990-05-01T00:008
14StewartHall1990-06-04T00:009

11. Вывести накопленную сумму продаж для каждого сотрудника, отсортированную по дате продажи

EMP_NOORDER_DATETOTAL_VALUECUMULATIVE_SALES
111991-03-04T00:0050005000
111992-10-15T00:00200077000
111992-10-15T00:007000077000
111993-02-03T00:00600.577600.5
111993-04-27T00:002000097600.5
111993-11-11T00:0027000124600.5
111993-12-27T00:0014850139450.5
111993-12-31T00:000139450.5
611992-07-26T00:0029852985
611993-08-01T00:00900011985
611993-10-10T00:00490.6912475.69
611993-12-12T00:001600028475.69
611994-02-13T00:00900037475.69
721993-03-22T00:0047.547.5
721993-08-09T00:00399960.5960008
721993-08-09T00:00560000960008
1181993-08-20T00:0018000.418000.4
1181993-10-30T00:0021018210.4
1181993-12-12T00:00598024190.4
1211993-10-27T00:002693122693
1211993-10-27T00:00120000122693
1271993-08-16T00:0000
1271993-09-09T00:0012582.1212582.12
1271993-12-12T00:006000072582.12
1271994-01-04T00:003999.9976582.11
1271994-01-17T00:003399.1579981.26
1271994-02-07T00:00422210.97502192.23
1341993-08-27T00:001000010000
1341993-09-20T00:00100.0210100.02
1341993-12-12T00:00450000.49460100.51
1341993-12-18T00:00999.98462600.49
1341993-12-18T00:001500462600.49
1411994-01-06T00:001980.721980.72

12*. Вывести ранг сотрудников по общей сумме продаж

EMP_NOTOTAL_SALESSALES_RANK
729600081
127502192.232
134462600.493
11139450.54
1211226935
6137475.696
11824190.47
1411980.728

13*. Вычислить скользящее среднее продаж для каждого сотрудника за последний год

EMP_NOORDER_DATETOTAL_VALUEMOVING_AVG
111991-03-04T00:0050005000
111992-10-15T00:00200036000
111992-10-15T00:007000036000
111993-02-03T00:00600.524200.16
111993-04-27T00:002000023150.12
111993-11-11T00:002700015866.83
111993-12-27T00:001485015612.62
111993-12-31T00:00012490.1
611992-07-26T00:0029852985
611993-08-01T00:0090009000
611993-10-10T00:00490.694745.34
611993-12-12T00:00160008496.89
611994-02-13T00:0090008622.67
721993-03-22T00:0047.547.5
721993-08-09T00:00399960.5320002.66
721993-08-09T00:00560000320002.66
1181993-08-20T00:0018000.418000.4
1181993-10-30T00:002109105.2
1181993-12-12T00:0059808063.46
1211993-10-27T00:00269361346.5
1211993-10-27T00:0012000061346.5
1271993-08-16T00:0000
1271993-09-09T00:0012582.126291.06
1271993-12-12T00:006000024194.04
1271994-01-04T00:003999.9919145.52
1271994-01-17T00:003399.1515996.25
1271994-02-07T00:00422210.9783698.7
1341993-08-27T00:001000010000
1341993-09-20T00:00100.025050.01
1341993-12-12T00:00450000.49153366.83
1341993-12-18T00:00999.9892520.09
1341993-12-18T00:00150092520.09
1411994-01-06T00:001980.721980.72

Полезная информация

  1. Для выполнения лабораторной работы использовать материалы лекций 20 учебника.

Язык PSQL

Цель

Изучить возможности языка PSQL и научится создавать хранимые процедуры, хранимые функции и триггеры различных событий.

Порядок выполнения

1. Используя БД employee.fdb разработайте следующие объекты БД.

2. Создайте хранимую функцию, которая проверяет, является ли число простым.

Например, запрос

SELECT IS_SIMPLE(NULL) FROM RDB$DATABASE
UNION ALL
SELECT IS_SIMPLE(2) FROM RDB$DATABASE
UNION ALL
SELECT IS_SIMPLE(4) FROM RDB$DATABASE
UNION ALL
SELECT IS_SIMPLE(17) FROM RDB$DATABASE

должен вернуть

IS_SIMPLE
NULL
TRUE
FALSE
TRUE

3. Создайте функцию, которая рекурсивно вычисляет факториал своего аргумента.

Запрос

SELECT FACT(0) FROM RDB$DATABASE
UNION ALL
SELECT FACT(1) FROM RDB$DATABASE
UNION ALL 
SELECT FACT(2) FROM RDB$DATABASE
UNION ALL 
SELECT FACT(3) FROM RDB$DATABASE
UNION ALL 
SELECT FACT(9) FROM RDB$DATABASE

должен вернуть

FACT
1
1
2
6
362880

4. Создайте хранимую процедуру, которая вычисляет факториалы чисел от 0 до 9 включительно и использует функцию задания 2.

Запрос

SELECT * FROM FACT10

должен вернуть

IF
01
11
22
36
424
5120
6720
75040
840320
9362880

5. Создайте хранимую процедуру, которая удаляет строки указанной таблицы, содержащие дубликаты в указанном столбце.

Например, для таблицы

CREATE TABLE DUP (
    I INTEGER,
    S VARCHAR(2));

Содержащей данные с дубликатами в столбце I,

IS
1a
1b
2b
2c
3c
4d
5e

Вызов процедуры

EXECUTE PROCEDURE DEDUP();

Должен оставить в таблице только эти строки

IS
1a
2b
3c
4d
5e

6. Сделайте триггер на логирование удаления записи в таблице EMPLOYEE.

Триггер должен сохранять в таблицу EMP_HISTORY дату удаления и полное имя сотрудника.

Тогда после выполнения запроса

UPDATE EMPLOYEE SET LAST_NAME = 'SMITH' WHERE EMP_NO = 2;

Таблица EMP_HISTORY должна содержать

OLDLASTNAMENEWLASTNAMEDEL_TIME
NelsonSmith2025-04-12T23:29:44.414

7. Сделайте триггер, которые не дает подключаться в четные минуты

При попытке подключения в четную минуту, пользователь должен получать ошибку Bad time.

Полезная информация

Разработка десктоп приложения

Цель

Научиться использовать БД при разработке приложений с помощью библиотеки Qt.

Порядок выполнения

  1. Изучить полезную информацию для создания приложения.
  2. Протестировать полученное приложение и исправить или улучшить неправильное поведение.

Полезная информация

Структура БД

Для разработки приложения необходимо создать БД следующей структуры:

Создание проекта и интерфейса

Создаем новый проект

В качестве шаблона выбираем “Приложение/Приложение Qt Widgets”

Далее выбираем все опции по умолчанию, указав имя проекта (phones) и его расположение. В дереве объектов дважды кликаем по форме (mainwindow.ui) и приступаем к размещению элементов на форме. Для этого используются Push Button, меню и QTreeView. В меню создаем ряд пунктов для открытия, закрытия БД, выхода и показа справки.

Остальные компоненты размещаем как на рисунке. Первой кнопке устанавливаем свойства:

  • objectName = "btnAdd"
  • text = "Добавить"

Свойство text можно выставить двойным кликом по компоненту.

Аналогичным образом выставляем свойства оставшихся кнопок, так что в итоге должно получиться (слева направо):

  • btnAdd, "Добавить"
  • btnDel, "Удалить"
  • btnSave, "Сохранить"
  • btnCancel, "Отменить"
  • btnRefresh, "Обновить"

Чтобы выровнять их необходимо все выделить и скомпоновать по горизонтали (ПКМ (правая кнопка мыши), Компоновка, По горизонтали).

Далее перетаскиваем компонент Tree View и чтобы все выровнять, ПКМ по свободному месту на форме, Компоновка, По вертикали. Меняем у компонента свойство objectName="abonentView". У нас получится форма, как на рисунке, с выровненными компонентами, способными адаптироваться под изменения размеров окна. Подробнее о компоновке изучите отдельно.

Написание кода

Перед написанием добавим в файл проекта CMakeLists.txt компонент Sql и дописать в компоненты, линкуемые к приложению phones.

find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Sql)
...
target_link_libraries(phones PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Sql)

Перед добавлением кода обработчиков, добавить нужные заголовочные файлы для работы с базами данных и объявить поле для модели данных. Это делается в классе MainWindow файла mainwindow.h следующим образом:

#include <QMainWindow>
#include <QtSql/QSqlQuery>
#include <QtSql/QSqlTableModel>

namespace Ui {
class MainWindow;
}

// ранее сгенерированный код

private:
	Ui::MainWindow *ui;

	QSqlTableModel *abonentModel = nullptr;
};

Также добавим заголовочные файлы, которые нам понадобятся в файле mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"

// новые файлы
#include <QMessageBox>
#include <QtSql/QSqlDatabase>
#include <QtSql/QSqlError>
#include <QFileDialog>
#include <QDebug>

Обработчик нажатия пунктов меню пишется следующим образом:

  1. Выделяем нужный пункт меню на форме.
  2. Внизу также будет список пунктов. На нужном кликаем правой кнопкой мыши и выбираем “Переход к слоту”. Далее выбираем “triggered”.
  3. Открывается редактор кода, в котором мы увидим заготовку метода, который будет вызван при нажатии на пункт меню.

Добавляем код соединения с БД, т.е. обработчик пункта меню “Файл/Открыть”

// Connection is already open
if (abonentModel)  // Field of MainWindow
	return;

QSqlDatabase db = QSqlDatabase::addDatabase("QIBASE", "phones");
db.setHostName("localhost");
db.setDatabaseName(QFileDialog::getOpenFileName(this, "Open Database", "/var/rdb", "Database files (*.fdb);;All files (*)"));
 
const bool ok = db.open("SYSDBA", "masterkey");
 
if (!ok)
{
	ShowMessage("Ошибка подключения");
	return;
}

abonentModel = new QSqlTableModel(this, db);
abonentModel->setTable("ABONENT");
abonentModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
abonentModel->select();
ui->abonentView->setModel(abonentModel);

Код для закрытия БД

delete abonentModel;
abonentModel = nullptr;
QSqlDatabase::database("phones").close();

Можно протестировать. БД должна открываться и данные должны появляться в таблице. Функции ShowMessage нужна для вывода сообщений на экран. Она определяется в начале файла mainwindow.cpp следующим образом.

void ShowMessage(const QString text)
{
	QMessageBox msg;
	msg.setText(text);
	msg.exec();
}

Для написания обработчика нажатия кнопки кликните на ней на форме правой кнопкой мыши и в “Перейти к слоту” выберете сlicked. Ниже все обработчики кнопок. Говорящие названия обработчиков описывают действия.

void MainWindow::on_btnAdd_clicked()
{
	if (!abonentModel->insertRow(0))
		ShowMessage(abonentModel->lastError().text());
}

void MainWindow::on_btnSave_clicked()
{
	if (!abonentModel->submitAll())
		ShowMessage(abonentModel->lastError().text());
}

void MainWindow::on_btnDel_clicked()
{
	QModelIndex i = ui->abonentView->selectionModel()->currentIndex();
	if (!abonentModel->removeRow(i.row()))
		ShowMessage(abonentModel->lastError().text());
}

void MainWindow::on_btnCancel_clicked()
{
	abonentModel->revertAll();
}

void MainWindow::on_btnRefresh_clicked()
{
	abonentModel->select();
}

Теперь приложение можно запустить и протестировать.

Разработка веб приложения

Цель

Научиться использовать БД при разработке приложений с помощью фреймворка Flask и языка программирования Python.

Порядок выполнения

  1. Изучить полезную информацию для создания приложения.
  2. Реализовать приложение и при этом русифицировать интерфейс.

Полезная информация

В данной лабораторной работе рассматривается пример создания простейшего веб-приложения, называемого FBlog. Пользователи могут регистрироваться, входить в систему, создавать посты, редактировать и удалять свои посты.

За основу взята пошаговая инструкция с официального сайта Flask, однако в качестве СУБД используется российская СУБД РЕД База Данных.

Создание приложения

Прежде всего необходимо создать каталог, в котором будет располагаться проект.

$ mkdir fblog_project
$ cd fblog_project

Далее будем предполагать, что вся работа выполняется в каталоге fblog_project. Все пути к файлам будем указывать относительно него.

Теперь необходимо настроить виртуальное окружение для Python (Python virtual environment) и установить Flask и драйвер СУБД Ред База Данных.

Виртуальное окружение используется для управления зависимостями проекта как при разработке так и при разворачивании на продуктиве. Чем больше проектов есть у разработчика, тем более вероятно что они требуют различных версий библиотек Python или даже разных версий самого Python. Новые версии одних библиотек могут разрушить совместимости в другом проекте.

Виртуальное окружение (virtual environment) – это независимая группа библиотек для каждого проекта. Пакеты установленные для одного проекта не влияют на другие проекты или пакеты операционной системы.

Python поставляется с модулем venv для создания виртуального окружения.

Для создания виртуального окружения выполните команду

$ python3 -m venv venv

Перед началом работы над проектом активируйте соответствующее окружение. Обратите внимание, что команда начинается с точки с пробелом!

$ . venv/bin/activate

В активированном окружении установите Flask и драйвер для СУБД Ред База Данных:

$ pip install Flask
$ pip install fdb

Проекты на Python используют пакеты для организации кода и мы этим воспользуемся.

Каталог проекта будет содержать:

  • fblog_app: Пакет Python, содержащий код приложения и другие файлы.
  • venv: Виртуальное окружение, в котором будет установлен Flask, драйвер СУБД fdb и другие зависимости.

Приложение Flask это объект класса Flask. Все, что связано с приложением (настройки, URL адреса, прочее) будет настраиваться в этом объекте.

Наиболее простой путь создания приложения – это создать глобальный объект непосредственно в начале программы, однако по мере роста проекта это может принести проблемы.

Вместо создания объекта глобально, мы будем создавать его внутри функции. Такая функция называется фабрикой приложения. Все настройки, регистрации и т.п. будут происходить внутри функции, после чего объект приложения будет возвращен.

Фабрика приложения

Создайте каталог fblog_app, а внутри файл __init__.py, который содержит фабрику приложения и говорит Python, что каталог fblog_app должен рассматриваться как пакет.

Помните, что этот каталог мы создаем внутри fblog_project.

$ mkdir fblog_app

Файл __init__.py можно создать с помощью любого текстового редактора и сохранить в только что созданной каталоге fblog_app.

fblog_app/__init__.py

Внутри напишем следующий текст и сохраним.

import os

from flask import Flask

def create_app(test_config=None):
	# Создаем и настраиваем объект приложения app
	app = Flask(__name__, instance_relative_config=True)
	app.config.from_mapping(
		SECRET_KEY='dev',
		# Каталог /var/rdb/ должен быть, например после лабораторной работы №1
		DATABASE='localhost:/var/rdb/fblog.fdb', 
		USER='sysdba',
		PASSWORD='masterkey',
		LIBRARY='/opt/RedDatabase/lib/libfbclient.so'
	)

	# Простая страница, которая показывает сообщение
	@app.route('/hello')
	def hello():
		return 'Привет!'

	return app

create_app – это функция фабрика приложения. Позже она будет дополнена, но и сейчас она многое делает:

  1. app = Flask(__name__, instance_relative_config=True) создает объект приложения Flask:

    • __name__: имя текущего модуля Python. Приложению необходимо знать где оно располагается, чтобы установить некоторые пути.
    • instance_relative_config: говорит приложению, что файлы конфигурации размещаются относительно каталога instance. Он размещается вне каталога fblog_app и содержит локальные данные, конфигурационные файлы, БД.
  2. app.config.from_mapping устанавливает значения параметров конфигурации по умолчанию.

    • SECRET_KEY: используется классом Flask и расширениями для обеспечения безопасности хранимых данных. Значение dev позволяет удобно разрабатывать приложения, но должно быть заменено случайным значением при поставке приложения заказчику.
    • DATABASE: путь к файлу БД. БД размещается в каталоге /var/rdb/. В зависимости от нужд приложения, может быть любым, в том числе псевдонимом БД на удаленном сервере. При проблемах с правами доступа, можно указать /tmp/, но при перезагрузке, БД может быть удалена.
    • USER: Имя пользователя, от которого будет производиться соединение.
    • PASSWORD: Пароль. Для встроенного сервера игнорируется.
    • LIBRARY: путь до клиентской библиотеки libfbclient.so.
  3. @app.route() создает простой маршрут, чтобы убедиться что приложение работает, прежде чем продолжить его разрабатывать. Это связывает URL /hello и функцию, которая сформирует ответ. В данном случае строку 'Привет!'.

Запуск приложения

Теперь можно запустить приложение, используя команду flask. Укажите Flask где искать приложение и запустите его в режиме разработчика.

Вы должны быть в каталоге fblog_project, но не в его подкаталогах.

Режим разработчика показывает интерактивный отладчик когда страница выбрасывает исключение и перезапускает сервер, когда вы делаете изменения в коде. Его можно оставить запущенным и просто обновлять страницу в браузере по мере разработки.

$ export FLASK_APP=fblog_app
$ export FLASK_ENV=development
$ flask run

Вы увидите вывод, подобный этому:

* Serving Flask app "fblog_app"
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 855-212-761

Перейдите по адресу http://127.0.0.1:5000/hello в браузере и вы увидите сообщение "Привет!".

Работа с БД

Подключение к БД

При работе с БД первое, что необходимо сделать, создать подключение. Все запросы выполняются через подключение, которое закрывается когда работа выполнена.

В web-приложениях подключение обычно привязывается к запросу. В какой-то момент обработки запроса оно создается, а перед отправкой ответа закрывается.

Кроме этого необходимо написать код для инициализации БД в файле fblog_app/db.py

import fdb

import click
from flask import current_app, g
from flask.cli import with_appcontext


def init_db():
	try:
		conn = fdb.connect(
			dsn=current_app.config['DATABASE'],
			user=current_app.config['USER'],
			password=current_app.config['PASSWORD'],
			fb_library_name=current_app.config['LIBRARY']
		)
		conn.drop_database()
	except Exception as e:
		print(e)


	conn = fdb.create_database(
		dsn=current_app.config['DATABASE'],
		user=current_app.config['USER'],
		password=current_app.config['PASSWORD'],
		fb_library_name=current_app.config['LIBRARY']
	)

	metadata = [
		'''
		RECREATE TABLE users (
			id integer generated by default as identity primary key,
			username varchar(256) UNIQUE NOT NULL,
			password varchar(256) NOT NULL
		)
		''',
		'''
		RECREATE TABLE posts (
			id integer generated by default as identity primary key,
			author_id INTEGER NOT NULL REFERENCES users (id),
			created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
			title varchar(120) NOT NULL,
			body varchar(5000) NOT NULL
		)
		'''
	]

	cursor = conn.cursor()

	for query in metadata:
		cursor.execute(query)

	conn.commit()


def get_db():
	if 'db' not in g:
		g.db = fdb.connect(
			dsn=current_app.config['DATABASE'],
			user=current_app.config['USER'],
			password=current_app.config['PASSWORD'],
			fb_library_name=current_app.config['LIBRARY']
		)

	return g.db


def close_db(e=None):
	db = g.pop('db', None)

	if db is not None:
		db.close()


@click.command('init-db')
@with_appcontext
def init_db_command():
	init_db()
	click.echo('База данных инициализирована.')


def init_app(app):
	app.teardown_appcontext(close_db)
	app.cli.add_command(init_db_command)

get_db создает подключение к БД.

g: специальный объект, уникальный для каждого запроса. Он используется для хранения данных, которые могут использоваться множеством функции во время обработки запросы. Подключение создается и повторно используется, если get_db вызывается не первый раз.

current_app: другой специальный объект, который указывает на приложение Flask, обрабатывающее запрос.

fdb.connect: устанавливает подключение к БД используя параметры конфигурации. Файл БД создается в функции init_db.

close_db: закрывает подключение к БД, если g.db установлен.

init_db: создает БД и необходимые объекты.

metadata: список SQL команд для создания объектов БД: таблицы пользователей users и таблицы постов posts.

Вначале функция пытается установить соединение с имеющейся БД для того, чтобы ее удалить. В случае неудачи печатается исключение для целей отладки. Далее в любом случае создается новая БД, используя все те же параметры конфигурации.

conn.cursor(): создает объект курсор, с помощью которого можно выполнять все запросы к СУБД.

В цикле выполняются все запросы из списка метаданных и в завершении производиться завершение транзакции и применение всех изменений.

click.command() определяет команду командной строки init-db, которая вызывает функцию init_db и сообщает об успешности выполнения инициализации пользователю.

init_app(app) регистрирует созданные функции.

Функции close_db и init_db_command должны быть зарегистрированы в объекте приложения, иначе они не будут использоваться.

app.teardown_appcontext(): говорит Flask вызвать указанную функцию при очистке после отправки ответа.

app.cli.add_command(): добавляет новую команду, которая может вызываться с командой flask.

Импортируйте и добавьте вызов этой функции в фабрике приложения в конце файла fbclog_app/__init__.py

def create_app():
	app = ...
	# Существующий код пропущен

	from . import db
	db.init_app(app)

	return app

Инициализация БД

Теперь, когда команды init-db зарегистрирована, она может быть вызвана используя команду flask, аналогично команде run.

Если у вас все еще запущен сервер с предыдущего этапа, необходимо его остановить или выполнить команду в другом терминале. Помните что необходимо находиться в каталоге fblog_project и активировать виртуальное окружение.

Запустите команду flask init-db. При этом может быть выдана ошибка удаления старой БД, поскольку ее не существует. Это нормально, если далее выводится сообщение ’База данных инициализирована.`.

Например,

(venv) [student@localhost fblog_project]$ flask init-db
('Error while connecting to database:\n- SQLCODE: -902\n- I/O error during "open" operation for file "/var/rdb/fblog.fdb"\n- Error while trying to open file\n- No such file or directory', -902, 335544344)
База данных инициализирована.

В каталоге /var/rdb должен появится файл fblog.fdb.

(venv) [student@localhost fblog_project]$ ls /var/rdb
fblog.fdb
(venv) [student@localhost fblog_project]$ 

Эскизы (Blueprints) и представления (Views)

Функция-представление: (View function) функция, которая отвечает на запросы к приложению.

Flask использует шаблоны, чтобы сопоставить входящие URL запросов функциям-представлениям.

Эскиз: (Blueprint) способ организовать группы связанных представлений и другой код.

Вместо регистрации представление и другого кода непосредственно в приложении, они регистрируются в эскизе. Далее эскиз регистрируется в приложении, в фабрике приложения.

Создание эскиза

FBlog будет иметь два эскиза. Один для аутентификации, другой для функций работы с постами. Код для каждого эскиза будет размещаться в отдельных модулях. В первую очередь сделаем модуль аутентификации.

Создадим еще один файл fblog_app/auth.py следующего содержания

import functools

from flask import (
	Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash

from fblog_app.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')

Этот код создает эскиз под названием 'auth'. Как и объект приложение, эскиз должен знать где его создали. Для этого __name__ передается вторым параметром. url_prefix будет предшествовать всем URL адресам, связанным с этим эскизом.

Импортируйте и зарегистрируйте эскиз в фабрике приложения, используя app.register_blueprint(). Добавьте следующий код в конец фабрики приложения перед возвращением объекта приложения.

В файле fblog_app/__init__.py

допишите

def create_app():
	app = ...
	# Существующий код пропущен

	from . import auth
	app.register_blueprint(auth.bp)

	return app

Эскиз аутентификации будет иметь функции-представления для регистрации новых пользователей и входа/выхода существующих.

Первое представление: регистрация

Когда пользователь заходит на страницу /auth/register представление register вернет HTML код с формой для заполнения. При отправке данных формы она будет проверена и либо будет показано сообщение об ошибке, либо будет создан новый пользователь и осуществлен переход на страницу входа.

Сейчас просто допишем код функции-представления к имеющемуся коду файлы fblog_app/auth.py. Далее будут написаны шаблоны для генерации HTML форм.

fblog_app/auth.py

# Существующий код
# ...

@bp.route('/register', methods=('GET', 'POST'))
def register():
	if request.method == 'POST':
		username = request.form['username']
		password = request.form['password']
		db = get_db()
		error = None

		if not username:
			error = 'Требуется имя пользователя.'
		elif not password:
			error = 'Требуется пароль.'

		if error is None:
			try:
				db.cursor().execute(
					"INSERT INTO users (username, password) VALUES (?, ?)",
					(username, generate_password_hash(password)),
				)
				db.commit()
			except db.DatabaseError as e:
				if e.args[2] == 335544665:  #isc_unique_key_violation
					error = f"Пользователь {username} уже существует."
			else:
				return redirect(url_for("auth.login"))

		flash(error)

	return render_template('auth/register.html')

Вот что делает эта функция:

  1. @bp.route: сопоставляет URL /register с функцией-представлением register. Когда Flask получит запрос на /auth/register будет вызвана функция register и ее результат будет отправлен пользователю.

  2. Для отправки формы пользователем используется метод POST. Если это так, то проверяет входные параметры.

  3. request.form: специальный тип ассоциативного массива с параметрами формы, содержащий пары ключ-значение. Пользователь будет вводить туда username и password.

  4. Проверяем что введенные значение не пустые.

  5. Если проверка успешна, вставляем данные нового пользователя в базу данных.

    • db.cursor().execute: создает объект курсор из подключения и вызывает его метод execute для выполнения запроса на вставку в таблицу. Метод принимает SQL динамического запроса, с ? вместо значений и список значений. Такой способ защищает от SQL инъекций и строго рекомендуется в отличие от конструирования запроса как статического в виде одной строки.

    • Для целей безопасности пароль не храниться в БД в открытом виде. Он хешируется функцией generate_password_hash(). Поскольку этот метод меняет данные, то транзакцию необходимо подтвердить с помощью db.commit().

    • db.DatabaseError: исключение, которое будет выброшено при ошибке выполнения запроса. Код ошибки 335544665 соответствует ошибке дубликата первичного ключа, т.е. возникнет когда такой пользователь уже есть.

  6. После сохранения пользователя он перенаправляется на страницу входа. url_for() генерирует URL адрес представления на основе его имени. Это предпочтительнее прямой записи URL, т.к. позволяет менять адрес позднее, без смены остального кода. regirect() генерирует ответ для перенаправление на указанный (сгенерированный) URL адрес.

  7. Если проверка параметров будет неудачной, то пользователю будет показана ошибка. flash() сохраняет сообщения и они будут показаны при следующем рендеринге шаблона.

  8. При первом открытии страницы /auth/register или при ошибке проверки параметров, должна быть снова показана страница регистрации. render_template() рендерит шаблон, содержащий HTML, который мы вскоре напишем.

Представление: вход

Это представление пишется по той же схеме, что и представление register выше, также дописывается к существующему коду в файле fblog_app/auth.py

# Существующий код
# ...

@bp.route('/login', methods=('GET', 'POST'))
def login():
	if request.method == 'POST':
		username = request.form['username']
		password = request.form['password']
		db = get_db()
		error = None
		user = db.cursor().execute('SELECT * FROM users WHERE username = ?', 
			(username,) ).fetchonemap()

		if user is None:
			error = 'Некорректное имя пользователя.'
		elif not check_password_hash(user['password'], password):
			error = 'Некорректный пароль.'

		if error is None:
			session.clear()
			session['user_id'] = user['id']
			return redirect(url_for('index'))

		flash(error)

	return render_template('auth/login.html')

Есть несколько отличий от представления register:

  1. Для последующего использования в переменной сохраняется пользователь из таблицы users. fetchonemap() возвращает одну строку из запроса. Результат имеет тип словарь. Если запрос не вернул ни одной строки, результат будет None.
  2. check_password_hash() хеширует пароль из формы и сравнивает его с сохраненным. Если равны хеши паролей, то и пароли совпадают.
  3. session переменная типа словарь, которая хранит данные между запросами. Если аутентификация прошла успешно, тогда id пользователя сохраняется для новой сессии. Данные сохраняются в cookie которые отправляются в браузер. Браузер пришлет их обратно в запросах. Flask безопасно подписывает данные, так что они не могут подменены.

Теперь, когда идентификатор пользователя сохранен в сессии, он будет доступен последующим запросам. В начале каждого запроса, если пользователь прошел аутентификацию, информация о нем должна быть загружена и стать доступной другим представлениям.

Снова допишем в файл fblog_app/auth.py

# Существующий код
# ...

@bp.before_app_request
def load_logged_in_user():
	user_id = session.get('user_id')

	if user_id is None:
		g.user = None
	else:
		g.user = get_db().cursor().execute('SELECT * FROM users WHERE id = ?', 
			(user_id,)).fetchonemap()

@bp.before_app_request() регистрирует функцию, которая выполняется перед функциями-представлениями, безотносительно какой URL был запрошен.

load_logged_in_user() проверяет сохранен ли в session и если да, то читает информацию о нем из БД, сохраняя в g.user, который существует на протяжении всей обработки запроса. В противном случае g.user будет установлен в None.

Представления: выход

Для выхода необходимо удалить user_id из session. Тогда load_logged_in_user не сможет загружать пользователя в последующих запросах.

Допишем в коде fblog_app/auth.py

# Существующий код
# ...

@bp.route('/logout')
def logout():
	session.clear()
	return redirect(url_for('index'))

Требования входа в других представлениях

Создание, редактирование и удаление постов блога требует чтобы пользователь был авторизован. Декоратор используется для проверки каждого представления, к которому он примеряется.

Добавим декоратор в файле fblog_app/auth.py

# Существующий код
# ...

def login_required(view):
	@functools.wraps(view)
	def wrapped_view(**kwargs):
		if g.user is None:
			return redirect(url_for('auth.login'))

		return view(**kwargs)

	return wrapped_view

Этот декоратор возвращает новую функцию-представление, которая оборачивает оригинальное представление. Новая функция проверяет загружен ли пользователь и если нет перенаправляет на страницу входа. Если пользователь загружен, то вызывается оригинальная функция-представление и выполнение продолжается как обычно. Этот декоратор будет использован далее, при написании представлений блога.

Шаблоны (Templates)

Функции-представления написаны, но если мы перейдем по любому адресу, мы получим ошибку TemplateNotFound. Это потому что используется функция render_template, но для нее еще не написаны шаблоны. Шаблоны располагаются в каталоге templates внутри пакета fblog_app.

Шаблоны - это файлы, содержащие статические данные вместе с подстановками (placeholders) для динамических данных. Шаблоны рендерятся с конкретными данными для получения финального документа. Flask использует библиотеку шаблонов Jinja.

Jinja похожа на Python. Специальные разделители используются для отличия синтаксиса Jinja от статических данных шаблона. Содержимое между {{ и }} является выражением, результат которого будет выведен в финальный документ. {% и %} выделяют управляющие потоки типа if и for. В отличие от Python блоки выделяются в начале и конце тегами.

Базовый шаблон

Каждая страница приложения будет иметь одинаковую структуру с различным заполнением. Вместо написания HTML структуры для каждого шаблона, каждый шаблон будет расширять базовый и перезаписывать отдельные секции.

Создайте в каталоге fblog_app подкаталог templates, а в нем файл base.html.

В файле

fblog_app/templates/base.html

напишите

<!doctype html>
<title>{% block title %}{% endblock %} - Flaskr</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<nav>
<h1>FBlog</h1>
<ul>
	{% if g.user %}
	<li><span>{{ g.user['username'] }}</span>
	<li><a href="{{ url_for('auth.logout') }}">Выход</a>
	{% else %}
	<li><a href="{{ url_for('auth.register') }}">Регистрация</a>
	<li><a href="{{ url_for('auth.login') }}">Вход</a>
	{% endif %}
</ul>
</nav>
<section class="content">
<header>
	{% block header %}{% endblock %}
</header>
{% for message in get_flashed_messages() %}
	<div class="flash">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</section>

g автоматически доступна в шаблонах. В зависимости от g.user показываются либо имя пользователя и ссылка на выход, либо ссылка на регистрацию и ссылка на вход. url_for() тоже автоматически доступна и используется для генерации URL адресов на представления вместо ручного их написания.

После заголовка страницы и перед основным содержимым шаблон в цикле проходит по всем сообщениям, возвращаемым get_flashed_messaged(). Функция flask() использовалась в представлениям для показа сообщений об ошибках и этот код их показывает.

Здесь определены 3 блока, которые будут переписаны другими шаблонами:

  1. {% block title %} будет изменять отображаемый в браузере заголовок вкладки.
  2. {% block header %} подобен title, но будет изменять заголовок, отображаемый на странице.
  3. {% block content %} будет отображать содержимое каждой страницы, например форму входа или пост блога.

Этот базовый шаблон размещается прямо в каталоге templates. Для организованного хранения остальных, разместим их в каталогах по названиям эскизов.

Регистрация

В каталоге fblog_app/templates сделайте подкаталог auth, а в нем файл register.html

В файле

fblog_app/templates/auth/register.html

напишите

{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Регистрация{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="post">
	<label for="username">Пользователь</label>
	<input name="username" id="username" required>
	<label for="password">Пароль</label>
	<input type="password" name="password" id="password" required>
	<input type="submit" value="Регистрация">
</form>
{% endblock %}

{% extends 'base.html' %} говорит Jinja что этот шаблон заменяет блоки базового шаблона. Все отображаемое содержимое должно содержаться внутри блока {% block %}, которые перезаписывают блоки базового шаблона.

Здесь используется полезный паттерн размещения блока {% block title %} внутри блока {% block header %}. Это установит title block и затем итоговое значение будет использовано в header block. Так что оба заголовка используют одно и тоже название, без необходимости писать его дважды.

Вход

Этот шаблон аналогичен шаблону регистрации, за исключением заголовка и кнопки submit.

Создайте файл

fblog_app/templates/auth/login.html

и напишите в нем

{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Вход{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="post">
	<label for="username">Пользователь</label>
	<input name="username" id="username" required>
	<label for="password">Пароль</label>
	<input type="password" name="password" id="password" required>
	<input type="submit" value="Вход">
</form>
{% endblock %}

Регистрация

Теперь когда все шаблоны аутентификации написаны, можно зарегистрировать пользователя. Убедитесь что сервер запущен и перейдите по адресу http://127.0.0.1:5000/auth/register.

Попробуйте нажать на кнопку Регистрация без заполнения формы и посмотрите на ошибки.

При успешном заполнении имени пользователя и пароля, вы будете перенаправлены на страницу логина. Попробуйте ввести некорректный логин или пароль.

Если вы войдете в систему, то все равно должны увидеть ошибку, т.к. еще нет представления для index.

Статические файлы

Представления аутентификации и шаблоны выглядят слишком примитивными. Чтобы немного стилизовать HTML добавим CSS. Таблицы стилей являются статическими файлами.

Flask автоматически добавляет представление static, которое принимает путь относительно каталога fblog_app/static и обрабатывает его. Базовый шаблон base.html уже имеет статическую ссылку на файл style.css:

{{ url_for('static', filename='style.css') }}

Кроме CSS могут быть и другие типы статических файлов: JavaScript, изображения и т.п. Все они располагаются в каталог fblog_app/static и для ссылки используется url_for('static', filename='...').

Здесь мы не делаем упор на изучение CSS, так что просто создадим файл fblog_app/static/style.css со следующим содержимым.

html { font-family: sans-serif; background: #eee; padding: 1rem; }
body { max-width: 960px; margin: 0 auto; background: white; }
h1 { font-family: serif; color: #377ba8; margin: 1rem 0; }
a { color: #377ba8; }
hr { border: none; border-top: 1px solid lightgray; }
nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; }
nav h1 { flex: auto; margin: 0; }
nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; }
nav ul  { display: flex; list-style: none; margin: 0; padding: 0; }
nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; }
.content { padding: 0 1rem 1rem; }
.content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; }
.content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; }
.flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; }
.post > header { display: flex; align-items: flex-end; font-size: 0.85em; }
.post > header > div:first-of-type { flex: auto; }
.post > header h1 { font-size: 1.5em; margin-bottom: 0; }
.post .about { color: slategray; font-style: italic; }
.post .body { white-space: pre-line; }
.content:last-child { margin-bottom: 0; }
.content form { margin: 1em 0; display: flex; flex-direction: column; }
.content label { font-weight: bold; margin-bottom: 0.5em; }
.content input, .content textarea { margin-bottom: 1em; }
.content textarea { min-height: 12em; resize: vertical; }
input.danger { color: #cc2f2e; }
input[type=submit] { align-self: start; min-width: 10em; }

Откройте ссылку http://127.0.0.1:5000/auth/login и сейчас страница должна выглядеть как на картинке.

Больше о CSS можно узнать из документации https://developer.mozilla.org/docs/Web/CSS

Эскиз блога

Будут использованы те же подходы, что и для написания эскиза аутентификации. Блог показывает список всех постов, позволяя авторизованным пользователям создавать посты, а авторам менять и удалять посты.

Во время реализации каждого представления, оставляйте сервер запущенным (в режиме разработчика). После сохранения изменений, переходите по соответствующему URL в браузере и тестируйте его.

Объявим эскиз и добавим его в фабрику приложения fblog_app/blog.py

from flask import (
	Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort

from fblog_app.auth import login_required
from fblog_app.db import get_db

bp = Blueprint('blog', __name__)

Импортируйте и зарегистрируйте эскиз в файле fblog_app/__init__.py

def create_app():
	app = ...
	# Существующий код пропущен

	from . import blog
	app.register_blueprint(blog.bp)
	app.add_url_rule('/', endpoint='index')

	return app

В отличие от эскиза аутентификации, эскиз блога не имеет url_prefix. Таким образом представление index будет размещаться в корне /, представление create по адресу /create и т.д. Блог - основная функция FBlog и логично сделать index основным представлением.

Однако, точка входа для index будет blog.index. Некоторые представления аутентификации ссылаются на простой index. app.add_url_rule() связывает точку входа index с адресом /, так что url_for('index') и url_for('blog.index') будут работать одинаково, генерируя одинаковый адрес URL.

В других приложениях вам может потребоваться дать другой url_prefix для эскиза и определить другой index для приложения, подобный представлению hello.

Представление Index

Индекс будет показывать все посты, начиная с последних. SQL запрос использует JOIN для получения аутентификационной информации из таблицы users.

Добавьте маршрут в файле fblog_app/blog.py

@bp.route('/')
def index():
	db = get_db()
	posts = db.cursor().execute(
		'SELECT p.id, title, body, created, author_id, username'
		' FROM posts p JOIN users u ON p.author_id = u.id'
		' ORDER BY created DESC'
	).fetchallmap()
	return render_template('blog/index.html', posts=posts)

В файле fblog_app/templates/blog/index.html опишем шаблон

{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Сообщения{% endblock %}</h1>
{% if g.user %}
	<a class="action" href="{{ url_for('blog.create') }}">Новое</a>
{% endif %}
{% endblock %}

{% block content %}
{% for post in posts %}
	<article class="post">
	<header>
		<div>
		<h1>{{ post['title'] }}</h1>
		<div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
		</div>
		{% if g.user['id'] == post['author_id'] %}
		<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Изменить</a>
		{% endif %}
	</header>
	<p class="body">{{ post['body'] }}</p>
	</article>
	{% if not loop.last %}
	<hr>
	{% endif %}
{% endfor %}
{% endblock %}    

Когда пользователь авторизован, блок header добавляет ссылку на представление create. Когда пользователь автор поста, он увидит ссылку Изменить связанную с представлением update для поста. loop.last – это специальная переменная, доступная внутри циклов Jinja. Она используется для исключения печати разделительной линии для последнего поста.

Представление Create

Представление create походе на представление register. Либо отображается форма для заполнения данных, либо введенные данные проверяются и пост добавляется в базу данных или показывается ошибка.

Декоратор login_required, который мы написали ранее, будет использован для представлений блога. Пользователь должен быть авторизован, чтобы открывать эти представления, иначе он будет перенаправлен на страницу входа.

Опишем в файле fblog_app/blog.py

@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
	if request.method == 'POST':
		title = request.form['title']
		body = request.form['body']
		error = None

		if not title:
			error = 'Заголовок обязателен.'

		if error is not None:
			flash(error)
		else:
			db = get_db()
			db.cursor().execute(
				'INSERT INTO posts (title, body, author_id)'
				' VALUES (?, ?, ?)',
				(title, body, g.user['id'])
			)
			db.commit()
			return redirect(url_for('blog.index'))

	return render_template('blog/create.html')

Созданим шаблон fblog_app/templates/blog/create.html

{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Новое сообщение{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="post">
	<label for="title">Заголовок</label>
	<input name="title" id="title" value="{{ request.form['title'] }}" required>
	<label for="body">Содержание</label>
	<textarea name="body" id="body">{{ request.form['body'] }}</textarea>
	<input type="submit" value="Сохранить">
</form>
{% endblock %}

Представление Update

Представлениям update и delete необходимо извлечь сообщение по его идентификатору и сравнить автора с зарегистрированным пользователем. Чтобы избежать дублирования кода, напишем функцию получения поста и в дальнейшем используем ее в обоих представлениях.

Допишем файл fblog_app/blog.py

def get_post(id, check_author=True):
	post = get_db().cursor().execute(
		'SELECT p.id, title, body, created, author_id, username'
		' FROM posts p JOIN users u ON p.author_id = u.id'
		' WHERE p.id = ?',
		(id,)
	).fetchonemap()

	if post is None:
		abort(404, f"Сообщение номер {id} не существует.")

	if check_author and post['author_id'] != g.user['id']:
		abort(403)

	return post

abort() выбрасывает специальное исключение, которое возвращает код статуса HTTP. Она принимает текст ошибки. Код 404 означает что страница не найдена (Not Found), а код 403 означает что доступ к странице запрещен (Forbidden).

Аргумент check_author может быть полезен в будущем.

Код представления update в файле fblog_app/blog.py

@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
	post = get_post(id)

	if request.method == 'POST':
		title = request.form['title']
		body = request.form['body']
		error = None

		if not title:
			error = 'Заголовок обязателен.'

		if error is not None:
			flash(error)
		else:
			db = get_db()
			db.cursor().execute(
				'UPDATE posts SET title = ?, body = ?'
				' WHERE id = ?',
				(title, body, id)
			)
			db.commit()
			return redirect(url_for('blog.index'))

	return render_template('blog/update.html', post=post)

В отличие от представлений, что мы писали до этого, update принимает аргумент, id. На это указывает <int:id>. Реальный адрес будет выглядеть, например, /1/update. Flask извлечет 1 из адреса, убедиться что это значение типа int и передаст его как аргумент функции. Если не указать int: то аргумент будет string. Чтобы генерировать адреса для страницы обновления, url_for() необходимо передать id для заполнения: url_for('blog.update', id=post['id']).

Представления create и update похожи друг на друга. Основное отличие в том, что update использует объект post и запрос UPDATE вместо INSERT. Теоретически можно придумать единый шаблон для этих двух представлений, но для наших целей мы не будем усложнять.

fblog_app/templates/blog/update.html

{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="post">
	<label for="title">Заголовок</label>
	<input name="title" id="title"
	value="{{ request.form['title'] or post['title'] }}" required>
	<label for="body">Содержание</label>
	<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
	<input type="submit" value="Сохранить">
</form>
<hr>
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
	<input class="danger" type="submit" value="Удалить" onclick="return confirm('Вы уверены?');">
</form>
{% endblock %}

Шаблон имеет две формы. Первая отправляет отредактированные данные на текущую страницу /<id>/update. Вторая форма содержит единственную кнопку и определяет атрибут action ссылающийся на представление delete. Кнопка использует JavaScript для запроса подтверждения действия.

Паттерн {{ request.form['title'] or post['title'] }} используется в зависимости от данных, которые надо показать в форме. Когда форма не отправлена, показываются данные оригинального поста, но если в форму переданы некорректные данные, необходимо показать это чтобы пользователь исправил ошибку. Для этого используется request.form. request - это еще одна переменная, автоматически доступная в шаблонах.

Представление Delete

Это представление не имеет своего шаблона, а кнопка удаления является частью шаблона update.html. Таким образом, достаточно написать только функцию представление и обработать только метод POST, а потом перенаправить на представление index.

Добавим маршрут в fblog_app/blog.py

@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
	get_post(id)
	db = get_db()
	db.cursor().execute('DELETE FROM posts WHERE id = ?', (id,))
	db.commit()
	return redirect(url_for('blog.index'))

На этом разработка первого приложения завершена. Необходимо его тщательно протестировать.

Для изучения того, как тестировать и распространять подобные приложения рекомендуем изучить материалы по Flask - https://flask.palletsprojects.com/en/2.0.x/tutorial

Разработка API для бэкенда

Цель

Научиться использовать БД при разработке RESTful API с помощью фреймворка Gin и языка программирования Go.

Порядок выполнения

  1. Изучить полезную информацию для создания приложения.
  2. Реализовать API и протестировать его работу.

Полезная информация

Данный пример демонстрирует реализацию простейшего RESTful API, работающего с БД, на языке программирования Go (Golang) с использованием фреймворка Gin и стандартного пакета database/sql для работы с базами данных.

Подробнее вы можете посмотреть следующие ссылки:

  • https://go.dev/doc/database/
  • https://go.dev/doc/tutorial/database-access
  • https://go.dev/doc/tutorial/web-service-gin

Пример включает в себя:

  1. Подготовка среды разработки.
  2. Проектирование API.
  3. Создание проекта.
  4. Инициализация БД.
  5. Подключение к БД.
  6. Получение всех альбомов
  7. Добавление альбома

Какие инструменты и технологии используются?

  1. Язык программирования: Go (Golang).
  2. Web фреймворк Gin.
  3. Пакет для работы с базой данных: database/sql.
  4. СУБД РЕД База Данных.
  5. Драйвер для РЕД Базы Данных.
  6. Операционная система РЕД ОС.
  7. Текстовый редактор для написания программы (Меню/Стандартные/Текстовый редактор)

Подготовка среды разработки

Установка GoLang

  1. Выполните в терминале команду для установки golang:
sudo dnf install golang
  1. Установите golang tools:
sudo go install -v golang.org/x/tools/gopls@latest
  1. Установите отладчик для GoLang:
go install github.com/go-delve/delve/cmd/dlv@latest

Установка RedDatabase

Установка РЕД Базы Данных подробно рассмотрена в:

  1. Руководство администратора в документации
  2. Видео-курсе администрирования

Проектирование API

В примере мы разработаем RESTful API, которое предоставляет доступ к магазину по продаже винтажных записей на виниле. Так что нужно будет предоставить конечные точки, через которые клиент может получить и добавить альбомы для пользователей.

Разработка API обычно начинается с разработки конечных точек. Важно чтобы точки можно было легко понять.

В в этом уроке мы создадим следующие точки

/albums

  • GET – Получить список альбомов в виде JSON.
  • POST – Добавить новый альбом из данных в виде JSON, полученных в запросе.

Создание проекта

  1. Для начала нужно создать папку нашего проекта (он будет находиться в домашней директории) и перейдем в нее для дальнейшей работы.
mkdir ~/rdb_go
cd ~/rdb_go
  1. Инициализируем модуль Go
go mod init rdb_go
  1. С помощью текстового редактора создадим файл main.go с таким содержимым и сохраним его в папке проекта ~/rdb_go:
package main

import "fmt"

func main() {
	fmt.Println("Hello, Go!")
}
  1. Выполните программу с помощью команды go run .. На экране выведется Hello, Go!.

Инициализация БД

Теперь создадим папку, в которой будут храниться базы данных и выдадим ей права доступа, чтобы сервер СУБД имел доступ к файлам БД.

Для версии РЕД База Данных не выше 3 версии или Firebird:

mkdir /db
sudo chown firebird: /db

Для версии РЕД База Данных 5 и выше.

mkdir /db
sudo chown reddatabase: /db

Теперь, используя РЕД Эксперт, создайте в каталоге /db новую БД recordings.fdb. Полный путь к БД будет /db/recordings.fdb. В этой БД создайте таблицу ALBUM и добавьте в нее несколько записей, согласно скрипту.

RECREATE TABLE ALBUM (
  ID         INT GENERATED ALWAYS AS IDENTITY NOT NULL PRIMARY KEY,
  TITLE      VARCHAR(128) NOT NULL,
  ARTIST     VARCHAR(255) NOT NULL,
  PRICE      DECIMAL(5,2) NOT NULL
);

INSERT INTO ALBUM (TITLE, ARTIST, PRICE) VALUES ('Лебединое озеро', 'Чайковский', 56.99);
INSERT INTO ALBUM (TITLE, ARTIST, PRICE) VALUES ('Чародейка', 'Чайковский', 63.99);
INSERT INTO ALBUM (TITLE, ARTIST, PRICE) VALUES  ('Времена года', 'Вивальди', 17.99);
INSERT INTO ALBUM (TITLE, ARTIST, PRICE) VALUES  ('Годы странствий', 'Ференц Лист', 34.98);

Подключение к БД

Добавьте импорт библиотеки для работы с РЕД База Данных в main.go. Теперь секция import должна выглядеть так.

import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/nakagami/firebirdsql"
)

Чтобы установить ее для сборки нашего проекта, в каталоге с нашей программой ~/rdb_go выполните

go get .

Эта команда автоматически загрузит все необходимые библиотеки и подключит все зависимости. Далее, при подключении новых зависимостей, эту команду нужно будет повторять. Сразу все зависимости прописать не получится, так как неиспользуемые библиотеки считаются ошибкой. Обратите внимание, что пакет github.com/nakagami/firebirdsql импортируется с подчеркиванием _, что заставляет компилятор вызвать инициализацию пакета, несмотря на то, что явно его содержимое нигде не используется. Оно будет использоваться объектами пакета database/sql.

Теперь напишем код для доступа к нашей БД. Для этого будем использовать глобальный указатель на структуру sql.DB.

В модуле main.go объявите глобальную переменную db и перепишите функцию main как указано ниже.

var db *sql.DB

func main() {
	// Строка подключения
	connStr := "sysdba:masterkey@localhost/db/recordings.fdb"

	// Подключение к базе данных
	var err error
	db, err = sql.Open("firebirdsql", connStr)
	if err != nil {
		log.Fatalf("Ошибка подключения к базе данных: %v", err)
	}
	defer db.Close()

	// Проверка соединения
	if err := db.Ping(); err != nil {
		log.Fatalf("Не удалось подключиться к базе данных: %v", err)
	}
	fmt.Print("Подключен")
}

db – это глобальная (для упрощения примера) переменная для работы с базой данных. В реальных проектах избегайте использование глобальных переменных, а передавайте необходимые значения как аргументы функции при вызовах.

connStr – строковая переменная для хранения строки подключения к БД. В строке подключения указан пользователь (sysdba), пароль (masterkey), адрес сервера (localhost) и путь к файлу БД на этом сервере (/db/recordings.fdb). Еще раз обратив внимание, что с файлом работает сервер СУБД и на том компьютере, где он запущен.

sql.Open – открывает БД и инициализирует переменную db. В случае возникновения ошибки, она будет записана в переменную err. Если ошибка возникла, для облегчения диагностики мы выводим ее с помощью log. При этом метод Fatalf выводит форматированное сообщение и немедленно завершает работу программы. В реальных проектах ошибку вероятнее всего необходимо будет обработать и продолжить работу программы.

sql.Ping – проверяет соединение. В зависимости от драйвера, sql.Open может не установить соединение с сервером сразу же, а лишь при первом обращении. sql.Ping используется чтобы выполнить простейший запрос и убедиться что соединение действительно установлено. Обработка ошибок осуществляется аналогичным образов.

В завершение функции, выводим сообщение об успешности подключения.

Получение всех альбомов

Теперь напишем функцию, которая будет запрашивать все записи таблицы ALBUM, записывать их в массив структур и возвращать в качестве результата.

В файле main.go опишем структуру для хранения записей об альбомах.

// Album представляет структуру записи в таблице ALBUM
type Album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

Тэги в этой структуре, такие как json:"artist" определяют имя полей, при сериализации или десериализации структуры JSON.

После функции main вставим описание новой функции.

func queryAlbums() ([]Album, error) {
	var albums []Album

	// SQL-запрос для поиска альбомов
	query := "SELECT ID, TITLE, ARTIST, PRICE FROM ALBUM"
	rows, err := db.Query(query)
	if err != nil {
		return nil, fmt.Errorf("ошибка getAlbums: %v", err)
	}
	defer rows.Close()

	// Обработка результата
	for rows.Next() {
		var alb Album
		if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
			return nil, fmt.Errorf("ошибка getAlbums: %v", err)
		}
		albums = append(albums, alb)
	}

	// Проверка ошибок при обработке строк
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("ошибка getAlbums: %v", err)
	}

	return albums, nil
}

Функция queryAlbums принимает параметр db *sql.DB – подключение к БД, которое используется для выполнения запроса.

Результатом функции является массив записей Album и код ошибки.

Для хранения результата функции объявляем переменную albums.

db.Query используется для выполнения запроса (первый параметр). Параметры запроса могут быть указаны через запятую как второй и последующие параметры.

defer rows.Close() гарантирует, что ресурсы, связанные с результатами запроса, будут освобождены после выхода из функции. Цикл для обработки строк rows.Next() перемещается по каждой строке результата. Для каждой строки создаётся новая переменная типа Album (alb), в которую записываются данные из столбцов.

rows.Scan считывает данные столбцов текущей строки и записывает их в поля структуры alb.

Проверить что код работает достаточно просто. Добавьте печать результата этой функции последней строкой функции main.

func main() {
...
	fmt.Print("Подключен")
	fmt.Print(queryAlbums())
}

И при выполнении будет выдан следующий результат

Подключен[{1 Лебединое озеро Чайковский 56.99} {2 Чародейка Чайковский 63.99} {3 Времена года Вивальди 17.99} {4 Годы странствий Ференц Лист 34.98}] <nil>

После объявления структуры Album добавьте функцию, которая будет записывать массив записей Album в JSON представление для ответа HTTP запроса GET.

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
	albums, _ := queryAlbums()
	c.IndentedJSON(http.StatusOK, albums)
}

Эта функция принимает в качестве параметра gin.Context. Это наиболее важная часть фреймворка Gin. Она хранит детали запроса, проверяет целостность и сериализует JSON и т.д.

IndentedJSON сериализует структуру в JSON и добавляет ее в ответ запроса. Первый параметр функции это код статуса HTTP, который будет отправлен клиенту. Здесь мы отправляем StatusOK, чтобы отправить статус 200 OK.

Теперь давайте изменим последний строки кода функции main, так, чтобы вместо печати результата вызова queryAlbums, запускать веб-сервер и регистрировать обработчик вызова GET /albums (getAlbums).

Теперь завершение функции main должно выглядеть следующий образом.

func main() {
...
	fmt.Print("Подключен")

	router := gin.Default()
	router.GET("/albums", getAlbums)

	router.Run("localhost:8080")
}

Здесь инициализируется маршрутизатор Gin и регистрируется обработчик HTTP GET для пути /albums с нашей функцией getAlbums.

В завершении запускается веб-сервер, слушающий сетевой порт 8080.

Не забудем импортировать вновь задействованные библиотеки.

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"github.com/gin-gonic/gin"

	_ "github.com/nakagami/firebirdsql"
)

и установить их

go get .

Запустим наше приложение и обратимся к веб-серверу в командной строке.

curl http://localhost:8080/albums

Результатом работы команды будет:

[
	{
		"ID": 1,
		"Title": "Лебединое озеро",
		"Artist": "Чайковский",
		"Price": 56.99
	},
	{
		"ID": 2,
		"Title": "Чародейка",
		"Artist": "Чайковский",
		"Price": 63.99
	},
	{
		"ID": 3,
		"Title": "Времена года",
		"Artist": "Вивальди",
		"Price": 17.99
	},
	{
		"ID": 4,
		"Title": "Годы странствий",
		"Artist": "Ференц Лист",
		"Price": 34.98
	}
]

Для более красивого вывода можно использовать различные средства фронтенд разработки, но это выходит за рамки данной инструкции.

Добавление альбома

Теперь давайте добавим функцию для вставки записи в нашу БД.

Для выполнения запросов, не возвращающих данные, используется метод Exec. Сразу после функции queryAlbums добавим нашу новую функцию.

// addAlbum добавляет запись в БД
func addAlbum(alb Album) (error) {
	_, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
	if err != nil {
		return fmt.Errorf("ошибка addAlbum: %v", err)
	}
	return nil
}

db.Exec – выполняет SQL-запрос на добавления записи в таблицу ALBUM. При этом ID мы не передаем, потому что это первичный ключ, и он сгенерируется автоматически.

Теперь давайте добавим вызов этой функции через метод POST /albums.

Для этого напишем обработчик HTTP-запроса.

// postAlbums добавляет в БД запись, на основе JSON запроса.
func postAlbums(c *gin.Context) {
	var newAlbum Album

	if err := c.BindJSON(&newAlbum); err != nil {
		return
	}

	addAlbum(newAlbum)
	c.IndentedJSON(http.StatusCreated, newAlbum)
}

BindJSON связывает запрос с полями переменной newAlbum. Далее вызывается наша функция addAlbum для сохранения полученных данных в БД и в HTTP-ответ добавляется статус 201 вместе с JSON только что вставленной записи.

Добавим регистрацию нового обработчика в функции main.

func main () {
...
	router := gin.Default()
	router.GET("/albums", getAlbums)
	router.POST("/albums", postAlbums)

	router.Run("localhost:8080")
}

Чтобы проверить, снова используем утилиту curl и выполним команду

curl http://localhost:8080/albums --include --header "Content-Type: application/json" --request "POST" --data '{"id": "0","title": "Кащей Бессмертный","artist": "Римский-Корсаков","price": 299.99}'

Ее вывод должен быть следующим.

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sun, 27 Apr 2025 20:39:06 GMT
Content-Length: 116

{
	"id": "0",
	"title": "Кащей Бессмертный",
	"artist": "Римский-Корсаков",
	"price": 299.99
}

Последующий вызов

curl http://localhost:8080/albums

должен уже показать наличие только что вставленной записи

[
	{
		"id": "1",
		"title": "Лебединое озеро",
		"artist": "Чайковский",
		"price": 56.99
	},
	{
		"id": "2",
		"title": "Чародейка",
		"artist": "Чайковский",
		"price": 63.99
	},
	{
		"id": "3",
		"title": "Времена года",
		"artist": "Вивальди",
		"price": 17.99
	},
	{
		"id": "4",
		"title": "Годы странствий",
		"artist": "Ференц Лист",
		"price": 34.98
	},
	{
		"id": "5",
		"title": "Кащей Бессмертный",
		"artist": "Римский-Корсаков",
		"price": 299.99
	}
]

Заключение

Поздравляем! Вы завершили реализацию простого RESTful API на языке Go с использованием фреймворка Gin, работающего с базой данных РЕД База Данных.