ГЛОБАЛЬНЫЕ АСПЕКТЫ SQL

Глава 23. ГЛОБАЛЬНЫЕ АСПЕКТЫ SQL


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

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

ПЕРЕИМЕНОВАНИЕ ТАБЛИЦ

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

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

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

Adrian может создать синоним с именем Clients для таблицы с именем Diane.Customers с помощью команды CREATE SYNONYM:

      CREATE SYNONYM Clients FOR Diane.Customers;

Теперь Adrian может использовать таблицу с именем Clients в команде точно так же, как её использует Diane.Customers. Синоним Clients это собственность, используемая исключительно Adrian.

ПЕРЕИМЕНОВАНИЕ С ТЕМ ЖЕ САМЫМ ИМЕНЕМ

Префикс (прозвище) пользователя это фактически часть имени любой таблицы. Всякий раз, когда вы не указываете ваше имя пользователя вместе с именем вашей собственной таблицы, SQL сам заполняет для вас это место. Следовательно, два одинаковых имени таблицы, но связанные с различными владельцами, становятся неидентичными и не приводят к какому-нибудь беспорядку (по крайней мере в SQL). Это означает, что два пользователя могут создать две полностью не связанные таблицы с одинаковыми именами, но это также будет означать, что один пользователь может создать представление, основанное на имени другого пользователя, стоящем после имени таблицы. Это иногда делается, когда представление рассматривается как сама таблица - например, если представление просто использует CHECK OPTION как заменитель ограничения CHECK в базовой таблице (смотрите подробности в Главе 22).

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

Например, Adrian может определить Customers как свой синоним для таблицы Diane.Customers:

 CREATE SYNONYM Customers FOR Diane.Customers;

С точки зрения SQL теперь имеются два разных имени одной таблицы: Diane.Customers и Adrian.Customers. Однако каждый из этих пользователей может обращаться к этой таблице просто Customers. SQL, как говорилось выше, сам добавит недостающие имена пользователей.

ОДНО ИМЯ ДЛЯ КАЖДОГО

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

Например, если все пользователи будут вызывать таблицу Заказчиков с именем Customers, вы можете ввести

  CREATE PUBLIC SYNONYM Customers FOR Customers;

Мы пронимаем, что таблица Заказчиков, это ваша собственность, поэтому никакого префикса имени пользователя в этой команде не указывается. В основном общие синонимы создаются владельцами объектов или привилегированными пользователями типа DBA. Пользователям, кроме того, должны ещё быть предоставлены привилегии в таблице Заказчиков, чтобы они могли иметь к ней доступ.

Даже если имя является общим, сама таблица общей не является. Общие синонимы становятся собственными с помощью команды PUBLIC, а не с помощью их создателей.

УДАЛЕНИЕ СИНОНИМОВ

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

Чтобы удалить, например, синоним Clients, когда вместо него уже появился общий синоним Customers, Adrian может ввести

           DROP SYNONYM Clients;

Сама таблица Заказчиков, естественно, становится недействующей.

КАК БАЗА ДАННЫХ РАСПРЕДЕЛЕНА МЕЖДУ ПОЛЬЗОВАТЕЛЯМИ?

Таблицы и другие объекты данных сохраняются в БД и находятся там связанными с определёнными пользователями, которые ими владеют. В некотором смысле вы могли бы сказать, что они сохраняются в "именной области пользователя", хотя это никак не отражает их физического расположения, но зато, как и большинство вещей в SQL, является строгой логической конструкцией.
Однако на самом деле объекты данных сохраняются, в физическом смысле; и количество памяти, которое может использоваться определенным объектом или пользователем в данное время, имеют свой предел.

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

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

Системы, которые используют области БД (в дальнейшем называемых - DBS (Data Base Spaces)), которые дают возможность, с помощью команд SQL, обрабатывать эти области как объекты.

DBS создаются командами CREATE DBSPACE (СОЗДАТЬ DBS), ACQUIRE DBSPACE (ПОЛУЧИТЬ DBS) или CREATE TABLE SPACE (СОЗДАТЬ ТАБЛИЧНУЮ ОБЛАСТЬ), в зависимости от используемой реализации.

Одна DBS может вмещать любое число пользователей, и отдельный пользователь может иметь доступ к многим DBS.

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

Мы можем создать DBS с именем Sampletables следующей командой:

           CREATE DBSPACE Sampletables
              ( pctindex 10,
                pctfree  25);

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

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

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

Если DBS создана, пользователям предоставляются права создавать в неё объекты.

Вы можете, например, предоставить Diane право создать таблицу Sampletables с помощью следующей команды:

     GRANT RESOURCE ON Sampletables TO Diane;

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

При разделении вашей БД на DBSы вы должны иметь в виду типы операций, которые вы будете часто выполнять.

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

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

КОГДА СДЕЛАННЫЕ ИЗМЕНЕНИЯ СТАНОВЯТСЯ ПОСТОЯННЫМИ ?

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

Команда SQL, которая воздействует на содержание или структуру БД - например, команда модификации DML или команда DROP TABLE, - не обязательно будет необратимой. Вы можете определить после окончания её действия, останутся ли изменения, сделанные данной командой или группой команд, постоянными в базе данных, или они будут полностью проигнорированы. С этой целью команды обрабатываются группами, называемыми транзакциями. Транзакция начинается всякий раз, когда вы начинаете сеанс с SQL. Все команды, которые вы введёте, будут частью этой транзакции, пока вы не завершите их вводом команды COMMIT WORK или команды ROLLBACK WORK. COMMIT может сделать все изменения постоянными с помощью транзакции, а ROLLBACK может откатить их обратно или отменить. Новая транзакция начинается после каждой команды COMMIT или ROLLBACK. Этот процесс известен как диалоговая обработка запросов или транзакция.

Вот синтаксис, чтобы оставить все ваши изменения постоянными во время регистрации или во время последнего COMMIT или ROLLBACK:

                     COMMIT WORK;

Синтаксис отмены изменения:

                    ROLLBACK WORK;

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

                 SET AUTOCOMMIT ON;

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

                 SET AUTOCOMMIT OFF;

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

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

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

Например, предположим, вы хотите удалить продавца Motika из базы данных. Прежде чем вы удалите его из таблицы Продавцов, вы сначала должны сделать что-нибудь с его заказами и его заказчиками. (Если используются ограничения внешнего ключа, и ваша система, следуя ANSI, ограничивает изменение родительского ключа, у вас не будет выбора: делать или не делать этого. Это будет сделано обязательно.) Одно из логических решений будет состоять в том, чтобы установить поле snum в его заказах в NULL, вследствие чего ни один продавец не получит комиссионные в этих заказах, пока комиссионные не будут предоставлены заказчикам для Peel. Затем вы можете удалить их из таблицы Продавцов:

             UPDATE Orders
                SET snum = NULL
                WHERE snum = 1004;

             UPDATE Cudomers
                SET snum = 1001
                WHERE snum = 1004;

             DELETE FROM Salespeople
                WHERE snum = 1004;

Если у вас проблема с удалением Motika (возможно, имеется другой внешний ключ, ссылающийся на него, о котором вы не знали и который не учитывали), вы могли бы отменить все изменения, которые вы сделали, до тех пор пока проблема не будет определена и решена. Более того, это должна быть группа команд, чтобы обрабатывать её как одиночную транзакцию. Вы можете предусмотреть это с помощью команды COMMIT и завершить её с помощью команды COMMIT или ROLLBACK.

КАК SQL ОБЩАЕТСЯ СРАЗУ С НЕСКОЛЬКИМИ ПОЛЬЗОВАТЕЛЯМИ?

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

          UPDATE Salespeople
             SET comm = comm * 2
             WHERE sname LIKE 'R%';

и в это же время Diane вводит такой запрос:

           SELECT city, AVG (comm)
              FROM Salespeople
              GROUP BY city;

Может ли усреднённое значение (AVG) Diane отразить изменения, которые вы делаете в таблице? Не важно, будет это сделано или нет, а важно, чтобы были отражены все или ни одно из значений комиссионных (comm), для которых выполнялись изменения. Любой промежуточный результат является случайным или непредсказуемым для заказа, в котором значения были изменены физически. Вывод запроса не должен быть случайным и непредсказуемым.

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

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

Вот примеры:

  • Модификация может быть сделана без учета другой модификации. Например, продавец должен сделать запрос в таблице инвентаризации, чтобы найти десять фрагментов пунктов торговцев акциями и упорядочить шесть из них для заказчика. Прежде чем это изменение было сделано, другой продавец делает запрос к таблице и упорядочивает семь из тех же фрагментов для своего заказчика.
ПРИМЕЧАНИЕ:

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

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

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

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

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

Механизм, используемый SQL для управления параллелизмом операций, называется блокировкой. Блокировки задерживают определенные операции в БД, пока другие операции или транзакции не завершены. Задержанные операции выстраиваются в очередь и выполняются только тогда, когда блокировка снята (некоторые инструменты блокировок дают вам возможность указывать NOWAIT, которая будет отклонять команду, вместо того чтобы поставить её в очередь, позволяя вам делать что-нибудь другое).

Блокировки в многопользовательских системах необходимы. Следовательно, должен быть некий тип схемы блокировки по умолчанию, которая могла бы применяться ко всем командам в базе данных. Такая схема по умолчанию может быть определена для всей БД, или в качестве параметра в команде CREATE DBSPACE или команде ALTER DBSPACE и таким образом использовать их по-разному в различных DBS.

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

ТИПЫ БЛОКИРОВОК

Имеется два базовых типа блокировок: распределяемые блокировки и специальные блокировки.

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

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

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

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

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

ДРУГИЕ СПОСОБЫ БЛОКИРОВКИ ДАННЫХ

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

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

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

РЕЗЮМЕ

Ключевые определения, с которыми вы познакомились в этой главе:

  • Синонимы, или как создавать новые имена для объектов данных.
  • Области базы данных (DBS), или как распределяется доступная память в базе данных.
  • Транзакция, или как сохранять или восстанавливать изменения в базе данных.
  • Управление Параллелизмом, или как SQL предохраняет от конфликта одной команды с другой.

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

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

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

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

РАБОТА СО SQL

1. Создайте область базы данных с именем Myspace, которая
   выделяет 15 процентов своей области для индексов и 40 процентов на расширение строк.

2. Вы получили право SELECT в таблице Заказов продавца Diane.
   Введите команду так, чтобы вы могли ссылаться к этой таблице
   как к "Orders", не используя имя "Diane" в качестве префикса.

3. Если произойдёт сбой питания, что случится со всеми изменениями,
   сделанными во время текущей транзакции?

4. Если вы не можете видеть строку из-за её блокировки, какой это тип блокировки?

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

(См. ответы в Приложении A.)
© Copyright 2006 INX.com.ua Communications. Все права защищены.

Отдел приёма заказов:

Создание сайта - студия дизайна "in-X"

Учебник SQL