Заняться выполнением этой лабораторной работы меня
побудили несколько причин. Во-первых, занимаясь построением серверов и
сетей на основе Линукс'а, я догадывался, что где-то совсем рядом лежит
сказочно богатый континент, пока не нанесенный на мою карту
компьютерного мира. Во-вторых, авторы одной из самых популярных
открытых программ - СУБД MySQL, недавно приняли GNU GPL (General Public
License) как лицензию, по которой распространяется эта программа, и
теперь MySQL является полноценным проектом GNU. Эти юридические
тонкости имеют самое непосредственное отношение к нам, пользователям,
чему я немного ниже приведу пример. И наконец, в-третьих, пытаясь
отыскать хорошие руководства по SQL в сети, я в конце концов обнаружил,
что самые лучшие он-лайновые учебники по этой теме, оказывается,
написаны нашими соотечественниками, на русском языке, и лежат у меня на
диске - в зеркале сервера CITFORUM
Особенно полезны учебный курс "Введение в системы управления базами данных" Пушникова А.Ю., и курс лекций "Основы современных баз данных" Сергея Кузнецова. Недавно к ним добавилось подробное описание СУБД MySQL, сделанное Паутовым Алексеем Валентиновичем на основе оригинальной документации и такое же доскональное.
Итак, пришла пора взяться за учебники, а для
меня еще и достать припасенный для такого случая особый файл. Этот файл
представляет собой телефонный справочник службы 09 нашего города, пару
лет назад попавший в местную ФИДО-сеть. Мне не очень важна его
актуальность, зато очень подходит его размер - свыше 120 тысяч записей.
Очень часто примеры, даваемые в учебниках, являются слишком
игрушечными, чтобы вызывать интерес. Затем, на крошечной БД невозможно
почувствовать скорость и мощь современных программ и компьютеров, или
наоборот, плохо настроенную БД или неправильно составленный запрос.
Кроме этого, ситуация с построеним БД вокруг уже имеющихся данных
вполне жизненна.
Вполне возможно, что у вас файла с такими
данными в пределах досягаемости нет. Ничего страшного - его можно
сделать самому, использовав подручные средства - например, взять
простой текстовый файл, обычные textutils, и интерпретатор языка awk.
Пример, как это можно сделать, приведен
здесь. Конечно, данные в таком файле будут случайными, только внешне похожими на настоящий телефонный справочник.
Для начала надо установить на вашем компьютере
MySQL. Не буду пересказывать главы из документации, имеющиеся в
описании Алексея Паутова. Скажу лишь, что для установленного у меня
дистрибутива Дебьян установка программы свелась к выполнению команды:
...$ dpkg -i mysql-server_номер_версии.deb mysql-client_номер_версии.deb
В дистрибутиве Mandrake, который я также иногда
использую, используется программа-установщик rpm с соответствующими
ключиками, или же какая-то из графических надстроек над rpm. Вполне
возможно, что в вашей Линукс системе эта СУБД установилась сама собой
по умолчанию.
Инсталяция MySQL под Windows, равно как и Apache, PHP и Perl, рассказана Дмитрием Котеровым на том же ЦитФоруме.
Предупреждаю, однако, что все, что написано ниже, проверено только под Линуксом.
Если вам повезло, и команда
...$ mysql
из вашего шелла выдала приглашение наподобие:
Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 28 to server version: 3.22.32-log Type 'help' for help. mysql>
, то в ответ на него наберите \q, оставим на время интерпретатор SQL запросов, и займемся администрированием сервера MySQL.
Прежде всего, надеюсь, вы установили пароль
администратора сервера БД, и пока его не забыли. Теперь нужно завести
пользователей и дать им некоторые права. Все администрирование ведется
через обычные таблицы MySQL, и их правка также осуществляется
стандартными SQL командами. Самая первая таблица, которая определяет
допуск юзера к серверу, так и называется - user. Давайте глянем, кто у
нас там есть и что он может делать:
...$ mysqldump -u root -p --opt mysql user>mysql-users.sql
После выполнения этой команды у нас появился файл
mysql-users.sql Загрузим его в текстовый редактор, чтобы поподробнее
изучить, и, возможно, немного поправить.
# MySQL dump 7.1 # # Host: localhost Database: mysql #-------------------------------------------------------- # Server version 3.22.32-log # # Table structure for table 'user' # DROP TABLE IF EXISTS user; CREATE TABLE user ( Host char(60) DEFAULT '' NOT NULL, User char(16) DEFAULT '' NOT NULL, Password char(16) DEFAULT '' NOT NULL, Select_priv enum('N','Y') DEFAULT 'N' NOT NULL, Insert_priv enum('N','Y') DEFAULT 'N' NOT NULL, Update_priv enum('N','Y') DEFAULT 'N' NOT NULL, Delete_priv enum('N','Y') DEFAULT 'N' NOT NULL, Create_priv enum('N','Y') DEFAULT 'N' NOT NULL, Drop_priv enum('N','Y') DEFAULT 'N' NOT NULL, Reload_priv enum('N','Y') DEFAULT 'N' NOT NULL, Shutdown_priv enum('N','Y') DEFAULT 'N' NOT NULL, Process_priv enum('N','Y') DEFAULT 'N' NOT NULL, File_priv enum('N','Y') DEFAULT 'N' NOT NULL, Grant_priv enum('N','Y') DEFAULT 'N' NOT NULL, References_priv enum('N','Y') DEFAULT 'N' NOT NULL, Index_priv enum('N','Y') DEFAULT 'N' NOT NULL, Alter_priv enum('N','Y') DEFAULT 'N' NOT NULL, PRIMARY KEY (Host,User) ); # # Dumping data for table 'user' # LOCK TABLES user WRITE; INSERT INTO user VALUES ('localhost','root','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'), ('localhost','ophil','','N','N','N','N','Y','N','Y','N','N','Y','N','N','N','N'), ('localhost','proba','','N','N','N','N','N','N','N','N','N','N','N','N','N','N'); UNLOCK TABLES;
Вот уже и показались первые SQL-предложения, хотя мы
пока не начинали программировать или что-либо запрашивать. В
предложении CREATE TABLE перечислены все 14 различных привилегий,
которые могут иметь или быть лишены пользователи. Первые 6 - Select,
Insert, Update, Delete, Create и Drop, касаются права пользователя
работать с записями таблиц и с самими таблицами. Следующие 4 - Reload,
Shutdown, Process и File - касаются сервера в целом. Привилегии Grant,
References, Index и Alter дают право передавать права, а также
изменять, связывать и индексировать таблицы.
Два важных замечания.
Во-первых, права, данные в этой таблице, по
умолчанию распространяются на все БД, имеющиеся на сервере. Поэтому не
давайте в этой таблице никаких привилегий, касающихся таблиц. Более
тонко, с точностью до отдельных полей и адресов хостов, права
пользователей настраиваются в других таблицах, а эта таблица должна
только разрешать вход на сервер.
Во-вторых, привилегии, касающиеся сервера в
целом, настраиваются только в этой таблице, и хотя бы один
пользователь, в данном случае root, должен иметь эти привилегии, иначе
сервер станет неуправляемым.
Если в ваши планы входит дать доступ к серверу
всем пользователям и под любым именем, заведите пользователя с пустым
именем ''. Те же правила применяются к именам и адресам
компьютеров-хостов.
Теперь, размножив и поправив записи в таблице, но ни в коем случае не ее структуру, отправим команды обратно в MySQL.
...$ mysql -u root -p < mysql-users.sql
и попросим сервер перечитать измененные права
...$ mysqladmin -u root -p reload
Еще одно замечание насчет паролей. В рассмотренном
нами файле поля паролей пусты, и это надо немедленно исправить. Править
их в текстовом файле неудобно, потому что MySQL используем для
шифрования пароля отдельную программу, и хранит пароль в зашифрованом
виде. Чтобы установить пароль, например, пользователю "proba", надо
выполнить такую команду:
...$ mysql mysql -e 'update user set password=password("0") where user="proba";'
Поздравляю, мы только что составили и выполнили
первый SQL запрос в нашей лабораторной работе, хотя и сделали это из
командной строки, со всеми ее удобствами и неудобствами. К неудобствам
можно отнести то, что наш пароль высветился на экране, мог попасть в
список процессов, в разные журнальные файлы. Будет лучше не полениться,
запустить монитор mysql и задавать пароли в нем с помощью того же
запроса внутри СУБД MySQL.
Отличие от обычной системы паролей в том, что
имена пользователей БД могут быть не связаны с их регистрационными
именами в системе, пользователи не могут менять свои пароли, и этот
пароль известен администратору БД.
Разобравшись с самой первой административной
таблицей user, остальные таблицы: db, host, tables_priv, columns_priv,
func - правим аналогично.
Каждую из команд, посылаемую MySQL, можно
задавать либо в мониторе запросов mysql, либо из командной строки, либо
создав файл и отправив его в интерпретатор MySQL через тот же монитор.
Можно также обратиться к MySQL через интерфейсы с другими языками
программирования из программ, написанных на C, Perl, PHP, Python и
других.
Вывод на экран может немного отличаться в
каждом случае, а также в зависимости от того, на экран или в файл
(канал) направлен вывод. При работе в интерпретаторе всегда сообщается
время, потраченное на выполнение запроса, а при выводе в файл (канал)
не рисуется рамочка вокруг таблицы. Это делается только для нашего
удобства, и не влияет ни на результат, ни на сам SQL запрос.
Итак, обговорив разные способы ввода команд и
вывода результата, займемся собственно SQL предложениями и
преобразованием исходных данных. Доставшийся мне по случаю файл
09phone.txt представляет собой текстовый файл с полями в фиксированых
колонках, как здесь :
107003 банки "приорбанк" первомайская ул. 1 бнк 107007 центры информацио жукова ул. 4а цен 107026 предприятия транс артиллерийская ул. 8а пре
и каждая запись содержит 5 полей:
- номер телефона
- фамилия или название организации
- улица
- номер дома
- номер квартиры или примечание
Если бы исходные данные пришли из другой SQL БД и
были в виде, как уже изученная нами таблица user, все, что нам пришлось
бы сделать, это отправить в интерпретатор этот файл. Если бы текстовый
файл был в более удобоваримом виде, например, с полями, разделенными
знаками табуляции, то загрузить его в таблицу также можно было бы за
один шаг. Но в нашем случае придется создать временную таблицу
create table tmp ( line varchar(80) );
и импортировать в нее данные с помощью
load data infile '/tmp/09phone.txt' into table tmp;
Таким образом мы записали в таблицу tmp каждую
запись как строку без разделения на поля. Импорт занял на моем
компьютере около 4.4 сек. Здесь и далее и привожу время только для
сравнения, для своего компьютера и настроек программы, сделанных по
умолчанию.
Следующий шаг - извлечь данные из полей на
фиксированных позициях и поместить их в промежуточную таблицу old с
теми же полями, что и в исходной БД.
Сначала создадим таблицу
create table old ( phonum int unsigned not null, title varchar(64) not null, street varchar(40) not null, bldng varchar(8) not null, other varchar(8) not null );
а затем заполняем ее данными, пройдя по всем строкам таблицы tmp:
insert into old ( phonum, title, street, bldng, other ) select trim(mid(line,1,6)), trim(mid(line,8,18)), trim(mid(line,34,19)), trim(mid(line,58,4)), trim(mid(line,63,3)) from tmp;
Функция mid(...) извлекает из первого аргумента
подстроку в соответствующих позициях, а функция trim(...) удаляет
пробелы в начале и конце строки. Теперь можно спокойно сделать
drop table tmp;
Опять же, только для сравнения, 123 тысячи записей
обработаны за 8.6 сек.Для того, чтобы узнать время запроса из
программы, содержащей все команды и выполняемой неинтерактивно,
пришлось применить такой способ:
create table times ( start int unsigned ); insert into times values ( unix_timestamp() );
Чистый SQL и MySQL не поддерживают никаких иных
переменных, кроме таблиц и полей, так что для хранения времени пришлось
завести отдельную таблицу. Нет также ничего похожего на print или echo,
так что сообщать результат получилось только злоупотребив оператором
select:
select "импорт данных выполнен: ", unix_timestamp()-start, " сек." from times;
Получившаяся таблица old еще нуждается в
нормализации, но уже первые тривиальные запросы выявили одну проблему.
Сортировка по алфавиту использовала по умолчанию чуждую русскому языку
кодировку ISO-8859-1. Хотя в последних версиях, возможно, уже можно
менять порядок сортировки на ходу, в той версии, которая входит в
Debian v2.2, для правильной работы с русским языком необходима
перекомпиляция (сборка) программы с параметром
...$ ./configure --with-charset=koi8_ru
В Debian'е для сборки пакетов есть масса скриптов,
которые и делают всю работу. Таким образом, поправив файл debian/rules
и произведя магическое заклинание
...$ debuild -b -uc 2>&1|tee build.log
вскоре я получил готовый к инсталяции пакет с правильным понятием по-русски.
Но тут же возникла следующая проблема. FSF (Free
Software Foundation) и Debian очень щепетильно относятся к любым
ограничениям на свободу программ, и те ограничения на коммерческое
использование MySQL, которые были в их старой лицензии, привели к тому,
что MySQL оказался в секции non-free. Желание иметь клиентскую часть
свободной вынудило разработчиков вырезать из оригинальных исходников
чисто GPL-ные куски и образовать отдельное дерево исходников.
По-английски это называется "fork", а в русском языке вполне подходит
слово "раскол". Хоть это слово и с маленькой буквы, явление весьма
неприятное, распыляющее силы разработчиков и создающее неудобства
пользователям. В моем случае пришлось пересобирать также и GPL-ные
исходники, а затем бороться с конфликтом зависимостей пакетов.
Но вот борьба позади, и мы приступаем к
разбиению единой таблицы на несколько связанных и нормализованных, что,
собственно, и дает право называться СУБД реляционной. Из таблицы old с
теми же полями, что и в исходном файле, мы сделаем 3 таблицы,
связанные, как это обычно рисуется на схемах, таким образом:
phone ------ phonum building naim -------- street bd_id >------------ bd_id ------ other st_id >----------- st_id bldng nick
Значок >-- обозначает сторону "много" в отношении
"один ко многим" и означает, что в одном здании может быть много
телефонных номеров, а на одной улице много зданий.
Начать придется с конца, с таблицы street,
которая будет содержать список улиц, и на которую будет ссылаться
таблица building, содержащая, в свою очередь, список всех
телефонизированных зданий в городе.
create table street ( st_id smallint unsigned not null auto_increment, nick varchar(32) not null, primary key (st_id) ); insert into street ( nick ) select distinct street from old;
Заполнение таблицы заняло 12.1 сек. Теперь создадим таблицу building
create table building ( bd_id smallint unsigned not null auto_increment, st_id smallint unsigned not null references street, bldng varchar(8) not null, tmp varchar(40) not null, # временно, для соответствия с old primary key (bd_id) );
и также заполним ее
insert into building ( st_id, tmp, bldng ) select distinct street.st_id, street.nick, old.bldng from old, street where old.street=street.nick;
Таблица заполнялась аж 5 мин. 23 сек., так что было
время задуматься. Прояснить ситуацию в таких случаях помогает особая
команда explain, например
explain select distinct street.st_id, street.nick, old.bldng from old, street where old.street=street.nick;
которая выдала следующую подсказку:
+--------+------+---------------+------+---------+------+--------+------------+ | table | type | possible_keys | key | key_len | ref | rows | Extra | +--------+------+---------------+------+---------+------+--------+------------+ | street | ALL | NULL | NULL | NULL | NULL | 591 | | | old | ALL | NULL | NULL | NULL | NULL | 122794 | where used | +--------+------+---------------+------+---------+------+--------+------------+
Оказывается, для каждой записи из old происходит поиск в таблице street, т.е. просматриваются O(122794*591) строк.
Попробуем проиндексировать эти две таблицы по общему полю
create index street on street (nick); create index street on old (street);
Тот же самый запрос теперь выглядит изнутри вот так:
+--------+------+---------------+------+---------+------+--------+ | table | type | possible_keys | key | key_len | ref | rows | +--------+------+---------------+------+---------+------+--------+ | street | ALL | street | NULL | NULL | NULL | 591 | | old | ALL | street | NULL | NULL | NULL | 122794 | +--------+------+---------------+------+---------+------+--------+ ----------------------------------------------+ Extra | ----------------------------------------------+ | range checked for each record (index map: 1) | ----------------------------------------------+
и занимает 19.7 секунд. Даже с учетом ~1 мин. на
создание индексов, выигрыш в скорости заметен. Разобравшись с
индексами, можно их удалить
drop index street on street; drop index street on old;
Создаем теперь новую таблицу phone
create table phone ( phonum char(6) not null default "000000", naim varchar(48) not null default "", bd_id smallint unsigned not null references building, other varchar(8) not null );
Для заполнения последней таблицы даже не пробуем
делать выборку из неиндексированых таблиц, а первым делом создаем
индексы, используя заранее предусмотренное временное поле,
соответствующее названию улицы.
create index building on building (tmp, bldng); create index building on old (street, bldng); insert into phone ( phonum, naim, bd_id, other ) select old.phonum, old.title, building.bd_id, old.other from old, building where old.street=building.tmp and old.bldng=building.bldng;
Индексы создались за 3 и 45 сек., а данные вставились за 19 сек. Теперь можно удалить рабочую таблицу и лишние индекс и поле:
drop table old; drop index building on building; alter table building drop tmp;
Подведем некоторые итоги.
Из исходного 8-мегабайтного текстового файла получились 3 связанные таблицы общим размером ~3.8MB. Простые запросы, например
select p.phonum, p.naim, s.nick, b.bldng from phone p, street s, building b # короткие синонимы таблиц where p.bd_id=b.bd_id # таким образом and b.st_id=s.st_id # связывают таблицы and p.phonum like "%1234%" # собственно запрос order by p.naim;
занимают ~1.6 сек. Это приблизительно совпадает с
результатом сканера grep на оригинальном текстовом файле при поиске тех
же строк, и немного превосходит время, демонстрируемое интерпретатором
awk.
Но, конечно, MySQL создан не для того, чтобы
соревноваться с grep или awk. Используя язык SQL, можно создавать БД и
манипулировать данными любой сложности. В области клиент-серверных
приложений MySQL вполне способен конкурировать с признанными
коммерческими СУБД. Но вся мощь MySQL раскрывается в соединении с
технологиями Internet, если так можно выразиться, в "дважды
клиент-серверных" технологиях. Доступ к БД выполняется из приложений,
запускаемых на web-сервере, результат выдается в виде HTML страниц.
Затем web-сервер доставляет страницу в клиентский браузер.
В таком виде web-сервер Apache и реляционная
СУБД MySQL образуют необычайно масштабируемую платформу для создания
приложений. MySQL успешно трудится на самых разных аппаратных
платформах, включая суперкомпьютеры, и может обслуживать много
web-серверов, работающих на одном или на разных компьютерах. А можно
настроить такой же тандем для работы на единственной скромной
персоналке.
Как уже упоминалось, для доступа к MySQL можно
использовать разные языки программирования. Похожий выбор языков
программирования предлагает также и Apache через дополнительные модули,
расширяющие возможности сервера. Существуют и успешно развиваются
общедоступные открытые проекты Zope и Midgard , объединяющие Apache, MySQL и распространенные языки программирования в интегрированную среду разработки с единым интерфейсом.
|