3. Язык баз данных SQL/92
Стандарт SQL/92 был принят в 1992 г. Он был разработан на основе предыдущего стандарта SQL/89 и предназначен для замены этого стандарта (как мы упоминали во введении курса, пока это не произошло в полном объеме). По сравнению со стандартом SQL/89, в стандарте SQL/92 специфицирован ряд новых возможностей. Тем не менее, в основном обеспечивается совместимость стандарта SQL/89 со стандартом SQL/92. Известные к моменту приема стандарта SQL/92 его несовместимости со стандартом SQL/89 специфицированы в приложении E (мы их обсудим отдельно в четвертой части курса).
В стандарте SQL/92 содержатся существенные расширения и уточнения возможностей стандарта SQL/89, а также введен ряд новых возможностей. Расширения и уточнения состоят в следующем:
- более точное определение средств прямого (интерактивного) вызова операторов SQL;
- улучшенные возможности диагностики, включающие поддержку нового параметра статуса (SQLSTATE), области диагностики и поддерживающих операторов.
Существенными новыми свойствами являются:
- поддержка дополнительных типов данных: DATE, TIME, TIMESTAMP, INTERVAL, битовая строка, символьные и битовые строки переменного размера, строки символов национального алфавита;
- поддержка наборов символов, не требуемых для самого языка SQL, и соответствующих средств сортировки;
- поддержка дополнительных скалярных операций, таких как операции конкатенации строк и выделения подстрок, операции с датами и временем, а также условные выражения;
- повышенная общность и ортогональность использования скалярных и табличных выражений запросов;
- расширенный набор операций над множествами (в частности, соединение объединением, естественное соединение, вычитание и пересечение множеств);
- возможность определения доменов в схеме;
- поддержка средств манипулирования схемой (к наиболее существенным относятся операторы DROP и ALTER);
- возможность связывания (в синтаксисе языка модулей и встроенного SQL) с языками Ада, Си и MUMPS;
- дополнительные возможности определения привилегий доступа;
- дополнительные возможности определения ссылочной целостности, включая ссылочные действия, разрешение использовать подзапросы в проверочных ограничениях, отдельно определяемые утверждения целостности, управление отложенным режимом проверки ограничений на уровне пользователя;
- определение информационной схемы базы данных;
- поддержка динамического выполнения операторов SQL;
- поддержка некоторых возможностей, требуемых для удаленного доступа к базам данных (особенно существенны операторы подключения к базе данных и средства задания квалифицированных имен схем);
- поддержка временных таблиц;
- поддержка уровней согласованности транзакций;
- поддержка преобразований типов данных (выражения CAST);
- поддержка прокручиваемых (scrolled) курсоров;
- требование наличия в реализации средства отслеживания соответствия стандарту используемых конструкций (flaggingcapability) для поддержки мобильности прикладных программ.
Стандарт имеет следующую структуру:
Раздел 1: "Область применения";
Раздел 2: "Нормативные ссылки" - перечень дополнительных стандартов;
Раздел 3: "Определения, обозначения и соглашения";
Раздел 4: "Понятия" - базовые понятия, используемые при определении языка;
Раздел 5: "Лексические элементы";
Раздел 6: "Скалярные выражения" - определение элементов языка, производящих скалярные значения;
Раздел 7: "Выражения запросов" - определение элементов языка, производящих строки и таблицы данных;
Раздел 8: "Предикаты";
Раздел 9: "Правила присваивания данных" - правила, определяющие порядок выборки данных из базы данных или сохранения данных в базе данных, и правила формирования операций над множествами;
Раздел 10: "Дополнительные общие элементы" - дополнительные общие языковые элементы, используемые в разных частях языка;
Раздел 11: "Определение схемы и манипулирование схемой" - определение средств создания схемы и управления схемой;
Раздел 12: "Модуль" - определение модулей и процедур;
Раздел 13: "Манипулирование данными" - определение операторов манипулирования данными;
Раздел 14: "Управление транзакциями";
Раздел 15: "Управление подключениями";
Раздел 16: "Управление сессиями";
Раздел17: "Динамический SQL" - определение средств, предназначенных для динамического выполнения операторов SQL;
Раздел 18: "Управление диагностикой"
Раздел 19: "Встроенный SQL" - определение синтаксиса встраивания SQL в некоторые стандартные языки программирования;
Раздел 20: "Прямой вызов SQL" - определение подмножества SQL/92, которое может использоваться в режиме прямого вызова;
Раздел21: "Информационная схема и схема определений" - определение представляемых таблиц, содержащих информацию о схеме;
Раздел22: "Коды статуса" - определение значений, идентифицирующих статус завершения SQL-операторов, и механизмов, посредством которых эти значения возвращаются;
Раздел 23: "Соответствие" - определение критериев соответствия языку SQL/92;
Далее следует набор приложений, которые не являются частью стандарта, а предназначены только для информирования пользователей и разработчиков.
Приложение A: "Уровни языка SQL" - перечисление правил, определяющих подмножества вводного SQL (EntrySQL) и промежуточного SQL (IntermediateSQL) стандарта SQL/92;
Приложение B: "Элементы, определяемые в реализации" - перечисление средств, для которых в стандарте установлено, что синтаксис, или смысл, или результат воздействия на базу данных частично или полностью определяется в реализации, и описание определяющей информации, которая должна быть обеспечена в реализации для каждого случая;
Приложение C: "Элементы, зависимые от реализации" - перечисление средств, для которых в стандарте явно установлено, что их смысл или результат воздействия на базу данных зависит от реализации;
Приложение D: "Неодобряемые свойства" - перечисление свойств, которые разработчики стандарта не хотели бы видеть в его следующей версии;
Приложение E: "Несовместимости с SQL/89" - перечисление несовместимостей между SQL/92 и SQL/89;
Приложение F: "Поддержка и интерпретация SQL" - разъясняются интерпретации SQL и коррективы, которые были внесены после принятия SQL/89.
В следующих разделах мы обсудим наиболее важные, с нашей точки зрения, свойства SQL/92. Это не будет полный пересказ стандарта, поскольку, во-первых, он имеет слишком большой объем, а во-вторых, многие важные в практических целях средства не отличаются от средств ранее рассмотренного стандарта SQL/89. Поэтому в основном мы будем говорить о дополнительных средствах SQL/92, отличающих этот стандарт от SQL/89.
3.1. Основные понятия
В отдельном разделе стандарта описываются основные понятия, на которые он опирается. Как кажется, знакомство с этой частью стандарта очень полезно для общего понимания сути языка.
3.1.1. Типы данных
В стандарте тип данных определяется как множество представляющих его значений. Логическим представлением значения является литерал. Физическое представление зависит от реализации. В языке SQL значение любого типа является примитивным в том смысле, что в соответствии со стандартом оно не может быть логически разбито на другие значения. Значения могут быть определенными или неопределенными. Неопределенное значение - это зависящее от реализации значение, которое гарантированно отлично от любого определенного значения соответствующего типа. Можно считать, что имеется всего одно неопределенное значение, входящее в любой тип данных языка SQL. Для неопределенного значения отсутствует представляющий его литерал, хотя в некоторых случаях используется ключевое слово NULL для выражения того, что желательно именно неопределенное значение.
В SQL/92 определяются различные типы данных, обозначаемые следующими ключевыми словами: CHARACTER, CHARACTERVARYING, BIT, BITVARYING, NUMERIC, DECIMAL, INTEGER, SMALLINT, FLOAT, REAL, DOUBLEPRECISION, DATE, TIME, TIMESTAMP и INTERVAL.
Типы данных CHARACTER и CHARACTERVARYING совместно называются типами данных символьных строк; типы данных BIT и BITVARYING - типами данных битовых строк. Типы данных символьных строк и типы данных битовых строк совместно называются строчными типами данных, а значения строчных типов называются строками.
Типы данных NUMERIC, DECIMAL, INTEGER и SMALLINT совместно называются типами данных точных чисел. Типы данных FLOAT, REAL и DOUBLEPRECISION совместно называются типами данных приблизительных чисел. Типы данных точных чисел и типы данных приблизительных чисел совместно называются числовыми типами. Значения числовых типов называются числами.
Типы данных DATE, TIME и TIMESTAMP совместно называются типами даты-времени. Значения типов даты-времени называются "дата-время". Тип данных INTERVAL называется интервальным типом.
3.1.1.1. Символьные строки
Тип данных символьных строк может содержать строки постоянной (CHARACTER) или переменной (CHARACTERVARYING) длины. Для конкретного типа CHARACTER указывается длина строк этого типа; в случае CHARACTERVARYING - максимальная длина.
Определен ряд операций, которые можно выполнять над символьными строками. Перечислим некоторые из них.
К операторам, операнды которых являются символьными строками и которые возвращают символьные строки, относятся следующие:
- Оператор конкатенации (обозначается в виде ) возвращает символьную строку, произведенную путем соединения строк-операндов в том порядке как они заданы.
- Функция выделения подстроки (SUBSTRING) принимает три параметра - строку, номер начальной позиции и длину и возвращает строку, выделенную из строки-параметра в соответствии со значениями двух последних параметров.
- Функция UPPER возвращает строку, в которой все малые буквы строки-параметра заменяются на прописные. Функция LOWER, наоборот, заменяет в заданной строке все прописные буквы на малые.
- Выражение длины возвращает длину заданной символьной строки в символах, октетах или битах (в зависимости от вида вычисляющей функции) виде целого числа.
- Выражение позиции определяет первую позицию в строке S, с которой в нее входит заданная строка S1 (если не входит, то возвращается нуль).
Заметим, что мы опускаем в этом курсе громоздкий и редко самостоятельно применяемый механизм, позволяющий работать с символьными строками в национальной кодировке. Обычно такую настройку производят поставщики СУБД.
3.1.1.2. Битовые строки
Битовая строка - это последовательность бит, каждый из которых имеет значение 0 или 1. Длиной битовой строки называется число битов в этой строке (0 или положительное целое число). При определении конкретного типа битовых строк указывается длина (в случае BIT) или максимальная длина строки (в случае BITVARYING).
Над битовыми строками определен ряд операций. Некоторые из них мы рассмотрим.
К операторам, которые работают с операндами - битовыми строками и возвращают битовые строки, относятся следующие:
- Битовая конкатенация (обозначается в виде ), которая возвращает результирующую битовую строку, полученную путем конкатенации строк-операндов в том порядке, в котором они заданы.
- Функция извлечения подстроки из битовой строки. Синтаксис и семантика этой функции идентичны синтаксису и семантике функции SUBSTRING для символьных строк за исключением того, что первый аргумент и возвращаемое значение являются битовыми строками.
- Выражение длины возвращает длину заданной битовой строки в октетах или битах в зависимости от выбранной функции.
- Выражение позиции определяет первую позицию в битовой строке S, с которой в нее входит строка S1. Если строка S1 не входит в строку S, возвращается значение нуль.
Численные типы SQL/92 не отличаются от аналогичных типов SQL/89 за исключением того, что они более точно и подробно определяются в новом стандарте.
3.1.1.3. Типы дата-время и интервальные типы
Начнем с того, по какой причине в языке SQL появились эти типы данных (все вместе их можно было бы назвать темпоральными типами, т.е. типами, связанными со временем). В принципе, таблицы реляционных баз данных являются плоскими; каждая строка таблицы соответствует текущему состоянию моделируемого объекта предметной области. Однако на практике часто возникает потребность хранить в одной таблице информацию о состоянии объектов в разные моменты времени. До появления в языке темпоральных типов это можно было реализовать только одним способом: ввести в таблицу дополнительный столбец, в котором хранились бы временные метки создания соответствующих строк. Но если нет унифицированного представления таких временных меток, то вся логика их обработки уходит в прикладную программу, и другая прикладная программа не сможет работать с той же базой данных, если эта логика не будет в нее перенесена. Так что идея очень проста: унифицировать и сделать понятным системе смысл временных меток и временных интервалов.
Однако механизм, предлагаемый в стандарте SQL/92, очень громоздок по причине большого количества несущественных по смыслу деталей: с какой точностью сохранять временные метки и интервалы, как задать временную зону, в которой они возникают и т.д. Поэтому мы рассмотрим особенности этих типов очень коротко (соответствующие спецификации составляют значительную часть громадного документа).
Элемент типа дата-время составляется из смежного поднабора полей:
YEAR | Год от Рождества Христова |
MONTH | Месяц в пределах года |
DAY | День в пределах месяца |
HOUR | Час в пределах дня |
MINUTE | Минута в пределах часа |
SECOND | Секунда и, возможно, доли секунды в пределах минуты |
Реальный смежный поднабор полей, составляющих элемент типа дата-время определяется квалификатором дата-времени, называемым точностью элемента. Имеется подразумеваемая упорядоченность полей YEAR, MONTH, DAY, HOUR, MINUTE, SECOND. Значения каждого поля ограничиваются естественными правилами, регулирующими законные даты и время.
Элемент типа INTERVAL составляется из смежного поднабора полей:
или
DAY | Дни |
HOUR | Часы |
MINUTE | Минуты |
SECOND | Секунды и, возможно, доли секунд |
Реальный смежный поднабор полей, составляющих элемент типа интервал, определяется квалификатором дата-времени, называемым точностью элемента.
Внутри элемента типа интервал первое поле не ограничивается. Последующие поля ограничиваются следующим образом:
MONTH | Месяцы в пределах года (0-11) |
DAY | Не ограничивается |
HOUR | Часы в пределах дня (0-23) |
MINUTE | Минуты в пределах часа (0-59) |
SECOND | Секунды в пределах минуты (0-59.999...) |
Над значениями темпоральных типов могут выполняться арифметические операции, смысл которых определяется следующей таблицей:
Тип первого операнда | Оператор | Тип второго операнда | Тип результата |
Datetime | - | Datetime | Interval |
Datetime | + или - | Interval | Datetime |
Interval | + | Datetime | Datetime |
Interval | + или - | Interval | Interval |
Interval | * или / | Numeric | Interval |
Numeric | * | Interval | Interval |
Арифметические операции, включающие даты и времена, подчиняются обычным правилам, связанным с датами и временами, и производят допустимые результирующие даты и времена.
3.1.2. Домены, столбцы, таблицы, ограничения целостности
Домен является определенным и именованным в схеме множеством допустимых значений данного типа. Назначение домена состоит в том, чтобы ограничить множество значений связанного с ним типа, которые могут храниться в базе данных.
При определении домена указываются базовый тип данных и так называемое ограничение домена, которое ограничивает множество значений домена. Кроме того, специфицируется раздел умолчания с указанием значения домена, которое должно в дальнейшем использоваться как значение по умолчанию для любого столбца, определенного на этом домене, если только при определении столбца значение по умолчанию не задано явно.
Столбец - это именованное мультимножество значений, которое может изменяться во времени. Все значения одного столбца относятся к одному типу данных или домену и являются значениями одной таблицы. Значение столбца - минимальный элемент данных, который может быть выбран из таблицы, и минимальный элемент данных, который может модифицироваться.
Для каждого столбца при его определении задается специальная характеристика, ограничивающая или допускающая появление в нем неопределенных значений. Попытка поместить неопределенное значение в столбец, для которого они запрещены, воспринимается как ошибка. Пусть C - имя столбца. В столбце С не могут быть разрешены неопределенные значения в любом из следующих трех случаев: (1) для содержащей его таблицы определено хотя бы одно неоткладываемое ограничение целостности с условием поиска, включающем С ISNOTNULL; (2) столбец определен на домене с неоткладываемым ограничением, содержащем фразу VALUESNOTNULL; (3) для столбца определено неоткладываемое ограничение уникальности, специфицирующее столбец как первичный ключ. Во всех остальных случаях в столбце могут быть разрешены неопределенные значения.
Таблица - это мультимножество строк. Строка - непустая последовательность значений. Каждая строка таблицы имеет одну и ту же мощность и содержит значения каждого столбца таблицы. i-тое значение каждой строки таблицы является значением ее i-того столбца. Строка является наименьшей единицей данных, которую можно вставить в таблицу и удалить из нее.
Степень таблицы - это число ее столбцов. В любой момент времени степень таблицы совпадает с мощностью любой из входящих в нее строк, а мощность таблицы совпадает с мощностью любого из ее столбцов. Пустой таблицей называется таблица с мощностью 0.
Таблица может быть базовой, представляемой или порождаемой. Базовая таблица может быть постоянно хранимой, глобальной временной, создаваемой локальной временной или объявляемой локальной временной. Постоянная базовая таблица - это именованная таблица, в определении которой не указано TEMPORARY.
Порождаемая таблица - таблица, прямо или косвенно порождаемая из одной или более других таблиц путем вычисления выражения запроса. Значения порождаемой таблицы порождаются из значений определяющих таблиц при вычислении выражения запроса.
Представляемая таблица - это именованная порождаемая таблица, определенная с помощью конструкции определения представления. Иногда представляемые таблицы называют просто представлениями.
Все базовые таблицы являются обновляемыми. Порожденные таблицы - либо обновляемые, либо только читаемые.
Глобальная временная таблица - именованная таблица, в определении которой указано GLOBALTEMPORARY. Создаваемая локальная временная таблица - именованная таблица, в определении которой указано LOCALTEMPORARY. Глобальные и создаваемые временные таблицы реально материализуются (т.е. начинают занимать память) только при обращении к ним в SQL-сессии, причем для локальных временных таблиц создаются отдельные экземпляры для каждого модуля, участвующего в сессии.
Объявляемая локальная временная таблица - именованная локальная таблица, которая реально материализуется при первом выполнении любой процедуры в модуле, содержащем объявление временной таблицы, и доступна только процедурам этого модуля. Образ такой таблицы во внешней памяти не сохраняется после конца SQL-сессии, в которой эта таблица была материализована.
Ограничения целостности, обычно называемые просто ограничениями, определяют допустимые состояния базы данных, ограничивая возможные значения базовых таблиц. Ограничение - это либо ограничение таблицы, либо ограничение домена, либо утверждение целостности. Для каждого ограничения известно его имя, откладываемое ли оно, является ли начальный режим ограничения откладываемым или же немедленным.
Выражение запроса или спецификация запроса является возможно недетерминированным, если реализация может в два разных момента времени при одном и том же состоянии базы данных произвести результаты, различающиеся не только порядком строк. Никакое ограничение целостности не должно использовать недетерминированные выражения или спецификации запроса.
Каждое ограничение может быть либо откладываемым, либо неоткладываемым. Внутри транзакции для каждого ограничения существует режим. Если ограничение неоткладываемое, то его режим всегда немедленный, в противном случае режим либо немедленный, либо отложенный. Для каждого ограничения имеется начальный режим, который указывает, каким должен быть режим при начале транзакции и немедленно сразу после определения ограничения. Если ограничение является откладываемым, то его режим может быть изменен (с немедленного на отложенный или наоборот) путем выполнения оператора установки режима ограничения.
Проверка ограничения производится в зависимости от его режима в текущей транзакции. Если режим немедленный, то ограничение проверяется в конце каждого оператора. Если режим отложенный, то ограничение проверяется либо при изменении режима на немедленный, либо в конце текущей транзакции.
Когда ограничение проверяется не в конце транзакции и не удовлетворяется, то возбуждается исключительная ситуация, и SQL-оператор, вызвавший проверку ограничения, не производит никакого действия, кроме занесения соответствующей информации в область диагностики. При завершении транзакции (выполнении оператора COMMIT) фактически проверяются все ограничения; если какое-либо ограничение не удовлетворяется, транзакция завершается откатом (неявным выполнением оператора ROLLBACK).
Мы опустим описание табличных ограничений, поскольку они мало отличаются от того, что было в SQL/89, а на незначительных отличиях мы остановимся при рассмотрении средств определения схемы базы данных.
Ограничение домена применяется ко всем столбцам, определенным на данном домене, и ко всем значениям, преобразуемым к этому домену. Ограничение домена содержит условие поиска и удовлетворяется в том и только в том случае, когда для любой таблицы T, которая содержит столбец C, основанный на этом домене, условие поиска с заменой каждого вхождения VALUE на C не является ложным для каждой строки T. Ограничение домена удовлетворяется результатом операции преобразования в том и только в том случае, когда условие поиска с заменой каждого вхождения VALUE на этот результат не является ложным.
Ограничение целостности - это именованное ограничение, которое может относиться к содержимому отдельных строк таблицы, ко всему содержимому таблицы или к состоянию, которым должен обладать набор таблиц. Ограничение целостности содержит условие поиска и удовлетворяется в том и только в том случае, когда это условие поиска не является ложным.
3.1.3. Привилегии
Привилегия авторизует данную категорию действий по отношению к указанным базовой таблице, представлению, столбцу, домену и т.д. на основе специфицированного идентификатора авторизации. Отображение идентификаторов авторизации на пользователей операционной системы является зависимым от реализации. Могут быть специфицированы следующие действия:
- INSERT
- INSERT (<columnnamelist>)
- UPDATE
- UPDATE (<columnnamelist>)
- DELETE
- SELECT
- REFERENCES
- REFERENCES (<columnnamelist>)
- USAGE
Идентификатор авторизации определяется для каждого определения схемы и модуля, а также для каждой SQL-сессии. Схема, которая связана с данным идентификатором привилегий может содержать дескрипторы привилегий, описывающие привилегии, которые переданы другим идентификаторам авторизации (получателям привилегий). Переданные привилегии применяются к объектам, определенным в текущей схеме. Раздел WITHGRANTOPTION оператора GRANT указывает, что получатель привилегии может передать ее дальше.
Текущий идентификатор авторизации определяет привилегии для выполнения каждого оператора SQL. При прямом использовании SQL идентификатор авторизации SQL-сессии всегда является текущим идентификатором авторизации.
Описатель привилегии с действием INSERT, UPDATE, DELETE, SELECT и REFERENCES называется описателем привилегий уровня таблицы и идентифицирует существование привилегии к таблице, идентифицируемой описателем привилегии.
Описатель привилегии с действием SELECT (<columnnamelist>), INSERT (<columnnamelist>), UPDATE (<columnnamelist>), и REFERENCES (<columnnamelist>) называется описателем привилегии уровня столбца и идентифицирует существование привилегии к столбцу таблицы, идентифицируемой описателем привилегии.
3.1.4. Транзакции, подключения к базе данных, сессии
SQL-агентом называется зависимый от реализации объект, вызывающий выполнение операторов SQL. Под SQL-транзакцией (иногда называемой просто транзакцией) понимается последовательность выполнения операторов SQL, являющаяся атомарной по отношению к восстановлению. Эти операции выполняются одной или более единицами компиляции и модулями или путем прямого вызова SQL. От реализации зависит, могут ли в одной транзакции выполняться динамические и/или статические операторы выборки и манипулирования данными и динамические и/или статические операторы определения и манипулирования схемой. Если такие сочетания допускаются, то поведение открытого курсора, подготовленного динамического оператора, отложенного ограничения определяются в реализации.
Каждый модуль или прямой вызов SQL, инициирующие выполнение оператора, ассоциируются с транзакцией. SQL-транзакция начинается при выполнении процедуры из некоторого модуля или прямого вызова оператора SQL вне активной транзакции. Транзакция завершается при выполнении операторов COMMIT или ROLLBACK. Если SQL-транзакция завершается успешным выполнением оператора COMMIT, то все изменения, произведенные ею над данными и/или схемой становятся постоянно хранимыми и доступными всем параллельно выполняющимся или образуемым впоследствии транзакциям. Если транзакция завершается оператором ROLLBACK или если выполнение оператора COMMIT оказывается неуспешным, то все изменения, произведенные транзакцией над данными и/или схемой, ликвидируются. Если выполнение оператора COMMIT было начато, но при этом возникли определенные исключительные условия, то неизвестно, зафиксированы или уничтожены результаты этой транзакции.
У каждой SQL-транзакции имеется режим доступа - "только чтение" или "чтение и запись". Режим доступа может быть явно установлен оператором SETTRANSACTION; по умолчанию он устанавливается в "чтение-запись". Термин "только чтение" применяется только к постоянно хранимым базовым и представляемым таблицам.
SQL-транзакции приписывается ограничение размера области диагностики - положительное целое число, определяющее максимальное число условий, которые могут быть помещены в область диагностики при выполнении оператора SQL в этой транзакции.
Транзакции, инициированные разными SQL-агентами, которые обращаются к одним и тем же данным и/или схемам, являются конкурирующими (concurrent; по-русски обычно такие транзакции называют "параллельно" выполняющимися).
Каждой SQL-транзакции приписывается некоторый уровень изоляции: READUNCOMMITTED, READCOMMITTED, REPEATABLEREAD или SERIALIZABLE. Уровень изоляции транзакции определяет степень, в которой на операции этой транзакции влияют операции параллельно выполняющихся транзакций и в которой операции данной транзакции влияют на операции других транзакций. Уровень изоляции может быть явно установлен оператором SETTRANSACTION. По умолчанию устанавливается уровень изоляции SERIALIZABLE. При выполнении конкурирующих транзакций на этом уровне изоляции гарантируется их сериализуемость. Сериализованное выполнение - это такое выполнение операций конкурирующих транзакций, которое производит тот же самый окончательный эффект, что и некоторое последовательное выполнение этих транзакций (т.е. такое выполнение, при котором каждая транзакция полностью завершается до начала следующей). Уровень изоляции определяет вид явления, которое может произойти при параллельном выполнении транзакций. Возможны следующие виды явлений:
- P1 ("Dirtyread" - "Грязное чтение"): Транзакция T1 модифицирует строку. Затем транзакция T2 читает эту строку до того, как T1 выполняет COMMIT. Если после этого T1 выполнит ROLLBACK, то окажется, что T2 прочитала строку, которая никогда не была зафиксирована; можно считать, что эта строка никогда не существовала.
- P2 ("Non-repeatableread" - "Неповторяющееся чтение"): Транзакция T1 читает строку. Затем транзакция T2 модифицирует или удаляет эту строку и выполняет COMMIT. Если после этого T1 попытается повторно прочитать эту строку, то либо получит ее измененное состояние, либо обнаружит, что строка удалена.
- P3 ("Phantom" - "Фантом"): Транзакция T1 читает набор строк N, которые удовлетворяют некоторому условию поиска. Затем транзакция T2 выполняет операторы SQL, которые генерируют одну или более строк, удовлетворяющих условию поиска, использованному T1. Если после этого транзакция T1 повторит чтение с тем же самым условием поиска, она получит другой набор строк.
Все четыре уровня изоляции гарантируют, что каждая SQL-транзакция либо выполнится полностью, либо не выполнится совсем и что ни одно изменение не будет потеряно. Уровни изоляции различаются по отношению к явлениям P1, P2 и P3. В приводимой ниже таблице показано, какие явления возможны, а какие невозможны на данном уровне изоляции.
Уровень изоляции | P1 | P2 | P3 |
READUNCOMMITTED | Возможно | Возможно | Возможно |
READCOMMITTED | Невозможно | Возможно | Возможно |
REPEATABLEREAD | Возможно | Невозможно | Возможно |
SERIALIZABLE | Невозможно | Невозможно | Невозможно |
Замечание: отсутствие рассмотренных выше явлений при выполнении транзакций на уровне изоляции SERIALIZABLE является следствием требования, что такие транзакции являются сериализуемыми.
Изменения данных и/или схем, произведенные транзакцией, не завершившей выполнение оператором COMMIT, могут быть восприняты этой транзакцией в той же самой SQL-сессии. Кроме того, изменения будут видны другим транзакциям и этой транзакции в других сессиях на уровне изоляции READUNCOMMITTED, но не будут видны другим транзакциям на уровне изоляции READCOMMITTED, REPEATABLEREAD, или SERIALIZABLE.
Если в реализации обнаруживается невозможность гарантировать сериализуемость двух или более параллельно выполняемых транзакций, то неявно может быть инициировано выполнение оператора ROLLBACK. Неявное выполнение ROLLBACK может быть также инициировано реализацией при распознавании невосстановимых ошибок. В этих случаях генерируется соответствующее исключительное условие.
С учетом того, что выполняемые внутри транзакции операторы изменения данных или схемы не оказывают влияния, если нарушают ограничения целостности, при сериализуемом выполнении транзакции все чтения являются повторяемыми за исключением следующих случаев:
- изменения данных или схем произведены явно самой транзакцией;
- при повторном чтении процедуре передаются другие значения параметров;
- используются изменяемые во времени переменные, такие как CURRENT_DATEandCURRENT_USER.
SQL-подключение - это ассоциация между SQL-клиентом и SQL-сервером. Подключение устанавливается и именуется с помощью оператора CONNECT, в котором сервер идентифицируется именем. В реализации определяется, каким образом используется имя сервера, чтобы установить его местоположение и коммуникационный протокол, требуемый для доступа к серверу и создания SQL-сессий.
SQL-соединение активно, если какой-либо оператор SQL, инициировавший или востребоваший SQL-транзакцию, был выполнен на соответствующем сервере в течение текущей транзакции. Соединение может быть текущим или потенциальным. Если соединение, установленное самым последним по времени выполнения явным или неявным оператором CONNECT (или SETCONNECTION), не разорвано, то это соединение является текущим; в противном случае оно не текущее. Существующее соединение, не являющееся текущим, называется потенциальным.
Реализация может обнаружить потерю текущего соединения при выполнении любого оператора SQL. Когда это происходит, генерируется исключительное условие, смысл которого состоит в том, что результаты действий, выполненных сервером в связи с этим оператором, неизвестны SQL-агенту.
Аналогично, реализация может обнаружить потерю текущего соединения при выполнении оператора COMMIT. Исключительное условие, возбуждаемое в этом случае, обозначает, что реализация не может узнать, была ли соответствующая транзакция успешно зафиксирована, откачена или продолжает быть активной.
Пользователь может инициировать установление соединения между SQL-клиентом, связанным с SQL-агентом, и конкретным SQL-сервером путем выполнения оператора CONNECT. Иначе установление соединения между клиентом и сервером инициируется, когда вызывается процедура и отсутствует текущее соединение. Возможно определяемое в реализации соединение по умолчанию с некоторым выделенным SQL-сервером.
SQL-соединение уничтожается либо при выполнении оператора DISCONNECT, либо после последнего вызова процедуры в последнем активном модуле, либо после последнего выполнения прямого вызова оператора SQL. Механизм и правила, по которым среда SQL определяет, что вызов процедуры или прямой вызов оператора SQL являются последними, определяются в реализации.
Реализация должна поддерживать по крайней мере одно SQL-соединение и может требовать, чтобы SQL-сервер идентифицировался во время связывания, выбираемое реализацией. Если реализация допускает наличие более чем одного соединения, то SQL-агент может соединяться с более чем одним SQL-сервером и выбирать SQL-сервер путем выполнения оператора SETCONNECTION.
SQL-сессия накрывает выполнение последовательности операторов SQL, вызываемых одним пользователем через единственного SQL-агента или с помощью механизма прямого вызова. Сессия ассоциируется с соединением. SQL-сессия, ассоциированная с соединением по умолчанию, называется сессией по умолчанию. Сессия может быть текущей или потенциальной. Текущая сессия - это сессия, ассоциированная с текущим соединением (соответствующим образом определяется потенциальная сессия).
SQL-сессии соответствует модуль сессии, который отличается от любого другого модуля, одновременно существующего в среде SQL. Модуль сессии содержит глобальные подготовленные операторы SQL, относящиеся к этой сессии.
3.1.5. Уровни языка
В стандарте SQL/92 специфицированы три уровня соответствия стандарту. Вводный уровень включает операторы определения схемы, язык манипулирования данными, ссылочную целостность, проверочные ограничения и раздел умолчания из SQL/89. Кроме того, вводный уровень содержит спецификации языка модулей и встроенного SQL для использования в семи различных языках программирования, а также определения подмножества SQL, предназначенного для прямого использования. Фактически, вводный уровень SQL/92 представляет собой более аккуратный вариант SQL/89.
Промежуточный уровень SQL/92 включает важные новые возможности, такие как операторы изменения схемы, динамический SQL и уровни изоляции SQL-транзакций. В промежуточном SQL содержатся также спецификации каскадного удаления при выполнении ссылочных действий, соединения объединением, операций со строками символов, операций пересечения и вычитания таблиц, простых доменов, выражения с переключателем, явного преобразования типов, средств управления диагностикой, интервальных типов данных и упрощенного варианта типа дата-время, строк переменной длины. К промежуточному уровню относится и требование наличия средства, контролирующего соответствие вводимых операторов стандарту.
Полный SQL включает возможности отложенной проверки ограничений целостности и определения именованных ограничений. Дополнительные свойства: возможность определять типы дата-время на уровне пользователя, модификации и удаления строк с возможностью ссылаться в условии на ту же таблицу, каскадные модификации при ссылочных действиях, подзапросы в проверочных ограничениях, тип данных битовых строк, временные таблицы, утверждения целостности.
На этом мы заканчиваем обсуждать понятия языка SQL/92 и переходим к более глубокому изучению его наиболее важных свойств (хотя, конечно, не всех и не так подробно, как это делается в самом стандарте).
3.2. Скалярные выражения
Скалярное выражение - это выражение, вырабатывающее результат некоторого типа, специфицированного в стандарте. Скалярные выражения являются основой языка SQL/92, поскольку, хотя это реляционный язык, все условия, элементы списков выборки и т.д. базируются именно на скалярных выражениях. Заметим, что в SQL/89 не требовалось вводить такое отдельное понятие, поскольку единственным допустимым скалярным выражением было арифметическое (т.е. вырабатывающее число). В связи с расширением ассортимента типов и операций над их значениями, в SQL/92 появилось три разных вида скалярных выражения - численные, над строками и над временем и датами (и интервалами). Мы не будем слишком глубоко вникать в тонкости, но тем не менее, приведем некоторые базовые спецификации и пояснения.
Прежде, чем перейти к конкретным видам скалярных выражений, рассмотрим некоторые более общие языковые конструкции, на которых эти выражения базируются. (Мы опускаем обсуждение типов данных, считая, что материала, приведенного в предыдущем разделе, достаточно для понимания.)
Спецификация значения и спецификация цели
Эти конструкции служат для спецификации значений, не выбираемых из таблиц базы данных, а заданных пользователями. Общий синтаксис выглядит следующим образом:
<value specification> ::=
<literal>
|<general value specification>
<unsigned value specification> ::=
<unsigned literal>
|<general value specification>
<general value specification> ::=
<parameter specification>
|<dynamic parameter specification>
|<variable specification>
|USER
|CURRENT_USER
|SESSION_USER
|SYSTEM_USER VALUE
<simple value specification> ::=
<parameter name>
|<embedded variable name
|<literal>
<target specification> ::=
<parameter specification>
|<variable specification>
<simple target specification> ::=
<parameter name>
<embedded variable name>
<parameter specification> ::= &
lt;parameter name> [ <indicator parameter> ]
<indicator parameter> ::=
[ INDICATOR ] <parameter name>
<dynamic parameter specification> ::= <question mark>
<variable specification> ::=
<embedded variable name> [ <indicator variable> ]
<indicator variable> ::=
[ INDICATOR ] <embedded variable name>
Пояснения: Спецификация параметра идентифицирует параметр или параметр и индикаторный параметр в модуле (вернее, в процедуре модуля). Спецификация динамического параметра идентифицирует параметр динамически подготовленного оператора. Спецификация переменной идентифицирует переменную включающего языка или такую переменную и индикаторную переменную. Спецификация цели определяет параметр или переменную, которым может быть присвоено значение. Отрицательное значение индикаторных параметра или переменной в спецификации параметра или переменной соответственно означает, что параметр или переменная содержат неопределенное значение. Значение CURRENT_USER есть значение текущего идентификатора авторизации; SESSION_USER - значение идентификатора авторизации SQL-сессии; SYSTEM_USER - определяемая в реализации строка, представляющая пользователя операционной системы, который выполнил оператор SQL, в результате которого было вычислено значение SYSTEM_USER.
Спецификация явного преобразования типа или домена
В SQL/89 существуют только неявные преобразования типов (например, FLOAT к DOUBLE). Конечно, это не всегда удобно, недостаточно гибко и иногда чревато ошибками. В SQL/92 существует специальная конструкция CAST, с помощью которой можно явно преобразовывать типы в пределах допускаемых преобразований. Синтаксис конструкции следующий:
<cast specification> ::=
CAST <left paren> <cast operand> AS
<cast target> <right paren>
<cast operand> ::=
<value expression>
|NULL
<cast target> ::=
<domain name>
|<data type>
Пояснения. Примем следующие обозначения типов данных:
EN | - ExactNumeric; |
AN | - ApproximateNumeric; |
C | - Character (Fixed- orVariable-length); |
FC | - Fixed-lengthCharacter; |
VC | - Variable-lengthCharacter; |
B | - BitString (Fixed- orVariable-length); |
FB | - Fixed-lengthBitString; |
VB | - Variable-lengthBitString; |
D | - Date; |
T | - Time; |
TS | - Timestamp; |
YM | - Year-MonthInterval; |
DT | - Day-TimeInterval. |
Пусть TD - тип данных, к которому производится преобразование, а SD - тип данных операнда. Тогда допустимы следующие комбинации ("да" означает безусловную допустимость, "нет"- безусловную недопустимость и "?" - допустимость с оговорками):
SD | TD | | | | | | | | | | |
| EN | AN | VC | FC | VB | FB | D | T | TS | YM | DT |
EN | Да | Да | Да | Да | Нет | Нет | Нет | Нет | Нет | ? | ? |
AN | Да | Да | Да | Да | Нет | Нет | Нет | Нет | Нет | Нет | Нет |
C | Да | Да | ? | ? | Да | Да | Да | Да | Да | Да | Да |
B | Нет | Нет | Да | Да | Да | Да | Нет | Нет | Нет | Нет | Нет |
D | Нет | Нет | Да | Да | Нет | Нет | Да | Нет | Да | Нет | Нет |
T | Нет | Нет | Да | Да | Нет | Нет | Нет | Да | Да | Нет | Нет |
TS | Нет | Нет | Да | Да | Нет | Нет | Да | Да | Да | Нет | Нет |
YM | ? | Нет | Да | Да | Нет | Нет | Нет | Нет | Нет | Да | Нет |
DT | ? | Нет | Да | Да | Нет | Нет | Нет | Нет | Нет | Нет | Да |
Оговорки состоят в следующем:
- Если TD - интервал, и SD - тип точных чисел, то TD должен содержать единственное поле даты-времени;
- Если TD - тип точных чисел, и SD - интервал, то SD должен содержать единственное поле даты-времени;
- Если SD - тип символьных строк, и TD - тип символьных строк постоянной или переменной длины, то репертуар символов SD и TD должен быть одним и тем же.
Результатом применения оператора CAST к неопределенному значению является неопределенное значение. Для значений, отличных от неопределенных, в стандарте приводятся подробные правила выполнения преобразований, которые интуитивно ясны.
Выражение, вырабатывающее значение
Это общая форма скалярного выражения, включающая все возможные типы результирующих значений. Синтаксис следующий:
<value expression> ::=
<numeric value expression>
|<string value expression>
|<datetime value expression>
|<interval value expression>
<value expression primary> ::=
<unsigned value specification>
|<column reference>
|<set function specification>
|<scalar subquery>
|<case expression>
|<left paren>
|<value expression>
|<right paren>
|<cast specification>
Пояснения: При вычислении выражения V для строки таблицы каждая ссылка на столбец этой таблицы, непосредственно содержащаяся в V, рассматривается как ссылка на значение данного столбца в данной строке. Если первичное выражение есть скалярный подзапрос (подзапрос, результатом которого является таблица, состоящая из одной строки и одного столбца), и результат подзапроса пуст, то результатом первичного выражения является неопределенное значение.
3.2.1. Численные выражения
Численное выражение - это выражение, значение которого относится к числовому типу данных. По сути дела, численные выражения SQL/92 являются не очень большим расширением арифметических выражений SQL/89. Вот формальный синтаксис численного выражения:
<numeric value expression> ::=
<term>
|<numeric value expression> <plus sign> <term>
|<numeric value expression> <minus sign> <term>
<term> ::=
<factor>
|<term> <asterisk> <factor>
|<term> <solidus> <factor>
<factor> ::=
[ <sign> ] <numeric primary>
<numeric primary> ::=
<value expression primary>
|<numeric value function>
Пояснения: Следует обратить внимание на то, что в отличие от SQL/89 в численных выражениях SQL/92 первичная составляющая является либо первичным численным выражением (см. выше), либо вызовом функции с численным значением. Из этого, в частности, следует, что в численные выражения могут входить выражения с переключателем и операторы преобразования типов.
Функция с численным значением определяется следующими синтаксическими правилами:
<numeric value function> ::=
<position expression>
|<extract expression>
|<length expression>
<position expression> ::=
POSITION <left paren> <character value expression>
IN <character value expression> <right paren>
<length expression> ::=
<char length expression>
|<octet length expression>
|<bit length expression>
<char length expression> ::=
{ CHAR_LENGTH | CHARACTER_LENGTH }
<left paren> <string value expression> <right paren>
<octet length expression> ::=
OCTET_LENGTH <left paren>
<string value expression> <right paren>
<bit length expression> ::=
BIT_LENGTH <left paren>
<string value expression> <right paren>
<extract expression> ::=
EXTRACT <left paren> <extract field>
FROM <extract source> <right paren>
<extract field> ::=
<datetime field>
|<time zone field>
<time zone field> ::= T
IMEZONE_HOUR
|TIMEZONE_MINUTE
<extract source> ::=
<datetime value expression>
|<interval value expression>
Пояснения: Что касается выражений позиции и длины по отношению к символьным и битовым строкам, мы достаточно подробно обсуждали их при рассмотрении соответствующих типов данных; здесь приводится только уточненный синтаксис. Выражение извлечения поля из значений дата-время или интервал позволяет получить в виде точного числа с масштабом 0 значение любого поля (года, месяца, дня и т.д.). Какой конкретный тип точных чисел будет выбран - определяется в реализации.
3.2.2. Выражения над строками
Выражения над строками - это выражения, значениями которых являются символьные или битовые строки. Соответствующие конструкции определяются следующим синтаксисом:
<string value expression> ::=
<character value expression> <bit value expression>
<character value expression> ::=
<concatenation> <character factor>
<concatenation> ::=
<character value expression>
|<concatenation operator> <character factor>
<character factor> ::=
<character primary> [ <collate clause> ]
<character primary> ::=
<value expression primary> <string value function>
<bit value expression> ::=
<bit concatenation> <bit factor>
<bit concatenation> ::=
<bit value expression>
<concatenation operator> <bit factor>
<bit factor> ::= <bit primary>
<bit primary> ::=
<value expression primary>
<string value function>
Если не вдаваться в тонкости, смысл выражений над строками понятен из описания синтаксиса: единственная применимая для построения выражений операция - конкатенация, производящая "склейку" строк-операндов. Более важно то, что первичной составляющей выражения над строками может быть как первичное выражение, вычисляющее значение (см. выше), так и вызов функций, возвращающих строчные значения. Репертуар и синтаксис вызова таких функций определяются следующим синтаксисом:
<string value function> ::=
<character value function>
<bit value function>
<character value function> ::=
<character substring function>
<fold>
<form-of-use conversion>
<character translation>
<trim function>
<character substring function> ::=
SUBSTRING <left paren>
<character value expression> FROM
<start position> [ FOR <string length> ]
<right paren>
<fold> ::=
{ UPPER LOWER } <left paren>
<character value expression> <right paren>
<form-of-use conversion> ::=
CONVERT <left paren>
<character value expression> USING
<form-of-use conversion name> <right paren>
<character translation> ::=
TRANSLATE <left paren>
<character value expression> USING
<translation name> <right paren>
<trim function> ::=
TRIM <left paren> <trim operands> <right paren>
<trim operands> ::=
[ [ <trim specification> ] [ <trim character> ] FROM ]
<trim source>
<trim source> ::= <character value expression>
<trim specification> ::= LEADING TRAILING BOTH
<trim character> ::= <character value expression>
<bit value function> ::= <bit substring function>
<bit substring function> ::=
SUBSTRING <left paren> <bit value expression> FROM
<start position> [ FOR <string length> ]
<right paren>
<start position> ::= <numeric value expression>
<string length> ::= <numeric value expression>
Пояснения: Основные полезные функции - выделение подстроки (SUBSTRING) и замена малых букв на заглавные и наоборот (UPPER и LOWER) - мы упоминали при рассмотрении строчных типов. Как видно из описания синтаксиса функций, возвращающих строчные значения, для символьных строк имеются еще три функции: CONVERT, TRANSLATE и TRIM. По смыслу они все очень просты. Функция CONVERT меняет кодировку символов в заданной строке, причем репертуар символов не меняется. Способ задания правил перекодировки определяется в реализации. Функция TRANSLATE, наоборот, в соответствии с правилами трансляции "переводит" текстовую строку на другой язык (используя набор символов целевого алфавита). Кодировка не меняется. Функция TRIM "отсекает" последовательности указанного символа в начале, в конце или в конце и начале заданной строки.
3.2.3. Выражения над временем и датами
К выражениям над временем и датой мы относим выражения, вырабатывающие значения типа дата-время и интервал. Выражения дата-время определяются следующими синтаксическими правилами:
>
<datetime value expression> ::=
<datetime term>
|<interval value expression> <plus sign> <datetime term>
|<datetime value expression> <plus sign> <interval term> <datetime value expression> <minus sign> <interval term>
<datetime term> ::= <datetime factor>
<datetime factor> ::= <datetime primary> [ <time zone> ]
<datetime primary> ::=
<value expression primary>
|<datetime value function>
<time zone> ::= AT <time zone specifier>
<time zone specifier> ::=
LOCAL TIME ZONE <interval value expression>
Пояснения: Как видно из описания синтаксиса, сами выражения строятся очень просто - на основе обычных арифметических операций. Снова более интересны первичные составляющие- вызовы функций, возвращающих значение дата-время. Эти вызовы определяются следующим синтаксисом:
<datetime value function> ::=
<current date value function>
|<current time value function>
|<current timestamp value function>
<current date value function> ::= CURRENT_DATE
<current time value function> ::=
CURRENT_TIME
[ <left paren> <time precision> <right paren> ]
<current timestamp value function> ::=
CURRENT_TIMESTAMP
[ <left paren> <timestamp precision> <right paren> ]
Видимо, приведенные синтаксические правила не нуждаются в комментариях: можно получить текущую дату, а также текущее время с желаемой точностью.
Синтаксис выражений со значениями типа интервал определяется следующими правилами:
<interval value expression> ::=
<interval term>
|<interval value expression 1> <plus sign> <interval term 1>
|<interval value expression 1> <minus sign> <interval term 1>
|<left paren> <datetime value expression> <minus sign>
|<datetime term> <right paren> <interval qualifier>
<interval term> ::= &
lt;interval factor>
|<interval term 2> <asterisk> <factor>
|<interval term 2> <solidus> <factor>
|<term> <asterisk> <interval factor>
<interval factor> ::= [ <sign> ] <interval primary>
<interval primary> ::=
<value expression primary> [ <interval qualifier> ]
<interval value expression 1> ::= <interval value expression>
<interval term 1> ::= <interval term>
<interval term 2> ::= <interval term>
Как видно из приведенных правил, выражения со значениями типа интервал устроены очень просто; почти вся содержательная информация была приведена при обсуждении соответствующего типа данных. Стоит только заметить, что квалификатор интервала указывается для того, чтобы явно специфицировать единицу измерения интервала.
3.2.4. Выражения с переключателем
Выражения с переключателем в некотором смысле ортогональны рассмотренным выше видам выражений, поскольку разные выражения с переключателем могут вырабатывать значения разных типов. Поскольку мы еще вообще не рассматривали этот вид выражений, обсудим их более подробно. Как обычно, начнем с синтаксиса:
<case expression> ::=
<case abbreviation>
|<case specification>
<case abbreviation> ::=
NULLIF <left paren> <value expression>
<comma> <value expression> <right paren>
|COALESCE <left paren> <value expression>
|<comma> <value expression> }... <right paren>
<case specification> ::=
<simple case> <searched case>
<simple case> ::=
CASE <case operand>
imple when clause>...
<else clause> ]
END
<searched case> ::=
CASE
earched when clause>...
<else clause> ]
END
<simple when clause> ::= WHEN <when operand> THEN <result>
<searched when clause> ::= WHEN <search condition> THEN <result>
<else clause> ::= ELSE <result>
<case operand> ::= <value expression>
<when operand> ::= <value expression>
<result> ::= <result expression> NULL
<result expression> ::= <value expression>
Пояснения:
- NULLIF (V1, V2) эквивалентно следующей спецификации выражения с переключателем: CASEWHENV1=V2 THENNULLELSEV1 END.
- COALESCE (V1, V2) эквивалентно следующему: CASEWHENV1 ISNOTNULLTHENV1 ELSEV2 END.
- COALESCE (V1, V2, . . . ,n) для n >= 3 эквивалентно CASEWHENV1 ISNOTNULLTHENV1 ELSECOALESCE (V2, . . . ,n) END.
- Если используется простая спецификация выражения (simplecase), то тип данных операнда переключателя (CO - CaseOperand) должен быть совместим с типов данных операнда каждого варианта (WO - WhenOperand); спецификация эквивалентна спецификации поискового переключателя (searchedcase), в котором каждое условие поиска имеет вид "CO=WO".
- По крайней мере в одном из вариантов должно быть специфицировано результирующее выражение.
- Если отсутствует раздел ELSE, то по умолчанию полагается ELSENULL.
- Тип данных результата определяется как минимальный накрывающий тип для всех возможных результатов. Например, если все результирующие значения имеют тип данных строк переменной длины с максимальными длинами m1, m2, ..., mn, то типом данных результата будет тип данных строк переменной длины с максимальной длиной, равной max (m1, m2, ..., mn)).
Назад |
Содержание |
Вперед