Лабораторная работа: MySQL
Заняться выполнением этой лабораторной работы меня побудили
несколько причин. Во-первых, занимаясь построением серверов
и сетей на основе Линукс'а, я догадывался, что где-то
совсем рядом лежит сказочно богатый континент, пока не
нанесенный на мою карту компьютерного мира. Во-вторых,
авторы одной из самых популярных открытых программ - СУБД
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 и распространенные
языки программирования в интегрированную среду разработки
с единым интерфейсом.
|