Изучение MySQL

Получите доступ к своим данным

Винисиус М. Гриппа и Сергей Кузьмичев

Второе издание, 2021

перевод В.Айсин

Благодарности за Learning MySQL, второе издание

Прошло много времени с тех пор, как была опубликована хорошая книга, посвященная MySQL и ее экосистеме, и с тех пор произошло много изменений. Многие темы четко освещены примерами, от установки и проектирования баз данных до обслуживания и архитектуры для высокой доступности и облака. Также рассматриваются многие сторонние инструменты, такие как dbdeployer и ProxySQL, которые являются хорошими друзьями администраторов баз данных MySQL, но часто не упоминаются в литературе. Очень хорошая работа Винисиуса и Сергея. Не пропустите последнюю главу — это очень интересно!

— Фредерик Декамп, евангелист MySQL в Oracle

Прежде всего, я хочу поблагодарить Винисиуса и Сергея за то, что они сделали возможной книгу моих мечтаний для всех новичков в MySQL, пока я работаю над версией для разработчиков. Эта книга предлагает наиболее полную информацию о MySQL, не только о том, как начать работу, но и по таким сложным темам, как высокая доступность и балансировка нагрузки. Это гладкое чтение с хорошо организованным содержанием, соответствующим качеству публикации O'Reilly. Я настоятельно рекомендую эту книгу всем читателям, от разработчиков до операторов.

— Алкин Тезуйсал, старший технический менеджер PlanetScale

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

— Бретт Холлеман, инженер-программист

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

— Диего Эллас, генеральный директор PerformanceDB

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

— Чарли Батиста, Percona

Предисловие

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

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

Появилось несколько типов баз данных, помогающих хранить больше данных, особенно неструктурированных данных, включая базы данных NoSQL, такие как MongoDB, Cassandra и Redis. Однако традиционные базы данных SQL остаются популярными, и нет никаких признаков того, что они исчезнут в ближайшем будущем. И в мире SQL, несомненно, самым популярным решением с открытым исходным кодом является MySQL.

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

Для кого эта книга

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

Для тех, кто работает в другой экосистеме, такой как Postgres, Oracle или SQL Server, в книге рассматриваются стратегии резервного копирования, высокой доступности и аварийного восстановления.

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

Как устроена эта книга

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

  1. Начало с MySQL

  2. Использование MySQL

  3. MySQL в производстве

  4. Разные темы

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

Начало с MySQL

В главе 1, Установка MySQL объясняется, как установить и настроить программное обеспечение MySQL в различных операционных системах. В этой главе содержится гораздо больше деталей, чем в большинстве книг. Мы знаем, что те, кто начинает свою карьеру с MySQL, часто незнакомы с различными дистрибутивами Linux и вариантами установки, а для запуска «hello world» MySQL требуется гораздо больше шагов, чем для компиляции hello world на любом языке программирования. Вы увидите, как настроить MySQL в Linux, Windows, macOS и Docker, а также как быстро развернуть экземпляры для тестирования.

Использование MySQL

Прежде чем углубиться в создание и использование баз данных, мы рассмотрим правильное проектирование базы данных в главе 2, Моделирование и проектирование баз данных. Вы узнаете, как получить доступ к функциям вашей базы данных, и увидите, как элементы информации в вашей базе данных связаны друг с другом. Вы увидите, что плохой дизайн базы данных сложно изменить, и это может привести к проблемам с производительностью. Мы введем понятие сильных и слабых сущностей и их взаимосвязей (внешних ключей (foreign keys)) и объясним процесс нормализации. В этой главе также показано, как загружать и настраивать примеры баз данных, такие как sakila, world и employee.

В главе 3, Основы SQL мы исследуем знаменитые команды SQL, являющиеся частью операций CRUD (создание, чтение, обновление и удаление (create, read, update, delete)). Вы увидите, как читать данные из существующей базы данных MySQL, сохранять в ней данные и манипулировать существующими данными.

В главе 4, Работа со структурами баз данных мы объясняем, как создавать новую базу данных MySQL, а также создавать и изменять таблицы, индексы и другие структуры баз данных.

Глава 5, Расширенные запросы охватывает более сложные операции, такие как использование вложенных запросов и использование различных механизмов баз данных MySQL. Эта глава даст вам возможность выполнять более сложные запросы.

MySQL в производстве

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

В главе 7, Дополнительные возможности MySQL вы увидите более сложные запросы, которые вы можете выполнять в MySQL, а также узнаете, как наблюдать за планом запроса, чтобы проверить, эффективен ли запрос или нет. Мы также расскажем о различных механизмах, доступных в MySQL (наиболее известными являются InnoDB и MyISAM).

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

Глава 9, Использование файлов опций посвящена файлам конфигурации MySQL или файлам опций, которые содержат все необходимые параметры для запуска MySQL и оптимизации ее производительности. Те, кто знаком с MySQL, узнают файл конфигурации /etc/my.cnf. Вы также увидите, что можно настроить доступ пользователей с помощью специальных файлов параметров.

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

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

Разные темы

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

В главе 13, Высокая доступность объясняется, как реплицировать серверы для обеспечения высокой доступности. Мы также представляем концепцию кластера, выделяя два решения: кластер InnoDB и кластер Galera/PXC.

Глава 14, MySQL в облаке расширяет вселенную MySQL до облака. Вы узнаете о варианте «база данных как услуга» (DBaaS) и о том, как использовать управляемые службы баз данных, предоставляемые тремя наиболее известными поставщиками облачных услуг: Amazon Web Services (AWS), Google Cloud Platform (GCP) и Microsoft Azure.

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

Наконец, в главе 16, Разное представлены более продвинутые методы и инструменты анализа, а также немного программирования. В этой главе мы поговорим о MySQL Shell, Flame Graph и о том, как анализировать ошибки.

Условные обозначения, используемые в этой книге

В этой книге используются следующие типографские соглашения:

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

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

Примеры кода доступны для загрузки по адресу https://github.com/learning-mysql-2nd/learning-mysql-2nd (и локально learning_mysql/learning-mysql-2nd-main.zip).

Если у вас есть технический вопрос или проблема с использованием примеров кода, отправьте электронное письмо по адресу bookquestions@oreilly.com.

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

Мы ценим, но обычно не требуем указания авторства. Атрибуция обычно включает название, автора, издателя и ISBN. Например: «Learning MySQL, 2nd ed., by Vinicius M. Grippa and Sergey Kuzmichev (O’Reilly). Copyright 2021 Vinicius M. Grippa and Sergey Kuzmichev, 978-1-492-08592-8».

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

Благодарности

От Винисиуса Гриппы

Спасибо следующим людям, которые помогли улучшить эту книгу: Корбину Коллинзу, Чарли Батисте, Сами Алрусу и Бретту Холлеману. Без них эта книга не достигла бы того совершенства, к которому мы стремились.

Спасибо сообществу MySQL (особенно Шломи Ноаку, Джузеппе Максиа, Джереми Коулу и Брендану Греггу) и всем блоггерам Planet MySQL, Multiple Nines, Percona Blog и MySQL Entomologist, которые предоставили так много материала и так много замечательных инструментов.

Спасибо всем в Percona, которые предоставили средства для написания этой книги, особенно Бенни Гранту, Карине Пунцо и Марсело Альтманну, и тем, кто помог мне вырасти как профессионалу и человеку.

Спасибо сотрудникам O'Reilly, которые отлично справляются с изданием книг и проведением конференций.

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

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

От Сергея Кузьмичева

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

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

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

Спасибо всем в O'Reilly за руководство и помощь в создании этого издания. Спасибо Корбину Коллинзу за помощь в формировании структуры книги и за то, что мы твердо придерживаемся нашего пути. Спасибо Рэйчел Хед за обнаружение множества проблем на этапе редактирования и за выявление проблем с техническими деталями MySQL в нашем тексте. Без вас и всех сотрудников O'Reilly эта книга не была бы книгой, а просто набором слабо связанных слов.

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

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

Я хочу поблагодарить авторов первого издания этой книги: Хью Э. Уильямса и Сейеда М. М. Тахагоги. Благодаря их работе мы построили этот проект на прочном фундаменте.

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

Это издание я посвящаю своему сыну Григорию.

Глава 1
Установка MySQL

Давайте начнем наш путь обучения с установки MySQL и первого доступа к ней.

Обратите внимание, что в этой книге мы не полагаемся на одну версию MySQL. Вместо этого мы будем использовать наши коллективные знания MySQL в реальном мире. Основное внимание в книге уделяется операционным системам Linux (в основном Ubuntu/Debian и CentOS/RHEL или их производным), а также MySQL 5.7 и MySQL 8.0, поскольку именно их мы считаем «текущими» версиями, способными выполнять рабочие нагрузки. Серии MySQL 5.7 и 8.0 все еще находятся в стадии разработки, а это означает, что новые версии с исправлениями ошибок и новыми функциями будут продолжать выпускаться.

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

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

Форки MySQL

В разработке программного обеспечения форк происходит, когда кто-то копирует исходный код и начинает свой собственный путь независимой разработки и поддержки. Форк может следовать по пути, близкому к пути исходной версии, как это делает дистрибутив Percona MySQL, или отклоняться от него, как MariaDB. Поскольку исходный код MySQL является открытым и бесплатным, новые проекты могут разветвлять код без разрешения его первоначального создателя. Давайте взглянем на несколько наиболее заметных форков.

MySQL Community Edition

MySQL Community Edition, также известный как исходная или ванильная версия MySQL, является версией с открытым исходным кодом, распространяемой Oracle. Эта версия способствует развитию механизма InnoDB и новых функций, и она первой получает обновления, новые функции и исправления ошибок.

Percona Server for MySQL

Дистрибутив Percona MySQL представляет собой бесплатную замену MySQL Community Edition с открытым исходным кодом. Разработка следует за этой версией, уделяя особое внимание повышению производительности и общей экосистемы MySQL. Percona Server также включает дополнительные усовершенствования, такие как движок MyRocks, плагин Audit Log и плагин PAM Authentication. Соучредителями Percona были Петр Зайцев и Вадим Ткаченко.

MariaDB Server

MariaDB Server, созданный Майклом «Монти» Видениусом и распространяемый MariaDB Foundation, на сегодняшний день является форком, который больше всего отдалился от ванильного MySQL. В последние годы он разработал новые функции и механизмы, такие как MariaDB ColumnStore, и стал первой базой данных, интегрировавшей функции кластеризации Galera 4.

MySQL Enterprise Edition

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

Варианты установки и платформы

Во-первых, вы должны выбрать версию MySQL, совместимую с вашей операционной системой (ОС). Вы можете проверить совместимость на веб-сайте MySQL. Те же политики поддержки доступны для Percona Server и MariaDB.

Мы часто слышим вопрос: можно ли установить MySQL на неподдерживаемую ОС? В большинстве случаев ответ положительный. Например, можно установить MySQL в Windows 7, но высок риск возникновения ошибки или непредсказуемого поведения (например, утечки памяти или недостаточной производительности). Из-за этих рисков мы не рекомендуем делать это для производственных сред.

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

Последнее, что нужно решить, это какой формат дистрибутива установить для операционной системы. Для большинства случаев использования подходит бинарный дистрибутив. Двоичные дистрибутивы доступны в собственном формате для многих платформ, например пакеты .rpm для Linux или пакеты .dmg для macOS. Дистрибутивы также доступны в универсальных форматах, таких как архивы .zip или сжатые файлы .tar (tarballs). В Windows вы можете использовать установщик MySQL для установки бинарного дистрибутива.

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

1. Загрузите дистрибутив, который вы хотите установить

У каждого дистрибутива есть свой владелец и, как следствие, свой источник. Некоторые дистрибутивы Linux предоставляют пакеты по умолчанию в своих репозиториях. Например, в CentOS 8 ванильный дистрибутив MySQL доступен в репозиториях по умолчанию. Когда в ОС доступны пакеты по умолчанию, нет необходимости загружать MySQL с веб-сайта или настраивать репозиторий самостоятельно, что упрощает процесс установки.

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

2. Установите дистрибутив

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

3. Выполните любую необходимую настройку после установки

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

4. Запустите тесты

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

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

Установка MySQL в Linux

Экосистема Linux разнообразна и имеет множество вариантов, включая Red Hat Enterprise Linux (RHEL), CentOS, Ubuntu, Debian и другие. В этом разделе рассматриваются только самые популярные из них, иначе эта книга была бы целиком посвящена процессу установки!

Установка MySQL на CentOS 7

CentOS, сокращение от Community Enterprise Linux Operating System, была основана в 2004 году, а Red Hat приобрела ее в 2014 году. CentOS — это версия Red Hat для сообщества, поэтому они практически идентичны, но CentOS бесплатна, а поддержка предоставляется сообществом вместо самой Red Hat. CentOS 7 была выпущена в 2014 году, а ее срок службы истекает в 2024 году.

Установка MySQL 8.0

Чтобы установить MySQL 8.0 на CentOS 7 с помощью репозитория yum, выполните следующие шаги.

Войдите на сервер Linux. Обычно из соображений безопасности пользователи входят на серверы Linux как непривилегированные пользователи. Вот пример входа пользователя на Linux с терминала macOS с использованием закрытого ключа:

$ ssh -i key.pem centos@3.227.11.227

После успешного подключения вы увидите что-то вроде этого в терминале:

[centos@ip-172-30-150-91 ~]$

Станьте root в Linux. После подключения к серверу вам необходимо стать пользователем root:

$ sudo su - root

Затем вы увидите подсказку, подобную следующей, в вашем терминале:

[root@ip-172-30-150-91 ~]#

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

Настройте репозиторий yum. Выполните следующую команду, чтобы настроить репозиторий MySQL yum:

# rpm -Uvh https://repo.mysql.com/mysql80-community-release-el7.rpm

Установите MySQL 8.0 Community Server. Поскольку в репозитории MySQL yum есть репозитории для нескольких версий MySQL (основные версии 5.7 и 8.0), сначала мы должны отключить все репозитории:

# sed -i 's/enabled=1/enabled=0/'
/etc/yum.repos.d/mysql-community.repo

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

# yum --enablerepo=mysql80-community install mysql-community-server

Запустите службу MySQL. Теперь запустите службу MySQL с помощью команды systemctl:

# systemctl start mysqld

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

# mysqld --defaults-file=/etc/my.cnf --user=mysql

Узнайте пароль по умолчанию для пользователя root. Когда вы устанавливаете MySQL 8.0, MySQL создает временный пароль для учетной записи пользователя root. Чтобы определить пароль учетной записи пользователя root, выполните следующую команду:

# grep "A temporary password" /var/log/mysqld.log

Команда выдает следующий вывод:

2020-05-31T15:04:12.256877Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: #z?hhCCyj2aj

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

Выполните команду mysql_secure_installation для защиты сервера MySQL:

# mysql_secure_installation

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

Enter the password for user root:

Введите временный пароль, полученный на предыдущем шаге, и нажмите Enter. Появится следующее сообщение:

The existing password for the user account root has expired. Please set a new password.

New password:
Re-enter new password:

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

Затем он предложит вам несколько вопросов yes/no о том, хотите ли вы внести некоторые первоначальные изменения в настройку. Чтобы обеспечить максимальную защиту, мы рекомендуем удалить анонимных пользователей, отключить удаленный вход в систему root и удалить тестовую базу данных (т. е. ответить yes на все варианты):

Remove anonymous users? (Press y|Y for Yes, any other key for No) : y

Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y

Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y

Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y

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

# mysql -u root -p

Он запросит пароль пользователя root. Введите пароль и нажмите Enter:

Enter password:

В случае успеха он покажет командную строку MySQL:

mysql>

Запускайте MySQL 8.0 при загрузке сервера (опционально). Чтобы настроить автозапуск MySQL при каждой загрузке сервера, используйте следующую команду:

# systemctl enable mysqld
Установка MariaDB 10.5

Чтобы установить MariaDB 10.5 на CentOS 7, вам необходимо выполнить те же шаги, что и для ванильного дистрибутива MySQL.

Станьте root в Linux. Во-первых, нам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Установите репозиторий MariaDB. Следующий набор команд загрузит репозиторий MariaDB и настроит его для следующего шага. Обратите внимание, что в командах yum мы используем параметр -y. Эта опция сообщает Linux, что на все последующие вопросы ответ будет yes:

# yum install wget -y
# wget https://downloads.mariadb.com/MariaDB/mariadb_repo_setup
# chmod +x mariadb_repo_setup
# ./mariadb_repo_setup

Установите MariaDB. После настройки репозитория следующая команда установит последнюю стабильную версию MariaDB и ее зависимости:

# yum install MariaDB-server -y

Конец вывода будет примерно таким:

Installed:
  MariaDB-compat.x86_64 0:10.5.8-1.el7.centos

Dependency Installed:
  MariaDB-client.x86_64 0:10.5.8-1.el7.centos MariaDB-common.x86_64
  0:10.5.8-1.el7.centos boost-program-options.x86_64 0:1.53.0-28.el7
  galera-4.x86_64 0:26.4.6-1.el7.centos
  libaio.x86_64
  0:0.3.109-13.el7
  lsof.x86_64 0:4.87-6.el7
  pcre2.x86_64 0:10.23-2.el7
  perl.x86_64
  4:5.16.3-299.el7_9
  perl-Carp.noarch 0:1.26-244.el7
  ...

Replaced:
  mariadb-libs.x86_64 1:5.5.64-1.el7

Complete!

Complete! в конце вывода указывает на успешную установку.

Запустите MariaDB. Установив MariaDB, инициализируйте службу с помощью команды systemctl:

# systemctl start mariadb.service

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

# systemctl status mariadb

 mariadb.service - MariaDB 10.5.8 database server
  Loaded: loaded (/usr/lib/systemd/system/mariadb.service; disabled; vendor preset: disabled)
...
Feb 07 12:55:04 ip-172-30-150-91.ec2.internal systemd[1]: Started MariaDB 10.5.8 database server.

Защитите MariaDB. На данный момент MariaDB будет работать в небезопасном режиме. В отличие от MySQL 8.0, у MariaDB будет пустой пароль root, поэтому вы сможете мгновенно получить доступ:

# mysql

Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 44
Server version: 10.5.8-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

Вы можете выполнить mysql_secure_installation для защиты MariaDB точно так же, как и для MySQL 8.0 (подробности см. в предыдущем разделе). Есть небольшое различие в выводе с одним дополнительным вопросом:

Switch to unix_socket authentication [Y/n] y
Enabled successfully!
Reloading privilege tables..
... Success!

Ответ yes изменяет соединение с TCP/IP на режим сокета Unix. Мы обсудим различные типы соединений в разделе «Файлы MySQL 5.7 по умолчанию».

Установка Percona Server 8.0

Установите Percona Server 8.0 на CentOS 7, выполнив следующий шаг.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Установите репозиторий Percona. Вы можете установить репозиторий Percona yum, выполнив следующую команду от имени пользователя root или с помощью sudo:

# yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm

При установке создается новый файл репозитория /etc/yum.repos.d/percona-original-release.repo. Теперь включите репозиторий Percona Server 8.0 с помощью этой команды:

# percona-release setup ps80

Установите Percona Server 8.0. Чтобы установить сервер, выполните эту команду:

# yum install percona-server-server

Инициализируйте Percona Server 8.0 с помощью systemctl. После установки двоичных файлов Percona Server 8.0 запустите службу:

# systemctl start mysql

И подтвердите его статус:

# systemctl status mysql

 mysqld.service - MySQL Server
   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2021-02-07 13:22:15 UTC; 6s ago
     Docs: man:mysqld(8)
           http://dev.mysql.com/doc/refman/en/using-systemd.html
  Process: 14472 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS)
 Main PID: 14501 (mysqld)
   Status: "Server is operational"
    Tasks: 39 (limit: 5789)
   Memory: 345.2M
   CGroup: /system.slice/mysqld.service
           └─14501 /usr/sbin/mysqld

Feb 07 13:22:14 ip-172-30-92-109.ec2.internal systemd[1]: Starting MySQL Server...
Feb 07 13:22:15 ip-172-30-92-109.ec2.internal systemd[1]: Started MySQL Server.

На этом этапе шаги аналогичны установке vanilla. Обратитесь к разделам о получении временного пароля и выполнении команды mysql_secure_installation в разделе «Установка MySQL 8.0».

Установка MySQL 5.7

Установите MySQL 5.7 на CentOS 7, выполнив следующие действия.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Установите репозиторий MySQL 5.7. Вы можете установить репозиторий MySQL 5.7 yum, выполнив следующую команду от имени пользователя root или с помощью sudo:

# yum localinstall\
    https://dev.mysql.com/get/mysql57-community-release-el7-9.noarch.rpm -y

При установке создается новый файл репозитория /etc/yum.repos.d/mysql-community.repo.

Установите двоичные файлы MySQL 5.7. Чтобы установить сервер, выполните эту команду:

# yum install mysql-community-server -y

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

# systemctl start mysqld

И запустите эту команду, чтобы проверить его статус:

# systemctl status mysqld

На этом этапе шаги аналогичны стандартной установке MySQL 8.0. Обратитесь к разделам о получении временного пароля и выполнении команды mysql_secure_installation в разделе «Установка MySQL 8.0».

Установка Percona Server 5.7

Установите Percona Server 5.7 на CentOS 7, выполнив следующие действия.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Установите репозиторий Percona. Вы можете установить репозиторий Percona yum, выполнив следующую команду от имени пользователя root или с помощью sudo:

# yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm

При установке создается новый файл репозитория /etc/yum.repos.d/percona-original-release.repo. Используйте эту команду, чтобы включить репозиторий Percona Server 5.7:

# percona-release setup ps57

Установите двоичные файлы Percona Server 5.7. Чтобы установить сервер, выполните эту команду:

# yum install Percona-Server-server-57 -y

Инициализируйте Percona Server 5.7 с помощью systemctl. После установки двоичных файлов Percona Server 5.7 запустите службу:

# systemctl start mysql

И подтвердите его статус:

# systemctl status mysql

На этом этапе шаги аналогичны стандартной установке MySQL 8.0. Обратитесь к разделам о получении временного пароля и выполнении команды mysql_secure_installation в разделе «Установка MySQL 8.0».

Установка MySQL на CentOS 8

Текущая версия CentOS — CentOS 8, построенная на основе RHEL 8. Как правило, CentOS имеет тот же десятилетний жизненный цикл поддержки, что и сам RHEL. Этот традиционный жизненный цикл поддержки означает конец срока службы CentOS 8 в 2029 году. Однако в декабре 2020 года объявление Red Hat сигнализировало о намерении поставить надгробный камень на могиле CentOS 8 гораздо раньше — в 2021 году. (Red Hat будет поддерживать CentOS 7 вместе с RHEL 7 до 2024 года.) Текущие пользователи CentOS должны будут перейти либо на сам RHEL, либо на более новый проект CentOS Stream. Возникают некоторые проекты сообщества, но на данный момент будущее CentOS неопределенно.

Однако мы расскажем об этапах установки здесь, поскольку многие пользователи используют RHEL 8 и Oracle Linux 8 в отрасли.

Установка MySQL 8.0

Последняя версия MySQL 8.0 доступна для установки из репозитория AppStream по умолчанию с использованием модуля MySQL, который по умолчанию включен в системах CentOS 8 и RHEL 8. Таким образом, есть некоторые отличия от традиционного метода yum. Давайте посмотрим на детали.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Установите двоичные файлы MySQL 8.0. Выполните следующую команду, чтобы установить пакет mysql-server и ряд его зависимостей:

# dnf install mysql-server

При появлении запроса нажмите y, а затем Enter, чтобы подтвердить, что вы хотите продолжить:

Output
...
Transaction Summary
=======================================================================
Install 50 Packages
Upgrade
8 Packages

Total download size: 50 M
Is this ok [y/N]: y

Запустите MySQL. На данный момент вы установили MySQL на свой сервер, но он еще не работает. Установленный вами пакет настраивает MySQL для запуска в качестве службы systemd с именем mysqld.service. Чтобы запустить MySQL, вам нужно использовать команду systemctl:

# systemctl start mysqld.service

Проверьте, запущена ли служба. Чтобы проверить, правильно ли работает служба, выполните следующую команду:

# systemctl status mysqld

Если вы успешно запустили MySQL, вывод покажет, что служба MySQL активна:

# systemctl status mysqld

mysqld.service - MySQL 8.0 database server
   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; disabled; vendor preset: disabled)
   Active: active (running) since Sun 2020-06-21 22:57:57 UTC; 6s ago
  Process: 15966 ExecStartPost=/usr/libexec/mysql-check-upgrade (code=exited, status=0/SUCCESS)
  Process: 15887 ExecStartPre=/usr/libexec/mysql-prepare-db-dir mysqld.service (code=exited, status=0/SUCCESS)
  Process: 15862 ExecStartPre=/usr/libexec/mysql-check-socket (code=exited, status=0/SUCCESS)
 Main PID: 15924 (mysqld)
   Status: "Server is operational"
    Tasks: 39 (limit: 23864)
   Memory: 373.7M
   CGroup: /system.slice/mysqld.service
           └─15924 /usr/libexec/mysqld --basedir=/usr

Jun 21 22:57:57 ip-172-30-222-117.ec2.internal systemd[1]: Starting MySQL 8.0 database server...
Jun 21 22:57:57 ip-172-30-222-117.ec2.internal systemd[1]: Started MySQL 8.0 database server.

Защитите MySQL 8.0. Как и при установке MySQL 8.0 на CentOS 7, вам необходимо выполнить команду mysql_secure_installation (подробности см. в соответствующем разделе «Установка MySQL 8.0»). Основное отличие состоит в том, что для CentOS 8 нет временного пароля, поэтому, когда скрипт запрашивает пароль root, оставьте его пустым и нажмите Enter.

Запустите MySQL 8.0 при загрузке сервера (опционально). Чтобы настроить запуск MySQL при каждой загрузке сервера, используйте следующую команду:

# systemctl enable mysqld
Установка Percona Server 8.0

Чтобы установить Percona Server 8.0 на CentOS 8, вам необходимо сначала установить репозиторий. Давайте пройдемся по шагам.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Установите двоичные файлы Percona Server 8.0. Выполните следующую команду, чтобы установить репозиторий Percona:

# yum install https://repo.percona.com/yum/percona-release-latest.noarh.rpm

При появлении запроса нажмите y, а затем Enter, чтобы подтвердить, что вы хотите продолжить:

Last metadata expiration check: 0:03:49 ago on Sun 07 Feb 2021 01:16:41 AM UTC.
percona-release-latest.noarch.rpm
Dependencies resolved.

<snip>

Total size: 19 k
Installed size: 31 k
Is this ok [y/N]: y
Downloading Packages:
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :
  1/1
  Installing       : percona-release-1.0-25.noarch
  1/1
  Running scriptlet: percona-release-1.0-25.noarch
  1/1
* Enabling the Percona Original repository
<*> All done!
* Enabling the Percona Release repository
<*> All done!
The percona-release package now contains a percona-release script that
can enable additional repositories for our newer products. For example, to
enable the Percona Server 8.0 repository use:

  percona-release setup ps80

Note: To avoid conflicts with older product versions, the percona-release setup
command may disable our original repository for some products. For more
information, please visit:

  https://www.percona.com/doc/percona-repo-config/percona-release.html

  Verifying: percona-release-1.0-25.noarch 1/1

Installed:
  percona-release-1.0-25.noarch

Включите репозиторий для Percona 8.0. При установке создается новый файл репозитория в /etc/yum.repos.d/percona-original-release.repo. Включите репозиторий Percona Server 8.0 с помощью этой команды:

# percona-release setup ps80

Команда предлагает вам отключить модуль RHEL 8 для MySQL. Вы можете сделать это сейчас, нажав y:

* Disabling all Percona Repositories
On RedHat 8 systems it is needed to disable dnf mysql module to install
Percona-Server
Do you want to disable it? [y/N] y
Disabling dnf module...
Percona Release release/noarch YUM repository
6.4 kB/s | 1.4 kB
00:00
Dependencies resolved.

<snip>

Complete!
dnf mysql module was disabled
* Enabling the Percona Server 8.0 repository
* Enabling the Percona Tools repository
<*> All done!

Или сделайте это вручную с помощью следующей команды:

# dnf module disable mysql

Установите двоичные файлы Percona Server 8.0. Теперь вы готовы установить Percona Server 8.0 на свой сервер CentOS 8/RHEL 8. Чтобы избежать повторного запроса о том, хотите ли вы продолжить, добавьте -y в командную строку:

# yum install percona-server-server -y

Запустите и защитите Percona Server 8.0. Теперь, когда вы установили двоичные файлы Percona Server 8.0, вы можете запустить службу mysqld и настроить ее на запуск при загрузке системы:

# systemctl enable --now mysqld
# systemctl start mysqld

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

# systemctl status mysqld

mysqld.service - MySQL Server
   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2021-02-07 01:30:50 UTC; 28s ago
     Docs: man:mysqld(8)
           http://dev.mysql.com/doc/refman/en/using-systemd.html
  Process: 12864 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS)
 Main PID: 12942 (mysqld)
   Status: "Server is operational"
    Tasks: 39 (limit: 5789)
   Memory: 442.6M
   CGroup: /system.slice/mysqld.service
           └─12942 /usr/sbin/mysqld

Feb 07 01:30:40 ip-172-30-92-109.ec2.internal systemd[1]: Starting MySQL Server..
Feb 07 01:30:50 ip-172-30-92-109.ec2.internal systemd[1]: Started MySQL Server.
Установка MySQL 5.7

Установите MySQL 5.7 на CentOS 8, выполнив следующие действия.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Отключить модуль MySQL по умолчанию. Такие системы, как RHEL 8, Oracle Linux 8 и CentOS 8, включают модуль MySQL по умолчанию. Если этот модуль не отключен, он маскирует пакеты, предоставляемые репозиториями MySQL, не позволяя вам установить версию, отличную от MySQL 8.0. Итак, используйте эти команды, чтобы удалить этот модуль по умолчанию:

# dnf remove @mysql
# dnf module reset mysql && dnf module disable mysql

Настройте репозиторий MySQL 5.7. Для CentOS 8 нет репозитория MySQL, поэтому вместо него мы будем использовать репозиторий CentOS 7 в качестве эталона. Создайте новый файл репозитория:

# vi /etc/yum.repos.d/mysql-community.repo

И вставьте в файл следующие данные:

[mysql57-community]
name=MySQL 5.7 Community Server
baseurl=http://repo.mysql.com/yum/mysql-5.7-community/el/7/$basearch/
enabled=1
gpgcheck=0

[mysql-connectors-community]
name=MySQL Connectors Community
baseurl=http://repo.mysql.com/yum/mysql-connectors-community/el/7/$basearch/
enabled=1
gpgcheck=0

[mysql-tools-community]
name=MySQL Tools Community
baseurl=http://repo.mysql.com/yum/mysql-tools-community/el/7/$basearch/
enabled=1
gpgcheck=0

Установите двоичные файлы MySQL 5.7. С отключенным модулем по умолчанию и настроенным репозиторием выполните следующую команду, чтобы установить пакет mysql-server и его зависимости:

# dnf install mysql-community-server

При появлении запроса нажмите y, а затем Enter, чтобы подтвердить, что вы хотите продолжить:

Output
...
Install  5 Packages
Total download size: 202 M
Installed size: 877 M
Is this ok [y/N]: y

Запустите MySQL. Вы установили исполняемые файлы MySQL на свой сервер, но он еще не работает. Установленный вами пакет настраивает MySQL для запуска в качестве службы systemd с именем mysqld.service. Чтобы запустить MySQL, вам нужно использовать команду systemctl:

# systemctl start mysqld.service

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

# systemctl status mysqld

Если вы успешно запустили MySQL, вывод покажет, что служба MySQL активна:

# systemctl status mysqld

mysqld.service - MySQL Server
   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2021-02-07 18:22:12 UTC; 9s ago
     Docs: man:mysqld(8)
           http://dev.mysql.com/doc/refman/en/using-systemd.html
  Process: 14396 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS (code=exited, status=0/SUCCESS)
  Process: 8137 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS)
 Main PID: 14399 (mysqld)
    Tasks: 27 (limit: 5789)
   Memory: 327.2M
   CGroup: /system.slice/mysqld.service
           └─14399 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid

Feb 07 18:22:02 ip-172-30-36-53.ec2.internal systemd[1]: Starting MySQL Server...
Feb 07 18:22:12 ip-172-30-36-53.ec2.internal systemd[1]: Started MySQL Server.

Защитите MySQL 5.7. На этом этапе шаги аналогичны стандартной установке MySQL 8.0. Обратитесь к разделам о получении временного пароля и выполнении команды mysql_secure_installation в разделе «Установка MySQL 8.0».

Запустите MySQL 5.7 при загрузке сервера (опционально). Чтобы настроить запуск MySQL при каждой загрузке сервера, используйте следующую команду:

# systemctl enable mysqld

Установка MySQL на Ubuntu 20.04 LTS (Focal Fossa)

Ubuntu — это дистрибутив Linux, основанный на Debian, который состоит в основном из бесплатного программного обеспечения с открытым исходным кодом. Официально существует три версии Ubuntu: Desktop, Server и Core для устройств и роботов IoT. Версия, с которой мы будем работать в этой книге, — это Server.

Установка MySQL 8.0

Для Ubuntu процесс немного отличается, так как Ubuntu использует репозиторий apt. Давайте пройдемся по шагам.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Настройте репозиторий apt. В Ubuntu 20.04 (Focal Fossa) вы можете установить MySQL, используя репозиторий пакетов apt. Во-первых, убедитесь, что ваша система обновлена:

# apt update

Установите MySQL 8.0. Затем установите пакет mysql-server:

# apt install mysql-server -y

Команда apt install установит MySQL, но не предложит вам установить пароль или внести какие-либо другие изменения в конфигурацию. В отличие от установки CentOS, Ubuntu инициализирует MySQL в небезопасном режиме.

Для новых установок MySQL вам потребуется запустить встроенный сценарий безопасности системы управления базами данных (СУБД). Этот сценарий изменяет некоторые менее безопасные параметры по умолчанию для удаленного входа в систему root и тестовой базы данных. Мы решим эту проблему на этапе защиты после инициализации MySQL.

Запустите MySQL. На данный момент вы установили MySQL на свой сервер, но он еще не работает. Чтобы запустить MySQL, вам нужно использовать команду systemctl:

# systemctl start mysql

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

# systemctl status mysql

Если вы успешно запустили MySQL, вывод покажет, что служба MySQL активна:

mysql.service - MySQL Community Server
     Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2021-02-07 20:19:51 UTC; 22s ago
    Process: 3514 ExecStartPre=/usr/share/mysql/mysql-systemd-start pre (code=exited, status=0/SUCCESS)
   Main PID: 3522 (mysqld)
     Status: "Server is operational"
      Tasks: 38 (limit: 1164)
     Memory: 332.7M
     CGroup: /system.slice/mysql.service
             └─3522 /usr/sbin/mysqld

Feb 07 20:19:50 ip-172-30-202-86 systemd[1]: Starting MySQL Community Server...
Feb 07 20:19:51 ip-172-30-202-86 systemd[1]: Started MySQL Community Server.

Защитите MySQL 8.0. На этом этапе шаги аналогичны стандартной установке на CentOS 7 (см. «Установка MySQL 8.0»). Однако MySQL 8.0 в Ubuntu инициализируется незащищенным, что означает, что пароль root пуст. Чтобы защитить его, выполните mysql_secure_installation:

# mysql_secure_installation

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

Здесь есть небольшая разница, потому что в Ubuntu можно изменить политику проверки, которая управляет надежностью пароля. В этом примере мы устанавливаем политику проверки на MEDIUM (1):

Securing the MySQL server deployment.

Connecting to MySQL using a blank password.

VALIDATE PASSWORD COMPONENT can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD component?

Press y|Y for Yes, any other key for No: y

There are three levels of password validation policy:

LOW    Length >= 8
MEDIUM Length >= 8, numeric, mixed case, and special characters
STRONG Length >= 8, numeric, mixed case, special characters and dictionary

Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 1
Please set the password for root here.

New password:

Re-enter new password:

Estimated strength of the password: 50
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y
By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother.
You should remove them before moving into a production environment.
Установка Percona Server 8

Установите Percona Server 8.0 на Ubuntu 20.04 LTS, выполнив следующие действия.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Установите защиту конфиденциальности GNU. Oracle подписывает загружаемые пакеты MySQL с помощью GNU Privacy Guard (GnuPG), альтернативы с открытым исходным кодом известной Pretty Good Privacy (PGP), созданной Филом Циммерманном. Большинство дистрибутивов Linux поставляются с установленным по умолчанию GnuPG, но в этом случае его необходимо установить:

# apt-get install gnupg2 -y

Получите пакеты репозитория с веб-сайта Percona. Затем извлеките пакеты репозитория из репозитория Percona с помощью команды wget:

# wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb

Установите загруженный пакет с помощью dpkg. После загрузки установите пакет с помощью следующей команды:

# dpkg -i percona-release_latest.$(lsb_release -sc)_all.deb

Затем вы можете проверить репозиторий, настроенный в файле /etc/apt/sources.list.d/percona-original-release.list.

Включите репозиторий. Следующим шагом является включение Percona Server 8.0 в репозиторий и его обновление:

# percona-release setup ps80
# apt update

Установите двоичные файлы Percona Server 8.0. Затем установите пакет percona-server-server с помощью команды apt-get install:

# apt-get install percona-server-server -y

Запустите MySQL. На данный момент вы установили MySQL на свой сервер, но он еще не работает. Чтобы запустить MySQL, вам нужно использовать команду systemctl:

# systemctl start mysql

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

# systemctl status mysql

В этот момент Percona Server будет работать в небезопасном режиме. Выполнение mysql_secure_installation проведет вас через ряд запросов, чтобы внести некоторые изменения в параметры безопасности вашей установки MySQL, которые идентичны описанным для установки vanilla MySQL 8.0 в предыдущем разделе.

Установка MariaDB 10.5

Установите MariaDB 10.5 на Ubuntu 20.04 LTS, выполнив следующие действия.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Обновите систему с помощью менеджера пакетов apt. Убедитесь, что ваша система обновлена, и установите пакет software-properties-common с помощью следующих команд:

# apt update && sudo apt upgrade
# apt -y install software-properties-common

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

Импортируйте GPG-ключ MariaDB. Выполните следующую команду, чтобы добавить ключ репозитория в систему:

# apt-key adv --fetch-keys 'https://mariadb.org/mariadb_release_signing_key.asc'

Добавьте репозиторий MariaDB. После импорта ключа GPG репозитория вам необходимо добавить репозиторий apt, выполнив следующую команду:

# add-apt-repository 'deb [arch=amd64] http://mariadb.mirror.globo.tech/repo/10.5/ubuntu focal main'

Установите бинарные файлы MariaDB 10.5. Следующим шагом будет установка сервера MariaDB:

# apt install mariadb-server mariadb-client

Проверьте, запущена ли служба. Чтобы проверить, правильно ли работает служба MariaDB, выполните следующую команду:

# systemctl status mysql

На данный момент MariaDB 10.5 будет работать в небезопасном режиме. Выполнение mysql_secure_installation проведет вас через ряд запросов, чтобы внести некоторые изменения в параметры безопасности вашей установки MySQL, которые идентичны тем, которые описаны для установки vanilla MySQL 8.0 в Ubuntu ранее в этом разделе.

Установка MySQL 5.7

Установите MySQL 5.7 на Ubuntu 20.04 LTS, выполнив следующие действия.

Станьте root в Linux. Во-первых, вам нужно стать root. См. инструкции в разделе «Установка MySQL 8.0».

Обновите систему с помощью менеджера пакетов apt. Вы можете убедиться, что ваша система обновлена, и установить пакет software-properties-common с помощью следующей команды:

# apt update -y && sudo apt upgrade -y

Добавьте и настройте репозиторий MySQL 5.7. Добавьте репозиторий MySQL, выполнив следующие команды:

# wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb
# dpkg -i mysql-apt-config_0.8.12-1_all.deb

В командной строке выберите ubuntu bionic, как показано на рис. 1-1, и нажмите ОК.

Рисунок 1-1. Выберите ubuntu bionic

Следующее приглашение показывает, что MySQL 8.0 выбран по умолчанию (рис. 1-2). Выбрав эту опцию, нажмите Enter.

Рисунок 1-2. Выберите вариант MySQL Server & Cluster.

Для следующего варианта, как показано на рис. 1-3, выберите MySQL 5.7 и нажмите OK.

Рисунок 1-3. Выберите вариант MySQL 5.7.

Вернувшись на главный экран, нажмите OK для выхода, как показано на рис. 1-4.

Рисунок 1-4. Нажмите OK, чтобы выйти

Далее вам необходимо обновить пакеты MySQL:

# apt-get update -y

Подтвердите политику Ubuntu для установки MySQL 5.7:

# apt-cache policy mysql-server

Проверьте вывод, чтобы узнать, какая версия MySQL 5.7 доступна:

# apt-cache policy mysql-server

mysql-server:
  Installed: (none)
  Candidate: 8.0.23-0ubuntu0.20.04.1
  Version table:
     8.0.23-0ubuntu0.20.04.1 500
        500 http://br.archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages
        500 http://br.archive.ubuntu.com/ubuntu focal-security/main amd64 Packages
     8.0.19-0ubuntu5 500
        500 http://br.archive.ubuntu.com/ubuntu focal/main amd64 Packages
     5.7.33-1ubuntu18.04 500
        500 http://repo.mysql.com/apt/ubuntu bionic/mysql-5.7 amd64 Packages

Установите двоичные файлы MySQL 5.7. Теперь, когда вы убедились, что доступна версия MySQL 5.7 (5.7.33-1ubuntu18.04), установите ее:

# apt-get install mysql-client=5.7.33-1ubuntu18.04 -y
# apt-get install mysql-community-server=5.7.33-1ubuntu18.04 -y
# apt-get install mysql-server=5.7.33-1ubuntu18.04 -y

В процессе установки вам будет предложено выбрать пароль root, как показано на рисунке 1-5.

Рисунок 1-5. Определите пароль root и нажмите OK.

Проверьте, запущена ли служба. Чтобы проверить, правильно ли работает служба MySQL 5.7, выполните следующую команду:

# systemctl status mysql

На этом этапе в MySQL 5.7 будет установлен пароль для пользователя root. Тем не менее, вы все равно захотите запустить mysql_secure_installation, чтобы установить политику паролей, удалить удаленный вход root и анонимных пользователей, а также удалить тестовую базу данных. Подробности см. в разделе «Безопасность MySQL 8.0».

Установка MySQL на macOS Big Sur

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

Установка MySQL 8

Сначала загрузите файл MySQL .dmg с веб-сайта MySQL.

После загрузки запустите пакет, чтобы начать процедуру установки, как показано на рисунке 1-6.

Рисунок 1-6. Пакет MySQL 8.0.23 .dmg

Затем вам нужно авторизовать запуск MySQL, как показано на рис. 1-7.

Рисунок 1-7. Запрос авторизации MySQL 8.0.23

Рисунок 1-8 показывает экран приветствия установщика.

Рисунок 1-8. Начальный экран MySQL 8.0.23

Рисунок 1-9 показывает лицензионное соглашение. Даже с программным обеспечением с открытым исходным кодом необходимо согласиться с условиями лицензии; в противном случае вы не сможете продолжить.

Рисунок 1-9. Лицензионное соглашение MySQL 8.0.23

Теперь вы можете определить местоположение и настроить установку, как показано на рис. 1-10.

Рисунок 1-10. Настройка установки MySQL 8.0.23

Вы собираетесь продолжить стандартную установку. После нажатия Install вам может быть предложено ввести пароль пользователя macOS для запуска установки с более высокими привилегиями, как показано на рис. 1-11.

Рисунок 1-11. запрос авторизации macOS

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

Рисунок 1-12. Шифрование паролей MySQL 8.0.23

Последний шаг состоит из создания пароля root и инициализации MySQL, как показано на рисунке 1-13.

Рисунок 1-13. Корневой пароль MySQL 8.0.23

Теперь вы установили MySQL Server, но он не загружен (или не запущен) по умолчанию. Для начала откройте System Preferences и найдите значок MySQL, как показано на рис. 1-14.

Рисунок 1-14. MySQL в системных настройках

Щелкните значок, чтобы открыть панель MySQL. Вы должны увидеть что-то похожее на рисунок 1-15.

Рисунок 1-15. Варианты запуска MySQL

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

При работающей MySQL можно проверить соединение и убедиться, что MySQL Server работает правильно. Вы можете использовать MySQL Workbench, чтобы проверить это, или установить клиент MySQL, используя brew:

$ brew install mysql-client

После того, как вы установили клиент MySQL, вы можете подключиться с паролем, который вы определили на рис. 1-13. В терминале выполните следующую команду:

$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.23 MySQL Community Server - GPL

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners.

Type help; or \h for help. Type \c to clear the current input statement.

mysql> SELECT @@version;

+-----------+
| @@version |
+-----------+
| 8.0.23    |
+-----------+
1 row in set (0.00 sec)

Установка MySQL в Windows 10

Oracle предоставляет MySQL Installer for Windows, облегчающий установку. Обратите внимание, что MySQL Installer — это 32-разрядное приложение, но оно может устанавливать MySQL в 32-разрядных и 64-разрядных двоичных файлах. Чтобы начать процесс установки, вам необходимо запустить установочный файл и выбрать тип установки, как показано на рисунке 1-16.

Выберите тип установки Developer Default и нажмите Next. Мы не будем подробно останавливаться на других вариантах, потому что не рекомендуем использовать MySQL для производственных систем, главным образом потому, что экосистема MySQL разработана для Linux.

Рисунок 1-16. Настройка установки MySQL 8.0.23 Windows

Далее установщик проверяет, все ли требования выполнены (Рисунок 1-17).

Рисунок 1-17. Требования к установке

Щелкните Execute. Возможно, потребуется установить Microsoft Visual C++ (рис. 1-18).

Рисунок 1-18. Установите Microsoft Visual C++, если требуется

Нажмите Next, и программа установки покажет продукты, готовые к установке (Рисунок 1-19).

Рисунок 1-19. Нажмите Execute, чтобы установить программное обеспечение MySQL.

Нажмите Execute, и вы попадете на экран, где вы можете настроить свойства MySQL. Вы можете использовать параметры по умолчанию для TCP/IP и порта X-протокола, как показано на рис. 1-20, или настроить их по своему усмотрению.

Далее вы выберете метод аутентификации. Выберите более новую версию, которая является более безопасной, как показано на рисунке 1-21.

Рисунок 1-20. Тип и параметры конфигурации сети

Рисунок 1-21. Шифрование паролей — используйте пароли на основе SHA-256.

Затем укажите пароль пользователя root и укажите, хотите ли вы добавить дополнительных пользователей в базу данных MySQL, как показано на рисунке 1-22.

Рисунок 1-22. Настройка пользователей

Настроив пользователей, определите имя службы и пользователя, который будет запускать службу, как показано на рис. 1-23.

Рисунок 1-23. Настройка имени службы

Когда вы нажмете Next, программа установки начнет настройку MySQL. Как только установщик MySQL завершит свое выполнение, вы должны увидеть что-то вроде рис. 1-24.

Рисунок 1-24. Если установка прошла нормально, ошибок нет

Теперь ваш сервер базы данных работает. Поскольку вы выбрали профиль Developer, программа установки выполнит установку MySQL Router. MySQL Router не обязателен для этой установки, и, поскольку мы не рекомендуем Windows для производства, мы пропустим эту часть. Мы углубимся в детали маршрутизатора в MySQL Router.

Теперь вы можете проверить свой сервер с помощью MySQL Workbench, как показано на рисунке 1-25. Вы должны увидеть вариант подключения к MySQL.

Рисунок 1-25. Параметр подключения к MySQL в MySQL Workbench

Дважды щелкните соединение, и Workbench предложит вам ввести пароль, как показано на рисунке 1-26.

Рисунок 1-26. Введите пароль root для подключения

Теперь вы можете начать использовать MySQL на своей платформе Windows, как показано на рис. 1-27.

Рисунок 1-27. Теперь вы можете приступить к тестированию вашей среды

Содержимое каталога MySQL

В процессе установки MySQL создает все файлы, необходимые для запуска сервера. MySQL хранит свои файлы в каталоге, называемом каталогом данных (data directory). Администраторы баз данных (DBA) обычно называют это datadir, что является именем параметра MySQL, в котором хранится путь к этому каталогу. Расположение по умолчанию для дистрибутивов Linux — /var/lib/mysql. Вы можете проверить его местоположение, выполнив следующую команду в экземпляре MySQL:

mysql> SELECT @@datadir;
+-----------------+
| @@datadir       |
+-----------------+
| /var/lib/mysql/ |
+-----------------+
1 row in set (0.00 sec)

Файлы MySQL 5.7 по умолчанию

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

Файлы журнала REDO

MySQL создает файлы журнала повторов как ib_logfile0 и ib_logfile1 в каталоге данных. Она записывает в файлы журналов повторного выполнения по кругу, поэтому файлы не превышают размер своей конфигурации (опция innodb_log_file_size). Как и в любой другой системе управления реляционными базами данных (RDBMS), совместимой с ACID, файлы повторного выполнения необходимы для обеспечения устойчивости данных и возможности восстановления после аварийного сценария.

Файл auto.cnf

В MySQL 5.6 появился файл auto.cnf. Он имеет только один раздел [auto], содержащий один параметр и значение server_uuid. server_uuid создает уникальную подпись для сервера, и уровень репликации использует ее для связи с разными серверами для репликации данных.

Файлы *.pem

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

Подкаталог performance_schema

MySQL Performance Schema — это функция для мониторинга выполнения MySQL Server на низком уровне во время выполнения. Когда мы можем использовать Performance Schema для мониторинга конкретной метрики, мы говорим, что у MySQL есть инструменты. Например, инструменты Performance Schema могут отображать количество подключенных пользователей:

mysql> SELECT * FROM performance_schema.users;
+-----------------+---------------------+-------------------+
| USER            | CURRENT_CONNECTIONS | TOTAL_CONNECTIONS |
+-----------------+---------------------+-------------------+
| NULL            |                  40 |                46 |
| event_scheduler |                   1 |                 1 |
| root            |                   0 |                 1 |
| rsandbox        |                   2 |                 3 |
| msandbox        |                   1 |                 2 |
+-----------------+---------------------+-------------------+
5 rows in set (0.03 sec)

Хотя инструментарий существует со времен MySQL 5.6, именно в MySQL 5.7 он получил множество улучшений и стал фундаментальной частью инструментов DBA для исследования и устранения неполадок на уровне MySQL.

файл ibtmp1

Когда приложению необходимо создать временные таблицы или MySQL необходимо использовать внутреннюю временную таблицу на диске, MySQL создает их в общем временном табличном пространстве. Поведение по умолчанию заключается в создании автоматически расширяющегося файла данных с именем ibtmp1, размер которого немного превышает 12 МБ (его размер контролируется параметром innodb_temp_data_file_path).

Файл ibdata1

Файл ibdata1, вероятно, самый известный файл в экосистеме MySQL. Для MySQL 5.7 и старше он содержит данные для словаря данных InnoDB, буфера двойной записи, буфера изменений и журналов отмены. Он также может содержать данные таблицы и индекса, если мы отключим опцию innodb_file_per_table. Когда innodb_file_per_table включен, каждая пользовательская таблица имеет табличное пространство и выделенный файл. Обратите внимание, что в каталоге данных MySQL может быть несколько файлов ibdata.

Файл mysql.sock

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

Подкаталог mysql

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

# cd /var/lib/mysql
# ls -l mysql/

-rw-r-----. 1 vinicius.grippa percona   8820 Feb 20 15:51 columns_priv.frm
-rw-r-----. 1 vinicius.grippa percona      0 Feb 20 15:51 columns_priv.MYD
-rw-r-----. 1 vinicius.grippa percona   4096 Feb 20 15:51 columns_priv.MYI
-rw-r-----. 1 vinicius.grippa percona   9582 Feb 20 15:51 db.frm
-rw-r-----. 1 vinicius.grippa percona    976 Feb 20 15:51 db.MYD
-rw-r-----. 1 vinicius.grippa percona   5120 Feb 20 15:51 db.MYI
-rw-r-----. 1 vinicius.grippa percona     65 Feb 20 15:51 db.opt
-rw-r-----. 1 vinicius.grippa percona   8780 Feb 20 15:51 engine_cost.frm
-rw-r-----. 1 vinicius.grippa percona  98304 Feb 20 15:51 engine_cost.ibd
...
-rw-r-----. 1 vinicius.grippa percona  10816 Feb 20 15:51 user.frm
-rw-r-----. 1 vinicius.grippa percona   1292 Feb 20 15:51 user.MYD
-rw-r-----. 1 vinicius.grippa percona   4096 Feb 20 15:51 user.MYI

Файлы MySQL 8.0 по умолчанию

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

Файлы табличного пространства undo

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

Начиная с MySQL 8.0 файлы журнала отмены были отделены от системного табличного пространства (ibdata1) и помещены в каталог данных. Также можно установить другое местоположение, изменив параметр innodb_undo_directory.

Файлы .dblwr (представлены в версии 8.0.20)

Буфер двойной записи отвечает за запись страниц, сброшенных из пула буферов на диск, до того, как MySQL запишет страницы в файлы данных. Имена файлов двойной записи имеют следующий формат: #ib_<page_size>_<file_number>.dblwr (например, #ib_16384_0.dblwr, #ib_16384_0.dblwr). Расположение этих файлов можно изменить, изменив параметр innodb_doublewrite_dir.

Файл mysql.ibd (представлен в версии 8.0)

В MySQL 5.7 таблицы словарей и системные таблицы хранили данные и метаданные в каталоге mysql внутри каталога datadir. В MySQL 8.0 все это хранится в файле mysql.ibd и защищено механизмами InnoDB для обеспечения согласованности.

Использование интерфейса командной строки

Бинарный файл mysql представляет собой простую оболочку SQL с возможностью редактирования строки ввода. Его использование простое (мы уже использовали его несколько раз в процессе установки). Чтобы вызвать его, выполните следующую команду:

# mysql

Мы можем расширить его функциональность, выполнив в нем запросы:

# mysql -uroot -pseKret -e "SHOW ENGINE INNODB STATUS\G"

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

# mysql -e "SHOW MASTER STATUS\G" && nice -5 mysqldump \
    --all-databases --single-transaction -R --master-data=2 --flush-logs \
    --log-error=/tmp/donor.log --verbose=TRUE | ssh mysql@192.168.0.1 mysql \
    1> /tmp/receiver.log 2>&1

MySQL 8.0 представила MySQL Shell, который намного мощнее своего предшественника. MySQL Shell поддерживает языки JavaScript, Python и SQL, предоставляя возможности разработки и администрирования для MySQL Server. Подробнее об этом мы поговорим в разделе «MySQL Shell».

Использование Docker

С появлением виртуализации и ее популяризацией с помощью облачных сервисов появилось множество платформ, включая Docker. Созданный в 2013 году, Docker — это решение, которое предлагает портативный и гибкий способ развертывания программного обеспечения. Он обеспечивает изоляцию ресурсов за счет использования функций Linux, таких как контрольные группы (cgroups) и пространства имен ядра (kernel namespaces).

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

Установка Docker

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

Процесс установки Docker в чем-то похож на установку MySQL. Для Windows и macOS вы просто устанавливаете бинарные файлы, после чего сервис запускается и работает. Для операционных систем на базе Linux без графического интерфейса процесс требует настройки репозитория.

Установка Docker на CentOS 7

Пакеты CentOS для Docker, как правило, старше, чем пакеты, доступные для RHEL и в официальных репозиториях Docker. На момент написания статьи версия Docker, предоставляемая обычными репозиториями CentOS, была 1.13.1, а стабильная версия основной ветки — 20.10.3. Для целей этой книги нет никакой разницы, но мы всегда рекомендуем использовать последнюю версию для рабочих сред.

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

# yum install docker -y

Если вы хотите установить Docker из вышестоящего репозитория, чтобы убедиться, что вы используете последнюю версию, выполните следующие действия:

  1. Установите yum-utils, чтобы включить команду yum-config-manager:

    # yum install yum-utils -y
  2. Используйте yum-config-manager для добавления репозитория docker-ce:

    # yum-config-manager \
        --add-repo \
        https://download.docker.com/linux/centos/docker-ce.repo
    
  3. Установите необходимые пакеты:

    # yum install docker-ce docker-ce-cli containerd.io -y
  4. Запустите службу Docker:

    # systemctl start docker
  5. Включите службу Docker для автоматического запуска после перезагрузки системы:

    # systemctl enable --now docker
  6. Чтобы проверить, запущена ли служба Docker, выполните команду systemctl status:

    # systemctl status docker
  7. Чтобы убедиться, что Docker Engine установлен правильно, вы можете запустить контейнер hello-world:

    # docker run hello-world
Установка Docker на Ubuntu 20.04 (Focal Fossa)

Чтобы установить последнюю версию Docker из вышестоящего репозитория, сначала удалите все старые версии Docker (называемые docker, docker.io или docker-engine). Это можно сделать с помощью команды:

# apt-get remove -y docker docker-engine docker.io containerd runc

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

  1. Убедитесь, что Ubuntu обновлена с помощью этой команды:

  2. # apt-get update -y
  3. Установите пакеты, чтобы разрешить apt использовать репозиторий через HTTPS:

  4. # apt-get install -y \
        apt-transport-https \
        ca-certificates \
        curl \
        gnupg-agent \
        software-properties-common
    
  5. Затем добавьте официальный GPG-ключ Docker:

  6. # curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo \
        apt-key add -
    
  7. Имея ключ, добавьте стабильный репозиторий Docker:

  8. # add-apt-repository \
        "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
        $(lsb_release -cs) \
        stable"
    
  9. Теперь используйте команду apt для установки пакетов Docker:

  10. # apt-get install -y docker-ce docker-ce-cli containerd.io
  11. Ubuntu запустит службу для вас, но вы можете проверить это, выполнив эту команду:

  12. # systemctl status docker
  13. Чтобы служба Docker автоматически запускалась при перезагрузке ОС, используйте:

  14. # systemctl enable --now docker
  15. Проверьте версию Docker, которую вы установили:

  16. # docker --version
  17. Чтобы убедиться, что Docker Engine установлен правильно, вы можете запустить контейнер hello-world:

  18. # docker run hello-world
Развертывание контейнера MySQL

После того, как вы установили и запустили Docker Engine, следующим шагом будет развертывание контейнера MySQL Docker.

Чтобы развернуть последнюю версию MySQL с помощью Docker, выполните следующую команду:

# docker run --name mysql-latest \
  -p 3306:3306 -p 33060:33060 \
  -e MYSQL_ROOT_HOST=% \
  -e MYSQL_ROOT_PASSWORD='learning_mysql' \
  -d mysql/mysql-server:latest

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

Затем подключитесь к контейнеру MySQL с помощью клиента MySQL:

# docker exec -it mysql-latest mysql -uroot -plearning_mysql

Поскольку вы сопоставили TCP-порт 3306 в контейнере с портом 3306 на хосте Docker с параметром -p 3306:3306, вы можете подключиться к базе данных MySQL из любого доступного клиента MySQL (Workbench, MySQL Shell), который может подключаться к хосту (hostname или IP) и этот порт.

Давайте рассмотрим несколько команд для управления контейнером.

Чтобы остановить контейнер MySQL Docker, запустите:

# docker stop mysql-latest

Не пытайтесь использовать docker run для повторного запуска контейнера. Вместо этого используйте:

# docker start mysql-latest

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

# docker logs mysql-latest

Чтобы удалить созданный вами контейнер Docker, запустите:

# docker stop mysql-latest
# docker rm mysql-latest

Чтобы проверить, какие и сколько контейнеров Docker запущены на хосте, используйте:

# docker ps

Можно настроить параметризацию MySQL с помощью параметров командной строки для Docker Engine. Чтобы настроить размер буферного пула InnoDB и метод flush, выполните следующее:

# docker run --name mysql-latest \
  -p 3306:3306 -p 33060:33060 \
  -e MYSQL_ROOT_HOST=% \
  -e MYSQL_ROOT_PASSWORD='strongpassword' \
  -d mysql/mysql-server:latest \
  --innodb_buffer_pool_size=256M \
  --innodb_flush_method=O_DIRECT

Чтобы запустить версию MySQL, отличную от последней версии, сначала убедитесь, что она доступна в Docker Hub. Например, предположим, что вы хотите запустить MySQL 5.7.31. Первый шаг — проверить официальный список образов MySQL Docker в Docker Hub, чтобы узнать, существует ли он.

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

# docker run --name mysql-5.7.31 \
  -p 3307:3306 -p 33061:33060 \
  -e MYSQL_ROOT_HOST=% \
  -e MYSQL_ROOT_PASSWORD='learning_mysql' \
  -d mysql/mysql-server:5.7.31

Можно одновременно запускать несколько экземпляров MySQL Docker, но потенциальной проблемой являются конфликты TCP-портов. Обратите внимание, что в предыдущем примере мы сопоставили разные порты хоста для контейнера mysql-5.7.31 (3307 и 33061). Кроме того, имя контейнера должно быть уникальным.

Развертывание контейнеров MariaDB и Percona Server

Выполните те же действия, что и в предыдущем разделе, для развертывания контейнера MySQL для развертывания контейнера MariaDB или Percona Server. Основное отличие состоит в том, что они используют разные образы Docker и имеют собственные официальные репозитории.

Чтобы развернуть контейнер MariaDB, запустите:

# docker run --name maria-latest \
  -p 3308:3306 \
  -e MYSQL_ROOT_HOST=% \
  -e MYSQL_ROOT_PASSWORD='learning_mysql' \
  -d mariadb:latest

А для Percona Server запустите:

# docker run --name ps-latest \
  -p 3309:3306 -p 33063:33060 \
  -e MYSQL_ROOT_HOST=% \
  -e MYSQL_ROOT_PASSWORD='learning_mysql' \
  -d percona/percona-server:latest \
  --innodb_buffer_pool_size=256M \
  --innodb_flush_method=O_DIRECT

Использование песочниц

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

В 2018 году Джузеппе Максиа представил DBdeployer, инструмент, который обеспечивает простой и быстрый способ развертывания MySQL и его форков. Он поддерживает различные топологии MySQL, такие как master/slave (source/replica), master/master (source/source), Galera Cluster и Group Replication.

Установка DBdeployer

Инструмент разработан на языке Go и работает с macOS и Linux (Ubuntu и CentOS), а также предоставляются автономные исполняемые файлы. Получить последнюю версию здесь:

# wget https://github.com/datacharmer/dbdeployer/releases/download/v1.58.2/ dbdeployer-1.58.2.linux.tar.gz
# tar -xvf dbdeployer-1.58.2.linux.tar.gz
# mv dbdeployer-1.58.2.linux /usr/local/bin/dbdeployer

Если у вас есть каталог /usr/local/bin/ в переменной $PATH, то теперь вы сможете запускать команды dbdeployer:

# dbdeployer --version
dbdeployer version 1.58.2

Использование DBdeployer

Первым шагом в использовании DBdeployer является загрузка двоичного файла MySQL, который вы хотите запустить, и его распаковка в каталог, где вы храните свои двоичные файлы. Мы будем использовать Linux — Generic tarballs, поскольку они совместимы с большинством дистрибутивов Linux, и мы будем хранить наши двоичные файлы в каталоге /opt/mysql:

# wget https://dev.mysql.com/get/Downloads/MySQL-8.0/ \
    mysql-8.0.11-linux-glibc2.12-x86_64.tar.gz
# mkdir /opt/mysql
# dbdeployer --sandbox-binary=/opt/mysql/ unpack \
    mysql-8.0.11-linux-glibc2.12-x86_64.tar.gz

Команда unpack извлечет и переместит файлы в указанный каталог. Ожидаемый результат этой операции:

# dbdeployer --sandbox-binary=/opt/mysql/ unpack

mysql-8.0.11-linux-glibc2.12-x86_64.tar.gz
Unpacking tarball mysql-8.0.11-linux-glibc2.12-x86_64.tar.gz to /opt/mysql/8.0.11
.........100.........200........289
Renaming directory /opt/mysql/mysql-8.0.11-linux-glibc2.12-x86_64 to /opt/mysql/8.0.11

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

# dbdeployer --sandbox-binary=/opt/mysql/ deploy single 8.0.11

И мы можем наблюдать, как DBdeployer инициализирует MySQL:

# dbdeployer --sandbox-binary=/opt/mysql/ deploy single 8.0.11

Creating directory /root/sandboxes
Database installed in $HOME/sandboxes/msb_8_0_11
run 'dbdeployer usage single' for basic instructions'
. sandbox server started

Подтвердите, что MySQL работает командой ps:

# ps -ef | grep mysql

root   4249     1 0 20:18 pts/0   00:00:00 /bin/sh bin/mysqld_safe
--defaults-file=/root/sandboxes/msb_8_0_11/my.sandbox.cnf
root   4470  4249 1 20:18 pts/0   00:00:00 /opt/mysql/8.0.11/bin/mysqld
--defaults-file=/root/sandboxes/msb_8_0_11/my.sandbox.cnf
--basedir=/opt/mysql/8.0.11 --datadir=/root/sandboxes/msb_8_0_11/data
--plugin-dir=/opt/mysql/8.0.11/lib/plugin --user=root
--log-error=/root/sandboxes/msb_8_0_11/data/msandbox.err
--pid-file=/root/sandboxes/msb_8_0_11/data/mysql_sandbox8011.pid
--socket=/tmp/mysql_sandbox8011.sock --port=8011
root   4527  3836 0 20:18 pts/0   00:00:00 grep --color=auto mysql

Теперь мы можем подключиться к MySQL с помощью DBdeployer команды use:

# cd sandboxes/msb_8_0_11/
# ./use

или используя учетные данные root по умолчанию:

# mysql -uroot -pmsandbox -h 127.0.0.1 -P 8011

Если мы хотим настроить среду репликации с топологией source/replica, мы можем сделать это с помощью следующей командной строки:

# dbdeployer --sandbox-binary=/opt/mysql/ deploy replication 8.0.11

И у нас будет запущено три процесса mysqld:

# ps -ef | grep mysql
root     4673     1 0 20:26 pts/0   00:00:00 /bin/sh bin/mysqld_safe
--defaults-file=/root/sandboxes/rsandbox_8_0_11/master/my.sandbox.cnf
root     4942  4673 1 20:26 pts/0   00:00:00 /opt/mysql/8.0.11/bin/mysqld
...
--pid-file=/root/sandboxes/rsandbox_8_0_11/master/data/mysql_sandbox201
12.pid --socket=/tmp/mysql_sandbox20112.sock --port=20112

root     5051     1 0 20:26 pts/0   00:00:00 /bin/sh bin/mysqld_safe
--defaults-file=/root/sandboxes/rsandbox_8_0_11/node1/my.sandbox.cnf
root     5320  5051 1 20:26 pts/0   00:00:00 /opt/mysql/8.0.11/bin/mysqld
--defaults-file=/root/sandboxes/rsandbox_8_0_11/node1/my.sandbox.cnf
...
--pid-file=/root/sandboxes/rsandbox_8_0_11/node1/data/mysql_sandbox2011
3.pid --socket=/tmp/mysql_sandbox20113.sock --port=20113

root     5415     1 0 20:26 pts/0   00:00:00 /bin/sh bin/mysqld_safe
--defaults-file=/root/sandboxes/rsandbox_8_0_11/node2/my.sandbox.cnf
root     5684  5415 1 20:26 pts/0   00:00:00 /opt/mysql/8.0.11/bin/mysqld
...
--pid-file=/root/sandboxes/rsandbox_8_0_11/node2/data/mysql_sandbox2011
4.pid --socket=/tmp/mysql_sandbox20114.sock --port=20114

Еще одна топология, которую может настроить DBdeployer, — это Group Replication. В этом примере мы определим base-port. Сделав это, мы прикажем DBdeployer настроить наши серверы, начиная с порта 49007:

# dbdeployer deploy --topology=group replication --sandbox-binary=/opt/mysql/\
    8.0.11 --base-port=49007

Теперь давайте посмотрим на пример развертывания Galera Cluster с использованием Percona XtraDB Cluster 5.7.32. Мы укажем base-port и хотим, чтобы наши узлы были настроены с опцией log-slave-updates:

# wget https://downloads.percona.com/downloads/Percona-XtraDB-Cluster-57/\
    Percona-XtraDB-Cluster-5.7.32-31.47/binary/tarball/Percona-XtraDB-Cluster-\
    5.7.32-rel35-47.1.Linux.x86_64.glibc2.17-debug.tar.gz
# dbdeployer --sandbox-binary=/opt/mysql/ unpack\
    Percona-XtraDB-Cluster-5.7.32-rel35-47.1.Linux.x86_64.glibc2.17-debug.tar.gz
# dbdeployer deploy --topology=pxc replication\
    --sandbox-binary=/opt/mysql/ 5.7.32 --base-port=45007 -c log-slave-updates

Как мы видели, параметры MySQL можно настраивать. Одним из интересных вариантов является включение репликации MySQL с использованием глобальных идентификаторов транзакций (global transaction identifiers) или GTID (мы более подробно обсудим GTID в главе 13):

# dbdeployer deploy replication --sandbox-binary=/opt/mysql/ 5.7.32 --gtid

Наш последний пример показывает, что можно одновременно развернуть несколько автономных версий — здесь мы создаем пять автономных экземпляров:

# dbdeployer deploy multiple --sandbox-binary=/opt/mysql/ 5.7.32 -n 5

Предыдущие примеры — это лишь небольшая часть возможностей DBdeployer. Полная документация доступна на GitHub. Другой вариант понять вселенную возможностей — использовать --help в командной строке:

# dbdeployer --help

dbdeployer makes MySQL server installation an easy task.
Runs single, multiple, and replicated sandboxes.

Usage:
  dbdeployer [command]

Available Commands:
  admin           sandbox management tasks
  cookbook        Shows dbdeployer samples
  defaults        tasks related to dbdeployer defaults
  delete          delete an installed sandbox
  delete-binaries delete an expanded tarball
  deploy          deploy sandboxes
  downloads       Manages remote tarballs
  export          Exports the command structure in JSON format
  global          Runs a given command in every sandbox
  help            Help about any command
  import          imports one or more MySQL servers into a sandbox
  info            Shows information about dbdeployer environment samples
  sandboxes       List installed sandboxes
  unpack          unpack a tarball into the binary directory
  update          Gets dbdeployer newest version
  usage           Shows usage of installed sandboxes
  versions        List available versions

Flags:
      --config string          configuration file (default "/root/.dbdeployer/config.json")
  -h, --help                   help for dbdeployer
      --sandbox-binary string  Binary repository (default "/root/opt/mysql")
      --sandbox-home string    Sandbox deployment directory (default "/root/sandboxes")
      --shell-path string      Which shell to use for generated scripts (default "/usr/bin/bash")
      --skip-library-check     Skip check for needed libraries (may cause nasty errors)
      --version                version for dbdeployer

Use "dbdeployer [command] --help" for more information about a command.

Обновление сервера MySQL

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

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

Обновление на месте

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

Логическое обновление

Это включает в себя экспорт данных в формате SQL из старой версии MySQL с использованием утилиты резервного копирования или экспорта, такой как mysqldump или mysqlpump, установку новой версии MySQL и применение данных SQL к новой версии MySQL. Другими словами, этот процесс включает перестройку всего словаря данных и пользовательских данных. Логическое обновление обычно занимает больше времени, чем обновление на месте.

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

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

Давайте рассмотрим пример обновления с исходной версии MySQL 5.7 до восходящей версии MySQL 8.0 с использованием метода на месте:

  1. Остановите службу MySQL. Выполните полное завершение работы с помощью systemctl:

    # systemctl stop mysqld
  2. Удалите старые бинарники:

    # yum erase mysql-community -y

    Этот процесс удаляет только двоичные файлы и не затрагивает datadir (см. «Содержание каталога MySQL»).

  3. Следуйте обычным шагам процесса установки (см. «Установка MySQL в Linux»). Например, чтобы использовать версию сообщества MySQL 8.0 в CentOS 7 с помощью yum:

    # yum-config-manager --enable mysql80-community
  4. Установите новые бинарники:

    # yum install mysql-community-server -y
  5. Запустите службу MySQL:

    # systemctl start mysqld

В журналах мы можем наблюдать, что MySQL обновил словарь данных и теперь мы используем MySQL 8.0.21:

# tail -f /var/log/mysqld.log
2020-08-09T21:20:10.356938Z 2 [System] [MY-011003] [Server] Finished populating Data Dictionary tables with data.
2020-08-09T21:20:11.734091Z 5 [System] [MY-013381] [Server] Server upgrade from '50700' to '80021' started.
2020-08-09T21:20:17.342682Z 5 [System] [MY-013381] [Server] Server upgrade from '50700' to '80021' completed.
...
2020-08-09T21:20:17.463685Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.21' socket: '/var/lib/mysql/mysql.sock' port: 3306 MySQL Community Server - GPL.

Распространенный вопрос заключается в том, безопасно ли обновляться до последней мажорной версии. Ответ… это зависит от многого. Как и в случае с любым новым продуктом в отрасли, первые пользователи, как правило, получают выгоду от новых функций, но они также являются тестировщиками, и они могут обнаружить новые ошибки и пострадать от них. Когда был выпущен MySQL 8.0, мы рекомендовали дождаться трех второстепенных выпусков, прежде чем рассматривать возможность перехода. Золотое правило этой книги — все тестировать заранее, прежде чем переходить к следующему шагу. Если вы узнаете об этом из этой книги, мы будем считать, что наша миссия выполнена.

Глава 2
Моделирование и проектирование баз данных

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

Как не стоит разрабатывать базу данных

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

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

+------------+---------+-----------------------+------+
| GivenNames | Surname | CourseName            | Pctg |
+------------+---------+-----------------------+------+
| John Paul  | Bloggs  | Data Science          |   72 |
| Sarah      | Doe     | Programming 1         |   87 |
| John Paul  | Bloggs  | Computing Mathematics |   43 |
| John Paul  | Bloggs  | Computing Mathematics |   65 |
| Sarah      | Doe     | Data Science          |   65 |
| Susan      | Smith   | Computing Mathematics |   75 |
| Susan      | Smith   | Programming 1         |   55 |
| Susan      | Smith   | Computing Mathematics |   80 |
+------------+---------+-----------------------+------+

Список красивый и компактный, мы можем легко получить доступ к оценкам для любого студента или любого курса, и он похож на электронную таблицу. Однако у нас может быть более одного ученика с одним и тем же именем. Например, в выборке данных есть две записи для Сьюзен Смит и курса вычислительной математики. Какая Сьюзен Смит набрала 75%, а какая 80%? Обычный способ отличить повторяющиеся записи данных состоит в том, чтобы присвоить каждой записи уникальный номер. Здесь мы можем присвоить уникальный номер StudentID каждому студенту:

+------------+------------+---------+-----------------------+------+
| StudentID  | GivenNames | Surname | CourseName            | Pctg |
+------------+------------+---------+-----------------------+------+
| 12345678   | John Paul  | Bloggs  | Data Science          |   72 |
| 12345121   | Sarah      | Doe     | Programming 1         |   87 |
| 12345678   | John Paul  | Bloggs  | Computing Mathematics |   43 |
| 12345678   | John Paul  | Bloggs  | Computing Mathematics |   65 |
| 12345121   | Sarah      | Doe     | Data Science          |   65 |
| 12345876   | Susan      | Smith   | Computing Mathematics |   75 |
| 12345876   | Susan      | Smith   | Programming 1         |   55 |
| 12345303   | Susan      | Smith   | Computing Mathematics |   80 |
+------------+------------+---------+-----------------------+------+

Теперь мы знаем, какая Сьюзен Смит набрала 80%: та, у которой студенческий билет 12345303.

Есть еще одна проблема. В нашей таблице Джон Пол Блоггс имеет две оценки по курсу вычислительной математики: он провалил его один раз с 43%, а затем сдал его с 65% со второй попытки. В реляционной базе данных строки образуют набор, и между ними нет неявного порядка. Глядя на эту таблицу, мы можем предположить, что проход произошел после сбоя, но мы не можем быть уверены. Нет никакой гарантии, что новая оценка появится после старой, поэтому нам нужно добавить информацию о том, когда была выставлена каждая оценка, скажем, добавив год (Year) и семестр (Sem):

+------------+------------+---------+-----------------------+------+-----+------+
| StudentID  | GivenNames | Surname | CourseName            | Year | Sem | Pctg |
+------------+------------+---------+-----------------------+------+-----+------+
| 12345678   | John Paul  | Bloggs  | Data Science          | 2019 |   2 |   72 |
| 12345121   | Sarah      | Doe     | Programming 1         | 2020 |   1 |   87 |
| 12345678   | John Paul  | Bloggs  | Computing Mathematics | 2019 |   2 |   43 |
| 12345678   | John Paul  | Bloggs  | Computing Mathematics | 2020 |   1 |   65 |
| 12345121   | Sarah      | Doe     | Data Science          | 2020 |   1 |   65 |
| 12345876   | Susan      | Smith   | Computing Mathematics | 2019 |   1 |   75 |
| 12345876   | Susan      | Smith   | Programming 1         | 2019 |   2 |   55 |
| 12345303   | Susan      | Smith   | Computing Mathematics | 2020 |   1 |   80 |
+------------+------------+---------+-----------------------+------+-----+------+

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

+------------+------------+---------+
| StudentID  | GivenNames | Surname |
+------------+------------+---------+
| 12345121   | Sarah      | Doe     |
| 12345303   | Susan      | Smith   |
| 12345678   | John Paul  | Bloggs  |
| 12345876   | Susan      | Smith   |
+------------+------------+---------+

И мы могли бы хранить меньше информации в таблице Student_Grades:

+------------+-----------------------+------+-----+------+
| StudentID  | CourseName            | Year | Sem | Pctg |
+------------+-----------------------+------+-----+------+
| 12345678   | Data Science          | 2019 |   2 |   72 |
| 12345121   | Programming 1         | 2020 |   1 |   87 |
| 12345678   | Computing Mathematics | 2019 |   2 |   43 |
| 12345678   | Computing Mathematics | 2020 |   1 |   65 |
| 12345121   | Data Science          | 2020 |   1 |   65 |
| 12345876   | Computing Mathematics | 2019 |   1 |   75 |
| 12345876   | Programming 1         | 2019 |   2 |   55 |
| 12345303   | Computing Mathematics | 2020 |   1 |   80 |
+------------+-----------------------+------+-----+------+

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

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

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

Процесс проектирования базы данных

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

Анализ требований

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

Концептуальный дизайн

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

Логичный дизайн

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

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

Модель отношений сущностей

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

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

Представление сущностей

Чтобы помочь визуализировать дизайн, подход к моделированию ER включает в себя рисование ER-диаграммы. На диаграмме ER мы представляем сущность, заданную прямоугольником, содержащим имя сущности. В нашем примере с базой данных продаж наша ER-диаграмма будет отображать наборы сущностей продуктов и клиентов, как показано на рис. 2-1.

Рисунок 2-1. Набор сущностей представлен именованным прямоугольником

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

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

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

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

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

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

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

На диаграмме ER атрибуты представлены в виде помеченных овалов, связанных с их сущностью, как показано на рис. 2-2. Атрибуты, составляющие первичный ключ, показаны подчеркнутыми. Части любых составных атрибутов нарисованы соединенными с овалом составного атрибута, а многозначные атрибуты показаны как овалы с двойной линией.

Рисунок 2-2. Представление диаграммы ER сущности клиента

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

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

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

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

Для некоторых приложений никакая комбинация атрибутов не может однозначно идентифицировать объект (или было бы слишком громоздко использовать большой составной ключ), поэтому мы создаем искусственный атрибут, который определяется как уникальный и поэтому может использоваться в качестве ключа: номера учащихся, номера социального страхования, номера водительских прав и номера библиотечных карт — это примеры уникальных атрибутов, созданных для различных приложений. В нашем приложении для инвентаризации и продаж мы можем хранить разные товары с одинаковыми названиями и ценами. Например, мы могли бы продать две модели «Four-Port USB 2.0 Hub» по цене 4,95 доллара каждая. Чтобы различать продукты, мы можем присвоить каждому товару уникальный идентификационный номер; это будет первичный ключ. Каждый объект продукта будет иметь атрибуты имени, цены и идентификатора продукта. Это показано на диаграмме ER на рис. 2-3.

Рисунок 2-3. Представление диаграммы ER сущности продукта

Представление отношений

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

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

Затем наша база данных могла бы записывать каждую продажу и сообщать нам, например, что в 15:13 в среду, 22 марта, Маркос Альбе купил один «Raspberry Pi 4», один «SSD M.2 NVMe на 500 ГБ» и два комплекта «2000 Watt 5.1 Channel Sub-Woofer Speakers».

На каждой стороне отношений может появляться разное количество сущностей. Например, каждый клиент может купить любое количество продуктов, и каждый продукт может быть куплен любым количеством клиентов. Это известно как отношение многие ко многим (many-to-many). Мы также можем иметь отношения один ко многим (one-to-many). Например, у одного человека может быть несколько кредитных карт, но каждая кредитная карта принадлежит только одному человеку. Глядя на это с другой стороны, отношение один ко многим становится отношением многие к одному; например, многие кредитные карты принадлежат одному человеку. Наконец, серийный номер на автомобильном двигателе является примером отношения один к одному (one-to-one); каждый двигатель имеет только один серийный номер, и каждый серийный номер принадлежит только одному двигателю. Мы используем сокращенные термины 1:1, 1:N и M:N для отношений «один к одному», «один ко многим» и «многие ко многим».

Количество сущностей по обе стороны отношения (мощность (cardinality) отношения) определяет ключевые ограничения (key constraints) отношения. Важно тщательно продумать кардинальность (или мощность) отношений. Существует множество взаимосвязей, которые сначала могут показаться однозначными, но на деле оказываются более сложными. Например, люди иногда меняют свои имена; в некоторых приложениях, таких как полицейские базы данных, это представляет особый интерес, поэтому может возникнуть необходимость в моделировании отношения «многие ко многим» между сущностью-лицом и сущностью-именем. Перепроектирование базы данных может быть дорогостоящим и трудоемким, если вы предполагаете, что связь проще, чем она есть на самом деле.

На диаграмме ER мы представляем набор отношений с именованным ромбом. Мощность отношений часто указывается рядом с ромбом отношений; это стиль, который мы используем в этой книге. (Еще один распространенный стиль — это наличие стрелки на линии, соединяющей сущность со стороны «1» с ромбом отношения.) Рисунок 2-4 показывает взаимосвязь между объектами customer и product, а также атрибуты number и timestamp взаимосвязи sale.

Рисунок 2-4. Представление диаграммы ER сущностей клиента и продукта, а также отношения продажи между ними.

Частичное и полное участие

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

Сущность или атрибут?

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

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

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

Есть ли у предмета собственные компоненты?

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

Может ли объект иметь несколько экземпляров?

Если это так, мы должны найти способ хранить данные о каждом экземпляре. Самый простой способ сделать это — представить объект как отдельный объект. В нашем примере с продажами мы должны спросить, разрешено ли клиентам иметь более одного адреса электронной почты; если это так, мы должны смоделировать адрес электронной почты как отдельный объект.

Объект часто не существует или неизвестен?

Если это так, то это фактически атрибут только некоторых объектов, и было бы лучше моделировать его как отдельный объект, а не как атрибут, который часто бывает пустым. Рассмотрим простой пример: чтобы хранить оценки студентов по разным курсам, мы могли бы иметь атрибут для оценки студента по каждому возможному курсу, как показано на рис. 2-5. Но поскольку большинство учащихся получат оценки только по некоторым из этих курсов, лучше представить оценки в виде отдельного набора сущностей, как показано на рис. 2-6.

Рисунок 2-5. Представление на диаграмме ER оценок учащихся как атрибутов сущности ученика

Рисунок 2-6. Представление на диаграмме ER оценок учащихся как отдельного объекта

Сущность или отношения?

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

Промежуточные сущности

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

Рассмотрим такое утверждение: «Пассажир может забронировать место на рейс». Это отношение «многие ко многим» между сущностями «пассажир» и «рейс». Соответствующий фрагмент диаграммы ER показан на рисунке 2-7.

Рисунок 2-7. Пассажир участвует в отношениях M:N с рейсом

Однако давайте посмотрим на это с обеих сторон отношений:

Следовательно, мы можем рассматривать отношения «многие ко многим» как фактически два отношения «один ко многим», по одному в каждом направлении. Это указывает нам на существование скрытой промежуточной сущности, бронирования, между сущностями рейса и пассажира. Требование можно было бы лучше сформулировать так: «Пассажир может забронировать место на рейсе». Обновленный фрагмент ER-диаграммы показан на рис. 2-8.

Рисунок 2-8. Промежуточный объект бронирования между объектами пассажира и рейса

Каждый пассажир может участвовать в нескольких бронированиях, но каждое бронирование принадлежит одному пассажиру, поэтому кардинальность этого отношения равна 1:N. Точно так же может быть много бронирований для данного рейса, но каждое бронирование относится к одному рейсу, поэтому это отношение также имеет кардинальность 1:N. Поскольку каждое бронирование должно быть связано с конкретным пассажиром и рейсом, организация, осуществляющая бронирование, полностью участвует во взаимоотношениях с этими организациями (как описано в разделе «Частичное и полное участие»). Это общее участие не может быть эффективно отражено в представлении на рис. 2-7.

Слабые и сильные сущности

Контекст очень важен в нашем повседневном общении; если мы знаем контекст, мы можем работать с гораздо меньшим количеством информации. Например, мы обычно называем членов семьи только по имени или прозвищу. Там, где существует двусмысленность, мы добавляем дополнительную информацию, например фамилию, чтобы прояснить наши намерения. При проектировании базы данных мы можем опустить некоторую ключевую информацию для сущностей, которые зависят от других сущностей. Например, если бы мы хотели сохранить имена дочерних объектов наших клиентов, мы могли бы создать дочернюю сущность и хранить только достаточно ключевой информации, чтобы идентифицировать ее в контексте ее родителя. Мы могли бы просто указать имя ребенка, исходя из предположения, что у клиента никогда не будет нескольких детей с одинаковым именем. Здесь дочерняя сущность является слабой (weak) сущностью, и ее связь с сущностью клиента называется идентифицирующей связью (identifying relationship). Слабые сущности полностью участвуют в идентифицирующей связи, поскольку они не могут существовать в базе данных независимо от сущности, которой они принадлежат.

На диаграмме ER мы показываем слабые объекты и идентифицирующие отношения двойными линиями, а частичный ключ слабого объекта — пунктирным подчеркиванием, как на рис. 2-9. Слабый объект однозначно идентифицируется в контексте принадлежащего ему (или сильного (strong))) объекта, поэтому полный ключ слабого объекта представляет собой комбинацию его собственного (частичного) ключа с ключом объекта-владельца. Чтобы однозначно идентифицировать ребенка в нашем примере, нам нужно имя ребенка и адрес электронной почты родителя ребенка.

Рисунок 2-9. Представление диаграммы ER слабой сущности

На рис. 2.10 показана сводка символов, которые мы объяснили для ER-диаграмм.

Рисунок 2-10. Краткое изложение символов диаграммы ER

Нормализация базы данных

Нормализация базы данных — важная концепция при разработке реляционной структуры данных. Доктор Эдгар Ф. Кодд, изобретатель модели реляционной базы данных, предложил нормальные формы в начале 70-х, и они до сих пор широко используются в отрасли. Даже с появлением баз данных NoSQL в краткосрочной или среднесрочной перспективе нет никаких признаков того, что реляционные базы данных исчезнут или что обычные формы выйдут из употребления.

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

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

В реальных рабочих нагрузках обычно возникают проблемы с производительностью. Это одна из причин существования заданий извлечения, преобразования, загрузки (extract, transform, load) (ETL): они денормализуют данные для их обработки.

Рассмотрим первые три нормальные формы:

Первая нормальная форма (1NF) преследует следующие цели
  • Удалить повторяющиеся группы в отдельных таблицах.

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

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

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

Целями второй нормальной формы (2NF) являются
  • Создать отдельные таблицы для наборов значений, которые применяются к нескольким записям.

  • Связать эти таблицы с помощью внешнего ключа.

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

Третья нормальная форма (3NF) добавляет еще одну цель
  • Исключить поля, не зависящие от ключа.

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

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

Таблица 2-1. Нормальные формы (от наименее до наиболее нормализованных)
UNF
(1970)
1NF
(1970)
2NF
(1971)
3NF
(1971)
4NF
(1977)
5NF
(1979)
6NF
(2003)
Первичный ключ (без повторяющихся кортежей) Может быть Да Да Да Да Да Да
Нет повторяющихся групп Нет Да Да Да Да Да Да
Атомарные столбцы (ячейки имеют одно значение) Нет Да Да Да Да Да Да
Каждая нетривиальная функциональная зависимость либо не начинается с надлежащего подмножества ключа-кандидата, либо заканчивается первичным атрибутом (никаких частичных функциональных зависимостей непервичных атрибутов от ключей-кандидатов). Нет Нет Да Да Да Да Да
Каждая нетривиальная функциональная зависимость начинается с суперключа или заканчивается первичным атрибутом (без транзитивных функциональных зависимостей непервичных атрибутов от ключей-кандидатов). Нет Нет Нет Да Да Да Да
Каждая нетривиальная функциональная зависимость либо начинается с суперключа, либо заканчивается элементарным простым атрибутом. Нет Нет Нет Нет Да Да Н/Д
Каждая нетривиальная функциональная зависимость начинается с суперключа Нет Нет Нет Нет Да Да Н/Д
Каждая нетривиальная многозначная зависимость начинается с суперключа Нет Нет Нет Нет Да Да Н/Д
Каждая зависимость соединения имеет компонент суперключа Нет Нет Нет Нет Нет Да Н/Д
Каждая зависимость соединения имеет только суперключевые компоненты. Нет Нет Нет Нет Нет Да Н/Д
Каждое ограничение является следствием ограничений предметной области и ключевых ограничений. Нет Нет Нет Нет Нет Нет Н/Д
Каждая зависимость соединения тривиальна Нет Нет Нет Нет Нет Нет Да

Нормализация таблицы примеров

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

Начнем с ненормализованной таблицы:

Student#   Advisor   Adv-Room   Class1   Class2   Class3
1022       Jones     412        101-07   143-01   159-02
4123       Smith     216        201-01   211-02   214-01

Первая нормальная форма: нет повторяющихся групп

Таблицы должны иметь только одно поле для каждого атрибута. Так как у одного студента несколько классов, эти классы должны быть указаны в отдельной таблице. Поля Class1, Class2 и Class3 в нашей ненормализованной таблице указывают на проблемы с дизайном.

Электронные таблицы часто имеют несколько полей для одного и того же атрибута (например, address1, address2, address3), а таблицы — нет. Вот еще один способ взглянуть на эту проблему: при отношении «один ко многим» не помещайте одну сторону и многие стороны в одну и ту же таблицу. Вместо этого создайте другую таблицу в первой нормальной форме, удалив повторяющуюся группу, например, с Class#, как показано здесь:

Student#   Advisor   Adv-Room   Class#
1022       Jones     412        101-07
1022       Jones     412        143-01
1022       Jones     412        159-02
4123       Smith     216        201-01
4123       Smith     216        211-02
4123       Smith     216        214-01

Вторая нормальная форма: устранение избыточных данных

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

Следующие две таблицы демонстрируют преобразование во вторую нормальную форму. Теперь у нас есть таблица Students:

Student#   Advisor   Adv-Room
1022       Jones     412
4123       Smith     216

и таблица Registration:

Student#   Class#
1022       101-07
1022       143-01
1022       159-02
4123       201-01
4123       211-02
4123       214-01

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

В предыдущем примере Adv-Room (номер офиса консультанта) функционально зависит от атрибута Advisor. Решение состоит в том, чтобы переместить этот атрибут из таблицы Students в таблицу Faculty, как показано ниже.

Теперь таблица Students выглядит так:

Student#   Advisor
1022       Jones
4123       Smith

А вот и таблица Faculty:


Name  Room Dept
Jones 412  42
Smith 216  42

Примеры моделирования отношений сущностей

В предыдущих разделах мы рассмотрели гипотетические примеры, чтобы помочь вам понять основы проектирования баз данных, диаграмм ER и нормализации. Теперь мы рассмотрим несколько примеров ER из демонстрационных баз данных, доступных для MySQL. Для визуализации диаграмм ER мы будем использовать MySQL Workbench.

MySQL Workbench использует физическое представление ER. Модели физических диаграмм ER более детализированы и показывают процессы, необходимые для добавления информации в базу данных. Вместо использования символов мы используем таблицы на диаграмме ER, что делает ее ближе к реальной базе данных. MySQL Workbench делает еще один шаг вперед и использует расширенные диаграммы сущность-связь (EER). Диаграммы EER представляют собой расширенную версию диаграмм ER.

Мы не будем вдаваться во все детали, но главное преимущество диаграммы EER заключается в том, что она предоставляет все элементы диаграммы ER, добавляя поддержку:

Давайте начнем с процесса загрузки образцов баз данных и визуализации их диаграмм EER в MySQL Workbench.

Первая база, которую мы будем использовать, — это база данных sakila. Разработка этой базы данных началась в 2005 году. Ранние проекты были основаны на базе данных, использованной в официальном документе Dell Three Approaches to MySQL Applications on Dell PowerEdge Servers, который был разработан для представления онлайн-магазина DVD. Точно так же образец базы данных sakila предназначен для представления магазина проката DVD и заимствует имена фильмов и актеров из образца базы данных Dell. Вы можете использовать следующие команды для импорта базы данных sakila в ваш экземпляр MySQL:

# wget https://downloads.mysql.com/docs/sakila-db.tar.gz
# tar -xvf sakila-db.tar.gz
# mysql -uroot -pmsandbox < sakila-db/sakila-schema.sql
# mysql -uroot -pmsandbox < sakila-db/sakila-data.sql

sakila также предоставляет модель EER в файле sakila.mwb. Вы можете открыть файл с помощью MySQL Workbench, как показано на рисунке 2-11.

Рисунок 2-11. Модель EER базы данных sakila; обратите внимание на физическое представление объектов вместо использования символов

Далее идет база данных world, в которой используются выборочные данные из Statistics Finland.

Следующие команды импортируют базу данных world в ваш экземпляр MySQL:

# wget https://downloads.mysql.com/docs/world-db.tar.gz
# tar -xvf world-db.tar.gz
# mysql -uroot -plearning_mysql < world-db/world.sql

База данных world не поставляется с файлом EER, как это делает sakila, но вы можете создать модель EER из базы данных с помощью MySQL Workbench. Для этого выберите Reverse Engineer в меню Database, как показано на рис. 2-12.

Рисунок 2-12. Реверс-инжиниринг из базы данных world

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

Рисунок 2-13. Выбор схемы

Нажмите Continue, а затем нажмите Execute на следующем экране, показанном на рис. 2-14.

Рисунок 2-14. Нажмите Execute, чтобы начать процесс реверс-инжиниринга.

В результате получается модель ER для базы данных world, показанная на рис. 2-15.

Рисунок 2-15. Модель ER для базы данных world

Последняя база данных, которую вы импортируете, — это база данных employees. Фушэн Ван и Карло Дзаньоло создали исходные данные в Siemens Corporate Research. Джузеппе Максиа создал реляционную схему, а Патрик Крюс экспортировал данные в реляционный формат.

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

# git clone https://github.com/datacharmer/test_db.git
# cd test_db
# cat employees.sql | mysql -uroot -psekret

Затем вы можете снова использовать процедуру реверс-инжиниринга в MySQL Workbench, чтобы создать модель ER для базы данных employees, как показано на рисунке 2-16.

Рисунок 2-16. Модель ER для базы данных employees

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

Использование модели отношений сущностей

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

Сопоставление сущностей и отношений с таблицами базы данных

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

Сопоставление сущностей с таблицами базы данных

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

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

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

Сопоставление отношений с таблицами базы данных

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

Для каждого неидентифицирующего отношения «один ко многим» между двумя объектами включите первичный ключ объекта на стороне «1» в качестве внешнего ключа в таблицу для объекта на стороне «N». Добавьте любые атрибуты отношения в таблицу вместе с внешним ключом. Обратите внимание, что идентификация отношений «один ко многим» (между слабой сущностью и сущностью-владельцем) фиксируется как часть этапа сопоставления сущностей.

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

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

Создание модели ER базы данных банка

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

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

Отношения «многие ко многим» (N:M)

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

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

Сущности account_customers и branch_employees будут связующим звеном между сущностями счета и клиента, а также сущностями филиала и сотрудника соответственно. Мы преобразуем отношение N:M в два отношения 1:N. Вы увидите, как выглядит дизайн в следующем разделе.

Связь один ко многим (1:N)

Этот тип отношений существует между филиалами и счетами, а также между клиентами и account_customers. Это поднимает концепцию неотождествляющих отношений (nonidentifying relationship). Например, в таблице account поле branch_id не является частью первичного ключа (одна из причин этого заключается в том, что вы можете перенести свой банковский счет в другой филиал). В настоящее время принято сохранять суррогатный ключ в качестве первичного ключа в каждой таблице; поэтому подлинная идентифицирующая связь, в которой внешний ключ также является частью первичного ключа в модели данных, встречается редко.

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

На рис. 2-17 показано окончательное представление модели банка.

Рисунок 2-17. Модель EER для банковской базы данных

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

Вы можете скачать модель из репозитория книги на GitHub. Файл называется bank_model.mwb.

Преобразование EER в базу данных MySQL с помощью Workbench

Рекомендуется использовать инструмент для рисования диаграмм ER; таким образом, вы можете легко редактировать и переопределять их, пока окончательные диаграммы не станут четкими и однозначными. Как только вы освоитесь с моделью, вы можете развернуть ее. MySQL Workbench позволяет преобразовывать модель EER в операторы языка определения данных (DDL) для создания базы данных MySQL с помощью параметра Forward Engineer в меню базы данных (рис. 2-18).

Рисунок 2-18. Форвард-инжиниринг базы данных в MySQL Workbench

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

Рисунок 2-19. Варианты создания базы данных

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

Затем MySQL Workbench предоставит нам сценарий SQL, который будет выполняться для создания базы данных из нашей модели, как показано на рисунке 2-20.

Рисунок 2-20. Скрипт, созданный для создания базы данных

Когда мы нажмем Continue, MySQL Workbench выполнит операторы на нашем сервере MySQL, как показано на рисунке 2-21.

Рисунок 2-21. MySQL Workbench запускает скрипт

Мы рассмотрим детали операторов в этом скрипте в разделе «Создание таблиц».

Глава 3
Базовый SQL

Как упоминалось в главе 2, д-р Эдгар Ф. Кодд разработал модель реляционной базы данных и ее нормальные формы в начале 1970-х годов. В 1974 году исследователи из лаборатории IBM в Сан-Хосе начали работу над крупным проектом, призванным доказать жизнеспособность реляционной модели, под названием System R. В то же время доктор Дональд Чемберлин и его коллеги также работали над определением языка базы данных. Они разработали структурированный английский язык запросов (SEQUEL), который позволял пользователям запрашивать реляционную базу данных, используя четко определенные предложения в английском стиле. Позже он был переименован в язык структурированных запросов (SQL) по юридическим причинам.

Первые системы управления базами данных, основанные на SQL, стали коммерчески доступными к концу 70-х годов. С ростом активности, связанной с разработкой языков баз данных, появилась стандартизация для упрощения, и сообщество остановилось на SQL. В процессе стандартизации принимали участие как американские, так и международные организации по стандартизации (ANSI и ISO), и в 1986 году был утвержден первый стандарт SQL. Позже стандарт несколько раз пересматривался, а названия (SQL:1999, SQL:2003, SQL:2008 и т. д.) указывали на версии, выпущенные в соответствующие годы. Мы будем использовать фразу стандарт SQL или стандартный SQL для обозначения текущей версии стандарта SQL в любое время.

MySQL расширяет стандартный SQL, предоставляя дополнительные возможности. Например, MySQL реализует STRAIGHT_JOIN, синтаксис которого не распознается другими СУБД.

В этой главе представлена реализация SQL в MySQL, которую мы часто называем операциями CRUD: create, read, update и delete. Мы покажем вам, как читать данные из базы данных с помощью оператора SELECT и выбирать, какие данные извлекать и в каком порядке они отображаются. Мы также покажем вам основы изменения ваших баз данных с помощью инструкции INSERT для добавления данных, UPDATE для изменения данных и DELETE для удаления данных. Наконец, мы объясним, как использовать нестандартные операторы SHOW TABLES и SHOW COLUMNS для изучения вашей базы данных.

Использование базы данных sakila

В главе 2 мы показали вам принципы построения диаграммы базы данных с использованием модели ER. Мы также представили шаги, которые необходимо предпринять для преобразования модели ER в формат, подходящий для построения реляционной базы данных. В этом разделе будет показана структура базы данных MySQL sakila, чтобы вы могли начать знакомиться с различными реляционными моделями баз данных. Мы не будем здесь объяснять операторы SQL, используемые для создания базы данных; это тема главы 4.

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

Чтобы выбрать базу данных sakila в качестве нашей текущей базы данных, мы будем использовать оператор USE. Введите следующую команду:

mysql> USE sakila;
Database changed
mysql>

Вы можете проверить, какая база данных является активной, набрав команду SELECT DATABASE();:

mysql> SELECT DATABASE();
+------------+
| DATABASE() |
+------------+
| sakila     |
+------------+
1 row in set (0.00 sec)

Теперь давайте рассмотрим, какие таблицы составляют базу данных sakila, используя оператор SHOW TABLES:

mysql> SHOW TABLES;
+----------------------------+
| Tables_in_sakila           |
+----------------------------+
| actor                      |
| actor_info                 |
| ...                        |
| customer                   |
| customer_list              |
| film                       |
| film_actor                 |
| film_category              |
| film_list                  |
| film_text                  |
| inventory                  |
| language                   |
| nicer_but_slower_film_list |
| payment                    |
| rental                     |
| sales_by_film_category     |
| sales_by_store             |
| staff                      |
| staff_list                 |
| store                      |
+----------------------------+
23 rows in set (0.00 sec)

Пока никаких сюрпризов не было. Давайте узнаем больше о каждой из таблиц, составляющих базу данных sakila. Во-первых, давайте воспользуемся оператором SHOW COLUMNS для просмотра таблицы actor (обратите внимание, что выходные данные были свернуты, чтобы соответствовать полям страницы):

mysql> SHOW COLUMNS FROM actor;
+-------------+-------------------+------+-----+-------------------+...
| Field       | Type              | Null | Key | Default           |...
+-------------+-------------------+------+-----+-------------------+...
| actor_id    | smallint unsigned | NO   | PRI | NULL              |...
| first_name  | varchar(45)       | NO   |     | NULL              |...
| last_name   | varchar(45)       | NO   | MUL | NULL              |...
| last_update | timestamp         | NO   |     | CURRENT_TIMESTAMP |...
+-------------+-------------------+------+-----+-------------------+...
...+-----------------------------------------------+
...| Extra                                         |
...+-----------------------------------------------+
...| auto_increment                                |
...|                                               |
...|                                               |
...| DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
...+-----------------------------------------------+
4 rows in set (0.01 sec)

Ключевое слово DESCRIBE идентично SHOW COLUMNS FROM, и мы можем сократить его до просто DESC, поэтому мы можем написать предыдущий запрос следующим образом:

mysql> DESC actor;

Полученный результат идентичен. Рассмотрим структуру таблицы более подробно. Таблица actor содержит четыре столбца: actor_id, first_name, last_name и last_update. Мы также можем извлечь типы столбцов: smallint для actor_id, varchar(45) для first_name и last_name и отметку времени для last_update. Ни один из столбцов не принимает значение NULL (пустое), actor_id — это первичный ключ (PRI), а last_name — это первый столбец неуникального индекса (MUL). Не беспокойтесь о деталях; все, что сейчас важно, — это имена столбцов, которые мы будем использовать для команд SQL.

Далее давайте изучим таблицу city, выполнив оператор DESC:

mysql> DESC city;
+-------------+-------------------+------+-----+-------------------+...
| Field       | Type              | Null | Key | Default           |...
+-------------+-------------------+------+-----+-------------------+...
| city_id     | smallint unsigned | NO   | PRI | NULL              |...
| city        | varchar(50)       | NO   |     | NULL              |...
| country_id  | smallint unsigned | NO   | MUL | NULL              |...
| last_update | timestamp         | NO   |     | CURRENT_TIMESTAMP |...
+-------------+-------------------+------+-----+-------------------+...
...+-----------------------------------------------+
...| Extra                                         |
...+-----------------------------------------------+
...| auto_increment                                |
...|                                               |
...|                                               |
...| DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
...+-----------------------------------------------+
4 rows in set (0.01 sec)

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

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

Оператор SELECT и основные методы запросов

В предыдущих главах было показано, как установить и настроить MySQL и использовать командную строку MySQL, а также была представлена модель ER. Теперь вы готовы приступить к изучению языка SQL, который используют все клиенты MySQL для изучения данных и управления ими. В этом разделе представлено наиболее часто используемое ключевое слово SQL: SELECT. Мы объясняем основные элементы стиля и синтаксиса, а также особенности предложения WHERE, логических операторов и сортировки (многое из этого также относится к нашим более поздним обсуждениям INSERT, UPDATE и DELETE). Это не конец нашего обсуждения SELECT; вы найдете больше в главе 5, где мы покажем вам, как использовать его расширенные функции.

Однотабличные SELECT

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

mysql> USE sakila;
Database changed

Давайте получим все данные в таблице language:

mysql> SELECT * FROM language;
+-------------+----------+---------------------+
| language_id | name     | last_update         |
+-------------+----------+---------------------+
|           1 | English  | 2006-02-15 05:02:19 |
|           2 | Italian  | 2006-02-15 05:02:19 |
|           3 | Japanese | 2006-02-15 05:02:19 |
|           4 | Mandarin | 2006-02-15 05:02:19 |
|           5 | French   | 2006-02-15 05:02:19 |
|           6 | German   | 2006-02-15 05:02:19 |
+-------------+----------+---------------------+
6 rows in set (0.00 sec)

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

Простой оператор SELECT состоит из четырех компонентов:

  1. Ключевое слово SELECT.

  2. Столбцы для отображения. Символ звездочки (*) — это подстановочный знак, обозначающий все столбцы.

  3. Ключевое слово FROM.

  4. Имя таблицы.

Итак, в этом примере мы запросили все столбцы из таблицы language, и это то, что нам вернул MySQL.

Давайте попробуем еще один простой SELECT. На этот раз мы получим все столбцы из таблицы city:

mysql> SELECT * FROM city;
+---------+------------------------+------------+---------------------+
| city_id | city                   | country_id | last_update         |
+---------+------------------------+------------+---------------------+
|       1 | A Corua (La Corua)     |         87 | 2006-02-15 04:45:25 |
|       2 | Abha                   |         82 | 2006-02-15 04:45:25 |
|       3 | Abu Dhabi              |        101 | 2006-02-15 04:45:25 |
|     ...                                                             |
|     599 | Zhoushan               |         23 | 2006-02-15 04:45:25 |
|     600 | Ziguinchor             |         83 | 2006-02-15 04:45:25 |
+---------+------------------------+------------+---------------------+
600 rows in set (0.00 sec)

Имеется 600 городов, и выходные данные имеют ту же базовую структуру, что и в нашем первом примере.

Этот пример дает некоторое представление о том, как работают отношения между таблицами. Рассмотрим первую строку результатов. В столбце country_id вы увидите значение 87. Как вы увидите позже, мы можем проверить таблицу country, чтобы узнать, что страна с кодом 87 — это Испания. Мы обсудим, как писать запросы об отношениях между таблицами, в разделе «Объединение двух таблиц».

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

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

Выбор столбцов

Ранее мы использовали подстановочный знак * для получения всех столбцов в таблице. Если вы не хотите отображать все столбцы, можно уточнить, перечислив нужные столбцы в том порядке, в котором вы хотите их разделить запятыми. Например, если вам нужен только столбец city из таблицы city, введите:

mysql> SELECT city FROM city;
+--------------------+
| city               |
+--------------------+
| A Corua (La Corua) |
| Abha               |
| Abu Dhabi          |
| Acua               |
| Adana              |
+--------------------+
5 rows in set (0.00 sec)

Если вам нужны столбцы city и city_id в таком порядке, вы должны использовать:

mysql> SELECT city, city_id FROM city;
+--------------------+---------+
| city               | city_id |
+--------------------+---------+
| A Corua (La Corua) |       1 |
| Abha               |       2 |
| Abu Dhabi          |       3 |
| Acua               |       4 |
| Adana              |       5 |
+--------------------+---------+
5 rows in set (0.01 sec)

Вы даже можете перечислить столбцы более одного раза:

mysql> SELECT city, city FROM city;
+--------------------+--------------------+
| city               | city               |
+--------------------+--------------------+
| A Corua (La Corua) | A Corua (La Corua) |
| Abha               | Abha               |
| Abu Dhabi          | Abu Dhabi          |
| Acua               | Acua               |
| Adana              | Adana              |
+--------------------+--------------------+
5 rows in set (0.00 sec)

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

Вы можете указать имена базы данных, таблицы и столбца в операторе SELECT. Это позволяет вам избежать команды USE и работать с любой базой данных и таблицей напрямую с помощью SELECT; это также помогает устранять неоднозначности, как мы покажем в разделе «Объединение двух таблиц». Например, предположим, что вы хотите получить столбец name из таблицы language в базе данных sakila. Вы можете сделать это с помощью следующей команды:

mysql> SELECT name FROM sakila.language;
+----------+
| name     |
+----------+
| English  |
| Italian  |
| Japanese |
| Mandarin |
| French   |
| German   |
+----------+
6 rows in set (0.01 sec)

Компонент sakila.language после ключевого слова FROM указывает базу данных sakila и ее таблицу language. Нет необходимости вводить USE sakila; перед выполнением этого запроса. Этот синтаксис также можно использовать с другими операторами SQL, включая операторы UPDATE, DELETE, INSERT и SHOW, которые мы обсудим позже в этой главе.

Выбор строк с помощью предложения WHERE

В этом разделе представлено предложение WHERE и объясняется, как использовать операторы для написания выражений. Вы увидите их в инструкциях SELECT и других инструкциях, таких как UPDATE и DELETE; мы покажем вам примеры позже в этой главе.

Основы WHERE

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

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

mysql> SELECT * FROM sakila.language WHERE name = 'English';
+-------------+---------+---------------------+
| language_id | name    | last_update         |
+-------------+---------+---------------------+
|           1 | English | 2006-02-15 05:02:19 |
+-------------+---------+---------------------+
1 row in set (0.00 sec)

MySQL возвращает все строки, соответствующие вашим критериям поиска — в данном случае только одну строку и все ее столбцы.

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

mysql> SELECT first_name FROM actor WHERE actor_id = 4;
+------------+
| first_name |
+------------+
| JENNIFER   |
+------------+
1 row in set (0.00 sec)

Здесь вы указываете столбец и строку, включая столбец first_name после ключевого слова SELECT и указывая WHERE actor_id = 4.

Если значение соответствует более чем одной строке, результаты будут содержать все совпадения. Предположим, вы хотите просмотреть все города, принадлежащие Бразилии, для которой значение country_id равно 15. Введите:

mysql> SELECT city FROM city WHERE country_id = 15;
+----------------------+
| city                 |
+----------------------+
| Alvorada             |
| Angra dos Reis       |
| Anpolis              |
| Aparecida de Goinia  |
| Araatuba             |
| Bag                  |
| Belm                 |
| Blumenau             |
| Boa Vista            |
| Braslia              |
| ...                  |
+----------------------+
28 rows in set (0.00 sec)

Результаты показывают названия 28 городов, принадлежащих Бразилии. Если бы мы могли объединить информацию, которую мы получаем из таблицы city, с информацией, которую мы получаем из таблицы country, мы могли бы отображать названия городов с их соответствующими странами. Мы увидим, как выполнить этот тип запроса, в разделе «Объединение двух таблиц».

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

mysql> SELECT city FROM city WHERE city_id < 5;
+--------------------+
| city               |
+--------------------+
| A Corua (La Corua) |
| Abha               |
| Abu Dhabi          |
| Acua               |
+--------------------+
4 rows in set (0.00 sec)

Для чисел часто используются следующие операторы: «равно» (=), «больше» (>), «меньше» (<), «меньше или равно» (<=), «больше или равно» (>=) и «не равно» (<> или !=).

Рассмотрим еще один пример. Если вы хотите найти все языки, для которых language_id не равен 2, введите:

mysql> SELECT language_id, name FROM sakila.language
    -> WHERE language_id <> 2;
+-------------+----------+
| language_id | name     |
+-------------+----------+
|           1 | English  |
|           3 | Japanese |
|           4 | Mandarin |
|           5 | French   |
|           6 | German   |
+-------------+----------+
5 rows in set (0.00 sec)

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

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

mysql> SELECT first_name FROM actor WHERE first_name < 'B';
+------------+
| first_name |
+------------+
| ALEC       |
| AUDREY     |
| ANNE       |
| ANGELA     |
| ADAM       |
| ANGELINA   |
| ALBERT     |
| ADAM       |
| ANGELA     |
| ALBERT     |
| AL         |
| ALAN       |
| AUDREY     |
+------------+
13 rows in set (0.00 sec)

Под «без учета регистра» мы подразумеваем, что B и b будут считаться одним и тем же фильтром, поэтому этот запрос даст тот же результат:

mysql> SELECT first_name FROM actor WHERE first_name < 'b';
+------------+
| first_name |
+------------+
| ALEC       |
| AUDREY     |
| ANNE       |
| ANGELA     |
| ADAM       |
| ANGELINA   |
| ALBERT     |
| ADAM       |
| ANGELA     |
| ALBERT     |
| AL         |
| ALAN       |
| AUDREY     |
+------------+
13 rows in set (0.00 sec)

Другая распространенная задача, выполняемая со строками, — поиск совпадений, начинающихся с префикса, содержащих строку или заканчивающихся суффиксом. Например, нам может понадобиться найти все названия альбомов, начинающиеся со слова «Retro». Мы можем сделать это с помощью оператора LIKE в предложении WHERE. Давайте рассмотрим пример, в котором мы ищем фильм с названием, содержащим слово «family»:

mysql> SELECT title FROM film WHERE title LIKE '%family%';
+----------------+
| title          |
+----------------+
| CYCLONE FAMILY |
| DOGMA FAMILY   |
| FAMILY SWEET   |
+----------------+
3 rows in set (0.00 sec)

Давайте посмотрим, как это работает. Предложение LIKE используется со строками и означает, что совпадение должно соответствовать шаблону в следующей строке. В нашем примере мы использовали LIKE '%family%', что означает, что строка должна содержать family, и ей может предшествовать или следовать ноль или более символов. Большинство строк, используемых с LIKE, содержат символ процента (%) в качестве подстановочного знака, который соответствует всем возможным строкам. Вы можете использовать его для определения строки, которая заканчивается суффиксом, например "%ing", или строки, которая начинается с определенной подстроки, например "Corruption%".

Например, "John%" будет соответствовать всем строкам, начинающимся с John, таким как John Smith и John Paul Getty. Шаблон "%Paul" соответствует всем строкам, в конце которых есть Paul. Наконец, шаблон "%Paul%" соответствует всем строкам, в которых есть Paul, в том числе в начале или в конце.

Если вы хотите, чтобы в предложении LIKE соответствовал только один подстановочный знак, вы используете символ подчеркивания (_). Например, если вы хотите получить названия всех фильмов с участием актера, чье имя начинается с трех букв NAT, вы используете:

mysql> SELECT title FROM film_list WHERE actors LIKE 'NAT_%';
+----------------------+
| title                |
+----------------------+
| FANTASY TROOPERS     |
| FOOL MOCKINGBIRD     |
| HOLES BRANNIGAN      |
| KWAI HOMEWARD        |
| LICENSE WEEKEND      |
| NETWORK PEAK         |
| NUTS TIES            |
| TWISTED PIRATES      |
| UNFORGIVEN ZOOLANDER |
+----------------------+
9 rows in set (0.04 sec)
Сочетание условий с AND, OR, NOT и XOR

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

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

mysql> SELECT title FROM film_list WHERE category LIKE 'Sci-Fi'
    -> AND rating LIKE 'PG';
+----------------------+
| title                |
+----------------------+
| CHAINSAW UPTOWN      |
| CHARADE DUFFEL       |
| FRISCO FORREST       |
| GOODFELLAS SALUTE    |
| GRAFFITI LOVE        |
| MOURNING PURPLE      |
| OPEN AFRICAN         |
| SILVERADO GOLDFINGER |
| TITANS JERK          |
| TROJAN TOMORROW      |
| UNFORGIVEN ZOOLANDER |
| WONDERLAND CHRISTMAS |
+----------------------+
12 rows in set (0.07 sec)

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

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

mysql> SELECT title FROM film_list WHERE category LIKE 'Children'
    -> OR category LIKE 'Family';
+------------------------+
| title                  |
+------------------------+
| AFRICAN EGG            |
| APACHE DIVINE          |
| ATLANTIS CAUSE         |
...
| WRONG BEHAVIOR         |
| ZOOLANDER FICTION      |
+------------------------+
129 rows in set (0.04 sec)

Операция OR в предложении WHERE ограничивает ответы теми, которые удовлетворяют любому из двух условий. Кроме того, мы можем заметить, что результаты упорядочены. Это просто совпадение; в этом случае о них сообщается в том порядке, в котором они были добавлены в базу данных. Мы вернемся к сортировке вывода в разделе «Предложение ORDER BY».

Вы можете комбинировать AND и OR, но вам нужно четко указать очередность выполнения этих условий. Скобки объединяют части оператора вместе и помогают сделать выражения читабельными; вы можете использовать их так же, как и в базовой математике. Допустим, теперь вам нужны sci-fi или family фильмы с рейтингом PG. Вы можете написать этот запрос следующим образом:

mysql> SELECT title FROM film_list WHERE (category like 'Sci-Fi'
    -> OR category LIKE 'Family') AND rating LIKE 'PG';
+------------------------+
| title                  |
+------------------------+
| BEDAZZLED MARRIED      |
| CHAINSAW UPTOWN        |
| CHARADE DUFFEL         |
| CHASING FIGHT          |
| EFFECT GLADIATOR       |
...
| UNFORGIVEN ZOOLANDER   |
| WONDERLAND CHRISTMAS   |
+------------------------+
30 rows in set (0.07 sec)

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

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

mysql> SELECT (2+2)*3;
+---------+
| (2+2)*3 |
+---------+
|      12 |
+---------+
1 row in set (0.00 sec)mysql> SELECT 2+2*3;
+-------+
| 2+2*3 |
+-------+
|     8 |
+-------+
1 row in set (0.00 sec)

Унарный оператор NOT инвертирует логический оператор. Ранее мы приводили пример перечисления всех языков с language_id, не равным 2. Вы также можете написать этот запрос с оператором NOT:

mysql> SELECT language_id, name FROM sakila.language
    -> WHERE NOT (language_id = 2);
+-------------+----------+
| language_id | name     |
+-------------+----------+
|           1 | English  |
|           3 | Japanese |
|           4 | Mandarin |
|           5 | French   |
|           6 | German   |
+-------------+----------+
5 rows in set (0.01 sec)

Выражение в круглых скобках (language_id = 2) задает соответствие условию, а операция NOT отменяет его, поэтому вы получаете все, кроме тех результатов, которые соответствуют условию. Есть несколько других способов написать предложение WHERE с той же идеей. В главе 5 вы увидите, что некоторые из них работают лучше, чем другие.

Рассмотрим другой пример с использованием NOT и круглых скобок. Предположим, вы хотите получить список всех названий фильмов с FID меньше 7, но не с номерами 4 или 6. Это можно сделать с помощью следующего запроса:

mysql> SELECT fid,title FROM film_list WHERE FID < 7 AND NOT (FID = 4 OR FID = 6);
+------+------------------+
| fid  | title            |
+------+------------------+
|    1 | ACADEMY DINOSAUR |
|    2 | ACE GOLDFINGER   |
|    3 | ADAPTATION HOLES |
|    5 | AFRICAN EGG      |
+------+------------------+
4 rows in set (0.06 sec)

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

Эти операторы можно комбинировать различными способами для получения желаемых результатов. Например, вы можете написать запрос для получения названий любых фильмов с ценой от 2 до 4 долларов, относящихся к категории Documentary или Horror и с актером по имени Bob:

mysql> SELECT title
    -> FROM film_list
    -> WHERE price BETWEEN 2 AND 4
    -> AND (category LIKE 'Documentary' OR category LIKE 'Horror')
    -> AND actors LIKE '%BOB%';
+------------------+
| title            |
+------------------+
| ADAPTATION HOLES |
+------------------+
1 row in set (0.08 sec)

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

mysql> SELECT title FROM film_list
    -> WHERE price BETWEEN 2 AND 4
    -> AND (category LIKE 'Documentary' OR category LIKE 'Horror')
    -> AND actors LIKE '%GRIPPA%';

Empty set (0.04 sec)

Предложение ORDER BY

Мы обсудили, как выбирать столбцы и какие строки возвращаются как часть результата запроса, но не обсуждали, как управлять отображением результата. В реляционной базе данных строки таблицы образуют набор; между строками нет внутреннего порядка, поэтому мы должны попросить MySQL отсортировать результаты, если мы хотим, чтобы они были в определенном порядке. В этом разделе объясняется, как использовать для этого предложение ORDER BY. Сортировка не влияет на то, что возвращается; это влияет только на то, в каком порядке возвращаются результаты.

Предположим, вы хотите вернуть список первых 10 клиентов в базе данных sakila, отсортированных по name в алфавитном порядке. Вот что вы должны ввести:

mysql> SELECT name FROM customer_list
    -> ORDER BY name
    -> LIMIT 10;
+-------------------+
| name              |
+-------------------+
| AARON SELBY       |
| ADAM GOOCH        |
| ADRIAN CLARY      |
| AGNES BISHOP      |
| ALAN KAHN         |
| ALBERT CROUSE     |
| ALBERTO HENNING   |
| ALEX GRESHAM      |
| ALEXANDER FENNELL |
| ALFRED CASILLAS   |
+-------------------+
10 rows in set (0.01 sec)

Предложение ORDER BY указывает, что требуется сортировка, за которой следует столбец, который следует использовать в качестве ключа сортировки. В этом примере вы сортируете по имени в алфавитном порядке по возрастанию — по умолчанию сортировка выполняется без учета регистра и в порядке возрастания, а MySQL автоматически сортирует в алфавитном порядке, поскольку столбцы представляют собой строки символов. Способ сортировки строк определяется используемым набором символов и порядком сортировки. Мы обсуждаем их в разделе «Сопоставление и наборы символов». В большей части этой книги мы предполагаем, что вы используете настройки по умолчанию.

Давайте посмотрим на другой пример. На этот раз вы отсортируете вывод из таблицы адресов в порядке возрастания на основе столбца last_update и отобразите только первые пять результатов:

mysql> SELECT address, last_update FROM address
    -> ORDER BY last_update LIMIT 5;
+-----------------------------+---------------------+
| address                     | last_update         |
+-----------------------------+---------------------+
| 1168 Najafabad Parkway      | 2014-09-25 22:29:59 |
| 1031 Daugavpils Parkway     | 2014-09-25 22:29:59 |
| 1924 Shimonoseki Drive      | 2014-09-25 22:29:59 |
| 757 Rustenburg Avenue       | 2014-09-25 22:30:01 |
| 1892 Nabereznyje Telny Lane | 2014-09-25 22:30:02 |
+-----------------------------+---------------------+
5 rows in set (0.00 sec)

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

mysql> SELECT address, district FROM address
    -> ORDER BY district, address;
+----------------------------------------+----------------------+
| address                                | district             |
+----------------------------------------+----------------------+
| 1368 Maracabo Boulevard                |                      |
| 18 Duisburg Boulevard                  |                      |
| 962 Tama Loop                          |                      |
| 535 Ahmadnagar Manor                   | Abu Dhabi            |
| 669 Firozabad Loop                     | Abu Dhabi            |
| 1078 Stara Zagora Drive                | Aceh                 |
| 663 Baha Blanca Parkway                | Adana                |
| 842 Salzburg Lane                      | Adana                |
| 614 Pak Kret Street                    | Addis Abeba          |
| 751 Lima Loop                          | Aden                 |
| 1157 Nyeri Loop                        | Adygea               |
| 387 Mwene-Ditu Drive                   | Ahal                 |
| 775 ostka Drive                        | al-Daqahliya         |
| ...                                                           |
| 1416 San Juan Bautista Tuxtepec Avenue | Zufar                |
| 138 Caracas Boulevard                  | Zulia                |
+----------------------------------------+----------------------+
603 rows in set (0.00 sec)

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

mysql> SELECT address,district FROM address
    -> ORDER BY district ASC, address DESC
    -> LIMIT 10;
+-------------------------+-------------+
| address                 | district    |
+-------------------------+-------------+
| 962 Tama Loop           |             |
| 18 Duisburg Boulevard   |             |
| 1368 Maracabo Boulevard |             |
| 669 Firozabad Loop      | Abu Dhabi   |
| 535 Ahmadnagar Manor    | Abu Dhabi   |
| 1078 Stara Zagora Drive | Aceh        |
| 842 Salzburg Lane       | Adana       |
| 663 Baha Blanca Parkway | Adana       |
| 614 Pak Kret Street     | Addis Abeba |
| 751 Lima Loop           | Aden        |
+-------------------------+-------------+
10 rows in set (0.01 sec)

Если происходит конфликт значений и вы не указываете другой ключ сортировки, порядок сортировки не определен. Это может быть не важно для вас; вас может не волновать порядок появления двух клиентов с одинаковым именем «Джон А. Смит». Если вы хотите применить определенный порядок в этом случае, вам нужно добавить дополнительные столбцы в предложение ORDER BY, как показано в предыдущем примере.

Предложение LIMIT

Как вы могли заметить, несколько предыдущих запросов использовали предложение LIMIT. Это полезная нестандартная инструкция SQL, позволяющая контролировать количество выводимых строк. Его базовая форма позволяет вам ограничить количество строк, возвращаемых оператором SELECT, что полезно, когда вы хотите ограничить объем данных, передаваемых по сети или выводимых на экран. Вы можете использовать его, например, для получения выборки данных из таблицы, как показано здесь:

mysql> SELECT name FROM customer_list LIMIT 10;
+------------------+
| name             |
+------------------+
| VERA MCCOY       |
| MARIO CHEATHAM   |
| JUDY GRAY        |
| JUNE CARROLL     |
| ANTHONY SCHWAB   |
| CLAUDE HERZOG    |
| MARTIN BALES     |
| BOBBY BOUDREAU   |
| WILLIE MARKHAM   |
| JORDAN ARCHULETA |
+------------------+

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

mysql> SELECT name FROM customer_list LIMIT 5, 5;
+------------------+
| name             |
+------------------+
| CLAUDE HERZOG    |
| MARTIN BALES     |
| BOBBY BOUDREAU   |
| WILLIE MARKHAM   |
| JORDAN ARCHULETA |
+------------------+
5 rows in set (0.00 sec)

Результатом являются строки с 6 по 10 из запроса SELECT.

Существует альтернативный синтаксис, который вы можете увидеть для ключевого слова LIMIT: вместо LIMIT 10, 5 вы можете написать LIMIT 10 OFFSET 5. Синтаксис OFFSET отбрасывает указанные в нем N значений.

Вот пример без смещения:

mysql> SELECT id, name FROM customer_list
    -> ORDER BY id LIMIT 10;
+----+------------------+
| ID | name             |
+----+------------------+
|  1 | MARY SMITH       |
|  2 | PATRICIA JOHNSON |
|  3 | LINDA WILLIAMS   |
|  4 | BARBARA JONES    |
|  5 | ELIZABETH BROWN  |
|  6 | JENNIFER DAVIS   |
|  7 | MARIA MILLER     |
|  8 | SUSAN WILSON     |
|  9 | MARGARET MOORE   |
| 10 | DOROTHY TAYLOR   |
+----+------------------+
10 rows in set (0.00 sec)

А вот результаты со смещением 5:

mysql> SELECT id, name FROM customer_list
    -> ORDER BY id LIMIT 10 OFFSET 5;
+----+----------------+
| ID | name           |
+----+----------------+
|  6 | JENNIFER DAVIS |
|  7 | MARIA MILLER   |
|  8 | SUSAN WILSON   |
|  9 | MARGARET MOORE |
| 10 | DOROTHY TAYLOR |
| 11 | LISA ANDERSON  |
| 12 | NANCY THOMAS   |
| 13 | KAREN JACKSON  |
| 14 | BETTY WHITE    |
| 15 | HELEN HARRIS   |
+----+----------------+
10 rows in set (0.01 sec)

Объединение двух таблиц

До сих пор мы работали только с одной таблицей в наших запросах SELECT. Однако в большинстве случаев потребуется информация из более чем одной таблицы одновременно. Когда мы изучили таблицы в базе данных sakila, стало очевидно, что, используя отношения, мы можем отвечать на более интересные запросы. Например, было бы полезно знать страну, в которой находится каждый город. В этом разделе показано, как отвечать на такие запросы, объединяя две таблицы. Мы вернемся к этому вопросу в рамках более подробного обсуждения объединений в главе 5.

В этой главе мы используем только один синтаксис соединения. Есть еще два (LEFT и RIGHT JOIN), и каждый из них дает вам разные способы объединения данных из двух или более таблиц. Мы используем здесь синтаксис INNER JOIN, который чаще всего используется в повседневной деятельности. Давайте рассмотрим пример, а затем объясним подробнее, как это работает:

mysql> SELECT city, country FROM city INNER JOIN country
    -> ON city.country_id = country.country_id
    -> WHERE country.country_id < 5
    -> ORDER BY country, city;
+----------+----------------+
| city     | country        |
+----------+----------------+
| Kabul    | Afghanistan    |
| Batna    | Algeria        |
| Bchar    | Algeria        |
| Skikda   | Algeria        |
| Tafuna   | American Samoa |
| Benguela | Angola         |
| Namibe   | Angola         |
+----------+----------------+
7 rows in set (0.00 sec)

Вывод показывает города в каждой стране со значением country_id меньше 5. Вы можете впервые увидеть, какие города находятся в каждой стране.

Как работает INNER JOIN? Оператор состоит из двух частей: во-первых, двух имен таблиц, разделенных ключевыми словами INNER JOIN; и, во-вторых, ключевого слова ON, указывающего необходимые столбцы для составления условия. В этом примере две таблицы, которые необходимо объединить, - это city и country, выраженные как city INNER JOIN country (для базового INNER JOIN не имеет значения, в каком порядке вы перечисляете таблицы, поэтому использование country INNER JOIN city будет иметь тот же эффект). В предложении ON (ON city.country_id = country.country_id) мы сообщаем MySQL столбцы, которые содержат отношения между таблицами; вы должны помнить об этом из нашего проекта и нашего предыдущего обсуждения в главе 2.

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

mysql> SELECT city, country FROM city
    -> INNER JOIN country using (country_id)
    -> WHERE country.country_id < 5
    -> ORDER BY country, city;
+----------+----------------+
| city     | country        |
+----------+----------------+
| Kabul    | Afghanistan    |
| Batna    | Algeria        |
| Bchar    | Algeria        |
| Skikda   | Algeria        |
| Tafuna   | American Samoa |
| Benguela | Angola         |
| Namibe   | Angola         |
+----------+----------------+
7 rows in set (0.01 sec)

Диаграмма Венна на рис. 3-1 иллюстрирует внутреннее соединение.

Рисунок 3-1. Представление диаграммы Венна для INNER JOIN

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

mysql> SELECT COUNT(1) FROM city INNER JOIN country
    -> ON city.country_id = country.country_id
    -> WHERE country.country_id = 49
    -> ORDER BY country, city;
+----------+
| count(1) |
+----------+
|        7 |
+----------+
1 row in set (0.00 sec)

Мы объясним дополнительные возможности SELECT и агрегатных функций в главе 5. Дополнительные сведения о функции COUNT() см. в разделе «Агрегатные функции».

Оператор INSERT

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

Основы INSERT

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

Давайте начнем с основной задачи вставки одной новой строки в таблицу language. Для этого вам нужно понять структуру таблицы. Как мы объяснили в разделе «Использование базы данных sakila», вы можете узнать это с помощью оператора SHOW COLUMNS:

mysql> SHOW COLUMNS FROM language;
+-------------+-------------------+------+-----+-------------------+...
| Field       | Type             | Null | Key | Default            |...
+-------------+-------------------+------+-----+-------------------+...
| language_id | tinyint unsigned | NO   | PRI | NULL               |...
| name        | char(20)         | NO   |     | NULL               |...
| last_update | timestamp        | NO   |     | CURRENT_TIMESTAMP  |...
+-------------+-------------------+------+-----+-------------------+...
...+-----------------------------------------------+
...| Extra                                         |
...+-----------------------------------------------+
...| auto_increment                                |
...|                                               |
...| DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
...+-----------------------------------------------+
3 rows in set (0.00 sec)

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

Давайте добавим новую строку для португальского языка. Есть два способа сделать это. Чаще всего MySQL заполняет значение по умолчанию для language_id, например:

mysql> INSERT INTO language VALUES (NULL, 'Portuguese', NOW());
Query OK, 1 row affected (0.10 sec)

Если вы сейчас выполните SELECT для таблицы, мы увидим, что MySQL вставил строку:

mysql> SELECT * FROM language;
+-------------+------------+---------------------+
| language_id | name       | last_update         |
+-------------+------------+---------------------+
|           1 | English    | 2006-02-15 05:02:19 |
|           2 | Italian    | 2006-02-15 05:02:19 |
|           3 | Japanese   | 2006-02-15 05:02:19 |
|           4 | Mandarin   | 2006-02-15 05:02:19 |
|           5 | French     | 2006-02-15 05:02:19 |
|           6 | German     | 2006-02-15 05:02:19 |
|           7 | Portuguese | 2020-09-26 09:11:36 |
+-------------+------------+---------------------+
7 rows in set (0.00 sec)

Обратите внимание, что мы использовали функцию NOW() в столбце last_update. Функция NOW() возвращает текущую дату и время сервера MySQL.

Второй вариант — вставить значение столбца language_id вручную. Теперь, когда у нас уже есть семь языков, мы должны использовать 8 для следующего значения language_id. Мы можем проверить это с помощью этой инструкции SQL:

mysql> SELECT MAX(language_id) FROM language;
+------------------+
| max(language_id) |
+------------------+
|                7 |
+------------------+
1 row in set (0.00 sec)

Функция MAX() сообщает вам максимальное значение для столбца, переданного в качестве параметра. Это чище, чем использование SELECT language_id FROM language, который выводит все строки и требует, чтобы вы проверили их, чтобы найти максимальное значение. Добавление предложения ORDER BY и LIMIT делает это проще, но использование MAX() намного проще, чем SELECT language_id FROM language ORDER BY language_id DESC LIMIT 1, который возвращает тот же ответ.

Теперь мы готовы вставить строку. В этом INSERT мы также собираемся вставить значение last_update вручную. Вот нужная команда:

mysql> INSERT INTO language VALUES (8, 'Russian', '2020-09-26 10:35:00');
Query OK, 1 row affected (0.02 sec)

MySQL сообщает, что одна строка была затронута (в данном случае добавлена), что мы можем подтвердить, снова проверив содержимое таблицы:

mysql> SELECT * FROM language;
+-------------+------------+---------------------+
| language_id | name       | last_update         |
+-------------+------------+---------------------+
|           1 | English    | 2006-02-15 05:02:19 |
|           2 | Italian    | 2006-02-15 05:02:19 |
|           3 | Japanese   | 2006-02-15 05:02:19 |
|           4 | Mandarin   | 2006-02-15 05:02:19 |
|           5 | French     | 2006-02-15 05:02:19 |
|           6 | German     | 2006-02-15 05:02:19 |
|           7 | Portuguese | 2020-09-26 09:11:36 |
|           8 | Russian    | 2020-09-26 10:35:00 |
+-------------+------------+---------------------+
8 rows in set (0.00 sec)

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

mysql> INSERT INTO language VALUES (8, 'Arabic', '2020-09-26 10:35:00');
ERROR 1062 (23000): Duplicate entry '8' for key 'language.PRIMARY'

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

mysql> INSERT IGNORE INTO language VALUES (8, 'Arabic', '2020-09-26 10:35:00');
Query OK, 0 rows affected, 1 warning (0.00 sec)

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

Также можно вставить несколько значений одновременно:

mysql> INSERT INTO language VALUES (NULL, 'Spanish', NOW()),
    -> (NULL, 'Hebrew', NOW());
Query OK, 2 rows affected (0.02 sec)
Records: 2 Duplicates: 0 Warnings: 0

Обратите внимание, что MySQL сообщает о результатах массовой вставки иначе, чем одиночной вставки.

Первая строка сообщает вам, сколько строк было вставлено, а первая запись во второй строке сообщает вам, сколько строк (или записей) было фактически обработано. Если вы используете INSERT IGNORE и пытаетесь вставить повторяющуюся запись (такую, для которой первичный ключ совпадает с первичным ключом существующей строки), MySQL молча пропустит ее вставку и сообщит о ней как о дубликате во второй записи во второй строке:

mysql> INSERT IGNORE INTO language VALUES (9, 'Portuguese', NOW()),
    (11, 'Hebrew', NOW());
Query OK, 1 row affected, 1 warning (0.01 sec)
Records: 2 Duplicates: 1 Warnings: 1

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

Альтернативный синтаксис

Существует несколько альтернатив синтаксису VALUES, продемонстрированному в предыдущем разделе. В этом разделе мы рассмотрим их и объясним преимущества и недостатки каждого из них. Если вас устраивает базовый синтаксис, который мы описали выше, и вы хотите перейти к новой теме, можете сразу переходить к «Оператор DELETE».

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

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

mysql> INSERT INTO actor (actor_id, first_name, last_name, last_update)
    -> VALUES (NULL, 'Vinicius', 'Grippa', NOW());
Query OK, 1 row affected (0.03 sec)

Имена столбцов заключаются в круглые скобки после имени таблицы, а значения, хранящиеся в этих столбцах, перечислены в круглых скобках после ключевого слова VALUES. Итак, в этом примере создается новая строка, а значение 201 сохраняется в виде actor_id (помните, у actor_id есть свойство auto_increment), Vinicius сохраняется как first_name, Grippa сохраняется как last_name, а столбец last_update заполняется текущей отметкой времени. Преимущества этого синтаксиса заключаются в том, что он удобочитаем и гибок (устраняет третий недостаток, который мы описали) и не зависит от порядка (устраняет первый недостаток). Проблема в том, что вам нужно знать имена столбцов и вводить их.

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

mysql> DESC city;
+-------------+----------------------+------+-----+-------------------+...
| Field       | Type                 | Null | Key | Default           |...
+-------------+----------------------+------+-----+-------------------+...
| city_id     | smallint(5) unsigned | NO   | PRI | NULL              |...
| city        | varchar(50)          | NO   |     | NULL              |...
| country_id  | smallint(5) unsigned | NO   | MUL | NULL              |...
| last_update | timestamp            | NO   |     | CURRENT_TIMESTAMP |...
+-------------+----------------------+------+-----+-------------------+...
...+-----------------------------------------------+
...| Extra                                         |
...+-----------------------------------------------+
...| auto_increment                                |
...|                                               |
...|                                               |
...| on update CURRENT_TIMESTAMP                   |
...|-----------------------------------------------+

4 rows in set (0.00 sec)

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

mysql> INSERT INTO city (city, country_id) VALUES ('Bebedouro', 19);
Query OK, 1 row affected (0.00 sec)

Мы не устанавливали значение для столбца city_id, поэтому MySQL по умолчанию использует следующее доступное значение (из-за свойства auto_increment), а last_update сохраняет текущую дату и время. Проверить это можно запросом:

mysql> SELECT * FROM city where city like 'Bebedouro';
+---------+-----------+------------+---------------------+
| city_id | city      | country_id | last_update         |
+---------+-----------+------------+---------------------+
|     601 | Bebedouro |         19 | 2021-02-27 21:34:08 |
+---------+-----------+------------+---------------------+
1 row in set (0.01 sec)

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

mysql> INSERT INTO city (city,country_id) VALUES
    -> ('Sao Carlos',19),
    -> ('Araraquara',19),
    -> ('Ribeirao Preto',19);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

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

Если вам нужно использовать значения по умолчанию для столбцов таблицы, вы можете использовать ключевое слово DEFAULT (поддерживается в MySQL 5.7 и более поздних версиях). Вот пример добавления строки в таблицу стран с использованием DEFAULT:

mysql> INSERT INTO country VALUES (NULL, 'Uruguay', DEFAULT);
Query OK, 1 row affected (0.01 sec)

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

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

mysql> INSERT INTO country SET country_id=NULL,
    -> country='Bahamas', last_update=NOW();
Query OK, 1 row affected (0.01 sec)

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

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

Оператор DELETE

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

Основы DELETE

Самое простое использование DELETE — удалить все строки в таблице. Предположим, вы хотите очистить свою таблицу rental. Вы можете сделать это с помощью:

mysql> DELETE FROM rental;
Query OK, 16044 rows affected (2.41 sec)

Синтаксис DELETE не включает имена столбцов, поскольку он используется для удаления целых строк, а не только значений из строки. Чтобы сбросить или изменить значение в строке, вы используете оператор UPDATE, описанный в разделе «Оператор UPDATE». Обратите внимание, что оператор DELETE не удаляет саму таблицу. Например, удалив все строки в таблице rental, вы все равно можете запросить таблицу:

mysql> SELECT * FROM rental;
Empty set (0.00 sec)

Вы также можете продолжить изучение его структуры, используя DESCRIBE или SHOW CREATE TABLE, и вставлять новые строки, используя INSERT. Чтобы удалить таблицу, вы используете оператор DROP, описанный в главе 4.

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

mysql> DELETE FROM language;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key
constraint fails (`sakila`.`film`, CONSTRAINT `fk_film_language` FOREIGN KEY
(`language_id`) REFERENCES `language` (`language_id`) ON UPDATE CASCADE)

Использование WHERE, ORDER BY и LIMIT

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

Чтобы удалить одну или несколько строк, но не все строки в таблице, используйте предложение WHERE. Это работает так же, как и для SELECT. Например, предположим, что вы хотите удалить все строки из таблицы rental с rental_id меньше 10. Вы можете сделать это с помощью:

mysql> DELETE FROM rental WHERE rental_id < 10;
Query OK, 9 rows affected (0.01 sec)

В результате удаляются девять строк, соответствующих критерию.

Теперь предположим, что вы хотите удалить из базы данных все платежи от клиента по имени Мэри Смит. Сначала выполните SELECT с таблицами customer и payment, используя INNER JOIN (как описано в разделе «Объединение двух таблиц»):

mysql> SELECT first_name, last_name, customer.customer_id,
    -> amount, payment_date FROM payment INNER JOIN customer
    -> ON customer.customer_id=payment.customer_id
    -> WHERE first_name like 'Mary'
    -> AND last_name like 'Smith';
+------------+-----------+-------------+--------+---------------------+
| first_name | last_name | customer_id | amount | payment_date        |
+------------+-----------+-------------+--------+---------------------+
| MARY       | SMITH     |           1 |   2.99 | 2005-05-25 11:30:37 |
| MARY       | SMITH     |           1 |   0.99 | 2005-05-28 10:35:23 |
| MARY       | SMITH     |           1 |   5.99 | 2005-06-15 00:54:12 |
| MARY       | SMITH     |           1 |   0.99 | 2005-06-15 18:02:53 |
...
| MARY       | SMITH     |           1 |   1.99 | 2005-08-22 01:27:57 |
| MARY       | SMITH     |           1 |   2.99 | 2005-08-22 19:41:37 |
| MARY       | SMITH     |           1 |   5.99 | 2005-08-22 20:03:46 |
+------------+-----------+-------------+--------+---------------------+
32 rows in set (0.00 sec)

Затем выполните следующую операцию DELETE, чтобы удалить строку с идентификатором customer_id, равным 1, из таблицы payment:

mysql> DELETE FROM payment where customer_id=1;
Query OK, 32 rows affected (0.01 sec)

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

mysql> DELETE FROM payment ORDER BY customer_id LIMIT 10000;
Query OK, 10000 rows affected (0.22 sec)

Удаление всех строк с помощью TRUNCATE

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

Если вы хотите удалить все данные в таблице payment, вы можете выполнить это:

mysql> TRUNCATE TABLE payment;
Query OK, 0 rows affected (0.07 sec)

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

Оператор TRUNCATE TABLE во многом отличается от DELETE, стоит упомянуть некоторые из этих отличий:

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

Оператор UPDATE

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

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

Примеры

Простейшее использование оператора UPDATE — изменение всех строк в таблице. Предположим, вам нужно обновить столбец amount в таблице payment, добавив 10% ко всем платежам. Вы можете сделать это, выполнив:

mysql> UPDATE payment SET amount=amount*1.1;
Query OK, 16025 rows affected, 16025 warnings (0.41 sec)
Rows matched: 16049 Changed: 16025 Warnings: 16025

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

mysql> UPDATE payment SET last_update='2021-02-28 17:53:00';
Query OK, 16049 rows affected (0.27 sec)
Rows matched: 16049 Changed: 16049 Warnings: 0

Вторая строка, сообщаемая оператором UPDATE, показывает общий эффект оператора. В нашем примере вы видите:

Rows matched: 16049  Changed: 16049  Warnings: 0

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

mysql> UPDATE payment SET last_update='2021-02-28 17:53:00';
Query OK, 0 rows affected (0.07 sec)
Rows matched: 16049 Changed: 0 Warnings: 0

На этот раз, поскольку дата уже установлена на 2021-02-28 17:53:00 и условие WHERE отсутствует, все строки по-прежнему соответствуют запросу, но ни одна из них не изменяется. Также обратите внимание, что количество измененных строк всегда равно количеству затронутых строк, как указано в первой строке выходных данных.

Использование WHERE, ORDER BY и LIMIT

Часто вы не хотите изменять все строки в таблице. Вместо этого вы хотите обновить одну или несколько строк, соответствующих условию. Как и в случае с SELECT и DELETE, для задачи используется предложение WHERE. Кроме того, так же, как и с DELETE, вы можете использовать ORDER BY и LIMIT вместе, чтобы контролировать, сколько строк обновляется из упорядоченного списка.

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

mysql> UPDATE actor SET last_name= UPPER('cruz')
    -> WHERE first_name LIKE 'PENELOPE'
    -> AND last_name LIKE 'GUINESS';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Как и ожидалось, MySQL сопоставил одну строку и изменил одну строку.

Чтобы контролировать количество обновлений, вы можете использовать комбинацию ORDER BY и LIMIT:

mysql> UPDATE payment SET last_update=NOW() LIMIT 10;
Query OK, 10 rows affected (0.01 sec)
Rows matched: 10 Changed: 10 Warnings: 0

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

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

Изучение баз данных и таблиц с помощью SHOW и mysqlshow

Мы уже объясняли, как можно использовать команду SHOW для получения информации о структуре базы данных, ее таблицах и столбцах таблицы. В этом разделе мы рассмотрим наиболее распространенные типы оператора SHOW с краткими примерами использования базы данных sakila. Программа командной строки mysqlshow выполняет те же функции, что и несколько вариантов команды SHOW, но без необходимости запуска клиента MySQL.

Оператор SHOW DATABASES перечисляет базы данных, к которым вы можете получить доступ. Если вы выполнили шаги по установке образца базы данных в разделе «Примеры моделирования отношений сущностей» и развернули модель банка в разделе «Создание модели ER базы данных банка», результат должен быть следующим:

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| bank_model         |
| employees          |
| mysql              |
| performance_schema |
| sakila             |
| sys                |
| world              |
+--------------------+
8 rows in set (0.01 sec)

Это базы данных, к которым вы можете получить доступ с помощью команды USE (обсуждается в главе 4); если у вас есть права доступа к другим базам данных на вашем сервере, они также будут перечислены. Вы можете видеть только те базы данных, для которых у вас есть некоторые привилегии, если только у вас нет глобальной привилегии SHOW DATABASES. Вы можете получить тот же эффект из командной строки, используя программу mysqlshow:

$ mysqlshow -uroot -pmsandbox -h 127.0.0.1 -P 3306

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

mysql> SHOW DATABASES LIKE 's%';
+---------------+
| Database (s%) |
+---------------+
| sakila        |
| sys           |
+---------------+
2 rows in set (0.00 sec)

Синтаксис оператора LIKE идентичен его использованию в SELECT.

Чтобы просмотреть оператор, используемый для создания базы данных, вы можете использовать оператор SHOW CREATE DATABASE. Например, чтобы увидеть, как вы создали sakila, введите:

mysql> SHOW CREATE DATABASE sakila;
************************** 1. row ***************************
       Database: sakila
Create Database: CREATE DATABASE `sakila` /*!40100 DEFAULT CHARACTER SET
utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */
1 row in set (0.00 sec)

Это, пожалуй, наименее захватывающее выражение SHOW; он только отображает выражение. Обратите внимание, однако, что включены некоторые дополнительные комментарии, /*! и */:

40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci
80016 DEFAULT ENCRYPTION='N'

Эти комментарии содержат специфичные для MySQL ключевые слова, содержащие инструкции, которые вряд ли будут поняты другими программами баз данных. Сервер базы данных, отличный от MySQL, будет игнорировать этот текст комментария, поэтому синтаксис может использоваться как MySQL, так и другим программным обеспечением сервера баз данных. Необязательный номер в начале комментария указывает на минимальную версию MySQL, которая может обрабатывать эту конкретную инструкцию (например, 40100 указывает на версию 4.01.00); более старые версии MySQL игнорируют такие инструкции. Вы узнаете о создании баз данных в главе 4.

Оператор SHOW TABLES выводит список таблиц в базе данных. Чтобы проверить таблицы в sakila, введите:

mysql> SHOW TABLES FROM sakila;
+----------------------------+
| Tables_in_sakila           |
+----------------------------+
| actor                      |
| actor_info                 |
| address                    |
| category                   |
| city                       |
| country                    |
| customer                   |
| customer_list              |
| film                       |
| film_actor                 |
| film_category              |
| film_list                  |
| film_text                  |
| inventory                  |
| language                   |
| nicer_but_slower_film_list |
| payment                    |
| rental                     |
| sales_by_film_category     |
| sales_by_store             |
| staff                      |
| ...                 |
+----------------------------+
23 rows in set (0.01 sec)

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

mysql> SHOW TABLES;

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

$ mysqlshow -uroot -pmsandbox -h 127.0.0.1 -P 3306 sakila

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

Оператор SHOW COLUMNS выводит список столбцов в таблице. Например, чтобы проверить столбцы country, введите:

mysql> SHOW COLUMNS FROM country;
*************************** 1. row ***************************
  Field: country_id
   Type: smallint unsigned
   Null: NO
    Key: PRI
Default: NULL
  Extra: auto_increment
*************************** 2. row ***************************
  Field: country
   Type: varchar(50)
   Null: NO
    Key:
Default: NULL
  Extra:
*************************** 3. row ***************************
  Field: last_update
   Type: timestamp
   Null: NO
    Key:
Default: CURRENT_TIMESTAMP
  Extra: DEFAULT_GENERATED on update CURRENT_TIMESTAMP
3 rows in set (0.00 sec)

Вывод сообщает имена всех столбцов, их типы и размеры, могут ли они быть NULL, являются ли они частью ключа, их значения по умолчанию и любую дополнительную информацию. Типы, ключи, значения NULL и значения по умолчанию обсуждаются далее в главе 4. Если вы еще не выбрали базу данных sakila с помощью команды USE, вы можете добавить имя базы данных перед именем таблицы, как в sakila.country. В отличие от предыдущих операторов SHOW, вы всегда можете увидеть все имена столбцов, если у вас есть доступ к таблице; не имеет значения, что у вас нет определенных привилегий для всех столбцов.

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

$ mysqlshow -uroot -pmsandbox -h 127.0.0.1 -P 3306 sakila country

Вы можете увидеть оператор, используемый для создания конкретной таблицы, используя оператор SHOW CREATE TABLE (мы также рассмотрим создание таблиц в главе 4). Некоторые пользователи предпочитают этот вывод выводу SHOW COLUMNS, поскольку он имеет знакомый формат оператора CREATE TABLE. Вот пример таблицы стран:

mysql> SHOW CREATE TABLE country\G
*************************** 1. row ***************************
       Table: country
Create Table: CREATE TABLE `country` (
  `country_id` smallint unsigned NOT NULL AUTO_INCREMENT,
  `country` varchar(50) NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
  CURRENT_TIMESTAMP,
  PRIMARY KEY (`country_id`)
) ENGINE=InnoDB AUTO_INCREMENT=110 DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

Глава 4
Работа со структурами базы данных

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

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

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

Создание и использование баз данных

Когда вы закончили проектирование базы данных, первый практический шаг, который нужно сделать с MySQL, — это создать ее. Вы делаете это с помощью оператора CREATE DATABASE. Предположим, вы хотите создать базу данных с именем lucy. Вот выражение, которое вы должны ввести:

mysql> CREATE DATABASE lucy;
Query OK, 1 row affected (0.10 sec)

Здесь мы предполагаем, что вы знаете, как подключаться с помощью клиента MySQL, как описано в главе 1. Мы также предполагаем, что вы можете подключаться как пользователь root или как другой пользователь, который может создавать, удалять и изменять структуры (подробное обсуждение привилегий пользователей можно найти в главе 8). Обратите внимание, что когда вы создаете базу данных, MySQL сообщает, что была затронута одна строка. На самом деле это не обычная строка в какой-либо конкретной базе данных, а новая запись, добавленная в список, который вы видите с помощью команды SHOW DATABASES.

После того как вы создали базу данных, следующим шагом будет ее использование, то есть выбор ее в качестве базы данных, с которой вы работаете. Вы делаете это с помощью команды MySQL USE:

mysql> USE lucy;
Database changed

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

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

mysql> CREATE DATABASE lucy;
ERROR 1007 (HY000): Can't create database 'lucy'; database exists

Вы можете избежать этой ошибки, добавив ключевую фразу IF NOT EXISTS в выражение:

mysql> CREATE DATABASE IF NOT EXISTS lucy;
Query OK, 0 rows affected (0.00 sec)

Вы можете видеть, что MySQL не пожаловался, но и ничего не сделал: сообщение 0 затронутых строк указывает на то, что никакие данные не были изменены. Это дополнение полезно, когда вы добавляете операторы SQL в сценарий: оно предотвращает прерывание сценария при ошибке.

Давайте посмотрим, как выбирать имена баз данных и использовать регистр символов. Имена баз данных определяют имена физических каталогов (или папок) на диске. В некоторых операционных системах имена каталогов вводятся с учетом регистра; на других регистр не имеет значения. Например, Unix-подобные системы, такие как Linux и macOS, обычно чувствительны к регистру, а Windows — нет. В результате имена баз данных имеют те же ограничения: если регистр имеет значение для операционной системы, он имеет значение и для MySQL. Например, на машине Linux LUCY, lucy и Lucy — это разные имена баз данных; в Windows они относятся только к одной базе данных. Использование неправильной заглавной буквы в Linux или macOS приведет к тому, что MySQL будет жаловаться:

mysql> SELECT SaKilA.AcTor_id FROM ACTor;
ERROR 1146 (42S02): Table 'sakila.ACTor' doesn't exist

Но под Windows это нормально работает.

Это поведение контролируется параметром lower_case_table_names. Если установлено значение 0, имена таблиц сохраняются, как было указано, а сравнения чувствительны к регистру. Если установлено значение 1, имена таблиц сохраняются на диске в нижнем регистре, а сравнения не чувствительны к регистру. Если для этого параметра установлено значение 2, имена таблиц сохраняются как были заданны, но сравниваются в нижнем регистре. В Windows значение по умолчанию — 1. В macOS значение по умолчанию — 2. В Linux значение 2 не поддерживается; вместо этого сервер устанавливает значение равным 0.

Существуют и другие ограничения на имена баз данных. Они могут иметь длину не более 64 символов. Вы также не должны использовать зарезервированные слова MySQL, такие как SELECT, FROM и USE, в качестве имен для структур; они могут запутать синтаксический анализатор MySQL, делая невозможным интерпретацию смысла ваших утверждений. Вы можете обойти это ограничение, заключив зарезервированное слово в обратные кавычки (`), но помнить об этом больше проблем, чем пользы. Кроме того, в именах нельзя использовать определенные символы, в частности, косую черту, обратную косую черту, точку с запятой и символы точки, а имя базы данных не может заканчиваться пробелом. Опять же, использование этих символов сбивает с толку анализатор MySQL и может привести к непредсказуемому поведению. Например, вот что происходит, когда вы вставляете точку с запятой в имя базы данных:

mysql> CREATE DATABASE IF NOT EXISTS lu;cy;
Query OK, 1 row affected (0.00 sec)
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'cy' at line 1

Поскольку в одной строке может находиться более одного оператора SQL, в результате создается база данных lu, а затем возникает ошибка из-за очень короткого и неожиданного оператора SQL cy;. Если вы действительно хотите создать базу данных с точкой с запятой в имени, вы можете сделать это с помощью обратных кавычек:

mysql> CREATE DATABASE IF NOT EXISTS `lu;cy`;
Query OK, 1 row affected (0.01 sec)

И вы можете видеть, что теперь у вас есть две новые базы данных:

mysql> SHOW DATABASES LIKE `lu%`;
+----------------+
| Database (lu%) |
+----------------+
| lu             |
| lu;cy          |
+----------------+
2 rows in set (0.01 sec)

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

В этом разделе рассматриваются темы по структурам таблиц. Мы покажем вам, как:

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

Основы

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

mysql> DROP DATABASE sakila;
Query OK, 23 rows affected (0.06 sec)

Оператор DROP обсуждается далее в конце этой главы в разделе «Удаление структур».

Для начала создайте базу данных sakila с помощью инструкции:

mysql> CREATE DATABASE sakila;
Query OK, 1 row affected (0.00 sec)

Затем выберите базу данных с помощью:

mysql> USE sakila;
Database changed

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

mysql> CREATE TABLE actor (
    -> actor_id SMALLINT UNSIGNED NOT NULL DEFAULT 0,
    -> first_name VARCHAR(45) DEFAULT NULL,
    -> last_name VARCHAR(45),
    -> last_update TIMESTAMP,
    -> PRIMARY KEY (actor_id)
    -> );
Query OK, 0 rows affected (0.01 sec)

Не паникуйте — несмотря на то, что MySQL сообщает, что были затронуты ноль строк, она создала таблицу:

mysql> SHOW tables;
+------------------+
| Tables_in_sakila |
+------------------+
| actor            |
+------------------+
1 row in set (0.01 sec)

Рассмотрим все это подробно. Команда CREATE TABLE состоит из трех основных разделов:

  1. Оператор CREATE TABLE, за которым следует имя создаваемой таблицы. В данном примере это actor.

  2. Список из одного или нескольких столбцов, которые необходимо добавить в таблицу. В этом примере мы добавили довольно много: actor_id SMALLINT UNSIGNED NOT NULL DEFAULT 0, first_name VARCHAR(45) DEFAULT NULL, last_name VARCHAR(45) и last_update TIMESTAMP. Мы обсудим это через мгновение.

  3. Необязательные ключевые определения. В этом примере мы определили один ключ: PRIMARY KEY (actor_id). Мы подробно обсудим ключи и индексы позже в этой главе.

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

Давайте обсудим характеристики колонки. Основной синтаксис следующий: name type [NOT NULL | NULL] [DEFAULT value]. Поле name — это имя столбца, и оно имеет те же ограничения, что и имена баз данных, как обсуждалось в предыдущем разделе. Оно может содержать не более 64 символов, не допускаются обратная и прямая косая черта, не допускаются точки, оно не может заканчиваться пробелом, а чувствительность к регистру зависит от базовой операционной системы. Поле type определяет, как и что хранится в столбце; например, мы видели, что оно может быть установлено в VARCHAR для строк, SMALLINT для чисел или TIMESTAMP для даты и времени.

Если указать NOT NULL, строка недействительна без значения для столбца; если вы укажете NULL или опустите это предложение, строка может существовать без значения для столбца. Если вы укажете value в предложении DEFAULT, оно будет использовано для заполнения столбца, если вы не предоставите данные иным образом; это особенно полезно, когда вы часто повторно используете значение по умолчанию, такое как название страны. value должно быть константой (например, 0, "cat" или 20060812045623), за исключением случаев, когда столбец имеет тип TIMESTAMP. Типы подробно обсуждаются в разделе «Типы столбцов».

Функции NOT NULL и DEFAULT можно использовать вместе. Если вы укажете NOT NULL и добавите значение DEFAULT, будет использовано значение по умолчанию, если вы не укажете значение для столбца. Иногда это работает нормально:

mysql> INSERT INTO actor(first_name) VALUES ('John');
Query OK, 1 row affected (0.01 sec)

А иногда и нет:

mysql> INSERT INTO actor(first_name) VALUES ('Elisabeth');
ERROR 1062 (23000): Duplicate entry '0' for key 'actor.PRIMARY'

Работает это или нет, зависит от лежащих в его основе ограничений и условий базы данных: в этом примере, actor_id имеет значение по умолчанию 0, но это также первичный ключ. Наличие двух строк с одинаковым значением первичного ключа не допускается, поэтому вторая попытка вставить строку без значений (и результирующее значение первичного ключа равно 0) завершается неудачей. Мы подробно обсуждаем первичные ключи в разделе «Ключи и индексы».

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

Именование столбцов и других объектов базы данных является чем-то вроде личных предпочтений при запуске заново (вы можете получить вдохновение, просмотрев примеры баз данных) или вопросом соблюдения стандартов при работе с существующей кодовой базой. В общем, старайтесь избегать повторений: в таблице с именем actor используйте имя столбца first_name, а не actor_first_name, которое выглядело бы избыточным, если в сложном запросе ему предшествовало имя таблицы (actor.actor_first_name по сравнению с actor.first_name). Исключением является использование вездесущего имени столбца id; либо избегайте этого, либо добавляйте имя таблицы для ясности (например, actor_id). Хорошей практикой является использование символа подчеркивания для разделения слов. Вы можете использовать другой символ, например, тире или косую черту, но вы должны помнить, что должны заключать имена в обратные кавычки (например, actor-id). Вы также можете вообще опустить форматирование слов, но «ВерблюжийРегистр», возможно, труднее читать. Как и в случае с именами баз данных и таблиц, максимально допустимая длина имени столбца составляет 64 символа.

Сопоставление и наборы символов

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

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

В наших предыдущих примерах сравнения строк мы проигнорировали проблему сортировки и кодировки и просто позволили MySQL использовать свои значения по умолчанию. В версиях MySQL до 8.0 набор символов по умолчанию — latin1, а сопоставление по умолчанию — latin1_swedish_ci. MySQL 8.0 изменил значения по умолчанию, и теперь кодировка по умолчанию — utf8mb4, а сопоставление по умолчанию — utf8mb4_0900_ai_ci. MySQL можно настроить для использования различных наборов символов и порядка сортировки на уровне соединения, базы данных, таблицы и столбца. Результаты, показанные здесь, относятся к MySQL 8.0.

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

mysql> SHOW CHARACTER SET;
+----------+---------------------------------+---------------------+--------+
| Charset  | Description                     | Default collation   | Maxlen |
+----------+---------------------------------+---------------------+--------+
| armscii8 | ARMSCII-8 Armenian              | armscii8_general_ci |      1 |
| ascii    | US ASCII                        | ascii_general_ci    |      1 |
| big5     | Big5 Traditional Chinese        | big5_chinese_ci     |      2 |
| binary   | Binary pseudo charset           | binary              |      1 |
| cp1250   | Windows Central European        | cp1250_general_ci   |      1 |
| cp1251   | Windows Cyrillic                | cp1251_general_ci   |      1 |
| ...                                                                       |
| ujis     | EUC-JP Japanese                 | ujis_japanese_ci    |      3 |
| utf16    | UTF-16 Unicode                  | utf16_general_ci    |      4 |
| utf16le  | UTF-16LE Unicode                | utf16le_general_ci  |      4 |
| utf32    | UTF-32 Unicode                  | utf32_general_ci    |      4 |
| utf8     | UTF-8 Unicode                   | utf8_general_ci     |      3 |
| utf8mb4  | UTF-8 Unicode                   | utf8mb4_0900_ai_ci  |      4 |
+----------+---------------------------------+---------------------+--------+
41 rows in set (0.00 sec)

Например, набор символов latin1 на самом деле является кодовой страницей Windows 1252, которая поддерживает западноевропейские языки. Параметр сортировки по умолчанию для этого набора символов — latin1_swedish_ci, который следует шведским соглашениям по сортировке символов с диакритическими знаками (английский язык обрабатывается так, как вы ожидаете). Это сопоставление нечувствительно к регистру, на что указывают буквы ci. Наконец, каждый символ занимает 1 байт. Для сравнения, если вы используете набор символов utf8mb4 по умолчанию, каждый символ будет занимать до 4 байт памяти. Иногда имеет смысл изменить значение по умолчанию. Например, нет смысла хранить данные, закодированные в base64 (которые по определению являются ASCII), в utf8mb4.

Точно так же вы можете перечислить порядок сопоставления и наборы символов, к которым они применяются:

mysql> SHOW COLLATION;
+---------------------+----------+-----+---------+...+---------------+
| Collation           | Charset  | Id  | Default |...| Pad_attribute |
+---------------------+----------+-----+---------+...+---------------+
| armscii8_bin        | armscii8 |  64 |         |...| PAD SPACE     |
| armscii8_general_ci | armscii8 |  32 | Yes     |...| PAD SPACE     |
| ascii_bin           | ascii    |  65 |         |...| PAD SPACE     |
| ascii_general_ci    | ascii    |  11 | Yes     |...| PAD SPACE     |
| ...                                            |...|               |
| utf8mb4_0900_ai_ci  | utf8mb4  | 255 | Yes     |...| NO PAD        |
| utf8mb4_0900_as_ci  | utf8mb4  | 305 |         |...| NO PAD        |
| utf8mb4_0900_as_cs  | utf8mb4  | 278 |         |...| NO PAD        |
| utf8mb4_0900_bin    | utf8mb4  | 309 |         |...| NO PAD        |
| ...                                            |...|               |
| utf8_unicode_ci     | utf8     | 192 |         |...| PAD SPACE     |
| utf8_vietnamese_ci  | utf8     | 215 |         |...| PAD SPACE     |
+---------------------+----------+-----+---------+...+---------------+
272 rows in set (0.02 sec)

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

mysql> SHOW VARIABLES LIKE 'c%';
+--------------------------+--------------------------------+
| Variable_name            | Value                          |
+--------------------------+--------------------------------+
| ...                                                       |
| character_set_client     | utf8mb4                        |
| character_set_connection | utf8mb4                        |
| character_set_database   | utf8mb4                        |
| character_set_filesystem | binary                         |
| character_set_results    | utf8mb4                        |
| character_set_server     | utf8mb4                        |
| character_set_system     | utf8                           |
| character_sets_dir       | /usr/share/mysql-8.0/charsets/ |
| ...                                                       |
| collation_connection     | utf8mb4_0900_ai_ci             |
| collation_database       | utf8mb4_0900_ai_ci             |
| collation_server         | utf8mb4_0900_ai_ci             |
| ...                                                       |
+--------------------------+--------------------------------+
21 rows in set (0.00 sec)

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

mysql> CREATE DATABASE rose DEFAULT CHARACTER SET utf8mb4
    -> COLLATE utf8mb4_ru_0900_as_cs;
Query OK, 1 row affected (0.00 sec)

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

Другие особенности

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

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

mysql> CREATE TABLE IF NOT EXISTS actor (
    -> actor_id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
    -> first_name VARCHAR(45) NOT NULL,
    -> last_name VARCHAR(45) NOT NULL,
    -> last_update TIMESTAMP NOT NULL DEFAULT
    -> CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    -> PRIMARY KEY  (actor_id),
    -> KEY idx_actor_last_name (last_name));
Query OK, 0 rows affected, 1 warning (0.01 sec)

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

mysql> SHOW WARNINGS;
+-------+------+------------------------------+
| Level | Code | Message                      |
+-------+------+------------------------------+
| Note  | 1050 | Table 'actor' already exists |
+-------+------+------------------------------+
1 row in set (0.01 sec)

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

Функция AUTO_INCREMENT для числовых столбцов

Эта функция позволяет автоматически создавать уникальные идентификаторы для таблицы. Мы подробно обсуждаем это в разделе «Функция AUTO_INCREMENT».

Комментарии столбца

Вы можете добавить комментарий к столбцу; это отображается, когда вы используете команду SHOW CREATE TABLE, которую мы обсудим позже в этом разделе.

Ограничения внешнего ключа

Вы можете указать MySQL проверить, соответствуют ли данные в одном или нескольких столбцах данным в другой таблице. Например, база данных sakila имеет ограничение внешнего ключа для столбца city_id таблицы address, ссылаясь на столбец city_id таблицы city. Это означает, что невозможно иметь адрес в городе, которого нет в таблице city. Мы представили ограничения внешнего ключа в главе 2 и рассмотрим, какие механизмы поддерживают ограничения внешнего ключа в разделе «Альтернативные механизмы хранения». Не каждый механизм хранения в MySQL поддерживает внешние ключи.

Создание временных таблиц

Если вы создаете таблицу, используя ключевую фразу CREATE TEMPORARY TABLE, она будет удалена (dropped) при закрытии соединения. Это полезно для копирования и переформатирования данных, поскольку вам не нужно помнить об очистке. Иногда временные таблицы также используются в качестве оптимизации для хранения некоторых промежуточных данных.

Дополнительные параметры таблицы

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

Контроль над индексными структурами

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

Разделение

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

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

mysql> SHOW CREATE TABLE actor\G
*************************** 1. row ***************************
       Table: actor
Create Table: CREATE TABLE `actor` (
  `actor_id` smallint unsigned NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) NOT NULL,
  `last_name` varchar(45) NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
        ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`actor_id`),
  KEY `idx_actor_last_name` (`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
        COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

Вы заметите, что выходные данные включают контент, добавленный MySQL, которого не было в нашем исходном операторе CREATE TABLE:

Типы столбцов

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

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

Мы начнем с числовых типов данных, а точнее с целочисленных типов или типов, содержащих определенные целые числа. Во-первых, два самых популярных целочисленных типа:

INT[(width)] [UNSIGNED] [ZEROFILL]

Это наиболее часто используемый числовой тип; он хранит целые числа в диапазоне от –2 147 483 648 до 2 147 483 647. Если добавлено необязательное ключевое слово UNSIGNED, диапазон составляет от 0 до 4 294 967 295. Ключевое слово INT является сокращением от INTEGER, и они могут использоваться взаимозаменяемо. Для столбца INT требуется 4 байта дискового пространства.

INT, как и другие целочисленные типы, имеет два свойства, специфичных для MySQL: необязательное width и аргументы ZEROFILL. Они не являются частью стандарта SQL и, начиная с MySQL 8.0, устарели. Тем не менее, вы наверняка заметите их во многих кодовых базах, поэтому мы кратко рассмотрим их оба.

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

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

В некоторых приложениях, где полезны ZEROFILL и width, можно использовать функцию LPAD() или хранить числа в формате столбцов CHAR.

BIGINT[(width)] [UNSIGNED] [ZEROFILL]

В мире растущих объемов данных использование таблиц с количеством строк в миллиарды становится все более распространенным явлением. Даже для простых столбцов типа id может потребоваться более широкий диапазон, чем обеспечивает обычный INT. BIGINT решает эту проблему. Это большой целочисленный тип со знаком в диапазоне от –9 223 372 036 854 775 808 до 9 223 372 036 854 775 807. BIGINT без знака может хранить числа от 0 до 18 446 744 073 709 551 615. Для столбцов этого типа потребуется 8 байт памяти.

Внутри все вычисления в MySQL выполняются с использованием значений BIGINT или DOUBLE со знаком. Важным следствием этого является то, что вы должны быть очень осторожны при работе с очень большими числами. Есть две проблемы, о которых следует знать. Во-первых, большие целые числа без знака, превышающие 9 223 372 036 854 775 807, следует использовать только с битовыми функциями. Во-вторых, если результат арифметической операции больше 9 223 372 036 854 775 807, могут наблюдаться неожиданные результаты.

Например:

mysql> CREATE TABLE test_bigint (id BIGINT UNSIGNED);
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO test_bigint VALUES (18446744073709551615);
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO test_bigint VALUES (18446744073709551615-1);
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO test_bigint VALUES (184467440737095516*100);
ERROR 1690 (22003): BIGINT value is out of range in '(184467440737095516 * 100)'

Несмотря на то, что 18 446 744 073 709 551 600 меньше 18 446 744 073 709 551 615, поскольку для внутреннего умножения используется BIGINT со знаком, наблюдается ошибка выхода за диапазон.

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

SMALLINT[(width)] [UNSIGNED] [ZEROFILL]

Хранит небольшие целые числа в диапазоне от –32 768 до 32 767 со знаком и от 0 до 65 535 без знака. Занимает 2 байта памяти.

TINYINT[(width)] [UNSIGNED] [ZEROFILL]

Наименьший числовой тип данных, хранящий еще меньшие целые числа. Диапазон этого типа составляет от –128 до 127 со знаком и от 0 до 255 без знака. Занимает всего 1 байт памяти.

BOOL[(width)]

Сокращение от BOOLEAN и синоним TINYINT(1). Обычно логические типы принимают только два значения: true или false. Однако, поскольку BOOL в MySQL является целочисленным типом, вы можете хранить значения от –128 до 127 в BOOL. Значение 0 будет считаться ложным, а все ненулевые значения — истинными. Также можно использовать специальные истинные и ложные псевдонимы для 1 и 0 соответственно. Вот некоторые примеры:

mysql> CREATE TABLE test_bool (i BOOL);
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO test_bool VALUES (true),(false);
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0
mysql> INSERT INTO test_bool VALUES (1),(0),(-128),(127);
Query OK, 4 rows affected (0.02 sec)
Records: 4  Duplicates: 0  Warnings: 0
mysql> SELECT i, IF(i,'true','false') FROM test_bool;
+------+----------------------+
| i    | IF(i,'true','false') |
+------+----------------------+
|    1 | true                 |
|    0 | false                |
|    1 | true                 |
|    0 | false                |
| -128 | true                 |
|  127 | true                 |
+------+----------------------+
6 rows in set (0.01 sec)
MEDIUMINT[(width)] [UNSIGNED] [ZEROFILL]

Сохраняет значения в диапазоне от –8 388 608 до 8 388 607 со знаком и в диапазоне от 0 до 16 777 215 без знака. Занимает 3 байта памяти.

BIT[(M)]

Специальный тип, используемый для хранения битовых значений. M указывает количество битов на значение и по умолчанию равно 1, если этот аргумент опущен. MySQL использует синтаксис b'value для двоичных значений.

Типы с фиксированной запятой

Типы данных DECIMAL и NUMERIC в MySQL одинаковы, поэтому, хотя мы опишем здесь только DECIMAL, это описание также относится и к NUMERIC. Основное различие между типами с фиксированной и плавающей запятой заключается в точности. Для типов с фиксированной запятой извлекаемое значение идентично сохраненному значению; это не всегда относится к типам, содержащим десятичные точки, таким как типы FLOAT и DOUBLE, описанные ниже. Это наиболее важное свойство типа данных DECIMAL, который является широко используемым числовым типом в MySQL:

DECIMAL[(width[,decimals])] [UNSIGNED] [ZEROFILL]

Хранит число с фиксированной запятой, такое как зарплата или расстояние, с общим числом разрядов width, из которых некоторое меньшее число является decimals, следующими за десятичной запятой. Например, столбец, объявленный как цена DECIMAL(6,2), может использоваться для хранения значений в диапазоне от –9 999,99 до 9 999,99. цена DECIMAL(10,4) допускает такие значения, как 123 456,1234.

До MySQL 5.7, если вы пытались сохранить значение за пределами этого диапазона, оно сохранялось как ближайшее значение в допустимом диапазоне. Например, 100 будет сохранено как 99,99, а –100 будет сохранено как –99,99. Однако, начиная с версии 5.7.5, режим SQL по умолчанию включает режим STRICT_TRANS_TABLES, который запрещает это и другие небезопасные действия. Использование старого поведения возможно, но может привести к потере данных.

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

Параметр width является необязательным, и если он опущен, предполагается значение 10. Количество decimals также является необязательным, и если оно опущено, предполагается значение 0; максимальное значение decimals не может превышать значение width. Максимальное значение width — 65, а максимальное значение decimals — 30.

Если вы сохраняете только положительные значения, вы можете использовать ключевое слово UNSIGNED, как описано для INT. Если вы хотите заполнить нулями, используйте ключевое слово ZEROFILL для того же поведения, что и для INT. Ключевое слово DECIMAL имеет три идентичных взаимозаменяемых варианта: DEC, NUMERIC и FIXED.

Значения в столбцах DECIMAL хранятся в двоичном формате. Этот формат использует 4 байта для каждых девяти цифр.

Типы с плавающей запятой

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

Зачем вам приблизительные значения? Ответ заключается в том, что многие числа с десятичной точкой являются приближениями реальных величин. Например, предположим, что вы зарабатываете 50 000 долларов в год и хотите хранить их как ежемесячную заработную плату. Если перевести это в месячную сумму, получится 4166 долларов плюс 66 и 2/3 цента. Если вы сохраните это как 4 166,67 доллара, этого недостаточно для преобразования в годовую заработную плату (поскольку 12, умноженное на 4 166,67 доллара, равно 50 000,04 доллара). Однако, если вы храните 2/3 с достаточным количеством знаков после запятой, это более близкое приближение. Вы обнаружите, что это достаточно точно, чтобы правильно умножить, чтобы получить исходное значение в высокоточной среде, такой как MySQL, используя только небольшое округление. Вот где полезны DOUBLE и FLOAT: они позволяют хранить такие значения, как 2/3 или число пи, с большим количеством знаков после запятой, позволяя точное приближенное представление точных величин. Позже вы можете использовать функцию ROUND() для восстановления результатов с заданной точностью.

Давайте продолжим предыдущий пример, используя DOUBLE. Предположим, вы создаете таблицу следующим образом:

mysql> CREATE TABLE wage (monthly DOUBLE);
Query OK, 0 rows affected (0.09 sec)

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

mysql> INSERT INTO wage VALUES (50000/12);
Query OK, 1 row affected (0.00 sec)

И посмотрите, что хранится:

mysql> SELECT * FROM wage;
+----------------+
| monthly        |
+----------------+
| 4166.666666666 |
+----------------+
1 row in set (0.00 sec)

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

mysql> SELECT monthly*12 FROM wage;
+--------------------+
| monthly*12         |
+--------------------+
| 49999.999999992004 |
+--------------------+
1 row in set (0.00 sec)

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

mysql> SELECT ROUND(monthly*12,5) FROM wage;
+---------------------+
| ROUND(monthly*12,5) |
+---------------------+
|         50000.00000 |
+---------------------+
1 row in set (0.00 sec)

Но точность до восьми знаков после запятой не приведет к исходному значению:

mysql> SELECT ROUND(monthly*12,8) FROM wage;
+---------------------+
| ROUND(monthly*12,8) |
+---------------------+
|      49999.99999999 |
+---------------------+
1 row in set (0.00 sec)

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

Вот подробности типов FLOAT и DOUBLE:

FLOAT[(width, decimals)] [UNSIGNED] [ZEROFILL] или
FLOAT[(precision)] [UNSIGNED] [ZEROFILL]

Сохраняет числа с плавающей запятой. Имеет два необязательных синтаксиса: первый допускает необязательное количество decimals и необязательную width отображения, а второй допускает необязательную precision, которая контролирует точность приближения, измеренного в битах. Без параметров (типичное использование) этот тип хранит небольшие 4-байтовые значения с плавающей запятой одинарной точности. Когда precision находится в диапазоне от 0 до 24, происходит поведение по умолчанию. Когда precision находится в диапазоне от 25 до 53, тип ведет себя как DOUBLE. Параметр width не влияет на то, что хранится, только на то, что отображается. Параметры UNSIGNED и ZEROFILL ведут себя как INT.

DOUBLE[(width, decimals)] [UNSIGNED] [ZEROFILL]

Сохраняет числа с плавающей запятой. Это позволяет указать необязательное количество decimals и необязательную width отображения. Без параметров (типичное использование) этот тип хранит обычные 8-байтовые значения с плавающей запятой двойной точности. Параметр width не влияет на то, что хранится, только на то, что отображается. Параметры UNSIGNED и ZEROFILL ведут себя как INT. Тип DOUBLE имеет два идентичных синонима: REAL и DOUBLE PRECISION.

Типы строк

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

[NATIONAL] VARCHAR(width) [CHARACTER SET charset_name] [COLLATE collation_name]

Вероятно, это наиболее часто используемый тип строки. VARCHAR хранит строки переменной длины до максимальной width. Максимальное значение width составляет 65 535 символов. Большая часть информации, применимой к этому типу, применима и к другим типам строк.

Типы CHAR и VARCHAR очень похожи, но есть несколько важных отличий. VARCHAR берет на себя один или два дополнительных байта служебных данных для хранения значения строки, в зависимости от того, меньше или больше значение, чем 255 байтов. Обратите внимание, что этот размер отличается от длины строки в символах, так как для некоторых символов может потребоваться до 4 байтов пространства. Тогда может показаться очевидным, что VARCHAR менее эффективен. Однако это не всегда так. Поскольку VARCHAR может хранить строки произвольной длины (вплоть до определенной width), для более коротких строк потребуется меньше места для хранения, чем для CHAR аналогичной длины.

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

mysql> CREATE TABLE test_varchar_trailing(d VARCHAR(2) UNIQUE);
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO test_varchar_trailing VALUES ('a'), ('a ');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0
mysql> SELECT d, LENGTH(d) FROM test_varchar_trailing;
+------+-----------+
| d    | LENGTH(d) |
+------+-----------+
| a    |         1 |
| a    |         2 |
+------+-----------+
2 rows in set (0.00 sec)

Вторая строка, которую мы вставили, имеет завершающий пробел, и, поскольку width столбца d равна 2, этот пробел учитывается при определении уникальности строки. Однако если мы попытаемся вставить строку с двумя завершающими пробелами:

mysql> INSERT INTO test_varchar_trailing VALUES ('a  ');
ERROR 1062 (23000): Duplicate entry 'a '
for key 'test_varchar_trailing.d'

MySQL отказывается принимать новую строку. VARCHAR(2) неявно обрезает конечные пробелы за пределами установленной width, поэтому сохраняемое значение меняется с "a " (с двойным пробелом после a) на "a " (с одинарным пробелом после a). Поскольку у нас уже есть строка с таким значением, сообщается об ошибке дублирования записи. Это поведение для VARCHAR и TEXT можно контролировать, изменив параметры сортировки столбцов. Некоторые сопоставления, такие как latin1_bin, имеют атрибут PAD SPACE, означающий, что при извлечении они дополняются пробелами по ширине width. Это не влияет на хранение, но влияет на проверки уникальности, а также на работу операторов GROUP BY и DISTINCT, которые мы обсудим в главе 5. Вы можете проверить, является ли параметр сортировки PAD SPACE или NO PAD, запустив команду SHOW COLLATION, как мы показали в разделе «Сопоставление и наборы символов». Давайте посмотрим на эффект в действии, создав таблицу с параметрами сортировки PAD SPACE:

mysql> CREATE TABLE test_varchar_pad_collation(
    -> data VARCHAR(5) CHARACTER SET latin1
    -> COLLATE latin1_bin UNIQUE);
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO test_varchar_pad_collation VALUES ('a');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO test_varchar_pad_collation VALUES ('a ');
ERROR 1062 (23000): Duplicate entry 'a '
for key 'test_varchar_pad_collation.data'

Сопоставление NO PAD — это новое дополнение MySQL 8.0. В предыдущих выпусках MySQL, которые вы все еще можете часто видеть в использовании, каждая сортировка неявно имеет атрибут PAD SPACE. Таким образом, в MySQL 5.7 и более ранних версиях единственным вариантом сохранения конечных пробелов является использование двоичного типа: VARBINARY или BLOB.

Сортировка и сравнение типов VARCHAR, CHAR и TEXT происходит в соответствии с сопоставлением назначенного набора символов. Вы видите, что можно указать набор символов, а также параметры сортировки для каждого отдельного столбца строкового типа. Также можно указать двоичный набор символов, который эффективно преобразует VARCHAR в VARBINARY. Не путайте двоичную кодировку с атрибутом BINARY для кодировки; последний является сокращением только для MySQL для указания двоичного (_bin) сопоставления.

Более того, можно указать параметры сортировки непосредственно в предложении ORDER BY. Доступные сопоставления будут зависеть от набора символов столбца. Продолжая работу с таблицей test_varchar_pad_collation, можно сохранить там символ ä, а затем посмотреть, как сопоставления влияют на порядок строк:

mysql> INSERT INTO test_varchar_pad_collation VALUES ('ä'), ('z');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0
mysql> SELECT * FROM test_varchar_pad_collation
    -> ORDER BY data COLLATE latin1_german1_ci;
+------+
| data |
+------+
| a    |
| ä    |
| z    |
+------+
3 rows in set (0.00 sec)
mysql> SELECT * FROM test_varchar_pad_collation
    -> ORDER BY data COLLATE latin1_swedish_ci;
+------+
| data |
+------+
| a    |
| z    |
| ä    |
+------+
3 rows in set (0.00 sec)

Атрибут NATIONAL (или эквивалентная ему короткая форма NCHAR) — это стандартный способ SQL указать, что столбец строкового типа должен использовать предопределенный набор символов. MySQL использует utf8 в качестве этой кодировки. Однако важно отметить, что MySQL 5.7 и 8.0 расходятся во мнениях относительно того, что такое utf8: первая использует его как псевдоним для utf8mb3, а вторая — для utf8mb4. Таким образом, лучше не использовать атрибут NATIONAL, а также неоднозначные псевдонимы. Лучшая практика с любыми столбцами и данными, связанными с текстом, — быть максимально однозначными и конкретными.

[NATIONAL] CHAR(width) [CHARACTER SET charset_name] [COLLATE collation_name]

CHAR хранит строку фиксированной длины (например, имя, адрес или город) длиной width. Если width не указана, предполагается CHAR(1). Максимальное значение width равно 255. Как и в случае с VARCHAR, значения в столбцах CHAR всегда сохраняются с указанной длиной. Одна буква, хранящаяся в столбце CHAR(255), будет занимать 255 байт (в кодировке latin1) и будет дополнена пробелами. Заполнение удаляется при чтении данных, если не включен режим SQL PAD_CHAR_TO_FULL_LENGTH. Стоит еще раз упомянуть, что это означает, что строки, хранящиеся в столбцах CHAR, потеряют все свои конечные пробелы.

В прошлом width столбца CHAR часто ассоциировалась с размером в байтах. Это не всегда так сейчас, и это определенно не так по умолчанию. Многобайтовые наборы символов, такие как utf8mb4 по умолчанию в MySQL 8.0, могут привести к гораздо большим значениям. InnoDB фактически кодирует столбцы фиксированной длины как столбцы переменной длины, если их максимальный размер превышает 768 байт. Таким образом, в MySQL 8.0 InnoDB по умолчанию будет хранить столбец CHAR(255) так же, как и столбец VARCHAR. Вот пример:

mysql> CREATE TABLE test_char_length(
    ->   utf8char CHAR(10) CHARACTER SET utf8mb4
    -> , asciichar CHAR(10) CHARACTER SET binary
    -> );
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO test_char_length VALUES ('Plain text', 'Plain text');
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO test_char_length VALUES ('的開源軟體', 'Plain text');
Query OK, 1 row affected (0.00 sec)
mysql> SELECT LENGTH(utf8char), LENGTH(asciichar) FROM test_char_length;
+------------------+-------------------+
| LENGTH(utf8char) | LENGTH(asciichar) |
+------------------+-------------------+
|               10 |                10 |
|               15 |                10 |
+------------------+-------------------+
2 rows in set (0.00 sec)

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

BINARY[(width)] и VARBINARY(width)

Эти типы очень похожи на CHAR и VARCHAR, но хранят двоичные строки. Двоичные строки имеют специальный набор двоичных символов и сопоставление, и их сортировка зависит от числовых значений байтов в хранимых значениях. Вместо строк символов хранятся строки байтов. В более раннем обсуждении VARCHAR мы описали двоичную кодировку и атрибут BINARY. Только двоичная кодировка «преобразует» VARCHAR или CHAR в соответствующую двоичную форму. Применение атрибута BINARY к набору символов не изменит того факта, что строки символов сохраняются. В отличие от VARCHAR и CHAR, здесь width равна количеству байтов. Когда width не указана для BINARY, по умолчанию она равна 1.

Как и в случае с CHAR, данные в столбце BINARY дополняются справа. Однако, будучи двоичными данными, они дополняются нулевыми байтами, обычно записываемыми как 0x00 или \0. BINARY рассматривает пробел как значимый символ, а не как заполнение. Если вам нужно хранить важные для вас данные, которые могут заканчиваться нулевыми байтами, используйте типы VARBINARY или BLOB.

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

mysql> CREATE TABLE test_binary_data (
    ->   d1 BINARY(16)
    -> , d2 VARBINARY(16)
    -> , d3 CHAR(16)
    -> , d4 VARCHAR(16)
    -> );
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO test_binary_data VALUES (
    ->   'something'
    -> , 'something'
    -> , 'something'
    -> , 'something');
Query OK, 1 row affected (0.00 sec)
mysql> SELECT d1, d2, d3, d4 FROM test_binary_data;
*************************** 1. row ***************************
d1: 0x736F6D657468696E6700000000000000
d2: 0x736F6D657468696E67
d3: something
d4: something
1 row in set (0.00 sec)
mysql> SELECT UPPER(d2), UPPER(d4) FROM test_binary_data;
*************************** 1. row ***************************
UPPER(d2): 0x736F6D657468696E67
UPPER(d4): SOMETHING
1 row in set (0.01 sec)

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

mysql> SELECT CAST(d1 AS CHAR) d1t, CAST(d2 AS CHAR) d2t
    -> FROM test_binary_data;
+------------------+-----------+
| d1t              | d2t       |
+------------------+-----------+
| something        | something |
+------------------+-----------+
1 row in set (0.00 sec)

Вы также можете видеть, что заполнение BINARY было преобразовано в пробелы при выполнении приведения.

BLOB[(width)] и
TEXT[(width)] [CHARACTER SET charset_name] [COLLATE collation_name]

BLOB и TEXT обычно используются для хранения больших данных. Вы можете думать о BLOB как о VARBINARY, содержащей столько данных, сколько вам нужно, и то же самое для TEXT и VARCHAR. Типы BLOB и TEXT могут хранить до 65 535 байтов или символов соответственно. Как обычно, обратите внимание, что существуют многобайтовые кодировки. Атрибут width является необязательным, и когда он указан, MySQL фактически изменит тип данных BLOB или TEXT на любой наименьший тип, способный хранить такое количество данных. Например, BLOB(128) приведет к использованию TINYBLOB:

mysql> CREATE TABLE test_blob(data BLOB(128));
Query OK, 0 rows affected (0.07 sec)
mysql> DESC test_blob;
+-------+----------+------+-----+---------+-------+
| Field | Type     | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+-------+
| data  | tinyblob | YES  |     | NULL    |       |
+-------+----------+------+-----+---------+-------+
1 row in set (0.00 sec)

Для типа BLOB и родственных типов данные обрабатываются точно так же, как и в случае VARBINARY. То есть набор символов не предполагается, а сравнение и сортировка основаны на числовых значениях фактически сохраненных байтов. Для TEXT вы можете точно указать желаемую кодировку и сопоставление. Для обоих типов и их вариантов при INSERT не выполняется заполнение, а при SELECT не выполняется обрезка, что делает их идеальными для хранения данных в том виде, в котором они есть. Кроме того, предложение DEFAULT не разрешено, и когда индекс создается для столбца BLOB или TEXT, необходимо определить префикс, ограничивающий длину индексируемых значений. Подробнее об этом мы поговорим в разделе «Ключи и индексы».

Одно потенциальное различие между BLOB и TEXT заключается в их обработке конечных пробелов. Как мы уже показали, VARCHAR и TEXT могут дополнять строки в зависимости от используемой сортировки. BLOB и VARBINARY используют двоичный набор символов с одной двоичной сортировкой без заполнения и невосприимчивы к путанице сортировки и связанным с этим проблемам. Иногда может быть хорошим выбором использовать эти типы для дополнительной безопасности. Кроме того, до MySQL 8.0 это были единственные типы, которые сохраняли конечные пробелы.

TINYBLOB и
TINYTEXT [CHARACTER SET charset_name] [COLLATE collation_name]

Идентичны BLOB и TEXT соответственно, за исключением того, что можно сохранить максимум 255 байтов или символов.

MEDIUMBLOB и
MEDIUMTEXT [CHARACTER SET charset_name] [COLLATE collation_name]

Идентичны BLOB и TEXT соответственно, за исключением того, что можно сохранить максимум 16 777 215 байтов или символов. Типы LONG и LONG VARCHAR сопоставляются с типом данных MEDIUMTEXT для совместимости.

LONGBLOB и
LONGTEXT [CHARACTER SET charset_name] [COLLATE collation_name]

Они идентичны BLOB и TEXT соответственно, за исключением того, что можно хранить максимум 4 ГБ данных. Обратите внимание, что это жесткое ограничение даже в случае LONGTEXT, поэтому количество символов в многобайтовых кодировках может быть меньше 4 294 967 295. Эффективный максимальный размер данных, которые может хранить клиент, будет ограничен объемом доступной памяти, а также значением переменной max_packet_size, которая по умолчанию равна 64 МБ.

ENUM(value1[,value2[, ...]]) [CHARACTER SET charset_name] [COLLATE collation_name]

Этот тип хранит список или перечисление (enumeration) строковых значений. Для столбца типа ENUM можно задать значение из списка value1, value2 и т. д., максимум до 65 535 различных значений. Хотя значения хранятся и извлекаются в виде строк, в базе данных хранится целочисленное представление. Столбец ENUM может содержать значения NULL (сохраненные как NULL), пустую строку '' (сохраненную как 0) или любые допустимые элементы (сохраненные как 1, 2, 3 и т. д.). Вы можете предотвратить принятие значений NULL, объявив столбец как NOT NULL при создании таблицы.

Этот тип предлагает компактный способ хранения значений из списка предопределенных значений, таких как названия штатов или стран. Рассмотрим этот пример, используя названия фруктов; имя может быть любым из предопределенных значений Apple, Orange или Pear (в дополнение к NULL и пустой строке):

mysql> CREATE TABLE fruits_enum
    -> (fruit_name ENUM('Apple', 'Orange', 'Pear'));
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO fruits_enum VALUES ('Apple');
Query OK, 1 row affected (0.00 sec)

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

mysql> INSERT INTO fruits_enum VALUES ('Banana');
ERROR 1265 (01000): Data truncated for column 'fruit_name' at row 1

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

mysql> INSERT INTO fruits_enum VALUES ('Apple,Orange');
ERROR 1265 (01000): Data truncated for column 'fruit_name' at row 1

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

mysql> SELECT * FROM fruits_enum;
+------------+
| fruit_name |
+------------+
| Apple      |
+------------+
1 row in set (0.00 sec)

Более ранние версии MySQL выдавали предупреждение вместо ошибки и сохраняли пустую строку вместо недопустимого значения. Это поведение можно включить, отключив строгий режим SQL по умолчанию. Также можно указать значение по умолчанию, отличное от пустой строки:

mysql> CREATE TABLE new_fruits_enum
    -> (fruit_name ENUM('Apple', 'Orange', 'Pear')
    -> DEFAULT 'Pear');
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO new_fruits_enum VALUES();
Query OK, 1 row affected (0.02 sec)
mysql> SELECT * FROM new_fruits_enum;
+------------+
| fruit_name |
+------------+
| Pear       |
+------------+
1 row in set (0.00 sec)

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

SET( value1 [, value2 [, ...]]) [CHARACTER SET charset_name] [COLLATE collation_name]

Этот тип хранит набор строковых значений. Столбец типа SET может содержать ноль или более значений из списка value1, value2 и т. д., максимум до 64 различных значений. Хотя значения являются строками, то, что хранится в базе данных, представляет собой целочисленное представление. SET отличается от ENUM тем, что каждая строка может хранить только одно значение ENUM в столбце, но может хранить несколько значений SET. Этот тип полезен для хранения выбора вариантов из списка, например пользовательских настроек. Рассмотрим этот пример, используя названия фруктов; имя может быть любой комбинацией предопределенных значений:

mysql> CREATE TABLE fruits_set
    -> ( fruit_name SET('Apple', 'Orange', 'Pear') );
Query OK, 0 rows affected (0.08 sec)
mysql> INSERT INTO fruits_set VALUES ('Apple');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO fruits_set VALUES ('Banana');
ERROR 1265 (01000): Data truncated for column 'fruit_name' at row 1
mysql> INSERT INTO fruits_set VALUES ('Apple,Orange');
Query OK, 1 row affected (0.00 sec)
mysql> SELECT * FROM fruits_set;
+--------------+
| fruit_name   |
+--------------+
| Apple        |
| Apple,Orange |
+--------------+
2 rows in set (0.00 sec)

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

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

Использование фиксированного размера с типом CHAR часто быстрее, чем использование переменного размера с VARCHAR, поскольку сервер MySQL знает, где начинается и заканчивается каждая строка, и может быстро пропустить строки, чтобы найти ту, которая ему нужна. Однако с полями фиксированной длины любое неиспользуемое пространство тратится впустую. Например, если вы разрешаете использовать до 40 символов в названии города, то CHAR(40) всегда будет использовать 40 символов, независимо от того, насколько длинным является название города. Если вы объявите название города как VARCHAR(40), то вы израсходуете ровно столько места, сколько вам нужно, плюс 1 байт для хранения длины имени. Если среднее название города состоит из 10 символов, это означает, что использование поля переменной длины займет в среднем на 29 байт меньше на запись. Это может иметь большое значение, если вы храните миллионы адресов.

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

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

Эти типы служат для хранения определенных меток времени, дат или диапазонов времени. Особое внимание следует уделить работе с часовыми поясами. Мы постараемся объяснить детали, но этот раздел и документацию стоит перечитать позже, когда вам действительно понадобится работать с часовыми поясами. Типы даты и времени в MySQL:

DATE

Сохраняет и отображает дату в формате YYYY-MM-DD для диапазона от 1000-01-01 до 9999-12-31. Даты всегда должны вводиться в виде тройки год, месяц, день, но формат ввода может варьироваться, как показано в следующих примерах:

YYYY-MM-DD or YY-MM-DD
Можно указывать год с двумя или с четырьмя цифрами. Мы настоятельно рекомендуем вам использовать четырехзначную версию, чтобы избежать путаницы с веком. На практике, если вы используете двузначную версию, вы обнаружите, что от 70 до 99 интерпретируются как от 1970 до 1999, а от 00 до 69 интерпретируются как от 2000 до 2069.
YYYY/MM/DD, YYYY:MM:DD, YY-MM-DD, или другие форматы с пунктуацией
MySQL позволяет использовать любые знаки препинания для разделения компонентов даты. Мы рекомендуем использовать тире и, опять же, избегать двузначных чисел года.
YYYY-M-D, YYYY-MM-D, or YYYY-M-DD
Когда используется пунктуация (опять же, разрешены любые знаки препинания), дни и месяцы могут быть указаны как таковые. Например, 2 февраля 2006 г. можно указать как 2006-2-2. Двузначные эквиваленты года доступны, но не рекомендуются.
YYYYMMDD or YYMMDD
Пунктуация может быть опущена в обоих стилях даты, но последовательность цифр должна состоять из шести или восьми цифр.

Вы также можете ввести дату, указав дату и время в форматах, описанных ниже для DATETIME и TIMESTAMP, но в столбце DATE хранится только компонент даты. Независимо от типа ввода, тип хранения и отображения всегда YYYY-MM-DD. Нулевая дата 0000-00-00 разрешена во всех версиях и может использоваться для представления неизвестного или фиктивного значения. Если введенная дата выходит за пределы допустимого диапазона, сохраняется нулевая дата. Однако только версии MySQL до 5.6 включительно позволяют это по умолчанию. И 5.7, и 8.0 по умолчанию устанавливают режимы SQL, запрещающие такое поведение: STRICT_TRANS_TABLES, NO_ZERO_DATE и NO_ZERO_IN_DATE.

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

mysql> SET sql_mode=CONCAT(@@sql_mode,
    -> ',STRICT_TRANS_TABLES',
    -> ',NO_ZERO_DATE', ',NO_ZERO_IN_DATE');

Вот несколько примеров вставки дат на сервере MySQL 8.0 с настройками по умолчанию:

mysql> CREATE TABLE testdate (mydate DATE);
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO testdate VALUES ('2020/02/0');
ERROR 1292 (22007): Incorrect date value: '2020/02/0' for column 'mydate' at row 1
mysql> INSERT INTO testdate VALUES ('2020/02/1');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO testdate VALUES ('2020/02/31');
ERROR 1292 (22007): Incorrect date value: '2020/02/31' for column 'mydate' at row 1
mysql> INSERT INTO testdate VALUES ('2020/02/100');
ERROR 1292 (22007): Incorrect date value: '2020/02/100' for column 'mydate' at row 1

После выполнения операторов INSERT в таблице будут следующие данные:

mysql> SELECT * FROM testdate;
+------------+
| mydate     |
+------------+
| 2020-02-01 |
+------------+
1 row in set (0.00 sec)

MySQL защитил вас от «плохих» данных, хранящихся в вашей таблице. Иногда вам может понадобиться сохранить фактический ввод и вручную обработать его позже. Вы можете сделать это, удалив вышеупомянутые режимы SQL из списка режимов в переменной sql_mode. В этом случае после выполнения предыдущих операторов INSERT вы получите следующие данные:

mysql> SELECT * FROM testdate;
+------------+
| mydate     |
+------------+
| 2020-02-00 |
| 2020-02-01 |
| 0000-00-00 |
| 0000-00-00 |
+------------+
4 rows in set (0.01 sec)

Еще раз обратите внимание, что дата отображается в формате YYYY-MM-DD, независимо от того, как она была введена.

TIME [fraction]

Сохраняет время в формате HHH:MM:SS в диапазоне от –838:59:59 до 838:59:59. Это полезно для хранения продолжительности некоторых действий. Значения, которые можно сохранить, находятся за пределами диапазона 24-часового формата, что позволяет вычислять и сохранять большие различия между значениями времени (до 34 дней, 22 часов, 59 минут и 59 секунд). fraction в TIME и других связанных типах данных задает точность долей секунды в диапазоне от 0 до 6. Значение по умолчанию равно 0, что означает, что дробные секунды не сохраняются.

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

DD HH:MM:SS[.fraction], HH:MM:SS[.fraction], DD HH:MM, HH:MM, DD HH, или SS[.fraction]

DD представляет собой однозначное или двузначное значение дней в диапазоне от 0 до 34. Значение DD отделяется от значения часа, HH, пробелом, а остальные компоненты разделяются двоеточием. Обратите внимание, что MM:SS не является допустимой комбинацией, так как ее нельзя отличить от HH:MM. Если в определении TIME не указана fraction или установлено значение 0, вставка доли секунды приведет к округлению значений до ближайшей секунды.

Например, если вы вставите 2 13:25:58.999999 в столбец TIME с fraction 0, значение 61:25:59 будет сохранено, поскольку сумма 2 дня (48 часов) и 13 часов составляет 61 час. Начиная с MySQL 5.7, установленный по умолчанию режим SQL запрещает вставку неправильных значений. Однако можно включить старое поведение. Затем, если вы попытаетесь вставить значение, выходящее за пределы, будет сгенерировано предупреждение, и значение будет ограничено максимальным доступным временем. Точно так же, если вы попытаетесь вставить недопустимое значение, генерируется предупреждение, и значение устанавливается равным нулю. Вы можете использовать команду SHOW WARNINGS, чтобы сообщить подробности предупреждения, сгенерированного предыдущим оператором SQL. Мы рекомендуем придерживаться строгого режима SQL по умолчанию. В отличие от типа DATE, по-видимому, нет смысла разрешать неверные записи TIME, помимо упрощения управления ошибками на стороне приложения и поддержания устаревшего поведения.

Давайте попробуем все это на практике:

mysql> CREATE TABLE test_time(id SMALLINT, mytime TIME);
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO test_time VALUES(1, "2 13:25:59");
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO test_time VALUES(2, "35 13:25:59");
ERROR 1292 (22007): Incorrect time value: '35 13:25:59'
for column 'mytime' at row 1
mysql> INSERT INTO test_time VALUES(3, "900.32");
Query OK, 1 row affected (0.00 sec)
mysql> SELECT * FROM test_time;
+------+----------+
| id   | mytime   |
+------+----------+
|    1 | 61:25:59 |
|    3 | 00:09:00 |
+------+----------+
2 rows in set (0.00 sec)
H:M:S, а также одно-, двух- и трехзначные комбинации

Вы можете использовать различные комбинации цифр при вставке или обновлении данных; MySQL преобразует их во внутренний формат времени и последовательно отображает. Например, 1:1:3 эквивалентно 01:01:03. Разное количество цифр можно смешивать; например, 1:12:3 эквивалентно 01:12:03. Рассмотрим эти примеры:

mysql> CREATE TABLE mytime (testtime TIME);
Query OK, 0 rows affected (0.12 sec)
mysql> INSERT INTO mytime VALUES
    -> ('-1:1:1'), ('1:1:1'),
    -> ('1:23:45'), ('123:4:5'),
    -> ('123:45:6'), ('-123:45:6');
Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0
mysql> SELECT * FROM mytime;
+------------+
| testtime   |
+------------+
|  -01:01:01 |
|   01:01:01 |
|   01:23:45 |
|  123:04:05 |
|  123:45:06 |
| -123:45:06 |
+------------+
5 rows in set (0.01 sec)

Обратите внимание, что часы отображаются двумя цифрами для значений в диапазоне от –99 до 99.

HHMMSS, MMSS, и SS

Знаки препинания можно опустить, но последовательность цифр должна состоять из двух, четырех или шести цифр. Обратите внимание, что крайняя правая пара цифр всегда интерпретируется как значение SS (секунды), вторая крайняя правая пара (если присутствует) как MM (минуты), а третья крайняя правая пара (если присутствует) как HH (часы). В результате такое значение, как 1222, интерпретируется как 12 минут и 22 секунды, а не как 12 часов и 22 минуты.

Вы также можете ввести время, указав дату и время в форматах, описанных для DATETIME и TIMESTAMP, но в столбце TIME сохраняется только компонент времени. Независимо от типа ввода, тип хранения и отображения всегда HH:MM:SS. Нулевое время 00:00:00 может использоваться для представления неизвестного или фиктивного значения.

TIMESTAMP[(fraction)]

Сохраняет и отображает пару даты и времени в формате YYYY-MM-DD HH:MM:SS[.fraction][time zone offset] для диапазона от 1970-01-01 00:00:01.000000 до 2038-01-19 03:14:07.999999. Этот тип очень похож на тип DATETIME, но есть несколько отличий. Оба типа принимают модификатор часового пояса для входного значения MySQL 8.0, и оба типа будут хранить и представлять данные одинаково любому клиенту в том же часовом поясе. Однако значения в столбцах TIMESTAMP внутренне всегда хранятся в часовом поясе UTC, что позволяет автоматически получать локальный часовой пояс для клиентов в разных часовых поясах. Это само по себе является очень важным различием, о котором следует помнить. Возможно, TIMESTAMP удобнее использовать при работе с разными часовыми поясами.

До MySQL 5.6 только тип TIMESTAMP поддерживал автоматическую инициализацию и обновление. Более того, это мог сделать только один такой столбец в данной таблице. Однако, начиная с версии 5.6, и TIMESTAMP, и DATETIME поддерживают поведение, и это может делать любое количество столбцов.

Значения, хранящиеся в столбце TIMESTAMP, всегда соответствуют шаблону YYYY-MM-DD HH:MM:SS[.fraction][time zone offset], но значения могут быть предоставлены в широком диапазоне форматов:

YYYY-MM-DD HH:MM:SS или YY-MM-DD HH:MM:SS
Компоненты даты и времени подчиняются тем же смягченным ограничениям, что и компоненты DATE и TIME, описанные ранее. Это включает в себя допустимость любых знаков препинания, включая (в отличие от TIME) гибкость пунктуации, используемой в компоненте времени. Например, 0 – допустимо.
YYYYMMDDHHMMSS или YYMMDDHHMMSS
Знаки препинания можно не ставить, но длина строки должна быть 12 или 14 цифр. Мы рекомендуем использовать только однозначную 14-значную версию по причинам, описанным для типа DATE. Вы можете указать значения другой длины без разделителей, но мы не рекомендуем этого делать.

Рассмотрим подробнее функцию автоматического обновления. Вы контролируете это, добавляя следующие атрибуты в определение столбца при создании таблицы или позже, как мы объясним в разделе «Изменение структур»:

  1. Если вы хотите, чтобы метка времени устанавливалась только при вставке новой строки в таблицу, добавьте DEFAULT CURRENT_TIMESTAMP в конец объявления столбца.

  2. Если вам не нужна отметка времени по умолчанию, но вы хотите, чтобы текущее время использовалось при каждом обновлении данных в строке, добавьте ON UPDATE CURRENT_TIMESTAMP в конец объявления столбца.

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

Если вы не укажете DEFAULT NULL или NULL для столбца TIMESTAMP, он будет иметь 0 в качестве значения по умолчанию.

YEAR[(4)]

Сохраняет четырехзначный год в диапазоне от 1901 до 2155, а также нулевой год, 0000. Недопустимые значения преобразуются в нулевой год. Вы можете вводить значения года в виде строк (например, '2005') или целых чисел (например, 2005). Для типа YEAR требуется 1 байт дискового пространства.

В более ранних версиях MySQL можно было указать параметр digits, передав либо 2, либо 4. Двухзначная версия хранила значения от 70 до 69, представляющие 1970–2069 годы. MySQL 8.0 не поддерживает двузначный тип YEAR, а указание параметра digits для целей отображения устарело.

DATETIME[(fraction)]

Сохраняет и отображает пару даты и времени в формате YYYY-MM-DD HH:MM:SS[.fraction][time zone offset] для диапазона от 1000-01-01 00:00:00 до 9999-12-31 23:59:59. Что касается TIMESTAMP, сохраняемое значение всегда соответствует шаблону YYYY-MM-DD HH:MM:SS, но значение можно вводить в тех же форматах, которые указаны в описании TIMESTAMP. Если вы назначаете только дату столбцу DATETIME, предполагается нулевое время 00:00:00. Если вы назначаете только время столбцу DATETIME, предполагается нулевая дата 0000-00-00. Этот тип имеет те же функции автоматического обновления, что и TIMESTAMP. Если для столбца DATETIME не указан атрибут NOT NULL, по умолчанию используется значение NULL; в противном случае по умолчанию используется значение 0. В отличие от TIMESTAMP, значения DATETIME не преобразуются в часовой пояс UTC для хранения.

Другие типы

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

Типы пространственных данных связаны с хранением геометрических объектов, а в MySQL есть типы, соответствующие классам OpenGIS. Работа с этими типами — тема, достойная отдельной книги.

Тип данных JSON обеспечивает встроенное хранилище действительных документов JSON. До MySQL 5.7 JSON обычно хранился в столбце TEXT или аналогичном столбце. Однако у этого есть много недостатков: например, документы не проверяются и не выполняется оптимизация хранилища (весь JSON просто хранится в текстовом виде). С собственным типом JSON он хранится в двоичном формате. Если бы мы резюмировали одним предложением: используйте тип данных JSON для JSON, дорогой читатель.

Ключи и индексы

Вы обнаружите, что почти все используемые вами таблицы будут иметь предложение PRIMARY KEY, объявленное в операторе CREATE TABLE, а иногда и несколько предложений KEY. Причины, по которым вам нужны первичный ключ и вторичные ключи, обсуждались в главе 2. В этом разделе обсуждается, как объявляются первичные ключи, что происходит за кулисами, когда вы это делаете, и почему вы, возможно, захотите также создать другие ключи и индексы для ваших данных.

Первичный ключ (primary key) однозначно идентифицирует каждую строку в таблице. Что еще более важно, для механизма хранения InnoDB по умолчанию первичный ключ также используется в качестве кластеризованного индекса (clustered index). Это означает, что все фактические данные таблицы хранятся в структуре индекса. Это отличается от MyISAM, который хранит данные и индексы отдельно. Когда таблица использует кластеризованный индекс, она называется кластеризованной таблицей. Как мы уже говорили, в кластеризованной таблице каждая строка хранится в индексе, а не в том, что обычно называется кучей (heap). Кластеризация таблицы приводит к тому, что ее строки сортируются в соответствии с порядком кластеризованного индекса и фактически физически сохраняются на листовых страницах этого индекса. Для каждой таблицы не может быть более одного кластеризованного индекса. Для таких таблиц вторичные индексы ссылаются на записи в кластеризованном индексе, а не на фактические строки таблицы. Как правило, это приводит к повышению производительности запросов, хотя может отрицательно сказаться на записи. InnoDB не позволяет выбирать между кластеризованными и некластеризованными таблицами; это дизайнерское решение, которое вы не можете изменить.

Первичные ключи, как правило, являются рекомендуемой частью любой структуры базы данных, но для InnoDB они необходимы. На самом деле, если вы не укажете предложение PRIMARY KEY при создании таблицы InnoDB, MySQL будет использовать первый столбец UNIQUE NOT NULL в качестве основы для кластеризованного индекса. Если такой столбец недоступен, создается скрытый кластеризованный индекс на основе значений ID, присвоенных InnoDB каждой строке.

Учитывая, что InnoDB является механизмом хранения MySQL по умолчанию и стандартом де-факто в настоящее время, мы сосредоточимся на его поведении в этой главе. Альтернативные механизмы хранения, такие как MyISAM, MEMORY или MyRocks, будут обсуждаться в разделе «Альтернативные механизмы хранения».

Как упоминалось ранее, когда первичный ключ определен, он становится кластеризованным индексом, и все данные в таблице хранятся в листовых блоках этого индекса. InnoDB использует индексы B-дерева (точнее, вариант B+tree), за исключением индексов пространственных типов данных, которые используют структуру R-дерева. Другие механизмы хранения могут реализовывать другие типы индексов, но если механизм хранения таблицы не указан, можно предположить, что все индексы являются B-деревьями.

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

Отойдя от внутренних деталей InnoDB, когда вы объявляете первичный ключ для таблицы в MySQL, создается структура, в которой хранится информация о том, где хранятся данные из каждой строки в таблице. Эта информация называется индексом, и ее цель — ускорить поиск, использующий первичный ключ. Например, когда вы объявляете PRIMARY KEY (actor_id) в таблице actor в базе данных sakila, MySQL создает структуру, которая позволяет очень быстро находить строки, соответствующие определенному actor_id (или диапазону идентификаторов).

Это полезно, например, для сопоставления актеров с фильмами или фильмов с категориями. Вы можете отобразить индексы, доступные в таблице, с помощью команды SHOW INDEX (или SHOW INDEXES):

mysql> SHOW INDEX FROM category\G
*************************** 1. row ***************************
        Table: category
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: category_id
    Collation: A
  Cardinality: 16
     Sub_part: NULL
       Packed: NULL
         Null:
   Index_type: BTREE
      Comment:
Index_comment:
      Visible: YES
   Expression: NULL
1 row in set (0.00 sec)

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

Обратите внимание, что все столбцы, являющиеся частью первичного ключа, должны быть объявлены как NOT NULL, поскольку они должны иметь значение, чтобы строка была действительной. Без индекса единственный способ найти строки в таблице — прочитать каждую с диска и проверить, соответствует ли она искомому category_id. Для таблиц с большим количеством строк такой исчерпывающий последовательный поиск выполняется очень медленно. Однако вы не можете просто проиндексировать все; мы вернемся к этому вопросу в конце этого раздела.

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

mysql> SHOW CREATE TABLE actor\G
*************************** 1. row ***************************
       Table: actor
Create Table: CREATE TABLE `actor` (
  `actor_id` smallint unsigned NOT NULL AUTO_INCREMENT,
  ...
  `last_name` varchar(45) NOT NULL,
  ...
  PRIMARY KEY (`actor_id`),
  KEY `idx_actor_last_name` (`last_name`)
) ...
1 row in set (0.00 sec)

Вы можете видеть, что ключевое слово KEY используется, чтобы сообщить MySQL, что необходим дополнительный индекс. В качестве альтернативы вы можете использовать слово INDEX вместо KEY. За этим ключевым словом следует имя индекса, а затем индексируемый столбец заключен в круглые скобки. Вы также можете добавлять индексы после того, как таблицы созданы — фактически, вы можете практически что-либо изменить в таблице после ее создания. Это обсуждается в разделе «Изменение структур».

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

mysql> CREATE TABLE customer_mod (
    -> customer_id smallint unsigned NOT NULL AUTO_INCREMENT,
    -> first_name varchar(45) NOT NULL,
    -> last_name varchar(45) NOT NULL,
    -> email varchar(50) DEFAULT NULL,
    -> PRIMARY KEY (customer_id),
    -> KEY idx_names_email (first_name, last_name, email));
Query OK, 0 rows affected (0.06 sec)

Вы можете видеть, что мы добавили индекс первичного ключа в столбец идентификатора customer_id, а также добавили еще один индекс, называемый idx_names_email, который включает столбцы first_name, last_name и email в указанном порядке. Давайте теперь рассмотрим, как вы можете использовать этот дополнительный индекс.

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

mysql> SELECT * FROM customer_mod WHERE
    -> first_name = 'Rose' AND
    -> last_name = 'Williams' AND
    -> email = 'rose.w@nonexistent.edu';

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

mysql> EXPLAIN SELECT * FROM customer_mod WHERE
    -> first_name = 'Rose' AND
    -> last_name = 'Williams' AND
    -> email = 'rose.w@nonexistent.edu'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: customer_mod
   partitions: NULL
         type: ref
possible_keys: idx_names_email
          key: idx_names_email
      key_len: 567
          ref: const,const,const
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

Вы можете видеть, что MySQL сообщает, что possible_keys — это idx_names_email (это означает, что индекс может быть использован для этого запроса) и что ключ, который было решено использовать, — это idx_names_email. Итак, то, что вы ожидаете, и то, что происходит, совпадают, и это хорошая новость! Вы узнаете больше об операторе EXPLAIN в главе 7.

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

mysql> SELECT * FROM customer_mod WHERE
    -> first_name = 'Rose';

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

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

mysql> SELECT * FROM customer_mod WHERE
    -> first_name = 'Rose' AND
    -> last_name = 'Williams';

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

mysql> SELECT * FROM customer_mod WHERE
    -> last_name = 'Williams' AND
    -> email = 'rose.w@nonexistent.edu';

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

  1. Крайний левый столбец, указанный в предложении KEY (или PRIMARY KEY), должен быть в запросе.

  2. Запрос не должен содержать предложений OR для неиндексированных столбцов.

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

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

Чтобы завершить этот раздел, нам нужно обсудить некоторые особенности, касающиеся вторичных ключей в InnoDB. Помните, что все данные таблицы хранятся в листьях кластеризованного индекса. Это означает, что, используя пример с actor, если нам нужно получить данные first_name при фильтрации по last_name, даже если мы можем использовать idx_actor_last_name для быстрой фильтрации, нам нужно будет получить доступ к данным по первичному ключу. Как следствие, каждый вторичный ключ в InnoDB имеет все столбцы первичного ключа, неявно добавленные к его определению. Таким образом, наличие излишне длинных первичных ключей в InnoDB приводит к значительному раздуванию вторичных ключей.

Это также можно увидеть в выводе EXPLAIN (обратите внимание на Extra: Using index в первом выводе первой команды):

mysql> EXPLAIN SELECT actor_id, last_name FROM actor WHERE last_name = 'Smith'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: actor
   partitions: NULL
         type: ref
possible_keys: idx_actor_last_name
          key: idx_actor_last_name
      key_len: 182
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)
mysql> EXPLAIN SELECT first_name FROM actor WHERE last_name = 'Smith'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: actor
   partitions: NULL
         type: ref
possible_keys: idx_actor_last_name
          key: idx_actor_last_name
      key_len: 182
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

По сути, idx_actor_last_name является покрывающим индексом (covering index) для первого запроса, а это означает, что InnoDB может извлечь все необходимые данные только из этого индекса. Однако для второго запроса InnoDB придется выполнить дополнительный поиск кластеризованного индекса, чтобы получить значение для столбца first_name.

Функция AUTO_INCREMENT

Собственная функция MySQL AUTO_INCREMENT позволяет вам создать уникальный идентификатор для строки без выполнения запроса SELECT. Вот как это работает. Возьмем снова упрощенную таблицу actor:

mysql> CREATE TABLE actor (
    -> actor_id smallint unsigned NOT NULL AUTO_INCREMENT,
    -> first_name varchar(45) NOT NULL,
    -> last_name varchar(45) NOT NULL,
    -> PRIMARY KEY (actor_id)
    -> );
Query OK, 0 rows affected (0.03 sec)

В эту таблицу можно вставлять строки без указания actor_id:

mysql> INSERT INTO actor VALUES (NULL, 'Alexander', 'Kaidanovsky');
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO actor VALUES (NULL, 'Anatoly', 'Solonitsyn');
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO actor VALUES (NULL, 'Nikolai', 'Grinko');
Query OK, 1 row affected (0.00 sec)

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

mysql> SELECT * FROM actor;
+----------+------------+-------------+
| actor_id | first_name | last_name   |
+----------+------------+-------------+
|        1 | Alexander  | Kaidanovsky |
|        2 | Anatoly    | Solonitsyn  |
|        3 | Nikolai    | Grinko      |
+----------+------------+-------------+
3 rows in set (0.00 sec)

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

Рассмотрим, как работает эта функция. Вы можете видеть, что столбец actor_id объявлен как целое число с предложениями NOT NULL AUTO_INCREMENT. AUTO_INCREMENT сообщает MySQL, что, когда значение для этого столбца не указано, выделенное значение должно быть на единицу больше, чем максимальное значение, хранящееся в настоящее время в таблице. Последовательность AUTO_INCREMENT начинается с 1 для пустой таблицы.

Предложение NOT NULL требуется для столбцов AUTO_INCREMENT; когда вы вставляете NULL (или 0, хотя это не рекомендуется), сервер MySQL автоматически находит следующий доступный идентификатор и присваивает его новой строке. Вы можете вручную вставить отрицательные значения, если столбец не был определен как UNSIGNED; однако для следующего автоматического приращения MySQL просто использует наибольшее (положительное) значение в столбце или начинает с 1, если положительных значений нет.

Функция AUTO_INCREMENT имеет следующие требования:

MySQL поддерживает различные механизмы хранения; мы поговорим об этом подробнее в разделе «Альтернативные механизмы хранения». При использовании типа таблицы MyISAM, отличного от используемого по умолчанию, вы можете использовать функцию AUTO_INCREMENT для ключей, состоящих из нескольких столбцов. По сути, вы можете иметь несколько независимых счетчиков в одном столбце AUTO_INCREMENT. Однако это невозможно с InnoDB.

Хотя функция AUTO_INCREMENT полезна, ее нельзя перенести в другие среды баз данных, и она скрывает логические шаги по созданию новых идентификаторов. Это также может привести к двусмысленности; например, удаление или усечение таблицы приведет к сбросу счетчика, но удаление выбранных строк (с предложением WHERE) не сбрасывает счетчик. Более того, если в транзакцию вставляется строка, а затем эта транзакция откатывается, идентификатор все равно будет израсходован. В качестве примера создадим таблицу count, содержащую автоматически увеличивающееся поле counter:

mysql> CREATE TABLE count (counter INT AUTO_INCREMENT KEY);
Query OK, 0 rows affected (0.13 sec)
mysql> INSERT INTO count VALUES (),(),(),(),(),();
Query OK, 6 rows affected (0.01 sec)
Records: 6  Duplicates: 0  Warnings: 0
mysql> SELECT * FROM count;
+---------+
| counter |
+---------+
| 1       |
| 2       |
| 3       |
| 4       |
| 5       |
| 6       |
+---------+
6 rows in set (0.00 sec)

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

mysql> DELETE FROM count WHERE counter > 4;
Query OK, 2 rows affected (0.00 sec)
mysql> INSERT INTO count VALUES (),(),(),(),(),();
Query OK, 6 rows affected (0.00 sec)
Records: 6  Duplicates: 0  Warnings: 0
mysql> SELECT * FROM count;
+---------+
| counter |
+---------+
| 1       |
| 2       |
| 3       |
| 4       |
| 7       |
| 8       |
| 9       |
| 10      |
| 11      |
| 12      |
+---------+
10 rows in set (0.00 sec)

Здесь мы видим, что счетчик не сбрасывается и продолжается с 7. Однако если мы усекаем таблицу, таким образом удаляя все данные, счетчик сбрасывается до 1:

mysql> TRUNCATE TABLE count;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO count VALUES (),(),(),(),(),();
Query OK, 6 rows affected (0.01 sec)
Records: 6  Duplicates: 0  Warnings: 0
mysql> SELECT * FROM count;
+---------+
| counter |
+---------+
| 1       |
| 2       |
| 3       |
| 4       |
| 5       |
| 6       |
+---------+
6 rows in set (0.00 sec)

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

Изменение структур

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

Добавление, удаление и изменение столбцов

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

Давайте начнем с рассмотрения того, как вы изменяете существующие столбцы. Рассмотрим пример, в котором мы переименовываем столбец таблицы. В таблице language есть столбец с именем last_update, который содержит время изменения записи. Чтобы изменить имя этого столбца на last_updated_time, вы должны написать:

mysql> ALTER TABLE language RENAME COLUMN last_update TO last_updated_time;
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0

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

Выполнение операторов DDL требует специальных внутренних механизмов, включая специальную блокировку — это хорошо, так как вы, вероятно, не хотели бы, чтобы таблицы изменялись во время выполнения ваших запросов! Эти специальные блокировки называются блокировками метаданных в MySQL, и мы даем подробный обзор того, как они работают в разделе «Блокировки метаданных».

Обратите внимание, что все операторы DDL, в том числе те, которые выполняются через интерактивный DDL, требуют получения блокировки метаданных. В этом смысле оперативные операторы DDL не являются такими уж «онлайновыми», но они не будут полностью блокировать целевую таблицу во время выполнения. Выполнение операторов DDL на работающей системе под нагрузкой — рискованное предприятие: даже оператор, который должен выполняться почти мгновенно, может нанести ущерб. Мы рекомендуем вам внимательно прочитать о блокировке метаданных в главе 6 и в ссылке на документацию MySQL, а также поэкспериментировать с выполнением различных операторов DDL с одновременной загрузкой и без нее. Это может быть не слишком важно, пока вы изучаете MySQL, но мы считаем, что стоит предупредить вас заранее. После этого давайте вернемся к нашему ALTER таблицы language.

Вы можете проверить результат с помощью оператора SHOW COLUMNS:

mysql> SHOW COLUMNS FROM language;
+-------------------+------------------+------+-----+-------------------+...
| Field             | Type             | Null | Key | Default           |...
+-------------------+------------------+------+-----+-------------------+...
| language_id       | tinyint unsigned | NO   | PRI | NULL              |...
| name              | char(20)         | NO   |     | NULL              |...
| last_updated_time | timestamp        | NO   |     | CURRENT_TIMESTAMP |...
+-------------------+------------------+------+-----+-------------------+...
3 rows in set (0.01 sec)

В предыдущем примере мы использовали оператор ALTER TABLE с ключевым словом RENAME COLUMN. Это функция MySQL 8.0. В качестве альтернативы мы могли бы использовать ALTER TABLE с ключевым словом CHANGE для совместимости:

mysql> ALTER TABLE language CHANGE last_update last_updated_time TIMESTAMP
    -> NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

В этом примере вы можете видеть, что мы предоставили оператору ALTER TABLE четыре параметра с ключевым словом CHANGE:

  1. Название таблицы, language

  2. Исходное имя столбца, last_update

  3. Имя нового столбца, last_updated_time

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

Вы должны предоставить все четыре; это означает, что вам нужно повторно указать тип и любые предложения, которые сопровождают его. В этом примере, поскольку мы используем MySQL 8.0 с настройками по умолчанию, TIMESTAMP больше не имеет явных значений по умолчанию. Как видите, использовать RENAME COLUMN намного проще, чем CHANGE.

Если вы хотите изменить тип и предложения столбца, но не его имя, вы можете использовать ключевое слово MODIFY:

mysql> ALTER TABLE language MODIFY name CHAR(20) DEFAULT 'n/a';
Query OK, 0 rows affected (0.14 sec)
Records: 0 Duplicates: 0 Warnings: 0

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

mysql> ALTER TABLE language CHANGE name name CHAR(20) DEFAULT 'n/a';
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0

Будьте осторожны при изменении типов:

Предположим, вы хотите добавить дополнительный столбец в существующую таблицу. Вот как это сделать с помощью инструкции ALTER TABLE:

mysql> ALTER TABLE language ADD native_name CHAR(20);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0

Вы должны указать ключевое слово ADD, новое имя столбца, а также тип столбца и условия. В этом примере новый столбец, native_name, добавляется в качестве последнего столбца в таблице, как показано с помощью инструкции SHOW COLUMNS:

mysql> SHOW COLUMNS FROM artist;
+-------------------+------------------+------+-----+-------------------+...
| Field             | Type             | Null | Key | Default           |...
+-------------------+------------------+------+-----+-------------------+...
| language_id       | tinyint unsigned | NO   | PRI | NULL              |...
| name              | char(20)         | YES  |     | n/a               |...
| last_updated_time | timestamp        | NO   |     | CURRENT_TIMESTAMP |...
| native_name       | char(20)         | YES  |     | NULL              |...
+-------------------+------------------+------+-----+-------------------+...
4 rows in set (0.00 sec)

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

mysql> ALTER TABLE language ADD native_name CHAR(20) FIRST;
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0
mysql> SHOW COLUMNS FROM language;
+-------------------+------------------+------+-----+-------------------+...
| Field             | Type             | Null | Key | Default           |...
+-------------------+------------------+------+-----+-------------------+...
| native_name       | char(20)         | YES  |     | NULL              |...
| language_id       | tinyint unsigned | NO   | PRI | NULL              |...
| name              | char(20)         | YES  |     | n/a               |...
| last_updated_time | timestamp        | NO   |     | CURRENT_TIMESTAMP |...
+-------------------+------------------+------+-----+-------------------+...
4 rows in set (0.01 sec)

Если вы хотите добавить его в определенную позицию, используйте ключевое слово AFTER:

mysql> ALTER TABLE language ADD native_name CHAR(20) AFTER name;
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0
mysql> SHOW COLUMNS FROM language;
+-------------------+------------------+------+-----+-------------------+...
| Field             | Type             | Null | Key | Default           |...
+-------------------+------------------+------+-----+-------------------+...
| language_id       | tinyint unsigned | NO   | PRI | NULL              |...
| name              | char(20)         | YES  |     | n/a               |...
| native_name       | char(20)         | YES  |     | NULL              |...
| last_updated_time | timestamp        | NO   |     | CURRENT_TIMESTAMP |...
+-------------------+------------------+------+-----+-------------------+...
4 rows in set (0.00 sec)

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

mysql> ALTER TABLE language DROP native_name;
Query OK, 0 rows affected (0.07 sec)
Records: 0 Duplicates: 0 Warnings: 0

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

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

mysql> ALTER TABLE language ADD native_name CHAR(255), MODIFY name CHAR(255);
Query OK, 6 rows affected (0.06 sec)
Records: 6 Duplicates: 0 Warnings: 0

Обратите внимание, что на этот раз вы видите, что шесть записей были изменены. В предыдущих командах ALTER TABLE MySQL сообщал, что никакие строки не были затронуты. Разница в том, что на этот раз мы не выполняем онлайн-операцию DDL, поскольку изменение любого типа столбца всегда приводит к перестроению таблицы. Мы рекомендуем прочитать об онлайн-операциях DDL в Справочном руководстве при планировании изменений. Объединение онлайн- и офлайн-операций приведет к офлайн-операции.

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

Добавление, удаление и изменение индексов

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

Начнем с добавления нового индекса. Представьте, что таблица language часто запрашивается с использованием предложения WHERE, в котором указывается name. Чтобы ускорить эти запросы, вы решили добавить новый индекс, который вы назвали idx_name. Вот как вы добавляете его после создания таблицы:

mysql> ALTER TABLE language ADD INDEX idx_name (name);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0

Опять же, вы можете использовать термины KEY и INDEX взаимозаменяемо. Вы можете проверить результаты с помощью оператора SHOW CREATE TABLE:

mysql> SHOW CREATE TABLE language\G
*************************** 1. row ***************************
       Table: language
Create Table: CREATE TABLE `language` (
  `language_id` tinyint unsigned NOT NULL AUTO_INCREMENT,
  `name` char(255) DEFAULT NULL,
  `last_updated_time` timestamp NOT NULL
    DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`language_id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=8
    DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

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

mysql> CREATE TABLE no_pk (id INT);
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO no_pk VALUES (1),(2),(3);
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE no_pk ADD PRIMARY KEY (id);
Query OK, 0 rows affected (0.13 sec)
Records: 0 Duplicates: 0 Warnings: 0

Теперь давайте рассмотрим, как удалить index. Чтобы удалить индекс, не являющийся первичным ключом, выполните следующие действия:

mysql> ALTER TABLE language DROP INDEX idx_name;
Query OK, 0 rows affected (0.08 sec)
Records: 0 Duplicates: 0 Warnings: 0

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

mysql> ALTER TABLE no_pk DROP PRIMARY KEY;
Query OK, 3 rows affected (0.07 sec)
Records: 3 Duplicates: 0 Warnings: 0

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

mysql> ALTER TABLE language DROP PRIMARY KEY,
    -> ADD PRIMARY KEY (language_id, name);
Query OK, 0 rows affected (0.09 sec)
Records: 0  Duplicates: 0  Warnings: 0

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

mysql> ALTER TABLE language DROP INDEX idx_name,
    -> ADD INDEX idx_name (name(10));
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

Переименование таблиц и изменение других структур

Мы видели, как изменять столбцы и индексы в таблице; теперь давайте посмотрим, как модифицировать сами таблицы. Таблицу легко переименовать. Предположим, вы хотите переименовать language в languages. Используйте следующую команду:

mysql> ALTER TABLE language RENAME TO languages;
Query OK, 0 rows affected (0.04 sec)

Ключевое слово TO является необязательным.

Есть еще несколько вещей, которые вы можете делать с операторами ALTER, в том числе:

Вы можете найти больше об этих операциях в Справочном руководстве по MySQL, в разделах, посвященных операторам ALTER DATABASE и ALTER TABLE. Альтернативная более короткая запись для того же оператора — RENAME TABLE:

mysql> RENAME TABLE languages TO language;
Query OK, 0 rows affected (0.04 sec)

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

mysql> CREATE DATABASE sakila_new;
Query OK, 1 row affected (0.05 sec)
mysql> RENAME TABLE sakila.language TO sakila_new.language;
Query OK, 0 rows affected (0.05 sec)
mysql> USE sakila;
Database changed
mysql> SHOW TABLES LIKE 'lang%';
Empty set (0.00 sec)
mysql> USE sakila_new;
Database changed
mysql> SHOW TABLES LIKE 'lang%';
+------------------------------+
| Tables_in_sakila_new (lang%) |
+------------------------------+
| language                     |
+------------------------------+
1 row in set (0.00 sec)

Удаление структур

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

Удаление баз данных

Удалить или dropping базу данных очень просто. Вот как вы удаляете базу данных sakila:

mysql> DROP DATABASE sakila;
Query OK, 25 rows affected (0.16 sec)

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

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

mysql> DROP DATABASE sakila;
ERROR 1008 (HY000): Can't drop database 'sakila'; database doesn't exist

Вы можете избежать ошибки, которая полезна при включении оператора в сценарий, используя фразу IF EXISTS:

mysql> DROP DATABASE IF EXISTS sakila;
Query OK, 0 rows affected, 1 warning (0.00 sec)

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

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

Удаление таблиц так же просто, как удаление базы данных. Давайте создадим и удалим таблицу из базы данных sakila:

mysql> CREATE TABLE temp (id SERIAL PRIMARY KEY);
Query OK, 0 rows affected (0.05 sec)
mysql> DROP TABLE temp;
Query OK, 0 rows affected (0.03 sec)

Не беспокойтесь: сообщение 0 rows affected вводит в заблуждение. Вы обнаружите, что таблица определенно исчезла.

Вы можете использовать фразу IF EXISTS для предотвращения ошибок. Попробуем снова удалить таблицу temp:

mysql> DROP TABLE IF EXISTS temp;
Query OK, 0 rows affected, 1 warning (0.01 sec)

Как обычно, вы можете изучить предупреждение с помощью инструкции SHOW WARNINGS:

mysql> SHOW WARNINGS;
+-------+------+-----------------------------+
| Level | Code | Message                     |
+-------+------+-----------------------------+
| Note  | 1051 | Unknown table 'sakila.temp' |
+-------+------+-----------------------------+
1 row in set (0.00 sec)

Вы можете удалить более одной таблицы в одном выражении, разделив имена таблиц запятыми:

mysql> DROP TABLE IF EXISTS temp, temp1, temp2;
Query OK, 0 rows affected, 3 warnings (0.00 sec)

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

Глава 5
Расширенные запросы

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

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

Псевдонимы (Aliases)

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

Псевдонимы столбцов

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

mysql> SELECT first_name AS 'First Name', last_name AS 'Last Name'
    -> FROM actor LIMIT 5;
+------------+--------------+
| First Name | Last Name    |
+------------+--------------+
| PENELOPE   | GUINESS      |
| NICK       | WAHLBERG     |
| ED         | CHASE        |
| JENNIFER   | DAVIS        |
| JOHNNY     | LOLLOBRIGIDA |
+------------+--------------+
5 rows in set (0.00 sec)

Столбец first_name имеет псевдоним First Name, а столбец last_nameLast Name. Вы можете видеть, что в выводе обычные заголовки столбцов, first_name и last_name, заменены псевдонимами First Name и Last Name. Преимущество состоит в том, что псевдонимы могут быть более значимыми для пользователей. В этом случае, по крайней мере, они более удобочитаемы. Кроме этого, это не очень полезно, но иллюстрирует идею о том, что для столбца вы добавляете ключевое слово AS, а затем строку, представляющую то, как вы хотите, чтобы столбец назывался. Указание ключевого слова AS не требуется, но делает ситуацию более понятной.

Теперь давайте посмотрим, как псевдонимы столбцов делают что-то полезное. Вот пример, в котором используется функция MySQL и предложение ORDER BY:

mysql> SELECT CONCAT(first_name, ' ', last_name, ' played in ', title) AS movie
    -> FROM actor JOIN film_actor USING (actor_id)
    -> JOIN film USING (film_id)
    -> ORDER BY movie LIMIT 20;
+--------------------------------------------+
| movie                                      |
+--------------------------------------------+
| ADAM GRANT played in ANNIE IDENTITY        |
| ADAM GRANT played in BALLROOM MOCKINGBIRD  |
| ...                                        |
| ADAM GRANT played in TWISTED PIRATES       |
| ADAM GRANT played in WANDA CHAMBER         |
| ADAM HOPPER played in BLINDNESS GUN        |
| ADAM HOPPER played in BLOOD ARGONAUTS      |
+--------------------------------------------+
20 rows in set (0.03 sec)

MySQL-функция CONCAT() объединяет (concatenates) строки, которые являются параметрами — в данном случае first_name, константная строка с пробелом, last_name, константная строка played in, и title — для получения вывода, такого как ZERO CAGE played in CAN YON STOCK. Мы добавили к функции псевдоним AS movie, чтобы мы могли легко ссылаться на нее как на фильм во всем запросе. Вы можете видеть, что мы делаем это в предложении ORDER BY, где мы просим MySQL отсортировать вывод по возрастанию значения фильма. Это намного лучше, чем альтернатива без псевдонимов, которая требует повторного написания функции CONCAT():

mysql> SELECT CONCAT(first_name, ' ', last_name, ' played in ', title) AS movie
    -> FROM actor JOIN film_actor USING (actor_id)
    -> JOIN film USING (film_id)
    -> ORDER BY CONCAT(first_name, ' ', last_name, ' played in ', title)
    -> LIMIT 20;
+--------------------------------------------+
| movie                                      |
+--------------------------------------------+
| ADAM GRANT played in ANNIE IDENTITY        |
| ADAM GRANT played in BALLROOM MOCKINGBIRD  |
| ...                                        |
| ADAM GRANT played in TWISTED PIRATES       |
| ADAM GRANT played in WANDA CHAMBER         |
| ADAM HOPPER played in BLINDNESS GUN        |
| ADAM HOPPER played in BLOOD ARGONAUTS      |
+--------------------------------------------+
20 rows in set (0.03 sec)

Альтернатива громоздка, и, что еще хуже, вы рискуете ошибиться в какой-то части предложения ORDER BY и получить результат, отличный от ожидаемого. (Обратите внимание, что мы использовали AS movie в первой строке, чтобы отображаемый столбец имел метку movie.)

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

mysql> SELECT first_name AS name FROM actor WHERE name = 'ZERO CAGE';
ERROR 1054 (42S22): Unknown column 'name' in 'where clause'

Вы не можете этого сделать, потому что MySQL не всегда знает значения столбцов до того, как выполнит предложение WHERE. Однако вы можете использовать псевдонимы столбцов в предложении ORDER BY, а также в предложениях GROUP BY и HAVING, обсуждаемых далее в этой главе.

Как мы уже упоминали, ключевое слово AS является необязательным. Из-за этого следующие два запроса эквивалентны:

mysql> SELECT actor_id AS id FROM actor WHERE first_name = 'ZERO';
+----+
| id |
+----+
| 11 |
+----+
1 row in set (0.00 sec)
mysql> SELECT actor_id id FROM actor WHERE first_name = 'ZERO';
+----+
| id |
+----+
| 11 |
+----+
1 row in set (0.00 sec)

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

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

Псевдонимы таблиц

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

Вот простой пример псевдонима таблицы, который показывает вам, как сэкономить на вводе:

mysql> SELECT ac.actor_id, ac.first_name, ac.last_name, fl.title FROM
    -> actor AS ac INNER JOIN film_actor AS fla USING (actor_id)
    -> INNER JOIN film AS fl USING (film_id)
    -> WHERE fl.title = 'AFFAIR PREJUDICE';
+----------+------------+-----------+------------------+
| actor_id | first_name | last_name | title            |
+----------+------------+-----------+------------------+
|       41 | JODIE      | DEGENERES | AFFAIR PREJUDICE |
|       81 | SCARLETT   | DAMON     | AFFAIR PREJUDICE |
|       88 | KENNETH    | PESCI     | AFFAIR PREJUDICE |
|      147 | FAY        | WINSLET   | AFFAIR PREJUDICE |
|      162 | OPRAH      | KILMER    | AFFAIR PREJUDICE |
+----------+------------+-----------+------------------+
5 rows in set (0.00 sec)

Вы можете видеть, что таблицы film и actor имеют псевдонимы fl и ac соответственно с использованием ключевого слова AS. Это позволяет более компактно выражать имена столбцов, например, fl.title. Заметьте также, что вы можете использовать псевдонимы таблиц в предложении WHERE; в отличие от псевдонимов столбцов, нет никаких ограничений на использование псевдонимов таблиц в запросах. Из нашего примера видно, что мы ссылаемся на псевдонимы таблиц в SELECT до того, как они были определены в FROM. Однако с псевдонимами таблиц есть одна загвоздка: если для таблицы был использован псевдоним, невозможно обратиться к этой таблице, не используя ее новый псевдоним. Например, следующий оператор выдаст ошибку, как если бы мы упомянули film в предложении SELECT:

mysql> SELECT ac.actor_id, ac.first_name, ac.last_name, fl.title FROM
    -> actor AS ac INNER JOIN film_actor AS fla USING (actor_id)
    -> INNER JOIN film AS fl USING (film_id)
    -> WHERE film.title = 'AFFAIR PREJUDICE';
ERROR 1054 (42S22): Unknown column 'film.title' in 'where clause'

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

actor AS ac INNER JOIN film_actor AS fla

такой же как

actor ac INNER JOIN film_actor fla

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

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

mysql> SELECT * FROM film WHERE title = title;

Но это не имеет смысла — каждый фильм имеет то же название, что и он сам, поэтому запрос просто выдает все фильмы в качестве вывода:

+---------+------------------...
| film_id | title            ...
+---------+------------------...
|       1 | ACADEMY DINOSAUR ...
|       2 | ACE GOLDFINGER   ...
|       3 | ADAPTATION HOLES ...
|     ...                    ...
|    1000 | ZORRO ARK        ...
+---------+------------------...
1000 rows in set (0.01 sec)

Что вам действительно нужно, так это знать, имеют ли два разных фильма из таблицы film одно и то же имя. Но как это сделать в одном запросе? Ответ состоит в том, чтобы дать таблице два разных псевдонима; затем вы проверяете, соответствует ли одна строка в первой таблице с псевдонимами строке во второй:

mysql> SELECT m1.film_id, m2.title
    -> FROM film AS m1, film AS m2
    -> WHERE m1.title = m2.title;
+---------+-------------------+
| film_id | title             |
+---------+-------------------+
|       1 | ACADEMY DINOSAUR  |
|       2 | ACE GOLDFINGER    |
|       3 | ADAPTATION HOLES  |
|     ...                     |
|    1000 | ZORRO ARK         |
+---------+-------------------+
1000 rows in set (0.02 sec)

Но это все еще не работает! Мы получаем все 1000 фильмов в качестве ответов. Причина в том, что снова каждый фильм соответствует самому себе, потому что он встречается в обеих таблицах псевдонимов.

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

mysql> SELECT m1.film_id, m2.title
    -> FROM film AS m1, film AS m2
    -> WHERE m1.title = m2.title
    -> AND m1.film_id <> m2.film_id;
Empty set (0.00 sec)

Теперь вы можете видеть, что в базе данных нет двух фильмов с одинаковым названием. Дополнительный оператор AND m1.film_id != m2.film_id предотвращает вывод ответов, если идентификатор фильма одинаков в обеих таблицах.

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

Объединение данных

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

В этом разделе объясняются предложения GROUP BY и HAVING, два наиболее часто используемых оператора SQL для агрегирования. Но сначала объясняется предложение DISTINCT, которое используется для сообщения уникальных результатов вывода запроса. Если ни предложение DISTINCT, ни предложение GROUP BY не указаны, возвращенные необработанные данные все равно могут быть обработаны с помощью агрегатных функций, которые мы описываем в этом разделе.

Предложение DISTINCT

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

Пример — лучший способ понять DISTINCT. Рассмотрим этот запрос:

mysql> SELECT DISTINCT first_name
    -> FROM actor JOIN film_actor USING (actor_id);
+-------------+
| first_name  |
+-------------+
| PENELOPE    |
| NICK        |
| ...         |
| GREGORY     |
| JOHN        |
| BELA        |
| THORA       |
+-------------+
128 rows in set (0.00 sec)

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

mysql> SELECT first_name
    -> FROM actor JOIN film_actor USING (actor_id)
    -> LIMIT 5;
+------------+
| first_name |
+------------+
| PENELOPE   |
| PENELOPE   |
| PENELOPE   |
| PENELOPE   |
| PENELOPE   |
+------------+
5 rows in set (0.00 sec)

Таким образом, предложение DISTINCT помогает вам получить сводку.

Предложение DISTINCT применяется к выходным данным запроса и удаляет строки с одинаковыми значениями в столбцах, выбранных для вывода в запросе. Если вы перефразируете предыдущий запрос для вывода как first_name, так и last_name (но в противном случае не изменяйте предложение JOIN и по-прежнему используете DISTINCT), вы получите 199 строк на выходе (поэтому мы используем фамилии):

mysql> SELECT DISTINCT first_name, last_name
    -> FROM actor JOIN film_actor USING (actor_id);
+-------------+--------------+
| first_name  | last_name    |
+-------------+--------------+
| PENELOPE    | GUINESS      |
| NICK        | WAHLBERG     |
| ...                        |
| JULIA       | FAWCETT      |
| THORA       | TEMPLE       |
+-------------+--------------+
199 rows in set (0.00 sec)

К сожалению, имена людей, даже если добавлены фамилии, по-прежнему являются плохим уникальным ключом. В таблице actor в базе данных sakila 200 строк, и нам не хватает одной из них. Вы должны помнить об этой проблеме, так как использование DISTINCT без разбора может привести к неверным результатам запроса.

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

Предложение GROUP BY

Предложение GROUP BY группирует выходные данные для агрегирования. В частности, это позволяет нам использовать агрегатные функции (описанные в разделе «Агрегатные функции») для наших данных, когда наша проекция (то есть содержимое предложения SELECT) содержит столбцы, отличные от тех, которые входят в агрегатную функцию. GROUP BY похож на ORDER BY тем, что в качестве аргумента принимает список столбцов. Однако эти пункты оцениваются в разное время и похожи только тем, как они выглядят, а не тем, как они работают.

Давайте рассмотрим несколько примеров GROUP BY, которые продемонстрируют, для чего ее можно использовать. В самой простой форме, когда мы перечисляем каждый столбец, который мы SELECT в GROUP BY, мы получаем эквивалент DISTINCT. Мы уже установили, что имя не является уникальным идентификатором актера:

mysql> SELECT first_name FROM actor
    -> WHERE first_name IN ('GENE', 'MERYL');
+------------+
| first_name |
+------------+
| GENE       |
| GENE       |
| MERYL      |
| GENE       |
| MERYL      |
+------------+
5 rows in set (0.00 sec)

Мы можем указать MySQL сгруппировать вывод по заданному столбцу, чтобы избавиться от дубликатов. В данном случае мы выбрали только один столбец, поэтому воспользуемся им:

mysql> SELECT first_name FROM actor
    -> WHERE first_name IN ('GENE', 'MERYL')
    -> GROUP BY first_name;
+------------+
| first_name |
+------------+
| GENE       |
| MERYL      |
+------------+
2 rows in set (0.00 sec)

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

В соответствии со стандартом SQL каждый столбец, спроецированный в предложении SELECT, который не является частью агрегатной функции, должен быть указан в предложении GROUP BY. Единственный случай, когда это правило может быть нарушено, — это когда результирующие группы имеют только одну строку в каждой. Если подумать, то это логично: если из таблицы actor выбрать first_name и last_name и сгруппировать только по first_name, то как должна вести себя база данных? Он не может выводить более одной строки с одним и тем же именем, так как это противоречит правилам группировки, но для данного имени может быть более одной фамилии.

В течение долгого времени MySQL расширял стандарт, позволяя выполнять GROUP BY на основе меньшего количества столбцов, чем определено в SELECT. Что он сделал с дополнительными столбцами? Ну, он выводит какое-то значение недетерминированным образом. Например, если вы сгруппировали по имени, но не по фамилии, вы можете получить любую из двух строк GENE, WILLIS и GENE, HOPKINS. Это нестандартное и опасное поведение. Представьте, что на год вы получили Хопкинса, как будто результаты были упорядочены по алфавиту, и стали полагаться на это, но потом таблица была реорганизована, и порядок изменился. Мы твердо верим, что стандарт SQL правильно ограничивает такое поведение, чтобы избежать непредсказуемости.

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

Теперь давайте создадим более полезный пример. Актер обычно принимает участие во многих фильмах на протяжении всей своей карьеры. Мы можем захотеть узнать, в скольких фильмах сыграл конкретный актер, или сделать расчет для каждого известного нам актера и получить рейтинг по продуктивности. Для начала мы можем использовать методы, которые мы изучили до сих пор, и выполнить INNER JOIN между таблицами actor и film_actor. Таблица film нам не нужна, так как мы не ищем никаких подробностей о самих фильмах. Затем мы можем упорядочить вывод по имени актера, что упрощает подсчет того, что мы хотим:

mysql> SELECT first_name, last_name, film_id
    -> FROM actor INNER JOIN film_actor USING (actor_id)
    -> ORDER BY first_name, last_name LIMIT 20;
+------------+-----------+---------+
| first_name | last_name | film_id |
+------------+-----------+---------+
| ADAM       | GRANT     |      26 |
| ADAM       | GRANT     |      52 |
| ADAM       | GRANT     |     233 |
| ADAM       | GRANT     |     317 |
| ADAM       | GRANT     |     359 |
| ADAM       | GRANT     |     362 |
| ADAM       | GRANT     |     385 |
| ADAM       | GRANT     |     399 |
| ADAM       | GRANT     |     450 |
| ADAM       | GRANT     |     532 |
| ADAM       | GRANT     |     560 |
| ADAM       | GRANT     |     574 |
| ADAM       | GRANT     |     638 |
| ADAM       | GRANT     |     773 |
| ADAM       | GRANT     |     833 |
| ADAM       | GRANT     |     874 |
| ADAM       | GRANT     |     918 |
| ADAM       | GRANT     |     956 |
| ADAM       | HOPPER    |      81 |
| ADAM       | HOPPER    |      82 |
+------------+-----------+---------+
20 rows in set (0.01 sec)

Пробегая по списку, легко подсчитать, сколько фильмов у нас есть для каждого актера или, по крайней мере, для Адама Гранта. Однако без LIMIT запрос вернет 5462 различных строки, и расчет количества вручную займет много времени. Предложение GROUP BY может помочь автоматизировать этот процесс, группируя фильмы по актерам; затем мы можем использовать функцию COUNT() для подсчета количества фильмов в каждой группе. Наконец, мы можем использовать ORDER BY и LIMIT, чтобы получить 10 лучших актеров по количеству фильмов, в которых они снялись. Вот запрос, который делает то, что нам нужно:

mysql> SELECT first_name, last_name, COUNT(film_id) AS num_films FROM
    -> actor INNER JOIN film_actor USING (actor_id)
    -> GROUP BY first_name, last_name
    -> ORDER BY num_films DESC LIMIT 10;
+------------+-------------+-----------+
| first_name | last_name   | num_films |
+------------+-------------+-----------+
| SUSAN      | DAVIS       |        54 |
| GINA       | DEGENERES   |        42 |
| WALTER     | TORN        |        41 |
| MARY       | KEITEL      |        40 |
| MATTHEW    | CARREY      |        39 |
| SANDRA     | KILMER      |        37 |
| SCARLETT   | DAMON       |        36 |
| VAL        | BOLGER      |        35 |
| ANGELA     | WITHERSPOON |        35 |
| UMA        | WOOD        |        35 |
+------------+-------------+-----------+
10 rows in set (0.01 sec)

Вы можете видеть, что результат, который мы запросили, это first_name, last_name, COUNT(film_id) как num_films, и это говорит нам именно то, что мы хотели знать. Мы группируем наши данные по столбцам first_name и last_name, запуская в процессе агрегатную функцию COUNT(). Для каждого «сегмента» строк, которые мы получили в предыдущем запросе, теперь мы получаем только одну строку, хотя и дающую нужную нам информацию. Обратите внимание, как мы объединили GROUP BY и ORDER BY, чтобы получить нужный порядок: по количеству фильмов, от большего к меньшему. GROUP BY не гарантирует упорядочение, только группировку. Наконец, мы ограничиваем вывод (LIMIT) до 10 строк, представляющих наших наиболее продуктивных участников, иначе мы получили бы 199 строк вывода.

Рассмотрим запрос дальше. Мы начнем с предложения GROUP BY. Это говорит нам, как объединять строки в группы: в этом примере мы сообщаем MySQL, что способ группировки строк — это first_name, last_name. В результате строки для субъектов с одинаковыми именами образуют кластер или корзину, то есть каждое отдельное имя становится одной группой. Как только строки сгруппированы, они обрабатываются в остальной части запроса, как если бы они были одной строкой. Так, например, когда мы пишем SELECT first_name, last_name, мы получаем только одну строку для каждой группы. Это то же самое, что и DISTINCT, как мы уже обсуждали. Функция COUNT() сообщает нам о свойствах группы. В частности, она сообщает нам количество строк, образующих каждую группу; вы можете посчитать любой столбец в группе, и вы получите тот же ответ, поэтому COUNT(film_id) почти всегда совпадает с COUNT(*) или COUNT(first_name). (Подробнее о том, почему мы говорим почти, см. в разделе «Агрегатные функции».) Мы также могли бы просто выполнить COUNT(1) или указать любой литерал. Думайте об этом как о выполнении SELECT 1 из таблицы и последующем подсчете результатов. Значение 1 будет выведено для каждой строки в таблице, а COUNT() сделает подсчет. Единственным исключением является NULL: хотя вполне приемлемо и законно указывать COUNT(NULL), результат всегда будет нулевым, поскольку COUNT() отбрасывает значения NULL. Конечно, вы можете использовать псевдоним столбца для столбца COUNT().

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

mysql> SELECT title, name AS category_name, COUNT(*) AS cnt
    -> FROM film INNER JOIN film_actor USING (film_id)
    -> INNER JOIN film_category USING (film_id)
    -> INNER JOIN category USING (category_id)
    -> GROUP BY film_id, category_id
    -> ORDER BY cnt DESC LIMIT 5;
+------------------+---------------+-----+
| title            | category_name | cnt |
+------------------+---------------+-----+
| LAMBS CINCINATTI | Games         |  15 |
| CRAZY HOME       | Comedy        |  13 |
| CHITTY LOCK      | Drama         |  13 |
| RANDOM GO        | Sci-Fi        |  13 |
| DRACULA CRYSTAL  | Classics      |  13 |
+------------------+---------------+-----+
5 rows in set (0.03 sec)

Прежде чем мы обсудим, что нового, подумайте об общей функции запроса. Мы объединяем четыре таблицы с помощью INNER JOIN, используя столбцы их идентификаторов: film, film_actor, film_category и category. Забудем на мгновение об агрегировании, результат этого запроса — одна строка для комбинации фильма и актера.

Предложение GROUP BY объединяет строки в кластеры. В этом запросе мы хотим, чтобы фильмы были сгруппированы по категориям. Предложение GROUP BY использует для этого film_id и category_id. Вы можете использовать столбец film_id из любой из трех таблиц; film.film_id, film_actor.film_id и film_category.film_id для этой цели одинаковы. Неважно, какой из них вы используете; INNER JOIN гарантирует, что они все равно совпадают. То же самое относится и к category_id.

Как упоминалось ранее, несмотря на то, что требуется перечислить каждый неагрегированный столбец в GROUP BY, вы можете группировать по столбцам за пределами SELECT. В предыдущем примере запроса мы используем функцию COUNT(), чтобы сообщить нам, сколько строк находится в каждой группе. Например, вы можете видеть, что COUNT(*) говорит нам, что в комедии CRAZY HOME задействовано 13 актеров. Опять же, не имеет значения, какой столбец или столбцы вы считаете в запросе: например, COUNT(*) имеет тот же эффект, что и COUNT(film.film_id) или COUNT(category.name).

Затем мы упорядочиваем выходные данные по столбцу COUNT(*) с псевдонимом cnt в порядке убывания и выбираем первые пять строк. Обратите внимание, что есть несколько строк с cnt, равным 13. На самом деле их даже больше (всего шесть) в базе данных, что делает этот порядок немного несправедливым, поскольку фильмы с одинаковым количеством актеров будут отсортированы случайным образом. Вы можете добавить еще один столбец в предложение ORDER BY, например title, чтобы сделать сортировку более предсказуемой.

Давайте попробуем другой пример. База данных sakila касается не только фильмов и актеров: в конце концов, она основана на прокате фильмов. У нас есть, в том числе, информация о клиентах, в том числе данные о том, какие фильмы они брали напрокат. Скажем, мы хотим знать, какие клиенты склонны брать напрокат фильмы из той же категории. Например, мы можем настроить нашу рекламу в зависимости от того, нравятся ли человеку разные категории фильмов или большую часть времени он придерживается какой-то одной. Нам нужно тщательно продумать нашу группировку: мы не хотим группировать по фильмам, так как это просто даст нам количество раз, когда клиент брал его напрокат. Полученный запрос довольно сложен, хотя он по-прежнему основан на INNER JOIN и GROUP BY:

mysql> SELECT email, name AS category_name, COUNT(category_id) AS cnt
    -> FROM customer cs INNER JOIN rental USING (customer_id)
    -> INNER JOIN inventory USING (inventory_id)
    -> INNER JOIN film_category USING (film_id)
    -> INNER JOIN category cat USING (category_id)
    -> GROUP BY 1, 2
    -> ORDER BY 3 DESC LIMIT 5;
+----------------------------------+---------------+-----+
| email                            | category_name | cnt |
+----------------------------------+---------------+-----+
| WESLEY.BULL@sakilacustomer.org   | Games         |   9 |
| ALMA.AUSTIN@sakilacustomer.org   | Animation     |   8 |
| KARL.SEAL@sakilacustomer.org     | Animation     |   8 |
| LYDIA.BURKE@sakilacustomer.org   | Documentary   |   8 |
| NATHAN.RUNYON@sakilacustomer.org | Animation     |   7 |
+----------------------------------+---------------+-----+
5 rows in set (0.08 sec)

Эти клиенты повторно арендуют фильмы из одной и той же категории. Чего мы не знаем, так это того, брал ли кто-нибудь из них один и тот же фильм несколько раз, или это были разные фильмы в одной категории. Предложение GROUP BY скрывает детали. Опять же, мы используем COUNT(*) для подсчета строк в группах, и вы можете видеть, что INNER JOIN распределяется по строкам со 2 по 5 в запросе.

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

Как и в случае с DISTINCT, в GROUP BY есть опасность, о которой следует упомянуть. Рассмотрим следующий запрос:

mysql> SELECT COUNT(*) FROM actor GROUP BY first_name, last_name;
+----------+
| COUNT(*) |
+----------+
|        1 |
|        1 |
|      ... |
|        1 |
|        1 |
+----------+
199 rows in set (0.00 sec)

Он выглядит достаточно просто и выдает количество раз, когда комбинация заданного имени и фамилии встречалась в таблице actor. Вы можете предположить, что он просто выводит 199 строк цифры 1. Однако, если мы выполним COUNT(*) в таблице actor, мы получим 200 строк. В чем подвох? Судя по всему, у двух актеров одинаковые имя и фамилия. Такие вещи случаются, и вы должны помнить о них. При группировании на основе столбцов, которые не образуют уникальный идентификатор, вы можете случайно сгруппировать несвязанные строки, что приведет к вводящим в заблуждение данным. Чтобы найти дубликаты, мы можем изменить запрос, который мы создали в «Псевдонимы таблицы», для поиска фильмов с таким же именем:

mysql> SELECT a1.actor_id, a1.first_name, a1.last_name
    -> FROM actor AS a1, actor AS a2
    -> WHERE a1.first_name = a2.first_name
    -> AND a1.last_name = a2.last_name
    -> AND a1.actor_id <> a2.actor_id;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
|      101 | SUSAN      | DAVIS     |
|      110 | SUSAN      | DAVIS     |
+----------+------------+-----------+
2 rows in set (0.00 sec)

Прежде чем мы закончим этот раздел, давайте еще раз коснемся того, как MySQL расширяет стандарт SQL вокруг предложения GROUP BY. До MySQL 5.7 можно было по умолчанию указывать неполный список столбцов в предложении GROUP BY, и, как мы объясняли, это приводило к тому, что в группах для несгруппированных зависимых столбцов выводились случайные строки. По причинам поддержки устаревшего программного обеспечения и MySQL 5.7, и My SQL 8.0 продолжают обеспечивать это поведение, хотя оно должно быть включено явно. Поведение управляется режимом SQL ONLY_FULL_GROUP_BY, который установлен по умолчанию. Если вы окажетесь в ситуации, когда вам нужно перенести программу, основанную на устаревшем поведении GROUP BY, мы рекомендуем вам не прибегать к изменению режима SQL. Обычно есть два способа справиться с этой проблемой. Во-первых, нужно понять, требует ли вообще логика запроса неполной группировки, что случается редко. Во-вторых, существует поддержка поведения случайных данных для несгруппированных столбцов с помощью либо агрегатной функции, такой как MIN() или MAX(), либо специальной агрегатной функции ANY_VALUE(), которая, что неудивительно, просто создает случайное значение внутри группы. Далее мы более подробно рассмотрим агрегатные функции.

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

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

COUNT()

Возвращает количество строк или количество значений в столбце. Помните, мы упоминали, что COUNT(*) почти всегда эквивалентен COUNT(<column>). Проблема с NULL. COUNT(*) будет подсчитывать возвращенные строки, независимо от того, является ли столбец в этих строках NULL или нет. Однако при выполнении COUNT(<column>) будут учитываться только значения, отличные от NULL. Например, в базе данных sakila адрес электронной почты клиента может быть NULL, и мы можем наблюдать влияние:

mysql> SELECT COUNT(*) FROM customer;
+----------+
| count(*) |
+----------+
|      599 |
+----------+
1 row in set (0.00 sec)
mysql> SELECT COUNT(email) FROM customer;
+--------------+
| count(email) |
+--------------+
|          598 |
+--------------+
1 row in set (0.00 sec)

Мы также должны добавить, что COUNT() может быть запущен с внутренним предложением DISTINCT, как в COUNT(DISTINCT <column>), и в этом случае будет возвращать количество различных значений вместо всех значений.

AVG()

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

SELECT AVG(cost) FROM house_prices GROUP BY city;
MAX()

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

MIN()

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

STD(), STDDEV(), и STDDEV_POP()

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

SUM()

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

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

Предложение HAVING

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

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

mysql> SELECT first_name, last_name, COUNT(film_id)
    -> FROM actor INNER JOIN film_actor USING (actor_id)
    -> GROUP BY actor_id, first_name, last_name
    -> HAVING COUNT(film_id) > 40
    -> ORDER BY COUNT(film_id) DESC;
+------------+-----------+----------------+
| first_name | last_name | COUNT(film_id) |
+------------+-----------+----------------+
| GINA       | DEGENERES |             42 |
| WALTER     | TORN      |             41 |
+------------+-----------+----------------+
2 rows in set (0.01 sec)

Вы видите, что только два актера соответствуют новым критериям.

Предложение HAVING должно содержать выражение или столбец, указанные в предложении SELECT. В этом примере мы использовали HAVING COUNT(film_id) >= 40, и вы можете видеть, что COUNT(film_id) является частью предложения SELECT. Как правило, выражение в предложении HAVING использует агрегатную функцию, такую как COUNT(), SUM(), MIN() или MAX(). Если вы обнаружите, что хотите написать предложение HAVING, в котором используется столбец или выражение, которых нет в предложении SELECT, скорее всего, вместо этого вам следует использовать предложение WHERE. Предложение HAVING предназначено только для принятия решения о том, как формировать каждую группу или кластер, а не для выбора строк в выходных данных. Позже мы покажем вам пример, иллюстрирующий, когда не следует использовать HAVING.

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

mysql> SELECT title, COUNT(rental_id) AS num_rented FROM
    -> film INNER JOIN inventory USING (film_id)
    -> INNER JOIN rental USING (inventory_id)
    -> GROUP BY title
    -> HAVING num_rented > 30
    -> ORDER BY num_rented DESC LIMIT 5;
+--------------------+------------+
| title              | num_rented |
+--------------------+------------+
| BUCKET BROTHERHOOD |         34 |
| ROCKETEER MOTHER   |         33 |
| FORWARD TEMPLE     |         32 |
| GRIT CLOCKWORK     |         32 |
| JUGGLER HARDLY     |         32 |
+--------------------+------------+
5 rows in set (0.04 sec)

Вы снова можете видеть, что выражение COUNT() используется как в предложениях SELECT, так и в предложениях HAVING. Однако на этот раз мы присвоили функции COUNT(rental_id) псевдоним num_rented и использовали псевдоним в предложениях HAVING и ORDER BY.

Теперь давайте рассмотрим пример, в котором вам не следует использовать HAVING. Вы хотите узнать, в скольких фильмах сыграл тот или иной актер. Вот запрос, который вам не следует использовать:

mysql> SELECT first_name, last_name, COUNT(film_id) AS film_cnt FROM
    -> actor INNER JOIN film_actor USING (actor_id)
    -> GROUP BY actor_id, first_name, last_name
    -> HAVING first_name = 'EMILY' AND last_name = 'DEE';
+------------+-----------+----------+
| first_name | last_name | film_cnt |
+------------+-----------+----------+
| EMILY      | DEE       |       14 |
+------------+-----------+----------+
1 row in set (0.02 sec)

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

mysql> SELECT first_name, last_name, COUNT(film_id) AS film_cnt FROM
    -> actor INNER JOIN film_actor USING (actor_id)
    -> WHERE first_name = 'EMILY' AND last_name = 'DEE'
    -> GROUP BY actor_id, first_name, last_name;
+------------+-----------+----------+
| first_name | last_name | film_cnt |
+------------+-----------+----------+
| EMILY      | DEE       |       14 |
+------------+-----------+----------+
1 row in set (0.00 sec)

Этот правильный запрос формирует группы, а затем выбирает группы для отображения на основе предложения WHERE.

Расширенные соединения

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

Внутреннее соединение

Предложение INNER JOIN сопоставляет строки между двумя таблицами на основе критериев, указанных в предложении USING. Например, теперь вы хорошо знакомы с внутренним соединением таблиц actor и film_actor:

mysql> SELECT first_name, last_name, film_id FROM
    -> actor INNER JOIN film_actor USING (actor_id)
    -> LIMIT 20;
+------------+-----------+---------+
| first_name | last_name | film_id |
+------------+-----------+---------+
| PENELOPE   | GUINESS   |       1 |
| PENELOPE   | GUINESS   |      23 |
| ...                              |
| PENELOPE   | GUINESS   |     980 |
| NICK       | WAHLBERG  |       3 |
+------------+-----------+---------+
20 rows in set (0.00 sec)

Давайте рассмотрим ключевые особенности INNER JOIN:

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

mysql> SELECT first_name, last_name, film_id
    -> FROM actor, film_actor
    -> WHERE actor.actor_id = film_actor.actor_id
    -> LIMIT 20;
+------------+-----------+---------+
| first_name | last_name | film_id |
+------------+-----------+---------+
| PENELOPE   | GUINESS   |       1 |
| PENELOPE   | GUINESS   |      23 |
| ...                              |
| PENELOPE   | GUINESS   |     980 |
| NICK       | WAHLBERG  |       3 |
+------------+-----------+---------+
20 rows in set (0.00 sec)

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

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

mysql> SELECT first_name, last_name, film_id FROM
    -> actor INNER JOIN film_actor
    -> ON actor.actor_id = film_actor.actor_id
    -> LIMIT 20;
+------------+-----------+---------+
| first_name | last_name | film_id |
+------------+-----------+---------+
| PENELOPE   | GUINESS   |       1 |
| PENELOPE   | GUINESS   |      23 |
| ...                              |
| PENELOPE   | GUINESS   |     980 |
| NICK       | WAHLBERG  |       3 |
+------------+-----------+---------+
20 rows in set (0.00 sec)

Вы можете видеть, что предложение ON заменяет предложение USING и что следующие за ним столбцы полностью определены и включают имена таблиц и столбцов. Если бы столбцы были названы по-разному и уникально между двумя таблицами, вы могли бы опустить имена таблиц. Нет никаких реальных преимуществ или недостатков в использовании предложения ON или WHERE; это просто дело вкуса. Как правило, в наши дни вы обнаружите, что большинство специалистов по SQL используют INNER JOIN с предложением ON, а не WHERE, но это не универсально.

Прежде чем мы двинемся дальше, давайте рассмотрим, для чего служат предложения WHERE, ON и USING. Если вы опустите предложение WHERE в запросе, который мы вам только что показали, вы получите совсем другой результат. Вот запрос и первые несколько строк вывода:

mysql> SELECT first_name, last_name, film_id
    -> FROM actor, film_actor LIMIT 20;
+------------+-------------+---------+
| first_name | last_name   | film_id |
+------------+-------------+---------+
| THORA      | TEMPLE      |       1 |
| JULIA      | FAWCETT     |       1 |
| ...                                |
| DEBBIE     | AKROYD      |       1 |
| MATTHEW    | CARREY      |       1 |
+------------+-------------+---------+
20 rows in set (0.00 sec)

Вывод не имеет смысла: произошло то, что каждая строка из таблицы actor была выведена вместе с каждой строкой из таблицы film_actor для всех возможных комбинаций. Поскольку в таблице film_actor 200 актеров и 5 462 записи, в выводе 200 × 5 462 = 1 092 400 строк, и мы знаем, что только 5 462 из этих комбинаций действительно имеют смысл (всего 5 462 записи для актеров, сыгравших в фильмах). Мы можем увидеть количество строк, которые мы получили бы без LIMIT, с помощью следующего запроса:

mysql> SELECT COUNT(*) FROM actor, film_actor;
+----------+
| COUNT(*) |
+----------+
|  1092400 |
+----------+
1 row in set (0.00 sec)

Этот тип запроса без предложения, которое соответствует строкам, известен как декартово произведение. Между прочим, вы также получите декартово произведение, если выполните внутреннее соединение без указания столбца с предложением USING или ON, как в этом запросе:

SELECT first_name, last_name, film_id
FROM actor INNER JOIN film_actor;

В разделе «Естественное соединение» мы представим естественное соединение, которое представляет собой внутреннее соединение столбцов с одинаковыми именами. Хотя естественное соединение не использует явно указанные столбцы, оно по-прежнему создает внутреннее соединение, а не декартово произведение.

Ключевую фразу INNER JOIN можно заменить на JOIN или STRAIGHT JOIN; все они делают одно и то же. Однако STRAIGHT JOIN заставляет MySQL всегда читать таблицу слева, прежде чем читать таблицу справа. Мы рассмотрим, как MySQL обрабатывает запросы за кулисами, в главе 7. Ключевая фраза JOIN используется чаще всего: это стандартное сокращение для INNER JOIN, используемое многими другими системами баз данных, помимо MySQL, и мы будет использовать его в большинстве наших примеров внутреннего соединения.

Объединение (UNION)

Оператор UNION на самом деле не является оператором соединения. Скорее, он позволяет вам комбинировать вывод более чем одного оператора SELECT, чтобы получить объединенный набор результатов. Это полезно в тех случаях, когда вы хотите создать один список из более чем одного источника или хотите создать списки из одного источника, которые трудно выразить в одном запросе.

Давайте посмотрим на пример. Если вы хотите вывести имена всех актеров и фильмов и клиентов в базе данных sakila, вы можете сделать это с помощью оператора UNION. Это надуманный пример, но вы можете сделать это только для того, чтобы перечислить все текстовые фрагменты, а не для осмысленного представления взаимосвязей в данных. В столбцах actor.first_name, film.title и customer.first_name есть текст. Вот как это отобразить:

mysql> SELECT first_name FROM actor
    -> UNION
    -> SELECT first_name FROM customer
    -> UNION
    -> SELECT title FROM film;
+-----------------------------+
| first_name                  |
+-----------------------------+
| PENELOPE                    |
| NICK                        |
| ED                          |
| ...                         |
| ZHIVAGO CORE                |
| ZOOLANDER FICTION           |
| ZORRO ARK                   |
+-----------------------------+
1647 rows in set (0.00 sec)

Мы показали только несколько строк из 1647. Оператор UNION выводит результаты всех запросов вместе под заголовком, соответствующим первому запросу.

Чуть менее надуманный пример — создать список из пяти наиболее и наименее продаваемых фильмов в нашей базе данных. Вы можете легко сделать это с помощью оператора UNION:

mysql> (SELECT title, COUNT(rental_id) AS num_rented
    -> FROM film JOIN inventory USING (film_id)
    -> JOIN rental USING (inventory_id)
    -> GROUP BY title ORDER BY num_rented DESC LIMIT 5)
    -> UNION
    -> (SELECT title, COUNT(rental_id) AS num_rented
    -> FROM film JOIN inventory USING (film_id)
    -> JOIN rental USING (inventory_id)
    -> GROUP BY title ORDER BY num_rented ASC LIMIT 5);
+--------------------+------------+
| title              | num_rented |
+--------------------+------------+
| BUCKET BROTHERHOOD |         34 |
| ROCKETEER MOTHER   |         33 |
| FORWARD TEMPLE     |         32 |
| GRIT CLOCKWORK     |         32 |
| JUGGLER HARDLY     |         32 |
| TRAIN BUNCH        |          4 |
| HARDLY ROBBERS     |          4 |
| MIXED DOORS        |          4 |
| BUNCH MINDS        |          5 |
| BRAVEHEART HUMAN   |          5 |
+--------------------+------------+
10 rows in set (0.04 sec)

Первый запрос использует ORDER BY с модификатором DESC (по убыванию) и предложением LIMIT 5, чтобы найти пять самых популярных фильмов. Второй запрос использует ORDER BY с модификатором ASC (по возрастанию) и предложением LIMIT 5, чтобы найти пять фильмов, которые берут напрокат меньше всего. UNION объединяет наборы результатов. Обратите внимание, что существует несколько заголовков с одинаковым значением num_rented, и не гарантируется, что порядок заголовков с одинаковым значением будет определен. Вы можете увидеть различные заголовки, перечисленные для значений num_rented 32 и 5 на вашей стороне.

Оператор UNION имеет несколько ограничений:

Левое и правое соединение

Соединения, которые мы обсуждали до сих пор, выводят только те строки, которые совпадают между таблицами. Например, когда вы объединяете таблицы film и rental через таблицу inventory, вы видите только те фильмы, которые были взяты напрокат. Строки для фильмов, которые не были взяты напрокат, игнорируются. Во многих случаях это имеет смысл, но это не единственный способ объединения данных. В этом разделе объясняются другие варианты, которые у вас есть.

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

Вот простой пример LEFT JOIN. Вы хотите перечислить все фильмы и рядом с каждым фильмом, который вы хотите показать, когда он был взят напрокат. Если фильм никогда не брали напрокат, вы хотите его посмотреть. Если его много раз сдавали в аренду, вы тоже хотите это увидеть. Вот запрос:

mysql> SELECT title, rental_date
    -> FROM film LEFT JOIN inventory USING (film_id)
    -> LEFT JOIN rental USING (inventory_id);
+-----------------------------+---------------------+
| title                       | rental_date         |
+-----------------------------+---------------------+
| ACADEMY DINOSAUR            | 2005-07-08 19:03:15 |
| ACADEMY DINOSAUR            | 2005-08-02 20:13:10 |
| ACADEMY DINOSAUR            | 2005-08-21 21:27:43 |
| ...                                               |
| WAKE JAWS                   | NULL                |
| WALLS ARTIST                | NULL                |
| ...                                               |
| ZORRO ARK                   | 2005-07-31 07:32:21 |
| ZORRO ARK                   | 2005-08-19 03:49:28 |
+-----------------------------+---------------------+
16087 rows in set (0.06 sec)

Вы можете видеть, что происходит: фильмы, которые были взяты напрокат, имеют дату и время, а те, которые не были взяты напрокат, — нет (значение rental_date равно NULL). Обратите также внимание, что в этом примере мы дважды использовали LEFT JOIN. Сначала мы соединяем film и inventory, и мы хотим убедиться, что даже если фильм не находится в нашем списке (и, следовательно, не может быть взят напрокат по определению), мы все равно выводим его. Затем мы соединяем таблицу rental с набором данных, полученным в результате предыдущего соединения. Мы снова используем LEFT JOIN, так как у нас могут быть фильмы, которых нет в нашем списке, и у них не будет строк в таблице rental. Однако у нас также могут быть фильмы, перечисленные в нашем списке, которые просто не были взяты напрокат. Вот почему нам нужно соединение LEFT JOIN обеих таблиц здесь.

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

mysql> SELECT title, rental_date
    -> FROM rental LEFT JOIN inventory USING (inventory_id)
    -> LEFT JOIN film USING (film_id)
    -> ORDER BY rental_date DESC;
+-----------------------------+---------------------+
| title                       | rental_date         |
+-----------------------------+---------------------+
| ...                                               |
| LOVE SUICIDES               | 2005-05-24 23:04:41 |
| GRADUATE LORD               | 2005-05-24 23:03:39 |
| FREAKY POCUS                | 2005-05-24 22:54:33 |
| BLANKET BEVERLY             | 2005-05-24 22:53:30 |
+-----------------------------+---------------------+
16044 rows in set (0.06 sec)

В этой версии запрос управляется таблицей rental, поэтому все строки из нее сопоставляются с таблицей inventory, а затем с film. Поскольку все строки в таблице rental по определению основаны на таблице inventory, которая связана с таблицей film, в выводе нет значений NULL. Не может быть проката фильма, которого не существует. Мы скорректировали запрос с помощью ORDER BY rent_date DESC, чтобы показать, что мы действительно не получили никаких значений NULL (они были бы последними).

Теперь вы можете видеть, что левое соединение полезно, когда мы уверены, что в левой таблице есть какие-то важные данные, но мы не уверены, есть ли они в правой таблице. Мы хотим получить строки из левой с соответствующими строками из правой или без них. Давайте попробуем применить это к запросу, который мы написали в «Предложении GROUP BY», который показал, что клиенты арендуют участки одной и той же категории. Вот запрос, как напоминание:

mysql> SELECT email, name AS category_name, COUNT(cat.category_id) AS cnt
    -> FROM customer cs INNER JOIN rental USING (customer_id)
    -> INNER JOIN inventory USING (inventory_id)
    -> INNER JOIN film_category USING (film_id)
    -> INNER JOIN category cat USING (category_id)
    -> GROUP BY email, category_name
    -> ORDER BY cnt DESC LIMIT 5;
+----------------------------------+---------------+-----+
| email                            | category_name | cnt |
+----------------------------------+---------------+-----+
| WESLEY.BULL@sakilacustomer.org   | Games         |   9 |
| ALMA.AUSTIN@sakilacustomer.org   | Animation     |   8 |
| KARL.SEAL@sakilacustomer.org     | Animation     |   8 |
| LYDIA.BURKE@sakilacustomer.org   | Documentary   |   8 |
| NATHAN.RUNYON@sakilacustomer.org | Animation     |   7 |
+----------------------------------+---------------+-----+
5 rows in set (0.06 sec)

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

Рассмотрим эту задачу. Нам нужно начать с таблицы category, так как в ней будут все категории для наших фильмов. Затем нам нужно начать строить целую цепочку левых соединений. Сначала мы делаем левое соединение category с film_category, так как у нас могут быть категории без фильмов. Затем мы левое соединение результата с таблицей inventory, так как некоторые фильмы, о которых мы знаем, могут отсутствовать в нашем каталоге. Затем делаем левое соединение этого результата к таблице rental, поскольку клиенты могли не брать напрокат некоторые фильмы в категории. Наконец, нам нужно левое соединение этого результата с нашей таблицей customer. Несмотря на то, что не может быть связанной записи о клиенте без аренды, пропуск левого соединения здесь приведет к тому, что MySQL отбросит строки для категорий, которые заканчиваются без записей о клиентах.

Теперь, после всего этого длинного объяснения, можем ли мы, наконец, продолжить фильтрацию по адресу электронной почты и получить наши данные? Нет! К сожалению, добавляя к таблице условие WHERE, которого нет в нашем отношении левого соединения, мы ломаем идею этого соединения. Посмотрите, что происходит:

mysql> SELECT COUNT(*) FROM category;
+----------+
| COUNT(*) |
+----------+
|       16 |
+----------+
1 row in set (0.00 sec)
mysql> SELECT email, name AS category_name, COUNT(category_id) AS cnt
    -> FROM category cat LEFT JOIN film_category USING (category_id)
    -> LEFT JOIN inventory USING (film_id)
    -> LEFT JOIN rental USING (inventory_id)
    -> JOIN customer cs ON rental.customer_id = cs.customer_id
    -> WHERE cs.email = 'WESLEY.BULL@sakilacustomer.org'
    -> GROUP BY email, category_name
    -> ORDER BY cnt DESC;
+--------------------------------+---------------+-----+
| email                          | category_name | cnt |
+--------------------------------+---------------+-----+
| WESLEY.BULL@sakilacustomer.org | Games         |   9 |
| WESLEY.BULL@sakilacustomer.org | Foreign       |   6 |
| ...                                                  |
| WESLEY.BULL@sakilacustomer.org | Comedy        |   1 |
| WESLEY.BULL@sakilacustomer.org | Sports        |   1 |
+--------------------------------+---------------+-----+
14 rows in set (0.00 sec)

Мы получили 14 категорий для нашего клиента, а всего их 16. На самом деле, MySQL оптимизирует все левые соединения в этом запросе, так как понимает, что они бессмысленны в таком виде. Нет простого способа ответить на вопрос, который у нас есть, используя только соединения — мы вернемся к этому примеру в разделе «Вложенные запросы в соединениях».

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

mysql> INSERT INTO category(name) VALUES ('Thriller');
Query OK, 1 row affected (0.01 sec)
mysql> SELECT cat.name, COUNT(rental_id) cnt
    -> FROM category cat LEFT JOIN film_category USING (category_id)
    -> LEFT JOIN inventory USING (film_id)
    -> LEFT JOIN rental USING (inventory_id)
    -> LEFT JOIN customer cs ON rental.customer_id = cs.customer_id
    -> GROUP BY 1
    -> ORDER BY 2 DESC;
+---------------+------+
| category_name | cnt  |
+---------------+------+
| Sports        | 1179 |
| Animation     | 1166 |
| ...                  |
| Music         |  830 |
| Thriller      |    0 |
+---------------+------+
17 rows in set (0.07 sec)

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

Мы показали вам, что имеет значение то, что идет до и после ключевой фразы LEFT JOIN. То, что находится слева, управляет процессом, отсюда и название «left join». Если вы действительно не хотите реорганизовывать свой запрос, чтобы он соответствовал этому шаблону, вы можете использовать RIGHT JOIN. Это точно то же самое, за исключением того, что то, что находится справа, управляет процессом.

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

mysql> SELECT title, rental_date
    -> FROM rental RIGHT JOIN inventory USING (inventory_id)
    -> RIGHT JOIN film USING (film_id)
    -> ORDER BY rental_date DESC;
...
| SUICIDES SILENCE            | NULL                |
| TADPOLE PARK                | NULL                |
| TREASURE COMMAND            | NULL                |
| VILLAIN DESPERATE           | NULL                |
| VOLUME HOUSE                | NULL                |
| WAKE JAWS                   | NULL                |
| WALLS ARTIST                | NULL                |
+-----------------------------+---------------------+
16087 rows in set (0.06 sec)

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

Как левое, так и правое соединение могут использовать предложения USING и ON, описанные в разделе «Внутреннее соединение». Вы должны использовать один или другой: без них вы получите декартово произведение, как обсуждалось в этом разделе.

Также есть дополнительное ключевое слово OUTER, которое вы можете при желании использовать в левом и правом соединениях, чтобы они читались как LEFT OUTER JOIN и RIGHT OUTER JOIN. Это просто альтернативный синтаксис, который не делает ничего другого, и вы не часто увидите его использование. В этой книге мы придерживаемся основных версий.

Естественное соединение

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

Естественное соединение должно быть волшебным образом естественным. Это означает, что вы сообщаете MySQL, к каким таблицам вы хотите присоединиться, и она выясняет, как это сделать, и выдает вам набор результатов INNER JOIN. Вот пример для таблиц actor_info и film_actor:

mysql> SELECT first_name, last_name, film_id
    -> FROM actor_info NATURAL JOIN film_actor
    -> LIMIT 20;
+------------+-----------+---------+
| first_name | last_name | film_id |
+------------+-----------+---------+
| PENELOPE   | GUINESS   |       1 |
| PENELOPE   | GUINESS   |      23 |
| ...                              |
| PENELOPE   | GUINESS   |     980 |
| NICK       | WAHLBERG  |       3 |
+------------+-----------+---------+
20 rows in set (0.28 sec)

На самом деле это не совсем волшебство: все, что MySQL делает, это ищет столбцы с одинаковыми именами и незаметно добавляет их во внутреннее соединение с условиями соединения, записанными в предложении WHERE. Итак, предыдущий запрос на самом деле переводится примерно так:

mysql> SELECT first_name, last_name, film_id FROM
    -> actor_info JOIN film_actor
    -> WHERE (actor_info.actor_id = film_actor.actor_id)
    -> LIMIT 20;

Если столбцы идентификаторов не имеют одинаковых имен, естественные соединения не будут работать. Кроме того, что более опасно, если столбцы с одинаковыми именами не являются идентификаторами, они все равно попадут в закулисное предложение USING. Вы можете очень легко увидеть это в базе данных sakila. На самом деле, именно поэтому мы прибегли к показу предыдущего примера с actor_info, который даже не является таблицей: это представление. Давайте посмотрим, что было бы, если бы мы использовали обычные таблицы actor и film_actor:

mysql> SELECT first_name, last_name, film_id FROM actor NATURAL JOIN film_actor;
Empty set (0.01 sec)

Но как? Проблема в том, что NATURAL JOIN действительно учитывает все столбцы. С базой данных sakila это огромное препятствие, так как в каждой таблице есть столбец last_update. Если бы вы выполнили оператор EXPLAIN для предыдущего запроса, а затем выполнили бы SHOW WARNINGS, вы бы увидели, что результирующий запрос бессмысленен:

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `sakila`.`customer`.`email` AS `email`,
`sakila`.`rental`.`rental_date` AS `rental_date`
from `sakila`.`customer` join `sakila`.`rental`
where ((`sakila`.`rental`.`last_update` = `sakila`.`customer`.`last_update`)
and (`sakila`.`rental`.`customer_id` = `sakila`.`customer`.`customer_id`))
1 row in set (0.00 sec)

Иногда вы будете видеть естественное соединение, смешанное с левым и правым соединением. Допустимы следующие синтаксисы соединения: NATURAL LEFT JOIN, NATURAL LEFT OUTER JOIN, NATURAL RIGHT JOIN и NATURAL RIGHT OUTER JOIN. Первые два являются левыми соединениями без предложений ON или USING, а последние два являются правыми соединениями. Опять же, по возможности избегайте их написания, но вы должны понимать, что они означают, если видите, что они используются.

Константные выражения в соединениях

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

Рассмотрим пример, перечислив все фильмы для конкретного актера:

mysql> SELECT first_name, last_name, title
    -> FROM actor JOIN film_actor USING (actor_id)
    -> JOIN film USING (film_id)
    -> WHERE actor_id = 11;
+------------+-----------+--------------------+
| first_name | last_name | title              |
+------------+-----------+--------------------+
| ZERO       | CAGE      | CANYON STOCK       |
| ZERO       | CAGE      | DANCES NONE        |
| ...                                         |
| ZERO       | CAGE      | WEST LION          |
| ZERO       | CAGE      | WORKER TARZAN      |
+------------+-----------+--------------------+
25 rows in set (0.00 sec)

Мы можем переместить предложение actor_id в соединение следующим образом:

mysql> SELECT first_name, last_name, title
    -> FROM actor JOIN film_actor
    ->   ON actor.actor_id = film_actor.actor_id
    ->   AND actor.actor_id = 11
    -> JOIN film USING (film_id);
+------------+-----------+--------------------+
| first_name | last_name | title              |
+------------+-----------+--------------------+
| ZERO       | CAGE      | CANYON STOCK       |
| ZERO       | CAGE      | DANCES NONE        |
| ...                                         |
| ZERO       | CAGE      | WEST LION          |
| ZERO       | CAGE      | WORKER TARZAN      |
+------------+-----------+--------------------+
25 rows in set (0.00 sec)

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

Помните этот запрос из раздела о левых соединениях:

mysql> SELECT email, name AS category_name, COUNT(rental_id) AS cnt
    -> FROM category cat LEFT JOIN film_category USING (category_id)
    -> LEFT JOIN inventory USING (film_id)
    -> LEFT JOIN rental USING (inventory_id)
    -> LEFT JOIN customer cs USING (customer_id)
    -> WHERE cs.email = 'WESLEY.BULL@sakilacustomer.org'
    -> GROUP BY email, category_name
    -> ORDER BY cnt DESC;
+--------------------------------+---------------+-----+
| email                          | category_name | cnt |
+--------------------------------+---------------+-----+
| WESLEY.BULL@sakilacustomer.org | Games         |   9 |
| WESLEY.BULL@sakilacustomer.org | Foreign       |   6 |
| ...                                                  |
| WESLEY.BULL@sakilacustomer.org | Comedy        |   1 |
| WESLEY.BULL@sakilacustomer.org | Sports        |   1 |
+--------------------------------+---------------+-----+
14 rows in set (0.01 sec)

Если мы продолжим и переместим предложение cs.email в часть LEFT JOIN customer cs, мы увидим совершенно другие результаты:

mysql> SELECT email, name AS category_name, COUNT(rental_id) AS cnt
    -> FROM category cat LEFT JOIN film_category USING (category_id)
    -> LEFT JOIN inventory USING (film_id)
    -> LEFT JOIN rental USING (inventory_id)
    -> LEFT JOIN customer cs ON rental.customer_id = cs.customer_id
    -> AND cs.email = 'WESLEY.BULL@sakilacustomer.org'
    -> GROUP BY email, category_name
    -> ORDER BY cnt DESC;
+--------------------------------+-------------+------+
| email                          | name        | cnt  |
+--------------------------------+-------------+------+
| NULL                           | Sports      | 1178 |
| NULL                           | Animation   | 1164 |
| ...                                                 |
| NULL                           | Travel      |  834 |
| NULL                           | Music       |  829 |
| WESLEY.BULL@sakilacustomer.org | Games       |    9 |
| WESLEY.BULL@sakilacustomer.org | Foreign     |    6 |
| ...                                                 |
| WESLEY.BULL@sakilacustomer.org | Comedy      |    1 |
| NULL                           | Thriller    |    0 |
+--------------------------------+-------------+------+
31 rows in set (0.07 sec)

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

Содержимое предложения WHERE применяется логически после разрешения и выполнения объединений. Мы сообщаем MySQL, что нам нужны только строки из того, к чему мы присоединяемся, где столбец cs.email равен 'WESLEY.BULL@sakilacustomer.org'. На самом деле MySQL достаточно умен, чтобы оптимизировать эту ситуацию, и фактически начнет выполнение плана, как если бы использовались обычные внутренние соединения. Когда у нас есть условие cs.email в предложении клиента LEFT JOIN, мы сообщаем MySQL, что хотим добавить столбцы из таблицы customer в наш набор результатов (который включает в себя таблицы category, inventory и rental), но только когда определенное значение присутствует в столбце email. Поскольку это LEFT JOIN, мы получаем NULL в каждом столбце клиента в строках, которые не совпадают.

Важно знать об этом поведении.

Вложенные запросы

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

Основы вложенных запросов

Вы знаете, как найти имена всех актеров, сыгравших в определенном фильме, с помощью INNER JOIN:

mysql> SELECT first_name, last_name FROM
    -> actor JOIN film_actor USING (actor_id)
    -> JOIN film USING (film_id)
    -> WHERE title = 'ZHIVAGO CORE';
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| UMA        | WOOD      |
| NICK       | STALLONE  |
| GARY       | PENN      |
| SALMA      | NOLTE     |
| KENNETH    | HOFFMAN   |
| WILLIAM    | HACKMAN   |
+------------+-----------+
6 rows in set (0.00 sec)

Но есть и другой способ, используя вложенный запрос (nested query):

mysql> SELECT first_name, last_name FROM
    -> actor JOIN film_actor USING (actor_id)
    -> WHERE film_id = (SELECT film_id FROM film
    -> WHERE title = 'ZHIVAGO CORE');
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| UMA        | WOOD      |
| NICK       | STALLONE  |
| GARY       | PENN      |
| SALMA      | NOLTE     |
| KENNETH    | HOFFMAN   |
| WILLIAM    | HACKMAN   |
+------------+-----------+
6 rows in set (0.00 sec)

Он называется вложенным запросом, потому что один запрос находится внутри другого. Внутренний запрос (inner query), или подзапрос (subquery) — тот, который является вложенным, — написан в скобках, и вы можете видеть, что он определяет film_id для фильма с названием ZHIVAGO CORE. Скобки необходимы для внутренних запросов. Внешний запрос (outer query) указан первым и здесь не заключен в скобки: вы можете видеть, что он находит first_name и last_name актеров из JOIN с film_actor с film_id, которые соответствуют результату подзапроса. Таким образом, внутренний запрос находит film_id, а внешний запрос использует его для поиска имен актеров. Всякий раз, когда используются вложенные запросы, их можно переписать как несколько отдельных запросов. Давайте сделаем это с предыдущим примером, так как это может помочь вам понять, что происходит:

mysql> SELECT film_id FROM film WHERE title = 'ZHIVAGO CORE';
+---------+
| film_id |
+---------+
|     998 |
+---------+
1 row in set (0.03 sec)
mysql> SELECT first_name, last_name
    -> FROM actor JOIN film_actor USING (actor_id)
    -> WHERE film_id = 998;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| UMA        | WOOD      |
| NICK       | STALLONE  |
| GARY       | PENN      |
| SALMA      | NOLTE     |
| KENNETH    | HOFFMAN   |
| WILLIAM    | HACKMAN   |
+------------+-----------+
6 rows in set (0.00 sec)

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

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

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

mysql> SELECT MAX(rental_date) FROM rental
    -> JOIN customer USING (customer_id)
    -> WHERE email = 'WESLEY.BULL@sakilacustomer.org';
+---------------------+
| MAX(rental_date)    |
+---------------------+
| 2005-08-23 15:46:33 |
+---------------------+
1 row in set (0.01 sec)

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

mysql> SELECT title FROM film
    -> JOIN inventory USING (film_id)
    -> JOIN rental USING (inventory_id)
    -> JOIN customer USING (customer_id)
    -> WHERE email = 'WESLEY.BULL@sakilacustomer.org'
    -> AND rental_date = '2005-08-23 15:46:33';
+-------------+
| title       |
+-------------+
| KARATE MOON |
+-------------+
1 row in set (0.00 sec)

С помощью вложенного запроса вы можете выполнить оба шага за один раз:

mysql> SELECT title FROM film JOIN inventory USING (film_id)
    -> JOIN rental USING (inventory_id)
    -> WHERE rental_date = (SELECT MAX(rental_date) FROM rental
    -> JOIN customer USING (customer_id)
    -> WHERE email = 'WESLEY.BULL@sakilacustomer.org');
+-------------+
| title       |
+-------------+
| KARATE MOON |
+-------------+
1 row in set (0.01 sec)

Вы можете видеть, что вложенный запрос объединяет два предыдущих запроса. Вместо использования постоянных значений даты и времени, обнаруженных в предыдущем запросе, он выполняет запрос непосредственно как подзапрос. Это самый простой тип вложенного запроса, который возвращает скалярный операнд (scalar operand), то есть одно значение.

Предложения ANY, SOME, ALL, IN и NOT IN

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

База данных, которую мы установим, — это образец базы данных employees. Вы можете найти инструкции по установке в документации MySQL или в репозитории базы данных GitHub. Либо клонируйте репозиторий с помощью git, либо загрузите последнюю версию (1.0.7 на момент написания статьи). Когда у вас будут готовы необходимые файлы, вам нужно запустить две команды.

Первая команда создает необходимые структуры и загружает данные:

$ mysql -uroot -p < employees.sql
INFO
CREATING DATABASE STRUCTURE
INFO
storage engine: InnoDB
INFO
LOADING departments
INFO
LOADING employees
INFO
LOADING dept_emp
INFO
LOADING dept_manager
INFO
LOADING titles
INFO
LOADING salaries
data_load_time_diff
00:00:28

Вторая команда проверяет установку:

$ mysql -uroot -p < test_employees_md5.sql
INFO
TESTING INSTALLATION
table_name      expected_records        expected_crc
departments     9       d1af5e170d2d1591d776d5638d71fc5f
dept_emp        331603  ccf6fe516f990bdaa49713fc478701b7
dept_manager    24      8720e2f0853ac9096b689c14664f847e
employees       300024  4ec56ab5ba37218d187cf6ab09ce1aa1
salaries        2844047 fd220654e95aea1b169624ffe3fca934
titles  443308  bfa016c472df68e70a03facafa1bc0a8
table_name      found_records           found_crc
departments     9       d1af5e170d2d1591d776d5638d71fc5f
dept_emp        331603  ccf6fe516f990bdaa49713fc478701b7
dept_manager    24      8720e2f0853ac9096b689c14664f847e
employees       300024  4ec56ab5ba37218d187cf6ab09ce1aa1
salaries        2844047 fd220654e95aea1b169624ffe3fca934
titles  443308  bfa016c472df68e70a03facafa1bc0a8
table_name      records_match   crc_match
departments     OK      ok
dept_emp        OK      ok
dept_manager    OK      ok
employees       OK      ok
salaries        OK      ok
titles  OK      ok
computation_time
00:00:25
summary result
CRC     OK
count   OK

Как только это будет сделано, вы можете приступить к работе с примерами, которые мы предоставим дальше.

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

$ mysql employees

Или выполните следующее в командной строке mysql, чтобы изменить базу данных по умолчанию:

mysql> USE employees

Теперь вы готовы двигаться вперед.

Использование ANY и IN

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

mysql> SELECT emp_no, first_name, last_name, hire_date
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Assistant Engineer'
    -> AND hire_date < ANY (SELECT hire_date FROM
    -> employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager');
+--------+----------------+------------------+------------+
| emp_no | first_name     | last_name        | hire_date  |
+--------+----------------+------------------+------------+
|  10009 | Sumant         | Peac             | 1985-02-18 |
|  10066 | Kwee           | Schusler         | 1986-02-26 |
| ...                                                     |
| ...                                                     |
| 499958 | Srinidhi       | Theuretzbacher   | 1989-12-17 |
| 499974 | Shuichi        | Piazza           | 1989-09-16 |
+--------+----------------+------------------+------------+
10747 rows in set (0.20 sec)

Оказывается, есть много людей, которые соответствуют этим критериям! Подзапрос находит даты найма менеджеров:

mysql> SELECT hire_date FROM
    -> employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager';
+------------+
| hire_date  |
+------------+
| 1985-01-01 |
| 1986-04-12 |
| ...        |
| 1991-08-17 |
| 1989-07-10 |
+------------+
24 rows in set (0.10 sec)

Внешний запрос проходит через каждого сотрудника со званием Associate Engineer, возвращая инженера, если его дата найма ниже (старее), чем любое из значений в наборе, возвращаемом подзапросом. Так, например, Sumant Peac выводится, потому что 1985-02-18 старше по крайней мере одного значения в наборе (как вы можете видеть, вторая дата найма, возвращенная для менеджеров, — 1986-04-12). Ключевое слово ANY означает именно это: оно истинно, если предшествующий ему столбец или выражение истинно для любого (any) из значений в наборе, возвращаемом подзапросом. Используемый таким образом, ANY имеет псевдоним SOME, который был включен для того, чтобы некоторые запросы можно было более четко читать как английские выражения; он не делает ничего другого, и вы редко увидите, как он используется.

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

Рассмотрим другой пример с использованием ANY. Предположим, вы хотите узнать менеджеров, которые также имеют какую-либо другую должность. Вы можете сделать это с помощью следующего вложенного запроса:

mysql> SELECT emp_no, first_name, last_name
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager'
    -> AND emp_no = ANY (SELECT emp_no FROM employees
    -> JOIN titles USING (emp_no) WHERE
    -> title <> 'Manager');
+--------+-------------+--------------+
| emp_no | first_name  | last_name    |
+--------+-------------+--------------+
| 110022 | Margareta   | Markovitch   |
| 110039 | Vishwani    | Minakawa     |
| ...                                 |
| 111877 | Xiaobin     | Spinelli     |
| 111939 | Yuchang     | Weedman      |
+--------+-------------+--------------+
24 rows in set (0.11 sec)

= ANY приводит к тому, что внешний запрос возвращает менеджера, когда emp_no равно любому из номеров сотрудников инженера, возвращенных подзапросом. Ключевая фраза = ANY имеет псевдоним IN, который часто используется во вложенных запросах. Используя IN, предыдущий пример можно переписать так:

mysql> SELECT emp_no, first_name, last_name
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager'
    -> AND emp_no IN (SELECT emp_no FROM employees
    -> JOIN titles USING (emp_no) WHERE
    -> title <> 'Manager');
+--------+-------------+--------------+
| emp_no | first_name  | last_name    |
+--------+-------------+--------------+
| 110022 | Margareta   | Markovitch   |
| 110039 | Vishwani    | Minakawa     |
| ...                                 |
| 111877 | Xiaobin     | Spinelli     |
| 111939 | Yuchang     | Weedman      |
+--------+-------------+--------------+
24 rows in set (0.11 sec)

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

mysql> SELECT DISTINCT emp_no, first_name, last_name
    -> FROM employees JOIN titles mgr USING (emp_no)
    -> JOIN titles nonmgr USING (emp_no)
    -> WHERE mgr.title = 'Manager'
    -> AND nonmgr.title <> 'Manager';
+--------+-------------+--------------+
| emp_no | first_name  | last_name    |
+--------+-------------+--------------+
| 110022 | Margareta   | Markovitch   |
| 110039 | Vishwani    | Minakawa     |
| ...                                 |
| 111877 | Xiaobin     | Spinelli     |
| 111939 | Yuchang     | Weedman      |
+--------+-------------+--------------+
24 rows in set (0.11 sec)

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

Использование ALL

Предположим, вы хотите найти помощников инженеров, которые более опытны, чем все менеджеры, то есть более опытны, чем самый опытный менеджер. Вы можете сделать это с помощью ключевого слова ALL вместо ANY:

mysql> SELECT emp_no, first_name, last_name, hire_date
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Assistant Engineer'
    -> AND hire_date < ALL (SELECT hire_date FROM
    -> employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager');
Empty set (0.18 sec)

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

mysql> (SELECT 'Assistant Engineer' AS title,
    -> MIN(hire_date) AS mhd FROM employees
    -> JOIN titles USING (emp_no)
    -> WHERE title = 'Assistant Engineer')
    -> UNION
    -> (SELECT 'Manager' title, MIN(hire_date) mhd FROM employees
    -> JOIN titles USING (emp_no)
    -> WHERE title = 'Manager');
+--------------------+------------+
| title              | mhd        |
+--------------------+------------+
| Assistant Engineer | 1985-02-01 |
| Manager            | 1985-01-01 |
+--------------------+------------+
2 rows in set (0.26 sec)

Глядя на данные, мы видим, что первый менеджер был принят на работу 1 января 1985 года, а первый помощник инженера только 1 февраля того же года. В то время как ключевое слово ANY возвращает значения, удовлетворяющие хотя бы одному условию (логическое ИЛИ), ключевое слово ALL возвращает значения, только если выполняются все условия (логическое И).

Мы можем использовать псевдоним NOT IN вместо <> ANY или != ANY. Найдем всех менеджеров, не являющихся старшими сотрудниками:

mysql> SELECT emp_no, first_name, last_name
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager' AND emp_no NOT IN
    -> (SELECT emp_no FROM titles
    -> WHERE title = 'Senior Staff');
+--------+-------------+--------------+
| emp_no | first_name  | last_name    |
+--------+-------------+--------------+
| 110183 | Shirish     | Ossenbruggen |
| 110303 | Krassimir   | Wegerle      |
| ...                                 |
| 111400 | Arie        | Staelin      |
| 111692 | Tonny       | Butterworth  |
+--------+-------------+--------------+
15 rows in set (0.09 sec)

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

У ключевого слова ALL есть несколько уловок и ловушек:

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

Написание подзапросов строки

В предыдущих примерах подзапрос возвращал одно скалярное значение (например, actor_id) или набор значений из одного столбца (например, все значения emp_no). В этом разделе описывается другой тип подзапроса, подзапрос строки (row subquery), который работает с несколькими столбцами из нескольких строк.

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

mysql> SELECT mgr.emp_no, YEAR(mgr.from_date) AS fd
    -> FROM titles AS mgr, titles AS other
    -> WHERE mgr.emp_no = other.emp_no
    -> AND mgr.title = 'Manager'
    -> AND mgr.title <> other.title
    -> AND YEAR(mgr.from_date) = YEAR(other.from_date);
+--------+------+
| emp_no | fd   |
+--------+------+
| 110765 | 1989 |
| 111784 | 1988 |
+--------+------+
2 rows in set (0.11 sec)

Но вы также можете написать это как вложенный запрос:

mysql> SELECT emp_no, YEAR(from_date) AS fd
    -> FROM titles WHERE title = 'Manager' AND
    -> (emp_no, YEAR(from_date)) IN
    -> (SELECT emp_no, YEAR(from_date)
    -> FROM titles WHERE title <> 'Manager');
+--------+------+
| emp_no | fd   |
+--------+------+
| 110765 | 1989 |
| 111784 | 1988 |
+--------+------+
2 rows in set (0.12 sec)

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

Синтаксис подзапроса строки позволяет сравнивать несколько значений в строке. Выражение (emp_no, YEAR(from_date)) означает, что два значения в строке сравниваются с выходными данными подзапроса. После ключевого слова IN видно, что подзапрос возвращает два значения: emp_no и YEAR(from_date). Итак, фрагмент:

(emp_no, YEAR(from_date)) IN (SELECT emp_no, YEAR(from_date)
FROM titles WHERE title <> 'Manager')

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

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

mysql> SELECT first_name, last_name
    -> FROM employees, titles
    -> WHERE (employees.emp_no, first_name, last_name, title) =
    -> (titles.emp_no, 'Marjo', 'Giarratana', 'Senior Staff');
+------------+------------+
| first_name | last_name  |
+------------+------------+
| Marjo      | Giarratana |
+------------+------------+
1 row in set (0.09 sec)

Это не вложенный запрос, но он показывает, как работает новый синтаксис подзапроса строки. Вы можете видеть, что запрос сопоставляет список столбцов перед знаком равенства (employees.emp_no, first_name, last_name, title) со списком столбцов и значений после знака равенства (titles.emp_no, 'Marjo', 'Giarratana', 'Senior Staff'). Таким образом, когда значения emp_no совпадают, полное имя сотрудника — Marjo Giarratana, а должность — Senior Staff, мы получаем результат запроса. Мы не рекомендуем писать подобные запросы — вместо этого используйте обычное предложение WHERE с несколькими условиями AND, — но это точно иллюстрирует, что происходит. В качестве упражнения попробуйте написать этот запрос, используя соединение.

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

Предложения EXISTS и NOT EXISTS

Вы познакомились с тремя типами подзапросов: скалярные подзапросы, подзапросы столбцов и подзапросы строк. В этом разделе вы узнаете о четвертом типе, коррелированном подзапросе (correlated subquery), где таблица, используемая во внешнем запросе, упоминается в подзапросе. Коррелированные подзапросы часто используются с оператором IN, который мы уже обсуждали, и почти всегда используются с предложениями EXISTS и NOT EXISTS, которым посвящен этот раздел.

Основы EXISTS и NOT EXISTS

Прежде чем мы начнем обсуждение коррелированных подзапросов, давайте рассмотрим, что делает предложение EXISTS. Нам понадобится простой, но странный пример, чтобы представить предложение, так как мы пока не обсуждаем коррелированные подзапросы. Итак, приступим: предположим, вы хотите найти количество всех фильмов в базе данных, но только в том случае, если база данных активна, что, как вы определили, означает, что хотя бы один фильм из любого филиала был взят напрокат. Вот запрос, который это делает (не забудьте снова подключиться к базе данных sakila перед запуском этого запроса — подсказка: используйте команду <db>):

mysql> SELECT COUNT(*) FROM film
    -> WHERE EXISTS (SELECT * FROM rental);
+----------+
| COUNT(*) |
+----------+
|     1000 |
+----------+
1 row in set (0.01 sec)

Подзапрос возвращает все строки из таблицы аренды. Однако важно то, что он возвращает хотя бы одну строку; не имеет значения, что находится в строке, сколько строк в ней или содержит ли строка только значения NULL. Таким образом, вы можете думать о подзапросе как об истинном или ложном, и в этом случае это истинно, потому что он производит некоторый вывод. Если подзапрос истинен, внешний запрос, использующий предложение EXISTS, возвращает строку. Общий результат заключается в том, что подсчитываются все строки в таблице film, потому что для каждой из них подзапрос верен.

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

mysql> SELECT title FROM film
    -> WHERE EXISTS (SELECT * FROM film
    -> WHERE title = 'IS THIS A MOVIE?');
Empty set (0.00 sec)

Поскольку подзапрос неверен, строки не возвращаются, потому что IS THIS A MOVIE? отсутствует в нашей базе данных — внешний запрос не возвращает никаких результатов.

Предложение NOT EXISTS делает обратное. Представьте, что вам нужен список всех актеров, если в базе данных нет определенного фильма. Вот:

mysql> SELECT * FROM actor WHERE NOT EXISTS
    -> (SELECT * FROM film WHERE title = 'ZHIVAGO CORE');
Empty set (0.00 sec)

На этот раз внутренний запрос верен, но предложение NOT EXISTS отрицает его, чтобы дать false. Поскольку оно ложно, внешний запрос не дает результатов.

Вы заметите, что подзапрос начинается с SELECT * FROM film. На самом деле не имеет значения, что вы выбираете во внутреннем запросе, когда используете предложение EXISTS, так как оно все равно не используется внешним запросом. Вы можете выбрать один столбец, все или даже константу (как в SELECT 'cat' from film), и это будет иметь тот же эффект. Однако традиционно вы увидите, что большинство авторов SQL пишут SELECT * по соглашению.

Коррелированные подзапросы

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

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

mysql> SELECT first_name, last_name FROM staff
    -> WHERE EXISTS (SELECT * FROM customer
    -> WHERE customer.first_name = staff.first_name
    -> AND customer.last_name = staff.last_name);
Empty set (0.01 sec)

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

mysql> INSERT INTO customer(store_id, first_name, last_name,
    -> email, address_id, create_date)
    -> VALUES (1, 'Mike', 'Hillyer',
    -> 'Mike.Hillyer@sakilastaff.com', 3, NOW());
Query OK, 1 row affected (0.02 sec)

И повторите запрос:

mysql> SELECT first_name, last_name FROM staff
    -> WHERE EXISTS (SELECT * FROM customer
    -> WHERE customer.first_name = staff.first_name
    -> AND customer.last_name = staff.last_name);
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| Mike       | Hillyer   |
+------------+-----------+
1 row in set (0.00 sec)

Итак, запрос работает; теперь нам просто нужно понять, как!

Давайте рассмотрим подзапрос в нашем предыдущем примере. Вы можете видеть, что в предложении FROM указана только таблица клиентов, но в предложении WHERE используется столбец из таблицы staff. Если вы запустите его изолированно, вы увидите, что это не разрешено:

mysql> SELECT * FROM customer WHERE customer.first_name = staff.first_name;
ERROR 1054 (42S22): Unknown column 'staff.first_name' in 'where clause'

Однако это допустимо при выполнении в качестве подзапроса, поскольку доступ к таблицам, перечисленным во внешнем запросе, разрешен в подзапросе. Таким образом, в этом примере текущее значение staff.first_name и staff.last_name во внешнем запросе передается в подзапрос как постоянное скалярное значение и сравнивается с именем и фамилией клиента. Если имя клиента совпадает с именем сотрудника, подзапрос верен, и поэтому внешний запрос выводит строку. Рассмотрим два случая, иллюстрирующие это более наглядно:

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

Теперь мы рассмотрим другой пример с использованием EXISTS. Попробуем найти количество всех фильмов, у которых есть хотя бы две копии. Чтобы сделать это с помощью EXISTS, нам нужно продумать, что должны делать внутренние и внешние запросы. Внутренний запрос должен давать результат только тогда, когда условие, которое мы проверяем, истинно; в этом случае он должен производить вывод, когда в списке есть как минимум две строки для одного и того же фильма. Внешний запрос должен увеличивать счетчик всякий раз, когда внутренний запрос имеет значение true. Вот запрос:

mysql> SELECT COUNT(*) FROM film WHERE EXISTS
    -> (SELECT film_id FROM inventory
    -> WHERE inventory.film_id = film.film_id
    -> GROUP BY film_id HAVING COUNT(*) >= 2);
+----------+
| COUNT(*) |
+----------+
|      958 |
+----------+
1 row in set (0.00 sec)

Это еще один запрос, в котором нет необходимости вложенности и достаточно объединения, но давайте остановимся на этой версии в целях объяснения. Взгляните на внутренний запрос: вы можете видеть, что предложение WHERE гарантирует, что фильмы совпадают по уникальному film_id, и подзапрос рассматривает только совпадающие строки для текущего фильма. Предложение GROUP BY группирует строки для этого фильма, но только если в списке есть как минимум две записи. Поэтому внутренний запрос выдает результат только тогда, когда в нашем списке есть как минимум две строки для текущего фильма. Внешний запрос прост: его можно рассматривать как увеличение счетчика, когда подзапрос производит вывод.

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

mysql> SELECT emp_no, first_name, last_name
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager'
    -> AND emp_no IN (SELECT emp_no FROM employees
    -> JOIN titles USING (emp_no) WHERE
    -> title <> 'Manager');
+--------+-------------+--------------+
| emp_no | first_name  | last_name    |
+--------+-------------+--------------+
| 110022 | Margareta   | Markovitch   |
| 110039 | Vishwani    | Minakawa     |
| ...                                 |
| 111877 | Xiaobin     | Spinelli     |
| 111939 | Yuchang     | Weedman      |
+--------+-------------+--------------+
24 rows in set (0.11 sec)

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

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

mysql> SELECT emp_no, first_name, last_name
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager'
    -> AND EXISTS (SELECT emp_no FROM titles
    -> WHERE titles.emp_no = employees.emp_no
    -> AND title <> 'Manager');
+--------+-------------+--------------+
| emp_no | first_name  | last_name    |
+--------+-------------+--------------+
| 110022 | Margareta   | Markovitch   |
| 110039 | Vishwani    | Minakawa     |
| ...                                 |
| 111877 | Xiaobin     | Spinelli     |
| 111939 | Yuchang     | Weedman      |
+--------+-------------+--------------+
24 rows in set (0.09 sec)

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

Коррелированные подзапросы можно использовать с любым вложенным запросом. Вот предыдущий запрос IN, переписанный с внешней ссылкой:

mysql> SELECT emp_no, first_name, last_name
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager'
    -> AND emp_no IN (SELECT emp_no FROM titles
    -> WHERE titles.emp_no = employees.emp_no
    -> AND title <> 'Manager');
+--------+-------------+--------------+
| emp_no | first_name  | last_name    |
+--------+-------------+--------------+
| 110022 | Margareta   | Markovitch   |
| 110039 | Vishwani    | Minakawa     |
| ...                                 |
| 111877 | Xiaobin     | Spinelli     |
| 111939 | Yuchang     | Weedman      |
+--------+-------------+--------------+
24 rows in set (0.09 sec)

Запрос более запутан, чем должен быть, но он иллюстрирует идею. Вы можете видеть, что emp_no в подзапросе ссылается на таблицу employees из внешнего запроса.

Если запрос будет возвращать одну строку, его также можно переписать так, чтобы вместо IN использовалось равенство:

mysql> SELECT emp_no, first_name, last_name
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager'
    -> AND emp_no = (SELECT emp_no FROM titles
    -> WHERE titles.emp_no = employees.emp_no
    -> AND title <> 'Manager');
ERROR 1242 (21000): Subquery returns more than 1 row

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

mysql> SELECT emp_no, first_name, last_name
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager'
    -> AND emp_no = (SELECT emp_no FROM titles
    -> WHERE titles.emp_no = employees.emp_no
    -> AND title = 'Senior Engineer');
+--------+------------+-----------+
| emp_no | first_name | last_name |
+--------+------------+-----------+
| 110344 | Rosine     | Cools     |
| 110420 | Oscar      | Ghazalie  |
| 110800 | Sanjoy     | Quadeer   |
+--------+------------+-----------+
3 rows in set (0.10 sec)

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

Вложенные запросы в предложении FROM

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

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

mysql> SELECT emp_no, monthly_salary FROM
    -> (SELECT emp_no, salary/12 AS monthly_salary FROM salaries) AS ms
    -> LIMIT 5;
+--------+----------------+
| emp_no | monthly_salary |
+--------+----------------+
|  10001 |      5009.7500 |
|  10001 |      5175.1667 |
|  10001 |      5506.1667 |
|  10001 |      5549.6667 |
|  10001 |      5580.0833 |
+--------+----------------+
5 rows in set (0.00 sec)

Сосредоточьтесь на том, что следует за предложением FROM. Подзапрос использует таблицу salaries и возвращает два столбца: первый столбец — emp_no; второй столбец имеет псевдоним month_salary и представляет собой значение salary, деленное на 12. Внешний запрос прост: он просто возвращает значения emp_no и month_salary, созданные с помощью подзапроса. Обратите внимание, что мы добавили псевдоним таблицы ms для подзапроса. Когда мы используем подзапрос в качестве таблицы, то есть применяем к нему операцию SELECT FROM, эта «производная таблица» должна иметь псевдоним, даже если мы не используем псевдоним в нашем запросе. MySQL жалуется, если мы опускаем псевдоним:

mysql> SELECT emp_no, monthly_salary FROM
    -> (SELECT emp_no, salary/12 AS monthly_salary FROM salaries)
    -> LIMIT 5;
ERROR 1248 (42000): Every derived table must have its own alias

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

mysql> SELECT AVG(gross) FROM
    -> (SELECT SUM(amount) AS gross
    -> FROM payment JOIN rental USING (rental_id)
    -> JOIN inventory USING (inventory_id)
    -> JOIN film USING (film_id)
    -> GROUP BY film_id) AS gross_amount;
+------------+
| AVG(gross) |
+------------+
|  70.361754 |
+------------+
1 row in set (0.05 sec)

Вы можете видеть, что внутренний запрос объединяет payment, rental, inventory и film, а также группирует продажи по фильмам, чтобы вы могли получить сумму для каждого фильма. Если вы запустите его изолированно, вот что произойдет:

mysql> SELECT SUM(amount) AS gross
    -> FROM payment JOIN rental USING (rental_id)
    -> JOIN inventory USING (inventory_id)
    -> JOIN film USING (film_id)
    -> GROUP BY film_id;
+--------+
| gross  |
+--------+
|  36.77 |
|  52.93 |
|  37.88 |
|    ... |
|  14.91 |
|  73.83 |
| 214.69 |
+--------+
958 rows in set (0.08 sec)

Теперь внешний запрос берет эти суммы, которые обозначены как gross, и усредняет их, чтобы получить окончательный результат. Этот запрос представляет собой типичный способ применения двух агрегатных функций к одному набору данных. Вы не можете применять агрегатные функции в каскаде, как в AVG(SUM(amount)):

mysql> SELECT AVG(SUM(amount)) AS avg_gross
    -> FROM payment JOIN rental USING (rental_id)
    -> JOIN inventory USING (inventory_id)
    -> JOIN film USING (film_id) GROUP BY film_id;
ERROR 1111 (HY000): Invalid use of group function

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

Вложенные запросы в JOIN

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

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

mysql> SELECT cat.name AS category_name, COUNT(cat.category_id) AS cnt
    -> FROM category AS cat LEFT JOIN film_category USING (category_id)
    -> LEFT JOIN inventory USING (film_id)
    -> LEFT JOIN rental USING (inventory_id)
    -> JOIN customer AS cs ON rental.customer_id = cs.customer_id
    -> WHERE cs.email = 'WESLEY.BULL@sakilacustomer.org'
    -> GROUP BY category_name ORDER BY cnt DESC;
+-------------+-----+
| name        | cnt |
+-------------+-----+
| Games       |   9 |
| Foreign     |   6 |
| ...               |
| ...               |
| Comedy      |   1 |
| Sports      |   1 |
+-------------+-----+
14 rows in set (0.00 sec)

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

mysql> SELECT cat.name AS category_name, cnt
    -> FROM category AS cat
    -> LEFT JOIN (SELECT cat.name, COUNT(cat.category_id) AS cnt
    ->    FROM category AS cat
    ->    LEFT JOIN film_category USING (category_id)
    ->    LEFT JOIN inventory USING (film_id)
    ->    LEFT JOIN rental USING (inventory_id)
    ->    JOIN customer cs ON rental.customer_id = cs.customer_id
    ->    WHERE cs.email = 'WESLEY.BULL@sakilacustomer.org'
    ->    GROUP BY cat.name) customer_cat USING (name)
    -> ORDER BY cnt DESC;
+-------------+------+
| name        | cnt  |
+-------------+------+
| Games       |    9 |
| Foreign     |    6 |
| ...                |
| Children    |    1 |
| Sports      |    1 |
| Sci-Fi      | NULL |
| Action      | NULL |
| Thriller    | NULL |
+-------------+------+
17 rows in set (0.01 sec)

Наконец, мы получаем все отображаемые категории и получаем значения NULL для тех категорий, в которых аренда не производилась. Давайте посмотрим, что происходит в нашем новом запросе. Подзапрос, который мы назвали customer_cat, является нашим предыдущим запросом без предложения ORDER BY. Таким образом, мы знаем, что он вернет: 14 строк для категорий, в которых Wesley что-то арендовал, и количество аренд в каждой. Затем используйте LEFT JOIN, чтобы соединить эту информацию с полным списком категорий из таблицы category. Таблица category управляет объединением, поэтому в ней будут выбраны все строки. Мы присоединяемся к подзапросу, используя столбец имени, который совпадает между выходными данными подзапроса и столбцом таблицы category.

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

Пользовательские переменные

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

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

mysql> SELECT @film:=title FROM film WHERE film_id = 1;
+------------------+
| @film:=title     |
+------------------+
| ACADEMY DINOSAUR |
+------------------+
1 row in set, 1 warning (0.00 sec)

Пользовательская переменная называется film и обозначается как пользовательская переменная символом @, который предшествует ей. Значение присваивается с помощью оператора :=. Вы можете распечатать содержимое пользовательской переменной с помощью следующего очень короткого запроса:

mysql> SELECT @film;
+------------------+
| @film            |
+------------------+
| ACADEMY DINOSAUR |
+------------------+
1 row in set (0.00 sec)

Возможно, вы заметили предупреждение — о чем это было?

mysql> SELECT @film:=title FROM film WHERE film_id = 1;
mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Warning
   Code: 1287
Message: Setting user variables within expressions is deprecated
and will be removed in a future release. Consider alternatives:
'SET variable=expression, ...', or
'SELECT expression(s) INTO variables(s)'.
1 row in set (0.00 sec)

Давайте рассмотрим две предложенные альтернативы. Во-первых, мы все еще можем выполнить вложенный запрос в операторе SET:

mysql> SET @film := (SELECT title FROM film WHERE film_id = 1);
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @film;
+------------------+
| @film            |
+------------------+
| ACADEMY DINOSAUR |
+------------------+
1 row in set (0.00 sec)

Во-вторых, мы можем использовать оператор SELECT INTO:

mysql> SELECT title INTO @film FROM film WHERE film_id = 1;
Query OK, 1 row affected (0.00 sec)
mysql> SELECT @film;
+------------------+
| @film            |
+------------------+
| ACADEMY DINOSAUR |
+------------------+
1 row in set (0.00 sec)

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

mysql> SET @counter := 0;
Query OK, 0 rows affected (0.00 sec)

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

mysql> SET @counter = 0, @age := 23;
Query OK, 0 rows affected (0.00 sec)

Альтернативный синтаксис для SETSELECT INTO. Вы можете инициализировать одну переменную:

mysql> SELECT 0 INTO @counter;
Query OK, 1 row affected (0.00 sec)

Или несколько переменных одновременно:

mysql> SELECT 0, 23 INTO @counter, @age;
Query OK, 1 row affected (0.00 sec)

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

mysql> SELECT MAX(rental_date) FROM rental
    -> JOIN customer USING (customer_id)
    -> WHERE email = 'WESLEY.BULL@sakilacustomer.org';
+---------------------+
| MAX(rental_date)    |
+---------------------+
| 2005-08-23 15:46:33 |
+---------------------+
1 row in set (0.01 sec)
mysql> SELECT title FROM film
    -> JOIN inventory USING (film_id)
    -> JOIN rental USING (inventory_id)
    -> JOIN customer USING (customer_id)
    -> WHERE email = 'WESLEY.BULL@sakilacustomer.org'
    -> AND rental_date = '2005-08-23 15:46:33';
+-------------+
| title       |
+-------------+
| KARATE MOON |
+-------------+
1 row in set (0.00 sec)

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

mysql> SELECT MAX(rental_date) INTO @recent FROM rental
    -> JOIN customer USING (customer_id)
    -> WHERE email = 'WESLEY.BULL@sakilacustomer.org';
1 row in set (0.01 sec)
mysql> SELECT title FROM film
    -> JOIN inventory USING (film_id)
    -> JOIN rental USING (inventory_id)
    -> JOIN customer USING (customer_id)
    -> WHERE email = 'WESLEY.BULL@sakilacustomer.org'
    -> AND rental_date = @recent;
+-------------+
| title       |
+-------------+
| KARATE MOON |
+-------------+
1 row in set (0.00 sec)

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

Вот несколько рекомендаций по использованию пользовательских переменных:

Давайте рассмотрим первую проблему более подробно, используя новую переменную @fid. Поскольку мы ранее не использовали эту переменную, она пуста. Теперь давайте покажем film_id для фильмов, у которых есть запись в таблице inventory. Вместо того, чтобы показывать его напрямую, мы назначим film_id переменной @fid. Наш запрос покажет переменную три раза — один раз перед операцией присваивания, один раз как часть операции присваивания и один раз после нее:

mysql> SELECT @fid, @fid:=film.film_id, @fid FROM film, inventory
    -> WHERE inventory.film_id = @fid; 
Empty set, 1 warning (0.16 sec)

Это ничего не возвращает, кроме предупреждения об устаревании; поскольку в переменной нет ничего, с чего можно было бы начать, предложение WHERE пытается найти пустые значения inventory.film_id. Если мы изменим запрос, чтобы использовать film.film_id как часть предложения WHERE, все будет работать так, как ожидалось:

mysql> SELECT @fid, @fid:=film.film_id, @fid FROM film, inventory
    -> WHERE inventory.film_id = film.film_id LIMIT 20; 
+------+--------------------+------+
| @fid | @fid:=film.film_id | @fid |
+------+--------------------+------+
| NULL |                  1 | 1    |
| 1    |                  1 | 1    |
| 1    |                  1 | 1    |
| ...                              |
| 4    |                  4 | 4    |
| 4    |                  4 | 4    |
+------+--------------------+------+
20 rows in set, 1 warning (0.00 sec) 

Теперь, если @fid не пуст, первоначальный запрос даст некоторые результаты:

mysql> SELECT @fid, @fid:=film.film_id, @fid FROM film, inventory
    -> WHERE inventory.film_id = film.film_id LIMIT 20; 
+------+--------------------+------+
| @fid | @fid:=film.film_id | @fid |
+------+--------------------+------+
|    4 |                  1 |    1 |
|    1 |                  1 |    1 |
|  ...                             |
|    4 |                  4 |    4 |
|    4 |                  4 |    4 |
+------+--------------------+------+
20 rows in set, 1 warning (0.00 sec)

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

Глава 6
Транзакции и блокировки

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

Во-первых, давайте определимся с ключевыми понятиями. Транзакция (transaction) — это операция, выполняемая (с использованием одного или нескольких операторов SQL) в базе данных как единая логическая единица работы. Все изменения операторов SQL в транзакции либо фиксируются (применяются к базе данных), либо откатываются (отменяются из базы данных) как единое целое, а не только частично. Транзакция базы данных должна быть атомарной, согласованной, изолированной и надежной (известная аббревиатура ACID (atomic, consistent, isolated, durable)).

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

Базам данных не понадобились бы транзакции и блокировки, если бы запросы выдавались последовательно и обрабатывались по порядку, один за другим (SELECT, затем INSERT, затем UPDATE и т. д.). Мы иллюстрируем это поведение на рисунке 6-1.

Рисунок 6-1. Серийное выполнение операторов SQL

Однако реальность (к счастью!) такова, что MySQL может обрабатывать тысячи запросов в секунду и обрабатывать их параллельно, а не последовательно. В этой главе обсуждается, что делает MySQL для достижения такого параллелизма, например, когда запросы на SELECT и UPDATE в одной и той же строке поступают одновременно или когда один поступает, а другой все еще выполняется. На рис. 6-2 показано, как это выглядит.

Рисунок 6-2. Параллельное выполнение операторов SQL

В этой главе нас особенно интересует, как MySQL изолирует (isolates) транзакции (I в ACID). Мы покажем вам распространенные ситуации, когда происходит блокировка, исследуем их и обсудим параметры MySQL, которые определяют, сколько времени транзакция может ждать предоставления блокировки.

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

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

Стандарт SQL:1992 определяет четыре классических уровня изоляции, и MySQL поддерживает их все. InnoDB поддерживает каждый из описанных здесь уровней изоляции транзакций, используя разные стратегии блокировки. Пользователь также может изменить уровень изоляции для одного сеанса или всех последующих соединений с помощью инструкции SET [GLOBAL/SESSION] TRANSACTION.

Мы можем обеспечить высокую степень согласованности с уровнем изоляции REPEATABLE READ по умолчанию для операций с данными, где важно соответствие ACID, и мы можем ослабить правила согласованности с помощью изоляции READ COMMITTED или даже READ UNCOMMITTED в таких ситуациях, как массовые отчеты, где требуется точная согласованность и воспроизводимые результаты менее важны, чем минимизация накладных расходов на блокировку. Изоляция SERIALIZABLE применяет еще более строгие правила, чем REPEATABLE READ, и используется в основном для особых ситуаций, таких как устранение неполадок. Прежде чем углубиться в детали, давайте взглянем еще на некоторые термины:

Dirty reads (Грязное чтение)

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

Non-repeatable reads (Неповторяющееся чтение)

Это происходит, когда два запроса в транзакции выполняют SELECT, и возвращаемые значения различаются между чтениями из-за изменений, сделанных другой транзакцией в промежутке (если вы читаете строку в момент времени T1, а затем пытаетесь прочитать ее снова в момент времени T2, строка могла быть обновлена). Отличие от грязного чтения в том, что в этом случае есть COMMIT. Первоначальный запрос SELECT нельзя повторить, поскольку он возвращает другие значения при повторном выполнении.

Phantom reads (Фантомное чтение)

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

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

REPEATABLE READ

REPEATABLE READ — это уровень изоляции по умолчанию для InnoDB. Он обеспечивает согласованное чтение в рамках одной и той же транзакции, то есть все запросы в рамках транзакции будут видеть один и тот же моментальный снимок данных, созданный при первом чтении. В этом режиме InnoDB блокирует отсканированный диапазон индексов, используя блокировки промежутков или блокировки следующего ключа (описанные в разделе «Блокировка»), чтобы блокировать вставки другими сеансами в любые промежутки в этом диапазоне.

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

session1> SELECT * FROM person WHERE i BETWEEN 1 AND 4;
+---+----------+
| i | name     |
+---+----------+
| 1 | Vinicius |
| 2 | Sergey   |
| 3 | Iwo      |
| 4 | Peter    |
+---+----------+
4 rows in set (0.00 sec)

И в другом сеансе (сеанс 2) мы обновляем имя во второй строке:

session2> UPDATE person SET name = 'Kuzmichev' WHERE i=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
session2 > COMMIT;
Query OK, 0 rows affected (0.00 sec)

Мы можем подтвердить изменение в сеансе 2:

session2> SELECT * FROM person WHERE i BETWEEN 1 AND 4;
+---+-----------+
| i | name      |
+---+-----------+
| 1 | Vinicius  |
| 2 | Kuzmichev |
| 3 | Iwo       |
| 4 | Peter     |
+---+-----------+
4 rows in set (0.00 sec)

Но сеанс 1 по-прежнему показывает старое значение из исходного снимка данных:

session1> SELECT * FROM person WHERE i BETWEEN 1 AND 4;
+---+----------+
| i | name     |
+---+----------+
| 1 | Vinicius |
| 2 | Sergey   |
| 3 | Iwo      |
| 4 | Peter    |
+---+----------+

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

READ COMMITTED

Любопытно, что уровень изоляции READ COMMITTED используется по умолчанию для многих баз данных, таких как Postgres, Oracle и SQL Server, но не для MySQL. Таким образом, те, кто переходит на MySQL, должны знать об этой разнице в поведении по умолчанию.

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

session1> SELECT * FROM person WHERE i = 1;
+---+----------+
| i | name     |
+---+----------+
| 1 | Vinicius |
+---+----------+
1 row in set (0.00 sec)

Теперь предположим, что в сеансе 2 мы обновляем первую строку таблицы person и фиксируем транзакцию:

session2> UPDATE person SET name = 'Grippa' WHERE i = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
session2 > COMMIT;
Query OK, 0 rows affected (0.00 sec)

Если мы снова проверим сеанс 1, мы увидим, что значение первой строки изменилось:

session1> SELECT * FROM person WHERE i = 1;
+---+--------+
| i | name   |
+---+--------+
| 1 | Grippa |
+---+--------+

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

READ UNCOMMITTED

С уровнем изоляции READ UNCOMMITTED MySQL выполняет операторы SELECT неблокирующим образом, что означает, что два оператора SELECT в одной и той же транзакции могут не читать одну и ту же версию строки. Как мы видели ранее, это явление называется грязным чтением. Подумайте, как предыдущий пример будет воспроизводиться с использованием READ UNCOMMITTED. Основное отличие состоит в том, что сеанс 1 может видеть результаты обновления сеанса 2 до фиксации. Давайте рассмотрим еще один пример. Предположим, что в сеансе 1 мы выполняем следующий оператор SELECT:

session1> SELECT * FROM person WHERE i = 5;
+---+---------+
| i | name    |
+---+---------+
| 5 | Marcelo |
+---+---------+
1 row in set (0.00 sec)

И в сеансе 2 мы выполняем это обновление без фиксации:

session2> UPDATE person SET name = 'Altmann' WHERE i = 5;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

Если теперь мы снова выполним SELECT в сеансе 1, вот что мы увидим:

session1> SELECT * FROM person WHERE i = 5;
+---+---------+
| i | name    |
+---+---------+
| 5 | Altmann |
+---+---------+
1 row in set (0.00 sec)

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

SERIALIZABLE

Самый ограниченный уровень изоляции, доступный в MySQL, — SERIALIZABLE. Он похож на REPEATABLE READ, но имеет дополнительное ограничение, заключающееся в том, что одна транзакция не может мешать другой. Таким образом, с этим механизмом блокировки сценарий противоречивых данных больше невозможен.

Чтобы было понятнее, представьте финансовую базу данных, в которой мы регистрируем остатки на счетах клиентов в таблице accounts. Что произойдет, если две транзакции попытаются одновременно обновить баланс счета клиента? Следующий пример иллюстрирует этот сценарий. Предположим, что мы запустили два сеанса, используя уровень изоляции по умолчанию, REPEATABLE READ, и явно открыли транзакцию в каждом с помощью BEGIN. В сеансе 1 мы выбираем все учетные записи в таблице accounts:

session1> SELECT * FROM accounts;
+----+--------+---------+----------+---------------------+
| id | owner  | balance | currency | created_at          |
+----+--------+---------+----------+---------------------+
|  1 | Vinnie |      80 | USD      | 2021-07-13 20:39:27 |
|  2 | Sergey |     100 | USD      | 2021-07-13 20:39:32 |
|  3 | Markus |     100 | USD      | 2021-07-13 20:39:39 |
+----+--------+---------+----------+---------------------+
3 rows in set (0.00 sec)

Затем в сеансе 2 мы выбираем все счета с балансом не менее 80 долларов США:

session2> SELECT * FROM accounts WHERE balance >= 80;
+----+--------+---------+----------+---------------------+
| id | owner  | balance | currency | created_at          |
+----+--------+---------+----------+---------------------+
|  1 | Vinnie |      80 | USD      | 2021-07-13 20:39:27 |
|  2 | Sergey |     100 | USD      | 2021-07-13 20:39:32 |
|  3 | Markus |     100 | USD      | 2021-07-13 20:39:39 |
+----+--------+---------+----------+---------------------+
3 rows in set (0.00 sec)

Теперь в сессии 1 вычитаем 10 USD со счета 1 и проверяем результат:

session1> UPDATE accounts SET balance = balance - 10 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1> SELECT * FROM accounts;
+----+--------+---------+----------+---------------------+
| id | owner  | balance | currency | created_at          |
+----+--------+---------+----------+---------------------+
|  1 | Vinnie |      70 | USD      | 2021-07-13 20:39:27 |
|  2 | Sergey |     100 | USD      | 2021-07-13 20:39:32 |
|  3 | Markus |     100 | USD      | 2021-07-13 20:39:39 |
+----+--------+---------+----------+---------------------+
3 rows in set (0.00 sec)

Мы видим, что баланс счета 1 уменьшился до 70 долларов США. Итак, мы фиксируем сеанс 1, а затем переходим к сеансу 2, чтобы посмотреть, сможет ли он прочитать новые изменения, сделанные сеансом 1:

session1> COMMIT;
Query OK, 0 rows affected (0.01 sec)
session2> SELECT * FROM accounts WHERE id = 1;
+----+--------+---------+----------+---------------------+
| id | owner  | balance | currency | created_at          |
+----+--------+---------+----------+---------------------+
|  1 | Vinnie |      80 | USD      | 2021-07-13 20:39:27 |
+----+--------+---------+----------+---------------------+
1 row in set (0.01 sec)

Этот запрос SELECT по-прежнему возвращает старые данные для учетной записи 1 с балансом 80 долларов США, даже несмотря на то, что транзакция 1 изменила его на 70 долларов США и была успешно зафиксирована. Это связано с тем, что уровень изоляции REPEATABLE READ гарантирует, что все запросы на чтение в транзакции являются повторяемыми, что означает, что они всегда возвращают один и тот же результат, даже если изменения были внесены другими зафиксированными транзакциями.

Но что произойдет, если мы также запустим запрос UPDATE, чтобы вычесть 10 долларов США из баланса счета 1 во время сеанса 2? Изменится ли баланс на 70 USD или 60 USD или выдаст ошибку? Давайте посмотрим:

session2> UPDATE accounts SET balance = balance - 10 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
session2> SELECT * FROM accounts WHERE id = 1;
+----+--------+---------+----------+---------------------+
| id | owner  | balance | currency | created_at          |
+----+--------+---------+----------+---------------------+
|  1 | Vinnie |      60 | USD      | 2021-07-13 20:39:27 |
+----+--------+---------+----------+---------------------+
1 row in set (0.01 sec)

Ошибки нет, и остаток на счете теперь составляет 60 долларов США, что является правильным значением, поскольку транзакция 1 уже зафиксировала изменение, изменившее баланс на 70 долларов США.

Однако с точки зрения транзакции 2 это не имеет смысла: в последнем SELECT-запросе она видела баланс 80 USD, а после вычитания 10 USD со счета теперь видит остаток 60 USD. Математика здесь не работает, потому что на эту транзакцию все еще влияют параллельные обновления от других транзакций.

Это сценарий, в котором может помочь использование SERIALIZABLE. Давайте вернемся к моменту до внесения каких-либо изменений. На этот раз мы явно установим уровень изоляции обоих сеансов в SERIALIZABLE с помощью SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE перед запуском транзакций с помощью BEGIN. Опять же, в сеансе 1 мы выбираем все учетные записи:

session1> SELECT * FROM accounts;
+----+--------+---------+----------+---------------------+
| id | owner  | balance | currency | created_at          |
+----+--------+---------+----------+---------------------+
|  1 | Vinnie |      80 | USD      | 2021-07-13 20:39:27 |
|  2 | Sergey |     100 | USD      | 2021-07-13 20:39:32 |
|  3 | Markus |     100 | USD      | 2021-07-13 20:39:39 |
+----+--------+---------+----------+---------------------+
3 rows in set (0.00 sec)

И в сеансе 2 мы выбираем все счета с балансом больше 80 долларов США:

session2> SELECT * FROM accounts WHERE balance >= 80;
+----+--------+---------+----------+---------------------+
| id | owner  | balance | currency | created_at          |
+----+--------+---------+----------+---------------------+
|  1 | Vinnie |      80 | USD      | 2021-07-13 20:39:27 |
|  2 | Sergey |     100 | USD      | 2021-07-13 20:39:32 |
|  3 | Markus |     100 | USD      | 2021-07-13 20:39:39 |
+----+--------+---------+----------+---------------------+
3 rows in set (0.00 sec)

Теперь в сеансе 1 мы вычитаем 10 долларов США со счета 1:

session1> UPDATE accounts SET balance = balance - 10 WHERE id = 1;

И… ничего не происходит. На этот раз запрос UPDATE заблокирован — запрос SELECT в сеансе 1 заблокировал эти строки и препятствует успешному выполнению UPDATE в сеансе 2. Поскольку мы явно начали наши транзакции с BEGIN (что имеет тот же эффект, что и отключение автоматической фиксации), InnoDB неявно преобразует все простые операторы SELECT в каждой транзакции в SELECT ... FOR SHARE. Он не знает заранее, будет ли транзакция выполнять только чтение или будет изменять строки, поэтому InnoDB необходимо заблокировать ее, чтобы избежать проблемы, которую мы продемонстрировали в предыдущем примере. В этом примере, если бы автофиксация была включена, запрос SELECT в сеансе 2 не блокировал бы обновление, которое мы пытаемся выполнить в сеансе 1: MySQL распознал бы, что запрос является простым SELECT и не должен блокировать другие запросы, поскольку он не собирается изменять строки.

Однако обновление в сеансе 2 не будет зависать навсегда; эта блокировка имеет время ожидания, которое контролируется параметром innodb_lock_wait_timeout. Таким образом, если сеанс 1 не фиксирует или не откатывает свою транзакцию для снятия блокировки, по истечении времени ожидания сеанса MySQL выдает следующую ошибку:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

Блокировка

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

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

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

Если существует активная транзакция (выполняется, незафиксирована или откатывается), когда сеанс выполняет одну из операций, упомянутых в следующем списке, сеанс, запрашивающий запись данных, будет удерживаться в состоянии Waiting for table metadata lock. Ожидание блокировки метаданных может произойти в любом из следующих сценариев:

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

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

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

Блокировки метаданных

Документация MySQL дает лучшее определение блокировок метаданных:

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

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

USE test;

DROP TABLE IF EXISTS `joinit`;

CREATE TABLE `joinit` (
  `i` int(11) NOT NULL AUTO_INCREMENT,
  `s` varchar(64) DEFAULT NULL,
  `t` time NOT NULL,
  `g` int(11) NOT NULL,
  PRIMARY KEY (`i`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1;

INSERT INTO joinit VALUES (NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )));
INSERT INTO joinit SELECT NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )) FROM joinit;
INSERT INTO joinit SELECT NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )) FROM joinit;
INSERT INTO joinit SELECT NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )) FROM joinit;
INSERT INTO joinit SELECT NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )) FROM joinit;
INSERT INTO joinit SELECT NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )) FROM joinit;
INSERT INTO joinit SELECT NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )) FROM joinit;
INSERT INTO joinit SELECT NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )) FROM joinit;
INSERT INTO joinit SELECT NULL, uuid(), time(now()),  (FLOOR( 1 + RAND( ) *60 )) FROM joinit;

Теперь, когда у нас есть фиктивные данные, мы откроем один сеанс (сеанс 1) и выполним UPDATE:

session1> UPDATE joinit SET t=now();

Затем во втором сеансе мы попытаемся добавить новый столбец в эту таблицу, пока выполняется UPDATE:

session2> ALTER TABLE joinit ADD COLUMN b INT;

И в третьем сеансе мы можем выполнить команду SHOW PROCESSLIST, чтобы визуализировать блокировку метаданных:

session3> SHOW PROCESSLIST;
+----+----------+-----------+------+---------+------+...
| Id | User     | Host      | db   | Command | Time |...
+----+----------+-----------+------+---------+------+...
| 10 | msandbox | localhost | test | Query   |    3 |...
| 11 | msandbox | localhost | test | Query   |    1 |...
| 12 | msandbox | localhost | NULL | Query   |    0 |...
+----+----------+-----------+------+---------+------+...
...+---------------------------------+-------------------------------------+...
...| State                           | Info                                |...
...+---------------------------------+-------------------------------------+...
...| updating                        | UPDATE joinit SET t=now()           |...
...| Waiting for table metadata lock | ALTER TABLE joinit ADD COLUMN b INT |...
...| starting                        | SHOW PROCESSLIST                    |...
...+---------------------------------+-------------------------------------+...
...+-----------+---------------+
...| Rows_sent | Rows_examined |
...+-----------+---------------+
...|         0 |        179987 |
...|         0 |             0 |
...|         0 |             0 |
...+-----------+---------------+

Обратите внимание, что длительный запрос или запрос, не использующий автофиксацию, будет иметь тот же эффект. Например, предположим, что у нас есть UPDATE, работающий в сеансе 1:

mysql> SET SESSION autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE joinit SET t=NOW() LIMIT 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

И мы выполняем оператор DML в сеансе 2:

mysql> ALTER TABLE joinit ADD COLUMN b INT;

Если мы проверим список процессов в сеансе 3, то увидим, что DDL ожидает блокировки метаданных (поток 11), в то время как поток 10 бездействует с момента выполнения UPDATE (все еще не зафиксирован):

mysql> SHOW PROCESSLIST;

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

mysql> UPDATE performance_schema.setup_instruments SET enabled = 'YES'
    -> WHERE NAME = 'wait/lock/metadata/sql/mdl';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1 Changed: 0 Warnings: 0

Следующий запрос использует представление schema_table_lock_waits из схемы sys, чтобы проиллюстрировать, как отслеживать блокировки метаданных в базе данных MySQL:

mysql> SELECT * FROM sys.schema_table_lock_waits;

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

mysql> SELECT object_name, waiting_thread_id, waiting_lock_type,
    -> waiting_query, sql_kill_blocking_query, blocking_thread_id
    -> FROM sys.schema_table_lock_waits;
+-------------+-------------------+-------------------+...
| object_name | waiting_thread_id | waiting_lock_type |...
+-------------+-------------------+-------------------+...
| joinit      |                29 | EXCLUSIVE         |...
| joinit      |                29 | EXCLUSIVE         |...
+-------------+-------------------+-------------------+...
...+-------------------------------------------------------------------+...
...| waiting_query                                                     |...
...+-------------------------------------------------------------------+...
...| ALTER TABLE joinit ADD COLUMN  ...  CHAR(32) DEFAULT 'dummy_text' |...
...| ALTER TABLE joinit ADD COLUMN  ...  CHAR(32) DEFAULT 'dummy_text' |...
...|-------------------------------------------------------------------+...
...+-------------------------+--------------------+
...| sql_kill_blocking_query | blocking_thread_id |
...+-------------------------+--------------------+
...| KILL QUERY 3            |                 29 |
...| KILL QUERY 5            |                 31 |
...+-------------------------+--------------------+
2 rows in set (0.00 sec)

Давайте посмотрим, что происходит, когда мы запрашиваем таблицу metadata_locks:

mysql> SELECT * FROM performance_schema.metadata_locks\G
*************************** 1. row ***************************
          OBJECT_TYPE: GLOBAL
        OBJECT_SCHEMA: NULL
          OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140089691017472
            LOCK_TYPE: INTENTION_EXCLUSIVE
        LOCK_DURATION: STATEMENT
          LOCK_STATUS: GRANTED
               SOURCE:
      OWNER_THREAD_ID: 97
       OWNER_EVENT_ID: 34
...
*************************** 6. row ***************************
          OBJECT_TYPE: TABLE
        OBJECT_SCHEMA: performance_schema
          OBJECT_NAME: metadata_locks
OBJECT_INSTANCE_BEGIN: 140089640911984
            LOCK_TYPE: SHARED_READ
        LOCK_DURATION: TRANSACTION
          LOCK_STATUS: GRANTED
               SOURCE:
      OWNER_THREAD_ID: 98
       OWNER_EVENT_ID: 10
6 rows in set (0.00 sec)

Обратите внимание, что для объединенной таблицы установлена блокировка SHARED_UPGRADABLE, а для той же таблицы ожидается блокировка EXCLUSIVE.

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

mysql> SELECT object_type, object_schema, object_name, lock_type,
    -> lock_status, thread_id, processlist_id, processlist_info FROM
    -> performance_schema.metadata_locks INNER JOIN performance_schema.threads
    -> ON thread_id = owner_thread_id WHERE processlist_id <> connection_id();
+-------------+---------------+-------------+---------------------+...
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE           |...
+-------------+---------------+-------------+---------------------+...
| GLOBAL      | NULL          | NULL        | INTENTION_EXCLUSIVE |...
| SCHEMA      | test          | NULL        | INTENTION_EXCLUSIVE |...
| TABLE       | test          | joinit      | SHARED_UPGRADABLE   |...
| BACKUP      | NULL          | NULL        | INTENTION_EXCLUSIVE |...
| TABLE       | test          | joinit      | EXCLUSIVE           |...
+-------------+---------------+-------------+---------------------+...
...+-------------+-----------+----------------+...
...| LOCK_STATUS | THREAD_ID | PROCESSLIST_ID |...
...+-------------+-----------+----------------+...
...| GRANTED     |        97 |             71 |...
...| GRANTED     |        97 |             71 |...
...| GRANTED     |        97 |             71 |...
...| GRANTED     |        97 |             71 |...
...| PENDING     |        97 |             71 |...
...+-------------+-----------+----------------+...
...+-------------------------------------+
...| PROCESSLIST_INFO                    |
...+-------------------------------------+
...| alter table joinit add column b int |
...| alter table joinit add column b int |
...| alter table joinit add column b int |
...| alter table joinit add column b int |
...| alter table joinit add column b int |
...+-------------------------------------+
5 rows in set (0.00 sec)

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

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

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

$ pt-kill --busy-time 60 --kill

Блокировка строк

InnoDB реализует стандартную блокировку на уровне строк. Это означает, что в общих чертах существует два типа блокировок:

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

InnoDB также поддерживает множественную блокировку детализации, что позволяет сосуществовать блокировкам строк и блокировкам таблиц. Детализированная блокировка возможна из-за существования блокировок по намерению (intention locks), которые являются блокировками на уровне таблицы и указывают, какой тип блокировки (общая или монопольная) транзакция потребует позже для строки в таблице. Есть два типа блокировки намерения:

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

Чтобы немного упростить понимание, взгляните на Таблицу 6-1.

Таблица 6-1. Матрица совместимости типов блокировок
X IX S IS
X Конфликт Конфликт Конфликт Конфликт
IX Конфликт Совместимы Конфликт Совместимы
S Конфликт Конфликт Совместимы Совместимы
IS Конфликт Совместимы Совместимы Совместимы

Другой важной концепцией является блокировка промежутка (gap lock), которая представляет собой блокировку промежутка между записями индекса. Блокировки промежутка гарантируют, что новые строки не будут добавлены в интервале, заданном запросом; это означает, что когда вы запускаете один и тот же запрос дважды, вы получаете одинаковое количество строк, независимо от модификаций этой таблицы другими сеансами. Они делают чтения согласованными и, следовательно, обеспечивают согласованность репликации между серверами. Если вы выполните SELECT * FROM example_table WHERE id > 1000 FOR UPDATE дважды, вы ожидаете дважды получить один и тот же результат. Для этого InnoDB блокирует все записи индекса, найденные предложением WHERE, монопольной блокировкой, а промежутки между ними — общей блокировкой промежутка.

Давайте посмотрим на пример блокировку промежутка в действии. Сначала мы выполним оператор SELECT для таблицы person:

mysql> SELECT * FROM PERSON;
+----+-----------+
| i  | name      |
+----+-----------+
|  1 | Vinicius  |
|  2 | Kuzmichev |
|  3 | Iwo       |
|  4 | Peter     |
|  5 | Marcelo   |
|  6 | Guli      |
|  7 | Nando     |
| 10 | Jobin     |
| 15 | Rafa      |
| 18 | Leo       |
+----+-----------+
10 rows in set (0.00 sec)

Теперь в сессии 1 мы выполним операцию удаления, но не будем фиксировать:

session1> DELETE FROM person WHERE name LIKE 'Jobin';
Query OK, 1 row affected (0.00 sec)

И если мы проверим сеанс 2, мы все еще можем увидеть строку с Jobin:

session2> SELECT * FROM person;
+----+-----------+
| i  | name      |
+----+-----------+
|  1 | Vinicius  |
|  2 | Kuzmichev |
|  3 | Iwo       |
|  4 | Peter     |
|  5 | Marcelo   |
|  6 | Guli      |
|  7 | Nando     |
| 10 | Jobin     |
| 15 | Rafa      |
| 18 | Leo       |
+----+-----------+
10 rows in set (0.00 sec)

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

transaction2 > INSERT INTO person VALUES (11, 'Bennie');
ERROR 1205 (HY000): Lockwait timeout exceeded; try restarting transaction

Если мы запустим SHOW ENGINE INNODB STATUS, мы увидим заблокированную транзакцию в разделе TRANSACTIONS:

------- TRX HAS BEEN WAITING 17 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 28 page no 3 n bits 80 index PRIMARY of table `test`.`person` trx id 4773 lock_mode X locks gap before rec insert intention waiting

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

mysql> CREATE UNIQUE INDEX idx ON PERSON (name);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> DELETE FROM person WHERE name LIKE 'Jobin';
Query OK, 1 row affected (0.00 sec)

Взаимная блокировка (deadlock)

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

Для возникновения взаимоблокировки должны существовать четыре условия (известные как условия Коффмана (Coffman conditions)):

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

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

  3. Нет преимущественного права. Ресурс может быть освобожден только добровольно удерживающим его процессом.

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

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

Уровни изоляции транзакций ответственны за взаимоблокировки.

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

Небольшие транзакции не подвержены взаимным блокировкам.

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

Взаимные блокировки — ужасная вещь.

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

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

Начнем с получения списка итальянских городов в провинции Тоскана:

mysql> SELECT * FROM city WHERE CountryCode = 'ITA' AND District='Toscana';
+------+---------+-------------+----------+------------+
| ID   | Name    | CountryCode | District | Population |
+------+---------+-------------+----------+------------+
| 1471 | Firenze | ITA         | Toscana  |     376662 |
| 1483 | Prato   | ITA         | Toscana  |     172473 |
| 1486 | Livorno | ITA         | Toscana  |     161673 |
| 1516 | Pisa    | ITA         | Toscana  |      92379 |
| 1518 | Arezzo  | ITA         | Toscana  |      91729 |
+------+---------+-------------+----------+------------+
5 rows in set (0.00 sec)

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

session1> UPDATE city SET Population=Population + 1 WHERE ID = 1471;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
session2> UPDATE city SET Population=Population + 1 WHERE ID =1516;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
session1> UPDATE city SET Population=Population + 1 WHERE ID =1516;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
session2> UPDATE city SET Population=Population + 1 WHERE ID = 1471;
Query OK, 1 row affected (5.15 sec)
Rows matched: 1 Changed: 1 Warnings: 0

И у нас была взаимоблокировка в сеансе 1. Важно отметить, что не всегда вторая транзакция завершается ошибкой. В этом примере сеанс 1 прервал MySQL. Мы можем получить информацию о последней взаимоблокировке, которая произошла в базе данных MySQL, запустив SHOW ENGINE INNODB STATUS:

mysql> SHOW ENGINE INNODB STATUS\G
------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-12-05 16:08:19 0x7f6949359700
*** (1) TRANSACTION:
TRANSACTION 10502342, ACTIVE 34 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log
entries 1
MySQL thread id 71, OS thread handle 140090386671360, query id 5979282
localhost msandbox updating
update city set Population=Population + 1 where ID = 1471
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 6041 page no 15 n bits 248 index PRIMARY of table
`world`.`city` trx id 10502342 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 10502341, ACTIVE 62 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 75, OS thread handle 140090176542464, query id 5979283
localhost msandbox updating
update city set Population=Population + 1 where ID =1516
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 6041 page no 15 n bits 248 index PRIMARY of table
`world`.`city` trx id 10502341 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 6041 page no 16 n bits 248 index PRIMARY of table
`world`.`city` trx id 10502341 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (2)
...

Если вы хотите, вы можете регистрировать все взаимоблокировки, которые происходят в MySQL, в журнале ошибок MySQL. Используя параметр innodb_print_all_deadlocks, MySQL записывает всю информацию о взаимоблокировках из пользовательских транзакций InnoDB в журнал ошибок. В противном случае вы увидите информацию только о последней взаимоблокировке с помощью команды SHOW ENGINE INNODB STATUS.

Параметры MySQL, относящиеся к изоляции и блокировкам

Чтобы завершить эту главу, давайте взглянем на несколько параметров MySQL, которые связаны с поведением изоляции и длительностью блокировки:

transaction_isolation

Устанавливает уровень изоляции транзакции. Этот параметр может изменить поведение на уровне GLOBAL, SESSION или NEXT_TRANSACTION:

mysql> SET SESSION transaction_isolation='READ-COMMITTED';
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW SESSION VARIABLES LIKE '%isol%';
+-----------------------+----------------+
| Variable_name         | Value          |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
| tx_isolation          | READ-COMMITTED |
+-----------------------+----------------+
innodb_lock_wait_timeout

Указывает количество времени в секундах, в течение которого транзакция InnoDB ожидает блокировки строки, прежде чем сдаться. Значение по умолчанию — 50 секунд. Транзакция вызывает следующую ошибку, если время ожидания блокировки превышает значение innodb_lock_wait_timeout:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
innodb_print_all_deadlocks

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

mysql> SET GLOBAL innodb_print_all_deadlocks = 1;
lock_wait_timeout

Задает время ожидания в секундах для попыток получения блокировки метаданных. Чтобы избежать зависания базы данных из-за длинных блокировок метаданных, мы можем установить lock_wait_timeout=1 на уровне сеанса перед выполнением оператора DDL. В этом случае, если операция не может получить блокировку, она откажется и позволит выполнять другие запросы. Например:

mysql> SET SESSION lock_wait_timeout=1;
mysql> CREATE TABLE t1(i INT NOT NULL AUTO_INCREMENT PRIMARY KEY)
    -> ENGINE=InnoDB;
innodb_deadlock_detect

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

Глава 7
Дополнительные возможности MySQL

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

В этой главе вы узнаете, как:

Вставка данных с помощью запросов

Большую часть времени вы будете создавать таблицы, используя данные из другого источника. Таким образом, примеры, которые вы видели в главе 3, иллюстрируют только часть проблемы: они показывают, как вставлять данные, которые уже находятся в нужной вам форме (то есть отформатированы как инструкция SQL INSERT). Другие способы вставки данных включают использование операторов SQL SELECT в других таблицах или базах данных и чтение файлов из других источников. В этом разделе показано, как использовать прежний метод вставки данных; вы узнаете, как вставлять данные из файла значений, разделенных запятыми, в следующем разделе «Загрузка данных из файлов, разделенных запятыми».

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

mysql> CREATE TABLE recommend
    ->    film_id SMALLINT UNSIGNED,
    ->    language_id TINYINT UNSIGNED,
    ->    release_year YEAR,
    ->    title VARCHAR(128),
    ->    length SMALLINT UNSIGNED,
    ->    sequence_id SMALLINT AUTO_INCREMENT,
    ->    PRIMARY KEY (sequence_id)
    -> );
Query OK, 0 rows affected (0.05 sec)

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

Теперь нам нужно заполнить нашу новую таблицу рекомендаций случайным выбором фильмов. Важно отметить, что мы собираемся выполнять SELECT и INSERT вместе в одном операторе. Вот так:

mysql> INSERT INTO recommend (film_id, language_id, release_year, title, length)
    -> SELECT film_id, language_id, release_year, title, length
    -> FROM film ORDER BY RAND() LIMIT 10;
Query OK, 10 rows affected (0.02 sec)
Records: 10  Duplicates: 0  Warnings: 0

Теперь давайте разберемся, что произошло, прежде чем объяснять, как работает эта команда:

mysql> SELECT * FROM recommend;
+---------+-----+--------------------+--------+-------------+
| film_id | ... | title              | length | sequence_id |
+---------+-----+--------------------+--------+-------------+
|     542 | ... | LUST LOCK          |     52 |           1 |
|     661 | ... | PAST SUICIDES      |    157 |           2 |
|     613 | ... | MYSTIC TRUMAN      |     92 |           3 |
|     757 | ... | SAGEBRUSH CLUELESS |    106 |           4 |
|     940 | ... | VICTORY ACADEMY    |     64 |           5 |
|     917 | ... | TUXEDO MILE        |    152 |           6 |
|     709 | ... | RACER EGG          |    147 |           7 |
|     524 | ... | LION UNCUT         |     50 |           8 |
|      30 | ... | ANYTHING SAVANNAH  |     82 |           9 |
|     602 | ... | MOURNING PURPLE    |    146 |          10 |
+---------+-----+--------------------+--------+-------------+
10 rows in set (0.00 sec)

Как видите, в нашем списке рекомендаций 10 фильмов, пронумерованных значениями sequence_id от 1 до 10. Мы готовы начать рекомендовать случайный выбор фильмов. Не волнуйтесь, если ваши результаты отличаются; это следствие того, как работает функция RAND().

Оператор SQL, который мы использовали для заполнения таблицы, состоит из двух частей: INSERT INTO и SELECT. Оператор INSERT INTO перечисляет целевую таблицу, в которой будут храниться данные, за которой следует необязательный список имен столбцов в круглых скобках; если вы опустите имена столбцов, предполагается, что все столбцы в целевой таблице будут отображаться в том порядке, в котором они появляются в выходных данных оператора DESCRIBE TABLE или SHOW CREATE TABLE. Оператор SELECT выводит столбцы, которые должны соответствовать типу и порядку списка, предоставленного для оператора INSERT INTO (или неявного полного списка, если он не указан). Общий эффект заключается в том, что строки, выводимые оператором SELECT, вставляются в целевую таблицу с помощью оператора INSERT INTO. В нашем примере, film_id, language_id, release_year, title, и значения length из таблицы film вставляются в пять столбцов с теми же именами и типами в таблице recommend; sequence_id автоматически создается с помощью функции MySQL AUTO_INCREMENT, поэтому он не указывается в операторах.

Наш пример включает предложение ORDER BY RAND(); оно упорядочивает результаты в соответствии с функцией MySQL RAND(). Функция RAND() возвращает псевдослучайное число в диапазоне от 0 до 1:

mysql> SELECT RAND();
+--------------------+
| RAND()             |
+--------------------+
| 0.4593397513584604 |
+--------------------+
1 row in set (0.00 sec)

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

Если вы запросите значение RAND() в операции SELECT, вы получите случайное значение для каждой возвращаемой строки:

mysql> SELECT title, RAND() FROM film LIMIT 5;
+------------------+---------------------+
| title            | RAND()              |
+------------------+---------------------+
| ACADEMY DINOSAUR |  0.5514843506286706 |
| ACE GOLDFINGER   | 0.37940252980161693 |
| ADAPTATION HOLES |  0.2425596278557178 |
| AFFAIR PREJUDICE | 0.07459058060738312 |
| AFRICAN EGG      |  0.6452740502034072 |
+------------------+---------------------+
5 rows in set (0.00 sec)

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

SELECT title, RAND(1) FROM film LIMIT 5;

Вернемся к операции INSERT. Когда мы просим упорядочить результаты с помощью RAND(), результаты оператора SELECT сортируются в псевдослучайном порядке. LIMIT 10 предназначен для ограничения количества строк, возвращаемых SELECT; мы ограничили в этом примере просто для удобства чтения.

Оператор SELECT в операторе INSERT INTO может использовать все обычные функции операторов SELECT. Вы можете использовать объединения, агрегацию, функции и любые другие возможности по вашему выбору. Вы также можете запрашивать данные из одной базы данных в другой, указав перед именами таблиц имя базы данных, за которым следует символ точки (.). Например, если вы хотите вставить таблицу actor из базы данных film в новую базу данных произведений искусства, вы можете сделать следующее:

mysql> CREATE DATABASE art;
Query OK, 1 row affected (0.01 sec)
mysql> USE art;
Database changed
mysql> CREATE TABLE people
    ->   person_id SMALLINT UNSIGNED,
    ->   first_name VARCHAR(45),
    ->   last_name VARCHAR(45),
    ->   PRIMARY KEY (person_id)
    -> );
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO art.people (person_id, first_name, last_name)
    -> SELECT actor_id, first_name, last_name FROM sakila.actor;
Query OK, 200 rows affected (0.01 sec)
Records: 200  Duplicates: 0  Warnings: 0

Вы можете видеть, что новая таблица people называется art.people (хотя это и не обязательно, так как art — это база данных, которая в настоящее время используется), а таблица actor называется sakila.actor (как и должно быть, поскольку это база данных не используется). Также обратите внимание, что имена столбцов не обязательно должны совпадать для SELECT и INSERT.

Иногда вы сталкиваетесь с проблемами дублирования при вставке с помощью оператора SELECT. Если вы попытаетесь вставить одно и то же значение первичного ключа дважды, работа MySQL прервется. Этого не произойдет в таблице recommend, если вы автоматически выделяете новый sequence_id с помощью функции AUTO_INCREMENT. Однако мы можем принудительно добавить дубликат в таблицу, чтобы показать поведение:

mysql> USE sakila;
Database changed
mysql> INSERT INTO recommend (film_id, language_id, release_year,
    -> title, length, sequence_id )
    -> SELECT film_id, language_id, release_year, title, length, 1
    -> FROM film LIMIT 1;
ERROR 1062 (23000): Duplicate entry '1' for key 'recommend.PRIMARY'

Если вы хотите, чтобы MySQL игнорировал это и продолжал работать, добавьте ключевое слово IGNORE после INSERT:

mysql> INSERT IGNORE INTO recommend (film_id, language_id, release_year,
    -> title, length, sequence_id )
    -> SELECT film_id, language_id, release_year, title, length, 1
    -> FROM film LIMIT 1;
Query OK, 0 rows affected, 1 warning (0.00 sec)
Records: 1  Duplicates: 1  Warnings: 1

MySQL не жалуется, но сообщает, что обнаружил дубликат. Обратите внимание, что данные не изменяются; все, что мы сделали, это проигнорировали ошибку. Это полезно в операциях массовой загрузки, когда вы не хотите, чтобы произошел сбой на полпути к выполнению сценария, вставляющего миллион строк. Теперь мы можем проверить предупреждение, чтобы увидеть ошибку Duplicate entry в качестве предупреждения:

mysql> SHOW WARNINGS;
+---------+------+-------------------------------------------------+
| Level   | Code | Message                                         |
+---------+------+-------------------------------------------------+
| Warning | 1062 | Duplicate entry '1' for key 'recommend.PRIMARY' |
+---------+------+-------------------------------------------------+
1 row in set (0.00 sec)

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

mysql> INSERT INTO actor SELECT
    -> actor_id, first_name, last_name, NOW() FROM actor;
ERROR 1062 (23000): Duplicate entry '1' for key 'actor.PRIMARY'

Есть два способа избежать ошибки. Во-первых, в таблице actor включено AUTO_INCREMENT для actor_id, поэтому, если вы полностью пропустите этот столбец в INSERT, вы не получите ошибку, так как новые значения будут сгенерированы автоматически. (Синтаксис оператора INSERT объясняется в разделе «Альтернативный синтаксис».) Вот пример, в котором будет только одна запись (из-за предложения LIMIT):

INSERT INTO actor(first_name, last_name, last_update)
SELECT first_name, last_name, NOW() FROM actor LIMIT 1;

Второй способ — изменить actor_id в запросе SELECT таким образом, чтобы предотвратить коллизии. Давайте попробуем это:

mysql> INSERT INTO actor SELECT
    -> actor_id+200, first_name, last_name, NOW() FROM actor;
Query OK, 200 rows affected (0.01 sec)
Records: 200 Duplicates: 0 Warnings: 0

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

mysql> SELECT * FROM actor;
+----------+-------------+--------------+---------------------+
| actor_id | first_name  | last_name    | last_update         |
+----------+-------------+--------------+---------------------+
|        1 | PENELOPE    | GUINESS      | 2006-02-15 04:34:33 |
|        2 | NICK        | WAHLBERG     | 2006-02-15 04:34:33 |
|      ...                                                    |
|      198 | MARY        | KEITEL       | 2006-02-15 04:34:33 |
|      199 | JULIA       | FAWCETT      | 2006-02-15 04:34:33 |
|      200 | THORA       | TEMPLE       | 2006-02-15 04:34:33 |
|      201 | PENELOPE    | GUINESS      | 2021-02-28 10:24:49 |
|      202 | NICK        | WAHLBERG     | 2021-02-28 10:24:49 |
|      ...                                                    |
|      398 | MARY        | KEITEL       | 2021-02-28 10:24:49 |
|      399 | JULIA       | FAWCETT      | 2021-02-28 10:24:49 |
|      400 | THORA       | TEMPLE       | 2021-02-28 10:24:49 |
+----------+-------------+--------------+---------------------+
400 rows in set (0.00 sec)

Вы можете видеть, как имена, фамилии и значения last_update начинают повторяться, начиная с actor_id 201.

Также можно использовать подзапросы в операторах INSERT SELECT. Например, верно следующее утверждение:

*INSERT INTO actor SELECT * FROM*
*(SELECT actor_id+400, first_name, last_name, NOW() FROM actor) foo;*

Загрузка данных из файлов с разделителями-запятыми

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

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

Если вы не используете программу для работы с электронными таблицами, вы все равно можете часто использовать инструменты командной строки, такие как sed и awk — очень старые и мощные утилиты Unix — для преобразования текстовых данных в формат CSV, пригодный для импорта MySQL. Некоторые облачные базы данных позволяют экспортировать свои данные напрямую в CSV. В некоторых других случаях приходится писать небольшие программы, которые считывают данные и создают CSV-файл. В этом разделе показаны основы того, как импортировать данные CSV в MySQL.

Давайте поработаем на примере. У нас есть список объектов НАСА с их адресами и контактной информацией, которые мы хотим сохранить в базе данных. В настоящее время он хранится в файле CSV с именем NASA_Facilities.csv и имеет формат, показанный на рис. 7-1.

Рисунок 7-1. Список объектов НАСА, хранящийся в файле электронной таблицы

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

Этот пример взят непосредственно с общедоступного Open Data Portal НАСА, а файл доступен в репозитории книги на GitHub. Поскольку это уже файл CSV, нам не нужно преобразовывать его из другого формата файла (например, XLS). Однако, если вам нужно сделать это в вашем собственном проекте, обычно это так же просто, как использовать команду «Сохранить как» в программе для работы с электронными таблицами; только не забудьте выбрать CSV в качестве выходного формата.

Если вы откроете файл NASA_facilities.csv с помощью текстового редактора, вы увидите, что в нем есть одна строка на строку электронной таблицы со значениями для каждого столбца, разделенными запятыми. Если вы работаете на платформе, отличной от Windows, вы можете обнаружить, что в некоторых CSV-файлах каждая строка заканчивается знаком ^M, но не беспокойтесь об этом; это артефакт происхождения Windows. Данные в этом формате часто называют форматом DOS, и большинство программных приложений могут работать с ним без проблем. В нашем случае данные в формате Unix, поэтому в Windows вы можете увидеть, что все строки объединены. Вы можете попробовать использовать другой текстовый редактор, если это так. Вот несколько усеченных по ширине строк, выбранных из NASA_Facilities.csv:

Center,Center Search Status,Facility,FacilityURL,Occupied,Status,...
Kennedy Space Center,Public,Control Room 2/1726/HGR-S ,,...
Langley Research Center,Public,Micometeroid/LDEF Analysis Laboratory,,...
Kennedy Space Center,Public,SRM Rotation and Processing Facility/K6-0494 ,...
Marshall Space Flight Center,..."35812(34.729538, -86.585283)",Huntsville,...

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

Давайте импортируем эти данные в MySQL. Сначала создайте новую базу данных nasa:

mysql> CREATE DATABASE nasa;
Query OK, 1 row affected (0.01 sec)

Выберите ее в качестве активной базы данных:

mysql> USE nasa;
Database changed

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

mysql> CREATE TABLE facilities (
    ->   center TEXT,
    ->   center_search_status TEXT,
    ->   facility TEXT,
    ->   facility_url TEXT,
    ->   occupied TEXT,
    ->   status TEXT,
    ->   url_link TEXT,
    ->   record_date DATETIME,
    ->   last_update TIMESTAMP NULL,
    ->   country TEXT,
    ->   contact TEXT,
    ->   phone TEXT,
    ->   location TEXT,
    ->   city TEXT,
    ->   state TEXT,
    ->   zipcode TEXT
    -> );
Query OK, 0 rows affected (0.03 sec)

Здесь мы немного жульничаем с типами данных. НАСА предоставляет схему набора данных, но для большинства полей тип задается как «Plain Text», и мы также не можем хранить «Website URL» как что-либо, кроме текста. Однако мы не знаем, сколько данных будет храниться в каждом столбце. Таким образом, мы по умолчанию используем тип TEXT, что похоже на определение столбца как VARCHAR(65535). Между этими двумя типами есть некоторые различия, как вы, наверное, помните из раздела «Строковые типы», но в данном примере они не важны. Мы не определяем никаких индексов и не накладываем никаких ограничений на нашу таблицу. Если вы загружаете совершенно новый набор данных, который довольно мал, может быть полезно сначала загрузить его, а затем проанализировать. Для больших наборов данных убедитесь, что таблица структурирована как можно лучше, иначе вы потратите значительное количество времени на ее изменение позже.

Теперь, когда мы настроили таблицу базы данных, мы можем импортировать данные из файла с помощью команды LOAD DATA INFILE:

mysql> LOAD DATA INFILE 'NASA_Facilities.csv' INTO TABLE facilities
    -> FIELDS TERMINATED BY ',';
ERROR 1290 (HY000): The MySQL server is running with
the --secure-file-priv option so it cannot execute this statement

О, нет! Мы получили ошибку. По умолчанию MySQL не позволяет вам загружать какие-либо данные с помощью команды LOAD DATA INFILE. Поведение управляется системной переменной secure_file_priv. Если для переменной задан путь, загружаемый файл должен находиться по этому конкретному пути и быть доступным для чтения сервером MySQL. Если переменная не установлена, что считается небезопасным, то загружаемый файл должен быть доступен для чтения только серверу MySQL. По умолчанию MySQL 8.0 в Linux устанавливает эту переменную следующим образом:

mysql> SELECT @@secure_file_priv;
+-----------------------+
| @@secure_file_priv    |
+-----------------------+
| /var/lib/mysql-files/ |
+-----------------------+
1 row in set (0.00 sec)

И в Windows:

mysql> SELECT @@secure_file_priv;
+------------------------------------------------+
| @@secure_file_priv                             |
+------------------------------------------------+
| C:\ProgramData\MySQL\MySQL Server 8.0\Uploads\ |
+------------------------------------------------+
1 row in set (0.00 sec)

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

Давай сделаем это. В Linux или аналогичных системах вы можете запускать такие команды:

$ ls -lh $HOME/Downloads/NASA_Facilities.csv
-rw-r--r--. 1 skuzmichev skuzmichev 114K
    Feb 28 14:19 /home/skuzmichev/Downloads/NASA_Facilities.csv
$ sudo cp -vip ~/Downloads/NASA_Facilities.csv /var/lib/mysql-files
[sudo] password for skuzmichev:
'/home/skuzmichev/Downloads/NASA_Facilities.csv'
    -> '/var/lib/mysql-files/NASA_Facilities.csv'
$ sudo chown mysql:mysql /var/lib/mysql-files/NASA_Facilities.csv
$ sudo ls -lh /var/lib/mysql-files/NASA_Facilities.csv
-rw-r--r--. 1 mysql mysql 114K
    Feb 28 14:19 /var/lib/mysql-files/NASA_Facilities.csv

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

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

mysql> LOAD DATA INFILE '/var/lib/mysql-files/NASA_Facilities.csv'
    -> INTO TABLE facilities FIELDS TERMINATED BY ',';
ERROR 1292 (22007): Incorrect datetime value:
'Record Date' for column 'record_date' at row 1

Что ж, это выглядит неправильно: Record Date — это действительно не дата, а имя столбца. Мы сделали глупую, но распространенную ошибку, загрузив файл CSV с заголовком. Нам нужно указать MySQL опустить его:

mysql> LOAD DATA INFILE '/var/lib/mysql-files/NASA_Facilities.csv'
    -> INTO TABLE facilities FIELDS TERMINATED BY ','
    -> IGNORE 1 LINES;
ERROR 1292 (22007): Incorrect datetime value:
'03/01/1996 12:00:00 AM' for column 'record_date' at row 1

Оказывается, этот формат даты не соответствует ожиданиям MySQL. Это чрезвычайно распространенная проблема. Есть пара выходов. Во-первых, мы можем просто изменить наш столбец record_date на тип TEXT. Мы потеряем тонкости правильного типа данных даты и времени, но сможем получить данные в нашу базу данных. Во-вторых, мы можем конвертировать данные, полученные из файла, на лету. Чтобы продемонстрировать разницу в результатах, мы указали занятый столбец (который является полем даты) как ТЕКСТ. Прежде чем мы перейдем к сложностям преобразования, давайте попробуем запустить ту же команду в Windows:

mysql> LOAD DATA INFILE
    -> 'C:\ProgramData\MySQL\MySQL Server 8.0\Uploads\NASA_Facilities.csv'
    -> INTO TABLE facilities FIELDS TERMINATED BY ',';
ERROR 1290 (HY000): The MySQL server is running with
the --secure-file-priv option so it cannot execute this statement

Даже если файл присутствует в этом каталоге, выдается ошибка LOAD DATA INFILE. Причина этого в том, как MySQL работает с путями в Windows. Мы не можем просто использовать обычные пути в стиле Windows с этой или другими командами MySQL. Нам нужно экранировать каждую обратную косую черту (\) другой обратной косой чертой или изменить наш путь, чтобы использовать прямую косую черту (/). Оба варианта будут работать… или, скорее, в этом случае оба выдадут ошибку из-за ожидаемой проблемы с преобразованием record_date:

mysql> LOAD DATA INFILE
    -> 'C:\\ProgramData\\MySQL\\MySQL Server 8.0\\Uploads\\NASA_Facilities.csv'
    -> INTO TABLE facilities FIELDS TERMINATED BY ',';
ERROR 1292 (22007): Incorrect datetime value:
'Record Date' for column 'record_date' at row 1
mysql> LOAD DATA INFILE
    -> 'C:/ProgramData/MySQL/MySQL Server 8.0/Uploads/NASA_Facilities.csv'
    -> INTO TABLE facilities FIELDS TERMINATED BY ',';
ERROR 1292 (22007): Incorrect datetime value:
'Record Date' for column 'record_date' at row 1

После этого давайте вернемся к нашей проблеме преобразования даты. Как мы уже упоминали, это чрезвычайно распространенная проблема. Вы неизбежно столкнетесь с проблемами преобразования типов, потому что CSV не имеет типов, а разные базы данных предъявляют разные требования к разным типам. В этом случае открытый набор данных, который мы получили, имеет даты в следующем формате: 01.03.1996 00:00:00. Хотя это сделает нашу операцию более сложной, мы считаем, что преобразование значений дат из нашего CSV-файла является хорошим упражнением. Чтобы преобразовать произвольную строку в дату или хотя бы попытаться сделать такое преобразование, мы можем использовать функцию STR_TO_DATE(). Изучив документацию, мы пришли к следующему решению:

mysql> SELECT STR_TO_DATE('03/01/1996 12:00:00 AM',
    -> '%m/%d/%Y %h:%i:%s %p') converted;
+---------------------+
| converted           |
+---------------------+
| 1996-03-01 00:00:00 |
+---------------------+
1 row in set (0.01 sec)

Поскольку функция возвращает NULL при неудачном приведении, мы знаем, что нам удалось найти правильный вызов. Теперь нам нужно выяснить, как использовать функцию в команде LOAD DATA INFILE. Гораздо более длинная версия с использованием функции выглядит так:

mysql> LOAD DATA INFILE '/var/lib/mysql-files/NASA_Facilities.csv'
    -> INTO TABLE facilities FIELDS TERMINATED BY ','
    -> OPTIONALLY ENCLOSED BY '"'
    -> IGNORE 1 LINES
    -> (center, center_search_status, facility, facility_url,
    -> occupied, status, url_link, @var_record_date, @var_last_update,
    -> country, contact, phone, location, city, state, zipcode)
    -> SET record_date = IF(
    ->   CHAR_LENGTH(@var_record_date)=0, NULL,
    ->     STR_TO_DATE(@var_record_date, '%m/%d/%Y %h:%i:%s %p')
    -> ),
    -> last_update = IF(
    ->   CHAR_LENGTH(@var_last_update)=0, NULL,
    ->     STR_TO_DATE(@var_last_update, '%m/%d/%Y %h:%i:%s %p')
    -> );
Query OK, 485 rows affected (0.05 sec)
Records: 485  Deleted: 0  Skipped: 0  Warnings: 0

Это слишком много для команды команды! Давайте сломаем это. Первая строка указывает нашу команду LOAD DATA INFILE и путь к файлу. Во второй строке указывается целевая таблица и начинается спецификация FIELDS, начиная с TERMINATED BY ',', что означает, что наши поля разделены запятыми, как и ожидается для CSV. Третья строка добавляет еще один параметр в спецификацию FIELDS и сообщает MySQL, что некоторые поля (но не все) заключены в символ ". Это важно, потому что в нашем наборе данных есть некоторые записи с запятыми внутри полей "...". В четвертой строке мы указываем, что пропускаем первую строку файла, где, как мы знаем, находится заголовок.

Строки с 5 по 7 содержат спецификацию списка столбцов. Нам нужно преобразовать два столбца даты и времени, а для этого нам нужно прочитать их значения в переменные, которые затем устанавливаются в значения столбцов таблицы nasa.facilities. Однако мы не можем сказать это MySQL, не указав также все остальные столбцы. Если бы мы исключили некоторые столбцы из списка или указали их в неправильном порядке, MySQL не присвоил бы значения правильно. CSV по своей сути является позиционно-ориентированным форматом. По умолчанию, когда спецификация FIELDS не указана, MySQL будет читать каждую строку CSV и будет ожидать, что каждое поле во всех строках будет сопоставлено со столбцом в целевой таблице (в порядке столбцов, который дает команда DESCRIBE или SHOW CREATE TABLE) . Изменив порядок столбцов в этой спецификации, мы можем заполнить таблицу из CSV-файла, поля которого расположены не на месте.

Строки с 8 по 15 — это вызовы наших функций для преобразования значений даты и времени. В предыдущей спецификации столбца мы определили, что поле 8 считывается в переменную @var_record_date, а поле 9 — в @var_last_update. Мы знаем, что поля 8 и 9 — это наши проблемные поля даты и времени. С заполненными переменными мы можем определить параметр SET, который позволяет изменять значения столбца целевой таблицы на основе полей, считанных из файла CSV. В этом очень простом примере вы можете умножить конкретное значение на два. В нашем случае мы используем две функции: сначала мы проверяем, что переменная не пуста (,, в CSV), оценивая количество символов, считанных из файла, а затем мы вызываем фактическое преобразование, если предыдущая проверка не возвращает результат. нуль. Если мы обнаружили, что длина равна нулю, мы устанавливаем значение NULL.

Наконец, когда команда выполнена, можно проверить результаты:

mysql> SELECT facility, occupied, last_update
    -> FROM facilities
    -> ORDER BY last_update DESC LIMIT 5;
+---------------------...+------------------------+---------------------+
| facility            ...| occupied               | last_update         |
+---------------------...+------------------------+---------------------+
| Turn Basin/K7-1005  ...| 01/01/1963 12:00:00 AM | 2015-06-22 00:00:00 |
| RPSF Surge Building ...| 01/01/1984 12:00:00 AM | 2015-06-22 00:00:00 |
| Thermal Protection S...| 01/01/1988 12:00:00 AM | 2015-06-22 00:00:00 |
| Intermediate Bay/M7-...| 01/01/1995 12:00:00 AM | 2015-06-22 00:00:00 |
| Orbiter Processing F...| 01/01/1987 12:00:00 AM | 2015-06-22 00:00:00 |
+---------------------...+------------------------+---------------------+
5 rows in set (0.00 sec)

Помните, мы упоминали, что occupied останется как TEXT. Вы можете увидеть это здесь. Хотя его можно использовать для сортировки, никакие функции даты не будут работать со значениями в этом столбце, если они явно не приведены к DATETIME.

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

Запись данных в файлы с разделителями-запятыми

Вы можете использовать оператор SELECT INTO OUTFILE, чтобы записать результат запроса в файл CSV, который можно открыть в электронной таблице или другой программе.

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

mysql> USE employees;
Database changed
mysql> SELECT emp_no, first_name, last_name, title, from_date
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager' AND to_date = '9999-01-01';
+--------+------------+------------+---------+------------+
| emp_no | first_name | last_name  | title   | from_date  |
+--------+------------+------------+---------+------------+
| 110039 | Vishwani   | Minakawa   | Manager | 1991-10-01 |
| 110114 | Isamu      | Legleitner | Manager | 1989-12-17 |
| 110228 | Karsten    | Sigstam    | Manager | 1992-03-21 |
| 110420 | Oscar      | Ghazalie   | Manager | 1996-08-30 |
| 110567 | Leon       | DasSarma   | Manager | 1992-04-25 |
| 110854 | Dung       | Pesch      | Manager | 1994-06-28 |
| 111133 | Hauke      | Zhang      | Manager | 1991-03-07 |
| 111534 | Hilary     | Kambil     | Manager | 1991-04-08 |
| 111939 | Yuchang    | Weedman    | Manager | 1996-01-03 |
+--------+------------+------------+---------+------------+
9 rows in set (0.13 sec)

Мы можем немного изменить этот запрос SELECT, чтобы записать эти данные в выходной файл в виде значений, разделенных запятыми. INTO OUTFILE подчиняется тем же правилам опции --secure-file-priv, что и LOAD DATA INFILE. Путь к файлу по умолчанию ограничен, и мы перечислили параметры по умолчанию в разделе «Загрузка данных из файлов с разделителями-запятыми»:

mysql> SELECT emp_no, first_name, last_name, title, from_date
    -> FROM employees JOIN titles USING (emp_no)
    -> WHERE title = 'Manager' AND to_date = '9999-01-01'
    -> INTO OUTFILE '/var/lib/mysql-files/managers.csv'
    -> FIELDS TERMINATED BY ',';
Query OK, 9 rows affected (0.14 sec)

Здесь мы сохранили результаты в файле manager.csv в каталоге /var/lib/mysql-files; сервер MySQL должен иметь возможность записи в указанный вами каталог, и он должен быть указан в системной переменной secure_file_priv (если она установлена). В системе Windows вместо этого укажите путь, например C:\ProgramData\MySQL\MySQL Server 8.0\Uploads\managers.csv. Если вы опустите предложение FIELDS TERMINATED BY, сервер будет использовать табуляцию в качестве разделителя по умолчанию между значениями данных.

Вы можете просмотреть содержимое файла manager.csv в текстовом редакторе или импортировать его в программу для работы с электронными таблицами:

110039,Vishwani,Minakawa,Manager,1991-10-01
110114,Isamu,Legleitner,Manager,1989-12-17
110228,Karsten,Sigstam,Manager,1992-03-21
110420,Oscar,Ghazalie,Manager,1996-08-30
110567,Leon,DasSarma,Manager,1992-04-25
110854,Dung,Pesch,Manager,1994-06-28
111133,Hauke,Zhang,Manager,1991-03-07
111534,Hilary,Kambil,Manager,1991-04-08
111939,Yuchang,Weedman,Manager,1996-01-03

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

mysql> USE sakila;
Database changed
mysql> SELECT title, special_features FROM film LIMIT 10
    -> INTO OUTFILE '/var/lib/mysql-files/film.csv'
    -> FIELDS TERMINATED BY ',';
Query OK, 10 rows affected (0.00 sec)

Если вы сейчас посмотрите на данные в файле film.csv (опять же, не стесняйтесь использовать текстовый редактор, программу для работы с электронными таблицами или утилиту командной строки, например, head в Linux), вот что вы увидите:

ACADEMY DINOSAUR,Deleted Scenes\,Behind the Scenes
ACE GOLDFINGER,Trailers\,Deleted Scenes
ADAPTATION HOLES,Trailers\,Deleted Scenes
AFFAIR PREJUDICE,Commentaries\,Behind the Scenes
AFRICAN EGG,Deleted Scenes
AGENT TRUMAN,Deleted Scenes
AIRPLANE SIERRA,Trailers\,Deleted Scenes
AIRPORT POLLOCK,Trailers
ALABAMA DEVIL,Trailers\,Deleted Scenes
ALADDIN CALENDAR,Trailers\,Deleted Scenes

Обратите внимание, что в строках, где второе поле содержит запятую, она автоматически экранируется обратной косой чертой, чтобы отличить ее от разделителя. Некоторые программы для работы с электронными таблицами могут понимать это и удалять обратную косую черту при импорте файла, а некоторые нет. MySQL будет уважать экранирование и не будет рассматривать такие запятые как разделители. Обратите внимание, что если мы укажем FIELDS TERMINATED BY '^', все символы ^ в полях будут экранированы; это не относится к запятым.

Так как не все программы могут изящно обрабатывать escape-последовательности, мы можем попросить MySQL явно определить поля, используя опцию ENCLOSED:

mysql> SELECT title, special_features FROM film LIMIT 10
    -> INTO OUTFILE '/var/lib/mysql-files/film_quoted.csv'
    -> FIELDS TERMINATED BY ',' ENCLOSED BY '"';
Query OK, 10 rows affected (0.00 sec)

Мы использовали эту опцию раньше при загрузке данных. Взгляните на результаты в файле film_quoted.csv:

"ACADEMY DINOSAUR","Deleted Scenes,Behind the Scenes"
"ACE GOLDFINGER","Trailers,Deleted Scenes"
"ADAPTATION HOLES","Trailers,Deleted Scenes"
"AFFAIR PREJUDICE","Commentaries,Behind the Scenes"
"AFRICAN EGG","Deleted Scenes"
"AGENT TRUMAN","Deleted Scenes"
"AIRPLANE SIERRA","Trailers,Deleted Scenes"
"AIRPORT POLLOCK","Trailers"
"ALABAMA DEVIL","Trailers,Deleted Scenes"
"ALADDIN CALENDAR","Trailers,Deleted Scenes"

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

Создание таблиц с запросами

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

В MySQL вы можете легко продублировать структуру таблицы, используя вариант синтаксиса CREATE TABLE:

mysql> USE sakila;
Database changed
mysql> CREATE TABLE actor_2 LIKE actor;
Query OK, 0 rows affected (0.24 sec)
mysql> DESCRIBE actor_2;
+-------------+-------------------+------+-----+...
| Field       | Type              | Null | Key |...
+-------------+-------------------+------+-----+...
| actor_id    | smallint unsigned | NO   | PRI |...
| first_name  | varchar(45)       | NO   |     |...
| last_name   | varchar(45)       | NO   | MUL |...
| last_update | timestamp         | NO   |     |...
+-------------+-------------------+------+-----+...
...+-------------------+-----------------------------------------------+
...| Default           | Extra                                         |
...+-------------------+-----------------------------------------------+
...| NULL              | auto_increment                                |
...| NULL              |                                               |
...| NULL              |                                               |
...| CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
...+-------------------+-----------------------------------------------+
4 rows in set (0.01 sec)
mysql> SELECT * FROM actor_2;
Empty set (0.00 sec)

Синтаксис LIKE позволяет создать новую таблицу с точно такой же структурой, как и другая, включая ключи. Вы можете видеть, что он не копирует данные. Вы также можете использовать функции IF NOT EXISTS и TEMPORARY с этим синтаксисом.

Если вы хотите создать таблицу и скопировать некоторые данные, вы можете сделать это с помощью комбинации операторов CREATE TABLE и SELECT. Давайте удалим таблицу actor_2 и создадим ее заново, используя этот новый подход:

mysql> DROP TABLE actor_2;
Query OK, 0 rows affected (0.08 sec)
mysql> CREATE TABLE actor_2 AS SELECT * from actor;
Query OK, 200 rows affected (0.03 sec)
Records: 200  Duplicates: 0  Warnings: 0
mysql> SELECT * FROM actor_2 LIMIT 5;
+----------+------------+--------------+---------------------+
| actor_id | first_name | last_name    | last_update         |
+----------+------------+--------------+---------------------+
|        1 | PENELOPE   | GUINESS      | 2006-02-15 04:34:33 |
|        2 | NICK       | WAHLBERG     | 2006-02-15 04:34:33 |
|        3 | ED         | CHASE        | 2006-02-15 04:34:33 |
|        4 | JENNIFER   | DAVIS        | 2006-02-15 04:34:33 |
|        5 | JOHNNY     | LOLLOBRIGIDA | 2006-02-15 04:34:33 |
+----------+------------+--------------+---------------------+
5 rows in set (0.01 sec)

Создается идентичная таблица, actor_2, и все данные копируются оператором SELECT. CREATE TABLE AS SELECT или CTAS — это обычное название для этого действия, но на самом деле не обязательно указывать часть AS, и мы опустим ее позже.

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

mysql> CREATE TABLE report (title VARCHAR(128), category VARCHAR(25))
    -> SELECT title, name AS category FROM
    -> film JOIN film_category USING (film_id)
    -> JOIN category USING (category_id);
Query OK, 1000 rows affected (0.06 sec)
Records: 1000  Duplicates: 0  Warnings: 0

Вы можете видеть, что синтаксис немного отличается от предыдущего примера. В этом примере за новым именем таблицы, report, следует список имен и типов столбцов в круглых скобках; это необходимо, потому что мы не дублируем структуру существующей таблицы. Более того, мы фактически меняем название name на category. Затем следует оператор SELECT, вывод которого соответствует новым столбцам в новой таблице. Вы можете проверить содержимое новой таблицы, чтобы увидеть результат:

mysql> SELECT * FROM report LIMIT 5;
+---------------------+----------+
| title               | category |
+---------------------+----------+
| AMADEUS HOLY        | Action   |
| AMERICAN CIRCUS     | Action   |
| ANTITRUST TOMATOES  | Action   |
| ARK RIDGEMONT       | Action   |
| BAREFOOT MANCHURIAN | Action   |
+---------------------+----------+
5 rows in set (0.00 sec)

Таким образом, в этом примере значения title и name из инструкции SELECT используются для заполнения новых столбцов title и category в таблице report.

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

mysql> DESCRIBE actor_2;
+-------------+-------------------+------+-----+...
| Field       | Type              | Null | Key |...
+-------------+-------------------+------+-----+...
| actor_id    | smallint unsigned | NO   |     |...
| first_name  | varchar(45)       | NO   |     |...
| last_name   | varchar(45)       | NO   |     |...
| last_update | timestamp         | NO   |     |...
+-------------+-------------------+------+-----+...
...+-------------------+-----------------------------------------------+
...| Default           | Extra                                         |
...+-------------------+-----------------------------------------------+
...| 0                 |                                               |
...| NULL              |                                               |
...| NULL              |                                               |
...| CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
...+-------------------+-----------------------------------------------+
4 rows in set (0.00 sec)
mysql> SHOW CREATE TABLE actor_2\G
*************************** 1. row ***************************
       Table: actor_2
Create Table: CREATE TABLE `actor_2` (
  `actor_id` smallint unsigned NOT NULL DEFAULT '0',
  `first_name` varchar(45) NOT NULL,
  `last_name` varchar(45) NOT NULL,
  `last_update` timestamp NOT NULL
    DEFAULT CURRENT_TIMESTAMP
    ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

Вы можете видеть, что первичного ключа нет; ключ idx_actor_last_name также отсутствует, как и свойство AUTO_INCREMENT столбца actor_id.

Чтобы скопировать индексы в новую таблицу, вы можете сделать как минимум три вещи. Первый — использовать оператор LIKE для создания пустой таблицы с индексами, как описано ранее, а затем скопировать данные с помощью оператора INSERT с оператором SELECT, как описано в разделе «Вставка данных с помощью запросов».

Второе, что вы можете сделать, это использовать CREATE TABLE с оператором SELECT, а затем добавить индексы с помощью ALTER TABLE, как описано в главе 4.

Третий вариант — использовать ключевое слово UNIQUE (или PRIMARY KEY или KEY) в сочетании с CREATE TABLE и SELECT для добавления индекса первичного ключа. Вот пример такого подхода:

mysql> DROP TABLE actor_2;
Query OK, 0 rows affected (0.04 sec)
mysql> CREATE TABLE actor_2 (UNIQUE(actor_id))
    -> AS SELECT * from actor;
Query OK, 200 rows affected (0.05 sec)
Records: 200  Duplicates: 0  Warnings: 0
mysql> DESCRIBE actor_2;
+-------------+-------------------+------+-----+...
| Field       | Type              | Null | Key |...
+-------------+-------------------+------+-----+...
| actor_id    | smallint unsigned | NO   | PRI |...
| first_name  | varchar(45)       | NO   |     |...
| last_name   | varchar(45)       | NO   |     |...
| last_update | timestamp         | NO   |     |...
+-------------+-------------------+------+-----+...
...+-------------------+-----------------------------------------------+
...| Default           | Extra                                         |
...+-------------------+-----------------------------------------------+
...| 0                 |                                               |
...| NULL              |                                               |
...| NULL              |                                               |
...| CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
...+-------------------+-----------------------------------------------+
4 rows in set (0.01 sec)

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

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

mysql> CREATE TABLE actor_3 (
    ->   actor_id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
    ->   first_name VARCHAR(45) NOT NULL,
    ->   last_name VARCHAR(45) NOT NULL,
    ->   last_update TIMESTAMP NOT NULL
    ->     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    ->   PRIMARY KEY (actor_id),
    ->   KEY idx_actor_last_name (last_name)
    -> ) SELECT * FROM actor;
Query OK, 200 rows affected (0.05 sec)
Records: 200  Duplicates: 0  Warnings: 0

Здесь мы установили NOT NULL для новых столбцов, использовали функцию AUTO_INCREMENT для actor_id и создали два ключа. Все, что вы можете сделать в обычном операторе CREATE TABLE, можно сделать и в этом варианте; просто не забудьте добавить эти индексы явно!

Выполнение обновлений и удалений с несколькими таблицами

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

Удаление

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

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

Рассмотрим запрос, который вам нужно написать, чтобы найти в таблице inventory фильмы, которые никогда не брались напрокат. Один из способов сделать это — использовать вложенный запрос, используя методы, которые мы показали вам в главе 5, с предложением NOT EXISTS. Вот запрос:

mysql> SELECT * FROM inventory WHERE NOT EXISTS
    -> (SELECT 1 FROM rental WHERE
    -> rental.inventory_id = inventory.inventory_id);
+--------------+---------+----------+---------------------+
| inventory_id | film_id | store_id | last_update         |
+--------------+---------+----------+---------------------+
|            5 |       1 |        2 | 2006-02-15 05:09:17 |
+--------------+---------+----------+---------------------+
1 row in set (0.01 sec)

Вы, вероятно, видите, как это работает, но давайте кратко обсудим это, прежде чем двигаться дальше. Вы можете видеть, что этот запрос использует коррелированный подзапрос, где текущая строка, обрабатываемая во внешнем запросе, ссылается на подзапрос; вы можете сказать это, потому что имеется ссылка на столбец inventory_id из inventory, но таблица inventory не указана в предложении FROM подзапроса. Подзапрос выдает выходные данные, когда в таблице rental есть строка, совпадающая с текущей строкой во внешнем запросе (и поэтому фильм из этой записи брался напрокат). Однако, поскольку в запросе используется NOT EXISTS, внешний запрос не выдает вывода в этом случае, и поэтому общий результат состоит в том, что выводятся строки для записей фильмов, которые не были взяты напрокат.

Теперь давайте возьмем наш запрос и превратим его в инструкцию DELETE. Вот:

mysql> DELETE FROM inventory WHERE NOT EXISTS
    -> (SELECT 1 FROM rental WHERE
    -> rental.inventory_id = inventory.inventory_id);
Query OK, 1 row affected (0.04 sec)

Вы можете видеть, что подзапрос остается прежним, но внешний запрос SELECT заменяется оператором DELETE. Здесь мы следуем стандартному синтаксису DELETE: за ключевым словом DELETE следует FROM и спецификация таблицы или таблиц, из которых должны быть удалены строки, затем предложение WHERE (и любые другие предложения запроса, такие как GROUP BY или HAVING ). В этом запросе строки удаляются из таблицы инвентаризации, но в предложении WHERE подзапрос указывается в операторе NOT EXISTS.

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

DELETE inventory FROM inventory LEFT JOIN rental
USING (inventory_id) WHERE rental.inventory_id IS NULL;

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

DELETE FROM inventory USING inventory
LEFT JOIN rental USING (inventory_id)
WHERE rental.inventory_id IS NULL;

Этот запрос представляет собой смесь двух предыдущих. Мы не указываем цели удаления между DELETE и FROM, а записываем их так, как если бы это было обычное удаление. Вместо этого мы используем специальное предложение USING, которое указывает, что последует фильтрующий запрос (объединение или что-то другое). На наш взгляд, это немного понятнее, чем предыдущий пример DELETE table FROM table. Одним из недостатков использования ключевого слова USING является то, что его можно перепутать с ключевым словом USING оператора JOIN. Однако с некоторой практикой вы никогда не совершите эту ошибку.

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

mysql> DELETE FROM film WHERE NOT EXISTS
    -> (SELECT 1 FROM inventory WHERE
    -> film.film_id = inventory.film_id);
ERROR 1451 (23000): Cannot delete or update a parent row:
a foreign key constraint fails (
`sakila`.`film_actor`, CONSTRAINT `fk_film_actor_film`
FOREIGN KEY (`film_id`) REFERENCES `film` (`film_id`)
ON DELETE RESTRICT ON UPDATE CASCADE)

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

DELETE FROM film_actor, film USING
film JOIN film_actor USING (film_id)
LEFT JOIN inventory USING (film_id)
WHERE inventory.film_id IS NULL;

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

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

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM film_actor USING
    -> film JOIN film_actor USING (film_id)
    -> LEFT JOIN inventory USING (film_id)
    -> WHERE inventory.film_id IS NULL;
Query OK, 216 rows affected (0.01 sec)
mysql> DELETE FROM film_category USING
    -> film JOIN film_category USING (film_id)
    -> LEFT JOIN inventory USING (film_id)
    -> WHERE inventory.film_id IS NULL;
Query OK, 42 rows affected (0.00 sec)
mysql> DELETE FROM film USING
    -> film LEFT JOIN inventory USING (film_id)
    -> WHERE inventory.film_id IS NULL;
Query OK, 42 rows affected (0.00 sec)
mysql> ROLLBACK;
Query OK, 0 rows affected (0.02 sec)

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

Второй вариант опасен. Можно приостановить ограничения внешнего ключа, временно установив для системной переменной foreign_key_checks значение 0 на уровне сеанса. Мы не рекомендуем эту практику, но это единственный способ удалить все три таблицы одновременно:

mysql> SET foreign_key_checks=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM film, film_actor, film_category
    -> USING film JOIN film_actor USING (film_id)
    -> JOIN film_category USING (film_id)
    -> LEFT JOIN inventory USING (film_id)
    -> WHERE inventory.film_id IS NULL;
Query OK, 300 rows affected (0.03 sec)
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql> SET foreign_key_checks=1;
Query OK, 0 rows affected (0.00 sec)

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

Разберем этот запрос. Таблицы, из которых будут удалены строки (при совпадении), — это film, film_actor и film_category. Мы указали их между терминами DELETE FROM и USING для ясности. USING запускает наш запрос, фильтрующую часть оператора DELETE. В этом примере мы создали объединение четырех таблиц. Мы соединили film, film_actor и film_category с помощью INNER JOIN, так как нам нужны только совпадающие строки. Для этих объединений мы выполнили LEFT JOIN таблицы inventory. В этом контексте использование левого соединения чрезвычайно важно, потому что на самом деле нас интересуют только строки, в которых в inventory не будет записей. Мы выражаем это с помощью WHERE inventory.film_id IS NULL. Результатом этого запроса является то, что мы получаем все фильмы, которых нет в inventory, а затем все отношения актеров и актеров для этих фильмов, а также все отношения категорий для фильмов.

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

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM film_actor, film_category USING
    -> film JOIN film_actor USING (film_id)
    -> JOIN film_category USING (film_id)
    -> LEFT JOIN inventory USING (film_id)
    -> WHERE inventory.film_id IS NULL;
Query OK, 258 rows affected (0.02 sec)
mysql> DELETE FROM film USING
    -> film LEFT JOIN inventory USING (film_id)
    -> WHERE inventory.film_id IS NULL;
Query OK, 42 rows affected (0.01 sec)
mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)

Здесь мы объединили удаление из таблиц film_actor и film_category в один оператор DELETE, что позволяет выполнять удаление из film без ошибок. Отличие от предыдущего примера в том, что мы с DELETE FROM делаем две таблицы вместо трех.

Давайте поговорим о количестве затронутых строк. В первом примере мы удалили 42 строки из film, 42 строки из film_category и 216 строк из таблицы film_actor. Во втором примере наш единственный запрос DELETE удалил 300 строк. В последнем примере мы удалили 258 строк из таблиц film_category и film_actor и 42 строки из таблицы film. Вы, наверное, уже догадались, что для удаления нескольких таблиц MySQL выведет общее количество удаленных строк без разбивки на отдельные таблицы. Это затрудняет точное отслеживание того, сколько строк было затронуто в каждой таблице.

Кроме того, при удалении нескольких таблиц нельзя использовать предложения ORDER BY или LIMIT.

Обновления

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

mysql> SELECT name category, title, rating
    -> FROM film JOIN film_category USING (film_id)
    -> JOIN category USING (category_id)
    -> WHERE name = 'Horror';
+----------+-----------------------------+--------+
| category | title                       | rating |
+----------+-----------------------------+--------+
| Horror   | ACE GOLDFINGER              | G      |
| Horror   | AFFAIR PREJUDICE            | G      |
| Horror   | AIRPORT POLLOCK             | R      |
| Horror   | ALABAMA DEVIL               | PG-13  |
| ...                                             |
| Horror   | ZHIVAGO CORE                | NC-17  |
+----------+-----------------------------+--------+
56 rows in set (0.00 sec)
mysql> SELECT COUNT(title)
    -> FROM film JOIN film_category USING (film_id)
    -> JOIN category USING (category_id)
    -> WHERE name = 'Horror' AND rating <> 'R';
+--------------+
| COUNT(title) |
+--------------+
|           42 |
+--------------+
1 row in set (0.00 sec)

Не знаем, как вы, а мы бы хотели посмотреть фильм ужасов с рейтингом G! Теперь давайте поместим этот запрос в инструкцию UPDATE:

mysql> UPDATE film JOIN film_category USING (film_id)
    -> JOIN category USING (category_id)
    -> SET rating = 'R' WHERE category.name = 'Horror';
Query OK, 42 rows affected (0.01 sec)
Rows matched: 56 Changed: 42 Warnings: 0

Давайте посмотрим на синтаксис. Обновление нескольких таблиц похоже на запрос SELECT. За оператором UPDATE следует список таблиц, который включает любые предложения соединения, которые вам нужны или предпочтительны; в этом примере мы использовали JOIN (помните, что это INNER JOIN) для объединения таблиц film и film_category. За ним следует ключевое слово SET с присвоением отдельным столбцам. Здесь вы можете видеть, что изменен только один столбец (чтобы изменить рейтинг на R), поэтому столбцы во всех других таблицах, кроме film, не изменены. Следующее WHERE является необязательным, но необходимо в этом примере, чтобы касаться только строк с названием категории Horror.

Обратите внимание, как MySQL сообщает, что 56 строк были сопоставлены, но только 42 обновлены. Если вы посмотрите на результаты предыдущих запросов SELECT, вы увидите, что они показывают количество фильмов в категории Horror (56) и фильмов в этой категории с рейтингом, отличным от R (42). Были обновлены только 42 строки, потому что другие фильмы уже имели такой рейтинг.

Как и в случае удаления нескольких таблиц, существуют некоторые ограничения на обновления нескольких таблиц:

В остальном многотабличные обновления почти такие же, как и однотабличные.

Замена данных

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

Оператор REPLACE предоставляет третий удобный способ изменения данных. В этом разделе объясняется, как это работает.

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

Давайте попробуем пример, где мы заменим строку для актрисы PENELOPE GUINESS в базе данных sakila:

mysql> REPLACE INTO actor VALUES (1, 'Penelope', 'Guiness', NOW());
ERROR 1451 (23000): Cannot delete or update a parent row:
a foreign key constraint fails (`sakila`.`film_actor`,
CONSTRAINT `fk_film_actor_actor` FOREIGN KEY (`actor_id`)
REFERENCES `actor` (`actor_id`) ON DELETE RESTRICT ON UPDATE CASCADE)

К сожалению, как вы уже догадались после прочтения предыдущего абзаца, REPLACE на самом деле должен выполнить DELETE. Если ваша база данных сильно ограничена ссылочно, как база данных sakila, REPLACE часто не будет работать. Давайте не будем бороться с базой данных, а вместо этого воспользуемся таблицей actor_2, которую мы создали в разделе «Создание таблиц с запросами»:

mysql> REPLACE actor_2 VALUES (1, 'Penelope', 'Guiness', NOW());
Query OK, 2 rows affected (0.00 sec)

Вы можете видеть, что MySQL сообщает, что были затронуты две строки: сначала была удалена старая строка, а затем была вставлена новая строка. Вы можете видеть, что сделанное нами изменение было незначительным — мы просто изменили регистр имени — и поэтому его легко можно было выполнить с помощью UPDATE. Поскольку таблицы в базе данных sakila относительно малы, сложно построить пример, в котором REPLACE выглядит проще, чем UPDATE.

Вы можете использовать различные синтаксисы INSERT с REPLACE, включая использование запросов SELECT. Вот некоторые примеры:

mysql> REPLACE INTO actor_2 VALUES (1, 'Penelope', 'Guiness', NOW());
Query OK, 2 rows affected (0.00 sec)
mysql> REPLACE INTO actor_2 (actor_id, first_name, last_name)
    -> VALUES (1, 'Penelope', 'Guiness');
Query OK, 2 rows affected (0.00 sec)
mysql> REPLACE actor_2 (actor_id, first_name, last_name)
    -> VALUES (1, 'Penelope', 'Guiness');
Query OK, 2 rows affected (0.00 sec)
mysql> REPLACE actor_2 SET actor_id = 1,
    -> first_name = 'Penelope', last_name = 'Guiness';
Query OK, 2 rows affected (0.00 sec)

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

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

mysql> REPLACE actor_2 (actor_id, first_name, last_name)
    -> VALUES (2, 'Nick', 'Wahlberg'),
    -> (3, 'Ed', 'Chase');
Query OK, 4 rows affected (0.00 sec)
Records: 2 Duplicates: 2 Warnings: 0

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

mysql> REPLACE actor_2 (actor_id, first_name, last_name)
    -> VALUES (1000, 'William', 'Dyer');
Query OK, 1 row affected (0.00 sec)

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

Замена также работает с оператором SELECT. Вспомните таблицу recommend из раздела «Вставка данных с помощью запросов» в начале этой главы. Предположим, вы добавили в нее 10 фильмов, но вам не нравится выбор седьмого фильма в списке. Вот как вы можете заменить его случайным выбором другого фильма:

mysql> REPLACE INTO recommend SELECT film_id, language_id,
    -> release_year, title, length, 7 FROM film
    -> ORDER BY RAND() LIMIT 1;
Query OK, 2 rows affected (0.00 sec)
Records: 1 Duplicates: 1 Warnings: 0

Опять же, синтаксис такой же, как и у INSERT, но попытка удаления выполняется (и успешно!) перед вставкой. Обратите внимание, что мы сохраняем значение sequence_id равным 7.

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

При указании списка столбцов для REPLACE вы должны указать каждый столбец, не имеющий значения по умолчанию. В наших примерах нам нужно было указать actor_id, first_name и last_name, но мы пропустили столбец last_update, который имеет значение по умолчанию CURRENT_TIMESTAMP.

MySQL предоставляет еще одно нестандартное расширение SQL: INSERT ... ON DUPLICATE KEY UPDATE. Оно похоже на REPLACE, но вместо DELETE, за которым следует INSERT, оно выполняет UPDATE всякий раз, когда обнаруживается повторяющийся ключ. В начале этого раздела у нас возникла проблема с заменой строки в таблице actor. MySQL отказался выполнять REPLACE, потому что удаление строки из таблицы actor нарушило бы ограничение внешнего ключа. Однако легко добиться желаемого результата с помощью следующего выражения:

mysql> INSERT INTO actor_3 (actor_id, first_name, last_name)
    -> VALUES (1, 'Penelope', 'Guiness')
    -> ON DUPLICATE KEY UPDATE first_name = 'Penelope', last_name = 'Guiness';
Query OK, 2 rows affected (0.00 sec)

Обратите внимание, что мы используем таблицу actor_3, созданную в разделе «Создание таблиц с помощью запросов», поскольку она имеет все те же ограничения, что и исходная таблица actor. Оператор, который мы только что показали, семантически очень похож на REPLACE, но имеет несколько ключевых отличий. Если вы не указываете значение для поля в команде REPLACE, это поле должно иметь значение DEFAULT, и будет установлено это значение по умолчанию. Это естественно следует из того факта, что вставляется совершенно новая строка. В случае INSERT... ON DUPLICATE KEY UPDATE мы обновляем существующую строку, поэтому нет необходимости перечислять все столбцы. Мы можем сделать это, если захотим:

mysql> INSERT INTO actor_3 VALUES (1, 'Penelope', 'Guiness', NOW())
    -> ON DUPLICATE KEY UPDATE
    -> actor_id = 1, first_name = 'Penelope',
    -> last_name = 'Guiness', last_update = NOW();
Query OK, 2 rows affected (0.01 sec)

Чтобы свести к минимуму объем ввода, необходимого для этой команды, и разрешить вставку нескольких строк, мы можем ссылаться на новые значения полей в предложении UPDATE. Вот пример с несколькими строками, одна из которых новая:

mysql> INSERT INTO actor_3 (actor_id, first_name, last_name) VALUES
    -> (1, 'Penelope', 'Guiness'), (2, 'Nick', 'Wahlberg'),
    -> (3, 'Ed', 'Chase'), (1001, 'William', 'Dyer')
    -> ON DUPLICATE KEY UPDATE first_name = VALUES(first_name),
    -> last_name = VALUES(last_name);
Query OK, 5 rows affected (0.01 sec)
Records: 4 Duplicates: 2

Рассмотрим этот запрос более подробно. Мы вставляем четыре строки в таблицу actor_3 и, используя ON DUPLICATE KEY UPDATE, сообщаем MySQL, что необходимо выполнить обновление для всех найденных повторяющихся строк. Однако, в отличие от нашего предыдущего примера, на этот раз мы не устанавливаем обновленные значения столбца явно. Вместо этого мы используем специальную функцию VALUES() для получения значения каждого столбца в строках, которые мы передали в INSERT. Например, для второй строки 2, Nick, Walhberg, VALUES(first_name) вернет Nick. Обратите внимание, что MySQL сообщает, что мы обновили нечетное количество строк: пять. Всякий раз, когда вставляется новая строка, количество затрагиваемых строк увеличивается на единицу. Всякий раз, когда старая строка обновляется, количество затронутых строк увеличивается на два. Поскольку мы уже обновили запись для Penelope, выполнив предыдущий запрос, наша новая вставка не добавляет ничего нового, и MySQL также пропустит обновление. У нас осталось два обновления для повторяющихся строк и вставка совершенно новой строки или всего затронуто пять строк.

Оператор EXPLAIN

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

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

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

Давайте попробуем простой пример, иллюстрирующий идею:

mysql> EXPLAIN SELECT * FROM actor\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: actor
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 200
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

Выражение дает вам много информации. Оно говорит вам, что:

Таким образом, вывод EXPLAIN SELECT * FROM actor сообщает вам, что будут обработаны все строки из таблицы actor (их 200), и никакие индексы не будут использоваться для разрешения запроса. Это имеет смысл и, вероятно, произойдет именно то, что вы ожидали.

Обратите внимание, что каждый оператор EXPLAIN выдает предупреждение. Каждый запрос, который мы отправляем в MySQL, переписывается перед выполнением, и предупреждающее сообщение будет содержать переписанный запрос. Например, * может быть расширен до явного списка столбцов, или подзапрос может быть неявно оптимизирован в JOIN. Вот пример:

mysql> EXPLAIN SELECT * FROM actor WHERE actor_id IN
    -> (SELECT actor_id FROM film_actor
    -> WHERE film_id = 11);
+----+-------------+------------+------------+--------+...
| id | select_type | table      | partitions | type   |...
+----+-------------+------------+------------+--------+...
|  1 | SIMPLE      | film_actor | NULL       | ref    |...
|  1 | SIMPLE      | actor      | NULL       | eq_ref |...
+----+-------------+------------+------------+--------+...
...+------------------------+----------------+---------+...
...| possible_keys          | key            | key_len |...
...+------------------------+----------------+---------+...
...| PRIMARY,idx_fk_film_id | idx_fk_film_id | 2       |...
...| PRIMARY                | PRIMARY        | 2       |...
...+------------------------+----------------+---------+...
...+----------------------------+------+----------+-------------+
...| ref                        | rows | filtered | Extra       |
...+----------------------------+------+----------+-------------+
...| const                      |    4 |   100.00 | Using index |
...| sakila.film_actor.actor_id |    1 |   100.00 | NULL        |
...+----------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
mysql> SHOW WARNINGS\G
*************************** 1. row ***************************

  Level: Note
   Code: 1003
Message: /* select#1 */ select
`sakila`.`actor`.`actor_id` AS `actor_id`,
`sakila`.`actor`.`first_name` AS `first_name`,
`sakila`.`actor`.`last_name` AS `last_name`,
`sakila`.`actor`.`last_update` AS `last_update`
from `sakila`.`film_actor` join `sakila`.`actor` where
((`sakila`.`actor`.`actor_id` = `sakila`.`film_actor`.`actor_id`)
and (`sakila`.`film_actor`.`film_id` = 11))
1 row in set (0.00 sec)

Мы упомянули, что покажем пример с разными значениями id и подзапросом. Вот запрос:

mysql> EXPLAIN SELECT * FROM actor WHERE actor_id IN
    -> (SELECT actor_id FROM film_actor JOIN
    -> film USING (film_id)
    -> WHERE title = 'ZHIVAGO CORE');
+----+--------------+-------------+------------+------+...
| id | select_type  | table       | partitions | type |...
+----+--------------+-------------+------------+------+...
|  1 | SIMPLE       | <subquery2> | NULL       | ALL  |...
|  1 | SIMPLE       | actor       | NULL       | ALL  |...
|  2 | MATERIALIZED | film        | NULL       | ref  |...
|  2 | MATERIALIZED | film_actor  | NULL       | ref  |...
+----+--------------+-------------+------------+------+...
...+------------------------+----------------+---------+---------------------+...
...| possible_keys          | key            | key_len | ref                 |...
...+------------------------+----------------+---------+---------------------+...
...| NULL                   | NULL           | NULL    | NULL                |...
...| PRIMARY                | NULL           | NULL    | NULL                |...
...| PRIMARY,idx_title      | idx_title      | 514     | const               |...
...| PRIMARY,idx_fk_film_id | idx_fk_film_id | 2       | sakila.film.film_id |...
...+------------------------+----------------+---------+---------------------+...
...+------+----------+--------------------------------------------+
...| rows | filtered | Extra                                      |
...+------+----------+--------------------------------------------+
...| NULL |   100.00 | NULL                                       |
...|  200 |     0.50 | Using where; Using join buffer (hash join) |
...|    1 |   100.00 | Using index                                |
...|    5 |   100.00 | Using index                                |
...+------+----------+--------------------------------------------+
4 rows in set, 1 warning (0.01 sec)

В этом примере вы можете видеть, что идентификатор 1 используется для таблиц actor и <subquery2>, а id 2 используется для film и film_actor. Но что такое <subquery2>? Это имя виртуальной таблицы, которое используется здесь, потому что оптимизатор материализовал результаты подзапроса или, другими словами, сохранил их во временной таблице в памяти. Вы можете видеть, что запрос с id 2 имеет тип (select_type) MATERIALIZED. Внешний запрос (id 1) будет искать результаты внутреннего запроса (id 2) из этой временной таблицы. Это лишь одна из многих оптимизаций, которые MySQL может выполнять при выполнении сложных запросов.

Далее мы дадим оператору EXPLAIN некоторую работу. Давайте попросим его объяснить INNER JOIN между actor, film_actor, film, film_category и category:

mysql> EXPLAIN SELECT first_name, last_name FROM actor
    -> JOIN film_actor USING (actor_id)
    -> JOIN film USING (film_id)
    -> JOIN film_category USING (film_id)
    -> JOIN category USING (category_id)
    -> WHERE category.name = 'Horror';
+----+-------------+---------------+------------+--------+...
| id | select_type | table         | partitions | type   |...
+----+-------------+---------------+------------+--------+...
|  1 | SIMPLE      | category      | NULL       | ALL    |...
|  1 | SIMPLE      | film_category | NULL       | ref    |...
|  1 | SIMPLE      | film          | NULL       | eq_ref |...
|  1 | SIMPLE      | film_actor    | NULL       | ref    |...
|  1 | SIMPLE      | actor         | NULL       | eq_ref |...
+----+-------------+---------------+------------+--------+...
...+-----------------------------------+---------------------------+---------+...
...| possible_keys                     | key                       | key_len |...
...+-----------------------------------+---------------------------+---------+...
...| PRIMARY                           | NULL                      | NULL    |...
...| PRIMARY,fk_film_category_category | fk_film_category_category | 1       |...
...| PRIMARY                           | PRIMARY                   | 2       |...
...| PRIMARY,idx_fk_film_id            | idx_fk_film_id            | 2       |...
...| PRIMARY                           | PRIMARY                   | 2       |...
...+-----------------------------------+---------------------------+---------+...
...+------------------------------+------+----------+-------------+
...| ref                          | rows | filtered | Extra       |
...+------------------------------+------+----------+-------------+
...| NULL                         |   16 |    10.00 | Using where |
...| sakila.category.category_id  |   62 |   100.00 | Using index |
...| sakila.film_category.film_id |    1 |   100.00 | Using index |
...| sakila.film_category.film_id |    5 |   100.00 | Using index |
...| sakila.film_actor.actor_id   |    1 |   100.00 | NULL        |
...+------------------------------+------+----------+-------------+
5 rows in set, 1 warning (0.00 sec)

Прежде чем мы обсудим вывод, подумайте о том, как запрос может быть оценен. MySQL может просмотреть каждую строку в таблице actor, затем сопоставить ее с film_actor, затем с film, film_category и, наконец, с category. У нас есть фильтр в таблице category, поэтому в этом воображаемом случае MySQL сможет сопоставить меньшее количество строк только после того, как попадет в эту таблицу. Это плохая стратегия исполнения. Можете ли вы придумать лучше?

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

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

MySQL 8.0 представил новый формат вывода EXPLAIN PLAN, который доступен через оператор EXPLAIN ANALYZE. Хотя это может быть несколько легче для чтения, здесь есть оговорка, что оператор действительно должен быть выполнен, в отличие от обычного EXPLAIN. Мы не будем вдаваться в подробности этого нового формата, но покажем здесь пример:

mysql> EXPLAIN ANALYZE SELECT first_name, last_name
    -> FROM actor JOIN film_actor USING (actor_id)
    -> JOIN film USING (film_id)
    -> WHERE title = 'ZHIVAGO CORE'\G
*************************** 1. row ***************************
EXPLAIN:
-> Nested loop inner join
   (cost=3.07 rows=5)
   (actual time=0.036..0.055 rows=6 loops=1)
  -> Nested loop inner join
     (cost=1.15 rows=5)
     (actual time=0.028..0.034 rows=6 loops=1)
    -> Index lookup on film
       using idx_title (title='ZHIVAGO CORE')
       (cost=0.35 rows=1)
       (actual time=0.017..0.018 rows=1 loops=1)
    -> Index lookup on film_actor
       using idx_fk_film_id (film_id=film.film_id)
       (cost=0.80 rows=5)
       (actual time=0.010..0.015 rows=6 loops=1)
  -> Single-row index lookup on actor
     using PRIMARY (actor_id=film_actor.actor_id)
     (cost=0.27 rows=1)
     (actual time=0.003..0.003 rows=1 loops=6)

1 row in set (0.00 sec)

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

Альтернативные механизмы хранения

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

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

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

На момент написания в официальной документации MySQL было зарегистрировано 8 дополнительных механизмов хранения, а для MariaDB — 18 дополнительных механизмов. На самом деле существует еще больше доступных механизмов хранения, но не все из них включены в документацию по основным разновидностям MySQL. Здесь мы опишем только те движки, которые мы считаем полезными и которые хотя бы частично используются. Вполне может быть, что механизм хранения, который лучше всего подходит для вашего варианта использования, не описан нами. Не обижайтесь; их слишком много, чтобы покрыть их всех справедливо.

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

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

Доступные в настоящее время транзакционные механизмы включают InnoDB по умолчанию, активно разрабатываемый MyRocks и устаревший TokuDB. Все различные движки доступны в основных версиях MySQL; но только эти три поддерживают транзакции. Любой другой движок не является транзакционным.

Следующее широкое разделение, которое мы можем провести, основано на безопасности при столкновении или способности двигателя гарантировать свойство долговечности набора свойств ACID. Если в таблице используется отказоустойчивый движок, мы можем ожидать, что каждый бит данных, записанных зафиксированной транзакцией, будет доступен после нечистого перезапуска экземпляра. К аварийно-безопасным движкам относятся уже упомянутые InnoDB, MyRocks и TokuDB, а также движок Aria. Ни один из других доступных движков не гарантирует безопасность при столкновении.

Мы могли бы привести больше примеров того, как сгруппировать типы таблиц, но давайте перейдем непосредственно к описанию некоторых механизмов и их свойств. Прежде всего, давайте посмотрим, как на самом деле просмотреть список доступных движков. Для этого мы используем специальную команду SHOW ENGINES. Вот его вывод для установки MySQL 8.0.23 Linux по умолчанию:

mysql> SHOW ENGINES;
+--------------------+---------+...
| Engine             | Support |...
+--------------------+---------+...
| ARCHIVE            | YES     |...
| BLACKHOLE          | YES     |...
| MRG_MYISAM         | YES     |...
| FEDERATED          | NO      |...
| MyISAM             | YES     |...
| PERFORMANCE_SCHEMA | YES     |...
| InnoDB             | DEFAULT |...
| MEMORY             | YES     |...
| CSV                | YES     |...
+--------------------+---------+...
...+----------------------------------------------------------------+...
...| Comment                                                        |...
...+----------------------------------------------------------------+...
...| Archive storage engine                                         |...
...| /dev/null storage engine (anything you write to it disappears) |...
...| Collection of identical MyISAM tables                          |...
...| Federated MySQL storage engine                                 |...
...| MyISAM storage engine                                          |...
...| Performance Schema                                             |...
...| Supports transactions, row-level locking, and foreign keys     |...
...| Hash based, stored in memory, useful for temporary tables      |...
...| CSV storage engine                                             |...
...+----------------------------------------------------------------+...
...+--------------+------+------------+
...| Transactions | XA   | Savepoints |
...+--------------+------+------------+
...| NO           | NO   | NO         |
...| NO           | NO   | NO         |
...| NO           | NO   | NO         |
...| NULL         | NULL | NULL       |
...| NO           | NO   | NO         |
...| NO           | NO   | NO         |
...| YES          | YES  | YES        |
...| NO           | NO   | NO         |
...| NO           | NO   | NO         |
...+--------------+------+------------+
9 rows in set (0.00 sec)

Вы можете видеть, что MySQL удобно сообщает нам, поддерживает ли механизм транзакции. Столбец XA предназначен для распределенных транзакций — мы не будем рассматривать их в этой книге. Точки сохранения — это, по сути, возможность создавать мини-транзакции внутри транзакций, еще одна сложная тема. В качестве упражнения рассмотрите возможность запуска SHOW ENGINES; в установках MariaDB и Percona Server.

InnoDB

Прежде чем мы перейдем к «альтернативным» механизмам хранения, давайте обсудим стандартный: InnoDB. InnoDB надежен, эффективен и полнофункционален. Почти все, что вы ожидаете от современной СУБД, в некотором роде достижимо с InnoDB. В этой книге мы никогда не меняем движок таблицы, поэтому в каждом примере используется InnoDB. Пока вы изучаете MySQL, мы рекомендуем вам придерживаться этого движка. Важно понимать его недостатки, но если они не станут для вас проблематичными, почти нет причин не использовать его постоянно.

Тип таблицы InnoDB включает следующие функции:

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

Это подробно обсуждается в главе 6.

Расширенные функции восстановления после сбоев

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

Блокировка на уровне строки

В отличие от предыдущего механизма по умолчанию, MyISAM (который мы рассмотрим в следующем разделе), InnoDB предоставляет детализированную инфраструктуру блокировок. Самый низкий уровень блокировки — уровень строки, что означает, что отдельная строка может быть заблокирована выполняющимся запросом или транзакцией. Это важно для большинства приложений оперативной обработки транзакций (OLTP), требующих больших объемов записи; если вы блокируете на более высоком уровне, например на уровне таблицы, вы можете столкнуться со слишком большим количеством проблем с параллелизмом.

Поддержка внешнего ключа

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

Поддержка шифрования

Таблицы InnoDB могут быть прозрачно зашифрованы MySQL.

Поддержка разделов

InnoDB поддерживает секционирование (partitioning); то есть физическое распространение данных между несколькими файлами данных на основе некоторых правил. Это позволяет InnoDB эффективно работать с таблицами огромных размеров.

Плюсов много, но есть и минусы:

Сложность

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

Объем данных

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

Масштабирование с размером базы данных

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

MyISAM и Aria

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

В настоящее время мы не рекомендуем использовать MyISAM, если это не требуется по устаревшим причинам. Вы можете прочитать в Интернете, что его производительность лучше, чем у InnoDB. К сожалению, большая часть этой информации очень старая и все более устаревает — сегодня в подавляющем большинстве случаев это просто не так. Одной из причин этого являются изменения в ядре Linux, вызванные уязвимостями безопасности Spectre и Meltdown в январе 2018 года, что привело к снижению производительности MyISAM до 90%.

До MySQL 8.0 MyISAM использовался в MySQL для всех объектов словаря данных. Начиная с этой версии словарь данных теперь полностью InnoDB для поддержки расширенных функций, таких как атомарный DDL.

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

Тип таблицы MyISAM включает следующие функции:

Блокировка на уровне таблицы

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

Поддержка разделов

До MySQL 8.0 MyISAM поддерживал секционирование. В MySQL 8.0 это уже не так, и для этого нужно прибегнуть к использованию других механизмов хранения (Merge или MRG_MyISAM).

Сжатие

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

Тип MyISAM имеет следующие ограничения:

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

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

Транзакции

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

Шифрование

Таблицы MyISAM не поддерживают шифрование.

MyRocks и TokuDB

Одной из наиболее значительных проблем с InnoDB является относительная сложность работы с большими наборами данных. Мы упоминали, что желательно иметь часто используемые данные в памяти, но это не всегда возможно. Более того, когда объем данных достигает нескольких терабайт, производительность InnoDB на диске также страдает. Объекты в InnoDB также имеют довольно большие накладные расходы с точки зрения размера. В последние годы появилось несколько различных проектов, которые пытаются исправить проблемы, присущие базовой структуре данных InnoDB, B-дереву, путем создания механизма хранения на другой структуре данных. К ним относятся MyRocks, основанный на LSM-дереве, и TokuDB, основанный на собственной структуре данных фрактального дерева.

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

Тип таблицы MyRocks включает в себя следующие функции:

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

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

Расширенные функции восстановления после сбоев

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

Поддержка шифрования

Таблицы MyRocks могут быть зашифрованы.

Поддержка разделов

Таблицы MyRocks могут быть разбиты на разделы.

Сжатие данных и компактность

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

Стабильная производительность записи в масштабе

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

Тип таблицы MyRocks имеет следующие ограничения:

Транзакции и блокировки

MyRocks не поддерживает уровень изоляции SERIALIZABLE или блокировку промежутков, описанную в главе 6.

Внешние ключи

Только InnoDB поддерживает ограничения внешнего ключа.

Компромиссы производительности

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

Сложность

Мы упоминали, что InnoDB сложнее, чем MyISAM. Однако в некоторых отношениях MyRocks сложнее, чем InnoDB. Он плохо документирован, активно развивается (поэтому менее стабилен) и с ним может быть сложно работать.

Общая доступность

MyRocks недоступен в Community или Enterprise MySQL; чтобы использовать его, вам нужно использовать другую версию MySQL, например, MariaDB или Percona Server. Это может привести к трудностям в работе. Пакетные версии отстают от разработки, и для использования всех текущих функций необходимо построить выделенный сервер MySQL с исходными кодами MyRocks.

Другие типы таблиц

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

Память

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

Архив

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

CSV

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

Объединенные таблицы (Federated)

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

Черная дыра (Blackhole)

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

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

Глава 8
Управление пользователями и привилегиями

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

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

Понимание пользователей и привилегий

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

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

Рассмотрим эти моменты чуть подробнее. Каждый раз, когда вы получаете доступ к серверу MySQL, вы должны указать пользователя, который будет использоваться во время аутентификации. После того, как вы прошли аутентификацию и ваша личность подтверждена, вы получаете доступ к базе данных. Обычно пользователь, от имени которого вы будете работать с объектами схемы, будет тем же, что и для аутентификации, но это не обязательно, и поэтому мы отделим второй пункт. Прокси-пользователь — это пользователь, который используется для проверки привилегий и фактически действует в базе данных, когда во время аутентификации используется другой пользователь. Это довольно сложная тема и требует конфигурации не по умолчанию, но это все же возможно.

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

Разобравшись с этими двумя понятиями, давайте обсудим последний пункт. Некоторые СУБД поддерживают концепцию владения объектами. То есть, когда пользователь создает объект базы данных — базу данных или схему, таблицу или хранимую процедуру, — этот пользователь автоматически становится владельцем нового объекта. Владелец обычно имеет возможность изменять объекты, которыми он владеет, и предоставлять доступ к ним другим пользователям. Здесь важно то, что MySQL никоим образом не имеет концепции владения объектами.

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

Пользователи в MySQL также немного отличаются от пользователей в других базах данных, поскольку объект пользователя включает в себя список контроля доступа к сети (ACL). Обычно пользователь MySQL представлен не только своим именем, например bob, но и добавленным сетевым адресом, например bob@localhost. В этом конкретном примере определяется пользователь, к которому можно получить доступ только локально через loopback-интерфейс или соединение через сокет Unix. Мы коснемся этой темы позже, когда будем обсуждать синтаксис SQL для создания существующих пользователей и управления ими.

MySQL хранит всю информацию, связанную с пользователями и привилегиями, в специальных таблицах системной базы данных mysql, которые называются таблицами привилегий (grant tables). Мы поговорим об этой концепции более подробно в разделе «Таблицы грантов».

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

Пользователь root

Каждая установка MySQL поставляется с несколькими пользователями, установленными по умолчанию. К большинству из них вам никогда не нужно иметь дело, но есть один, который используется очень часто. Кто-то может даже сказать, что им злоупотребляют, но это не та дискуссия, которую мы хотим здесь вести. Пользователь, о котором мы говорим, — это вездесущий и всемогущий пользователь root. С тем же именем, что и у суперпользователя Unix и Linux по умолчанию, этот пользователь является именно таким в MySQL: пользователь, который по умолчанию может делать все что угодно.

Чтобы быть более конкретным, пользователь root@localhost, иногда называемый начальным пользователем (initial user). Часть localhost имени пользователя, как вы теперь знаете, ограничивает его использование только локальными соединениями. Когда вы устанавливаете MySQL, в зависимости от конкретного варианта MySQL и ОС вы можете получить доступ к root@localhost из корневой учетной записи ОС, просто выполнив команду mysql. В некоторых других случаях для этого пользователя будет сгенерирован временный пароль.

Начальный пользователь — не единственный суперпользователь, которого вы можете создать, как вы увидите в разделе «Привилегия SUPER». Хотя вы можете создать пользователя root@<ip> или даже пользователя root@%, мы настоятельно не рекомендуем вам этого делать, так как это уязвимость в системе безопасности, которую можно использовать. Не каждому серверу MySQL даже нужно прослушивать интерфейс, кроме loopback (то есть localhost), не говоря уже о том, чтобы иметь суперпользователя с именем по умолчанию, доступным для входа в систему. Конечно, вы можете установить безопасные пароли для всех пользователей и, вероятно, следует установить один для root, но, возможно, немного безопаснее не разрешать удаленный доступ суперпользователя, если это возможно.

Во всех смыслах и целях root@localhost — это обычный пользователь со всеми предоставленными привилегиями. Вы даже можете удалить его, что может произойти по ошибке. Потеря доступа к пользователю root@localhost является довольно распространенной проблемой при работе с MySQL. Возможно, вы установили пароль и забыли его, или вы унаследовали сервер, и вам не дали пароль, или что-то еще могло произойти. Мы рассмотрим процедуру восстановления забытого пароля для первоначального пользователя root@localhost в разделе «Изменение пароля root и небезопасный запуск». Если вы удалили своего последнего доступного суперпользователя, вам придется выполнить ту же процедуру, но создать нового пользователя вместо изменения существующего.

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

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

CREATE USER 
'bob'@'10.0.2.%' 
IDENTIFIED BY 'password'; 

Оператор SQL для создания пользователя

Определение пользователя и хоста

Спецификация пароля

Вот более сложный пример:

CREATE USER 
'bob'@'10.0.2.%' 
IDENTIFIED WITH mysql_native_password 
BY 'password' 
DEFAULT ROLE 'user_role' 
REQUIRE SSL 
AND CIPHER 'EDH-RSA-DES-CBC3-SHA' 
WITH MAX_USER_CONNECTIONS 10 
PASSWORD EXPIRE NEVER; 

Оператор SQL для создания пользователя

Определение пользователя и хоста

Спецификация плагина аутентификации

Строка аутентификации/пароль

Роль по умолчанию устанавливается после аутентификации и подключения пользователя

Требовать SSL для соединений для этого пользователя

Требовать конкретных шифров

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

Переопределить глобальные настройки срока действия пароля

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

Определение пользователя и хоста

Мы упоминали в разделе «Понимание пользователей и привилегий», что пользователи в MySQL определяются не только по имени, но и по имени хоста. В предыдущем примере пользователь — 'bob'@'10.0.2.%', где bob — имя пользователя, а 10.0.2.% — спецификация имени хоста. По сути, это спецификация имени хоста с подстановочным знаком. Каждый раз, когда кто-то подключается к имени пользователя bob, используя TCP, MySQL делает несколько вещей:

  1. Получает IP-адрес подключающегося клиента.

  2. Выполняет обратный DNS-поиск IP-адреса по имени хоста.

  3. Выполняет поиск DNS для этого имени хоста (чтобы убедиться, что обратный поиск не был скомпрометирован).

  4. Сравнивает имя хоста или IP-адрес со спецификацией имени хоста пользователя.

Доступ предоставляется только в том случае, если имена хостов совпадают. Для нашего пользователя bob соединение с IP-адреса 10.0.2.121 будет разрешено, а соединение с 10.0.3.22 — запрещено. Фактически, чтобы разрешить подключения с другого имени хоста, необходимо создать нового пользователя. Внутренне 'bob'@'10.0.2.%' — это совершенно другой пользователь, чем 'bob'@'10.0.3.%'. Также можно использовать полные доменные имена (FQDN) в спецификации имени хоста, например, 'bob'@'acme.com', но поиск в DNS требует времени, и обычно их полностью отключают при оптимизации.

Указание всех возможных имен хостов для подключения всех пользователей может быть утомительным, но это полезная функция для безопасности. Однако иногда база данных устанавливается за брандмауэром или просто нецелесообразно указывать имена хостов. Чтобы полностью разрушить эту систему, можно использовать один подстановочный знак в спецификации хоста, например, 'bob'@'10.0.2%'. Подстановочный знак '%' также используется, когда вы вообще не указываете хост ('bob'@'%').

Вы заметите, что мы заключили имя пользователя и спецификацию хоста в одинарные кавычки (''). Это не является обязательным, и спецификация имени пользователя и хоста подчиняется тому же набору правил, что и правила, изложенные для имен и псевдонимов таблиц и столбцов в разделах «Создание и использование баз данных» и «Псевдонимы». Например, при создании или изменении пользователя bob@localhost или bob@172.17.0.2 нет необходимости использовать кавычки. Однако вы не можете создать этого пользователя без кавычек: 'username with a space'@'172.%'. Двойные кавычки, одинарные кавычки или обратные кавычки могут использоваться для заключения имен пользователей и имен хостов со специальными символами.

Спецификация плагинов аутентификации

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

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

MySQL 8.0 использует подключаемый модуль caching_sha2_password по умолчанию, который обеспечивает более высокую безопасность и производительность по сравнению с устаревшим mysql_native_password, но не совместим со всеми клиентскими библиотеками. Чтобы изменить плагин по умолчанию, вы можете настроить переменную default_authentication_plugin, что приведет к созданию новых пользователей с указанным плагином.

Строка аутентификации/пароль

Некоторые плагины аутентификации, в том числе плагин по умолчанию, требуют, чтобы вы установили пароль для пользователя. Другие плагины, такие как PAM, требуют, чтобы вы определили сопоставление пользователей ОС с пользователями MySQL. auth_string будет использоваться в обоих случаях. Давайте посмотрим на пример сопоставления с PAM:

mysql> CREATE USER ''@'' IDENTIFIED WITH auth_pam
    -> AS 'mysqld, dba=dbausr, dev=devusr';
Query OK, 0 row affected (0.01 sec)

Здесь определено сопоставление, которое можно прочитать следующим образом: будет использоваться файл конфигурации PAM mysqld (обычно находится в /etc/pam.d/mysqld); Пользователи ОС с группой dba будут сопоставлены с пользователем MySQL dbausr, а пользователи ОС с группой dev — с devusr. Однако одного сопоставления недостаточно, поскольку необходимо назначить необходимые разрешения.

Обратите внимание, что для этого требуется либо подключаемый модуль Percona PAM, либо MySQL Enterprise Edition. В этом примере создается прокси-пользователь, которого мы кратко обсуждали в разделе «Знакомство с пользователями и привилегиями».

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

Набор ролей по умолчанию

Роли — довольно недавнее дополнение к MySQL. Вы можете думать о роли как о наборе привилегий. Мы обсуждаем их в разделе «Роли».

SSL-конфигурация

Вы можете заставить соединения определенных пользователей использовать SSL, передав команду REQUIRE SSL команде CREATE USER или ALTER USER. Незашифрованные соединения с пользователем будут запрещены. Кроме того, вы можете, как показано в приведенном нами примере, указать конкретный набор шифров или несколько наборов, которые можно использовать для этого пользователя. В идеале вы должны установить приемлемые наборы шифров на системном уровне, но установка этого на уровне пользователя полезна, чтобы разрешить некоторые менее безопасные наборы для определенных подключений. Вам не нужно указывать REQUIRE SSL, чтобы указать REQUIRE CIPHER, и в этом случае могут быть установлены незашифрованные соединения. Однако, если установлено зашифрованное соединение, оно будет использовать только указанный вами набор шифров:

mysql> CREATE USER 'john'@'192.168.%' IDENTIFIED BY 'P@ssw0rd#'
    -> REQUIRE CIPHER 'EDH-RSA-DES-CBC3-SHA';
Query OK, 0 row affected (0.02 sec)

Доступны следующие дополнительные настраиваемые параметры:

Х509

Принуждение клиента к предъявлению действительного сертификата. Этот, а также следующие варианты предполагают использование SSL.

ISSUER issuer

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

SUBJECT subject

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

Эти параметры можно комбинировать, чтобы указать очень конкретные требования к сертификату и шифрованию:

mysql> CREATE USER 'john'@'192.168.%'
    -> REQUIRE SUBJECT '/C=US/ST=NC/L=Durham/
    -> O=BI Dept certificate/
    -> CN=client/emailAddress=john@nonexistent.com'
    -> AND ISSUER '/C=US/ST=NC/L=Durham/
    -> O=MySQL/CN=CA/emailAddress=ca@nonexistent.com'
    -> AND CIPHER 'EDH-RSA-DES-CBC3-SHA';
Лимиты потребления ресурсов

Вы можете определить лимиты потребления ресурсов. В нашем примере мы ограничиваем максимальное количество одновременных подключений этого пользователя до 10. Этот и другие параметры ресурса по умолчанию равны 0, что означает неограниченность. Другими возможными ограничениями являются MAX_CONNECTIONS_PER_HOUR, MAX_QUERIES_PER_HOUR и MAX_UPDATES_PER_HOUR. Все эти параметры являются частью спецификации WITH.

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

mysql> CREATE USER 'john'@'192.168.%'
    -> WITH MAX_QUERIES_PER_HOUR 10
    -> MAX_CONNECTIONS_PER_HOUR 2
    -> MAX_USER_CONNECTIONS 1;

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

Переопределение параметров управления паролями

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

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

mysql> CREATE USER 'app_admin'@'192.168.%'
    -> IDENTIFY BY '...'
    -> WITH PASSWORD EXPIRE INTERVAL 30 DAY
    -> PASSWORD REUSE INTERVAL 180 DAY
    -> PASSWORD REQUIRE CURRENT
    -> FAILED_LOGIN_ATTEMPTS 3
    -> PASSWORD_LOCK_TIME 1;

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

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

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

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

Подключение и аутентификация

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

mysql> SELECT CURRENT_USER(), USER();
+----------------+----------------+
| CURRENT_USER() | USER()         |
+----------------+----------------+
| root@localhost | root@localhost |
+----------------+----------------+
1 row in set (0.00 sec)

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

Обеспечение безопасности хранимых объектов

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

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

Давайте посмотрим на пример. Вот простая хранимая процедура, которая возвращает текущего пользователя, использованного для авторизации, а также пользователя, прошедшего проверку подлинности. DEFINER установлен на 'bob'@'localhost':

DELIMITER ;;
CREATE DEFINER = 'bob'@'localhost' PROCEDURE test_proc()
BEGIN
    SELECT CURRENT_USER(), USER();
END;
;;
DELIMITER ;

Если эту процедуру выполняет пользователь john из предыдущих примеров, будет напечатан вывод, аналогичный следующему:

mysql> CALL test_proc();
+----------------+--------------------+
| CURRENT_USER() | USER()             |
+----------------+--------------------+
| bob@localhost  | john@192.168.1.174 |
+----------------+--------------------+
1 row in set (0.00 sec)

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

Прокси

Некоторые методы аутентификации, такие как PAM и LDAP, не работают с однозначным сопоставлением между аутентифицируемыми пользователями и пользователями базы данных. Ранее мы показали, как создать пользователя с аутентификацией PAM — давайте посмотрим, что увидит такой пользователь, если он запросит аутентификацию и предоставит пользователей:

mysql> SELECT CURRENT_USER(), USER();
+------------------+------------------------+
| CURRENT_USER()   | USER()                 |
+------------------+------------------------+
| dbausr@localhost | localdbauser@localhost |
+------------------+------------------------+
1 row in set (0.00 sec)

Прежде чем мы закроем этот раздел, мы должны поднять пару важных моментов, связанных с оператором CREATE USER. Во-первых, можно создать несколько учетных записей пользователей с помощью одной команды вместо индивидуального выполнения нескольких операторов CREATE USER. Во-вторых, если пользователь уже существует, CREATE USER не приведет к сбою, но изменит этого пользователя тонкими способами. Это может быть опасно. Чтобы избежать этого, вы можете указать для команды опцию IF NOT EXISTS. Тем самым вы указываете MySQL создать пользователя только в том случае, если такой пользователь еще не существует, и ничего не делать, если он существует.

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

Таблицы привилегий

MySQL хранит как информацию о пользователях, так и привилегии в виде записей в таблицах привилегий. Это специальные внутренние таблицы в базе данных mysql, которые в идеале никогда не должны изменяться вручную, а вместо этого неявно изменяются при выполнении таких операторов, как CREATE USER или GRANT. Например, вот немного усеченный вывод запроса SELECT к таблице mysql.user, которая содержит записи пользователей, включая их пароли (в хешированной форме):

mysql> SELECT * FROM user WHERE user = 'root'\G
*************************** 1. row ***************************
                    Host: localhost
                    User: root
             Select_priv: Y
             Insert_priv: Y
             Update_priv: Y
             Delete_priv: Y
...
     Create_routine_priv: Y
      Alter_routine_priv: Y
        Create_user_priv: Y
              Event_priv: Y
            Trigger_priv: Y
  Create_tablespace_priv: Y
                ssl_type:
              ssl_cipher: 0x
             x509_issuer: 0x
            x509_subject: 0x
           max_questions: 0
             max_updates: 0
         max_connections: 0
    max_user_connections: 0
                  plugin: mysql_native_password
   authentication_string: *E1206987C3E6057289D6C3208EACFC1FA0F2FA56
        password_expired: N
   password_last_changed: 2020-09-06 17:20:57
       password_lifetime: NULL
          account_locked: N
        Create_role_priv: Y
          Drop_role_priv: Y
  Password_reuse_history: NULL
     Password_reuse_time: NULL
Password_require_current: NULL
         User_attributes: NULL
1 row in set (0.00 sec)

Сразу видно, что многие поля напрямую соответствуют конкретным вызовам операторов CREATE USER или ALTER USER. Например, вы можете видеть, что для этого пользователя root не установлены какие-либо определенные правила, касающиеся жизненного цикла его пароля. Вы также можете увидеть довольно много привилегий, хотя мы опустили некоторые из них для краткости. Это привилегии, для которых не требуется цель, например таблица. Такие привилегии называются глобальными (global). Позже мы покажем вам, как просмотреть целевые привилегии.

Начиная с MySQL 8.0, другие таблицы привилегий:

mysql.user

Учетные записи пользователей, статические глобальные привилегии и другие непривилегированные столбцы

mysql.global_grants

Динамические глобальные привилегии

mysql.db

Привилегии уровня базы данных

mysql.tables_priv

Привилегии на уровне таблицы

mysql.columns_priv

Привилегии на уровне столбца

mysql.procs_priv

Привилегии хранимых процедур и функций

mysql.proxy_priv

Привилегии прокси-пользователя

mysql.default_roles

Роли пользователей по умолчанию

mysql.role_edges

Ребра для подграфов ролей

mysql.password_history

История смены пароля

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

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

Команды управления пользователями и ведение журнала

Есть некоторые прямые последствия того факта, что все команды, которые мы обсуждаем в этой главе, под капотом изменяют таблицы привилегий. В некоторых отношениях они близки к операциям DML. Они атомарны: любая операция CREATE USER, ALTER USER, GRANT или другая подобная операция либо завершается успешно, либо терпит неудачу, фактически не меняя своей цели. Они протоколируются: все изменения, сделанные для предоставления таблиц либо вручную, либо с помощью соответствующих команд, регистрируются в бинарном журнале. Таким образом, они реплицируются (см. главу 13) и также будут доступны для восстановления на определенный момент времени (см. главу 10).

Применение этих операторов к источнику может нарушить репликацию, если целевой пользователь не существует в реплике. Поэтому мы рекомендуем, чтобы ваши реплики согласовывались с их источниками не только в данных, но и в пользователях и других метаданных. Конечно, это только «meta» в том смысле, что она существует вне ваших реальных данных приложения; пользователи являются записями в таблице mysql.user, и это следует помнить при настройке репликации.

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

Хотя мы говорим, что выполнение CREATE USER похоже на INSERT в таблицу mysql.user, само выражение CREATE USER не изменяется никоим образом перед записью в журнал. Это верно для бинарного журнала, журнала медленных запросов (с оговоркой), общего журнала запросов и журналов аудита. То же верно и для любой другой операции, обсуждаемой в этой главе. Предостережение относительно журнала медленных запросов заключается в том, что для регистрации здесь административных операторов необходимо включить дополнительную серверную опцию log_slow_admin_statements.

Рассмотрим следующий пример:

mysql> CREATE USER 'vinicius' IDENTIFIED BY '...';
Query OK, 0 rows affected (0.02 sec)

Вот пример того, как одна и та же команда CREATE USER отражается в общем, медленном запросе и бинарном журнале:

Общий журнал (General log)
2020-11-22T15:53:17.354270Z       29 Query
    CREATE USER 'vinicius'@'%' IDENTIFIED BY <secret>
Журнал медленных запросов (Slow query log)
# Time: 2020-11-22T15:53:17.368010Z
# User@Host: root[root] @ localhost [] Id:   29
# Query_time: 0.013772 Lock_time: 0.000161 Rows_sent: 0 Rows_examined: 0
SET timestamp=1606060397;
CREATE USER 'vinicius'@'%' IDENTIFIED BY <secret>;
Бинарный журнал (Binary log)
#201122 18:53:17 server id 1 end_log_pos 4113 CRC32 0xa12ac622
    Query thread_id=29   exec_time=0    error_code=0   Xid = 243
SET TIMESTAMP=1606060397.354238/*!*/;
CREATE USER 'vinicius'@'%' IDENTIFIED WITH 'caching_sha2_password'
    AS '$A$005$|v>\ZKe^R...'
/*!*/;

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

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

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

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

Любой параметр, который можно установить во время создания пользователя, также можно позже изменить. Обычно это достигается с помощью команды ALTER USER. В MySQL версии 5.7 и ранее также были команды RENAME USER и SET PASSWORD, тогда как в версии 8.0 этот список был расширен, включив команду SET DEFAULT ROLE (мы рассмотрим систему ролей в разделе «Роли»). Обратите внимание, что ALTER USER можно использовать для изменения всех сведений о пользователе, а другие команды — это просто удобные способы выполнения общих операций обслуживания.

Мы начнем с обычной команды ALTER USER. В первом примере мы собираемся изменить используемый плагин аутентификации. Многие старые программы не поддерживают новый и стандартный в MySQL 8.0 плагин caching_sha2_password и требуют, чтобы вы либо создавали пользователей, используя старый плагин mysql_native_password, либо изменяли их после создания, чтобы использовать этот плагин. Мы можем проверить текущий используемый плагин, запросив одну из таблиц привилегий (см. «Таблицы привилегий» для получения дополнительной информации о них):

mysql> SELECT plugin FROM mysql.user WHERE
    -> user = 'bob' AND host = 'localhost';
+-----------------------+
| plugin                |
+-----------------------+
| caching_sha2_password |
+-----------------------+
1 row in set (0.00 sec)

И теперь мы можем изменить плагин для этого пользователя и убедиться, что изменение отражено:

mysql> ALTER USER 'bob'@'localhost' IDENTIFIED WITH mysql_native_password;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT plugin FROM mysql.user WHERE
    -> user = 'bob' AND host = 'localhost';
+-----------------------+
| plugin                |
+-----------------------+
| mysql_native_password |
+-----------------------+
1 row in set (0.00 sec)

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

Свойства, которые может изменить ALTER USER, довольно многочисленны, и они были объяснены или, по крайней мере, описаны в разделе «Создание и использование новых пользователей». Однако есть некоторые часто требуемые операции, о которых вам следует знать немного больше:

Изменение пароля пользователя

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

mysql> ALTER USER 'bob'@'localhost' IDENTIFIED by 'new password';
Query OK, 0 rows affected (0.01 sec)

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

mysql> SET PASSWORD = 'new password';
Query OK, 0 rows affected (0.01 sec)

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

mysql> SET PASSWORD FOR 'bob'@'localhost' = 'new password';
Query OK, 0 rows affected (0.01 sec)
Блокировка и разблокировка пользователя

Если вам нужно временно (или навсегда) заблокировать доступ к определенному пользователю, вы можете сделать это с помощью параметра ACCOUNT LOCK в ALTER USER. Пользователь в этом случае блокируется только для аутентификации. Хотя никто не сможет подключиться к MySQL в качестве заблокированного пользователя, его все же можно использовать как в качестве прокси, так и в предложении DEFINER. Это делает таких пользователей немного более безопасными и более простыми в управлении. Параметр ACCOUNT LOCK также может быть использован, например, для блокировки трафика от приложения, подключающегося от имени определенного пользователя, который создает чрезмерную нагрузку.

Мы можем заблокировать аутентификацию пользователя bob, используя следующую команду:

mysql> ALTER USER 'bob'@'localhost' ACCOUNT LOCK;
Query OK, 0 rows affected (0.00 sec)

Будут затронуты только новые соединения. Сообщение, которое MySQL выдает в этом случае, ясно:

$ mysql -ubob -p
Enter password:
ERROR 3118 (HY000): Access denied for user 'bob'@'localhost'.
Account is locked.

Аналогом ACCOUNT LOCK является ACCOUNT UNLOCK. Этот параметр ALTER USER делает именно то, что говорит. Давайте снова разрешим доступ для bob:

mysql> ALTER USER 'bob'@'localhost' ACCOUNT UNLOCK;
Query OK, 0 rows affected (0.01 sec)

Теперь попытка подключения будет успешной:

$ mysql -ubob -p
Enter password:
mysql>
Срок действия пароля пользователя

Вместо того, чтобы полностью блокировать доступ пользователя или менять для него пароль, вы можете заставить его самого сменить пароль. Это возможно в MySQL с опцией PASSWORD EXPIRE команды ALTER USER. После выполнения этой команды пользователь по-прежнему сможет подключаться к серверу, используя предыдущий пароль. Однако, как только он запустит запрос из нового соединения, то есть как только его привилегии будут проверены, пользователь получит сообщение об ошибке и будет вынужден сменить пароль. Существующие соединения не затрагиваются.

Давайте посмотрим, как это выглядит для пользователя. Во-первых, сама команда:

mysql> ALTER USER 'bob'@'localhost' PASSWORD EXPIRE;
Query OK, 0 rows affected (0.01 sec)

Теперь о том, что получает пользователь. Обратите внимание на успешную аутентификацию:

$ mysql -ubob -p
Enter password:
mysql> SELECT id, data FROM bobs_db.bobs_private_table;
ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.

Несмотря на то, что в сообщении об ошибке указано, что вы должны запустить ALTER USER, теперь вы знаете, что вместо этого вы можете использовать SET PASSWORD. Также не имеет значения, кто меняет пароль: рассматриваемый пользователь или другой пользователь. Параметр PASSWORD EXPIRE просто заставляет изменить пароль. Если другой пользователь изменит пароль, сеансы, аутентифицированные со старым паролем после истечения срока действия пароля, необходимо будет открыть повторно. Как мы видели ранее, аутентифицированный пользователь может изменить пароль без указания пользователя, для которого это делается, и он сможет продолжать свою сессию в обычном режиме (однако новые подключения должны будут аутентифицироваться с новым паролем):

mysql> SET PASSWORD = 'new password';
Query OK, 0 rows affected (0.06 sec)
mysql> SELECT id, data FROM bobs_db.bobs_private_table;
Empty set (0.00 sec)

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

Переименование пользователя

Смена имени пользователя — относительно нечастая операция, но иногда необходимая. Для этой операции есть специальная команда: RENAME USER. Для этого требуется привилегия CREATE USER или привилегия UPDATE для базы данных mysql или только для таблиц грантов. Для этой команды нет альтернативы ALTER USER.

Вы можете изменить как часть «name» имени пользователя, так и часть «host». Поскольку, как вы уже знаете, часть «host» действует как файрволл, будьте осторожны при ее изменении, так как вы можете вызвать сбои (на самом деле, то же самое относится и к части «name»). Давайте переименуем нашего пользователя bob во что-то более формальное:

mysql> RENAME USER 'bob'@'localhost' TO 'robert'@'172.%';
Query OK, 0 rows affected, 1 warning (0.01 sec)

Когда имя пользователя изменяется, MySQL автоматически просматривает свои внутренние таблицы, чтобы увидеть, был ли этот пользователь назван в предложении DEFINER представления или сохраненного объекта. Всякий раз, когда это так, выдается предупреждение. Поскольку мы получили предупреждение, когда переименовывали bob, давайте проверим его:

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Warning
   Code: 4005
Message: User 'bob'@'localhost' is referenced as a definer account in a stored routine.
1 row in set (0.00 sec)

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

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

Заключительной частью жизненного цикла пользователя базы данных является конец его жизни. Как и любой объект базы данных, пользователи могут быть удалены. В MySQL для этого используется команда DROP USER. Это одна из самых простых команд, обсуждаемых в этой главе, а возможно, и во всей книге. DROP USER принимает пользователя или, опционально, список пользователей в качестве аргумента и имеет единственный модификатор: IF NOT EXISTS. Успешное выполнение команды безвозвратно удаляет относящуюся к пользователю информацию из таблиц привилегий (с оговоркой, которую мы обсудим позже) и, таким образом, предотвращает дальнейшие входы в систему.

Если вы удаляете пользователя, который выполнил одно или несколько подключений к базе данных, которая все еще открыта, даже если удаление прошло успешно, связанные записи будут удалены только после завершения последнего из этих подключений. Следующая попытка соединения с данным пользователем приведет к сообщению ERROR 1045 (28000): Access denied message.

Модификатор IF NOT EXISTS работает аналогично CREATE USER: если пользователь, которого вы указываете в команде DROP USER, не существует, ошибка не будет возвращена. Это полезно в автоматических сценариях. Если хост-часть имени пользователя не указана, по умолчанию используется подстановочный знак %.

В самом общем виде команда DROP USER выглядит так:

mysql> DROP USER 'jeff'@'localhost';
Query OK, 0 rows affected (0.02 sec)

Повторное выполнение той же команды приведет к ошибке:

mysql> DROP USER 'jeff'@'localhost';
ERROR 1396 (HY000): Operation DROP USER failed for 'jeff'@'localhost'

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

mysql> DROP USER IF EXISTS 'jeff'@'localhost';
Query OK, 0 rows affected, 1 warning (0.01 sec)
mysql> SHOW WARNINGS;
+-------+------+-----------------------------------------------------+
| Level | Code | Message                                             |
+-------+------+-----------------------------------------------------+
| Note | 3162 | Authorization ID 'jeff'@'localhost' does not exist.  |
+-------+------+-----------------------------------------------------+
1 row in set (0.01 sec)

Опять же, если вы не укажете хостовую часть имени пользователя, MySQL примет значение по умолчанию %. Также можно удалить нескольких пользователей одновременно:

mysql> DROP USER 'jeff', 'bob'@'192.168.%';
Query OK, 0 rows affected (0.01 sec)

В MySQL, поскольку пользователи не владеют объектами, их можно легко удалить без особой подготовки. Однако, как мы уже говорили, пользователи могут выполнять дополнительные роли. Если удаленный пользователь используется в качестве прокси-пользователя или является частью предложения DEFINER какого-либо объекта, то его удаление может привести к созданию потерянной записи. Всякий раз, когда удаляемый вами пользователь является частью таких отношений, MySQL выдает предупреждение. Обратите внимание, что команда DROP USER по-прежнему будет выполнена успешно, поэтому вы должны устранить возникающие несоответствия и исправить все потерянные записи:

mysql> DROP USER 'bob'@'localhost';
Query OK, 0 rows affected, 1 warning (0.01 sec)
mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Warning
   Code: 4005
Message: User 'bob'@'localhost' is referenced as a definer account in a stored routine.
1 row in set (0.00 sec)

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

mysql> CALL test.test_proc();
ERROR 1449 (HY000): The user specified as a definer ('bob'@'localhost') does not exist

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

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

mysql> CREATE USER 'bob'@'localhost' IDENTIFIED BY 'new password';
Query OK, 0 rows affected, 1 warning (0.01 sec)
mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Warning
   Code: 4005
Message: User 'bob'@'localhost' is referenced as a definer account in a stored routine.
1 row in set (0.00 sec)

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

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

mysql> SELECT table_schema, table_name FROM information_schema.columns
    -> WHERE column_name = 'DEFINER';
+--------------------+------------+
| TABLE_SCHEMA       | TABLE_NAME |
+--------------------+------------+
| information_schema | EVENTS     |
| information_schema | ROUTINES   |
| information_schema | TRIGGERS   |
| information_schema | VIEWS      |
+--------------------+------------+

Теперь, когда вы это знаете, вы можете легко создать запрос, чтобы проверить, указан ли пользователь, которого вы собираетесь удалить или уже удалили, в каких-либо предложениях DEFINER:

SELECT EVENT_SCHEMA AS obj_schema
    , EVENT_NAME obj_name
    , 'EVENT' AS obj_type
FROM INFORMATION_SCHEMA.EVENTS
WHERE DEFINER = 'bob@localhost'
UNION
SELECT ROUTINE_SCHEMA AS obj_schema
    , ROUTINE_NAME AS obj_name
    , ROUTINE_TYPE AS obj_type
FROM INFORMATION_SCHEMA.ROUTINES
WHERE DEFINER = 'bob@localhost'
UNION
SELECT TRIGGER_SCHEMA AS obj_schema
    , TRIGGER_NAME AS obj_name
    , 'TRIGGER' AS obj_type
FROM INFORMATION_SCHEMA.TRIGGERS
WHERE DEFINER = 'bob@localhost'
UNION
SELECT TABLE_SCHEMA AS obj_scmea
    , TABLE_NAME AS obj_name
    , 'VIEW' AS obj_type
FROM INFORMATION_SCHEMA.VIEWS
WHERE DEFINER = 'bob@localhost';

Этот запрос может показаться пугающим, но к настоящему моменту вы уже должны были видеть, что UNION используется в «Объединение (UNION)», и запрос представляет собой просто объединение четырех простых запросов. Каждый отдельный запрос ищет объект со значением DEFINER для bob@localhost в одной из следующих таблиц: EVENTS, ROUTINES, TRIGGERS и VIEWS.

В нашем примере запрос возвращает одну запись для bob@localhost:

+------------+-----------+-----------+
| obj_schema | obj_name | obj_type   |
+------------+-----------+-----------+
| test       | test_proc | PROCEDURE |
+------------+-----------+-----------+

Точно так же легко проверить, была ли предоставлена привилегия прокси для этого пользователя:

mysql> SELECT user, host FROM mysql.proxies_priv
    -> WHERE proxied_user = 'bob'
    -> AND proxied_host = 'localhost';
+------+------+
| user | host |
+------+------+
| jeff | %    |
+------+------+

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

Привилегии

Когда пользователь подключается к серверу MySQL, аутентификация выполняется с использованием имени пользователя и информации о хосте, как объяснялось ранее. Однако разрешения, необходимые пользователю для выполнения различных действий, не проверяются перед выполнением каких-либо команд. MySQL предоставляет привилегии в соответствии с идентификатором подключенного пользователя и выполняемыми им действиями. Как обсуждалось в начале этой главы, привилегии — это наборы разрешений на выполнение действий над различными объектами. По умолчанию пользователь не имеет никаких разрешений, и, следовательно, ему не назначаются привилегии после выполнения CREATE USER.

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

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

Глобальные привилегии

Эти привилегии позволяют получателю (пользователю, которому предоставлена привилегия — мы рассмотрим команду GRANT в разделе «Команды управления привилегиями») либо воздействовать на каждый объект в каждой базе данных, либо воздействовать на кластер в целом. Последнее относится к командам, которые обычно считаются административными. Например, вы можете разрешить пользователю завершить работу кластера.

Привилегии этой категории хранятся в таблицах mysql.user и mysql.global_grants. В первой хранятся обычные статические привилегии, а во второй — динамические привилегии. Разница объясняется в следующем разделе. Версии MySQL до 8.0 хранят все глобальные привилегии в mysql.user.

Ппривилегии базы данных

Привилегии, предоставленные на уровне базы данных, позволят пользователю воздействовать на объекты в этой базе данных. Как вы понимаете, на этом уровне список привилегий уже, так как нет особого смысла разбивать привилегию SHUTDOWN, например, ниже глобального уровня. Записи для этих привилегий хранятся в таблице mysql.db и включают возможность запуска запросов DDL и DML в целевой базе данных.

Привилегии объекта

Логическое продолжение привилегий уровня базы данных, они нацелены на конкретный объект. Отслеживаемые в mysql.tables_priv, mysql.procs_priv и mysql.proxies_priv, они соответственно охватывают таблицы и представления, все типы хранимых подпрограмм и, наконец, разрешения пользователей прокси. Привилегии прокси-сервера являются особыми, но другие привилегии в этой категории снова являются обычными разрешениями DDL и DML.

Привилегии столбца

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

Полный список привилегий, честно говоря, очень длинный. Всегда рекомендуется обращаться к документации MySQL для вашей конкретной версии для получения полной информации. Вы должны помнить, что любое действие, которое может выполнить пользователь, либо будет иметь назначенную специальную привилегию, либо будет покрыто привилегией, управляющей более широким диапазоном поведения. Как правило, привилегии уровня базы данных и объекта будут иметь выделенное имя привилегии, которое вы можете предоставить (UPDATE, SELECT и т. д.), а глобальные привилегии будут довольно широко сгруппированы вместе, позволяя выполнять несколько действий одновременно. Например, привилегия GROUP_REPLICATION_ADMIN позволяет выполнять пять различных действий одновременно. Глобальные привилегии также обычно предоставляются на системном уровне (объект . (точка)).

Вы всегда можете получить доступ к списку привилегий, доступных в вашем экземпляре MySQL, выполнив команду SHOW PRIVILEGES:

mysql> SHOW PRIVILEGES;
+----------------------------+----------------------+--------------------+
| Privilege                  | Context              | Comment            |
+----------------------------+----------------------+--------------------+
| Alter                      | Tables               | To alter the table |
| Alter routine              | Functions,Procedures | ...                |
| ...                                                                    |
| REPLICATION_SLAVE_ADMIN    | Server Admin         |                    |
| AUDIT_ADMIN                | Server Admin         |                    |
+----------------------------+----------------------+--------------------+
58 rows in set (0.00 sec)

Статические и динамические привилегии

Прежде чем мы перейдем к рассмотрению команд, используемых для управления привилегиями в MySQL, мы должны сделать паузу и поговорить о важном различии. В MySQL, начиная с версии 8.0, есть два типа привилегий: статические и динамические. Статические привилегии встроены в сервер, и они будут доступны при каждой установке. Динамические привилегии, с другой стороны, «изменчивы»: их постоянное присутствие не гарантируется.

Что это за динамические привилегии? Это привилегии, которые регистрируются на сервере во время выполнения. Могут быть предоставлены только зарегистрированные привилегии, поэтому возможно, что некоторые привилегии никогда не будут зарегистрированы и никогда не будут предоставлены. Все это — причудливый способ сказать, что теперь можно расширять привилегии с помощью плагинов и компонентов. Однако большинство доступных в настоящее время динамических привилегий регистрируются по умолчанию в обычной установке Community Server.

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

Привилегия SUPER

Этот раздел не длинный, но важный. Мы упоминали в разделе «Пользователь root», что при каждой установке MySQL по умолчанию создается суперпользователь: root@localhost. Иногда вы можете захотеть предоставить те же возможности другому пользователю, например, администратору баз данных. Удобный встроенный способ сделать это — использовать специальную привилегию SUPER.

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

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

Использование привилегии SUPER сопряжено с теми же рисками и приводит к тем же проблемам, что и использование пользователя root@localhost по умолчанию. Вместо тщательного изучения объема требуемых привилегий мы прибегаем к использованию универсального молотка для решения всех проблем. Основная проблема с SUPER — его всеобъемлющий охват. Когда вы создаете суперпользователя, вы создаете ответственность: пользователь должен быть ограничен и в идеале проверен, а операторы и программы, аутентифицирующие пользователя, должны быть предельно точными и осторожными в своих действиях. С большой силой приходит большая ответственность — и, среди прочего, возможность просто полностью завершить работу экземпляра MySQL. Представьте, что вы делаете это по ошибке.

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

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

Иногда вам все равно понадобится полноценный администратор баз данных, который может делать все что угодно. Вместо предоставления SUPER рассмотрите возможность просмотра привилегий root@localhost и их копирования. Мы покажем вам, как это сделать, в разделе «Проверка привилегий». Продолжая это, вы можете пропустить предоставление некоторых привилегий, таких как привилегия INNODB_REDO_LOG_ENABLE, которая разрешает пользователю в основном включать аварийно-небезопасный режим. Гораздо безопаснее вообще не предоставлять эту привилегию и иметь возможность предоставить ее себе, когда это абсолютно необходимо, чем подвергать риску того, что кто-то по ошибке может запустить это выражение.

Команды управления привилегиями

Теперь, когда вы немного знаете о привилегиях, мы можем перейти к основным командам, которые позволяют вам ими управлять. Вы никогда не сможете использовать ALTER как привилегию самостоятельно, поэтому под контролем привилегий здесь мы подразумеваем предоставление их пользователям и удаление их у пользователей. Эти действия выполняются с помощью операторов GRANT и REVOKE.

GRANT

Оператор GRANT используется для предоставления пользователям (или ролям) разрешений на выполнение действий либо в целом, либо с конкретными объектами. Этот же оператор можно также использовать для назначения ролей пользователям, но вы не можете одновременно изменять разрешения и назначать роли. Чтобы иметь возможность предоставлять разрешение (привилегию), вам необходимо назначить эту привилегию себе и иметь специальную привилегию GRANT OPTION (мы рассмотрим ее позже). Пользователи с привилегией SUPER (или новее CONNECTION_ADMIN) могут предоставлять что угодно, и есть особое условие, связанное с таблицами предоставления, которое мы вскоре обсудим.

А пока давайте проверим базовую структуру оператора GRANT:

mysql> GRANT SELECT ON app_db.* TO 'john'@'192.168.%';
Query OK, 0 row affected (0.01 sec)

Этот оператор после выполнения сообщает MySQL, что пользователю 'john'@'192.168.%' разрешено выполнять запросы только для чтения (SELECT) к любой таблице в базе данных app_db. Обратите внимание, что мы использовали подстановочный знак в спецификации объекта. Мы могли бы разрешить конкретному пользователю доступ к каждой таблице каждой базы данных, указав также подстановочный знак для базы данных:

mysql> GRANT SELECT ON *.* TO 'john';
Query OK, 0 row affected (0.01 sec)

В предыдущем вызове явно отсутствует спецификация хоста для пользователя 'john'. Это сокращение означает 'john'@'%'; таким образом, это не будет тот же пользователь, что и 'john'@'192.168.%', которого мы использовали ранее. Говоря о подстановочных знаках и пользователях, невозможно указать подстановочный знак для имени пользователя. Вместо этого вы можете указать несколько пользователей или ролей за один раз, например:

mysql> GRANT SELECT ON app_db.* TO 'john'@'192.168.%',
    -> 'kate'@'192.168.%';
Query OK, 0 row affected (0.06 sec)

Мы предупреждали вас о предоставлении слишком большого количества привилегий в предыдущем разделе, но может быть полезно помнить, что существует команда ALL, который позволяет вам предоставлять все возможные привилегии для объекта или набора объектов. Это может пригодиться, когда вы определяете разрешения для пользователя «owner» (владелец) — например, пользователя приложения для чтения/записи:

mysql> GRANT ALL ON app_db.* TO 'app_db_user';
Query OK, 0 row affected (0.06 sec)

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

Интересное свойство команды GRANT состоит в том, что она не проверяет наличие разрешенных вами объектов. То есть подстановочный знак не расширяется, а остается подстановочным знаком навсегда. Независимо от того, сколько новых таблиц будет добавлено в базу данных app_db, и john, и kate смогут выполнять для них операторы SELECT. Более ранние версии MySQL также создавали пользователя, которому предоставлялись привилегии, если он не был найден в таблице mysql.user, но такое поведение устарело, начиная с MySQL 8.0.

Как мы подробно обсуждали в разделе «Таблицы привилегий», оператор GRANT обновляет таблицы грантов. Одна вещь, которая следует из факта обновления таблиц привилегий, заключается в том, что если у пользователя есть привилегия UPDATE для этих таблиц, этот пользователь может предоставить любой учетной записи любую привилегию. Будьте предельно осторожны с разрешениями на объекты в базе данных mysql: от предоставления пользователям каких-либо привилегий мало пользы. Также обратите внимание, что когда включена системная переменная read_only, любой грант требует привилегий суперпользователя (SUPER или CONNECTION_ADMIN).

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

mysql> GRANT SELECT(id), INSERT(id, data)
    -> ON bobs_db.bobs_private_table TO 'kate'@'192.168.%';
Query OK, 0 rows affected (0.01 sec)

Пользователь kate теперь сможет выполнить команду SELECT id FROM bobs_db.bobs_private_table, но не SELECT * или SELECT data.

Наконец, вы можете предоставить каждую статическую привилегию для конкретного объекта или глобально, запустив GRANT ALL PRIVILEGES вместо указания каждой привилегии по очереди. ALL PRIVILEGES — это просто сокращение и само по себе не является особой привилегией, в отличие, например, от SUPER.

REVOKE

Оператор REVOKE противоположен оператору GRANT: его можно использовать для отзыва привилегий и ролей, назначенных с помощью GRANT. Если не указано иное, каждое свойство GRANT применяется к REVOKE. Например, для отзыва привилегий вам необходимо иметь привилегию GRANT OPTION и конкретные привилегии, которые вы отзываете.

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

mysql> GRANT SELECT ON *.* TO 'bi_app_user';
Query OK, 0 rows affected (0.03 sec)

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

mysql> REVOKE SELECT ON restricted_database.* FROM 'bi_app_user';
Query OK, 0 rows affected (0.03 sec)

До версии 8.0.16 для достижения этого вам нужно было вернуться к явному запуску GRANT SELECT для каждой отдельной разрешенной схемы.

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

mysql> REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'john'@'192.168.%';
Query OK, 0 rows affected (0.03 sec)

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

Проверка привилегий

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

mysql> SHOW GRANTS FOR 'john'@'192.168.%';
+--------------------------------------------------+
| Grants for john@192.168.%                        |
+--------------------------------------------------+
| GRANT UPDATE ON *.* TO `john`@`192.168.%`        |
| GRANT SELECT ON `sakila`.* TO `john`@`192.168.%` |
+--------------------------------------------------+
2 rows in set (0.00 sec)

В общем, вы можете ожидать увидеть все привилегии в этом выводе, но есть особый случай. Когда у пользователя есть все статические привилегии, предоставленные для определенного объекта, вместо того, чтобы перечислять каждую из них, MySQL вместо этого выводит ALL PRIVILEGES. Это не особая привилегия сама по себе, а скорее сокращение для всех возможных привилегий. Внутри ALL PRIVILEGES просто преобразуется в Y, установленный для каждой привилегии в соответствующей таблице привилегий:

mysql> SHOW GRANTS FOR 'bob'@'localhost';
+----------------------------------------------------------+
| Grants for bob@localhost                                 |
+----------------------------------------------------------+
...
| GRANT ALL PRIVILEGES ON `bobs_db`.* TO `bob`@`localhost` |
...

Вы также можете просмотреть права, предоставленные ролям, с помощью команды SHOW GRANTS, но об этом подробнее мы поговорим в разделе «Роли». Чтобы просмотреть разрешения текущего аутентифицированного и авторизованного пользователя, вы можете использовать любое из следующих утверждений, которые являются синонимами:

SHOW GRANTS;
SHOW GRANTS FOR CURRENT_USER;
SHOW GRANTS FOR CURRENT_USER();

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

Иногда вам может потребоваться просмотреть привилегии, относящиеся ко всем учетным записям, или перенести эти привилегии в другую систему. Один из вариантов, который у вас есть, — это использовать команду mysqldump, поставляемую с сервером MySQL для всех поддерживаемых платформ. Мы подробно рассмотрим эту команду в Главе 10. Короче говоря, вам нужно будет сделать дамп всех таблиц привилегий, иначе вы можете пропустить некоторые разрешения. Самый безопасный способ — сбросить все данные в базу данных mysql:

$ mysqldump -uroot -p mysql
Enter password:

Вывод будет включать все определения таблиц, а также множество операторов INSERT. Этот вывод можно перенаправить в файл, а затем использовать для заполнения новой базы данных. Мы поговорим об этом подробнее в главе 10. Если версии вашего сервера не совпадают или на целевом сервере уже сохранены некоторые пользователи и привилегии, лучше не удалять существующие объекты. Добавьте параметр --no-create-info к вызову mysqldump, чтобы получать только операторы INSERT.

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

--
-- Dumping data for table `tables_priv`
--

LOCK TABLES `tables_priv` WRITE;
/*!40000 ALTER TABLE `tables_priv` DISABLE KEYS */;
INSERT INTO `tables_priv` VALUES ('172.%','sakila','robert'...
'Select,Insert,Update,Delete,Create,Drop,Grant,References,...
('localhost','sys','mysql.sys','sys_config','root@localhost' '2020-07-13 07:14:57','Select','');
/*!40000 ALTER TABLE `tables_priv` ENABLE KEYS */;
UNLOCK TABLES;

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

Еще один способ — запустить SHOW GRANTS для каждого пользователя в базе данных. Объединив это с оператором SHOW CREATE USER, вы можете создать список привилегий, который также можно использовать для воссоздания пользователей и их привилегий в другой базе данных:

mysql> SELECT CONCAT("SHOW GRANTS FOR `", user, "`@`", host,
    -> "`; SHOW CREATE USER `", user, "`@`", host, "`;") grants
    -> FROM mysql.user WHERE user = "bob";
+----------------------------------------------------------------+
| grants
|
+----------------------------------------------------------------+
| SHOW GRANTS FOR bob@%; SHOW CREATE USER bob@%;
|
| SHOW GRANTS FOR bob@localhost; SHOW CREATE USER bob@localhost; |
+----------------------------------------------------------------+
2 rows in set (0.00 sec)

Как вы понимаете, идея автоматизации этой процедуры не нова. На самом деле, в Percona Toolkit есть инструмент — pt-show-grants — который делает именно это и многое другое. К сожалению, этот инструмент официально можно использовать только в Linux и он может вообще не работать на любой другой платформе:

$ pt-show-grants
-- Grants dumped by pt-show-grants
-- Dumped from server Localhost via Unix socket, MySQL 8.0.22 at 2020-12-12 14:32:33
-- Roles
CREATE ROLE IF NOT EXISTS `application_ro`;
-- End of roles listing
...
-- Grants for 'robert'@'172.%'
CREATE USER IF NOT EXISTS 'robert'@'172.%';
ALTER USER 'robert'@'172.%' IDENTIFIED WITH 'mysql_native_password'
AS '*E1206987C3E6057289D6C3208EACFC1FA0F2FA56' REQUIRE NONE
PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK PASSWORD HISTORY DEFAULT
PASSWORD REUSE INTERVAL DEFAULT PASSWORD REQUIRE CURRENT DEFAULT;
GRANT ALL PRIVILEGES ON `bobs_db`.* TO `robert`@`172.%`;
GRANT ALL PRIVILEGES ON `sakila`.`actor` TO `robert`@`172.%` WITH GRANT OPTION;
GRANT SELECT ON `sakila`.* TO `robert`@`172.%` WITH GRANT OPTION;
GRANT SELECT ON `test`.* TO `robert`@`172.%` WITH GRANT OPTION;
GRANT USAGE ON *.* TO `robert`@`172.%`;

Привилегия GRANT OPTION

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

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

mysql> CREATE TABLE bobs_db.bobs_private_table
    -> (id SERIAL PRIMARY KEY, data TEXT);
Query OK, 0 rows affected (0.04 sec)

Оператор, использующий пользователя bob, хочет разрешить пользователю john читать данные во вновь созданной таблице, но, увы, это невозможно:

mysql> GRANT SELECT ON bobs_db.bobs_private_table TO 'john'@'192.168.%';
ERROR 1142 (42000): SELECT, GRANT command denied to user 'bob'@'localhost' for table 'bobs_private_table'

Давайте проверим, какие привилегии на самом деле есть у пользователя bob:

mysql> SHOW GRANTS FOR 'bob'@'localhost';
+----------------------------------------------------------+
| Grants for bob@localhost                                 |
+----------------------------------------------------------+
| GRANT USAGE ON *.* TO `bob`@`localhost`                  |
| GRANT SELECT ON `sakila`.* TO `bob`@`localhost`          |
| GRANT ALL PRIVILEGES ON `bobs_db`.* TO `bob`@`localhost` |
+----------------------------------------------------------+
3 rows in set (0.00 sec)

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

mysql> GRANT SELECT ON bobs_db.* TO 'bob'@'localhost'
    -> WITH GRANT OPTION;
Query OK, 0 rows affected (0.01 sec)

Обратите внимание на дополнение WITH GRANT OPTION. Это именно та привилегия, которую мы искали. Эта опция позволит пользователю bob передавать свои привилегии другим пользователям. Давайте подтвердим это, снова запустив оператор GRANT SELECT от имени пользователя bob:

mysql> GRANT SELECT ON bobs_db.bobs_private_table TO 'john'@'192.168.%';
Query OK, 0 rows affected (0.02 sec)

Как и ожидалось, запрос был принят и выполнен. Однако остается сделать несколько уточнений. Во-первых, мы можем захотеть узнать, насколько детализирована привилегия GRANT OPTION. То есть, что именно (помимо SELECT на bobs_private_table) bob может предоставить другим пользователям? SHOW GRANTS может четко ответить на этот вопрос:

mysql> SHOW GRANTS FOR 'bob'@'localhost';
+----------------------------------------------------------------------------+
| Grants for bob@localhost                                                   |
+----------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `bob`@`localhost`                                    |
| GRANT SELECT ON `sakila`.* TO `bob`@`localhost`                            |
| GRANT ALL PRIVILEGES ON `bobs_db`.* TO `bob`@`localhost` WITH GRANT OPTION |
+----------------------------------------------------------------------------+
3 rows in set (0.00 sec)

Это намного яснее. Мы видим, что WITH GRANT OPTION применяется к привилегиям, которые bob имеет в конкретной базе данных. Это важно помнить. Несмотря на то, что мы выполнили GRANT SELECT ... WITH GRANT OPTION, bob получил возможность предоставлять все привилегии, которые у него есть в базе данных bobs_db.

Во-вторых, мы можем захотеть узнать, можно ли отозвать только привилегию GRANT OPTION:

mysql> REVOKE GRANT OPTION ON bobs_db.* FROM 'bob'@'localhost';
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW GRANTS FOR 'bob'@'localhost';
+----------------------------------------------------------+
| Grants for bob@localhost                                 |
+----------------------------------------------------------+
| GRANT USAGE ON *.* TO `bob`@`localhost`                  |
| GRANT SELECT ON `sakila`.* TO `bob`@`localhost`          |
| GRANT ALL PRIVILEGES ON `bobs_db`.* TO `bob`@`localhost` |
+----------------------------------------------------------+
3 rows in set (0.00 sec)

Наконец, глядя на то, как можно отозвать GRANT OPTION, мы можем захотеть узнать, может ли он быть предоставлен сам по себе. Ответ — да, с оговоркой, которую мы покажем. Предоставим привилегию GRANT OPTION для доступа к базам данных sakila и test. Как видно из предыдущего вывода, у пользователя bob в настоящее время есть привилегия SELECT для sakila, но нет привилегий для базы данных test:

mysql> GRANT GRANT OPTION ON sakila.* TO 'bob'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> GRANT GRANT OPTION ON test.* TO 'bob'@'localhost';
Query OK, 0 rows affected (0.01 sec)

Оба запроса удались. Совершенно ясно, что именно bob может предоставить для sakila привилегию SELECT. Однако менее ясно, что произошло с test. Давайте проверим это:

mysql> SHOW GRANTS FOR 'bob'@'localhost';
+-------------------------------------------------------------------+
| Grants for bob@localhost                                          |
+-------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `bob`@`localhost`                           |
| GRANT SELECT ON `sakila`.* TO `bob`@`localhost` WITH GRANT OPTION |
| GRANT USAGE ON `test`.* TO `bob`@`localhost` WITH GRANT OPTION    |
| GRANT ALL PRIVILEGES ON `bobs_db`.* TO `bob`@`localhost`          |
+-------------------------------------------------------------------+
4 rows in set (0.00 sec)

Итак, привилегия GRANT OPTION сама по себе дает пользователю только привилегию USAGE, которая является спецификатором «без привилегий». Однако привилегию GRANT OPTION можно рассматривать как переключатель, и когда она «включена», она будет применяться к привилегиям, предоставленным пользователю bob в базе данных test:

mysql> GRANT SELECT ON test.* TO 'bob'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW GRANTS FOR 'bob'@'localhost';
+-------------------------------------------------------------------+
| Grants for bob@localhost                                          |
+-------------------------------------------------------------------+
...
| GRANT SELECT ON `test`.* TO `bob`@`localhost` WITH GRANT OPTION   |
...
+-------------------------------------------------------------------+
4 rows in set (0.00 sec)

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

mysql> GRANT INSERT ON sakila.actor
    -> TO 'bob'@'localhost' WITH GRANT OPTION;
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW GRANTS FOR 'bob'@'localhost';
+-------------------------------------------------------------------------+
| Grants for bob@localhost                                                |
+-------------------------------------------------------------------------+
...
| GRANT INSERT ON `sakila`.`actor` TO `bob`@`localhost` WITH GRANT OPTION |
+-------------------------------------------------------------------------+
5 rows in set (0.00 sec)

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

Роли

Роли, представленные в MySQL 8.0, представляют собой наборы привилегий. Они упрощают управление пользователями и привилегиями за счет группировки и «контейнеризации» необходимых разрешений. У вас может быть несколько разных пользователей DBA, которым нужны одинаковые разрешения. Вместо того, чтобы предоставлять привилегии индивидуально каждому из пользователей, вы можете создать роль, предоставить разрешения для этой роли и назначить эту роль пользователям. При этом вы также упрощаете управление, поскольку вам не нужно будет обновлять каждого пользователя по отдельности. Если вашим администраторам баз данных потребуется настроить свои привилегии, вы можете просто настроить роль.

Роли очень похожи на пользователей в том, как они создаются, хранятся и управляются. Чтобы создать роль, вам необходимо выполнить оператор CREATE ROLE [IF NOT EXISTS] role1[, role2[, role3 ...]]. Чтобы удалить роль, выполните оператор DROP ROLE [IF EXISTS] role1[, role2[, role3 ...]]. Когда вы удаляете роль, назначения этой роли всем пользователям удаляются. Привилегии, необходимые для создания роли: CREATE ROLE или CREATE USER. Чтобы удалить роль, требуется привилегия DROP ROLE или DROP USER. Как и в случае с командами управления пользователями, если установлен параметр read_only, для создания и удаления ролей дополнительно требуются права администратора. Привилегии прямого изменения таблиц привилегий позволяют пользователю изменять что угодно, как мы уже обсуждали.

Как и имена пользователей, имена ролей состоят из двух частей: самого имени и спецификации хоста. Если хост не указан, предполагается подстановочный знак %. Спецификация хоста для роли никоим образом не ограничивает ее использование. Причина в том, что роли хранятся точно так же, как и пользователи в таблице привилегий mysql.user. Как следствие, вы не можете иметь то же rolename@host, что и у существующего пользователя. Чтобы иметь роль с тем же именем, что и у существующего пользователя, укажите для роли другое имя хоста.

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

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

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

Команды GRANT PRIVILEGE и REVOKE PRIVILEGE

Мы рассмотрели эти команды в разделе «Команды управления привилегиями». Во всех смыслах и целях роли можно использовать точно так же, как пользователей с командами GRANT и REVOKE PRIVILEGE. То есть вы можете назначить роли все те же привилегии, что и пользователю, и отозвать их тоже.

GRANT role [, role ...] команде пользователя

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

mysql> CREATE ROLE 'application_rw';
Query OK, 0 rows affected (0.01 sec)
mysql> CREATE ROLE 'application_ro';
Query OK, 0 rows affected (0.00 sec)
mysql> GRANT ALL ON sakila.* TO 'application_rw';
Query OK, 0 rows affected (0.06 sec)
mysql> GRANT SELECT ON sakila.* TO 'application_ro';
Query OK, 0 rows affected (0.00 sec)

Теперь вы можете назначать эти роли произвольному количеству пользователей и менять роли только при необходимости. Здесь мы разрешаем нашему пользователю bob доступ только для чтения к базе данных sakila:

mysql> GRANT 'application_ro' TO 'bob'@'localhost';
Query OK, 0 rows affected (0.00 sec)

Вы также можете предоставить более одной роли в одном операторе.

Модификатор WITH ADMIN OPTION

Когда вы предоставляете роль пользователю, этому пользователю разрешается только активировать роль, но не изменять ее каким-либо образом. Этот пользователь не может предоставлять роль любому другому пользователю. Если вы хотите разрешить пользователю как изменять роль, так и предоставлять ее другим пользователям, вы можете указать WITH ADMIN OPTION в команде GRANT ROLE. Результат будет отражен в таблицах привилегий и будет виден в выводе команды SHOW GRANTS:

mysql> SHOW GRANTS FOR 'bob'@'localhost';
+-------------------------------------------------------------------+
| Grants for bob@localhost                                          |
+-------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `bob`@`localhost`                           |
| GRANT `application_ro`@`%` TO `bob`@`localhost` WITH ADMIN OPTION |
+-------------------------------------------------------------------+
2 rows in set (0.00 sec)
SHOW GRANTS и роли

Команда SHOW GRANTS, которую мы представили в разделе «Проверка привилегий», способна показать вам как назначенные роли, так и действующие разрешения с одной или несколькими активированными ролями. Это возможно, если добавить необязательный модификатор USING role. Здесь мы показываем действующие привилегии, которые будут у пользователя bob, как только будет активирована роль application_ro:

mysql> SHOW GRANTS FOR 'bob'@'localhost' USING 'application_ro';
+-------------------------------------------------------------------+
| Grants for bob@localhost                                          |
+-------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `bob`@`localhost`                           |
| GRANT SELECT ON `sakila`.* TO `bob`@`localhost`                   |
| GRANT `application_ro`@`%` TO `bob`@`localhost` WITH ADMIN OPTION |
+-------------------------------------------------------------------+
3 rows in set (0.00 sec)
Команда SET ROLE DEFAULT | NONE | ALL | ALL EXCEPT role [, role1 ...] | role [, role1 ...]

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

$ mysql -ubob
mysql> SELECT staff_id, first_name FROM sakila.staff;
ERROR 1142 (42000): SELECT command denied to user 'bob'@'localhost' for table 'staff'
mysql> SET ROLE 'application_rw';
ERROR 3530 (HY000): `application_rw`@`%` is not granted to `bob`@`localhost`
mysql> SET ROLE 'application_ro';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT staff_id, first_name FROM sakila.staff;
+----------+------------+
| staff_id | first_name |
+----------+------------+
|        1 | Mike       |
|        2 | Jon        |
+----------+------------+
2 rows in set (0.00 sec)

Только когда роль назначена, bob может использовать свои привилегии. Обратите внимание, что вы не можете использовать SET ROLE, чтобы назначить себе роль, на использование которой вы не авторизованы (через GRANT ROLE).

Команды UNSET ROLE нет, но есть несколько других расширений SET ROLE, допускающих такое поведение. Чтобы отключить каждую роль, запустите SET ROLE NONE. Пользователь также может вернуться к своему набору ролей по умолчанию, выполнив SET ROLE DEFAULT, или активировать все роли, к которым у него есть доступ, выполнив SET ROLE ALL. Если вам нужно установить подмножество ролей, которое не является ни значением по умолчанию, ни всеми, вы можете создать оператор SET ROLE ALL EXCEPT role [, role1 ...] и явно избежать задания одной или нескольких ролей.

Параметр пользователя DEFAULT ROLE

Когда вы запускаете CREATE USER или позже через ALTER USER, вы можете установить одну или несколько ролей по умолчанию для определенного пользователя. Эти роли будут неявно установлены после аутентификации пользователя, что избавит вас от оператора SET ROLE. Это удобно, например, для пользователей приложения, которые большую часть времени используют одну роль или известный набор ролей. Давайте установим application_ro как роль по умолчанию для пользователя bob:

$ mysql -uroot
mysql> ALTER USER 'bob'@'localhost' DEFAULT ROLE 'application_ro';
Query OK, 0 rows affected (0.02 sec)
$ mysql -ubob
mysql> SELECT CURRENT_ROLE();
+----------------------+
| CURRENT_ROLE()       |
+----------------------+
| `application_ro`@`%` |
+----------------------+
1 row in set (0.00 sec)

Как только bob@localhost войдет в систему, функция CURRENT_ROLE() вернет желаемое application_ro.

Обязательные роли

Каждому пользователю в базе данных можно неявно предоставить одну или несколько ролей. Это достигается установкой системного параметра required_roles (глобального по объему и динамического) в список ролей. Роли, предоставленные таким образом, не активируются до тех пор, пока не будет запущена команда SET ROLE. Отменить назначенные таким образом роли невозможно, но вы можете явно предоставить их пользователю. Роли, перечисленные в required_roles, нельзя удалить, пока они не будут удалены из настройки.

Автоматическая активация ролей

По умолчанию роли не активны до тех пор, пока не будет выполнена команда SET ROLE. Однако это поведение можно переопределить и автоматически активировать каждую роль, доступную пользователю после аутентификации. Это аналогично запуску SET ROLE ALL при входе в систему. Это поведение можно включить или отключить (по умолчанию), изменив системный параметр enable_all_roles_on_login (глобальный и динамический). Если для параметра enable_all_roles_on_login установлено значение ON, для каждого пользователя будут активированы предоставленные роли как явно, так и неявно (через mandatory_roles).

Каскадные разрешения ролей

Роли могут быть предоставлены ролям. Затем происходит то, что все разрешения предоставленной роли наследуются ролью получателя. Как только роль получателя активируется пользователем, вы можете считать, что этот пользователь активировал предоставленную роль. Давайте немного усложним наш пример. У нас будет роль приложения, которой предоставлены роли application_ro и application_rw. Сама роль application не имеет прямого назначения разрешений. Мы назначим роль application нашему пользователю bob и изучим результат:

mysql> CREATE ROLE 'application';
Query OK, 0 rows affected (0.01 sec)
mysql> GRANT 'application_rw', 'application_ro' TO 'application';
Query OK, 0 rows affected (0.01 sec)
mysql> REVOKE 'application_ro' FROM 'bob'@'localhost';
Query OK, 0 rows affected (0.02 sec)
mysql> GRANT 'application' TO 'bob'@'localhost';
Query OK, 0 rows affected (0.00 sec)

Что происходит сейчас, так это то, что когда bob активирует роль application, у него будут разрешения обеих ролей rw и ro. Мы можем легко убедиться в этом. Обратите внимание, что bob не может активировать ни одну из ролей, которые ему были предоставлены косвенно:

$ mysql -ubob
mysql> SET ROLE 'application';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT staff_id, first_name FROM sakila.staff;
+----------+------------+
| staff_id | first_name |
+----------+------------+
|        1 | Mike       |
|        2 | Jon        |
+----------+------------+
2 rows in set (0.00 sec)
Графическое отображение ролей

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

mysql> SELECT * FROM mysql.role_edges;
+-----------+----------------+-----------+-------------+----------------+
| FROM_HOST | FROM_USER      | TO_HOST   | TO_USER     | WITH_ADMIN_... |
+-----------+----------------+-----------+-------------+----------------+
| %         | application    | localhost | bob         | N              |
| %         | application_ro | %         | application | N              |
| %         | application_rw | %         | application | N              |
| %         | developer      | 192.168.% | john        | N              |
| %         | developer      | localhost | bob         | N              |
| 192.168.% | john           | %         | developer   | N              |
+-----------+----------------+-----------+-------------+----------------+
6 rows in set (0.00 sec)

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

mysql> SELECT * FROM mysql.roles_graphml()\G
*************************** 1. row ***************************
roles_graphml(): <?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns="...
...
    <node id="n0">
      <data key="key1">`application`@`%`</data>
    </node>
    <node id="n1">
      <data key="key1">`application_ro`@`%`</data>
    </node>
...

В идеале следует использовать SELECT ... INTO OUTFILE (см. «Запись данных в файлы с разделителями-запятыми»). Затем вы можете использовать такой инструмент, как графический редактор yEd, который представляет собой мощное кроссплатформенное бесплатное настольное приложение, для визуализации этого вывода. На рисунке 8-1 вы можете увидеть увеличенную часть полного графика, в которой основное внимание уделяется нашему пользователю bob и окружающим его ролям. Для запуска этой функции требуется привилегия ROLE_ADMIN.

Рисунок 8-1. Раздел визуализированного графа ролей MySQL

Различия между ролями и пользователями

Ранее мы упоминали, что привилегии CREATE USER и DROP USER позволяют изменять роли. Учитывая, что роли хранятся вместе с пользователями в mysql.user, вы также можете предположить, что обычные команды управления пользователями будут работать для ролей. Это легко проверить и подтвердить: просто запустите RENAME USER или DROP USER для роли. Еще одна вещь, на которую стоит обратить внимание, это то, как команды GRANT и REVOKE PRIVILEGE нацелены на роли, как если бы они были пользователями.

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

mysql> CREATE ROLE 'developer';
Query OK, 0 rows affected (0.02 sec)
mysql> GRANT 'john'@'192.168.%' TO 'developer';
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT from_user, to_user FROM mysql.role_edges;
+-----------+-----------+
| from_user | to_user   |
+-----------+-----------+
| john      | developer |
+-----------+-----------+
1 row in set (0.00 sec)

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

Изменение пароля root и небезопасный запуск

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

Чтобы выполнить обход аутентификации и привилегий, вы должны перезапустить экземпляр MySQL с указанной опцией --skip-grant-tables. Поскольку в большинстве установок для запуска экземпляра используются служебные сценарии, вы можете указать skip-grant-tables в файле конфигурации my.cnf в разделе [mysqld]. Когда mysqld запускается в этом режиме, он (очевидно) пропускает чтение таблиц разрешений, что имеет следующие последствия:

Когда вы подключаетесь к экземпляру MySQL, работающему с параметром --skip-grant-tables, вы авторизуетесь как специальный пользователь. Этот пользователь имеет доступ к каждой таблице и может изменить любого пользователя. Прежде чем изменить, например, утерянный пароль пользователя root, вам нужно запустить FLUSH PRIVILEGES; в противном случае ALTER завершится ошибкой:

mysql> SELECT current_user();
+-----------------------------------+
| current_user()                    |
+-----------------------------------+
| skip-grants user@skip-grants host |
+-----------------------------------+
1 row in set (0.00 sec)
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'P@ssw0rd!';
ERROR 1290 (HY000): The MySQL server is running with the --skip-grant-tables option so it cannot execute this statement
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.02 sec)
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'P@ssw0rd!';
Query OK, 0 rows affected (0.01 sec)

После сброса пароля рекомендуется перезапустить экземпляр MySQL в обычном режиме.

Есть еще один способ восстановить пароль root, возможно, более безопасный. Одним из многочисленных аргументов командной строки, которые принимает mysqld, является --init-file (или init_file, если используется через my.cnf). Этот аргумент указывает путь к файлу, содержащему некоторые операторы SQL, которые будут выполняться во время запуска MySQL. В это время проверка привилегий не выполняется, поэтому можно ввести здесь инструкцию ALTER USER root. Рекомендуется удалить файл и отключить параметр, как только вы восстановите доступ или создадите нового пользователя root.

Некоторые идеи для безопасной установки

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

С административной стороны у нас есть следующие рекомендации:

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

Завершив эту главу, вы должны освоиться с администрированием пользователей и привилегий в MySQL.

Глава 9
Использование файлов параметров

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

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

Знание того, как обращаться с файлом параметров — понимание его разделов и приоритета опций — важная часть эффективной работы с MySQL Server и связанным программным обеспечением. Прочитав эту главу, вы сможете чувствовать себя комфортно при настройке сервера MySQL и других программ, использующих файлы параметров. В этой главе основное внимание будет уделено самим файлам. Конфигурация самого сервера и некоторые идеи настройки подробно обсуждаются в главе 11.

Структура файла параметров

Файлы конфигурации в MySQL следуют повсеместно распространенной файловой схеме INI. Короче говоря, это обычные текстовые файлы, предназначенные для редактирования вручную. Конечно, вы можете автоматизировать процесс редактирования, но структура этих файлов намеренно очень проста. Почти каждый файл конфигурации MySQL можно создать и изменить с помощью любого текстового редактора. Есть только два исключения из этого правила, рассмотренные в «Специальных файлах параметров».

Чтобы дать вам представление о файловой структуре, давайте взглянем на файл конфигурации, поставляемый с MySQL 8 в Fedora Linux (обратите внимание, что точное содержимое файлов параметров в вашей системе может отличаться). Мы отредактировали несколько строк для краткости:

$ cat /etc/my.cnf
...
[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
...
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock

log-error=/var/log/mysqld.log
pid-file=/run/mysqld/mysqld.pid

В файловой структуре есть несколько основных частей:

Заголовки разделов (групп)

Это значения в квадратных скобках перед параметрами конфигурации. Все программы, использующие файлы параметров, ищут параметры в одном или нескольких именованных разделах. Например, [mysqld] — это раздел, используемый сервером MySQL, а [mysql] — использует программа интерфейса командной строки mysql. Названия разделов, строго говоря, произвольны, и туда можно поместить что угодно. Однако, если вы измените [mysqld] на [section], ваш сервер MySQL будет игнорировать все параметры, следующие за этим заголовком.

В документации MySQL разделы (sections) называются группами (groups), но оба термина могут использоваться взаимозаменяемо.

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

[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
[mysql]
default-character-set=latin1

Здесь параметры datadir и socket находятся (и будут атрибутированы) в разделе [mysqld], тогда как параметр default-character-set находится в разделе [mysql]. Обратите внимание, что некоторые программы MySQL читают несколько разделов; но мы поговорим об этом позже.

Заголовки разделов могут переплетаться. Следующий пример полностью верен:

[mysqld]
datadir=/var/lib/mysql
[mysql]
default-character-set=latin1
[mysqld]
socket=/var/lib/mysql/mysql.sock
[mysqld_safe]
core-file-size=unlimited
[mysqld]
core-file

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

Пары «опция-значение»

Это основная часть файла параметров, состоящая из самих переменных конфигурации и их значений. Каждая из этих пар определяется на новой строке и соответствует одному из двух общих шаблонов. В дополнение к шаблону option=value, показанному в предыдущем примере, есть еще только шаблон option. Например, в том же стандартном конфигурационном файле MySQL 8 есть следующие строки:

# Remove the leading "# " to disable binary logging
# Binary logging captures changes between backups and is enabled by
# default. Its default setting is log_bin=binlog
# disable_log_bin

disable_log_bin — это опция без значения. Если мы раскомментируем ее, MySQL Server применит эту опцию.

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

Значения параметров также могут быть заключены в одинарные или двойные кавычки. Это полезно, если вы не уверены, будет ли значение правильно интерпретировано. Например, в Windows пути содержат символ \, который рассматривается как escape-символ. Таким образом, вы должны помещать пути в Windows в двойные кавычки (хотя вы также можете экранировать каждый символ \, удвоив его как \\). Заключение значений параметра в кавычки требуется, когда значение включает символ #, который в противном случае рассматривался бы как указание на начало комментария.

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

slow_query_log_file = "C:\mysqldata\query.log"
slow_query_log_file=C:\\mysqldata\\query.log
innodb_temp_tablespaces_dir="./#innodb_temp/"

При установке значений числовых параметров, таких как размеры различных буферов и файлов, работа с байтами может стать утомительной. Чтобы упростить жизнь, MySQL понимает множество суффиксов, обозначающих разные единицы измерения. Например, все приведенные ниже значения эквивалентны и определяют пул буферов одинакового размера (268 435 456 байт):

innodb_buffer_pool_size = 268435456
innodb_buffer_pool_size = 256M
innodb_buffer_pool_size = 256MB
innodb_buffer_pool_size = 256MiB

Вы также можете указать G, GB и GiB для гигабайт и T, TB и TiB для терабайт, если у вас достаточно большой сервер. Конечно, K и другие формы также принимаются. MySQL всегда использует единицы измерения с основанием 2: 1 ГБ равен 1024 МБ, а не 1000 МБ.

Вы не можете указывать дробные значения для опций. Так, например, 0.25G — неверное значение для переменной innodb_buffer_pool_size. Кроме того, в отличие от установки значений из интерфейса командной строки mysql или другого клиентского соединения, вы не можете использовать математическую запись для значений параметров. Вы можете запустить SET GLOBAL max_heap_table_size=16*1024*1024;, но вы не можете поместить то же значение в файл опций.

Вы даже можете настроить одну и ту же опцию несколько раз, как мы сделали с innodb_buffer_pool_size. Последняя настройка будет иметь приоритет над предыдущими, а файлы сканируются сверху вниз. Файлы параметров также имеют глобальный порядок приоритета; мы поговорим об этом в разделе «Порядок поиска файлов параметров».

Очень важно помнить, что установка неправильного имени опции приведет к тому, что программы не запустятся. Конечно, если неправильный вариант находится в разделе, который конкретная программа не читает, это нормально. Но mysqld потерпит неудачу, если обнаружит неизвестный параметр в [mysqld]. В MySQL 8.0 вы можете проверить некоторые изменения, которые вы делаете в файлах параметров, используя аргумент командной строки --validate-config с mysqld. Однако эта проверка будет охватывать только основные функции сервера и не будет проверять параметры механизма хранения.

Иногда вам нужно установить параметр, который MySQL не знает во время запуска. Например, это может быть полезно при настройке плагинов, которые могут загружаться после запуска. Вы можете добавить к таким опциям префикс loose- (или --loose- в командной строке), и MySQL выдаст предупреждение только когда увидит их, но не откажет в запуске.. Вот пример с неизвестной опцией:

# mysqld --validate-config
2021-02-11T08:02:58.741347Z 0 [ERROR] [MY-000067] [Server] ...
... unknown variable audit_log_format=JSON.
2021-02-11T08:02:58.741470Z 0 [ERROR] [MY-010119] [Server] Aborting

После того, как параметр изменен на loose-audit_log_format, вместо этого мы видим следующее. Отсутствие вывода означает, что все параметры были успешно проверены:

# mysqld --validate-config
#
Комментарии

Часто упускаемая из виду, но важная особенность файлов параметров MySQL — возможность добавлять комментарии. Комментарии позволяют вам включать произвольный текст, обычно описание того, почему параметр находится здесь, который не будет анализироваться никакими программами MySQL. Как вы видели в примере с disable_log_bin, комментарии — это строки, начинающиеся с #. Вы также можете создавать комментарии, начинающиеся с точки с запятой (;); принимаются любые комментарии. Вам не обязательно выделять целую строку для комментария: они также могут появляться в конце строки, хотя в этом случае они должны начинаться с #, а не с ;. Как только MySQL находит # в строке (если он не экранирован), всё, что находится за этой точкой, рассматривается как комментарий. Следующая строка является допустимой конфигурацией:

innodb_buffer_pool_size = 268435456 # 256M
Директивы включения

Файлы конфигурации (и целые каталоги) могут быть включены в другие файлы конфигурации. Это может упростить управление сложными конфигурациями, но также затруднит чтение параметров, поскольку люди, в отличие от программ, не могут легко объединять файлы вместе. Тем не менее, полезно иметь возможность разделять конфигурации различных программ MySQL. Утилита xtrabackup (см. главу 10), например, не имеет специального конфигурационного файла и считывает стандартные системные файлы настроек. Благодаря включению вы можете аккуратно упорядочить конфигурации xtrabackup в специальном файле и навести порядок в основном файле параметров MySQL. Затем вы можете включить его следующим образом:

$ cat /etc/my.cnf
!include /etc/mysql.d/xtrabackup.cnf
...

Вы можете видеть, что /etc/my.cnf включает файл /etc/mysql.d/xtrabackup.cnf, который, в свою очередь, имеет несколько параметров конфигурации, перечисленных в разделе [xtrabackup].

Однако нет необходимости иметь разные разделы в разных файлах. Например, Percona XtraDB Cluster имеет параметры конфигурации библиотеки wsrep в разделе [mysqld]. Таких конфигураций много, и они не всегда полезны в вашем файле my.cnf. Вы можете создать отдельный файл, например, /etc/mysql.d/wsrep.conf, и перечислить там переменные wsrep в разделе [mysqld]. Любая программа, читающая основной файл my.cnf, также будет читать все включенные файлы и только затем анализировать переменные в разных разделах.

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

!includedir /etc/mysql.d

Программы MySQL воспринимают этот путь как каталог и пытаются включить каждый файл параметров в дерево этого каталога. В Unix-подобных системах включены файлы .cnf; в Windows включены файлы .cnf и .ini.

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

В каждом включенном файле должен быть определен хотя бы один раздел конфигурации. Например, в начале может быть [mysqld].

Пустые строки

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

Область видимости параметров

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

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

Давайте рассмотрим пример, когда у нас есть сервер MySQL, сконфигурированный с набором символов latin1 по устаревшим причинам. Однако теперь есть более новые таблицы с кодировкой utf8mb4. Мы хотим, чтобы наши логические дампы mysqldump были только в UTF-8, поэтому мы хотим переопределить кодировку для этой программы. Удобно, что mysqldump считывает свой собственный раздел конфигурации, поэтому мы можем написать файл опций следующим образом:

[mysqld]
character_set_server=latin1
[mysqldump]
default_character_set=utf8mb4

Этот небольшой пример показывает, как параметры могут быть установлены на разных уровнях. В данном конкретном случае мы использовали разные варианты, но это мог быть один и тот же вариант в разных масштабах. Например, предположим, что мы хотим ограничить будущий размер значений BLOB и TEXT (см. «Типы строк») до 32 МБ, но у нас уже есть строки размером до 256 МБ. Мы можем добавить искусственный барьер для локальных клиентов, используя такую конфигурацию:

[mysqld]
max_allowed_packet=256M
[client]
max_allowed_packet=32M

Значение max_allowed_packet сервера MySQL будет установлено в глобальной области и будет действовать как жесткое ограничение на максимальный размер запроса (а также на размер поля BLOB или TEXT). Значение клиента будет установлено в области сеанса и будет действовать как мягкое ограничение. Если конкретному клиенту требуется большее значение (например, чтобы прочитать старую строку), он может использовать оператор SET, чтобы перейти к ограничению сервера.

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

Давайте рассмотрим файлы параметров, устанавливаемые и читаемые в Linux и Windows при обычной установке MySQL 8.0. Мы начнем с Windows, в таблице 9-1.

Таблица 9-1. Файлы параметров MySQL в Windows
Имя файла Область видимости и назначение
%WINDIR%\my.ini, %WINDIR%\my.cnf Глобальные опции, читаемые всеми программами
C:\my.ini, C:\my.cnf Глобальные опции, читаемые всеми программами
BASEDIR\my.ini, BASEDIR\my.cnf Глобальные опции, читаемые всеми программами
Дополнительный файл конфигурации Файл, опционально указанный с --defaults-extra-file
%APPDATA%\MySQL\.mylogin.cnf Конфигурационный файл пути входа в систему
DATADIR\mysqld-auto.cnf Файл параметров для постоянных переменных

В Таблице 9-2 представлены файлы параметров для типичной установки в Fedora Linux.

Таблица 9-2. Файлы параметров MySQL в Fedora Linux
Имя файла Область видимости и назначение
/etc/my.cnf, /etc/mysql/my.cnf, /usr/etc/my.cnf Глобальные параметры, читаемые всеми программами
$MYSQL_HOME/my.cnf Параметры сервера, только для чтения, если переменная установлена
~/.my.cnf Глобальные параметры, читаемые всеми программами, запущенными конкретным пользователем ОС
Дополнительный файл конфигурации Файл, опционально указанный с --defaults-extra-file
~/.mylogin.cnf Файл конфигурации пути входа в систему под определенным пользователем ОС
DATADIR/mysqld-auto.cnf Файл параметров для сохраняемых переменных

В Linux трудно определить универсальный полный список файлов конфигурации, поскольку пакеты MySQL для разных дистрибутивов Linux могут считывать немного разные файлы или места. Как правило, хорошей отправной точкой в Linux является /etc/my.cnf, а в Windows — либо %WINDIR%\my.cnf, либо BASEDIR\my.cnf.

Пара файлов конфигурации, которые мы перечислили, могут различаться по своим путям в разных системах. /usr/etc/my.cnf также можно записать как SYSCONFIGDIR/my.cnf, а путь определяется во время компиляции. $MYSQL_HOME/my.cnf читается только в том случае, если установлена переменная. Упакованная по умолчанию программа mysqld_safe (используемая для запуска демона mysqld) установит $MYSQL_HOME в BASEDIR перед запуском mysqld. Вы не найдете $MYSQL_HOME установленным ни для одного из пользователей ОС, и установка этой переменной актуальна, только если вы запускаете mysqld вручную, другими словами, не используя команды service или systemctl.

Есть одно своеобразное различие между Windows и Linux. В Linux программы MySQL читают некоторые файлы конфигурации, расположенные в домашнем каталоге данного пользователя ОС. В таблице 9-2 домашний каталог представлен как ~. MySQL в Windows не имеет такой возможности. Одним из частых вариантов использования таких файлов конфигурации является управление параметрами клиентов в зависимости от пользователя их ОС. Обычно они содержат учетные данные. Однако средство пути входа в систему, описанное в «Специальных файлах параметров», делает это излишним.

Дополнительный конфигурационный файл, указанный в командной строке с помощью --defaults-extra-file, будет считан после того, как будет прочитан любой другой глобальный файл, в соответствии с его положением в таблице. Это полезная опция, когда вы хотите выполнить однократный запуск программы, например, для проверки новых переменных. Однако чрезмерное использование этого параметра может привести к проблемам с пониманием текущего набора действующих параметров (см. «Определение действующих параметров»). Опция --defaults-extra-file не единственная, которая изменяет обработку файла параметров. --no-defaults запрещает программе вообще читать какие-либо файлы конфигурации. --defaults-file заставляет программу читать один файл, что может быть полезно, если у вас есть все настройки в одном месте.

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

Порядок поиска файлов параметров

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

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

$ mysqld --verbose --help | grep "Default options" -A2
Default options are read from the following files in the given order:
/etc/my.cnf /etc/mysql/my.cnf /usr/etc/my.cnf ~/.my.cnf
The following groups are read: mysqld server mysqld-8.0

В Windows вам нужно запустить mysqld.exe вместо mysqld, но вывод останется прежним. Этот вывод включает в себя список прочитанных файлов конфигурации и их порядок. Вы также можете увидеть список групп параметров, читаемых mysqld: [mysqld], [server] и [mysqld-8.0]. Обратите внимание, что вы можете изменить список групп параметров, которые читает любая программа, добавив параметр --defaults-group-suffix:

$ mysqld --defaults-group-suffix=-test --verbose --help | grep "groups are read"
The following groups are read: mysqld server mysqld-8.0 ...
... mysqld-test server-test mysqld-8.0-test

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

В таблицах 9-1 и 9-2 файлы читаются в порядке сверху вниз. Чем ниже файл конфигурации в списке, тем выше «вес» параметров в нем. Например, для любых программ, отличных от mysqld, значения в .mylogin.cnf имеют приоритет над значениями в любых других файлах конфигурации и имеют только более низкий приоритет, чем значения, заданные с помощью аргументов командной строки. Для mysqld то же самое верно для сохраняемых переменных, установленных в DATADIR /mysqld-auto.cnf.

Возможность включать файлы конфигурации в другие файлы с помощью директив включения немного усложняет ситуацию, но вы всегда включаете дополнительные файлы в один или несколько файлов параметров, перечисленных в таблицах 9-1 и 9-2. Вы можете думать об этом как о том, что MySQL добавляет включенные файлы в родительский файл конфигурации непосредственно перед его чтением, вставляя каждый из них в файл сразу после его директивы включения. Таким образом, глобальный приоритет параметров — приоритет родительского файла конфигурации. В самом результирующем файле (со всеми включенными файлами, добавленными по порядку) параметры, определенные позже, имеют приоритет над определенными ранее.

Файлы специальных параметров

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

Файл конфигурации пути входа в систему

Во-первых, это файл .mylogin.cnf, который используется как часть системы путей входа в систему. Несмотря на то, что вы можете думать о его структуре как об обычном файле параметров, этот конкретный файл не является обычным текстовым файлом. По сути, это зашифрованный текстовый файл. Этот файл предназначен для создания и изменения с помощью специальной программы mysql_config_editor, которая поставляется вместе с MySQL, обычно в клиентском пакете. Он зашифрован, потому что цель .mylogin.cnf (и всей системы путей входа в систему) состоит в том, чтобы хранить параметры подключения MySQL, включая пароли, удобным и безопасным способом.

По умолчанию mysql_config_editor и другие программы MySQL будут искать .mylogin.cnf в $HOME текущего пользователя в Linux и различных версиях Unix, а также в %APPDATA%\MySQL в Windows. Можно изменить местоположение и имя файла, установив переменную среды MYSQL_TEST_LOGIN_FILE.

Вы можете создать этот файл, если он еще не существует, сохранив в нем пароль для пользователя root:

$ mysql_config_editor set --user=root --password
Enter password:

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

$ ls -la ~/.mylogin.cnf
-rw-------. 1 skuzmichev skuzmichev 100 Jan 18 18:03 .mylogin.cnf
$ cat ~/.mylogin.cnf


>pZ
   prI
         R86w"># &.h.m:4+|DDKnl_K3>73x$
$ file ~/.mylogin.cnf
.mylogin.cnf: data
$ file ~/.my.cnf
.my.cnf: ASCII text

Как видите, по крайней мере на первый взгляд, .mylogin.cnf точно не является обычным файлом конфигурации. В связи с этим он требует особого отношения. Помимо создания файла, вы можете просматривать и изменять .mylogin.cnf с помощью редактора mysql_config_editor. Начнем с того, как на самом деле увидеть, что внутри. Опцией для этого является print:

$ mysql_config_editor print
[client]
user = "root"
password = *****

client — это путь входа по умолчанию. Все операции, выполняемые с помощью mysql_config_editor без явного указания пути входа, влияют на путь входа client. Мы не указывали никакого пути входа в систему при запуске set ранее, поэтому учетные данные root были записаны по пути client. Однако можно указать конкретный путь входа для любой операции. Давайте поместим учетные данные root в путь входа с именем root:

$ mysql_config_editor set --login-path=root --user=root --password
Enter password:

Чтобы указать путь для входа, используйте параметр --login-path или -G, а для просмотра всех путей при использовании print добавьте параметр --all:

$ mysql_config_editor print --login-path=root
[root]
user = root
password = *****
$ mysql_config_editor print --all
[client]
user = root
password = *****
[root]
user = root
password = *****

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

mysql_config_editor поддерживает аргумент --help (или -?), который можно комбинировать с другими параметрами, чтобы получить справку конкретно по print или set, например. Начнем с немного урезанного вывода справки для set:

$ mysql_config_editor set --help
...
MySQL Configuration Utility.

Description: Write a login path to the login file.
Usage: mysql_config_editor [program options] [set [command options]]
   -?, --help         Display this help and exit.
   -h, --host=name    Host name to be entered into the login file.
   -G, --login-path=name
                      Name of the login path to use in the login file. (Default: client)
   -p, --password     Prompt for password to be entered into the login file.
   -u, --user=name    User name to be entered into the login file.
   -S, --socket=name  Socket path to be entered into login file.
   -P, --port=name    Port number to be entered into login file.
   -w, --warn         Warn and ask for confirmation if set command attempts to overwrite an existing login path (enabled by default). (Defaults to on; use --skip-warn to disable.)
...

Здесь вы можете увидеть еще одно интересное свойство файла .mylogin.cnf: в него нельзя помещать произвольные параметры. Теперь мы знаем, что в основном мы можем установить только несколько параметров, связанных с входом в экземпляр или экземпляры MySQL, чего, конечно же, и следует ожидать от файла «путь входа». Теперь вернемся к редактированию файла:

$ mysql_config_editor set --login-path=scott --user=scott
$ mysql_config_editor set --login-path=scott --user=scott
WARNING : scott path already exists and will be overwritten. Continue? (Press y|Y for Yes, any other key for No) : y
$ mysql_config_editor set --login-path=scott --user=scott --skip-warn

Здесь мы показали все варианты поведения, которые mysql_config_editor может демонстрировать при изменении или создании пути входа в систему. Если путь входа еще не существует, предупреждение не выдается. Если такой путь уже есть, будет напечатано предупреждение и подтверждение, но только если не указан --skip-warn. Обратите внимание, что здесь мы говорим о полном пути входа в систему! Невозможно изменить одно свойство пути: каждый раз записывается весь путь входа. Если вы хотите изменить одно свойство, вам также нужно будет указать все остальные свойства, которые вам нужны.

Добавим еще немного деталей и посмотрим на результат:

$ mysql_config_editor set --login-path=scott \
--user=scott --port=3306 --host=192.168.122.1 \
--password --skip-warn
Enter password:
$ mysql_config_editor print --login-path=scott
[scott]
user = scott
password = *****
host = 192.168.122.1
port = 3306

Файл конфигурации постоянных системных переменных

Второй специальный файл — это mysqld-auto.cnf, который находится в каталоге данных, начиная с MySQL 8.0. Это часть новой функции постоянных системных переменных, которая позволяет обновлять параметры MySQL на диске с помощью обычных операторов SET. Раньше вы не могли изменить конфигурацию MySQL из соединения с базой данных. Обычный процесс состоял в том, чтобы изменить файлы параметров на диске, а затем запустить оператор SET GLOBAL для изменения переменных конфигурации в режиме онлайн. Как вы можете себе представить, это может привести к ошибкам и к тому, что изменения будут вноситься, например, только онлайн. Новый оператор SET PERSIST решает обе задачи: переменные, обновляемые онлайн, также обновляются на диске. Также возможно обновить переменную только на диске.

Сам файл, на удивление, совсем не похож ни на один другой файл конфигурации в MySQL. В то время как .mylogin.cnf был зашифрованным, но все еще обычным файлом параметров, mysqld-auto.cnf использует общий, но совершенно другой формат: JSON.

До того, как вы что-то сохраните, mysqld-auto.cnf не существует. Итак, начнем с изменения системной переменной:

mysql> SELECT @@GLOBAL.max_connections;
+--------------------------+
| @@GLOBAL.max_connections |
+--------------------------+
|                      100 |
+--------------------------+
1 row in set (0.00 sec)
mysql> SET PERSIST max_connections = 256;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT @@GLOBAL.max_connections;
+--------------------------+
| @@GLOBAL.max_connections |
+--------------------------+
|                      256 |
+--------------------------+
1 row in set (0.00 sec)

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

$ cat /var/lib/mysql/mysqld-auto.cnf | jq .
{
  "Version": 1,
  "mysql_server": {
    "max_connections": {
      "Value": "256",
      "Metadata": {
        "Timestamp": 1611728445802834,
        "User": "root",
        "Host": "localhost"
      }
    }
  }
}

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

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

Определение действующих параметров

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

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

Давайте начнем с того, как определить параметры, используемые в настоящее время программой. Для некоторых программ, таких как MySQL Server (mysqld), это легко. Вы можете получить список текущих значений, используемых mysqld, запустив SHOW GLOBAL VARIABLES. Невозможно изменить значение параметра, которое использует mysqld, и не увидеть эффект, отраженный в состоянии глобальных переменных. С другими программами дело обстоит сложнее. Чтобы понять, какие параметры используются mysql, вам нужно запустить ее, а затем проверить выходные данные SHOW VARIABLES и SHOW GLOBAL VARIABLES, чтобы увидеть, какие параметры переопределены на уровне сеанса. Но даже до того, как будет установлено успешное соединение с сервером, mysql должен прочитать или получить информацию о соединении.

Существует два простых способа определить список параметров, действующих при запуске программы: путем передачи этой программе аргумента --print-defaults или с помощью специальной программы my_print_defaults. Давайте посмотрим на первый вариант, выполненный в Linux. Вы можете проигнорировать часть sed, но она делает вывод более приятным для человеческого глаза:

$ mysql --print-defaults
mysql would have been started with the following arguments:
--user=root --password=*****
$ mysqld --print-defaults | sed 's/--/\n--/g'
/usr/sbin/mysqld would have been started with the following arguments:

--datadir=/var/lib/mysql
--socket=/var/lib/mysql/mysql.sock
--log-error=/var/log/mysqld.log
--pid-file=/run/mysqld/mysqld.pid
--max_connections=100000
--core-file
--innodb_buffer_pool_in_core_file=OFF
--innodb_buffer_pool_size=256MiB

Выбранные здесь переменные взяты из всех файлов опций, которые мы обсуждали ранее. Если значение переменной было задано несколько раз, последнее вхождение будет иметь приоритет. Однако --print-defaults на самом деле выведет каждый набор параметров. Например, вывод может выглядеть следующим образом: несмотря на то, что innodb_buffer_pool_size устанавливается пять раз, фактическое значение будет равно 384 МБ:

$ mysqld --print-defaults | sed 's/--/\n--/g'
/usr/sbin/mysqld would have been started with the following arguments:

--datadir=/var/lib/mysql
--socket=/var/lib/mysql/mysql.sock
--log-error=/var/log/mysqld.log
--pid-file=/run/mysqld/mysqld.pid
--max_connections=100000
--core-file
--innodb_buffer_pool_in_core_file=OFF
--innodb_buffer_pool_size=268435456
--innodb_buffer_pool_size=256M
--innodb_buffer_pool_size=256MB
--innodb_buffer_pool_size=256MiB
--large-pages
--innodb_buffer_pool_size=384M

Вы также можете комбинировать --print-defaults с другими аргументами командной строки. Например, если вы собираетесь запустить программу с аргументами командной строки, вы можете увидеть, будут ли они переопределять или повторять уже установленные значения параметров конфигурации для конкретного сеанса:

$ mysql --print-defaults --host=192.168.4.23 --user=bob | sed 's/--/\n--/g'
mysql would have been started with the following arguments:

--user=root
--password=*****
--host=192.168.4.23
--user=bob

Другой способ печати переменных — использование программы my_print_defaults. Она принимает один или несколько заголовков разделов в качестве аргументов и печатает все параметры, которые находит в отсканированных файлах и попадает в запрошенные группы. Это может быть лучше, чем использование --print-defaults, когда вам просто нужно просмотреть одну группу опций. В MySQL 8.0 программа mysqld считывает следующие группы: [mysqld], [server] и [mysqld-8.0]. Комбинированный вывод параметров может быть длительным, но что, если нам нужно просмотреть только параметры, специально установленные для версии 8.0? Для этого примера мы добавили группу параметров [mysqld-8.0] в файл параметров и поместили туда несколько значений параметров конфигурации:

$ my_print_defaults mysqld-8.0
--character_set_server=latin1
--collation_server=latin1_swedish_ci

Это также может помочь с другим программным обеспечением, таким как PXC, или с вариантом MySQL MariaDB, оба из которых включают несколько групп конфигурации. В частности, вы, вероятно, захотите просмотреть раздел [wsrep] без каких-либо других вариантов. my_print_defaults можно, конечно, использовать и для вывода полного набора опций; ей просто нужно передать все заголовки разделов, которые читает программа. Например, программа mysql считывает группы параметров [mysql] и [client], поэтому мы могли бы использовать:

$ my_print_defaults mysql client
--user=root
--password=*****
--default-character-set=latin1

Определения пользователя и пароля берутся из группы клиентов в конфигурации пути входа в систему, которую мы установили ранее, а кодировка — из группы параметров [mysql] в обычном .my.cnf. Обратите внимание, что мы добавили эту группу и конфигурацию кодировки вручную; по умолчанию этот параметр не установлен.

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

К сожалению, ни один из этих способов просмотра вариантов не идеален для определения полного набора действующих вариантов. Проблема в том, что они читают только файлы конфигурации, перечисленные в таблицах 9-1 и 9-2, но программы MySQL могут читать другие файлы конфигурации или запускаться с аргументами командной строки. Кроме того, переменные, сохраняемые в DATADIR /mysqld-auto.cnf через SET PERSIST, не предоставляются процедурами печати по умолчанию.

Мы упомянули, что программы MySQL не читают опции ни из каких других файлов, кроме тех, которые перечислены в таблицах 9-1 и 9-2. Однако эти списки включают «дополнительный файл конфигурации», который может находиться в произвольном месте. Если вы не укажете тот же дополнительный файл при вызове my_print_defaults или другой программы с параметром --print-defaults, параметры из этого дополнительного файла не будут прочитаны. Дополнительный файл указывается с помощью аргумента командной строки --defaults-extra-file и может быть указан для большинства, если не для всех программ MySQL. Две процедуры печати по умолчанию читают только предопределенные файлы конфигурации и пропустят этот файл. Однако вы можете указать --defaults-extra-file как для my_print_defaults, так и для программы, вызванной с --print-defaults, и тогда обе будут читать дополнительный файл. То же самое относится к параметру --defaults-file, о котором мы упоминали ранее.

И --defaults-extra-file, и --defaults-file имеют одну общую черту: они являются аргументами командной строки. Аргументы командной строки, передаваемые программе MySQL, переопределяют любые параметры, считанные из файлов конфигурации, но в то же время вы можете пропустить их, когда используете --print-defaults или my_print_defaults, поскольку они поступают извне любых файлов конфигурации. Говоря более кратко: определенная программа MySQL, такая как mysqld, может быть запущена кем-то с неизвестными и произвольными аргументами командной строки. Таким образом, когда мы говорим о вариантах, по сути, мы также должны учитывать наличие таких аргументов.

В Linux и Unix-подобных системах вы можете использовать утилиту ps (или аналогичную) для просмотра информации о запущенных в данный момент процессах, включая их полные командные строки. Давайте посмотрим на пример в Linux, где mysqld был запущен с --no-defaults и со всеми параметрами конфигурации, переданными в качестве аргументов:

$ ps auxf | grep mysqld | grep -v grep
root      397830 ...  \_ sudo -u mysql bash -c mysqld ...
mysql     397832 ...    \_ mysqld --datadir=/var/lib/mysql ...

Или, если мы напечатаем только командную строку для процесса mysqld и сделаем ее чище с помощью sed:

$ ps -p 397832 -ocommand ww | sed 's/--/\n--/g'
COMMAND
mysqld
--datadir=/var/lib/mysql
--socket=/var/lib/mysql/mysql.sock
--log-error=/var/log/mysqld.log
--pid-file=/run/mysqld/mysqld.pid
...
--character_set_server=latin1
--collation_server=latin1_swedish_ci

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

Вы можете передать любой параметр конфигурации в качестве аргумента, поэтому вывод может быть довольно длинным. Однако, если вы не уверены, как именно была запущена mysqld или другая программа, это важно проверить. В Windows вы можете просмотреть аргументы командной строки запущенной программы, либо открыв Диспетчер задач и добавив столбец Командная строка на вкладку Процессы (через меню Вид), либо с помощью инструмента Process Explorer из пакета sysinternals.

Если ваша программа MySQL запускается из сценария, вы должны проверить этот сценарий, чтобы найти все используемые аргументы. Хотя это, вероятно, будет редким случаем для mysqld, обычно из пользовательских сценариев запускаются mysql, mysqldump и xtrabackup.

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

Глава 10
Резервное копирование и восстановление

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

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

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

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

Физические и логические резервные копии

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

Возможно, это было бы проще объяснить на примере. Представьте себе резервное копирование одной таблицы MyISAM в базе данных MySQL. Как вы увидите позже в этой главе, механизм хранения InnoDB более сложен для правильного резервного копирования. Зная, что MyISAM не является транзакционным и что в эту таблицу не выполняются текущие операции записи, мы можем копировать связанные с ней файлы. При этом мы создаем физическую резервную копию таблицы. Вместо этого мы могли бы запустить операторы SELECT * и SHOW CREATE TABLE для этой таблицы и где-нибудь сохранить результаты этих операторов. Это очень простая форма логического резервного копирования. Конечно, это всего лишь простые примеры, и в реальности процесс получения обоих типов резервных копий будет более сложным и детализированным. Однако концептуальные различия между этими воображаемыми резервными копиями могут быть перенесены и применены к любым логическим и физическим резервным копиям.

Логические резервные копии

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

Вот несколько примеров логических резервных копий:

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

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

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

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

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

После повреждения файла данных база данных может быть не в состоянии считывать из него данные и обслуживать запросы. Поскольку искажение обычно происходит незаметно, вы можете не знать, когда оно произошло. Однако, если логическая резервная копия была создана без ошибок, это означает, что она исправна и содержит правильные данные. Повреждение может произойти во вторичном индексе (любом непервичном индексе; дополнительные сведения см. в Главе 4 «Работа со структурами базы данных»), поэтому логическая резервная копия, выполняющая полное сканирование таблицы, может генерироваться нормально и не вызывать ошибки. Короче говоря, логическое резервное копирование может помочь вам обнаружить повреждение данных на ранней стадии (так как оно сканирует все таблицы) и сохранить данные (так как последнее успешное логическое резервное копирование будет содержать корректную копию данных).

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

Физические резервные копии

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

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

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

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

Физические резервные копии, в отличие от логических, являются очень жесткими, что дает мало возможностей для контроля над тем, что может быть зарезервировано и где эта резервная копия может быть использована. Вообще говоря, большинство физических резервных копий можно использовать только для восстановления точно такого же состояния базы данных или таблицы. Обычно эти резервные копии также накладывают ограничения на версию программного обеспечения целевой базы данных и операционную систему. Приложив некоторые усилия, вы сможете восстановить логическую резервную копию из MySQL в PostgreSQL. Однако холодная копия каталога данных MySQL, сделанная в Linux, может не работать при восстановлении в Windows. Кроме того, вы не можете сделать физическую резервную копию, если у вас нет физического доступа к серверу базы данных. Это означает, что выполнение такого резервного копирования управляемой базы данных в облаке невозможно: поставщик может выполнять физические резервные копии в фоновом режиме, но у вас может не быть возможности их извлечь.

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

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

Обзор логических и физических резервных копий

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

Свойства логических резервных копий:

Инструменты логического резервного копирования:

Свойства физических резервных копий:

Инструменты физического резервного копирования:

Репликация как средство резервного копирования

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

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

Сбой инфраструктуры

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

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

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

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

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

Ошибка развертывания

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

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

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

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

Программа mysqldump

Возможно, самый простой способ создать резервную копию базы данных в режиме онлайн — это выгрузить ее содержимое в виде инструкций SQL. Это наиболее распространенный тип логического резервного копирования. В вычислительной технике под получением дампа обычно подразумевается вывод содержимого некоторой системы или ее частей, в результате чего создается дамп. В мире баз данных дамп обычно представляет собой логическую резервную копию, а получение дампа — это действие по получению такой резервной копии. Восстановление резервной копии включает в себя применение инструкций к базе данных. Вы можете создавать дампы вручную, используя, например, SHOW CREATE TABLE и некоторые операции CONCAT для получения операторов INSERT из строк данных в таблицах, например:

mysql> SHOW CREATE TABLE sakila.actor\G
*************************** 1. row ***************************
       Table: actor
Create Table: CREATE TABLE `actor` (
  `actor_id` smallint unsigned NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) NOT NULL,
  `last_name` varchar(45) NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
        ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`actor_id`),
  KEY `idx_actor_last_name` (`last_name`)
) ENGINE=InnoDB AUTO_INCREMENT=201 DEFAULT CHARSET=utf8mb4
        COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

mysql> SELECT CONCAT("INSERT INTO actor VALUES",
    -> "(",actor_id,",'",first_name,"','",
    -> last_name,"','",last_update,"');")
    -> AS insert_statement FROM actor LIMIT 1\G
*************************** 1. row ***************************
insert_statement: INSERT INTO actor VALUES
(1,'PENELOPE','GUINESS','2006-02-15 04:34:33');
1 row in set (0.00 sec)

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

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

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

В следующем примере mysqldump вызывается без перенаправления вывода, и инструмент выводит все операторы на стандартный вывод:

$ mysqldump sakila
...
--
-- Table structure for table `actor`
--

DROP TABLE IF EXISTS `actor`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `actor` (
  `actor_id` smallint unsigned NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) NOT NULL,
  `last_name` varchar(45) NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
        ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`actor_id`),
  KEY `idx_actor_last_name` (`last_name`)
) ENGINE=InnoDB AUTO_INCREMENT=201 DEFAULT CHARSET=utf8mb4
        COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `actor`
--

LOCK TABLES `actor` WRITE;
/*!40000 ALTER TABLE `actor` DISABLE KEYS */;
INSERT INTO `actor` VALUES
(1,'PENELOPE','GUINESS','2006-02-15 01:34:33'),
(2,'NICK','WAHLBERG','2006-02-15 01:34:33'),
...
(200,'THORA','TEMPLE','2006-02-15 01:34:33');
/*!40000 ALTER TABLE `actor` ENABLE KEYS */;
UNLOCK TABLES;
...

Вы можете заметить, что этот вывод более детализирован, чем вы могли ожидать. Например, есть оператор DROP TABLE IF EXISTS, который предотвращает ошибку для следующей команды CREATE TABLE, когда таблица уже существует в целевом объекте. Операторы LOCK и UNLOCK TABLES улучшат производительность вставки данных и так далее.

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

$ mysqldump --no-data sakila
...
--
-- Table structure for table `actor`
--

DROP TABLE IF EXISTS `actor`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `actor` (
  `actor_id` smallint unsigned NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) NOT NULL,
  `last_name` varchar(45) NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
        ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`actor_id`),
  KEY `idx_actor_last_name` (`last_name`)
) ENGINE=InnoDB AUTO_INCREMENT=201 DEFAULT CHARSET=utf8mb4
        COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Temporary view structure for view `actor_info`
--
...

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

$ mysqldump sakila category

Повышая гибкость, вы можете вывести всего несколько строк из таблицы, указав аргумент --where или -w. Как следует из названия, синтаксис такой же, как и для предложения WHERE в операторе SQL:

$ mysqldump sakila actor --where="actor_id > 195"
...
--
-- Table structure for table `actor`
--

DROP TABLE IF EXISTS `actor`;
CREATE TABLE `actor` (
...

--
-- Dumping data for table `actor`
--
-- WHERE: actor_id > 195

LOCK TABLES `actor` WRITE;
/*!40000 ALTER TABLE `actor` DISABLE KEYS */;
INSERT INTO `actor` VALUES
(196,'BELA','WALKEN','2006-02-15 09:34:33'),
(197,'REESE','WEST','2006-02-15 09:34:33'),
(198,'MARY','KEITEL','2006-02-15 09:34:33'),
(199,'JULIA','FAWCETT','2006-02-15 09:34:33'),
(200,'THORA','TEMPLE','2006-02-15 09:34:33');
/*!40000 ALTER TABLE `actor` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

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

$ mysqldump --all-databases --triggers --routines --events > dump.sql

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

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

$ mysqldump --all-databases \
--routines --events | gzip > dump.sql.gz

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

$ time mysqldump --all-databases \
--routines --events > dump.sql
real    0m24.608s
user    0m15.201s
sys     0m2.691s
$ time mysqldump --all-databases \
--routines --events | gzip > dump.sql.gz
real    2m2.769s
user    2m4.400s
sys     0m3.115s
$ ls -lh dump.sql
-rw... 2.0G ... dump.sql
-rw... 794M ... dump.sql.gz

Вторая проблема заключается в том, что для обеспечения согласованности на таблицы будут наложены блокировки, предотвращающие запись во время создания дампа базы данных (запись в другие базы данных может продолжаться). Это плохо как для производительности, так и для согласованности резервного копирования. Результирующий дамп согласован только внутри базы данных, а не во всем экземпляре. Такое поведение по умолчанию необходимо, поскольку некоторые механизмы хранения, используемые MySQL, не являются транзакционными (в основном, более старые MyISAM). Механизм хранения InnoDB по умолчанию, с другой стороны, имеет модель многоверсионного управления параллелизмом (MVCC), которая позволяет поддерживать моментальный снимок чтения. Мы более подробно рассмотрели различные подсистемы хранения в разделе «Альтернативные механизмы хранения», а блокировку — в главе 6.

Использование транзакционных возможностей InnoDB возможно путем передачи аргумента командной строки --single-transaction в mysqldump. Однако это устраняет блокировку таблицы, что делает нетранзакционные таблицы подверженными несогласованности во время дампа. Если ваша система использует, например, и таблицы InnoDB, и таблицы MyISAM, для них может потребоваться отдельное создание дампа, если не требуется прерывание записи и непротиворечивость.

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

$ mysqldump --single-transaction --all-databases \
--routines --events | gzip > dump.sql.gz

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

Дамп с --all-databases включает внутренние базы данных MySQL, такие как mysql, sys и information_schema. Эта информация не всегда необходима для восстановления ваших данных и может вызвать проблемы при восстановлении в экземпляр, в котором уже есть некоторые базы данных. Тем не менее, вы должны помнить, что данные пользователя MySQL будут выгружаться только как часть базы данных mysql.

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

Запуск репликации с помощью mysqldump

Программу mysqldump можно использовать для создания экземпляра реплики либо пустого, либо с данными. Для этого доступно несколько аргументов командной строки. Например, если указан параметр --master-data, результирующий вывод будет содержать оператор SQL (CHANGE MASTER TO), который правильно установит координаты репликации на целевом экземпляре. Когда позднее репликация будет запущена с использованием этих координат на целевом экземпляре, пробелов в данных не будет. В топологии репликации на основе GTID можно использовать --set-gtid-purged для достижения того же результата. Однако mysqldump обнаружит параметр gtid_mode=ON, и включит необходимые выходные данные даже без дополнительных аргументов командной строки.

Пример настройки репликации с помощью mysqldump приведен в разделе «Создание реплики с помощью mysqldump».

Загрузка данных из файла дампа SQL

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

Давайте рассмотрим простой пример резервного копирования и восстановления одной базы данных. Как мы видели ранее, mysqldump включит в дамп необходимые операторы DROP, так что даже если объекты присутствуют, они будут успешно восстановлены:

$ mysqldump sakila > /tmp/sakila.sql
$ mysql -e "CREATE DATABASE sakila_mod"
$ mysql sakila_mod < /tmp/sakila.sql
$ mysql sakila_mod -e "SHOW TABLES"
+----------------------------+
| Tables_in_sakila_mod       |
+----------------------------+
| actor                      |
| actor_info                 |
| ...                        |
| store                      |
+----------------------------+

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

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

$ mysqldump sakila > /tmp/sakila.sql &
$ mysqldump nasa > /tmp/nasa.sql &

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

$ mysql sakila < /tmp/sakila.sql &
$ mysql nasa < /tmp/nasa.sql &

В Windows также можно отправить выполнение команды в фоновый режим с помощью команды PowerShell Start-Process или, в более поздних версиях, того же &.

Второй вариант немного сложнее. Либо вам нужно сделать дамп отдельно для каждой таблицы (например, mysqldump sakila artist > sakila.artists.sql), что приводит к прямому восстановлению, либо вам нужно отредактировать файл дампа, чтобы разделить его на несколько. В крайнем случае вы даже можете распараллелить вставку данных на уровне таблицы, хотя это, вероятно, не будет практичным.

Хотя это выполнимо, предпочтительнее использовать инструменты, специально созданные для этой задачи.

mysqlpump

mysqlpump — это служебная программа, входящая в состав MySQL версии 5.7 и более поздних, которая улучшает mysqldump в нескольких областях, в основном в отношении производительности и удобства использования. Ключевые отличия заключаются в следующем:

Использование программы очень похоже на использование mysqldump. Главное непосредственное отличие состоит в том, что, когда никакие аргументы не передаются, mysqlpump по умолчанию выгружает все базы данных (за исключением INFORMATION_SCHEMA, performance_schema, ndbinfo и схемы sys). Другими примечательными вещами являются наличие индикатора выполнения и то, что mysqlpump по умолчанию использует параллельный дамп с двумя потоками:

$ mysqlpump > pump.out
Dump progress: 1/2 tables, 0/530419 rows
Dump progress: 80/184 tables, 2574413/646260694 rows
...
Dump progress: 183/184 tables, 16297773/646260694 rows
Dump completed in 10680

Концепция параллелизма в mysqlpump несколько сложна. Вы можете использовать параллелизм между разными базами данных и между разными объектами в данной базе данных. По умолчанию, когда никакие другие параллельные параметры не указаны, mysqlpump будет использовать одну очередь с двумя параллельными потоками для обработки всех баз данных и пользовательских определений (если требуется). Вы можете управлять уровнем параллелизма очереди по умолчанию, используя аргумент --default-parallelism. Для дальнейшей точной настройки параллелизма можно настроить несколько параллельных очередей для обработки отдельных баз данных. Будьте осторожны при выборе желаемого уровня параллелизма, так как в конечном итоге вы можете использовать большую часть ресурсов базы данных для выполнения резервного копирования.

Важное отличие от mysqldump при использовании mysqlpump заключается в том, как последний обрабатывает учетные записи пользователей. mysqldump управляет пользователями, создавая дамп mysql.user и других соответствующих таблиц. Если база данных mysql не была включена в дамп, никакая пользовательская информация не будет сохранена. mysqlpump улучшает это, вводя аргументы командной строки --users и --include-users. Первый говорит утилите добавить в дамп пользовательские команды для всех пользователей, а второй принимает список имен пользователей. Это большое улучшение по сравнению со старым способом ведения дел.

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

$ mysqlpump --compress-output=zlib --include-users=bob,kate \
--include-databases=sakila,nasa,employees \
--parallel-schemas=2:employees \
--parallel-schemas=sakila,nasa > pump.out
Dump progress: 1/2 tables, 0/331579 rows
Dump progress: 19/23 tables, 357923/3959313 rows
...
Dump progress: 22/23 tables, 3755358/3959313 rows
Dump completed in 10098

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

...,(294975,"1955-07-31","Lucian","Rosis","M","1986-12-08");
CREATE TABLE `sakila`.`store` (
`store_id` tinyint unsigned NOT NULL AUTO_INCREMENT,
`manager_staff_id` tinyint unsigned NOT NULL,
`address_id` smallint unsigned NOT NULL,
`last_update` timestamp NOT NULL DEFAULT
CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`store_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT
CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
;
INSERT INTO `employees`.`employees` VALUES
(294976,"1961-03-19","Rayond","Khalid","F","1989-11-03"),...

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

mydumper и myloader

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

Эти программы можно установить, взяв самую свежую версию со страницы проекта на GitHub или скомпилировав исходный код. На момент написания последний релиз несколько отставал от основной ветки. Пошаговые инструкции по установке доступны в разделе «Настройка утилит mydumper и myloader».

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

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

Чтобы создать дамп и изучить его, выполните следующие команды:

$ mydumper -u root -a
Enter the MySQL password:
$ ls -ld export
drwx... export-20210613-204512
$ ls -la export-20210613-204512
...
-rw... sakila.actor.sql
-rw... sakila.address-schema.sql
-rw... sakila.address.sql
-rw... sakila.category-schema.sql
-rw... sakila.category.sql
-rw... sakila.city-schema.sql
-rw... sakila.city.sql
...

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

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

Холодное резервное копирование и моментальные снимки файловой системы

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

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

Большинство моментальных снимков будут копироваться при записи (copy-on-write) (COW) и внутренне непротиворечивы в какой-то момент времени. Однако мы уже знаем, что файлы базы данных несовместимы на диске, особенно с механизмами хранения транзакций, такими как InnoDB. Это несколько затрудняет правильное создание резервной копии моментального снимка. Есть два варианта:

Моментальный снимок холодного резервного копирования

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

Горячий резервный снимок

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

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

  1. Создайте новый сеанс и заблокируйте все таблицы с помощью команды FLUSH TABLES WITH READ LOCK. Этот сеанс нельзя закрывать, иначе блокировки будут сняты.

  2. При необходимости запишите текущую позицию binlog, выполнив команду SHOW MASTER STATUS.

  3. Создайте снимки всех томов, на которых расположены файлы базы данных MySQL, в соответствии с руководством по системе хранения.

  4. Разблокируйте таблицы с помощью команды UNLOCK TABLES в первоначально открытом сеансе.

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

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

Percona XtraBackup

Логическим шагом вперед в физическом резервном копировании является внедрение так называемого горячего резервного копирования, т. е. создание копии файлов базы данных во время работы базы данных. Мы уже упоминали, что таблицы MyISAM можно копировать, но это не работает для InnoDB и других механизмов хранения транзакций, таких как MyRocks. Поэтому проблема в том, что вы не можете просто скопировать файлы, потому что база данных постоянно претерпевает изменения. Например, InnoDB может сбрасывать некоторые грязные страницы в фоновом режиме, даже если прямо сейчас в базу данных не поступает никаких записей. Вы можете испытать удачу и скопировать каталог базы данных в работающей системе, а затем попытаться восстановить этот каталог и запустить сервер MySQL, используя его. Скорее всего, это не сработает. И хотя иногда это может сработать, мы настоятельно рекомендуем не рисковать с резервным копированием базы данных.

Возможность выполнения горячего резервного копирования встроена в три основных инструмента резервного копирования MySQL: Percona XtraBackup, MySQL Enterprise Backup и mariabackup. Мы кратко расскажем обо всех из них, но в основном сконцентрируемся на утилите XtraBackup. Важно понимать, что все инструменты имеют общие свойства, поэтому знание того, как использовать один, поможет вам использовать другие.

Percona XtraBackup — это бесплатный инструмент с открытым исходным кодом, поддерживаемый Percona и более широким сообществом MySQL. Он способен выполнять онлайн-резервное копирование экземпляров MySQL с таблицами InnoDB, MyISAM и MyRocks. Программа доступна только для Linux. Обратите внимание, что невозможно использовать XtraBackup с последними версиями MariaDB: поддерживаются только MySQL и Percona Server. Для MariaDB используйте утилиту, которую мы рассматриваем в «mariabackup».

Вот обзор того, как работает утилита XtraBackup:

  1. Записывает текущий порядковый номер журнала (LSN), внутренний номер версии операции.

  2. Начинает накапливать данные повторного выполнения InnoDB (тип данных, которые InnoDB хранит для восстановления после сбоя)

  3. Блокирует таблицы наименее навязчивым способом

  4. Копирует таблицы InnoDB

  5. Полностью блокирует нетранзакционные таблицы

  6. Копирует таблицы MyISAM

  7. Разблокирует все таблицы

  8. Обрабатывает таблицы MyRocks, если они есть

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

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

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

Резервное копирование и восстановление

В отличие от других инструментов, о которых мы упоминали ранее, XtraBackup, по своей природе являющийся инструментом физического резервного копирования, требует не только доступа к серверу MySQL, но и доступа для чтения к файлам базы данных. В большинстве установок MySQL это обычно означает, что программа xtrabackup должна запускаться пользователем root или должна использоваться sudo. В этом разделе мы будем использовать пользователя root и настроим путь входа в систему, используя шаги из раздела «Файл конфигурации пути входа в систему».

Во-первых, нам нужно запустить основную команду xtrabackup:

# xtrabackup --host=127.0.0.1 --target-dir=/tmp/backup --backup
...
Using server version 8.0.25
210613 22:23:06 Executing LOCK INSTANCE FOR BACKUP...
...
210613 22:23:07 [01] Copying ./sakila/film.ibd to /tmp/backup/sakila/film.ibd
210613 22:23:07 [01]        ...done
...
210613 22:23:10 [00] Writing /tmp/backup/xtrabackup_info
210613 22:23:10 [00]        ...done
xtrabackup: Transaction log of lsn (6438976119) to (6438976129) was copied.
210613 22:23:11 completed OK!

Если путь входав систему не работает, вы можете передать учетные данные пользователя root в xtrabackup, используя аргументы командной строки --user и --password. XtraBackup обычно может определить каталог данных целевого сервера, прочитав файлы опций по умолчанию, но если это не работает или у вас несколько установок MySQL, вам может потребоваться также указать параметр --datadir. Несмотря на то, что xtrabackup работает только локально, ему все же необходимо подключиться к локально работающему экземпляру MySQL, и поэтому он имеет аргументы --host, --port и --socket. Возможно, вам придется указать некоторые из них в соответствии с вашей конкретной настройкой.

Результатом этого вызова xtrabackup --backup является набор файлов базы данных, которые на самом деле не соответствуют ни одному моменту времени, и фрагмент данных повторного выполнения, которые InnoDB не сможет применить:

# ls -l /tmp/backup/
...
drwxr-x---.  2 root root       160 Jun 13 22:23 mysql
-rw-r-----.  1 root root  46137344 Jun 13 22:23 mysql.ibd
drwxr-x---.  2 root root        60 Jun 13 22:23 nasa
drwxr-x---.  2 root root       580 Jun 13 22:23 sakila
drwxr-x---.  2 root root       580 Jun 13 22:23 sakila_mod
drwxr-x---.  2 root root        80 Jun 13 22:23 sakila_new
drwxr-x---.  2 root root        60 Jun 13 22:23 sys
...

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

# xtrabackup --target-dir=/tmp/backup --prepare
...
xtrabackup: cd to /tmp/backup/
xtrabackup: This target seems to be not prepared yet.
...
Shutdown completed; log sequence number 6438976524
210613 22:32:23 completed OK!

Результирующий каталог данных фактически полностью готов к использованию. Вы можете запустить экземпляр MySQL, указывающий непосредственно на этот каталог, и он будет работать. Очень распространенной ошибкой здесь является попытка запустить MySQL Server под пользователем mysql, в то время как восстановленная и подготовленная резервная копия принадлежит пользователю root или другому пользователю ОС. Не забудьте включить chown и chmod в процедуру восстановления из резервной копии. Тем не менее, есть полезная функция --copy-back. xtrabackup сохраняет исходные расположения файлов базы данных, а вызов --copy-back восстановит все файлы в их исходные расположения:

# xtrabackup --target-dir=/tmp/backup --copy-back
...
Original data directory /var/lib/mysql is not empty!

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

# systemctl stop mysqld
# mv /var/lib/mysql /var/lib/mysql_old
# xtrabackup --target-dir=/tmp/backup --copy-back
...
210613 22:39:01 [01] Copying ./sakila/actor.ibd to /var/lib/mysql/sakila/actor.ibd
210613 22:39:01 [01]        ...done
...
210613 22:39:01 completed OK!

После этого файлы находятся в своих правильных местах, но принадлежат пользователю root:

# ls -l /var/lib/mysql/
drwxr-x---. 2 root root     4096 Jun 13 22:39 sakila
drwxr-x---. 2 root root     4096 Jun 13 22:38 sakila_mod
drwxr-x---. 2 root root     4096 Jun 13 22:39 sakila_new

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

# chown -R mysql:mysql /var/lib/mysql/
# chmod o+rx /var/lib/mysql/
# systemctl start mysqld
# mysql sakila -e "SHOW TABLES;"
+----------------------------+
| Tables_in_sakila           |
+----------------------------+
| actor                      |
...
| store                      |
+----------------------------+

Расширенные возможности

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

Проверка файла базы данных

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

Сжатие

Несмотря на то, что копирование физических файлов происходит намного быстрее, чем запрос к базе данных, процесс резервного копирования может быть ограничен производительностью диска. Вы не можете уменьшить объем данных, которые вы читаете, но вы можете использовать сжатие, чтобы сделать саму резервную копию меньше, уменьшив объем данных, которые необходимо записать. Это особенно важно, когда местом назначения резервного копирования является сетевое расположение. Кроме того, вы просто будете использовать меньше места для хранения резервных копий. Обратите внимание, что, как мы показали в разделе «Программа mysqldump», сжатие системы с перегрузкой ЦП может фактически увеличить время, необходимое для создания резервной копии. XtraBackup использует инструмент qpress для сжатия. Этот инструмент доступен в пакете percona-release:

# xtrabackup --host=127.0.0.1 \
--target-dir=/tmp/backup_compressed/ \
--backup --compress
Параллелизм

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

Шифрование

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

Потоковая передача

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

Облачная загрузка

Резервные копии, сделанные с помощью XtraBackup, можно загружать в любое совместимое с S3 хранилище с помощью xbcloud. S3 — это объектное хранилище Amazon и API, широко используемое многими компаниями. Этот инструмент работает только с резервными копиями, передаваемыми через xbstream.

Инкрементное резервное копирование с помощью XtraBackup

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

Чтобы инкрементное резервное копирование работало, вам сначала нужно иметь полную резервную копию базы данных, называемую базовой резервной копией (base backup), иначе не из чего будет увеличиваться. Когда ваша базовая резервная копия будет готова, вы можете выполнить любое количество инкрементных резервных копий, каждая из которых будет состоять из изменений, внесенных с момента предыдущей (или из базовой резервной копии в случае первой инкрементной резервной копии). В крайнем случае вы можете создавать инкрементную резервную копию каждую минуту, достигая так называемого восстановления на момент времени (point-in-time recovery) (PITR), но это не очень практично, и, как вы скоро узнаете, есть лучшие способы сделать это.

Вот пример команд XtraBackup, которые можно использовать для создания базовой резервной копии, а затем добавочной резервной копии. Обратите внимание, как инкрементная резервная копия указывает на базовую резервную копию через аргумент --incremental-basedir:

# xtrabackup --host=127.0.0.1 \
--target-dir=/tmp/base_backup --backup
# xtrabackup --host=127.0.0.1 --backup \
--incremental-basedir=/tmp/base_backup \
--target-dir=/tmp/inc_backup1

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

# du -sh /tmp/base_backup
2.2G    /tmp/base_backup
6.0M    /tmp/inc_backup1

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

# xtrabackup --host=127.0.0.1 --backup \
--incremental-basedir=/tmp/inc_backup1 \
--target-dir=/tmp/inc_backup2
210613 23:32:20 completed OK!

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

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

# xtrabackup --prepare --apply-log-only \
--target-dir=/tmp/base_backup

Аргумент --apply-log-only указывает xtrabackup не завершать процесс подготовки, поскольку нам все еще нужно применить изменения из инкрементных резервных копий. Давайте сделаем это:

# xtrabackup --prepare --apply-log-only \
--target-dir=/tmp/base_backup \
--incremental-dir=/tmp/inc_backup1
# xtrabackup --prepare --apply-log-only \
--target-dir=/tmp/base_backup \
--incremental-dir=/tmp/inc_backup2

Все команды должны сообщать completed OK! в конце. После запуска операции --prepare --apply-log-only базовая резервная копия переходит к точке инкрементной резервной копии, что делает невозможным PITR на более раннее время. Таким образом, не рекомендуется сразу же готовиться к выполнению инкрементого резервного копирования. Чтобы завершить процесс подготовки, базовая резервная копия с изменениями, примененными к инкрементным резервным копиям, должна быть подготовлена обычным образом:

# xtrabackup --prepare --target-dir=/tmp/base_backup

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

xtrabackup: This target seems to be already prepared.
xtrabackup: error: applying incremental backup needs target prepared with --apply-log-only.

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

Другие инструменты физического резервного копирования

XtraBackup — не единственный доступный инструмент, способный выполнять «горячее» физическое резервное копирование MySQL. Наше решение объяснить концепции с помощью этого конкретного инструмента было продиктовано нашим опытом работы с ним. Однако это не означает, что другие инструменты хуже. Возможно, они лучше подходят для ваших нужд. Однако место у нас ограничено, а тема резервного копирования очень широка. Мы могли бы написать книгу Резервное копирование MySQL значительного объема!

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

MySQL Enterprise Backup

Этот инструмент, для краткости называемый MEB, доступен как часть Oracle MySQL Enterprise Edition. Это проприетарный инструмент с закрытым исходным кодом, по функциональности аналогичный XtraBackup. Вы найдете исчерпывающую документацию по нему на веб-сайте MYSQL. В настоящее время эти два инструмента имеют равные возможности, поэтому почти все, что было описано для XtraBackup, будет справедливо и для MEB.

Выдающимся свойством MEB является то, что это действительно кроссплатформенное решение. XtraBackup работает только в Linux, тогда как MEB также работает в Solaris, Windows, macOS и FreeBSD. MEB не поддерживает разновидности MySQL, кроме стандартной версии Oracle.

Некоторые дополнительные функции MEB, недоступные в XtraBackup, включают следующее:

mariabackup

mariabackup — это инструмент от MariaDB для резервного копирования баз данных MySQL. Первоначально созданный из XtraBackup, это бесплатный инструмент с открытым исходным кодом, доступный для Linux и Windows. Выдающимся свойством mariabackup является его бесперебойная работа с форком MySQL MariaDB, который продолжает значительно отличаться как от основного MySQL, так и от Percona Server. Поскольку это прямое ответвление XtraBackup, вы найдете много общего в том, как инструменты используются и в их свойствах. Некоторые новые функции XtraBackup, такие как шифрование резервных копий и исключение вторичного индекса, отсутствуют в mariabackup. Однако использование XtraBackup для резервного копирования MariaDB в настоящее время невозможно.

Восстановление на момент времени (Point-in-time recovery, PITR)

Теперь, когда вы знакомы с концепцией горячего резервного копирования, у вас есть почти все, что вам нужно, чтобы завершить свой набор инструментов для резервного копирования. До сих пор все типы резервного копирования, которые мы обсуждали, имели одинаковую черту — недостаток. Они позволяют восстановить только в тот момент времени, когда они были взяты. Если у вас есть две резервные копии, одна сделана в 23:00 в понедельник, а вторая в 23:00 во вторник, вы не сможете восстановить до 17:00 во вторник.

Помните пример отказа инфраструктуры, приведенный в начале главы? Теперь давайте усугубим ситуацию и скажем, что данные пропали, все диски вышли из строя, а репликации нет. Событие произошло в среду в 21:00. Без PITR и ежедневных резервных копий, сделанных в 23:00, это означает, что вы только что безвозвратно потеряли данные за целый день. Возможно, инкрементные резервные копии, сделанные с помощью XtraBackup, позволяют сделать эту проблему несколько менее выраженной, но они все же оставляют место для потери данных, и запускать их очень часто нецелесообразно.

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

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

Одна интересная особенность MySQL заключается в том, что она допускает PITR для логических резервных копий. В разделе «Загрузка данных из файла дампа SQL» обсуждается сохранение позиции binlog для подготовки реплик с помощью mysqldump. Эту же позицию в бинарном журнале можно использовать в качестве отправной точки для PITR. Каждый тип резервного копирования в MySQL подходит для PITR, в отличие от других баз данных. Чтобы упростить это свойство, обязательно запишите позицию binlog при создании резервной копии. Некоторые инструменты резервного копирования делают это за вас. При использовании тех, которые этого не делают, вы можете запустить SHOW MASTER STATUS, чтобы получить эти данные.

Технические сведения о бинарных журналах

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

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

На основе инструкций

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

На основе строк

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

Смешанный

В этом режиме «безопасные» операторы регистрируются как есть, а другие разбиваются.

Обычно в системах управления базами данных журнал транзакций используется для аварийного восстановления, репликации и PITR. Однако, поскольку MySQL поддерживает несколько механизмов хранения, его бинарные журналы нельзя использовать для аварийного восстановления. Вместо этого каждый движок поддерживает свой собственный механизм восстановления после сбоев. Например, MyISAM не является отказоустойчивым, тогда как InnoDB имеет свои собственные журналы повторного выполнения. Каждая транзакция в MySQL является распределенной транзакцией с двухфазной фиксацией, чтобы учесть этот многокомпонентный характер. Каждая зафиксированная транзакция гарантированно будет отражена в журналах повторов подсистемы хранения, если подсистема является транзакционной, а также в собственном журнале транзакций MySQL (бинарные журналы).

Подробнее о том, как работают бинарные журналы, мы поговорим в главе 13.

Сохранение бинарных журналов

Чтобы разрешить PITR, вы должны сохранять бинарные журналы, начиная с позиции binlog самой старой резервной копии. Есть несколько способов сделать это:

Определение цели PITR

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

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

Определение фактического конкретного времени, когда возникла проблема, может быть проблемой. Чаще всего единственный способ найти нужный момент времени — просмотреть бинарные журналы, записанные примерно в то время, когда возникла проблема. Например, если вы подозреваете, что таблица была удалена, вы можете искать имя таблицы, а затем любые операторы DDL, выполненные для этой таблицы, или конкретно оператор DROP TABLE.

Давайте проиллюстрируем этот пример. Во-первых, нам нужно удалить таблицу, поэтому мы удалим таблицу facilities, созданную нами в разделе «Загрузка данных из файлов с разделителями-запятыми». Однако перед этим мы вставим запись, которая наверняка отсутствует в оригинальной резервной копии:

mysql> INSERT INTO facilities(center)
    -> VALUES ('this row was not here before');
Query OK, 1 row affected (0.01 sec)
mysql> DROP TABLE nasa.facilities;
Query OK, 0 rows affected (0.02 sec)

Теперь мы могли бы вернуться и восстановить одну из резервных копий, которые мы сделали в этой главе, но тогда мы потеряли бы все изменения, внесенные в базу данных между этим моментом и DROP. Вместо этого мы будем использовать mysqlbinlog для проверки содержимого бинарных журналов и поиска цели восстановления непосредственно перед выполнением оператора DROP. Чтобы найти список бинарных журналов, доступных в каталоге данных, вы можете запустить следующую команду:

mysql> SHOW BINARY LOGS;
+---------------+-----------+-----------+
| Log_name      | File_size | Encrypted |
+---------------+-----------+-----------+
| binlog.000291 |       156 | No        |
| binlog.000292 |       711 | No        |
+---------------+-----------+-----------+
2 rows in set (0.00 sec)

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

# cd /var/lib/mysql
# mysqlbinlog binlog.000291 binlog.000292 \
-vvv --base64-output='decode-rows' > /tmp/mybinlog.sql

Изучив выходной файл, мы можем найти проблемное утверждение:

...
#210613 23:32:19 server id 1  end_log_pos 200 ... Rotate to binlog.000291
...
# at 499
#210614 0:46:08 server id 1 end_log_pos 576 ...
# original_commit_timestamp=1623620769019544 (2021-06-14 00:46:09.019544 MSK)
# immediate_commit_timestamp=1623620769019544 (2021-06-14 00:46:09.019544 MSK)
/*!80001 SET @@session.original_commit_timestamp=1623620769019544*//*!*/;
/*!80014 SET @@session.original_server_version=80025*//*!*/;
/*!80014 SET @@session.immediate_server_version=80025*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 576
#210614 0:46:08 server id 1 end_log_pos 711 ... Xid = 25
use `nasa`/*!*/;
SET TIMESTAMP=1623620768/*!*/;
DROP TABLE `facilities` /* generated by server */
/*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
...

Мы должны остановить восстановление до 2021-06-14 00:46:08 или на позиции бинарного журнала 499. Нам также потребуются все бинарные журналы из последней резервной копии, включая binlog.00291. Используя эту информацию, мы можем перейти к восстановлению и восстановлению резервной копии.

Пример восстановления на момент времени: XtraBackup

Сам по себе XtraBackup не предоставляет возможности PITR. Вам нужно добавить дополнительный шаг запуска mysqlbinlog для воспроизведения содержимого binlog в восстановленной базе данных:

  1. Восстановите резервную копию. Точные шаги см. в разделе «Резервное копирование и восстановление».

  2. Запустите сервер MySQL. Если вы восстанавливаете исходный экземпляр напрямую, рекомендуется использовать параметр --skip-networking, чтобы предотвратить доступ нелокальных клиентов к базе данных. В противном случае некоторые клиенты могут изменить базу данных до того, как вы закончите восстановление.

  3. Найдите позицию бинарного журнала резервной копии. Она доступна в файле xtrabackup_binlog_info в каталоге резервного копирования:

    # cat /tmp/base_backup/xtrabackup_binlog_info
    binlog.000291   156
  4. Найдите отметку времени или позицию в бинарном журнале, до которой вы хотите восстановиться, например, непосредственно перед выполнением DROP TABLE, как обсуждалось ранее.

  5. Воспроизведите binlog до нужной точки. Для этого примера мы сохранили бинарный журнал binlog.000291 отдельно, но вы должны использовать свое централизованное хранилище бинарных журналов в качестве источника бинарных журналов. Для этого вы используете команду mysqlbinlog:

    # mysqlbinlog /opt/mysql/binlog.000291 \
    /opt/mysql/binlog.000292 --start-position=156 \
    --stop-datetime="2021-06-14 00:46:00" | mysql
  6. Убедитесь, что восстановление прошло успешно и данные отсутствуют. В нашем случае мы будем искать запись, которую мы добавили в таблицу объектов, прежде чем удалить ее:

    mysql> SELECT center FROM facilities
        -> WHERE center LIKE '%before%';
    +------------------------------+
    | center                       |
    +------------------------------+
    | this row was not here before |
    +------------------------------+
    1 row in set (0.00 sec)

Пример восстановления на момент времени: mysqldump

Шаги, необходимые для PITR с mysqldump, аналогичны шагам, предпринятым ранее с XtraBackup. Мы показываем это только для полноты и чтобы вы могли видеть, что PITR одинаков для каждого типа резервного копирования в MySQL. Вот процесс:

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

  2. Найдите позицию бинарного журнала в файле резервной копии mysqldump:

    CHANGE MASTER TO MASTER_LOG_FILE='binlog.000010',
    MASTER_LOG_POS=191098797;
  3. Найдите отметку времени или позицию в бинарном журнале, до которой вы хотите восстановиться (например, непосредственно перед выполнением DROP TABLE, как обсуждалось ранее).

  4. Воспроизведите бинарные логи до нужной точки:

    # mysqlbinlog /path/to/datadir/mysql-bin.000010 \
    /path/to/datadir/mysql-bin.000011 \
    --start-position=191098797 \
    --stop-datetime="20-05-25 13:00:00" | mysql

Экспорт и импорт табличных пространств InnoDB

Одним из основных недостатков физических резервных копий является то, что они обычно требуют одновременного копирования значительной части файлов базы данных. Хотя механизм хранения, такой как MyISAM, позволяет копировать файлы данных простаивающих таблиц, вы не можете гарантировать согласованность файлов InnoDB. Однако бывают ситуации, когда вам нужно перенести только несколько таблиц или только одну таблицу. Пока единственный вариант, который мы видели для этого, — это использование логических резервных копий, которые могут быть неприемлемо медленными. Функция экспорта и импорта табличных пространств InnoDB, официально называемая Transportable Tablespace, — это способ получить лучшее из обоих миров. Мы также будем называть эту функцию экспортом/импортом для краткости.

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

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

Техническое образование

Чтобы помочь вам понять, как работает эта функция, мы кратко рассмотрим две концепции: физические резервные копии и табличные пространства.

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

Табличное пространство — это файл, в котором хранятся данные таблицы и ее индексы. По умолчанию InnoDB использует параметр innodb_file_per_table, который принудительно создает отдельный файл табличного пространства для каждой таблицы. Можно создать табличное пространство, которое будет содержать данные для нескольких таблиц, и вы можете использовать «старое» поведение, когда все таблицы находятся в одном табличном пространстве ibdata. Однако экспорт поддерживается только для конфигурации по умолчанию, где для каждой таблицы есть выделенное табличное пространство. Табличные пространства существуют отдельно для каждого раздела в многораздельной таблице, что дает интересную возможность переноса разделов между отдельными таблицами или создания таблицы из раздела.

Экспорт табличного пространства

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

Шаги для экспорта табличного пространства:

  1. Получите определение таблицы.

  2. Остановите все записи в таблицу (или таблицы) и сделайте ее согласованной.

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

    • В файле .cfg хранятся метаданные, используемые для проверки схемы.

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

Чтобы получить определение таблицы, вы можете использовать команду SHOW CREATE TABLE, которую мы неоднократно показывали в этой книге. Все остальные шаги выполняются MySQL автоматически с помощью одной команды: FLUSH TABLE ... FOR EXPORT. Эта команда блокирует таблицу и создает дополнительный требуемый файл (или файлы, если используется шифрование) рядом с обычным файлом .ibd целевой таблицы. Давайте экспортируем таблицу actor из базы данных sakila:

mysql> USE sakila
mysql> FLUSH TABLE actor FOR EXPORT;
Query OK, 0 rows affected (0.00 sec)

Сеанс, в котором был выполнен оператор FLUSH TABLE, должен оставаться открытым, потому что таблица actor будет освобождена, как только сеанс завершится. Рядом с обычным файлом actor.ibd в каталоге данных MySQL должен появиться новый файл actor.cfg. Давайте проверим:

# ls -1 /var/lib/mysql/sakila/actor.
/var/lib/mysql/sakila/actor.cfg
/var/lib/mysql/sakila/actor.ibd

Эту пару файлов .ibd и .cfg теперь можно куда-нибудь скопировать и использовать позже. После того, как вы скопировали файлы, обычно рекомендуется снять блокировки таблицы, выполнив оператор UNLOCK TABLES или закрыв сеанс, в котором был вызван FLUSH TABLE. Когда все это будет сделано, у вас будет готовое к импорту табличное пространство.

Импорт табличного пространства

Импорт табличного пространства довольно прост. Он состоит из следующих шагов:

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

  2. Удалите табличное пространство таблицы.

  3. Скопируйте файлы .ibd и .cfg.

  4. Измените таблицу, чтобы импортировать табличное пространство.

Если таблица существует на целевом сервере и имеет то же определение, то нет необходимости выполнять шаг 1.

Давайте восстановим таблицу actor в другой базе данных на том же сервере. Таблица должна существовать, поэтому мы ее создадим:

mysql> USE nasa
mysql> CREATE TABLE `actor` (
    -> `actor_id` smallint unsigned NOT NULL AUTO_INCREMENT,
    -> `first_name` varchar(45) NOT NULL,
    -> `last_name` varchar(45) NOT NULL,
    -> `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
    ->   ON UPDATE CURRENT_TIMESTAMP,
    -> PRIMARY KEY (`actor_id`),
    -> KEY `idx_actor_last_name` (`last_name`)
    -> ) ENGINE=InnoDB AUTO_INCREMENT=201 DEFAULT
    ->    CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Query OK, 0 rows affected (0.04 sec)

Как только таблица actor создана, MySQL создает для нее файл .ibd:

# ls /var/lib/mysql/nasa/
actor.ibd  facilities.ibd

Это подводит нас к следующему шагу: удаление табличного пространства этой новой таблицы. Это делается запуском специального оператора ALTER TABLE:

mysql> ALTER TABLE actor DISCARD TABLESPACE;
Query OK, 0 rows affected (0.02 sec)

Теперь файл .ibd исчез:

# ls /var/lib/mysql/nasa/
facilities.ibd

Теперь мы можем скопировать экспортированное табличное пространство исходной таблицы actor вместе с файлом .cfg:

# cp -vip /opt/mysql/actor.* /var/lib/mysql/nasa/
'/opt/mysql/actor.cfg' -> '/var/lib/mysql/nasa/actor.cfg'
'/opt/mysql/actor.ibd' -> '/var/lib/mysql/nasa/actor.ibd'

После выполнения всех шагов можно импортировать табличное пространство и проверить данные:

mysql> ALTER TABLE actor IMPORT TABLESPACE;
Query OK, 0 rows affected (0.02 sec)
mysql> SELECT * FROM nasa.actor LIMIT 5;
+----------+------------+--------------+---------------------+
| actor_id | first_name | last_name    | last_update         |
+----------+------------+--------------+---------------------+
|        1 | PENELOPE   | GUINESS      | 2006-02-15 04:34:33 |
|        2 | NICK       | WAHLBERG     | 2006-02-15 04:34:33 |
|        3 | ED         | CHASE        | 2006-02-15 04:34:33 |
|        4 | JENNIFER   | DAVIS        | 2006-02-15 04:34:33 |
|        5 | JOHNNY     | LOLLOBRIGIDA | 2006-02-15 04:34:33 |
+----------+------------+--------------+---------------------+
5 rows in set (0.00 sec)

Вы можете видеть, что у нас есть данные из sakila.actor в nasa.actor.

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

Восстановление одной таблицы XtraBackup

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

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

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

# xtrabackup --prepare --export --target-dir=/tmp/base_backup
# ls -1 /tmp/base_backup/sakila/
actor.cfg
actor.ibd
address.cfg
address.ibd
category.cfg
category.ibd
...

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

PITR с одной таблицей или частичной базой данных сложен, и это верно для большинства существующих систем управления базами данных. Как вы видели в разделе «Восстановление на момент времени», PITR в MySQL основан на бинарных журналах. Что это означает для частичного восстановления, так это то, что транзакции, касающиеся всех таблиц во всех базах данных, записываются, но бинарные журналы могут быть отфильтрованы при применении через репликацию. Поэтому очень кратко процедура частичного восстановления такова: вы экспортируете нужные таблицы, строите полностью отдельный инстанс и кормите его бинарными логами через канал репликации.

Дополнительную информацию можно найти в блогах сообщества и в таких статьях, как MySQL Single Table PITR, Filtering Binary Logs with MySQL и How to Make MySQL PITR Faster.

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

Тестирование и проверка ваших резервных копий

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

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

Несогласованные резервные копии

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

Повреждение исходной базы данных

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

Повреждение резервных копий

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

Ошибки

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

Операционные ошибки

Все мы люди, и мы делаем ошибки. Если все автоматизировать, риск здесь меняется с человеческих ошибок на баги.

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

Последний пункт особенно интересен. Ни одна резервная копия не должна считаться безопасной, пока она не будет восстановлена и протестирована. Это означает, что в идеальном мире ваша автоматизация на самом деле попытается использовать резервную копию для создания сервера базы данных и сообщит об успехе только тогда, когда все пойдет хорошо. Кроме того, эту новую базу данных можно подключить к источнику в качестве реплики, а для проверки согласованности данных можно использовать инструмент проверки данных, такой как pt-table-checksum из Percona Toolkit.

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

  1. Подготовьте резервную копию.

  2. Восстановите резервную копию.

  3. Запустите innochecksum для всех файлов .ibd.

    Следующая команда запустит четыре процесса innochecksum параллельно в Linux:

    $ find . -type f -name "*.ibd" -print0 |\
    xargs -t -0r -n1 --max-procs=4 innochecksum
  4. Запустите новый экземпляр MySQL, используя восстановленную резервную копию. Используйте запасной сервер или просто выделенный файл .cnf и не забудьте использовать нестандартные порты и пути.

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

  6. Присоедините новый экземпляр MySQL в качестве реплики к оригинальной исходной базе данных и используйте pt-table-checksum или любую другую альтернативу для проверки соответствия данных. Процедура хорошо описана в документации по xtrabackup, среди других источников.

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

Руководство по стратегии резервного копирования баз данных

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

Восстановление на момент времени

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

Логические резервные копии

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

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

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

Физические резервные копии

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

Первое решение, которое необходимо принять, — это насколько важно для нас целевое среднее время восстановления (mean time to recovery) (MTTR). Например, если мы делаем только еженедельные базовые резервные копии, нам может потребоваться применение почти недельных транзакций для восстановления резервной копии. Чтобы уменьшить MTTR, выполняйте инкрементное резервное копирование ежедневно или, возможно, даже ежечасно.

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

Резервное хранилище

Нам нужно убедиться, что наши резервные копии хранятся безопасно и, в идеале, с избыточностью. Мы можем добиться этого с помощью аппаратной настройки хранилища с использованием менее производительного, но избыточного RAID-массива уровня 5 или 6, или с менее надежной настройкой хранилища, если мы также будем постоянно передавать наши резервные копии в облачное хранилище, такое как Amazon S3. Или мы можем просто по умолчанию использовать S3, если это возможно для нас с выбранными инструментами резервного копирования.

Резервное копирование и проверка

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

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

Глава 11
Конфигурирование и настройка сервера

Процесс установки MySQL (см. главу 1) предоставляет все необходимое для установки процесса MySQL и начала его использования. Однако для производственных систем требуется тонкая настройка, настройка параметров MySQL и операционной системы для оптимизации производительности MySQL Server. В этой главе будут рассмотрены рекомендуемые передовые методы для различных установок и показаны параметры, которые необходимо настроить в зависимости от ожидаемой или текущей рабочей нагрузки. Как вы увидите, нет необходимости запоминать все параметры MySQL. Основываясь на принципе Парето, который гласит, что для многих событий примерно 80% эффектов исходят из 20% причин, мы сосредоточимся на параметрах MySQL и операционной системы, которые ответственны за большинство проблем с производительностью. В этой главе есть несколько дополнительных тем, связанных с архитектурой компьютера (например, NUMA); цель здесь состоит в том, чтобы познакомить вас с несколькими компонентами, которые могут повлиять на производительность MySQL, с которыми вам рано или поздно придется взаимодействовать в вашей карьере.

Демон сервера MySQL

С 2015 года большинство дистрибутивов Linux используют systemd. Из-за этого операционные системы Linux больше не используют процесс mysqld_safe для запуска MySQL. mysqld_safe называется ангельским процессом, потому что он добавляет некоторые функции безопасности, такие как перезапуск сервера при возникновении ошибки и запись информации о времени выполнения в журнал ошибок MySQL. Для операционных систем, использующих systemd (управляемых и настраиваемых с помощью команды systemctl), эти функции были включены в systemd и процесс mysqld.

mysqld — это основной процесс MySQL Server. Это одна многопоточная программа, которая выполняет большую часть работы на сервере. Он не порождает дополнительных процессов — мы говорим об одном процессе с несколькими потоками, что делает MySQL многопоточным процессом.

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

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

Поток — это единица выполнения внутри процесса. Процесс может иметь только один поток или много потоков. В однопоточных процессах процесс содержит один поток, поэтому одновременно выполняется только одна команда.

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

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

Переменные сервера MySQL

MySQL Server имеет множество переменных, которые позволяют настраивать его работу. Например, MySQL Server 8.0.25 имеет впечатляющие 588 серверных переменных!

Каждая системная переменная имеет значение по умолчанию. Кроме того, мы можем настроить большинство системных переменных динамически (или «на лету»); однако некоторые из них являются статическими, а это означает, что нам нужно изменить файл my.cnf и перезапустить процесс MySQL, чтобы они вступили в силу (как обсуждалось в главе 9).

Системные переменные могут иметь две разные области видимости: SESSION и GLOBAL. То есть системная переменная может иметь глобальное значение, влияющее на работу сервера в целом, например innodb_log_file_size, или значение сеанса, влияющее только на конкретный сеанс, например sql_mode.

Проверка настроек сервера

Базы данных не являются статическими объектами; напротив, их нагрузка динамична и меняется во времени, с тенденцией к росту. Это органичное поведение требует постоянного наблюдения, анализа и корректировки. Команда для отображения настроек MySQL:

SHOW [GLOBAL|SESSION] VARIABLES;

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

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

Лучшие практики

Есть много аспектов, которые нужно оптимизировать в базе данных. Если база данных работает на голом железе (физическом хосте), мы можем контролировать аппаратные ресурсы и ресурсы операционной системы. Когда мы переходим на виртуализированные машины, мы уменьшаем контроль над этими ресурсами, потому что не можем контролировать то, что происходит с базовым хостом. Последний вариант — это управляемые базы данных в облаке, подобные тем, которые предоставляет Amazon Relational Database Service (RDS), где доступно лишь несколько настроек базы данных. Существует компромисс между возможностью выполнять тонкую настройку для получения максимальной производительности и удобством автоматизации большинства задач (за счет нескольких дополнительных долларов).

Начнем с обзора некоторых настроек на уровне операционной системы. После этого мы проверим параметры MySQL.

Лучшие практики для операционных систем

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

Настройка подкачки и использование подкачки

Параметр swappiness управляет поведением операционной системы Linux в области подкачки. Подкачка — это процесс передачи данных между памятью и диском. Это может существенно повлиять на производительность, поскольку доступ к диску (даже NVMe) как минимум на порядок медленнее, чем доступ к памяти.

Значение по умолчанию (60) побуждает сервер к подкачке. Вы захотите, чтобы ваш сервер MySQL по соображениям производительности поддерживал подкачку на минимальном уровне. Рекомендуемое значение равно 1, что означает: не выполнять подкачку до тех пор, пока это не станет абсолютно необходимым для функционирования операционной системы. Чтобы настроить этот параметр, выполните следующую команду от имени пользователя root:

# echo 1 > /proc/sys/vm/swappiness

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

# sudo sysctl -w vm.swappiness=1

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

# free -m

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

#!/bin/bash
SUM=0
OVERALL=0
for DIR in `find /proc/ -maxdepth 1 -type d | egrep "^/proc/[0-9]"` ; do
    PID=`echo $DIR | cut -d / -f 3`
    PROGNAME=`ps -p $PID -o comm --no-headers`
    for SWAP in `grep Swap $DIR/smaps 2>/dev/null| awk '{ print $2 }'`
    do
        let SUM=$SUM+$SWAP
    done
    echo "PID=$PID - Swap used: $SUM - ($PROGNAME )"
    let OVERALL=$OVERALL+$SUM
    SUM=0
done
echo "Overall swap used: $OVERALL"
Планировщик ввода/вывода

Планировщик ввода-вывода — это алгоритм, который ядро использует для фиксации операций чтения и записи на диск. По умолчанию в большинстве установок Linux используется планировщик Completely Fair Queuing (cfq). Он хорошо работает для многих общих случаев использования, но дает мало гарантий задержки. Два других планировщика — это deadline и noop. Планировщик deadline превосходен в случаях использования, чувствительных к задержкам (например, в базах данных), а noop ближе к полному отсутствию планирования. Для установок на физическом хосте лучше будет либо deadline, либо noop (разница в производительности между ними незаметна), чем cfq.

Если вы используете MySQL на виртуальной машине (у которой есть собственный планировщик ввода-вывода), лучше всего использовать noop и позволить уровню виртуализации самому позаботиться о планировании ввода-вывода.

Во-первых, проверьте, какой алгоритм в настоящее время используется в Linux:

# cat /sys/block/xvda/queue/scheduler
noop [deadline] cfq

Чтобы изменить его динамически, запустите эту команду от имени пользователя root:

# echo "noop" > /sys/block/xvda/queue/scheduler

Чтобы сделать это изменение постоянным, вам нужно отредактировать файл конфигурации GRUB (обычно /etc/sysconfig/grub) и добавить опцию elevator в GRUB_CMDLINE_LINUX_DEFAULT. Например, замените эту строку:

GRUB_CMDLINE_LINUX="console=tty0 crashkernel=auto console=ttyS0,115200

строкой:

GRUB_CMDLINE_LINUX="console=tty0 crashkernel=auto console=ttyS0,115200 elevator=noop"

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

Файловые системы и параметры монтирования

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

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

Команда создания файловой системы XFS (mkfs.xfs) имеет несколько опций для настройки. Однако параметры по умолчанию для mkfs.xfs хороши для оптимальной скорости, поэтому команда по умолчанию для создания файловой системы обеспечит хорошую производительность при обеспечении целостности данных:

# mkfs.xfs /dev/target_volume

Что касается параметров монтирования файловой системы, значения по умолчанию снова должны подходить для большинства случаев. Вы можете увидеть увеличение производительности на некоторых файловых системах, добавив параметр монтирования noatime в файл /etc/fstab. Для файловых систем XFS поведением atime по умолчанию является relatime, которое почти не имеет накладных расходов по сравнению с noatime и по-прежнему поддерживает разумные значения atime. Если вы создаете файловую систему XFS на логическом устройстве с номером (LUN) с энергонезависимым кэшем с батарейным питанием, вы можете еще больше повысить производительность файловой системы, отключив барьер записи с помощью параметра монтирования nobarrier. Эти настройки помогают избежать сброса данных чаще, чем это необходимо. Однако, если блок резервной батареи (BBU) отсутствует или вы в нем не уверены, оставьте барьеры включенными; в противном случае вы можете поставить под угрозу целостность данных. В приведенном ниже примере показаны две воображаемые точки монтирования с этими параметрами:

/dev/sda2      /datastore     xfs   noatime,nobarrier
/dev/sdb2      /binlog        xfs   noatime,nobarrier

Другой популярный вариант — ext4, разработанный как преемник ext3 с дополнительными улучшениями производительности. Это надежный вариант, который подходит для большинства рабочих нагрузок. Здесь следует отметить, что он поддерживает файлы размером до 16 ТБ, что меньше, чем XFS. Это то, что вы должны учитывать, если требуется чрезмерный размер/рост табличного пространства. В отношении вариантов монтирования применяются те же соображения. Мы рекомендуем значения по умолчанию для надежной файловой системы без рисков для согласованности данных. Однако при наличии корпоративного контроллера системы хранения данных с кэшем BBU следующие параметры монтирования обеспечат наилучшую производительность:

/dev/sda2      /datastore      ext4  noatime,data=writeback,barrier=0,nobh,errors=remount-ro
/dev/sdb2      /binlog         ext4  noatime,data=writeback,barrier=0,nobh,errors=remount-ro
Transparent Huge Pages

Операционная система управляет памятью блоками, известными как страницы (pages). Страница имеет размер 4096 байт (или 4 КБ); 1 МБ памяти соответствует 256 страницам, 1 ГБ памяти соответствует 256 000 страниц и т. д. ЦП имеют встроенный модуль управления памятью, который содержит список этих страниц, причем ссылка на каждую страницу осуществляется через запись в таблице страниц. В настоящее время часто можно увидеть серверы с сотнями мегабайт или терабайтами памяти. Есть два способа разрешить системе управлять большими объемами памяти:

Первый метод является дорогостоящим, поскольку аппаратный блок управления памятью в современном процессоре поддерживает только сотни или тысячи записей в таблице страниц. Кроме того, аппаратные средства и алгоритмы управления памятью, которые хорошо работают с тысячами страниц (мегабайтами памяти), могут иметь проблемы с работой с миллионами (или даже миллиардами) страниц. Чтобы решить проблему масштабируемости, операционные системы начали использовать огромные страницы (huge pages). Проще говоря, огромные страницы — это блоки памяти, которые могут иметь размеры 2 МБ, 4 МБ, 1 ГБ и т. д. Использование памяти больших страниц увеличивает число попаданий кэша ЦП в резервный буфер транзакций (TLB).

Вы можете запустить cpuid для проверки кеша процессора и TLB:

# cpuid | grep "cache and TLB information" -A 5
   cache and TLB information (2):
      0x5a: data TLB: 2M/4M pages, 4-way, 32 entries
      0x03: data TLB: 4K pages, 4-way, 64 entries
      0x76: instruction TLB: 2M/4M pages, fully, 8 entries
      0xff: cache data is in CPUID 4
      0xb2: instruction TLB: 4K, 4-way, 64 entries

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

В частности, для MySQL использование THP не рекомендуется по нескольким причинам. Во-первых, базы данных MySQL используют небольшие страницы памяти (16 КБ), и использование THP может привести к чрезмерному вводу-выводу, поскольку MySQL считает, что обращается к 16 КБ, в то время как THP сканирует страницу большего размера. Кроме того, огромные страницы имеют тенденцию к фрагментации и снижению производительности. За прошедшие годы также было зарегистрировано несколько случаев, когда использование THP может привести к утечке памяти, что в конечном итоге приведет к сбою MySQL.

Чтобы отключить THP для RHEL/CentOS 6 и RHEL/CentOS 7, выполните следующие команды:

# echo "never" > /sys/kernel/mm/transparent_hugepage/enabled
# echo "never" > /sys/kernel/mm/transparent_hugepage/defrag

Чтобы гарантировать, что это изменение сохранится после перезагрузки сервера, вам нужно будет добавить флаг transparent_hugepage=never в параметры ядра (/etc/sysconfig/grub):

GRUB_CMDLINE_LINUX="console=tty0 crashkernel=auto console=ttyS0,115200 elevator=noop transparent_hugepage=never"

Создайте резервную копию существующего файла конфигурации GRUB2 (/boot/grub2/grub.cfg), а затем перестройте его заново. На машинах с BIOS это можно сделать с помощью следующей команды:

# grub2-mkconfig -o /boot/grub2/grub.cfg

Если THP по-прежнему не отключен, может потребоваться отключить настроенные службы:

# systemctl stop tuned
# systemctl disable tuned

Чтобы отключить THP для Ubuntu 20.04 (Focal Fossa), мы рекомендуем использовать пакет sysfsutils. Чтобы установить его, выполните следующую команду:

# apt install sysfsutils

Затем добавьте следующие строки в файл /etc/sysfs.conf:

kernel/mm/transparent_hugepage/enabled = never
kernel/mm/transparent_hugepage/defrag = never

Перезагрузите сервер и проверьте правильность настроек:

# cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]
# cat /sys/kernel/mm/transparent_hugepage/defrag
always defer defer+madvise madvise [never]
jemalloc

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

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

jemalloc — это распределитель памяти, в котором особое внимание уделяется предотвращению фрагментации и поддержке масштабируемого параллелизма.

Используя jemalloc (с отключенным THP), вы получаете меньшую фрагментацию памяти и более эффективное управление ресурсами доступной памяти сервера. Вы можете установить пакет jemalloc из репозитория jemalloc или репозитория Percona yum или apt. Мы предпочитаем использовать репозиторий Percona, поскольку считаем его более простым в установке и управлении. Мы описываем шаги по установке репозитория yum в разделе «Установка Percona Server 8.0», а репозитория apt — в разделе «Установка Percona Server 8».

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

Если вы используете Ubuntu 20.04, вам необходимо выполнить следующие шаги, чтобы включить jemalloc:

  1. Установить jemalloc:

    # apt-get install libjemalloc2
    # dpkg -L libjemalloc2
  2. Команда dpkg покажет расположение библиотеки jemalloc:

    # dpkg -L libjemalloc2
    /.
    /usr
    /usr/lib
    /usr/lib/x86_64-linux-gnu
    /usr/lib/x86_64-linux-gnu/libjemalloc.so.2
    /usr/share
    /usr/share/doc
    /usr/share/doc/libjemalloc2
    /usr/share/doc/libjemalloc2/README
    /usr/share/doc/libjemalloc2/changelog.Debian.gz
    /usr/share/doc/libjemalloc2/copyright
  3. Переопределить конфигурацию службы по умолчанию с помощью команды:

    # systemctl edit mysql

    которая создаст файл /etc/systemd/system/mysql.service.d/override.conf.

  4. Добавить в файл следующую конфигурацию:

    [Service]
    Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
  5. Перезапустить службу MySQL, чтобы включить библиотеку jemalloc:

    # systemctl restart mysql
  6. Чтобы убедиться, что это сработало, при запущенном процессе mysqld выполните следующую команду:

    # lsof -Pn -p $(pidof mysqld) | grep "jemalloc"

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

    mysqld 3844 mysql mem  REG  253,0 
    744776  36550  /usr/lib/x86_64-linux-gnu/libjemalloc.so.2

Если вы используете CentOS/RHEL, вам необходимо выполнить следующие шаги:

  1. Установить пакет jemalloc:

    # yum install jemalloc
    # rpm -ql jemalloc
  2. Команда rpm -ql покажет расположение библиотеки:

    /usr/bin/jemalloc.sh
    /usr/lib64/libjemalloc.so.1
    /usr/share/doc/jemalloc-3.6.0
    /usr/share/doc/jemalloc-3.6.0/COPYING
    /usr/share/doc/jemalloc-3.6.0/README
    /usr/share/doc/jemalloc-3.6.0/VERSION
    /usr/share/doc/jemalloc-3.6.0/jemalloc.html
  3. Переопределить конфигурацию службы по умолчанию с помощью команды:

    # systemctl edit mysqld

    которая создаст файл /etc/systemd/system/mysqld.service.d/override.conf.

  4. Добавить в файл следующую конфигурацию:

    [Service]
    Environment="LD_PRELOAD=/usr/lib64/libjemalloc.so.1"
  5. Перезапустить службу MySQL, чтобы включить библиотеку jemalloc:

    # systemctl restart mysqld
  6. Чтобы убедиться, что это сработало, при запущенном процессе mysqld выполните следующую команду:

    # lsof -Pn -p $(pidof mysqld) | grep "jemalloc"

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

    mysqld 4784 mysql mem  REG  253,0  212096  33985101  /usr/lib64/libjemalloc.so.1
Регулятор ЦПУ (CPU governor)

Один из наиболее эффективных способов снизить энергопотребление и тепловыделение вашей системы — использовать CPUfreq. CPUfreq, также называемая масштабированием частоты ЦП или масштабированием скорости ЦП, позволяет регулировать тактовую частоту процессора на лету. Эта функция позволяет системе работать на пониженной тактовой частоте для экономии энергии. Правила переключения частот — следует ли и когда переходить на более высокую или низкую тактовую частоту — определяются регулятором CPUfreq. Регулятор определяет характеристики питания системного ЦП, что, в свою очередь, влияет на производительность ЦП. Каждый регулятор имеет собственное уникальное поведение, назначение и пригодность с точки зрения рабочей нагрузки. Однако для баз данных MySQL мы рекомендуем использовать настройку максимальной производительности для достижения наилучшей пропускной способности.

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

# cat /sys/devices/system/cpu/cpu/cpufreq/scaling_governor

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

# cpupower frequency-set --governor performance

Для Ubuntu мы рекомендуем установить пакет linux-tools-common, чтобы у вас был доступ к утилите cpupower:

# apt install linux-tools-common

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

# cpupower frequency-set --governor performance
Лучшие практики MySQL

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

Размер буферного пула

Параметр innodb_buffer_pool_size управляет размером в байтах пула буферов InnoDB, области памяти, где InnoDB кэширует данные таблиц и индексов. Нет никаких сомнений в том, что для настройки InnoDB это самый важный параметр. Типичное эмпирическое правило состоит в том, чтобы установить его примерно на 70% от общего объема доступной оперативной памяти для выделенного сервера MySQL.

Однако чем больше сервер, тем больше вероятность того, что это приведет к пустой трате оперативной памяти. Например, для сервера с 512 ГБ ОЗУ для операционной системы останется 153 ГБ ОЗУ, что больше, чем ей нужно.

Так какое же правило лучше? Установите innodb_buffer_pool_size как можно большим, не вызывая подкачки, когда система выполняет рабочую нагрузку. Это потребует некоторой настройки.

В MySQL 5.7 и более поздних версиях это динамический параметр, поэтому вы можете изменять его на лету без необходимости перезапуска базы данных. Например, чтобы установить его на 1 ГБ, используйте эту команду:

mysql> SET global innodb_buffer_pool_size = 1024*1024*1024;
Query OK, 0 rows affected (0.00 sec)

Чтобы изменения сохранялись при перезапусках, вам нужно добавить этот параметр в my.cnf в разделе [mysqld]:

[mysqld]
innodb_buffer_pool_size = 1G
Параметр innodb_buffer_pool_instances

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

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

Если вы не запускаете тесты, подтверждающие прирост производительности, мы рекомендуем использовать значение по умолчанию (8).

Размер журнала повторов

Журнал повторов — это структура, используемая во время аварийного восстановления для исправления данных, записанных незавершенными транзакциями. Основная цель состоит в том, чтобы гарантировать свойство устойчивости (D) транзакций ACID путем обеспечения повторного восстановления для совершенных транзакций. Поскольку файл повторного выполнения регистрирует все данные, записанные в MySQL еще до фиксации, наличие правильного размера журнала повторного выполнения имеет основополагающее значение для бесперебойной работы MySQL без проблем. Недостаточный размер журнала повторов может даже привести к ошибкам в операциях!

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

[ERROR] InnoDB: The total blob data length (12299456) is greater than 10% of the total redo log size (100663296). Please increase total redo log size.

В этом случае MySQL использовал значение по умолчанию для параметра innodb_log_file_size, которое составляет 48 МБ. Для оценки оптимального размера журнала повторного выполнения существует формула, которую можно использовать в большинстве случаев. Взгляните на следующие команды:

mysql> pager grep sequence
PAGER set to 'grep sequence'
mysql> show engine innodb status\G select sleep(60);
    -> show engine innodb status\G
Log sequence number 3836410803
1 row in set (0.06 sec)
1 row in set (1 min 0.00 sec)
Log sequence number 3838334638
1 row in set (0.05 sec)

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

mysql> SELECT (((3838334638 - 3836410803)/1024/1024)*60)/2
    -> AS Estimated_innodb_log_file_size;
+--------------------------------+
| Estimated_innodb_log_file_size |
+--------------------------------+
|                55.041360855088 |
+--------------------------------+
1 row in set (0.00 sec)

Мы обычно округляем, поэтому окончательное число будет 56 МБ. Это значение необходимо добавить в my.cnf в разделе [mysqld]:

[mysqld]
innodb_log_file_size=56M
Параметр sync_binlog

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

По умолчанию сервер MySQL синхронизирует свой бинарный журнал с диском (используя fdatasync()) перед фиксацией транзакций. Преимущество в том, что в случае сбоя питания или сбоя операционной системы транзакции, отсутствующие в бинарном журнале, находятся только в подготовленном состоянии; это позволяет процедуре автоматического восстановления откатывать транзакции, гарантируя, что ни одна транзакция не будет потеряна из бинарного журнала. Однако значение по умолчанию (sync_binlog = 1) снижает производительность. Поскольку это динамический параметр, вы можете изменить его во время работы сервера с помощью следующей команды:

mysql> SET GLOBAL sync_binlog = 0;

Чтобы изменение сохранялось после перезагрузки, добавьте параметр в файл my.cnf в разделе [mysqld]:

[mysqld]
sync_binlog=0
Параметры binlog_expire_logs_seconds и expire_logs_days

Чтобы MySQL не заполнял весь диск бинарными логами, вы можете настроить параметры binlog_expire_logs_seconds и expire_logs_days. expire_logs_days указывает количество дней до автоматического удаления бинарных файлов журнала. Однако этот параметр устарел в MySQL 8.0, и вы должны ожидать, что он будет удален в будущем релизе.

Следовательно, лучше использовать binlog_expire_logs_seconds, который устанавливает срок действия бинарного журнала в секундах. Значение по умолчанию для этого параметра — 2592000 (30 дней). MySQL может автоматически удалить файлы бинарного журнала после истечения срока действия либо при запуске, либо при следующем сбросе бинарного журнала.

Параметр innodb_flush_log_at_trx_commit

innodb_flush_log_at_trx_commit управляет балансом между строгим соответствием ACID для операций фиксации и возможной более высокой производительностью, когда операции ввода-вывода, связанные с фиксацией, переупорядочиваются и выполняются пакетами. Это деликатный вариант, и многие предпочитают использовать значение по умолчанию (innodb_flush_log_at_trx_commit=1) на исходных серверах, тогда как для реплик они используют значение 0 или 2. Значение 2 указывает InnoDB записывать данные в файлы журнала после фиксации каждой транзакции, но сбрасывать их на диск только один раз в секунду. Это означает, что вы можете потерять до секунды обновлений в случае сбоя ОС, что при современном оборудовании, поддерживающем до миллиона вставок в секунду, не является незначительным. Значение 0 еще хуже: журналы записываются и сбрасываются на диск только раз в секунду, поэтому вы можете потерять транзакции продолжительностью до секунды, даже если процесс mysqld завершится сбоем.

Параметр innodb_thread_concurrency

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

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

innodb_thread_concurrency = Number of Cores * 2

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

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

innodb_thread_concurrency — это динамическая переменная, и мы можем изменять ее во время выполнения:

mysql> SET GLOBAL innodb_thread_concurrency = 0;

Чтобы сделать изменение постоянным, вам также необходимо добавить этот параметр в my.cnf в разделе [mysqld]:

[mysqld]
innodb-thread-concurrency=0

Вы можете проверить, что MySQL применил настройку с помощью этой команды:

mysql> SHOW GLOBAL VARIABLES LIKE '%innodb_thread_concurrency%';
Архитектура NUMA

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

Рисунок 11-1. Обзор архитектуры NUMA

Следующая команда показывает пример доступных узлов на сервере с включенной NUMA:

shell> numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 24 25 26 27 28 29 30 31 32 33 34 35
node 0 size: 130669 MB
node 0 free: 828 MB
node 1 cpus: 12 13 14 15 16 17 18 19 20 21 22 23 36 37 38 39 40 41 42 43 44 45 46
node 1 size: 131072 MB
node 1 free: 60 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

Как мы видим, в узле 0 больше свободной памяти, чем в узле 1. С этим связана проблема, из-за которой ОС выполняет подкачку даже при доступной памяти, как объясняется в отличной статье The MySQL Swap Insanity Problem and the Effects of the NUMA Architecture Джереми Коула.

В MySQL 5.7 параметры innodb_buffer_pool_populate и numa_interleave были удалены, и теперь их функции контролируются параметром innodb_numa_interleave. Когда мы включаем его, мы балансируем распределение памяти между узлами в системе NUMA, избегая проблемы безумия подкачки.

Этот параметр не является динамическим, поэтому для его включения нам нужно добавить его в файл my.cnf в разделе [mysqld] и перезапустить MySQL:

[mysqld]
innodb_numa_interleave = 1

Глава 12
Мониторинг серверов MySQL

Мониторинг можно определить как наблюдение или проверку качества или прогресса чего-либо в течение определенного периода времени. Применяя это определение к MySQL, мы наблюдаем и проверяем «здоровье» и производительность сервера. Таким образом, качество будет заключаться в поддержании времени безотказной работы и обеспечении производительности на желаемом уровне. Так что на самом деле мониторинг — это непрерывная попытка держать вещи под наблюдением и контролем. Обычно это считается чем-то необязательным, что может не понадобиться, если нет особенно высокой нагрузки или высоких ставок. Однако, как и резервное копирование, мониторинг приносит пользу почти каждой установке любой базы данных.

Мы думаем, что наличие мониторинга и понимание получаемых от него метрик — одна из самых важных задач для любого, кто работает с системой баз данных — вероятно, сразу после настройки надлежащих проверенных резервных копий. Как и при работе с базой данных без резервных копий, отсутствие контроля за вашей базой данных опасно: какая польза от системы, которая обеспечивает непредсказуемую производительность и может случайно «отключиться»? Данные могут быть в безопасности, но их нельзя использовать.

В этой главе мы попытаемся дать вам основу для понимания того, как эффективно контролировать MySQL. Эта книга не называется High Performance MySQL, и мы не будем вдаваться в подробности того, что именно означают различные показатели или как выполнять комплексный анализ системы. Но мы поговорим о нескольких основных метриках, которые следует регулярно проверять при каждой установке MySQL, и обсудим важные метрики и инструменты на уровне ОС. Затем мы кратко коснемся нескольких широко используемых методологий оценки производительности системы. После этого мы рассмотрим несколько популярных решений для мониторинга с открытым исходным кодом и, наконец, покажем вам, как собирать данные для целей расследования и мониторинга вручную.

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

Метрики операционной системы

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

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

Давайте взглянем на наиболее важные метрики ОС, которые вы хотите отслеживать и в целом иметь представление. В этом разделе мы рассмотрим две основные ОС: Linux и Windows. Unix-подобные системы, такие как macOS и другие, будут иметь либо те же инструменты, что и Linux, либо, по крайней мере, инструменты, показывающие такие же или похожие результаты.

Процессор

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

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

Каждая операция, которую выполняет компьютер, выполняется центральным процессором. Как сказал Кевин Клоссон: «Все дело в процессоре». Когда программа активно выполняется — например, MySQL анализирует запрос — ЦП выполняет всю работу. Когда программа ожидает ресурса — MySQL ожидает чтения данных с диска — ЦП участвует в «сообщении» программе, когда данные доступны. Такой список можно продолжать вечно.

Вот несколько наиболее важных показателей ЦП для сервера (или любого компьютера в целом):

Частота процессора

Частота ЦП — это количество раз в секунду, которое ядро ЦП может «просыпаться» для выполнения части работы. Это в основном «скорость» процессора. Чем больше, тем лучше, но на удивление часто частота не является самым важным показателем.

Кэш-память

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

Количество ядер

Это количество исполнительных единиц в одном «пакете» ЦП (физический элемент) и сумма этих единиц по всем ЦП, которые мы можем разместить на сервере. В настоящее время становится все труднее найти ЦП с одним ядром: большинство ЦП являются многоядерными системами. У некоторых даже есть «виртуальные» ядра, из-за чего разница между «реальным» количеством процессоров и общим количеством ядер становится еще больше.

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

Но если половина доступных ядер «виртуальна», вы не получите ожидаемого двукратного увеличения производительности. Скорее, вы можете получить 2-кратное увеличение, или вы можете получить где-то между 1-кратным и 2-кратным увеличением: не каждая рабочая нагрузка (даже в пределах MySQL) выигрывает от виртуальных ядер.

Кроме того, наличие нескольких процессоров в разных разъемах усложняет взаимодействие с памятью (ОЗУ) и другими встроенными устройствами (например, сетевыми картами). Обычно стандартные сервера физически расположены таким образом, что некоторые ЦП (и их ядра) будут получать доступ к частям оперативной памяти быстрее, чем к другим частям — это архитектура NUMA, о которой мы говорили в предыдущей главе. Для MySQL это означает, что распределение памяти и проблемы, связанные с памятью, могут стать болевой точкой. Мы рассмотрели необходимую конфигурацию систем NUMA в разделе «Архитектура NUMA».

Базовым показателем CPU является его загрузка в процентах. Когда вам говорят «CPU 20», вы можете быть совершенно уверены, что имеется в виду «CPU в данный момент занят на 20%». Однако вы никогда не можете быть полностью в этом уверены, поэтому вам лучше перепроверить. Например, 20% одного ядра в многоядерной системе могут составлять всего 1% от общей нагрузки. Попробуем визуализировать эту нагрузку.

В Linux основной командой для получения загрузки ЦП является vmstat. Если запустить его без аргументов, он выведет текущие средние значения, а затем завершится. Если мы запустим его с числовым аргументом (здесь мы назовем его X), он будет печатать значения каждые X секунд. Мы рекомендуем запускать vmstat с числовым аргументом, например, vmstat 1, на несколько секунд. Если вы запустите только vmstat, вы получите средние значения с момента загрузки, которые обычно вводят в заблуждение. vmstat 1 будет выполняться вечно, пока не будет прерван (нажатие Ctrl+C — самый простой выход).

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

Для начала давайте посмотрим на вывод vmstat на бездействующей системе. Раздел ЦП усечен; мы подробно рассмотрим его позже:

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b     swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id ...
2 0        0 1229924 1856608 6968268    0    0    39   125   61  144 18  7 75...
1 0        0 1228028 1856608 6969384    0    0     0    84 2489 3047  2  1 97...
0 0        0 1220972 1856620 6977688    0    0     0    84 2828 4712  3  1 96...
0 0        0 1217420 1856644 6976796    0    0     0   164 2405 3164  2  2 96...
0 0        0 1223768 1856648 6968352    0    0     0    84 2109 2762  2  1 97...

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

procs ------cpu-----
 r  b us sy id wa st
 2  0 18  7 75  0  0
 1  0  2  1 97  0  0
 0  0  3  1 96  0  0
 0  0  2  2 96  0  0
 0  0  2  1 97  0  0

r и b — метрики процесса: количество активно запущенных процессов и количество заблокированных процессов (обычно ожидающих ввода-вывода). Другие столбцы представляют разбивку загрузки ЦП в процентных пунктах (от 0% до 100%, даже в многоядерной системе). Вместе значения в этих столбцах всегда будут составлять 100. Вот что показывают столбцы cpu:

us (пользователь)

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

sy (система)

Время, потраченное на выполнение кода ядра. Из-за того, как устроены Linux и другие Unix-подобные системы, пользовательские программы увеличивают этот счетчик. Например, всякий раз, когда MySQL необходимо выполнить чтение с диска, ядро ОС должно выполнить некоторую работу. Время, потраченное на выполнение этой работы, будет включено в значение sy.

id (бездействие)

Время, потраченное на бездействие; время простоя. На полностью простаивающем сервере эта метрика будет равна 100.

wa (ожидание ввода/вывода)

Время ожидания ввода-вывода. Это важный показатель для MySQL, так как чтение и запись в различные файлы составляют относительно большую часть работы MySQL. Когда MySQL выполняет чтение с диска, некоторое время будет потрачено на внутренние функции MySQL и отразится в us. Затем некоторое время будет проведено внутри ядра и отразится в sy. Наконец, после того как ядро отправило запрос на чтение базовому устройству хранения (которое может быть локальным или сетевым устройством) и ожидает ответа и данных, все затраченное время накапливается в wa. Если наша программа и ядро работают очень медленно и все, что мы делаем, это ввод-вывод, теоретически этот показатель может быть близок к 100. В действительности двузначные значения встречаются редко и обычно указывают на некоторые проблемы с вводом-выводом. Мы подробно поговорим о вводе-выводе в разделе «Диск».

st (украдено)

Эту метрику трудно объяснить, не углубляясь в детали. Справочное руководство MySQL определяет его как «время, украденное у виртуальной машины». Вы можете думать об этом как о времени, в течение которого виртуальная машина хотела выполнить свои инструкции, но должна была ждать, пока хост-сервер выделит процессорное время. Есть несколько причин для такого поведения, пара из которых примечательна. Первая — это избыточное выделение хоста: запуск слишком большого количества больших ВМ, что приводит к ситуации, когда сумма ресурсов, требуемых ВМ, превышает возможности хоста. Вторая — ситуация «шумного соседа», когда одна или несколько ВМ страдают от особо загруженной ВМ.

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

Теперь давайте вернемся к нашим выводам vmstat 1 в бездействующей системе:

procs ------cpu-----
 r  b us sy id wa st
 2  0 18  7 75  0  0
 1  0  2  1 97  0  0
 0  0  3  1 96  0  0
 0  0  2  2 96  0  0
 0  0  2  1 97  0  0

Что мы можем сказать из этого вывода? Как упоминалось ранее, первая строка является средней с момента загрузки. В среднем в этой системе запущено два процесса (r), из которых 0 заблокировано (b); Загрузка ЦП пользователем составляет 18% (us), загрузка ЦП системы составляет 7% (sy), а в целом ЦП простаивает на 75% (id). Ожидание ввода-вывода (wa) и украденное время (st) равны 0.

После первой каждая строка вывода представляет собой среднее значение за интервал выборки, который в нашем примере равен 1 секунде. Это довольно близко к тому, что мы могли бы назвать «текущими» значениями. Поскольку это простаивающая машина, мы видим, что в целом значения ниже среднего. Выполняется или блокируется только один процесс или ни одного, процессорное время пользователя составляет 2–3%, системное процессорное время составляет 1–2%, а простаивает система 96–97% времени.

Для наглядности давайте посмотрим на вывод vmstat 1 в той же системе, выполняющей вычисления с интенсивным использованием ЦП в одном процессе:

procs ------cpu-----
 r  b us sy id wa st
 2  0 18  7 75  0  0
 1  0 13  0 87  0  0
 1  0 13  0 86  0  0
 1  0 14  0 86  0  0
 1  0 15  0 84  0  0

Средние значения с момента загрузки одинаковы, но в каждом примере выполняется один процесс, что приводит к увеличению времени ЦП пользователя до 13–15%. Проблема с vmstat заключается в том, что мы не можем узнать из его вывода, какой именно процесс загружает процессор. Конечно, если это выделенный сервер базы данных, вы можете предположить, что большая часть, если не все процессорное время пользователя будет приходиться на долю MySQL и его потоков, но всякое бывает. Другая проблема заключается в том, что на машинах с большим количеством ядер ЦП вы можете ошибочно принять низкие показания в выводе vmstat за факт, но vmstat дает показания от 0% до 100% даже на 256-ядерной машине. Если 8 ядер такой машины загружены на 100%, время пользователя, показанное vmstat, будет равно 3%, но в действительности некоторая рабочая нагрузка может быть занижена.

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

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

На рисунке 12-1 показан диспетчер задач, работающий в бездействующей системе.

Рисунок 12-1. Диспетчер задач показывает неактивный процессор

На рисунке 12-2 показан диспетчер задач, работающий в загруженной системе.

Рисунок 12-2. Диспетчер задач показывает загруженный процессор

Как мы уже говорили ранее, у vmstat есть несколько проблем: он не распределяет нагрузку по процессам или ядрам ЦП. Решение обеих проблем требует запуска других инструментов. Почему бы не запустить их сразу? vmstat универсален, дает больше, чем просто показания ЦП, и очень лаконичен. Это хороший способ быстро увидеть, есть ли что-то очень неправильное в данной системе. То же самое касается диспетчера задач, хотя на самом деле он более функционален, чем vmstat.

В Linux следующий по простоте использования инструмент после vmstat — это top, еще один базовый элемент в наборе инструментов любого, кто имеет дело с сервером Linux. Он расширяет базовые показатели ЦП, которые мы обсуждали, и добавляет как разбивку нагрузки по ядрам, так и учет нагрузки по процессам. Когда вы выполняете top без каких-либо аргументов, он запускается в режиме UI (пользовательского интерфейса) терминала или TUI. Нажмите ? чтобы увидеть меню помощи. Чтобы отобразить разбивку нагрузки по ядрам, нажмите 1. На рисунке 12-3 показано, как выглядят выходные данные команды top.

Рисунок 12-3. top в режиме TUI

Здесь вы можете видеть, что каждый процесс получает свое собственное общее использование ЦП, показанное в столбце %CPU. Например, mysqld использует 104,7% всего процессорного времени. Теперь мы также можем увидеть, как эта нагрузка распределяется между множеством ядер сервера. В данном конкретном случае одно ядро (Cpu0) загружено немного больше, чем другое. Бывают случаи, когда MySQL достигает предела пропускной способности одного процессора, и поэтому важна разбивка нагрузки по ядрам. Наличие представления о том, как нагрузка распределяется между процессами, важно, если вы подозреваете, что какой-то мошеннический процесс потребляет ресурсы сервера.

Есть много других инструментов, которые могут показать вам еще больше данных. Мы не можем подробно говорить обо всех из них, но назовем некоторые. mpstat может предоставить очень подробную статистику процессора. pidstat — это универсальный инструмент, предоставляющий статистику использования ЦП, памяти, диска и сети для каждого отдельного запущенного процесса. atop — это расширенная версия top. Их гораздо больше, и у каждого есть свой любимый набор инструментов. Мы твердо верим, что на самом деле важны не сами инструменты, хотя они и помогают, а понимание основных показателей и статистики, которые они предоставляют.

В Windows программа Диспетчер задач на самом деле намного ближе к top, чем к vmstat, хотя мы провели именно это сравнение. Способность Диспетчера задач отображать нагрузку на ядро и нагрузку на процесс делает его весьма полезным первым шагом в любом расследовании. Мы рекомендуем сразу же погрузиться в Монитор ресурсов, так как он предоставляет более подробную информацию. Самый простой способ получить к нему доступ — щелкнуть ссылку «Открыть Монитор ресурсов» в Диспетчере задач.

На рисунке 12-4 показано окно монитора ресурсов с информацией о загрузке ЦП.

Рисунок 12-4. Монитор ресурсов, показывающий сведения о загрузке ЦП

Диспетчер задач и Монитор ресурсов — не единственные инструменты в Windows, способные отображать показатели производительности. Вот еще несколько инструментов, которыми вы, возможно, захотите освоиться. Они более продвинутые, поэтому здесь мы не будем вдаваться в подробности:

Performance Monitor

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

Process Explorer

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

Диск

Дисковая подсистема или подсистема ввода-вывода имеют решающее значение для производительности базы данных. Хотя ЦП поддерживает каждую операцию, выполняемую в любой данной системе, в частности, для баз данных, диск, вероятно, будет наиболее проблематичным узким местом. Это вполне логично — в конце концов, базы данных хранят данные на диске, а затем обслуживают эти данные с диска. Существует много слоев кэшей поверх медленного и надежного долговременного хранилища, но они не всегда могут быть использованы и не бесконечно велики. Таким образом, понимание базовой производительности дисков чрезвычайно важно при работе с системами баз данных. Другое важное и часто недооцениваемое свойство любой системы хранения вообще не связано с производительностью — это емкость. Мы начнем с этого.

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

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

К счастью, контролировать использование дискового пространства очень просто. В Linux это можно сделать с помощью простой команды df. Без аргументов она покажет емкость и использование в блоках по 1 КБ для каждой файловой системы. Вы можете добавить аргумент -h, чтобы получить удобочитаемые измерения, и указать точку монтирования (или просто путь), чтобы ограничить проверку. Вот пример:

$ df -h /var/lib/mysql
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        40G   18G   23G  45% /

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

В Windows надежный File Explorer может предоставить информацию об использовании дискового пространства и емкости, как показано на рисунке 12-5.

Рисунок 12-5. Проводник, показывающий доступное место на диске

Рассмотрев тему дискового пространства, мы теперь углубимся в основные характеристики производительности любой подсистемы ввода-вывода:

Пропускная способность

Сколько байтов данных может быть передано (или извлечено) из хранилища в единицу времени

Операций ввода-вывода в секунду (IOPS)

Количество операций, которые диск (или другая система хранения) способен обслуживать в единицу времени.

Задержка

Сколько времени требуется для обработки системой хранения данных операций чтения или записи

Этих трех свойств достаточно, чтобы описать любую систему хранения и начать формировать понимание того, хороша она, плоха или отвратительна. Как и в разделе ЦП, мы покажем вам несколько инструментов для проверки производительности диска и используем их выходные данные для объяснения конкретных показателей. Мы снова сфокусируемся на Linux и Windows; в других системах будет что-то похожее, так что знания можно переносить.

Аналогом нагрузки ввода-вывода для vmstat в Linux является программа iostat. Схема взаимодействия должна быть вам знакома: вызовите команду без аргументов, и вы получите средние значения с момента загрузки; передайте число в качестве аргумента, и вы получите средние значения за период выборки. Мы также предпочитаем запускать инструмент с аргументом -x, что добавляет много полезных деталей. В отличие от vmstat, iostat предоставляет метрики с разбивкой по блочным устройствам, подобно команде mpstat, о которой мы упоминали ранее.

Давайте посмотрим на пример вывода. Мы будем использовать команду iostat -dxyt 5, которая означает: распечатать отчет об использовании устройства, отобразить расширенную статистику, пропустить первый отчет со средними значениями с момента загрузки, добавить метку времени для каждого отчета и сообщить средние значения за каждый 5-секундный период некоторого выборочного вывода в загруженной системе:

05/09/2021 04:45:09 PM
Device:        rrqm/s   wrqm/s   r/s      w/s    rkB/s      wkB/s...
sda              0.00     0.00  0.00  1599.00     0.00  204672.00...
...avgrq-sz avgqu-sz   await r_await w_await  svctm   %util
... 256.00    141.67   88.63    0.00   88.63   0.63  100.00

Здесь предстоит разобраться во многом. Мы не будем охватывать все столбцы, но выделим те, которые соответствуют упомянутым ранее свойствам:

Пропускная способность

В выходных данных iostat столбцы rkB/s и wkB/s соответствуют использованию полосы пропускания (чтение и запись соответственно). Если вы знаете характеристики базового хранилища (например, вы можете знать, что оно обещает 200 МБ/с комбинированной пропускной способности для чтения и записи), вы можете сказать, выходите ли вы за пределы. Здесь вы можете увидеть внушительную цифру — чуть более 200 000 КБ в секунду записывается на устройство /dev/sda, и никаких операций чтения не происходит.

IOPS

Эта метрика представлена столбцами r/s и w/s, указывающими количество операций чтения и записи в секунду соответственно. Наш пример показывает 1599 операций записи в секунду. Как и ожидалось, операций чтения не зарегистрировано.

Задержка

Показанная несколько более сложным образом, задержка разбита на четыре столбца (или более, в более новых версиях iostat): await, r_await, w_await и svctm. Для базового анализа вы должны смотреть на значение await, которое является средней задержкой для обслуживания любого запроса. r_await и w_await прерывают await при чтении и записи. svctime — это устаревшая метрика, которая пытается показать чистую задержку устройства без какой-либо очереди.

Имея эти основные показатели и зная некоторые основные факты об используемом хранилище, можно сказать, что происходит. Наш пример работает на современном твердотельном накопителе NVMe потребительского уровня в одном из ноутбуков автора. Хотя пропускная способность довольно хорошая, каждый запрос в среднем занимает 88 мс, что очень много. Вы также можете выполнить некоторые простые математические действия, чтобы получить шаблон нагрузки ввода-вывода из этих метрик. Например, если мы разделим пропускную способность на IOPS, мы получим цифру 128 КБ на запрос. На самом деле iostat включает эту метрику в столбец avgrq-sz, который показывает средний размер запроса в исторических единицах измерения секторов (512 байт). Вы можете пойти дальше и измерить, что 1599 операций записи в секунду могут обрабатываться только со скоростью ~40 мс/запрос, что означает наличие параллельной нагрузки при записи (а также то, что наше устройство способно обслуживать параллельные запросы).

Шаблоны ввода-вывода — размер запросов, степень параллелизма, случайный или последовательный — могут сдвигать верхние пределы базового хранилища. Большинство устройств будут объявлять максимальную пропускную способность, максимальное количество операций ввода-вывода в секунду и минимальную задержку при определенных условиях, но эти условия могут различаться для измерения максимального количества операций ввода-вывода в секунду и максимальной пропускной способности, а также для оптимальной задержки. Довольно сложно однозначно ответить на вопрос, хорошие или плохие показания метрики. Не зная ничего о базовом хранилище, один из способов оценить его использование — попытаться оценить насыщенность. Насыщенность, которую мы коснемся в разделе «Метод USE», является мерой того, насколько ресурс перегружен. Это становится все более сложным с современным хранилищем, способным эффективно обслуживать длинные очереди параллельно, но в целом очередь на устройстве хранения является признаком насыщения. В выходных данных iostat это столбец avgqu-sz (или aqu-sz в новых версиях iostat), и значения больше 1 обычно означают, что устройство перегружено. В нашем примере показана очередь из 146 запросов, что, вероятно, говорит о высокой загруженности операций ввода-вывода и может быть узким местом.

К сожалению, как вы, возможно, заметили, не существует простого и однозначного показателя использования ресурсов ввода-вывода: для каждого показателя есть свои нюансы. Измерение производительности хранилища - сложная задача!

Одни и те же метрики определяют устройства хранения в Linux, Windows и любой другой ОС.

Давайте теперь рассмотрим основные инструменты Windows для оценки производительности ввода-вывода. Их показания уже должны быть вам знакомы. Мы рекомендуем использовать Монитор ресурсов, который мы показали в разделе «Процессор», но на этот раз перейдите на вкладку «Диск». На рисунке 12.6 показано это представление с MySQL при большой нагрузке на запись.

Рисунок 12-6. Монитор ресурсов, показывающий сведения о нагрузке ввода-вывода

Метрики, представленные монитором ресурсов, аналогичны метрикам iostat. Вы можете увидеть пропускную способность, задержку и длину очереди запросов. Отсутствует одна метрика — IOPS. Чтобы получить эту информацию, вам нужно будет использовать Монитор производительности (perfmon), но мы оставим это в качестве упражнения.

Монитор ресурсов на самом деле показывает немного более подробное представление, чем iostat. Существует разбивка нагрузки ввода-вывода на процесс и дальнейшая разбивка этой нагрузки на файл. Мы не знаем ни одного инструмента в Linux, способного отображать такую разбивку нагрузки одновременно. Чтобы получить разбивку нагрузки по программам, вы можете использовать инструмент pidstat в Linux, о котором мы упоминали ранее. Вот пример вывода:

# pidstat -d 5
...
10:50:01 AM    UID    PID   kB_rd/s    kB_wr/s kB_ccwr/s  Command
10:50:06 AM     27   4725      0.00   30235.06      0.00  mysqld

10:50:06 AM    UID    PID   kB_rd/s    kB_wr/s kB_ccwr/s  Command
10:50:11 AM     27   4725      0.00   23379.20      0.00  mysqld
...

Получение разбивки по файлам в Linux довольно легко достигается с помощью BCC Toolkit, в частности инструмента filetop. В наборе инструментов есть еще много инструментов для изучения, но большинство из них довольно продвинуты. Инструментов, которые мы показали здесь, должно быть достаточно для удовлетворения основных потребностей в исследованиях и мониторинге.

Память

Память или ОЗУ — еще один важный ресурс для любой базы данных. Память обеспечивает значительно более высокую производительность по сравнению с диском при чтении и записи данных, поэтому базы данных стремятся как можно больше работать «в памяти». К сожалению, память не является постоянной, поэтому в конечном итоге каждая операция должна отражаться на диске. (Подробнее о производительности диска см. в предыдущем разделе.)

В отличие от разделов процессора и диска, мы фактически не будем говорить о производительности памяти. Несмотря на то, что это важно, это также очень продвинутая и глубокая тема. Вместо этого мы сосредоточимся на использовании памяти. Это также может довольно быстро усложниться, поэтому мы постараемся оставаться сосредоточенными.

Начнем с некоторых основных предпосылок. Каждая программа нуждается в некоторой памяти для работы. Системы баз данных, включая MySQL, обычно требуют много памяти. Когда она заканчивается, у приложений начинаются проблемы с производительностью, и они могут даже дать сбой, как вы увидите в конце раздела. Таким образом, мониторинг использования памяти имеет решающее значение для стабильности любой системы, но особенно для системы баз данных.

В данном случае мы фактически начнем с Windows, так как на первый взгляд у нее немного менее запутанный механизм учета памяти, чем у Linux. Чтобы получить общее использование памяти ОС в Windows, все, что вам нужно сделать, это запустить Диспетчер задач, как описано в разделе «Процессор», перейти на вкладку «Производительность» и выбрать «Память». На рисунке 12-7 вы можете увидеть отображение использования памяти в Диспетчере задач.

Рисунок 12-7. Диспетчер задач, показывающий сведения об использовании памяти

Эта машина имеет в общей сложности 4 ГБ памяти, из которых 2,4 ГБ используются в настоящее время и 1,6 ГБ доступны, что делает общее использование 60%. Это безопасное количество, и мы можем даже захотеть выделить больше памяти для MySQL, чтобы свести к минимуму «потерю» свободной памяти. Некоторые идеи по размеру буферного пула InnoDB в MySQL можно найти в разделе «Размер буферного пула».

В Linux самым простым инструментом для получения сведений об использовании памяти является команда free. Мы рекомендуем использовать его с аргументом -h, который преобразует все поля в удобочитаемый формат. Вот пример вывода на машине под управлением CentOS 7:

$ free -h
             total      used      free     shared  buff/cache   available
Mem:          3.7G      2.7G      155M       8.5M        905M        787M
Swap:         2.0G       13M      2.0G

Теперь это больше данных, чем мы видели в Windows. На самом деле большинство этих счетчиков есть в Windows; они просто не так видны.

Пройдемся по выводу команды. Сейчас мы рассмотрим строку Mem, а о Swap поговорим позже.

Две ключевые метрики здесь used и available, что означает Используется и Доступно в Диспетчере задач. Частая ошибка, которую часто совершали ваши авторы, — смотреть на метрику free, а не на available. Это не правильно! Linux (и, собственно, Windows) не любит оставлять память свободной. В конце концов, свободная память — это потраченный впустую ресурс. Когда есть доступная память, которая не нужна напрямую приложениям, Linux будет использовать эту память для хранения кэша данных, которые считываются и записываются с диска и на него. Позже мы покажем, что Windows делает то же самое, но вы не можете увидеть это из Диспетчера задач. Подробнее о том, почему ошибочно ориентироваться на метрику free, см. на сайте Linux ate my ram.

Давайте далее разберем вывод этой команды. Столбцы:

total

Общий объем памяти, доступный на машине

used

Объем памяти, используемой в настоящее время приложениями

free

Фактическая свободная память, вообще не используемая ОС

shared

Особый тип памяти, который необходимо специально запрашивать и выделять, и к которому несколько процессов могут обращаться одновременно; поскольку он не используется MySQL, мы опускаем здесь подробности.

buff/cache

Объем памяти, который ОС в настоящее время использует в качестве кэша для улучшения операций ввода-вывода.

available

Объем памяти, который приложения могли бы использовать, если бы они в ней нуждались; обычно сумма free и buff/cache

В общем, для базового, но надежного мониторинга вам нужно только посмотреть на сумму total, used и availableё. Linux должен иметь возможность самостоятельно обрабатывать кэшированную память. Мы намеренно не рассматриваем здесь кеш страниц, так как это сложная тема. По умолчанию MySQL в Linux будет использовать кеш страниц, поэтому размер вашего экземпляра должен соответствовать этому. Тем не менее, часто рекомендуемое изменение состоит в том, чтобы указать MySQL избегать кеша страниц (ищите документацию по innodb_flush_method), что позволит самой MySQL использовать больше памяти.

Мы упоминали, что Windows имеет в основном одни и те же показатели; они просто скрыты. Чтобы увидеть это, откройте Монитор ресурсов и перейдите на вкладку «Память». На рисунке 12-8 показано содержимое этой вкладки.

Рисунок 12-8. Монитор ресурсов показывает сведения об использовании памяти

Вы сразу заметите, что объем свободной памяти составляет всего 52 МБ, а резервной памяти прилично, с небольшим количеством измененной памяти. Кэшированное значение в списке ниже представляет собой сумму измененного и резервного количества. Когда был сделан снимок экрана, 1593 МБ памяти использовались кешем, из которых 33 МБ были грязными (dirty) (или модифицированными). Windows, как и Linux, кэширует страницы файловой системы, пытаясь свести к минимуму и сгладить ввод-вывод и максимально использовать память.

Еще одна вещь, которую вы можете увидеть, это разбивка использования памяти для каждого процесса, при этом mysqld.exe занимает чуть менее 500 МБ памяти. В Linux аналогичный вывод можно получить с помощью команды top, которую мы впервые использовали в разделе «Процессор». После запуска top нажмите Shift+M, чтобы отсортировать вывод по использованию памяти и получить удобочитаемые цифры. верхний вывод, показывающий детали использования памяти, показан на рисунке 12-9.

Рисунок 12-9. top показывает сведения об использовании памяти

В этой системе вывод не очень интересен, но вы можете быстро увидеть, что именно MySQL потребляет больше всего памяти своим процессом mysqld.

Прежде чем закончить этот раздел, мы хотим поговорить о том, что происходит, когда у вас заканчивается память. Однако перед этим давайте обсудим подкачку или пейджинг. Здесь следует упомянуть, что большинство современных операционных систем реализуют управление памятью таким образом, что каждое отдельное приложение имеет собственное представление системной памяти (отсюда вы можете видеть, что память приложения называется виртуальной памятью) и что общая сумма виртуальной памяти, которую приложение может использовать превышает общий фактический объем памяти системы. Обсуждение первого пункта лучше подходит для университетского курса по проектированию операционных систем, но последний пункт очень важен при работе с системами баз данных.

Последствия такого дизайна важны, потому что ОС не может просто волшебным образом увеличить емкость системной памяти. На самом деле происходит то, что ОС использует дисковое хранилище для расширения объема памяти, а, как мы уже упоминали, оперативная память обычно гораздо более производительна, чем даже самый быстрый диск. Таким образом, как вы понимаете, за это расширение памяти приходится платить. Пейджинг может происходить несколькими способами и по разным причинам. Наиболее важным для MySQL является тип подкачки, называемый свопингом, — запись частей памяти в выделенное место на диске. В Linux этим местом может быть отдельный раздел или файл. В Windows есть специальный файл pagefile.sys, который в основном используется так же.

Подкачка сама по себе неплоха, но проблематична для MySQL. Проблема в том, что наша база данных думает, что читает что-то из памяти, тогда как на самом деле ОС выгрузила часть этих данных в файл подкачки и на самом деле прочитает их с диска. MySQL не может предсказать, когда эта ситуация произойдет, и ничего не может сделать, чтобы предотвратить ее или оптимизировать доступ. Для конечного пользователя это может означать внезапное необъяснимое снижение времени ответа на запрос. Однако, как мы покажем, наличие некоторого пространства подкачки является важной защитной мерой.

Давайте перейдем к ответу на вопрос, что на самом деле происходит, когда у вас заканчивается память. Коротко: ничего хорошего. Есть только несколько общих результатов для MySQL, когда системе не хватает памяти, поэтому давайте поговорим о них:

Таким образом, MySQL либо станет медленнее, либо выйдет из строя, либо будет убит, вот так просто. Теперь вы должны ясно видеть, почему мониторинг доступной и используемой памяти очень важен. Мы также рекомендуем оставить некоторый запас памяти на ваших серверах и настроить файл подкачки. Советы по настройке подкачки в Linux см. в разделе «Лучшие практики для операционных систем».

Сеть

Из всех ресурсов ОС сеть, вероятно, чаще всего обвиняют в случайных необъяснимых проблемах. Для этого есть веская причина: мониторинг сети затруднен. Для понимания проблем с сетью иногда требуется подробный анализ всего сетевого потока. Это своеобразный ресурс, потому что, в отличие от ЦП, диска и памяти, он не содержится в одном сервере. По крайней мере, вам нужны две машины, взаимодействующие друг с другом, чтобы понятие «сеть» вообще имело значение. Конечно, есть локальные связи, но они, как правило, стабильны. И конечно, дисковое хранилище может быть общим, а ЦП и память в случае виртуальных машин также могут быть общими, но сеть всегда связана с несколькими машинами.

Поскольку эта глава посвящена мониторингу, мы не собираемся рассматривать здесь вопросы подключения, тем не менее удивительное количество проблем с сетью сводится к простой проблеме, когда один компьютер не может связаться с другим. Не принимайте подключение как должное. Сетевые топологии обычно сложны, и каждый пакет следует по сложному маршруту через несколько машин. В облачных средах маршруты могут быть еще более сложными и менее очевидными. Если вы считаете, что у вас есть какие-то проблемы с сетью, разумно проверить, можно ли вообще установить соединения.

Мы коснемся следующих свойств любой сети:

Пропускная способность и ее использование (throughput)

Это похоже на ту же концепцию, определенную в разделе «Диск». Каждое сетевое соединение имеет максимальную пропускную способность, обычно выражаемую в единицах объема данных в секунду. Интернет-соединения обычно используют Mbps или мегабиты в секунду, но также могут использоваться MBps или мегабайты в секунду. Сетевые соединения и оборудование накладывают жесткие ограничения на максимальную пропускную способность. Например, в настоящее время обычное бытовое сетевое оборудование редко имеет пропускную способность более 1 Gbps (Гбит/с). Более современное оборудование ЦОД обычно поддерживает 10 Гбит/с. Существует специальное оборудование, которое может увеличить пропускную способность до сотен Гбит/с, но такие соединения обычно представляют собой немаршрутизируемые прямые соединения между двумя серверами.

Ошибки — их количество и источники

Сетевые ошибки неизбежны. На самом деле протокол управления передачей (TCP), основа Интернета и протокол, используемый MySQL, основан на предположении, что пакеты будут потеряны. Вы, несомненно, будете время от времени сталкиваться с ошибками, но высокий уровень ошибок приведет к медленному подключению, так как взаимодействующим сторонам придется повторно отправлять пакеты снова и снова.

Продолжая аналогию с диском, мы могли бы также включить задержку и количество отправленных и полученных пакетов (относительно IOPS). Однако задержку при передаче пакетов можно измерить только приложением, выполняющим фактическую передачу. ОС не может измерить и показать среднюю задержку для сети. А количество пакетов обычно избыточно, поскольку оно соответствует показателям пропускной способности.

Одна конкретная метрика, которую полезно добавить при рассмотрении сетей, — это количество повторно переданных пакетов. Повторная передача происходит, когда пакет потерян или поврежден. Это не ошибка, а обычно результат некоторых проблем с подключением. Как и при нехватке полосы пропускания, увеличение количества повторных передач приведет к нестабильной работе сети.

В Linux мы можем начать с просмотра статистики сетевого интерфейса. Самый простой способ сделать это — запустить команду ifconfig. Ее вывод по умолчанию будет включать каждый сетевой интерфейс на конкретном хосте. Поскольку мы знаем, что в этом случае вся нагрузка идет через eth1, мы можем показать статистику только для него:

$ ifconfig eth1
...
eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
        inet 192.168.10.11 netmask 255.255.255.0 broadcast 192.168.10.255
        inet6 fe80::a00:27ff:fef6:b4f prefixlen 64 scopeid 0x20
        ether 08:00:27:f6:0b:4f txqueuelen 1000 (Ethernet)
        RX packets 6217203 bytes 735108061 (701.0 MiB)
        RX errors 0 dropped 0 overruns 0 frame 0
        TX packets 11381894 bytes 18025086781 (16.7 GiB)
        TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
...

Мы сразу видим, что сеть довольно исправна, просто по тому факту, что нет ошибок при приеме (RX) или отправке (TX) пакетов. Общая статистика данных RX и TX (701,0 МБ и 16,7 ГБ соответственно) будет расти каждый раз, когда вы запускаете ifconfig, поэтому вы можете легко измерить использование полосы пропускания, запуская его с течением времени. Это не очень удобно, и есть программы, которые показывают скорость передачи в реальном времени, но ни одна из них не поставляется по умолчанию в обычных дистрибутивах Linux. Чтобы увидеть историю скорости передачи и ошибок, вы можете использовать команду sar -n DEV или sar -n EDEV соответственно (sar является частью пакета sysstat, о котором мы упоминали, говоря о iostat):

$ sar -n DEV
                IFACE   rxpck/s    txpck/s   rxkB/s    txkB/s...
06:30:01 PM      eth0      0.16       0.08     0.01      0.01...
06:30:01 PM      eth1   7269.55   13473.28   843.84  21618.70...
06:30:01 PM        lo      0.00       0.00     0.00      0.00...
06:40:01 PM      eth0      0.48       0.28     0.03      0.05...
06:40:01 PM      eth1   7844.90   13941.09   893.95  19204.10...
06:40:01 PM        lo      0.00       0.00     0.00      0.00...
...rxcmp/s   txcmp/s  rxmcst/s
...   0.00      0.00      0.00
...   0.00      0.00      0.00
...   0.00      0.00      0.00
...   0.00      0.00      0.00
...   0.00      0.00      0.00
...   0.00      0.00      0.00

$ sar -n EDEV
04:30:01 PM     IFACE   rxerr/s   txerr/s    coll/s  rxdrop/s...
06:40:01 PM      eth0      0.00      0.00      0.00      0.00...
06:40:01 PM      eth1      0.00      0.00      0.00      0.00...
06:40:01 PM        lo      0.00      0.00      0.00      0.00...
...txdrop/s  txcarr/s  rxfram/s  rxfifo/s  txfifo/s
...    0.00      0.00      0.00      0.00      0.00
...    0.00      0.00      0.00      0.00      0.00
...    0.00      0.00      0.00      0.00      0.00

Опять же, мы видим, что в нашем примере интерфейс eth1 достаточно загружен, но сообщений об ошибках нет. Если мы остаемся в пределах пропускной способности, производительность сети должна быть нормальной.

Чтобы получить полное подробное представление о различных ошибках и проблемах, которые произошли в сети, вы можете использовать команду netstat. С флагом -s он сообщит о большом количестве счетчиков. Для простоты мы покажем только часть вывода раздела Tcp с несколькими повторными передачами. Для получения более подробного обзора ознакомьтесь с разделом выходных данных TcpExt:

$ netstat -s
...
Tcp:
    55 active connections openings
    39 passive connection openings
    0 failed connection attempts
    3 connection resets received
    9 connections established
    14449654 segments received
    25994151 segments send out
    54 segments retransmitted
    0 bad segments received.
    19 resets sent
...

Учитывая огромное количество отправленных сегментов, скорость повторной передачи превосходна. Эта сеть, кажется, в порядке.

В Windows мы снова прибегаем к проверке Монитора ресурсов, который предоставляет большинство нужных нам метрик и многое другое. На рисунке 12.10 показаны представления, связанные с сетью, которые Монитор ресурсов может предложить на узле, выполняющем искусственную нагрузку на MySQL.

Рисунок 12-10. Монитор ресурсов, показывающий сведения об использовании сети

Чтобы узнать количество ошибок в Windows, вы можете использовать команду netstat. Обратите внимание, что хотя у нее то же имя, что и у инструмента Linux, который мы использовали ранее, они немного отличаются. В этом случае у нас нет ошибок:

C:\Users\someuser> netstat -e
Interface Statistics

                           Received         Sent
Bytes                      58544920      7904968
Unicast packets               62504        32308
Non-unicast packets               0          364
Discards                          0            0
Errors                            0            0
Unknown protocols                 0

Модификатор -s для netstat существует и в Windows. Опять же, здесь мы показываем только часть вывода:

C:\Users\someuser> netstat -s
...
TCP Statistics for IPv4

  Active Opens                      = 457
  Passive Opens                     = 30
  Failed Connection Attempts        = 3
  Reset Connections                 = 121
  Current Connections               = 11
  Segments Received                 = 61237201
  Segments Sent                     = 30866526
  Segments Retransmitted            = 0
...

Судя по показателям, которые мы выделили для мониторинга — использованию полосы пропускания и ошибкам, — сеть этой системы работает идеально. Мы понимаем, что это лишь малая часть того, что касается сложности сетевого взаимодействия. Однако этот минимальный набор инструментов может отлично помочь вам понять, стоит ли вообще обвинять свою сеть.

На этом довольно длинный обзор основ мониторинга ОС заканчивается. Вероятно, мы могли бы сделать его короче, и вы можете спросить, почему мы поместили все это в книгу о MySQL. Ответ довольно прост: потому что это важно. Любая программа взаимодействует с ОС и требует некоторых системных ресурсов. MySQL по своей природе обычно очень требовательная программа, и вы ожидаете, что она будет работать хорошо. Однако для этого вам нужно убедиться, что у вас есть необходимые ресурсы и что у вас не заканчивается производительность диска, процессора или сети или просто хватает емкости диска и памяти. Иногда проблема с системными ресурсами, вызванная MySQL, также может привести к обнаружению проблем в самой MySQL. Например, плохо написанный запрос может сильно нагрузить процессор и диск, вызывая всплеск использования памяти.

Мониторинг сервера MySQL

Мониторинг MySQL одновременно прост и сложен. Это легко, потому что MySQL предоставляет почти 500 переменных состояния, которые позволяют вам довольно точно видеть, что происходит внутри вашей базы данных. В дополнение к этому InnoDB имеет собственный диагностический вывод. Однако мониторинг затруднен, потому что может быть сложно разобраться в имеющихся у вас данных.

В этом разделе мы собираемся объяснить основы мониторинга MySQL, начиная с того, что такое переменные состояния и как их получить, и переходя к диагностике InnoDB. Как только это будет рассмотрено, мы покажем несколько основных рецептов, которые, по нашему мнению, должны быть частью каждого пакета мониторинга базы данных MySQL. С помощью этих рецептов и того, что вы узнали о мониторинге ОС в предыдущем разделе, вы сможете понять, что происходит с вашей системой.

Переменные состояния

Мы начнем с переменных состояния сервера MySQL. Эти переменные, в отличие от параметров конфигурации, доступны только для чтения и отображают информацию о текущем состоянии сервера MySQL. Они различаются по своей природе: большинство из них представляют собой либо постоянно увеличивающиеся счетчики, либо датчики со значениями, движущимися вверх и вниз. Однако некоторые из них представляют собой статические текстовые поля, которые помогают понять текущую конфигурацию сервера. Доступ ко всем переменным состояния можно получить на глобальном уровне сервера и на уровне текущего сеанса. Но не каждая переменная имеет смысл на уровне сеанса, а некоторые будут показывать одинаковые значения на обоих уровнях.

SHOW STATUS используется для получения текущих значений переменных состояния. Он имеет два необязательных модификатора, GLOBAL и SESSION, и по умолчанию имеет значение SESSION. Вы также можете указать имя переменной или шаблон, но это не обязательно. Команда в следующем примере показывает все значения переменных состояния для текущего сеанса:

mysql> SHOW STATUS;
+-----------------------------------------------+-----------------------+
| Variable_name                                 | Value                 |
+-----------------------------------------------+-----------------------+
| Aborted_clients                               | 0                     |
| Aborted_connects                              | 0                     |
| Acl_cache_items_count                         | 0                     |
| ...                                                                   |
| Threads_connected                             | 2                     |
| Threads_created                               | 2                     |
| Threads_running                               | 2                     |
| Uptime                                        | 305662                |
| Uptime_since_flush_status                     | 305662                |
| validate_password.dictionary_file_last_parsed | 2021-05-22 20:53:08   |
| validate_password.dictionary_file_words_count | 0                     |
+-----------------------------------------------+-----------------------+
482 rows in set (0.01 sec)

Прокрутка сотен строк вывода неоптимальна, поэтому давайте вместо этого используем подстановочный знак, чтобы ограничить количество запрашиваемых переменных. LIKE в SHOW STATUS работает так же, как и для обычных операторов SELECT, как описано в главе 3:

mysql> SHOW STATUS LIKE 'Created%';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 0     |
| Created_tmp_files       | 7     |
| Created_tmp_tables      | 0     |
+-------------------------+-------+
3 rows in set (0.01 sec)

Теперь вывод намного легче читать. Чтобы прочитать значение одной переменной, просто укажите ее полное имя в кавычках без подстановочного знака, например:

mysql> SHOW STATUS LIKE 'Com_show_status';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Com_show_status | 11    |
+-----------------+-------+
1 row in set (0.00 sec)

Вы можете заметить, что в выходных данных для переменных состояния Created% MySQL показал значение 7 для Created_tmp_files. Означает ли это, что в этом сеансе было создано семь временных файлов и ноль временных таблиц? Нет — на самом деле переменная состояния Created_tmp_files имеет только глобальную область действия. На данный момент это известная проблема с MySQL: вы всегда видите все переменные состояния, независимо от запрошенной области, но их значения будут иметь правильную область. Документация MySQL включает полезный Server Status Variable Reference, который может помочь вам понять область действия различных переменных.

В отличие от Created_tmp_files, переменная Com_show_status имеет область действия «both», что означает, что вы можете получить глобальный счетчик, а также значение для сеанса. Посмотрим, что на практике:

mysql> SHOW STATUS LIKE 'Com_show_status';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Com_show_status | 13    |
+-----------------+-------+
1 row in set (0.00 sec)
mysql> SHOW GLOBAL STATUS LIKE 'Com_show_status';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Com_show_status | 45    |
+-----------------+-------+
1 row in set (0.00 sec)

Еще одна важная вещь, на которую следует обратить внимание при просмотре переменных состояния, заключается в том, что большинство из них можно сбросить до нуля на уровне сеанса. Это достигается запуском команды FLUSH STATUS. Эта команда сбрасывает переменные состояния во всех подключенных сеансах на ноль после добавления их текущих значений в глобальные счетчики. Таким образом, FLUSH STATUS работает на уровне сеанса, но для всех сеансов. Чтобы проиллюстрировать это, мы сбросим значения переменных состояния в сеансе, который мы использовали ранее:

mysql> FLUSH STATUS;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW STATUS LIKE 'Com_show_status';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Com_show_status | 1     |
+-----------------+-------+
1 row in set (0.00 sec)
mysql> SHOW GLOBAL STATUS LIKE 'Com_show_status';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Com_show_status | 49    |
+-----------------+-------+
1 row in set (0.00 sec)

Несмотря на то, что глобальный счетчик продолжает увеличиваться, счетчик сеансов был сброшен до 0 и увеличен только до 1, когда мы выполнили команду SHOW STATUS. Это может быть полезно, чтобы увидеть, как, например, выполнение одного запроса изменяет значения переменных состояния (в частности, семейство переменных состояния Handler_*).

Обратите внимание, что невозможно сбросить глобальные счетчики без перезагрузки.

Основные рецепты мониторинга

Вы можете отслеживать множество показателей в различных комбинациях. Однако мы считаем, что некоторые из них должны быть в наборе инструментов каждого оператора базы данных. Пока вы изучаете MySQL, этого должно быть достаточно, чтобы дать вам разумное представление о том, как работает ваша база данных. Большинство существующих систем мониторинга должны охватывать их и обычно включают гораздо больше показателей. Возможно, вы никогда не будете настраивать сбор данных самостоятельно, но наше объяснение должно помочь вам лучше понять, что именно сообщает вам ваша система мониторинга.

Мы собираемся дать несколько широких категорий метрик в следующих подразделах, в которых мы подробно расскажем о том, что мы считаем одними из наиболее важных счетчиков.

Доступность сервера MySQL

Это самое главное, за чем вы должны следить. Если MySQL Server не принимает соединения или не работает, все остальные показатели не имеют значения.

MySQL Server — это надежное программное обеспечение, способное безотказно работать в течение нескольких месяцев или лет. Тем не менее, есть ситуации, которые могут привести к преждевременному незапланированному отключению (или, проще говоря, к сбою). Например, в разделе «Память» мы обсуждали, что условия нехватки памяти могут привести к сбою или завершению работы MySQL. Случаются и другие инциденты. В MySQL есть ошибки, приводящие к сбоям, однако в настоящее время они редки. Есть и операционные ошибки: кто не забывал поднять базу после планового обслуживания? Отказ оборудования, перезапуск серверов — многие вещи могут поставить под угрозу доступность MySQL.

Существует несколько подходов к мониторингу доступности MySQL, и нет одного наилучшего; лучше совмещать несколько. Очень простой базовый подход заключается в проверке того, что процесс mysqld (или mysqld.exe) действительно запущен и виден с уровня ОС. В Linux и Unix-подобных системах вы можете использовать для этого команду ps, а в Windows вы можете проверить диспетчер задач или запустить команду Get-Service PowerShell. Это не бесполезная проверка, но у нее есть свои проблемы. Во-первых, тот факт, что MySQL работает, не гарантирует, что он действительно делает то, что должен, то есть обрабатывает запросы клиентов. MySQL может быть перегружен нагрузкой или страдать от сбоя диска и работать невыносимо медленно. С точки зрения ОС процесс запущен, но с точки зрения клиента он все равно остановлен.

Второй подход заключается в проверке доступности MySQL с точки зрения приложения. Обычно это достигается запуском монитора MySQL и выполнением какого-нибудь простого короткого запроса. В отличие от предыдущей проверки, эта проверяет возможность установки нового соединения с MySQL и обработку запросов базой данных. Вы можете разместить эти проверки на стороне приложения, чтобы сделать их еще ближе к тому, как приложения видят базу данных. Вместо того, чтобы настраивать такие проверки как независимые сущности, приложения можно настроить так, чтобы они исследовали MySQL и сообщали о явных ошибках либо операторам, либо системе мониторинга.

Третий подход лежит между двумя предыдущими и сконцентрирован на стороне БД. При мониторинге MySQL вам, по крайней мере, потребуется выполнить простые запросы к базе данных, чтобы проверить переменные состояния. Если эти запросы завершатся неудачей, ваша система мониторинга должна предупредить, так как, возможно, у MySQL начинаются проблемы. Вариантом может быть проверка того, были ли какие-либо данные от целевого экземпляра получены вашей системой мониторинга за последние несколько минут.

В идеале в результате этих проверок должно получиться не только оповещение «MySQL не работает» в нужное время, но и некоторая подсказка о том, почему она не работает. Например, если второй тип проверки не может инициировать новое соединение, потому что у MySQL закончились соединения, то это должно быть частью предупреждения. Если проверка третьего типа не проходит, а проверка первого типа в порядке, то это ситуация, отличная от сбоя.

Клиентские подключения

MySQL Server — это многопоточная программа, подробно описанная в разделе «Демон сервера MySQL». Каждый клиент, подключающийся к базе данных, вызывает создание нового потока в процессе сервера MySQL (mysqld или mysqld.exe). Этот поток будет отвечать за выполнение инструкций, отправленных клиентом, и теоретически может выполняться столько же одновременных запросов, сколько существует клиентских потоков.

Каждое соединение и его поток создают небольшую нагрузку на сервер MySQL, даже когда они простаивают. Кроме того, с точки зрения базы данных, каждое соединение является обязательством: база данных не может знать, когда соединение отправит запрос. Параллелизм, или количество одновременно выполняемых транзакций и запросов, обычно увеличивается с увеличением количества установленных соединений. Параллелизм сам по себе неплох, но у каждой системы есть предел масштабируемости. Как вы помните из раздела «Метрики операционной системы», ресурсы процессора и диска имеют ограничения производительности, и их невозможно преодолеть. И даже при бесконечном количестве ресурсов ОС сама MySQL имеет внутренние пределы масштабируемости.

Проще говоря: количество подключений, особенно активных, в идеале должно быть минимальным. Со стороны MySQL отсутствие соединений — идеальная ситуация, но со стороны приложения это неприемлемо. Некоторые приложения, тем не менее, не пытаются ограничить количество соединений, которые они устанавливают, и запросов, которые они посылают, предполагая, что база данных позаботится об этой нагрузке. Это может создать опасную ситуацию, известную как thundering herd: по какой-то причине запросы выполняются дольше, и приложение реагирует, отправляя все больше и больше запросов, перегружая базу данных.

Наконец, у MySQL есть верхний предел количества клиентских подключений, управляемый системной переменной max_connections. Как только количество существующих подключений достигнет значения этой переменной, MySQL откажется создавать новые подключения. Это плохо. max_connections следует использовать для защиты от полного обвала сервера, если клиенты устанавливают тысячи подключений. Но в идеале вы должны отслеживать количество подключений и работать с командами приложений, чтобы это число было низким.

Давайте рассмотрим конкретные счетчики соединений и потоков, которые предоставляет MySQL:

Threads_connected

Количество подключенных в данный момент клиентских потоков или, другими словами, количество установленных клиентских подключений. Мы объясняли важность этого в нескольких последних абзацах, поэтому вы должны знать, почему вы должны это проверить.

Threads_running

Количество клиентских потоков, которые в данный момент выполняют инструкцию. В то время как Threads_connected указывает на возможность высокого параллелизма, Threads_running фактически показывает текущую меру этого параллелизма. Всплески в этом счетчике указывают либо на повышенную нагрузку со стороны приложения, либо на медленную работу базы данных, что приводит к накоплению запросов.

Max_used_connections

Максимальное количество установленных соединений, зарегистрированных с момента последнего перезапуска сервера MySQL. Если вы подозреваете, что произошел поток подключений, но у вас нет записанной истории изменений в Threads_connected, вы можете проверить эту переменную состояния, чтобы увидеть самый высокий зарегистрированный пик.

Max_used_connections_time

Дата и время, когда MySQL Server видел максимальное количество подключений с момента последнего перезапуска до настоящего времени.

Еще одна важная метрика, которую необходимо отслеживать в отношении подключений, — это частота их отказов. Повышенный уровень ошибок может указывать на то, что у ваших приложений возникают проблемы при обмене данными с вашей базой данных. MySQL различает соединения, которые клиентам не удалось установить, и существующие соединения, которые не удалось установить из-за тайм-аута, например:

Aborted_clients

Количество уже установленных соединений, которые были прерваны. В документации MySQL упоминается, что «клиент умер, не закрыв соединение должным образом», но это также может произойти, если между сервером и клиентом возникла проблема с сетью. Частыми источниками увеличения этого счетчика являются нарушения max_allowed_packet (см. «Область видимости параметров») и тайм-ауты сеанса (см. системные переменные wait_timeout и interactive_timeout). Следует ожидать некоторых ошибок, но следует проверять острые всплески.

Aborted_connects

Количество новых соединений, которые не удалось установить. Среди причин могут быть неправильные пароли, подключение к базе данных, для которой у пользователя нет разрешения, несоответствие протоколов, нарушение времени ожидания подключения и достижение максимального_подключения, среди прочего. Они также включают различные проблемы, связанные с сетью. Под подстановочным знаком Connection_errors_% находится семейство переменных состояния, которые более подробно рассматривают некоторые конкретные проблемы. Необходимо проверить увеличение Aborted_connects, так как это может указывать на проблему с конфигурацией приложения (неправильный пользователь/пароль) или проблему с базой данных (исчерпание соединений).

Количество запросов

Следующая широкая категория метрик — это метрики, связанные с запросами. В то время как Threads_running показывает, сколько сеансов активно одновременно, метрики в этой категории будут показывать качество нагрузки, создаваемой этими сеансами. Здесь мы начнем с рассмотрения общего количества запросов, затем перейдем к разбивке запросов по типам и, наконец, что не менее важно, рассмотрим, как выполняются запросы.

Важно отслеживать количество запросов. Каждый из тридцати запущенных потоков может выполнять один часовой запрос или несколько десятков запросов в секунду. Выводы, которые вы сделаете, будут в каждом случае совершенно разными, и профиль нагрузки, скорее всего, тоже изменится. Это важные показатели, показывающие количество выполненных запросов:

Queries

Проще говоря, эта глобальная переменная состояния дает количество операторов, выполненных сервером (исключая COM_PING и COM_STATISTICS). Если вы запустите SHOW GLOBAL STATUS LIKE 'Queries' на бездействующем сервере, вы увидите, что значение счетчика увеличивается с каждым выполнением команды SHOW STATUS.

Questions

Почти то же, что и Queries, но исключает операторы, выполняемые внутри хранимых процедур, а также следующие типы запросов: COM_PING, COM_STATISTICS, COM_STMT_PREPARE, COM_STMT_CLOSE и COM_STMT_RESET. Если только клиенты вашей базы данных не используют хранимые процедуры широко, метрика Questions ближе к количеству фактических запросов, выполняемых сервером, по сравнению с количеством операторов в Queries. В дополнение к этому, Questions — это как переменная уровня сеанса, так и глобальная переменная состояния.

QPS

Число запросов в секунду. Это синтетическая метрика, которую вы можете получить, посмотрев, как переменная запросов меняется с течением времени. QPS, основанное на Queries, будет включать практически любую инструкцию, которую выполняет сервер.

Метрика QPS не говорит нам о качестве выполненных запросов, то есть о степени их воздействия на сервер, но, тем не менее, это полезный показатель. Обычно нагрузка на БД со стороны приложений носит регулярный характер. Она может двигаться волнообразно (больше днем, меньше ночью), но в течение недели или месяца проявится закономерность количества запросов с течением времени. Когда вы получаете отчет о медленной работе базы данных, просмотр QPS может дать вам быстрое представление о том, произошел ли внезапный неожиданный рост нагрузки приложения. С другой стороны, падение количества запросов в секунду может указывать на то, что проблемы связаны с базой данных, поскольку она не может обрабатывать столько запросов, сколько обычно, за одно и то же время.

Типы запросов и их качество

Следующим логическим шагом после получения информации о количестве запросов в секунду является понимание того, какие типы запросов выполняются клиентами, а также влияние этих запросов на сервер. Все запросы неравнозначны, а некоторые, можно сказать, плохие или производят ненужную нагрузку на систему. Поиск и перехват таких запросов — важная часть мониторинга. В этом разделе мы пытаемся ответить на вопрос «много ли плохих запросов?» а в разделе «Журнал медленных запросов» мы покажем вам, как поймать конкретных нарушителей.

Типы запросов

Каждый выполняемый MySQL запрос имеет тип. Более того, любая команда, которую вы можете выполнить, имеет тип. MySQL отслеживает различные типы команд и запросов, выполняемых с помощью семейства переменных состояния Com_%. В MySQL 8.0.25 таких переменных 172, что составляет почти треть всех переменных состояния. Как вы можете догадаться по этому числу, MySQL подсчитывает множество команд, о которых вы, возможно, даже не подозревали: например, Com_uninstall_plugin подсчитывает количество вызовов UNINSTALL PLUGIN, а Com_help подсчитывает использование статуса HELP.

Каждая переменная состояния Com_% доступна как на глобальном уровне, так и на уровне сеанса, как показано для Com_show_status в разделе «Переменные состояния». Однако MySQL не предоставляет счетчики других потоков для переменных Com_%, поэтому в целях мониторинга здесь предполагаются глобальные переменные состояния. Можно получить счетчики операторов других сеансов, но это достигается с помощью семейства событий Performance Schema, которое называется statement/sql/%. Это может быть полезно, чтобы попытаться найти поток, который отправляет непропорциональное количество операторов определенного типа, но это немного сложно и подпадает под исследование, а не мониторинг. Вы можете найти более подробную информацию в разделе Performance Schema Status Variable Tables документации MySQL.

Поскольку существует так много переменных состояния Com_%, отслеживание каждого типа команды было бы слишком излишним и бессмысленным. Однако вы должны попытаться сохранить значения всех из них. Вы можете пойти двумя путями, глядя на эти счетчики.

Первый вариант — выбрать типы команд, соответствующие вашему профилю загрузки базы данных, и отслеживать их. Например, если клиенты вашей базы данных не используют хранимые процедуры, просмотр Com_call_procedure — пустая трата времени. Хорошим начальным выбором является рассмотрение SELECT и основных операторов DML, которые обычно составляют большую часть нагрузки любой системы баз данных, например, Com_select, Com_insert, Com_update и Com_delete (имена переменных состояния здесь говорят сами за себя). Одна интересная вещь, которую делает MySQL, это учет обновлений и удалений нескольких таблиц (см. «Выполнение обновлений и удалений с несколькими таблицами») отдельно, в Com_update_multi и Com_delete_multi; их также следует отслеживать, если вы не уверены, что такие операторы никогда не запускаются в вашей системе.

Затем вы можете просмотреть все переменные состояния Com_%, увидеть, какие из них растут, и добавить их к выбранным отслеживаемым переменным. К сожалению, недостатком этого подхода является то, что вы можете пропустить некоторые неожиданные всплески.

Еще один способ взглянуть на эти счетчики — посмотреть на топ 5 или 10 из них с течением времени. Таким образом, внезапную смену режима нагрузки будет сложнее не заметить.

Знание того, какие типы запросов выполняются, важно для формирования общего понимания нагрузки на данную базу данных. Более того, это меняет подход к настройке базы данных, поскольку, например, для рабочей нагрузки с большим количеством вставок может потребоваться другая настройка по сравнению с рабочей нагрузкой только для чтения или в основном для чтения. Изменения в профиле загрузки запроса, такие как внезапное появление тысяч операторов UPDATE, выполняемых в секунду, могут указывать на изменения на стороне приложения.

Качество запроса

Следующим шагом после того, как мы узнаем, какие запросы выполняются, является понимание их качества или их влияния на систему. Мы упоминали об этом, но стоит повторить: не все запросы одинаковы. Некоторые из них будут нагружать систему больше, чем другие. Просмотр общих метрик, связанных с запросами, может дать вам заблаговременное предупреждение о росте проблем в базе данных. Вы узнаете, что можно заметить проблемное поведение, отслеживая всего несколько счетчиков.

Select_scan подсчитывает количество запросов, которые вызвали полное сканирование таблицы или, другими словами, заставили MySQL прочитать всю таблицу для формирования результата. Теперь мы должны сразу признать, что полное сканирование таблицы не всегда является проблемой. В конце концов, иногда просто чтение всех данных в таблице является жизнеспособной стратегией выполнения запроса, особенно когда количество строк невелико. Вы также можете ожидать, что всегда будет происходить некоторое количество полных сканирований таблиц, так как многие таблицы каталога MySQL читаются таким образом. Например, простое выполнение запроса SHOW GLOBAL STATUS приведет к увеличению Select_scan в два раза. Однако часто полное сканирование таблиц означает, что есть запросы к базе данных, которые выполняются неоптимально: либо они неправильно написаны и неэффективно фильтруют данные, либо просто отсутствуют индексы, которые могут использовать запросы. Более подробную информацию о деталях и планах выполнения запросов мы приводим в разделе «Оператор EXPLAIN».

Select_full_join аналогичен Select_scan, но подсчитывает количество запросов, вызвавших полное сканирование таблицы, на которую ссылаются, в запросе JOIN. Таблица, на которую ссылаются, является самой правой таблицей в условии JOIN — дополнительную информацию см. в разделе «Объединение двух таблиц». Опять же, как и в случае с Select_scan, трудно сказать, что большое количество Select_full_join всегда плохо. Например, в больших системах хранения данных обычно используются компактные словарные таблицы, и их полное чтение может не быть проблемой. Тем не менее, обычно высокое значение этой переменной состояния указывает на наличие плохо работающих запросов.

Select_range подсчитывает количество запросов, которые сканировали данные с некоторым условием диапазона (описано в разделе «Выбор строк с помощью предложения WHERE»). Обычно это вообще не проблема. Если условие диапазона невозможно удовлетворить с помощью индекса, значение Select_scan или Select_full_join будет расти вместе с этой переменной состояния. Вероятно, единственный случай, когда значение этого счетчика может указывать на проблему, — это когда вы видите, что оно растет, даже если вы знаете, что большинство запросов, выполняемых в базе данных, на самом деле не используют диапазоны. Пока количество связанных счетчиков сканирования таблицы также не растет, проблема, скорее всего, все еще безобидна.

Select_full_range_join объединяет Select_range и Select_full_join. Эта переменная содержит счетчик запросов, вызвавших сканирование диапазона таблиц, на которые ссылаются в запросах JOIN.

До сих пор мы считали отдельные запросы, но MySQL также ведет аналогичный учет для каждой строки, которую он считывает из механизмов хранения! Семейство переменных состояния, показывающих эти счетчики, — это переменные Handler_%. Проще говоря, каждая строка, которую MySQL читает, увеличивает некоторую переменную Handler_%. Объединение этой информации с типом запроса и счетчиками качества запроса, которые вы видели до сих пор, может сказать вам, например, является ли проблемой полное сканирование таблиц, которое выполняется в вашей базе данных.

Первый обработчик, который мы рассмотрим, это Handler_read_rnd_next, который подсчитывает количество прочитанных строк при выполнении полного или частичного сканирования таблицы. В отличие от переменных состояния Select_%, переменные Handler_% не имеют красивых, легко запоминающихся имен, поэтому необходимо некоторое усилие для запоминания. Высокие значения переменной состояния Handler_read_rnd_next в целом указывают на то, что либо множество таблиц не индексируются должным образом, либо многие запросы не используют существующие индексы. Помните, что при объяснении Select_scan мы упомянули, что некоторые полные сканирования таблиц не вызывают проблем. Чтобы понять, верно это или нет в вашем случае, посмотрите на отношение Handler_read_rnd_next к другим обработчикам. Вы хотите увидеть низкое значение для этого счетчика. Если ваша база данных возвращает в среднем миллион строк в минуту, то вы, вероятно, хотите, чтобы количество строк, возвращаемых при полном сканировании, исчислялось тысячами, а не десятками или сотнями тысяч.

Handler_read_rnd подсчитывает количество строк, обычно читаемых при выполнении сортировки результирующего набора. Высокие значения могут указывать на наличие множества полных просмотров таблиц и объединений без использования индексов. Однако, в отличие от Handler_read_rnd_next, это не является верным признаком проблем.

Handler_read_first подсчитывает, сколько раз была прочитана первая запись индекса. Высокое значение этого счетчика указывает на то, что выполняется много полных просмотров индекса. Это лучше, чем полное сканирование таблицы, но все равно вызывает проблемы. Вероятно, в некоторых запросах отсутствуют фильтры в предложениях WHERE. Значение этой переменной состояния следует снова рассматривать по отношению к другим обработчикам, поскольку некоторые полные сканирования индекса неизбежны.

Handler_read_key подсчитывает количество строк, прочитанных индексом. Вы хотите, чтобы значение этого обработчика было высоким по сравнению с другими обработчиками, связанными с чтением. В общем, большое число здесь означает, что ваши запросы правильно используют индексы.

Обратите внимание, что обработчики по-прежнему могут скрывать некоторые проблемы. Если запрос только читает строки с использованием индексов, но делает это неэффективно, то Select_scan не будет увеличиваться, а Handler_read_key — наш хороший обработчик чтения — будет расти, но конечным результатом все равно будет медленный запрос. Мы объясняем, как найти определенные медленные запросы в разделе «Журнал медленных запросов», но для них также есть специальный счетчик: Slow_queries. Эта переменная состояния подсчитывает запросы, выполнение которых заняло больше времени, чем значение Long_query_time, независимо от того, включен ли журнал медленных запросов. Вы можете постепенно уменьшать значение Long_query_time и смотреть, когда Slow_queries начнет приближаться к общему количеству запросов, выполняемых вашим сервером. Это хороший способ оценить, сколько запросов в вашей системе занимает, например, больше секунды, фактически не включая журнал медленных запросов, который имеет накладные расходы.

Не каждый выполняемый запрос доступен только для чтения, и MySQL также подсчитывает количество строк, вставленных, обновленных или удаленных, соответственно, в переменных состояния Handler_insert, Handler_update и Handler_delete. В отличие от запросов SELECT, здесь трудно делать выводы о качестве ваших операторов записи, основываясь только на переменных состояния. Однако вы можете отслеживать их, чтобы увидеть, например, что клиенты вашей базы данных начинают обновлять больше строк. Без изменения количества операторов UPDATE (переменные состояния Com_update и Com_update_multi), которые могут указывать на изменение параметров, передаваемых в те же запросы: более широкие диапазоны, больше элементов в предложениях IN и т. д. Это может не указывать на проблему само по себе, но его можно использовать при исследовании медлительности, чтобы увидеть, не увеличивается ли нагрузка на базу данных.

Помимо инструкций INSERT, инструкции UPDATE, DELETE и даже INSERT SELECT должны искать строки для изменения. Так, например, оператор DELETE увеличит счетчики, связанные с чтением, и может привести к неожиданной ситуации: не рост Select_scan, а увеличение значения Handler_read_rnd_next. Не забывайте об этой особенности, если видите расхождение между статусными переменными. Журнал медленных запросов будет включать операторы SELECT, а также операторы DML.

Временные объекты

Иногда при выполнении запросов MySQL необходимо создавать и использовать временные объекты, которые могут находиться в памяти или на диске. Примеры причин для создания временных объектов включают, среди прочего, использование предложения UNION, производных таблиц, общих табличных выражений и некоторых вариантов предложений ORDER BY и GROUP BY. В этой главе мы уже говорили почти обо всем этом, но временные объекты не проблема: некоторые из них неизбежны и действительно желательны. Тем не менее, они потребляют ресурсы вашего сервера: если временные таблицы достаточно малы, они будут храниться в памяти и использовать ее, а если они станут большими, MySQL начнет выгружать их на диск, как используя дисковое пространство, так и влияя на производительность.

MySQL поддерживает три переменные состояния, связанные с временными объектами, созданными во время выполнения запроса. Обратите внимание, что сюда не входят временные таблицы, созданные явно с помощью инструкции CREATE TEMPORARY TABLE; ищите их под счетчиком Com_create_table.

Created_tmp_tables подсчитывает количество временных таблиц, неявно созданных сервером MySQL при выполнении различных запросов. Вы не можете знать, почему и для каких запросов они были созданы, но здесь будет учитываться каждая таблица. При стабильной рабочей нагрузке вы должны увидеть одинаковое количество созданных временных таблиц, так как примерно одни и те же запросы выполняются одинаковое количество раз. Рост этого счетчика обычно связан с изменением запросов или их планов, например из-за роста базы данных, и может быть проблематичным. Хотя это полезно, создание временных таблиц даже в памяти требует ресурсов. Вы не можете полностью избежать временных таблиц, но вы должны проверить, почему их количество растет, выполнив, например, аудит запросов с помощью журнала медленных запросов.

Created_tmp_disk_tables подсчитывает количество временных таблиц, которые были выгружены на диск после того, как их размер превысил установленные верхние пределы для временных таблиц в памяти. В старом движке памяти ограничение контролировалось параметрами tmp_table_size или max_heap_table_size. MySQL 8.0 по умолчанию перешел на новый механизм TempTable для временных таблиц, который по умолчанию не переносится на диск так же, как таблицы памяти. Если для переменной temptable_use_mmap установлено значение по умолчанию ON, то временные таблицы TempTable не увеличивают эту переменную, даже если они записываются на диск.

Created_tmp_files подсчитывает количество временных файлов, созданных MySQL. Это отличается от переноса временных таблиц модуля памяти на диск, но будет учитывать записи таблиц TempTable на диск. Мы понимаем, что это может показаться сложным, и это действительно так, но серьезные изменения обычно не обходятся без некоторых недостатков.

Какую бы конфигурацию вы ни использовали, важен размер временных таблиц, а также отслеживание скорости их создания и утечки. Если рабочая нагрузка создает большое количество временных таблиц размером примерно 32 МБ, но верхний предел для таблиц в памяти составляет 16 МБ, то сервер увидит повышенную скорость ввода-вывода из-за того, что эти таблицы записываются на диск и считываются обратно с диска. Это нормально для сервера с ограниченным объемом памяти, но это пустая трата времени, если у вас есть доступная память. И наоборот, установка слишком высокого верхнего предела может привести к переключению сервера или полному сбою, как описано в разделе «Память».

Мы видели, как серверы останавливались из-за всплесков памяти, когда множество одновременно открытых подключений выполняли запросы, требующие временных таблиц. Мы также встречали серверы, на которых большая часть нагрузки ввода-вывода создавалась временными таблицами, переносящимися на диск. Как и в большинстве случаев, связанных с операционными базами данных, решение о размере таблицы является балансирующим актом. Три счетчика, которые мы показали, могут помочь вам сделать осознанный выбор.

Метрики ввода-вывода и транзакций InnoDB

До сих пор мы в основном говорили об общих показателях MySQL и игнорировали тот факт, что существуют такие вещи, как транзакции и блокировки. В этом подразделе мы рассмотрим некоторые полезные показатели, предоставляемые механизмом хранения InnoDB. Некоторые из этих показателей относятся к тому, сколько данных InnoDB читает и записывает, и почему. Некоторые, однако, могут отображать важную информацию о блокировках, которую можно комбинировать со счетчиками уровня MySQL, чтобы получить четкое представление о текущей ситуации блокировок в базе данных.

Механизм хранения InnoDB предоставляет 61 переменную состояния, показывающую различную информацию о его внутреннем состоянии. Глядя на их изменение с течением времени, вы можете увидеть, насколько загружена InnoDB и какую нагрузку на ОС она производит. Учитывая, что InnoDB является механизмом хранения по умолчанию, это, вероятно, будет большей частью нагрузки, которую производит MySQL.

Возможно, нам следовало поместить их в раздел о качестве запросов, но InnoDB поддерживает свои собственные счетчики для количества прочитанных, вставленных, обновленных или удаленных строк. Переменные, соответственно, Inndb_rows_read, Inndb_rows_inserted, Inndb_rows_updated и Inndb_rows_deleted. Обычно их значения довольно хорошо соответствуют значениям соответствующих переменных Handler_%. Если вы в основном используете таблицы InnoDB, может быть проще использовать счетчики Innodb_rows_% вместо счетчиков Handler_% для отслеживания относительной нагрузки от запросов, выраженной в количестве обработанных строк.

Другие важные и полезные переменные состояния, предоставляемые InnoDB, показывают количество данных, которые механизм хранения читает и записывает. В разделе «Диск» мы увидели, как проверять и отслеживать общее использование ввода-вывода и использование операций ввода-вывода. InnoDB позволяет вам точно увидеть, почему он читает и записывает данные и сколько их:

Innodb_data_read

Объем данных, выраженный в байтах, прочитанных с диска с момента запуска сервера. Если вы измеряете значение этой переменной с течением времени, вы можете преобразовать его в использование полосы пропускания в байтах в секунду. Эта метрика тесно связана с размером буферного пула InnoDB и его эффективностью, и мы вернемся к этому чуть позже. Можно предположить, что все эти данные считываются из файлов данных для удовлетворения запросов.

Innodb_data_writing

Объем данных, выраженный в байтах, записанных на диск с момента запуска сервера. Это то же самое, что и Innodb_data_read, но в другом направлении. Обычно на это значение приходится большая часть общей пропускной способности записи, которую генерирует MySQL. В отличие от чтения данных, InnoDB записывает данные в различных ситуациях; таким образом, существуют дополнительные переменные, определяющие части этого ввода-вывода, а также другие источники ввода-вывода.

Innodb_os_log_writing

Количество данных, выраженное в байтах, записанных InnoDB в свои журналы повторов. Эта сумма также включена в Innodb_data_writing, но ее стоит отслеживать отдельно, чтобы увидеть, не нужно ли изменить размер ваших журналов повторного выполнения. Дополнительные сведения см. в разделе «Размер журнала повторов».

Innodb_pages_writing

Количество данных, выраженное в страницах (по умолчанию 16 КиБ), записанных InnoDB во время его работы. Это вторая половина переменной состояния Innodb_data_writing. Полезно видеть количество операций ввода-вывода без повторного выполнения, которые генерирует InnoDB.

Innodb_buffer_pool_pages_flushed

Количество данных, выраженное в страницах, записанных InnoDB из-за сброса. В отличие от записей, охваченных двумя предыдущими счетчиками, записи, вызванные сбросом, не происходят сразу после выполнения фактической записи. Сброс — это сложная фоновая операция, подробности которой выходят за рамки нашей книги. Однако вы должны, по крайней мере, знать, что сбрасывание существует и что оно генерирует ввод-вывод независимо от других счетчиков.

Объединив Innodb_data_writing и Innodb_buffer_pool_pages_flushed, вы сможете получить довольно точную цифру пропускной способности диска, используемой InnoDB и MySQL Server. Добавление Innodb_data_read завершает профиль ввода-вывода InnoDB. MySQL использует не только InnoDB, а ввод-вывод может осуществляться из других частей системы, например, временные таблицы, переносимые на диск, как мы обсуждали ранее. Тем не менее, часто операции ввода-вывода InnoDB совпадают с операциями ввода-вывода MySQL Server, наблюдаемыми из операционной системы.

Одно из применений этой информации — увидеть, насколько ваш сервер MySQL близок к достижению пределов производительности вашей системы хранения. Это особенно важно в облаке, где хранилище часто имеет строгие ограничения. Во время инцидентов, связанных с производительностью базы данных, вы можете проверить счетчики, связанные с вводом-выводом, чтобы увидеть, что MySQL пишет или читает больше, возможно, указывая на увеличение нагрузки или вместо этого выполняя меньше фактических операций ввода-вывода. Последнее может означать, что MySQL в настоящее время ограничен каким-то другим ресурсом, например ЦП, или страдает от других проблем, таких как блокировка. К сожалению, уменьшение ввода-вывода также может означать, что у хранилища есть проблемы.

В InnoDB есть несколько переменных состояния, которые могут помочь найти проблемы с хранилищем или его производительностью: Innodb_data_pending_fsyncs, Innodb_data_pending_reads, Innodb_data_pending_writes, Innodb_os_log_pending_fsyncs и Innodb_os_log_pending_writes. Вы можете ожидать увидеть некоторое количество ожидающих операций чтения и записи данных, хотя, как всегда, полезно посмотреть на тенденции и предыдущие данные. Самым важным из них является Innodb_os_log_pending_fsyncs. Журналы повторов часто синхронизируются, и производительность операции синхронизации чрезвычайно важна для общей производительности и пропускной способности транзакций InnoDB.

В отличие от многих других переменных состояния, все они являются датчиками, что означает, что их значения увеличиваются и уменьшаются, а не просто увеличиваются. Вы должны попробовать эти переменные и посмотреть, как часто выполняются незавершенные операции, в частности, для синхронизации журнала повторов. Даже небольшое увеличение Innodb_os_log_pending_fsyncs может указывать на серьезные проблемы с хранилищем: либо у вас не хватает производительности, либо есть проблемы с оборудованием.

Когда мы писали о переменной Innodb_data_read, мы упомянули, что объем данных, которые читает InnoDB, связан с размером и использованием пула буферов. Давайте подробнее об этом. InnoDB кэширует страницы, считанные с диска, внутри своего буферного пула. Чем больше буферный пул, тем больше страниц будет в нем храниться и тем реже страницы придется считывать с диска. Мы говорим об этом в разделе «Размер буферного пула». Здесь, при обсуждении мониторинга, давайте посмотрим, как контролировать эффективность пула буферов. Это легко сделать с помощью всего двух переменных состояния:

Innodb_buffer_pool_read_requests

В документации MySQL это определяется как «количество логических запросов на чтение». Проще говоря, это количество страниц, которые различные операции в InnoDB хотели прочитать из пула буферов. Обычно большая часть страниц читается из-за активности запросов.

Innodb_buffer_pool_reads

Это количество страниц, которые InnoDB должна была прочитать с диска, чтобы удовлетворить запросы на чтение с помощью запросов или других операций. Значение этого счетчика обычно меньше или равно Innodb_buffer_pool_read_requests даже в самом худшем случае с полностью пустым (или «холодным») буферным пулом, потому что чтение с диска выполняется для удовлетворения запросов на чтение.

В нормальных условиях, даже при небольшом буферном пуле, вы не получите отношения 1:1 между этими переменными. То есть можно будет удовлетворить хоть какие-то чтения из буферного пула. В идеале вы должны попытаться свести количество операций чтения с диска к минимуму. Это не всегда возможно, особенно если размер базы данных намного превышает объем памяти сервера.

Вы можете попытаться оценить коэффициент попаданий в буферный пул, и в Интернете доступны формулы. Однако сравнивать значения двух переменных не совсем корректно, как сравнивать яблоки и апельсины. Если вы считаете, что значение Innodb_buffer_pool_reads слишком велико, возможно, стоит выполнить запросы, выполняемые в системе (например, используя журнал медленных запросов), вместо того, чтобы пытаться увеличить размер пула буферов. Конечно, вы должны стараться поддерживать как можно больший буферный пул, чтобы охватить большую часть или все горячие данные в базе данных. Тем не менее, по-прежнему будут запросы, которые могут привести к большому количеству операций ввода-вывода при чтении из-за получения страниц с диска (и увеличения Innodb_buffer_pool_reads при этом), и попытка исправить их путем еще большего увеличения размера пула буферов приведет к уменьшению отдачи.

Наконец, чтобы завершить обсуждение InnoDB, мы перейдем к транзакциям и блокировкам. Много информации по обеим темам было дано в главе 6, поэтому здесь мы сделаем краткий обзор связанных переменных состояния:

Счетчики команд, связанные с транзакциями

BEGIN, COMMIT и ROLLBACK — это специальные команды MySQL. Таким образом, MySQL подсчитает, сколько раз они были выполнены с переменными состояния Com_%: Com_begin, Com_commit и Com_rollback. Глядя на эти счетчики, вы можете увидеть, сколько транзакций запущено явно и либо зафиксировано, либо отменено.

Переменные состояния, связанные с блокировкой

Вы уже знаете, что InnoDB обеспечивает блокировку с детализацией на уровне строк. Это огромное улучшение по сравнению с блокировкой на уровне таблицы MyISAM, так как влияние каждой отдельной блокировки сведено к минимуму. Тем не менее, может быть влияние, если транзакции ждут друг друга даже в течение короткого времени.

InnoDB предоставляет переменные состояния, которые позволяют вам видеть, сколько блокировок создается, и дают вам подробную информацию об ожиданиях блокировок, которые происходят:

Innodb_row_lock_current_waits показывает, сколько транзакций, работающих с таблицами InnoDB, в настоящее время ожидают снятия блокировки некоторыми другими транзакциями. Значение этой переменной увеличивается с нуля при наличии заблокированных сеансов, а затем возвращается к нулю, как только блокировка разрешается.

Innodb_row_lock_waits показывает, сколько раз с момента запуска сервера транзакции в таблицах InnoDB ожидали блокировок на уровне строк. Эта переменная является счетчиком и будет постоянно увеличиваться, пока MySQL Server не будет перезапущен.

Innodb_row_lock_time показывает общее время в миллисекундах, затраченное сеансами на попытки установить блокировки для таблиц InnoDB.

Innodb_row_lock_time_avg показывает среднее время в миллисекундах, которое требуется сеансу для получения блокировки на уровне строки в таблице InnoDB. Вы можете получить такое же значение, разделив Innodb_row_lock_time на Innodb_row_lock_waits. Это значение может увеличиваться и уменьшаться в зависимости от того, сколько ожиданий блокировки встречается и насколько увеличивается накопленное время блокировки.

Innodb_row_lock_time_max показывает максимальное время в миллисекундах, которое потребовалось для получения блокировки таблицы InnoDB. Это значение будет увеличиваться только в том случае, если рекорд будет побит какой-либо другой неудачной транзакцией.

Вот пример сервера MySQL с умеренной нагрузкой на чтение/запись:

mysql> SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';
+-------------------------------+--------+
| Variable_name                 | Value  |
+-------------------------------+--------+
| Innodb_row_lock_current_waits | 0      |
| Innodb_row_lock_time          | 367465 |
| Innodb_row_lock_time_avg      | 165    |
| Innodb_row_lock_time_max      | 51056  |
| Innodb_row_lock_waits         | 2226   |
+-------------------------------+--------+
5 rows in set (0.00 sec)

Было 2 226 отдельных транзакций, ожидающих блокировок, и для получения всех этих блокировок потребовалось 367 465 миллисекунд, при этом средняя продолжительность получения блокировки составила 165 миллисекунд, а максимальная — чуть более 51 секунды. В настоящее время нет сеансов, ожидающих блокировки. Сама по себе эта информация мало что нам говорит: ни много, ни мало. Однако мы знаем, что одновременно этим сервером MySQL было выполнено более 100 000 транзакций. Результирующие значения метрик блокировки более чем разумны для уровня параллелизма.

Проблемы с блокировкой являются частым источником головной боли как для администраторов баз данных, так и для разработчиков приложений. Хотя эти метрики, как и все, что мы обсуждали до сих пор, агрегируются для каждого запущенного сеанса, отклонение от нормальных значений может помочь вам выявить некоторые проблемы. Чтобы найти и исследовать отдельные ситуации блокировки, вы можете использовать отчет о состоянии InnoDB; см. «Отчет о состоянии InnoDB Engine» для получения более подробной информации.

Журнал медленных запросов

В разделе «Типы запросов и их качество» мы показали, как искать явные признаки неоптимизированных запросов в MySQL. Однако этого недостаточно, чтобы начать оптимизировать эти запросы. Нужны конкретные примеры. Есть несколько способов сделать это, но, вероятно, самый надежный из них — использование журнала медленных запросов. Журнал медленных запросов — это именно то, на что он похож: специальный текстовый журнал, в который MySQL помещает информацию о медленных запросах. Насколько медленными должны быть эти запросы, можно контролировать, и вы можете вести журнал каждого запроса.

Чтобы включить журнал медленных запросов, вы должны изменить настройку системной переменной slow_query_log на ON вместо значения по умолчанию OFF. По умолчанию, когда журнал медленных запросов включен, MySQL будет регистрировать запросы, занимающие более 10 секунд. Это можно настроить, изменив переменную long_query_time, которая имеет минимальное значение 0, что означает, что каждый запрос, выполненный сервером, будет регистрироваться. Расположение журнала контролируется переменной slow_query_log_file, которая по умолчанию имеет значение hostname-slow.log. Если путь к журналу медленных запросов относительный, то есть он не начинается с / в Linux или, например, C:\ в Windows, то этот файл будет находиться в каталоге данных MySQL.

Вы также можете указать MySQL регистрировать запросы, не используя индексы, независимо от времени их выполнения. Для этого необходимо установить для переменной log_queries_not_using_indexes значение ON. По умолчанию DDL и административные операторы не регистрируются, но это поведение можно изменить, задав для log_slow_admin_statements значение ON.

MariaDB и Percona Server расширяют функциональность журнала медленных запросов, добавляя возможности фильтрации, ограничения скорости и расширенной детализации. Если вы используете эти продукты, стоит прочитать их документацию по этому вопросу, чтобы узнать, можете ли вы использовать расширенный журнал медленных запросов.

Вот пример из записи в журнале медленных запросов, показывающий, что оператор SELECT занимает больше времени, чем настроенное значение long_query_time, равное 1 секунде:

# Time: 2021-05-29T17:21:12.433992Z
# User@Host: root[root] @ localhost [] Id:    11
# Query_time: 1.877495 Lock_time: 0.000823 Rows_sent: 9 Rows_examined: 3473725
use employees;
SET timestamp=1622308870;
SELECT
        dpt.dept_name
      , emp.emp_no
      , emp.first_name
      , emp.last_name
      , sal.salary
FROM
        departments dpt
    JOIN dept_emp ON dpt.dept_no = dept_emp.dept_no
    JOIN employees emp ON dept_emp.emp_no = emp.emp_no
    JOIN salaries sal ON emp.emp_no = sal.emp_no
    JOIN (SELECT dept_emp.dept_no, MAX(sal.salary) maxsal
        FROM dept_emp JOIN salaries sal
            ON dept_emp.emp_no = sal.emp_no
        WHERE
                sal.from_date < now()
            AND sal.to_date > now()
        GROUP BY dept_no
        ) largest_sal_by_dept ON dept_emp.dept_no = largest_sal_by_dept.dept_no
            AND sal.salary = largest_sal_by_dept.maxsal;

Проанализировав этот вывод, вы можете сразу начать делать выводы по этому запросу. Это гораздо более информативно, чем просмотр метрик на уровне сервера. Например, мы можем видеть, что этот запрос был выполнен в 17:21:12 UTC пользователем root@localhost в базе данных employees, выполнялся 1,88 секунды и выдал 9 строк, но для получения этого результата пришлось отсканировать 3 473 725 строк. Эта информация сама по себе может многое рассказать вам о запросе, особенно если у вас уже есть опыт работы с MySQL. Теперь у вас также есть полный текст запроса, который вы можете превратить в информацию о плане выполнения, чтобы точно увидеть, как MySQL выполняет этот запрос. Вы можете найти более подробную информацию об этом процессе в разделе «Оператор EXPLAIN».

Если для параметра long_query_time задано низкое значение, журнал медленных запросов может стать большим. Иногда это может быть разумно, но чтение результирующего журнала может быть почти невозможным, если количество запросов велико. Есть инструмент под названием mysqldumpslow, который решает эту проблему. В качестве аргумента он принимает путь к файлу журнала медленных запросов, суммирует запросы из этого файла и сортирует их (по умолчанию по времени). В следующем примере команда запускается таким образом, что она покажет два самых популярных запроса, отсортированных по количеству возвращенных строк:

$ mysqldumpslow -s r -t 2 /var/lib/mysql/mysqldb1-slow.log
Reading mysql slow query log from /var/lib/mysql/mysqldb1-slow.log
Count: 2805 Time=0.00s (0s) Lock=0.00s (0s) Rows=100.0 (280500), sbuser[
sbuser]@localhost
  SELECT c FROM sbtest1 WHERE id BETWEEN N AND N

Count: 2760 Time=0.00s (0s) Lock=0.00s (0s) Rows=100.0 (276000), sbuser[
sbuser]@localhost
  SELECT c FROM sbtest1 WHERE id BETWEEN N AND N ORDER BY c

Вы можете видеть, что только эти два запроса были зарегистрированы в журнале медленных запросов 5565 раз. Представьте, что вы пытаетесь получить эту информацию без посторонней помощи! Другой инструмент, который может помочь в обобщении информации в журнале медленных запросов, — это pt-query-digest из Percona Toolkit. Инструмент немного более продвинутый и более сложный в использовании, чем mysqldumpslow, но дает много информации и имеет множество функций. Отчет, который он производит, начинается с резюме:

$ pt-query-digest /var/lib/mysql/mysqldb1-slow.log
# 7.4s user time, 60ms system time, 41.96M rss, 258.35M vsz
# Current date: Sat May 29 22:36:47 2021
# Hostname: mysqldb1
# Files: /var/lib/mysql/mysqldb1-slow.log
# Overall: 109.42k total, 15 unique, 7.29k QPS, 1.18x concurrency ________
# Time range: 2021-05-29T19:28:57 to 2021-05-29T19:29:12
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time            18s     1us    10ms   161us     1ms   462us    36us
# Lock time             2s       0     7ms    16us    14us   106us     5us
# Rows sent          1.62M       0     100   15.54   97.36   34.53    0.99
# Rows examine       3.20M       0     200   30.63  192.76   61.50    0.99
# Query size         5.84M       5     245   55.93  151.03   50.37   36.69

# Profile
# Rank Query ID                            Response time Calls R/Call V/M
# ==== =================================== ============= ===== ====== ====
#    1 0xFFFCA4D67EA0A78…  11.1853 63.1%  5467 0.0020  0.00 COMMIT
#    2 0xB2249CB854EE3C2…   1.5985  9.0%  5467 0.0003  0.00 UPDATE sbtest?
#    3 0xE81D0B3DB4FB31B…   1.5600  8.8% 54670 0.0000  0.00 SELECT sbtest?
#    4 0xF0C5AE75A52E847…   0.8853  5.0%  5467 0.0002  0.00 SELECT sbtest?
#    5 0x9934EF6887CC7A6…   0.5959  3.4%  5467 0.0001  0.00 SELECT sbtest?
#    6 0xA729E7889F57828…   0.4748  2.7%  5467 0.0001  0.00 SELECT sbtest?
#    7 0xFF7C69F51BBD3A7…   0.4511  2.5%  5467 0.0001  0.00 SELECT sbtest?
#    8 0x6C545CFB5536512…   0.3092  1.7%  5467 0.0001  0.00 INSERT sbtest?
# MISC 0xMISC               0.6629  3.7% 16482 0.0000   0.0 <7 ITEMS>

Затем каждый запрос резюмируется следующим образом:

# Query 2: 546.70 QPS, 0.16x concurrency, ID 0xB2249CB854E... at byte 1436377
# Scores: V/M = 0.00
# Time range: 2021-05-29T19:29:02 to 2021-05-29T19:29:12
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count          4    5467
# Exec time      9      2s    54us     7ms   292us     1ms   446us    93us
# Lock time     61      1s     7us     7ms   203us     1ms   437us     9us
# Rows sent      0       0       0       0       0       0       0       0
# Rows examine   0   5.34k       1       1       1       1       0       1
# Query size     3 213.55k      40      40      40      40       0      40
# String:
# Databases    sysbench
# Hosts        localhost
# Users        sbuser
# Query_time distribution
#   1us
#  10us ################################################################
# 100us ###############################################
#   1ms ############
#  10ms
# 100ms
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `sysbench` LIKE 'sbtest2'\G
#    SHOW CREATE TABLE `sysbench`.`sbtest2`\G
UPDATE sbtest2 SET k=k+1 WHERE id=497658\G
# Converted for EXPLAIN
# EXPLAIN /*!50100 PARTITIONS*/
select  k=k+1 from sbtest2 where id=497658\G

Это много ценной информации в плотном формате. Одной из отличительных особенностей этого вывода является визуализация распределения длительности запроса, которая позволяет быстро увидеть, есть ли в запросе проблемы с производительностью, зависящие от параметров. Объяснение каждой функции pt-query-digest заняло бы отдельную главу, а это продвинутый инструмент, поэтому мы оставляем его вам, чтобы вы попробовали его, как только закончите изучение MySQL.

Журнал медленных запросов — это мощный инструмент, позволяющий получить очень подробное представление о запросах, выполняемых сервером MySQL. Мы рекомендуем использовать журнал медленных запросов следующим образом:

Отчет о состоянии ядра InnoDB

Механизм хранения InnoDB имеет встроенный отчет, в котором представлены подробные технические сведения о текущем состоянии механизма. Можно многое сказать о нагрузке и производительности InnoDB, прочитав только один этот отчет, в идеале выбранный за определенный период времени. Чтение отчета о состоянии InnoDB — сложная тема, требующая большего количества инструкций, чем мы можем передать в нашей книге, а также большой практики. Тем не менее, мы считаем, что вы должны знать, что этот отчет существует, и мы дадим вам несколько советов, что в нем искать.

Для просмотра отчета нужно выполнить всего одну команду. Мы рекомендуем использовать вертикальное отображение результатов:

mysql> SHOW ENGINE INNODB STATUS\G
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2021-05-31 12:21:05 139908633830976 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 35 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 121 srv_active, 0 srv_shutdown, 69961 srv_idle
...
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
2 read views open inside InnoDB
Process ID=55171, Main thread ID=139908126139968 , state=sleeping
Number of rows inserted 2946375, updated 87845, deleted 46063, read 88688110
572.50 inserts/s, 1145.00 updates/s, 572.50 deletes/s, 236429.64 reads/s
Number of system rows inserted 109, updated 367, deleted 60, read 13218
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

Вывод, который мы здесь усекли, представлен разбитым на разделы. Сначала это может показаться пугающим, но со временем вы начнете ценить детали. А пока мы рассмотрим несколько разделов, которые, по нашему мнению, содержат информацию, которая будет полезна операторам любого уровня опыта:

Транзакции

В этом разделе представлена информация о транзакциях каждого сеанса, включая продолжительность, текущий запрос, количество удерживаемых блокировок и информацию об ожидании блокировок. Вы также можете найти здесь некоторые данные о видимости транзакций, но это редко требуется. Обычно вы хотите просмотреть раздел транзакций, чтобы увидеть текущее состояние активных транзакций в InnoDB. Пример записи из этого раздела выглядит так:

---TRANSACTION 252288, ACTIVE (PREPARED) 0 sec
5 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 4
MySQL thread id 82, OS thread handle 139908634125888,...
...query id 925076 localhost sbuser waiting for handler commit
COMMIT
Trx read view will not see trx with id >= 252287, sees < 252285

В записи говорится о том, что транзакция в настоящее время ожидает завершения COMMIT, она содержит три блокировки строк и выполняется довольно быстро, вероятно, завершится менее чем за секунду. Иногда вы увидите здесь длинные транзакции: вы должны стараться избегать их. InnoDB плохо обрабатывает длинные транзакции, и даже бездействующая транзакция, остающаяся открытой слишком долго, может повлиять на производительность.

Этот раздел также покажет вам информацию о текущем поведении блокировки, если есть транзакции, ожидающие получения блокировки. Вот пример:

---TRANSACTION 414311, ACTIVE 4 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 84, OS thread handle 139908634125888,...
...query id 2545483 localhost sbuser updating
UPDATE sbtest1 SET k=k+1 WHERE id=347110
Trx read view will not see trx with id >= 414310, sees < 413897
------- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 333 page no 4787 n bits 144 index PRIMARY of...
...table `sysbench`.`sbtest1` trx id 414311 lock_mode X locks...
...rec but not gap waiting
Record lock, heap no 33
------------------

К сожалению, отчет о состоянии не указывает напрямую на владельца блокировки, поэтому вам придется искать блокирующие транзакции отдельно. Некоторая информация об этом доступна в главе 6. Обычно, если вы видите одну активную транзакцию, в то время как другие ждут, это хороший признак того, что именно эта активная транзакция удерживает блокировки.

Файловый ввод/вывод

Этот раздел содержит информацию о текущих операциях ввода-вывода, а также агрегированные сводки по времени. Мы обсуждали это более подробно в разделе «Метрики ввода-вывода и транзакций InnoDB», но это дополнительный способ проверки наличия в InnoDB ожидающих операций с данными и журналами.

Буферный пул и память

В этом разделе InnoDB выводит информацию о своем пуле буферов и использовании памяти. Если у вас настроено несколько экземпляров пула буферов, в этом разделе будут показаны как итоговые значения, так и разбивка по экземплярам. В разделе много информации, в том числе о размере пула буферов и внутреннем состоянии пула буферов:

Total large memory allocated 274071552
Dictionary memory allocated 1377188
Buffer pool size   16384
Free buffers       1027
Database pages     14657
Old database pages 5390
Modified db pages  4168

Эти параметры также представлены как переменные Innodb_buffer_pool_%.

Семафоры

Этот раздел включает информацию о внутренних семафорах InnoDB или примитивах синхронизации. Проще говоря, семафоры — это специальные внутренние структуры в памяти, которые позволяют нескольким потокам работать, не мешая друг другу. Вы редко увидите что-либо ценное в этом разделе, если только в вашей системе нет конкуренции за семафоры. Обычно это происходит, когда InnoDB подвергается экстремальной нагрузке, поэтому каждая операция занимает больше времени, и есть больше шансов увидеть активное ожидание семафора в выводе состояния.

Методы исследования

Наличие такого количества доступных метрик, которые необходимо отслеживать, проверять и понимать, может привести к тому, что у вас закружится голова. Вы можете заметить, что мы не определили ни одной метрики с определенным диапазоном от хорошего до плохого. Блокировка плохая? Может быть, но это также ожидаемо и нормально. То же самое можно сказать почти о каждом аспекте MySQL, за исключением доступности сервера.

Эта проблема не уникальна для мониторинга серверов MySQL, она характерна для любой сложной системы. Много метрик, сложных зависимостей и почти нет строго определяемых правил, хорошо что-то или плохо. Чтобы решить эту проблему, нам нужен какой-то подход, какая-то методология, которую мы можем применить к большому количеству данных, чтобы быстро и легко делать выводы о текущей производительности системы.

К счастью, такие методики уже существуют. В этом разделе мы кратко опишем две из них и дадим идеи о том, как их применять для мониторинга метрик MySQL и ОС.

Метод USE

Метод Utilization, Saturation, Errors (USE), популяризированный Бренданом Греггом, представляет собой методологию общего назначения, которую можно применять к любой системе. Хотя он лучше подходит для ресурсов с четко определенными характеристиками производительности, такими как ЦП или диск, его также можно применять к некоторым частям MySQL.

Метод USE лучше всего использовать, создав контрольный список для каждой отдельной части системы. Во-первых, нам нужно определить, какие показатели использовать для измерения использования, насыщения и ошибок. Некоторые примеры контрольных списков для Linux можно найти на домашней странице USE: веб-сайт Грегга.

Давайте рассмотрим три разные категории на примере дискового ввода-вывода:

Использование (Utilization)

Для дисковой подсистемы, как мы изложили в разделе «Диск», мы можем посмотреть метрики для любого из этих параметров:

  • Общее использование пропускной способности хранилища

  • Использование пропускной способности хранилища для каждого процесса

  • Текущий IOPS (где известен верхний предел IOPS для устройства)

Насыщение (Saturation)

Мы упомянули насыщение, когда говорили о выводе iostat и метриках диска. Это показатель объема работы, который ресурс не может обработать и который обычно ставится в очередь. Для диска это выражается как размер очереди запросов ввода-вывода (avgqu-sz в выводе iostat). В большинстве систем значения >1 означают, что диск переполнен, и, таким образом, некоторые запросы будут поставлены в очередь и будут тратить время на бездействие.

Ошибки (Errors)

Для дисковых устройств это может означать ошибки ввода-вывода из-за деградации оборудования.

Проблема с методом USE заключается в том, что его трудно применить к сложной системе в целом. Возьмем, к примеру, MySQL: какие показатели мы могли бы использовать для измерения общего использования, насыщения и ошибок? Можно предпринять некоторые попытки применить USE к MySQL в целом, но он гораздо лучше подходит для использования в некоторых изолированных частях системы.

Давайте взглянем на некоторые возможные приложения MySQL:

Метод USE для клиентских подключений

Метод USE хорошо подходит для понимания метрик подключения клиента, которые мы обсуждали в разделе «Основные рецепты мониторинга».

Метрики использования включают Threads_connected и Threads_running.

Насыщение может быть определено произвольно на основе Threads_running. Например, в системе OLTP, которая преимущественно ориентирована на чтение и не генерирует много операций ввода-вывода, точка насыщения для Threads_running, скорее всего, будет примерно равна количеству доступных ядер ЦП. В системе, которая в основном связана с вводом-выводом, отправной точкой может быть удвоенное количество доступных ядер ЦП, но гораздо лучше определить, какое количество одновременно работающих потоков начинает насыщать подсистему хранения.

Ошибки измеряются метриками Aborted_clients и Aborted_connects. Как только значение переменной состояния Threads_connected станет равным значению системной переменной max_connections, запросы на создание новых соединений будут отклонены, а клиенты получат ошибки. Соединения также могут не работать по другим причинам, и существующие клиенты могут нечисто завершать свои соединения, не дожидаясь ответа MySQL.

Метод USE для транзакций и блокировки

Другой пример того, как можно применять USE, — это просмотр показателей, связанных с блокировкой InnoDB, относительно количества запросов, обработанных системой.

Использование можно измерить с помощью синтетической метрики QPS, которую мы определили ранее.

Насыщение может быть связано с тем, что происходит некоторое количество ожиданий блокировки. Помните, что по определению USE насыщение — это мера работы, которая не может быть выполнена и должна ждать в очереди. Здесь нет очереди, но транзакции ждут получения блокировки. В качестве альтернативы обычное количество ожиданий блокировки можно умножить вдвое, чтобы создать произвольный порог, или, что еще лучше, можно провести некоторые эксперименты, чтобы найти количество ожиданий блокировки относительно количества запросов в секунду, которые приводят к тайм-аутам блокировки.

Ошибки могут быть измерены как количество раз, когда сеансы ожидали блокировок по тайм-ауту или были прекращены для разрешения ситуации взаимоблокировки. В отличие от ранее упомянутых метрик, эти две метрики не отображаются как глобальные переменные состояния, а вместо этого могут быть найдены в таблице information_schema.innodb_metrics в разделах lock_deadlocks (количество зарегистрированных взаимоблокировок) и lock_timeouts (количество времени ожидания блокировки).

Определить частоту ошибок только на основе показателей мониторинга может быть сложно, поэтому часто используется только часть US в USE. Как видите, этот метод позволяет нам взглянуть на систему с предопределенной точки зрения. Вместо того, чтобы анализировать все возможные показатели при возникновении инцидента, можно просмотреть существующий контрольный список, что сэкономит время и усилия.

Метод RED

Метод Rate, Errors, Duration (RED) был создан для устранения недостатков USE. Методология аналогична, но ее проще применять к сложным системам и службам.

Логическое применение RED к MySQL выполняется с учетом производительности запросов:

Скорость (Rate)

QPS базы данных

Ошибки (Errors)

Количество или частота ошибок и неудачных запросов

Продолжительность (Duration)

Средняя задержка запроса

Одна проблема с RED в контексте этой книги и изучения MySQL в целом заключается в том, что применение этого метода требует мониторинга данных, который нельзя получить, просто прочитав переменные состояния. Не каждое существующее решение для мониторинга MySQL также может предоставить необходимые данные, хотя вы можете увидеть пример того, как это делается, в блоге Петра Зайцева RED Method for MySQL Performance Analyses. Одним из способов применения RED к MySQL или любой другой системе баз данных является просмотр метрик со стороны приложения, а не со стороны базы данных. Несколько решений для мониторинга позволяют вам (вручную или автоматически) использовать ваши приложения для сбора данных, таких как количество запросов, частота сбоев и задержка запросов. Как раз то, что нужно для RED!

Вы можете и должны использовать RED и USE вместе. В статье, объясняющей RED, The RED Method: How to Instrument Your Services, ее автор Том Уилки упоминает, что «на самом деле это просто два разных взгляда на одну и ту же систему».

Одно, возможно, неожиданное преимущество применения RED, USE или любого другого метода заключается в том, что вы делаете это до того, как произойдет инцидент. Таким образом, вы вынуждены понимать, что именно вы отслеживаете и измеряете, и как это соотносится с тем, что действительно важно для вашей системы и ее пользователей.

Инструменты мониторинга MySQL

На протяжении всей этой главы мы говорили о метриках и методологиях мониторинга, но не упомянули ни одного реального инструмента, который мог бы превратить эти метрики в информационные панели или оповещения. Другими словами, мы не говорили о реальных инструментах и системах мониторинга. Первая причина этого достаточно проста: мы считаем, что в начале для вас гораздо важнее знать, что отслеживать и почему, а не как. Если бы мы сосредоточились на том, как, мы могли бы посвятить всю эту главу особенностям и различиям различных инструментов мониторинга. Вторая причина заключается в том, что метрики MySQL и ОС меняются нечасто, но если вы читаете эту книгу в 2025 году, наш выбор инструментов мониторинга уже может показаться устаревшим.

Тем не менее, в качестве отправной точки мы составили список известных популярных инструментов мониторинга с открытым исходным кодом, которые можно использовать для мониторинга доступности и производительности MySQL. Мы не будем слишком углубляться в особенности их настройки или конфигурации или сравнивать их. Мы также не можем перечислить все возможные доступные решения для мониторинга: краткое исследование показывает нам, что почти все, что является «инструментом мониторинга» или «системой мониторинга», может каким-то образом отслеживать MySQL. Мы также не рассматриваем не-открытые и несвободные решения для мониторинга, за одним исключением Oracle Enterprise Monitor. Мы ничего не имеем против таких систем в целом, и многие из них замечательны. Тем не менее, большинство из них имеют отличную документацию и доступную поддержку, поэтому вы сможете быстро с ними ознакомиться.

Здесь будут упомянуты следующие системы мониторинга:

Мы начнем с Prometheus и InfluxDB и его стека TICK. Обе эти системы представляют собой современный подход к мониторингу, они хорошо подходят для мониторинга микросервисов и обширных облачных развертываний, но также широко используются в качестве решений для мониторинга общего назначения:

Prometheus

Созданная на основе внутренней системы мониторинга Google Borgmon, Prometheus является чрезвычайно популярной системой мониторинга и оповещения общего назначения. В его основе лежит база данных временных рядов и механизм сбора данных, основанный на pull-модели. Это означает, что именно сервер Prometheus активно собирает данные со своих целей мониторинга.

Фактический сбор данных с целей Prometheus осуществляется специальными программами, называемыми экспортерами. Экспортеры созданы специально: есть специальный экспортер MySQL, экспортер PostgreSQL, базовый экспортер метрик/узлов ОС и многие другие. Эти программы собирают метрики из системы, для мониторинга которой они написаны, и представляют эти метрики в формате, пригодном для использования сервером Prometheus.

Мониторинг MySQL с помощью Prometheus осуществляется запуском программы mysqld_exporter. Как и большинство частей экосистемы Prometheus, она написана на Go и доступна для Linux, Windows и многих других операционных системах, что делает ее хорошим выбором для гетерогенной среды.

Экспортер MySQL собирает все метрики, которые мы рассмотрели в этой главе (и многие другие!), и, поскольку он активно пытается получить информацию от MySQL, он также может сообщать о доступности сервера MySQL. Помимо стандартных метрик, можно задавать пользовательские запросы, которые будет выполнять экспортер, превращая их результаты в дополнительные метрики.

Prometheus предлагает только самые базовые возможности визуализации, поэтому в настройку обычно добавляется веб-приложение для аналитики и визуализации данных Grafana.

InfluxDB и стек TICK

Созданная на основе базы данных временных рядов InfluxDB, TICK, обозначающая Telegraf, InfluxDB, Chronograf и Kapacitor, представляет собой полную платформу временных рядов и мониторинга. Сравнивая эту систему с Prometheus, Telegraf занимает место экспортеров; это единая программа, способная отслеживать множество целей. В отличие от экспортеров, Telegraf активно отправляет данные в InfluxDB вместо того, чтобы данные извлекались сервером. Chronograf — это административный интерфейс и интерфейс данных. Kapacitor — это механизм обработки данных и оповещения.

Там, где вам нужно было установить специальный экспортер для MySQL, Telegraf расширяется с помощью плагинов. Плагин MySQL является частью стандартного пакета и предоставляет подробный обзор показателей базы данных MySQL. К сожалению, он не может выполнять произвольные запросы, поэтому возможности расширения ограничены. В качестве обходного пути можно использовать плагин exec. Telegraf также является мультиплатформенной программой и среди других ОС поддерживает Windows.

Стек TICK часто используется частично, а Grafana добавляется в InfluxDB и Telegraf.

Общим для Prometheus и TICK является то, что они представляют собой наборы строительных блоков, позволяющих вам создать собственное решение для мониторинга. Ни один из них не предлагает готовых рецептов для информационных панелей, предупреждений и т. д. Они мощные, но могут потребовать некоторого привыкания. Кроме того, они очень ориентированы на автоматизацию и инфраструктуру как код. Prometheus особенно, но TICK также предоставляет минимальный графический интерфейс и изначально не был задуман для исследования и визуализации данных. Это мониторинг, реагирующий на изменения значений метрик путем оповещения, а не визуальной проверки различных метрик. Добавление Grafana к уравнению, особенно с самодельными панелями управления или инструментальными панелями сообщества для MySQL, делает возможным визуальный контроль. Тем не менее, большая часть конфигурации и настройки не будет выполняться в графическом интерфейсе.

Обе эти системы приобрели популярность в середине и конце 2010-х годов, когда произошел переход к использованию множества небольших серверов вместо нескольких крупных. Этот сдвиг потребовал некоторых изменений в подходах к мониторингу, и эти системы стали практически стандартными решениями для мониторинга де-факто для многих компаний.

Далее в нашем обзоре еще пара «олдскульных» решений для мониторинга:

Zabbix

Zabbix — полностью бесплатная система мониторинга с открытым исходным кодом, впервые выпущенная в 2001 году, является проверенной и мощной. Zabbix поддерживает широкий спектр отслеживаемых целей и расширенные возможности автоматического обнаружения и оповещения.

Мониторинг MySQL с помощью Zabbix можно выполнять с помощью плагинов или официальных шаблонов MySQL. Покрытие метрик довольно хорошее, и каждый рецепт, который мы определили, доступен. Однако и mysqld_exporter, и Telegraf предлагают больше данных. Стандартных метрик MySQL, которые собирает Zabbix, достаточно для настройки базового мониторинга MySQL, но для более глубокого понимания вам потребуется настроить или использовать некоторые шаблоны и плагины сообщества.

Агент Zabbix является кроссплатформенным, поэтому вы можете отслеживать работу MySQL практически на любой ОС.

Хотя Zabbix предлагает довольно мощные оповещения, его возможности визуализации могут показаться немного устаревшими. Можно настроить пользовательские информационные панели на основе данных MySQL, а также можно использовать Zabbix в качестве источника данных для Grafana.

Zabbix полностью настраивается через графический интерфейс. Коммерческое предложение включает различные уровни поддержки и консультаций.

Nagios Core

Как и Zabbix, Nagios — система мониторинга ветеранов, первый выпуск которой увидел свет в 2002 году. В отличие от других систем, которые мы видели до сих пор, Nagios — это система «open core». Дистрибутив Nagios Core бесплатный и с открытым исходным кодом, но есть и коммерческая система Nagios.

Мониторинг MySQL настраивается с помощью плагинов. Они должны предоставлять достаточно данных для настройки базового мониторинга, аналогичного официальным шаблонам Zabbix. При необходимости можно расширить собранные метрики.

Оповещения, визуализации и настройка аналогичны Zabbix. Одной из примечательных особенностей Nagios является то, что на пике популярности он многократно разветвлялся. Одними из самых популярных вилок Nagios являются Icinga и Shinken. Check_MK также изначально был расширением Nagios, которое в конечном итоге стало самостоятельным коммерческим продуктом.

И Nagios, и его ответвления, и Zabbix могут и успешно используются многими компаниями для мониторинга MySQL. Несмотря на то, что они могут показаться устаревшими в своей архитектуре и представлении данных, они могут выполнять свою работу. Их самая большая проблема заключается в том, что стандартные данные, которые они собирают, могут показаться ограниченными по сравнению с альтернативными, и вам нужно будет использовать плагины и расширения сообщества. Раньше Percona поддерживала набор плагинов мониторинга для Nagios, а также для Zabbix, но отказалась от них и теперь сосредоточилась на своем собственном предложении мониторинга, Percona Monitoring and Management (PMM), которое мы вскоре обсудим.

Все рассмотренные нами системы имеют одну общую черту: они представляют собой решения для мониторинга общего назначения, а не специально предназначенные для мониторинга баз данных. Это их сила, но и их слабость. Когда дело доходит до мониторинга и исследования глубоких внутренних баз данных, вам часто приходится вручную расширять возможности этих систем. Например, одной функцией, которой нет ни у одного из них, является сохранение статистики выполнения отдельных запросов. Технически добавить эту функцию можно, но это может быть громоздко и проблематично.

Мы закончим этот раздел рассмотрением двух ориентированных на базы данных решений для мониторинга: MySQL Enterprise Monitor от Oracle и Percona Monitoring and Management. Как вы увидите, они похожи по функциональности, которую они предоставляют, и обе они являются большими улучшениями по сравнению с неспециализированными системами мониторинга:

MySQL Enterprise Monitor

Являясь частью MySQL Enterprise Edition, Enterprise Monitor представляет собой полную платформу мониторинга и управления базами данных MySQL.

С точки зрения мониторинга, MySQL Enterprise Monitor расширяет обычные метрики, собираемые системами мониторинга, добавляя сведения об использовании памяти MySQL, сведения о вводе-выводе для каждого файла и широкий спектр информационных панелей, основанных на переменных состояния InnoDB. Данные берутся из самой MySQL без участия каких-либо агентов. Теоретически все те же данные можно собрать и визуализировать любой другой системой мониторинга, но здесь они плотно забиты продуманными дашбордами и категориями.

Enterprise Monitor включает в себя подсистему событий, которая представляет собой набор предопределенных предупреждений. В дополнение к функциям, специфичным для базы данных, Enterprise Monitor включает обзор топологии репликации для обычной репликации и репликации с несколькими источниками, групповой репликации и кластера NDB. Еще одна функция — мониторинг состояния выполнения резервного копирования (для резервных копий, сделанных с помощью MySQL Enterprise Backup).

Мы упоминали, что статистика выполнения отдельных запросов и история запросов обычно отсутствуют в системах мониторинга общего назначения. MySQL Enterprise Monitor включает в себя анализатор запросов, который позволяет получить представление об истории запросов, выполненных с течением времени, а также о собранной статистике по запросам. Можно просматривать такую информацию, как среднее количество прочитанных и возвращенных строк и распределение продолжительности, и даже просматривать планы выполнения запросов.

Enterprise Monitor — хорошая система мониторинга баз данных. На самом деле его самым большим недостатком является то, что он доступен только в Enterprise Edition MySQL. К сожалению, большинство установок MySQL не могут извлечь выгоду из Enterprise Monitor и того уровня понимания, который он обеспечивает для базы данных и показателей операционной системы. Он также не подходит для мониторинга чего-либо, кроме MySQL, запросов, которые он выполняет, и ОС, на которой он работает, а мониторинг MySQL ограничен продуктами Oracle.

Доступна 30-дневная пробная версия MySQL Enterprise Edition, которая включает в себя Enterprise Monitor, и Oracle также поддерживает список визуальных демонстраций системы.

Percona Monitoring and Management

Решение для мониторинга Percona, PMM, похоже по функциональности на Oracle Enterprise Monitor, но является полностью бесплатным и с открытым исходным кодом. Задуманный как «единое стекло», PMM пытается обеспечить глубокое понимание производительности MySQL и ОС, а также может использоваться для мониторинга баз данных MongoDB и PostgreSQL.

PMM построен на основе существующих компонентов с открытым исходным кодом, таких как уже рассмотренный Prometheus и его экспортеры, а также Grafana. Percona поддерживает форки используемых экспортеров баз данных, в том числе для MySQL, и добавляет функциональность и метрики, которых не было в исходных версиях. Кроме того, PMM скрывает сложность, обычно связанную с развертыванием и настройкой этих инструментов, и вместо этого предоставляет собственный пакет и интерфейс настройки.

Как и Enterprise Monitor, PMM предлагает набор информационных панелей, визуализирующих почти все аспекты работы MySQL и InnoDB, а также предоставляющих множество подробностей о состоянии базовой ОС. Это было расширено за счет включения таких технологий, как PXC/Galera, обсуждаемых в главе 13, и ProxySQL, обсуждаемых в главе 15. Поскольку PMM использует Prometheus и экспортеры, можно расширить диапазон отслеживаемых баз данных, добавив внешние экспортеры. В дополнение к этому PMM поддерживает системы DBaaS, такие как RDS и CloudSQL.

PMM поставляется с пользовательским приложением под названием Query Analytics (QAN), которое представляет собой систему мониторинга запросов и показателей. Как и анализатор запросов Enterprise Monitor, QAN показывает общую историю запросов, выполненных в данной системе, а также информацию об отдельных запросах. Это включает в себя историю количества выполнений запроса с течением времени, прочитанных и отправленных строк, блокировок и созданных временных таблиц, среди прочего. QAN позволяет просматривать план запроса и структуры задействованных таблиц.

Управляющая (Management) часть PMM пока существует только в названии, так как на момент написания это чисто система мониторинга. PMM поддерживает оповещения через стандартный AlertManager Prometheus или с помощью внутренних шаблонов.

Одна существенная проблема с PMM заключается в том, что по умолчанию он поддерживает только цели, работающие в Linux. Поскольку экспортеры Prometheus являются кроссплатформенными, вы можете добавить цели Windows (или другой ОС) в PMM, но вы не сможете использовать некоторые преимущества инструмента, такие как упрощенная настройка экспортера и пакетная установка клиентского программного обеспечения.

Оба автора этой книги в настоящее время работают в Percona, поэтому у вас может возникнуть соблазн отклонить наше описание PMM как рекламу. Тем не менее, мы попытались дать честный обзор нескольких систем мониторинга и не утверждаем, что PMM идеальна. Если ваша компания уже использует корпоративную версию MySQL, то вам обязательно нужно сначала посмотреть, что может предложить MySQL Enterprise Monitor.

Прежде чем мы закроем этот раздел, мы хотим отметить, что во многих случаях фактическая система мониторинга, которую вы используете, не имеет большого значения. Каждая система, которую мы упомянули, обеспечивает мониторинг доступности MySQL, а также некоторый уровень понимания внутренних показателей — достаточный для выбора рецептов, которые мы дали ранее. Особенно, когда вы изучаете MySQL и, возможно, начинаете использовать ее установки в производственной среде, вам следует попытаться использовать существующую инфраструктуру мониторинга. Спешка все изменить к лучшему часто приводит к неудовлетворительным результатам. По мере накопления опыта вы будете замечать, что в имеющихся у вас инструментах отсутствует все больше и больше данных, и сможете принять взвешенное решение о том, какой новый инструмент выбрать.

Инциденты/диагностика и ручной сбор данных

Иногда у вас может не быть настроенной системы мониторинга для базы данных или вы можете не доверять ей, чтобы она содержала всю информацию, которая может понадобиться для расследования какой-либо проблемы. Или у вас может быть запущен экземпляр DBaaS, и вы хотите получить больше данных, чем предоставляет вам ваш облачный провайдер. В таких ситуациях ручной сбор данных может быть жизнеспособным вариантом в краткосрочной перспективе, чтобы быстро получить некоторые данные из системы. Мы покажем вам несколько инструментов, которые вы можете использовать, чтобы сделать это: быстро собрать много диагностической информации из любого экземпляра MySQL.

Следующие разделы представляют собой короткие полезные рецепты, которые вы можете взять и использовать в своей повседневной работе с базами данных MySQL.

Периодический сбор значений переменных состояния системы

В разделах «Переменные состояния» и «Основные рецепты мониторинга» мы много говорили о том, как меняются значения различных переменных состояния с течением времени. Каждый инструмент мониторинга, упомянутый в предыдущем разделе, делает это для накопления данных, которые затем используются для построения графиков и предупреждений. Такую же выборку переменных состояния можно выполнить вручную, если вы хотите просмотреть необработанные данные или просто произвести выборку с меньшим интервалом, чем использует ваша система мониторинга.

Вы можете написать простой сценарий, запускающий монитор MySQL в цикле, но лучше использовать встроенную утилиту mysqladmin. Эту программу можно использовать для выполнения широкого спектра административных операций на работающем сервере MySQL, хотя следует отметить, что каждую из них можно выполнять и через обычный файл mysql. Однако mysqladmin можно использовать для простой выборки глобальных переменных состояния, именно это мы и собираемся использовать здесь.

mysqladmin включает два вывода состояния: обычный и расширенный. Обычный менее информативен:

$ mysqladmin status
Uptime: 176190 Threads: 5 Questions: 5287160 ...
... Slow queries: 5114814 Opens: 761 Flush tables: 3 ...
... Open tables: 671 Queries per second avg: 30.008

На этом этапе расширенный вывод будет вам знаком. Это то же самое, что и вывод SHOW GLOBAL STATUS:

$ mysqladmin extended-status
+-----------------------------------------------+---------------------+
| Variable_name                                 | Value               |
+-----------------------------------------------+---------------------+
| Aborted_clients                               | 2                   |
| Aborted_connects                              | 30                  |
| ...                                                                 |
| Uptime                                        | 176307              |
| Uptime_since_flush_status                     | 32141               |
| validate_password.dictionary_file_last_parsed | 2021-05-29 21:50:38 |
| validate_password.dictionary_file_words_count | 0                   |
+-----------------------------------------------+---------------------+

Удобно, что mysqladmin может повторять команды, которые он запускает с заданным интервалом, заданное количество раз. Например, следующая команда заставит mysqladmin печатать значения переменной состояния каждую секунду в течение минуты (ext — это сокращение от extended-status (расширенное состояние)):

$ mysqladmin -i1 -c60 ext

Перенаправляя его вывод в файл, вы можете получить минутную выборку изменений метрик базы данных. Работать с текстовыми файлами не так приятно, как с полноценными системами мониторинга, но опять же, это обычно делается при особых обстоятельствах. В течение долгого времени сбор информации о MySQL с помощью mysqladmin был стандартной практикой, поэтому существует инструмент под названием pt-mext, который может преобразовать простые выходные данные SHOW GLOBAL STATUS в формат, более подходящий для использования людьми. К сожалению, инструмент доступен только в Linux. Вот пример его вывода:

$ pt-mext -r -- cat mysqladmin.output | grep Bytes_sent
Bytes_sent 10836285314 15120 15120 31080 15120 15120 31080 15120 15120

Начальное большое число — это значение переменной состояния в первой выборке, а значения после этого представляют собой изменение исходного числа. Если бы значение уменьшалось, отображалось бы отрицательное число.

Использование pt-stalk для сбора метрик MySQL и ОС

pt-stalk является частью Percona Toolkit и обычно запускается вместе с MySQL и используется для постоянной проверки определенных условий. Как только это условие выполнено — например, значение Threads_running больше 15 — pt-stalk запускает процедуру сбора данных, собирающую обширную информацию о MySQL и операционной системе. Однако можно использовать часть сбора данных без фактического вмешательства в работу сервера MySQL. Хотя это и неправильный способ использования pt-stalk, это полезный метод для быстрого просмотра неизвестного сервера или попытки собрать как можно больше информации о плохо работающем сервере.

pt-stalk, как и другие инструменты Percona Toolkit, доступен только для Linux, хотя целевой сервер MySQL может работать на любой ОС. Основной вызов pt-stalk для достижения этого прост:

$ sudo pt-stalk --no-stalk --iterations=2 --sleep=30 \
--dest="/tmp/collected_data" \
-- --user=root --password=<root password>;

Утилита запустит два раунда сбора данных, каждый из которых будет длиться минуту, и будет спать в течение 30 секунд между ними. Если вам не нужна информация об ОС или вы не можете ее получить, поскольку вашей целью является экземпляр DBaaS, вы можете использовать флаг --mysql-only:

$ sudo pt-stalk --no-stalk --iterations=2 --sleep=30 \
--mysql-only --dest="/tmp/collected_data" \
-- --user=root --password=<root password> \
--host=<mysql host> --port=<mysql port>;

Вот список файлов, созданных за один раунд сбора. Мы намеренно опустили файлы, относящиеся к ОС, но их довольно много:

2021_04_15_04_33_44-innodbstatus1
2021_04_15_04_33_44-innodbstatus2
2021_04_15_04_33_44-log_error
2021_04_15_04_33_44-mutex-status1
2021_04_15_04_33_44-mutex-status2
2021_04_15_04_33_44-mysqladmin
2021_04_15_04_33_44-opentables1
2021_04_15_04_33_44-opentables2
2021_04_15_04_33_44-processlist
2021_04_15_04_33_44-slave-status
2021_04_15_04_33_44-transactions
2021_04_15_04_33_44-trigger
2021_04_15_04_33_44-variables

Расширенный ручной сбор данных

pt-stalk не всегда доступен и работает не на всех платформах. Иногда вам также может понадобиться добавить (или удалить некоторые) данные, которые он собирает. Вы можете использовать ранее представленную команду mysqladmin, чтобы собрать немного больше данных и упаковать их в простой сценарий. Вариант этого сценария часто используется авторами этой книги в их повседневной работе.

Этот скрипт, который должен работать в любой Linux или Unix-подобной системе, будет выполняться непрерывно либо до завершения, либо до тех пор, пока не будет найден файл /tmp/exit-flag. Вы можете запустить touch /tmp/exit-flag, чтобы изящно завершить выполнение этого скрипта. Мы рекомендуем поместить его в файл и запустить через nohup ... & или выполнить его в сеансе screen или tmux. Если вы не знакомы с терминами, которые мы только что упомянули, все они представляют собой способы убедиться, что сценарий продолжает выполняться при отключении сеанса. Вот сценарий:

DATADEST="/tmp/collected_data";
MYSQL="mysql --host=127.0.0.1 -uroot -proot";
MYSQLADMIN="mysqladmin --host=127.0.0.1 -uroot -proot";
[ -d "$DATADEST" ] || mkdir $DATADEST;
while true; do {
  [ -f /tmp/exit-flag ] \
    && echo "exiting loop (/tmp/exit-flag found)" \
    && break;
  d=$(date +%F_%T |tr ":" "-");
  $MYSQL -e "SHOW ENGINE INNODB STATUS\G" > $DATADEST/$d-innodbstatus &
  $MYSQL -e "SHOW ENGINE INNODB MUTEX;" > $DATADEST/$d-innodbmutex &
  $MYSQL -e "SHOW FULL PROCESSLIST\G" > $DATADEST/$d-processlist &
  $MYSQLADMIN -i1 -c15 ext > $DATADEST/$d-mysqladmin ;
} done;
$MYSQL -e "SHOW GLOBAL VARIABLES;" > $DATADEST/$d-variables;

Мы также создали версию того же скрипта для Windows, написанную в PowerShell. Он ведет себя точно так же, как и предыдущий скрипт, и завершится сам по себе, как только будет найден файл C:\tmp\exit-flag:

$mysqlbin='C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe'
$mysqladminbin='C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqladmin.exe'

$user='root'
$password='root'
$mysqlhost='127.0.0.1'

$destination='C:\tmp\collected_data'
$stopfile='C:\tmp\exit-flag'

if (-Not (Test-Path -Path '$destination')) {
  mkdir -p '$destination'
}

Start-Process -NoNewWindow $mysqlbin -ArgumentList `
  '-h$mysqlhost','-u$user','-p$password','-e 'SHOW GLOBAL VARIABLES;' `
  -RedirectStandardOutput '$destination\variables'

while(1) {
  if (Test-Path -Path '$stopfile') {
    echo 'Found exit monitor file, aborting'
    break;
  }
  $d=(Get-Date -Format 'yyyy-MM-d_HHmmss')
  Start-Process -NoNewWindow $mysqlbin -ArgumentList `
    '-h$mysqlhost','-u$user','-p$password','-e 'SHOW ENGINE INNODB STATUS\G'' `
    -RedirectStandardOutput '$destination\$d-innodbstatus'
  Start-Process -NoNewWindow $mysqlbin -ArgumentList `
    '-h$mysqlhost','-u$user','-p$password','-e 'SHOW ENGINE INNODB MUTEX;'' `
    -RedirectStandardOutput '$destination\$d-innodbmutex'
  Start-Process -NoNewWindow $mysqlbin -ArgumentList `
    '-h$mysqlhost','-u$user','-p$password','-e 'SHOW FULL PROCESSLIST\G'' `
    -RedirectStandardOutput '$destination\$d-processlist'
  & $mysqladminbin '-h$mysqlhost' -u'$user' -p'$password' `
    -i1 -c15 ext > '$destination\$d-mysqladmin';
}

Вы должны помнить, что сбор данных на основе скриптов не заменяет надлежащий мониторинг. У него есть свои применения, которые мы описали в начале этого раздела, но он всегда должен быть дополнением к тому, что у вас уже есть, а не единственным способом взглянуть на метрики MySQL.

К настоящему времени, прочитав эту главу, вы должны иметь довольно хорошее представление о том, как подходить к мониторингу MySQL. Помните, что проблемы, инциденты и сбои будут происходить — они неизбежны. Однако при надлежащем мониторинге вы можете убедиться, что одна и та же проблема не возникает дважды, поскольку вы сможете найти основную причину после первого возникновения. Конечно, вы также сможете избежать некоторых проблем, изменив свою систему, чтобы исправить проблемы, обнаруженные вашими усилиями по мониторингу MySQL.

Мы оставим вас с заключительной мыслью: идеальный мониторинг недостижим, но даже самый элементарный мониторинг лучше, чем его полное отсутствие.

Глава 13
Высокая доступность

В контексте ИТ термин высокая доступность определяет состояние непрерывной работы в течение определенного периода времени. Цель не в том, чтобы устранить риск неудачи — это было бы невозможно. Скорее, мы пытаемся гарантировать, что в случае сбоя система останется доступной для продолжения работы. Мы часто измеряем доступность по стандарту 100% работоспособности или безотказности. Общий стандарт доступности известен как пять девяток или 99,999% доступности. Две девятки — это система, гарантирующая доступность на уровне 99% и допускающая время простоя до 1%. В течение года это будет означать 3,65 дня недоступности.

При проектировании надежности используются три принципа системного проектирования, помогающие достичь высокой доступности: устранение единых точек отказа (SPOF), надежные точки пересечения или переключения при отказе и возможности обнаружения отказов (включая мониторинг, обсуждаемый в главе 12).

Избыточность требуется для многих компонентов для достижения высокой доступности. Простой пример — самолет с двумя двигателями. Если во время полета выйдет из строя один двигатель, самолет все равно сможет приземлиться в аэропорту. Более сложный пример — атомная электростанция, где существует множество избыточных протоколов и компонентов, позволяющих избежать катастрофических сбоев. Точно так же для достижения высокой доступности базы данных нам необходимо резервирование сети, резервирование дисков, различные источники питания, несколько серверов приложений и баз данных и многое другое.

В этой главе основное внимание будет уделено вариантам достижения высокой доступности, предлагаемым базами данных MySQL.

Асинхронная репликация

Репликация позволяет копировать данные с одного сервера базы данных MySQL (известного как источник (source))) на один или несколько других серверов базы данных MySQL (известных как реплики (replicas)). Репликация MySQL по умолчанию является асинхронной. При асинхронной репликации источник записывает события в свой бинарный журнал, а реплики запрашивают их по мере готовности. Нет никакой гарантии, что какое-либо событие когда-либо достигнет какой-либо реплики. Это слабосвязанная связь источник/реплика, где верно следующее:

Асинхронная репликация обеспечивает меньшую задержку записи, поскольку запись подтверждается локально источником перед записью в реплики.

MySQL реализует свои возможности репликации, используя три основных потока, один на исходном сервере и два на репликах:

Поток дампа бинарного журнала

Источник создает поток для отправки содержимого бинарного журнала реплике при подключении реплики. Мы можем идентифицировать этот поток в выводе SHOW PROCESSLIST в источнике как поток Binlog Dump.

Поток дампа бинарного журнала блокирует бинарный журнал источника для чтения каждого события, отправленного реплике. Когда источник считывает событие, блокировка снимается еще до того, как источник отправит событие реплике.

Поток ввода/вывода репликации

Когда мы выполняем оператор START SLAVE на сервере-реплике, реплика создает поток ввода-вывода, подключенный к источнику, и запрашивает отправку обновлений, записанных в его бинарных журналах.

Поток ввода-вывода репликации считывает обновления, которые отправляет поток Binlog Dump источника (см. предыдущий пункт), и копирует их в локальные файлы, составляющие журнал ретрансляции реплики.

MySQL показывает состояние этого потока как Slave_IO_running в выводе SHOW SLAVE STATUS.

SQL-поток репликации

Реплика создает поток SQL для чтения журнала ретрансляции, записанного

Есть способы улучшить распараллеливание репликации, как вы увидите далее в этой главе.

На рисунке 13.1 показано, как выглядит архитектура репликации MySQL.

Рисунок 13-1. Архитектура потока асинхронной репликации

Репликация работает, потому что события, записываемые в бинарный журнал, считываются из источника, а затем обрабатываются на реплике, как показано на рисунке 13-1. События записываются в бинарный журнал в различных форматах в зависимости от типа события. Репликация MySQL имеет три вида бинарных форматов регистрации:

Репликация на основе строк (RBR)

Источник записывает в бинарный журнал события, указывающие на изменение отдельных строк таблицы. Репликация источника в реплику работает путем копирования событий, представляющих изменения строк таблицы реплики. Для MySQL 5.7 и 8.0 это формат репликации по умолчанию.

Репликация на основе операторов (SBR)

Источник записывает операторы SQL в бинарный журнал. Репликация источника в реплику работает путем выполнения инструкций SQL на реплике.

Смешанная репликация

Вы также можете настроить MySQL для использования сочетания ведения журнала на основе операторов и на основе строк, в зависимости от того, какой из них наиболее подходит для регистрации изменений. С ведением журнала смешанного формата MySQL по умолчанию использует журнал на основе операторов, но переключается на журнал на основе строк для определенных небезопасных операторов, которые имеют недетерминированное поведение. Например, предположим, что у нас есть следующее выражение:

mysql> UPDATE customer SET last_update=NOW() WHERE customer_id=1;

Мы знаем, что функция NOW() возвращает текущую дату и время. Представьте, что источник повторяет оператор с задержкой в 1 секунду (для этого могут быть разные причины, например, реплика находится на другом континенте, чем источник). Когда реплика получает оператор и выполняет его, разница в дате и времени, возвращаемых функцией, составляет 1 секунду, что приводит к несогласованности данных между источником и репликой. Когда используется смешанный формат репликации, всякий раз, когда MySQL анализирует недетерминированную функцию, подобную этой, она преобразует оператор в репликацию на основе строк. Список других функций, которые MySQL считает небезопасными, можно найти в документации.

Основные параметры для установки источника и реплики

Есть некоторые основные настройки, которые нам нужно установить как на исходном сервере, так и на сервере-реплике, чтобы репликация работала. Они необходимы для всех методов, описанных в этом разделе.

На исходном сервере необходимо включить ведение бинарного журнала и определить ID сервера. Вам потребуется перезапустить сервер после внесения этих изменений (если вы еще этого не сделали), потому что эти параметры не являются динамическими.

Вот как это будет выглядеть в файле my.cnf:

[mysqld]
log-bin=mysql-bin
server-id=1

Вам также необходимо установить ID сервера для каждой реплики. Как и в случае с исходным кодом, если вы еще этого не сделали, вам необходимо перезапустить сервер-реплику после присвоения ему идентификатора. Не обязательно включать бинарный журнал на сервере-реплике, хотя это рекомендуется сделать:

[mysqld]
log-bin=mysql-replica-bin
server-id=1617565330
binlog_format = ROW
log_slave_updates

Использование параметра log_slave_updates сообщает серверу реплики, что команды с исходного сервера должны регистрироваться в собственном бинарном журнале реплики. Опять же, это не обязательно, но рекомендуется в качестве хорошей практики.

Каждая реплика подключается к источнику с использованием имени пользователя и пароля MySQL, поэтому вам также потребуется создать учетную запись пользователя на исходном сервере, которую реплика может использовать для подключения (для обновления информации см. «Создание и использование новых пользователей»). Для этой операции можно использовать любую учетную запись, если ей предоставлена привилегия REPLICATION SLAVE. Вот пример того, как создать пользователя на исходном сервере:

mysqll> CREATE USER 'repl'@'%' IDENTIFIED BY 'P@ssw0rd!';
mysqll> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

В следующих разделах вы увидите различные варианты создания сервера-реплики.

Создание реплики с помощью PerconaXtraBackup

Как мы видели в главе 10, инструмент Percona XtraBackup предоставляет метод выполнения горячего резервного копирования ваших данных MySQL во время работы системы. Он также предлагает расширенные возможности, такие как распараллеливание, сжатие и шифрование.

Первым шагом является копирование текущего источника, чтобы мы могли запустить нашу реплику. Инструмент XtraBackup выполняет физическое резервное копирование источника (см. «Физические и логические резервные копии»). Мы будем использовать команды, представленные в «Percona XtraBackup»:

# xtrabackup --defaults-file=my.cnf -uroot -p_<password>_ \
-H <host> -P 3306 --backup --parallel=4 \
--datadir=./data/ --target-dir=./backup/

Кроме того, вы можете использовать rsync, NFS или любой другой метод, который вам удобен.

Как только XtraBackup завершит резервное копирование, мы отправим файлы в каталог резервного копирования на сервере-реплике. В этом примере мы отправим файлы с помощью команды scp:

# scp -r ./backup/* <user>@<host>:/backup

На этом мы закончили с источником. Следующие шаги будут выполняться только на сервере-реплике. Следующим шагом будет подготовка нашей резервной копии:

# xtrabackup --prepare --apply-log --target-dir=./

Когда все настроено, мы собираемся переместить резервную копию в каталог данных:

# xtrabackup --defaults-file=/etc/my.cnf --copy-back --target-dir=./backup

На реплике содержимое файла xtrabackup_binlog_info будет выглядеть примерно так:

$ cat /backup/xtrabackup_binlog_info
mysql-bin.000003    156

Эта информация важна, потому что она говорит нам, с чего начать репликацию. Помните, что источник все еще получал операции, когда мы делали резервную копию, поэтому нам нужно знать, в какой позиции находился MySQL в файле бинарного журнала, когда резервное копирование завершилось.

С этой информацией мы можем запустить команду для запуска репликации. Это будет выглядеть примерно так:

mysql> CHANGE MASTER TO MASTER_HOST='192.168.1.2', MASTER_USER='repl',
    -> MASTER_PASSWORD='P@ssw0rd!',
    -> MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=156;
mysql> START SLAVE;

После того, как вы начали, вы можете запустить команду SHOW SLAVE STATUS, чтобы проверить, работает ли репликация:

mysql> SHOW SLAVE STATUS\G
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 8332
              Relay_Log_Space: 8752
              Until_Condition: None
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:

Важно проверить, запущены ли оба потока (Slave_IO_Running и Slave_SQL_Running), были ли ошибки (Last_Error) и на сколько секунд реплика отстает от источника. Для больших баз данных с интенсивной рабочей нагрузкой записи реплике может потребоваться некоторое время, чтобы наверстать упущенное.

Создание реплики с помощью плагина Clone

MySQL 8.0.17 представил плагин клонирования, который можно использовать, чтобы сделать один экземпляр сервера MySQL клоном другого. Мы ссылаемся на экземпляр сервера, где выполняется оператор CLONE, как на получателя, а на исходный экземпляр сервера, с которого получатель будет клонировать данные, как на донора. Экземпляр-донор может быть локальным или удаленным. Процесс клонирования работает путем создания физического моментального снимка данных и метаданных, хранящихся в механизме хранения InnoDB на доноре, и передачи его получателю. И локальные, и удаленные экземпляры выполняют одну и ту же операцию клонирования; нет никакой разницы, связанной с данными между двумя вариантами.

Давайте рассмотрим реальный пример. По ходу дела мы покажем вам некоторые дополнительные подробности, например, как отслеживать ход выполнения долго выполняющейся команды CLONE, привилегии, необходимые для клонирования, и многое другое. В следующем примере используется классическая оболочка. Мы поговорим о MySQL Shell, представленном в MySQL 8.0, в главе 16.

Выберите сервер MySQL для клонирования и подключитесь к нему как пользователь root. Затем установите плагин клонирования, создайте пользователя для передачи данных с сервера-донора и предоставьте этому пользователю привилегию BACKUP_ADMIN:

mysql> INSTALL PLUGIN CLONE SONAME "mysql_clone.so";
mysql> CREATE USER clone_user@'%' IDENTIFIED BY "clone_password";
mysql> GRANT BACKUP_ADMIN ON *.* to clone_user;

Затем, чтобы наблюдать за ходом операции клонирования, нам нужно предоставить этому пользователю привилегии для просмотра базы данных performance_schema и выполнения функций:

mysql> GRANT SELECT ON performance_schema.* TO clone_user;
mysql> GRANT EXECUTE ON *.* to clone_user;

Теперь перейдем к серверу-получателю. Если вы предоставляете новый узел, сначала инициализируйте каталог данных и запустите сервер.

Подключитесь к серверу-получателю в качестве пользователя root. Затем установите плагин клонирования, создайте пользователя для замены данных текущего экземпляра клонированными данными и предоставьте этому пользователю привилегию CLONE_ADMIN. Мы также предоставим список действительных доноров, которых реципиент может клонировать (здесь только один):

mysql> INSTALL PLUGIN CLONE SONAME "mysql_clone.so";
mysql> SET GLOBAL clone_valid_donor_list = "127.0.0.1:21122";
mysql> CREATE USER clone_user IDENTIFIED BY "clone_password";
mysql> GRANT CLONE_ADMIN ON *.* to clone_user;

Мы предоставим этому пользователю те же привилегии, что и на стороне донора, чтобы наблюдать за прогрессом на стороне получателя:

mysql> GRANT SELECT ON performance_schema.* TO clone_user;
mysql> GRANT EXECUTE ON *.* to clone_user;

Теперь у нас есть все необходимое, так что пришло время начать процесс клонирования. Обратите внимание, что сервер-донор должен быть доступен для получателя. Получатель подключится к донору с предоставленным адресом и учетными данными и начнет клонирование:

mysql> CLONE INSTANCE FROM clone_user@192.168.1.2:3306
    -> IDENTIFIED BY "clone_password";

Получатель должен завершить работу и перезапустить себя, чтобы операция клонирования прошла успешно. Мы можем отслеживать прогресс с помощью следующего запроса:

SELECT STAGE, STATE, CAST(BEGIN_TIME AS TIME) as "START TIME",
CASE WHEN END_TIME IS NULL THEN
LPAD(sys.format_time(POWER(10,12) * (UNIX_TIMESTAMP(now()) - UNIX_TIMESTAMP(BEGIN_TIME))), 10,' )
ELSE
LPAD(sys.format_time(POWER(10,12) * (UNIX_TIMESTAMP(END_TIME) - UNIX_TIMESTAMP(BEGIN_TIME))), 10, )
END AS DURATION,
LPAD(CONCAT(FORMAT(ROUND(ESTIMATE/1024/1024,0), 0)," MB"), 16, )
AS "Estimate",
CASE WHEN BEGIN_TIME IS NULL THEN LPAD('0%, 7, ' )
WHEN ESTIMATE > 0 THEN
LPAD(CONCAT(CAST(ROUND(DATA*100/ESTIMATE, 0) AS BINARY), "%"), 7, ' ')
WHEN END_TIME IS NULL THEN LPAD('0%, 7, ' )
ELSE LPAD('100%, 7, ' ') END AS "Done(%)"
from performance_schema.clone_progress;

Это позволит нам наблюдать за каждым состоянием процесса клонирования. Вывод будет примерно таким:

+-----------+-----------+------------+-----------+----------+---------+
| STAGE     | STATE     | START TIME | DURATION  | Estimate | Done(%) |
+-----------+-----------+------------+-----------+----------+---------+
| DROP DATA | Completed | 14:44:46   |    1.33 s |     0 MB | 100%    |
+-----------+-----------+------------+-----------+----------+---------+
| FILE COPY | Completed | 14:44:48   |    5.62 s | 1,511 MB | 100%    |
+-----------+-----------+------------+-----------+----------+---------+
| PAGE COPY | Completed | 14:44:53   | 95.06 ms  | 0 MB     | 100%    |
+-----------+-----------+------------+-----------+----------+---------+
| REDO COPY | Completed | 14:44:54   | 99.71 ms  |     0 MB | 100%    |
+-----------+-----------+------------+-----------+----------+---------+
| FILE SYNC | Completed | 14:44:54   |    6.33 s |     0 MB | 100%    |
+-----------+-----------+------------+-----------+----------+---------+
| RESTART   | Completed | 14:45:00   |    4.08 s |     0 MB | 100%    |
+-----------+-----------+------------+-----------+----------+---------+
| RECOVERY  | Completed | 14:45:04   | 516.86 ms |     0 MB | 100%    |
+-----------+-----------+------------+-----------+----------+---------+
7 rows in set (0.08 sec)

Как упоминалось ранее, в конце есть перезагрузка. Обратите внимание, что репликация еще не началась.

В дополнение к клонированию данных операция клонирования извлекает позицию бинарного журнала и GTID с сервера-донора и передает их получателю. Мы можем выполнить следующие запросы к донору, чтобы просмотреть позицию бинарного журнала или GTID последней примененной транзакции:

mysql> SELECT BINLOG_FILE, BINLOG_POSITION FROM performance_schema.clone_status;
+------------------+-----------------+
| BINLOG_FILE      | BINLOG_POSITION |
+------------------+-----------------+
| mysql-bin.000002 |       816804753 |
+------------------+-----------------+
1 row in set (0.01 sec)
mysql> SELECT @@GLOBAL.GTID_EXECUTED;
+------------------------+
| @@GLOBAL.GTID_EXECUTED |
+------------------------+
|                        |
+------------------------+
1 row in set (0.00 sec)

В этом примере мы не используем GTID, поэтому запрос ничего не возвращает. Далее мы запустим команду для запуска репликации:

mysql> CHANGE MASTER TO MASTER_HOST = '192.168.1.2', MASTER_PORT = 3306,
    -> MASTER_USER = 'repl', MASTER_PASSWORD = 'P@ssw0rd!',
    -> MASTER_LOG_FILE = 'mysql-bin.000002',
    -> MASTER_LOG_POSITION = 816804753;
mysql> START SLAVE;

Как и в предыдущем разделе, мы можем проверить правильность работы репликации, выполнив команду SHOW SLAVE STATUS.

Преимущество такого подхода в том, что плагин клонирования автоматизирует весь процесс, и только в конце необходимо выполнить команду CHANGE MASTER. Минус в том, что плагин доступен только для MySQL 8.0.17 и выше. Хотя это все еще относительно новый процесс, мы считаем, что в ближайшие годы этот процесс может стать стандартным.

Создание реплики с помощью mysqldump

Это то, что можно назвать классическим подходом. Это типичный вариант для тех, кто только начинает работать с MySQL и все еще изучает экосистему. Как обычно, мы предполагаем, что вы выполнили необходимую настройку в разделе «Основные параметры для установки источника и реплики».

Давайте посмотрим на пример использования mysqldump для создания новой реплики. Мы выполним резервное копирование с исходного сервера:

# mysqldump -uroot -p<password> --single-transaction \
    --all-databases --routines --triggers --events \
    --master-data=2 > backup.sql

Дамп выполнен успешно, если в конце появляется сообщение Dump completed:

# tail -1f backup.sql
-- Dump completed on 2021-04-26 20:16:33

Сделав резервную копию, нам нужно импортировать ее на сервер-реплику. Например, вы можете использовать эту команду:

$ mysql < backup.sql

Как только это будет сделано, вам нужно будет выполнить команду CHANGE MASTER с координатами, извлеченными из дампа (для получения дополнительной информации о mysqldump см. «Программа mysqldump»). Поскольку мы использовали параметр --master-data=2, информация будет записана в начале дампа. Например:

$ head -n 35 out
-- MySQL dump 10.13 Distrib 5.7.31-34, for Linux (x86_64)
--
-- Host: 127.0.0.1   Database:
-- ------------------------------------------------------
-- Server version  5.7.33-log
...
--
-- Position to start replication or point-in-time recovery from
--
-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=4089;

Или, если вы используете GTID:

--
-- GTID state at the beginning of the backup
-- (origin: @@global.gtid_executed)
--
SET @@GLOBAL.GTID_PURGED=00048008-1111-1111-1111-111111111111:1-16;

Далее мы собираемся выполнить команду для запуска репликации. Для сценария GTID это выглядит так:

mysql> CHANGE MASTER TO MASTER_HOST='192.168.1.2', MASTER_USER='repl',
    -> MASTER_PASSWORD = 'P@ssw0rd!', MASTER_AUTO_POSITION=1;
mysql> START SLAVE;

Для традиционной репликации вы можете начать репликацию с ранее извлеченной позиции файла бинарного журнала следующим образом:

mysql> CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=4089,
    -> MASTER_HOST='192.168.1.2', MASTER_USER='repl',
    -> MASTER_PASSWORD='P@ssw0rd!';
mysql> START SLAVE;

Чтобы убедиться, что репликация работает, выполните команду SHOW SLAVE STATUS.

Создание реплики с использованием mydumper и myloader

mysqldump — наиболее распространенный инструмент, используемый новичками для создания резервных копий и создания реплик. Но есть более эффективный метод: mydumper. Как и mysqldump, этот инструмент создает логическую резервную копию и может использоваться для создания согласованной резервной копии вашей базы данных. Основное различие между mydumper и mysqldump заключается в том, что mydumper в паре с myloader может делать дамп и восстанавливать данные параллельно, улучшая дамп и, особенно, время восстановления. Представьте себе сценарий, в котором ваша база данных имеет дамп размером 500 ГБ. Используя mysqldump, вы получите один огромный файл. С mydumper у вас будет один файл для каждой таблицы, что позволит позже выполнять процесс восстановления параллельно.

Настройка утилит mydumper и myloader

Вы можете запускать mydumper непосредственно на исходном сервере или с другого сервера, что в целом лучше, поскольку позволяет избежать накладных расходов в системе хранения на запись файлов резервных копий на тот же сервер.

Чтобы установить mydumper, загрузите пакет для используемой версии операционной системы. Релизы можно найти в репозитории mydumper на GitHub. Давайте посмотрим на пример для CentOS:

# yum install https://github.com/maxbube/mydumper/releases/download/v0.10.3/ \
mydumper-0.10.3-1.el7.x86_64.rpm -y

Теперь у вас должны быть установлены обе команды mydumper и myloader на сервере. Вы можете подтвердить это с помощью:

$ mydumper --version
mydumper 0.10.3, built against MySQL 5.7.33-36
$ myloader --version
myloader 0.10.3, built against MySQL 5.7.33-36
Извлечение данных из источника

Следующая команда выполнит дамп всех баз данных (кроме mysql, test и схемы sys) с 15 одновременными потоками, а также будет включать триггеры, представления и функции:

# mydumper --regex '^(?!(mysql\.|test\.|sys\.))' --threads=15 \
    --user=learning_user --password='learning_mysql' --host=192.168.1.2 \
    --port=3306 --trx-consistency-only --events --routines --triggers \
    --compress --outputdir /backup --logfile /tmp/log.out --verbose=2

Если вы проверите выходной каталог (outputdir), вы увидите сжатые файлы. Вот вывод на одной из машин авторов:

# ls -l backup/
total 5008
-rw...1 vinicius.grippa percona   182 May 1 19:30 metadata
-rw...1 vinicius.grippa percona   258 May 1 19:30 sysbench.sbtest10-schema.sql.gz
-rw...1 vinicius.grippa percona 96568 May 1 19:30 sysbench.sbtest10.sql.gz
-rw...1 vinicius.grippa percona   258 May 1 19:30 sysbench.sbtest11-schema.sql.gz
-rw...1 vinicius.grippa percona 96588 May 1 19:30 sysbench.sbtest11.sql.gz
-rw...1 vinicius.grippa percona   258 May 1 19:30 sysbench.sbtest12-schema.sql.gz
...
Восстановление данных на сервере-реплике

Как и в случае с mysqldump, нам нужно, чтобы копия экземпляра MySQL уже была запущена и работала. Когда данные готовы к импорту, мы можем выполнить следующую команду:

# myloader --user=learning_user --password='learning_mysql'
--threads=25 --host=192.168.1.3 --port=3306
--directory=/backup --overwrite-tables --verbose 3
Создание системы репликации

Теперь, когда мы восстановили данные, настроим репликацию. Нам нужно найти правильную позицию бинарного журнала в начале резервного копирования. Эта информация хранится в файле метаданных mydumper:

$ cat backup/metadata
Started dump at: 2021-05-01 19:30:00
SHOW MASTER STATUS:
    Log: mysql-bin.000002
    Pos: 9530779
    GTID:00049010-1111-1111-1111-111111111111:1-319

Finished dump at: 2021-05-01 19:30:01

Теперь мы просто выполняем команду CHANGE MASTER, как мы это делали ранее для mysqldump:

mysql> CHANGE MASTER TO MASTER_HOST='192.168.1.2', MASTER_USER='repl',
    -> MASTER_PASSWORD='P@ssw0rd!', MASTER_LOG_FILE='mysql-bin.000002',
    -> MASTER_LOG_POS=9530779, MASTER_PORT=49010;
mysql> START SLAVE;

Групповая репликация

Включение групповой репликации в группу асинхронной репликации может вызвать некоторые споры. Краткое объяснение этого выбора заключается в том, что групповая репликация является асинхронной. Путаницу здесь можно объяснить сравнением с Galera (обсуждается в разделе «Кластер Galera/PXC»), который утверждает, что является синхронным или виртуально синхронным.

Более подробное рассуждение состоит в том, что это зависит от того, как мы определяем репликацию. В мире MySQL мы определяем репликацию как процесс, который позволяет автоматически дублировать изменения, внесенные в одну базу данных (источник), в другую (реплику). Весь процесс включает пять различных этапов:

  1. Локальное применение изменения к источнику

  2. Генерация бинарного журнала событий

  3. Отправка бинарного журнала событий в реплику(и)

  4. Добавление бинарного журнала событий в журнал ретрансляции реплики

  5. Применение бинарного журнала событий из журнала ретрансляции в реплике

В MySQL Group Replication и Galera (даже если кеш Galera в первую очередь заменяет файлы журналов бинарного и ретрансляции) синхронным является только шаг 3 — потоковая передача события бинарного журнала (или набора записей в Galera) в реплику (и).

Таким образом, хотя процесс отправки (репликации/потоковой передачи) данных на другие серверы является синхронным, применение этих изменений по-прежнему полностью асинхронно.

Установка групповой репликации

Первое преимущество групповой репликации по сравнению с Galera заключается в том, что вам не нужно устанавливать разные бинарные файлы. MySQL Server предоставляет групповую репликацию в качестве подключаемого модуля. Он также доступен для Oracle MySQL и Percona Server для MySQL; подробности об их установке см. в Главе 1.

Чтобы убедиться, что плагин групповой репликации включен, выполните следующий запрос:

mysql> SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE
    -> FROM INFORMATION_SCHEMA.PLUGINS
    -> WHERE PLUGIN_NAME LIKE 'group_replication';

На выходе должно отображаться значение ACTIVE, как вы видите здесь:

+-------------------+---------------+-------------------+
| PLUGIN_NAME       | PLUGIN_STATUS | PLUGIN_TYPE       |
+-------------------+---------------+-------------------+
| group_replication | ACTIVE        | GROUP REPLICATION |
+-------------------+---------------+-------------------+
1 row in set (0.00 sec)

Если плагин не установлен, выполните следующую команду, чтобы установить его:

mysql> INSTALL PLUGIN group_replication SONAME 'group_replication.so';

При активном плагине мы установим минимальные параметры, необходимые на серверах для запуска групповой репликации. Откройте my.cnf на сервере 1 и добавьте следующее:

[mysqld]
server_id=175907211
log-bin=mysqld-bin
enforce_gtid_consistency=ON
gtid_mode=ON
log-slave-updates
transaction_write_set_extraction=XXHASH64
master_info_repository=TABLE
relay_log_info_repository=TABLE
binlog_checksum=NONE

Давайте рассмотрим каждый из этих параметров:

server_id

Как и в случае с классической репликацией, этот параметр помогает идентифицировать каждого члена группы с помощью уникального ID. Вы должны использовать разные значения для каждого сервера, участвующего в групповой репликации.

log_bin

В MySQL 8.0 этот параметр включен по умолчанию. Он отвечает за запись всех изменений в базе данных в файлы бинарного журнала.

enforce_gtid_consistency

Это значение должно быть установлено в ON, чтобы указать MySQL выполнять операторы, безопасные для транзакций, чтобы обеспечить согласованность при репликации данных.

gtid_mode

Эта директива включает ведение журнала на основе идентификаторов глобальных транзакций, если установлено значение ON. Это необходимо для групповой репликации.

log_slave_updates

Для этого значения установлено значение ON, чтобы участники могли регистрировать обновления друг друга. Другими словами, директива объединяет серверы репликации в цепочку.

transaction_write_set_extraction

Это значение указывает серверу MySQL собирать наборы записей и кодировать их с использованием алгоритма хеширования. В данном случае мы используем алгоритм XXHASH64. Наборы записи определяются первичными ключами каждой записи.

master_info_repository

Если установлено значение TABLE, эта директива позволяет MySQL хранить сведения об исходных файлах бинарных журналов и позициях в таблице, а не в файле, чтобы обеспечить более быструю репликацию и гарантировать согласованность с использованием свойств ACID InnoDB. В MySQL 8.0.23 это значение по умолчанию, а параметр FILE устарел.

relay_log_info_repository

Если установлено значение TABLE, это настраивает MySQL для хранения информации о репликации в виде таблицы InnoDB. В MySQL 8.0.23 это значение по умолчанию, а параметр FILE устарел.

binlog_checksum

Установка этого параметра в NONE указывает MySQL не записывать контрольную сумму для каждого события в бинарном журнале. Вместо этого сервер будет проверять события, когда они записываются, проверяя их длину. В версиях MySQL до 8.0.20 включительно групповая репликация не может использовать контрольные суммы. Если вы используете более позднюю версию и хотите использовать контрольные суммы, вы можете опустить этот параметр и использовать значение по умолчанию CRC32.

Далее мы добавим некоторые параметры для групповой репликации:

[mysqld]

loose-group_replication_group_name="8dc32851-d7f2-4b63-8989-5d4b467d8251"
loose-group_replication_start_on_boot=OFF
loose-group_replication_local_address="10.124.33.139:33061"
loose-group_replication_group_seeds="10.124.33.139:33061,
10.124.33.90:33061, 10.124.33.224:33061"
loose-group_replication_bootstrap_group=OFF
bind-address = "0.0.0.0"
report_host = "10.124.33.139"

Давайте посмотрим, что делает каждый параметр:

group_replication_group_name

Это название группы, которую мы создаем. Мы собираемся использовать встроенную в Linux команду uuidgen для создания универсального уникального идентификатора (UUID). Он производит вывод следующим образом:

$ uuidgen
8dc32851-d7f2-4b63-8989-5d4b467d8251
group_replication_start_on_boot

Если установлено значение OFF, это значение указывает плагину не начинать работу автоматически при запуске сервера. Вы можете установить для этого значения значение ON после завершения настройки всех членов группы.

loose-group_replication_local_address

Это комбинация внутреннего IP-адреса и порта, используемая для связи с другими членами сервера MySQL в группе. Рекомендуемый порт для групповой репликации — 33061.

group_replication_group_seeds

Этоn параметр настраивает IP-адреса или имена хостов членов, участвующих в групповой репликации, вместе с их коммуникационным портом. Новые участники используют это значение, чтобы утвердиться в группе.

group_replication_bootstrap_group

Этот параметр указывает серверу, создавать ли группу или нет. Мы включим эту опцию только по запросу на сервере 1, чтобы избежать создания нескольких групп. Так что пока он останется выключенным.

bind_address

Значение 0.0.0.0 указывает MySQL прослушивать все сети.

report_host

Это IP-адрес или имя хоста, которые члены группы сообщают друг другу при регистрации в группе.

Настройка групповой репликации MySQL

Сначала мы настроим канал group_replication_recovery. MySQL Group Replication использует этот канал для передачи транзакций между участниками. Из-за этого мы должны настроить пользователя репликации с разрешением REPLICATION SLAVE на каждом сервере.

Итак, на сервере 1 войдите в консоль MySQL и выполните следующие команды:

mysql> SET SQL_LOG_BIN=0;
mysql> CREATE USER replication_user@'%' IDENTIFIED BY 'P@ssw0rd!';
mysql> GRANT REPLICATION SLAVE ON *.* TO 'replication_user'@'%';
mysql> FLUSH PRIVILEGES;
mysql> SET SQL_LOG_BIN=1;

Сначала мы устанавливаем для SQL_LOG_BIN значение 0, чтобы предотвратить запись сведений о новом пользователе в бинарный журнал, а затем снова включаем его в конце.

Чтобы указать серверу MySQL использовать пользователя репликации, которого мы создали для канала group_replication_recovery, выполните следующую команду:

mysql> CHANGE MASTER TO MASTER_USER='replication_user',
    -> MASTER_PASSWORD='P@ssw0rd!' FOR CHANNEL
    -> 'group_replication_recovery';

Эти настройки позволят участникам, присоединяющимся к группе, запустить процесс распределенного восстановления, чтобы достичь того же состояния, что и другие участники (доноры).

Теперь мы запустим службу групповой репликации на сервере 1. Мы загрузим группу с помощью этих команд:

mysql> SET GLOBAL group_replication_bootstrap_group=ON;
mysql> START GROUP_REPLICATION;
mysql> SET GLOBAL group_replication_bootstrap_group=OFF;

Чтобы избежать запуска большего количества групп, мы устанавливаем group_replication_bootstrap_group обратно в OFF после успешного запуска группы.

Чтобы проверить статус нового участника, используйте эту команду:

mysql> SELECT * FROM performance_schema.replication_group_members;
+---------------------------+--------------------------------------+...
| CHANNEL_NAME              | MEMBER_ID                            |...
+---------------------------+--------------------------------------+...
| group_replication_applier | d58b2766-ab90-11eb-ba00-00163ed02a2e |...
+-------------+-------------+--------------+-------------+---------+...
...+---------------+-------------+--------------+-------------+----------------+
...| MEMBER_HOST   | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE | MEMBER_VERSION |
...+---------------+-------------+--------------+-------------+----------------+
...| 10.124.33.139 |        3306 | ONLINE       | PRIMARY     | 8.0.22         |
...+---------------+-------------+--------------+-------------+----------------+
1 row in set (0.00 sec)

Отлично. На данный момент мы загрузили и инициировали одного члена группы. Переходим ко второму серверу. Убедитесь, что вы установили ту же версию MySQL, что и на сервере 1, и добавьте следующие настройки в файл my.cnf:

[mysqld]
loose-group_replication_group_name="8dc32851-d7f2-4b63-8989-5d4b467d851"
loose-group_replication_start_on_boot=OFF
loose-group_replication_local_address="10.124.33.90:33061"
loose-group_replication_group_seeds="10.124.33.139:33061,
10.124.33.90:33061, 10.124.33.224:33061"
loose-group_replication_bootstrap_group=OFF
bind-address = "0.0.0.0"

Все, что мы изменили, это group_replication_local_address; остальные настройки остаются прежними. Обратите внимание, что для сервера 2 требуются другие конфигурации MySQL, и мы настоятельно рекомендуем сохранять их одинаковыми для всех узлов.

После настройки перезапустите службу MySQL:

# systemctl restart mysqld

Введите следующие команды, чтобы настроить учетные данные для пользователя восстановления на сервере 2:

mysql> SET SQL_LOG_BIN=0;
mysql> CREATE USER 'replication_user'@'%' IDENTIFIED BY 'P@ssw0rd!';
mysql> GRANT REPLICATION SLAVE ON *. TO 'replication_user'@'%';*
mysql> SET SQL_LOG_BIN=1;
mysql> CHANGE MASTER TO MASTER_USER='replication_user', MASTER_PASSWORD='PASSWORD' FOR CHANNEL 'group_replication_recovery';

Затем добавьте сервер 2 в группу, которую мы загрузили ранее:

mysql> START GROUP_REPLICATION;

И запустите запрос, чтобы проверить состояние члена:

mysql> SELECT * FROM performance_schema.replication_group_members;
+---------------------------+--------------------------------------+...
| CHANNEL_NAME              | MEMBER_ID                            |...
+---------------------------+--------------------------------------+...
| group_replication_applier | 9e971ba0-ab9d-11eb-afc6-00163ec43109 |...
| group_replication_applier | d58b2766-ab90-11eb-ba00-00163ed02a2e |...
+-------------+-------------+--------------+-------------+---------+...
...+---------------+-------------+--------------+...
...| MEMBER_HOST   | MEMBER_PORT | MEMBER_STATE |...
...+---------------+-------------+--------------+...
...| 10.124.33.90  | 3306        | ONLINE       |...
...| 10.124.33.139 |        3306 | ONLINE       |...
...+---------------+-------------+--------------+...
...+-------------+----------------+
...| MEMBER_ROLE | MEMBER_VERSION |
...+-------------+----------------+
...| SECONDARY   | 8.0.22         |
...| PRIMARY     | 8.0.22         |
...+-------------+----------------+
2 rows in set (0.00 sec)

Теперь вы можете выполнить те же шаги для сервера 3, которые мы использовали для сервера 2, снова обновив локальный адрес. Когда вы закончите, вы можете проверить, отвечают ли все серверы, вставив некоторые фиктивные данные:

mysql> CREATE DATABASE learning_mysql;
Query OK, 1 row affected (0.00 sec)
mysql> USE learning_mysql
Database changed
mysql> CREATE TABLE test (i int primary key);
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO test VALUES (1);
Query OK, 1 row affected (0.00 sec)

Затем подключитесь к другим серверам, чтобы увидеть, можете ли вы визуализировать данные.

Синхронная репликация

Синхронная репликация используется Galera Clusters, где у нас есть более одного сервера MySQl, но они действуют как единое целое для приложения. На рисунке 13-2 показан кластер Galera с тремя узлами.

Рисунок 13-2. В кластере Galera все узлы взаимодействуют друг с другом.

Основное различие между синхронной и асинхронной репликацией заключается в том, что синхронная репликация гарантирует, что если изменение произойдет на одном узле в кластере, то это изменение произойдет на других узлах в кластере синхронно или в то же время. Асинхронная репликация не дает никаких гарантий относительно задержки между применением изменений на исходном узле и распространением этих изменений на узлы-реплики. Задержка при асинхронной репликации может быть короткой или длинной. Это также означает, что в случае сбоя исходного узла в топологии асинхронной репликации некоторые из последних изменений могут быть потеряны. Эти концепции источника и реплики не существуют в кластере Galera. Все узлы могут получать операции чтения и записи.

Теоретически синхронная репликация имеет несколько преимуществ перед асинхронной репликацией:

Однако у синхронной репликации есть и недостатки. Традиционно нетерпеливые протоколы репликации координируют узлы по одной операции за раз, используя двухэтапную фиксацию или распределенную блокировку. Увеличение количества узлов в кластере приводит к увеличению времени отклика транзакций и вероятности возникновения конфликтов и взаимоблокировок между узлами. Это связано с тем, что все узлы должны подтвердить транзакцию и ответить сообщением OK.

По этой причине асинхронная репликация остается доминирующим протоколом репликации для производительности, масштабируемости и доступности базы данных. Непонимание или недооценка влияния синхронной репликации — одна из причин, по которой компании иногда отказываются от использования кластеров Galera и возвращаются к использованию асинхронной репликации.

На момент написания статьи две компании поддерживали Galera Cluster: Percona и MariaDB. В следующем примере показано, как настроить кластер Percona XtraDB.

Galera/PXC Cluster

Установка Percona XtraDB Cluster (PXC) аналогична установке Percona Server (разница в пакетах), поэтому мы не будем углубляться в детали для всех платформ. Вы можете вернуться к главе 1, чтобы просмотреть процесс установки. Процесс настройки, которому мы будем следовать, предполагает наличие трех узлов PXC.

Таблица 13-1. IP-адрес хоста узла трех узлов PXC
Узел Хост IP
Узел 1 pxc1 172.16.2.56
Узел 2 pxc2 172.16.2.198
Узел 3 pxc3 172.16.3.177

Подключаемся к одному из узлов и устанавливаем репозиторий:

# yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm -y

Установив репозиторий, установите бинарники:

# yum install Percona-XtraDB-Cluster-57 -y

Затем вы можете применить типичные конфигурации, которые вы использовали бы для обычного процесса MySQL (см. главу 11). С внесенными изменениями запустите процесс mysqld и получите временный пароль:

# 'systemctl start mysqld'
# 'grep temporary password'/var/log/mysqld.log'

Используйте предыдущий пароль для входа в систему как root и измените пароль:

$ mysql -u root -p
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'P@ssw0rd!';

Остановите процесс mysqld:

# systemctl stop mysql

Повторите предыдущие шаги для двух других узлов.

Имея двоичные файлы и базовую конфигурацию, мы можем начать работу над параметрами кластера.

Нам нужно добавить следующие переменные конфигурации в /etc/my.cnf на первом узле:

[mysqld]
wsrep_provider=/usr/lib64/galera3/libgalera_smm.so
wsrep_cluster_name=pxc-cluster
wsrep_cluster_address=gcomm://172.16.2.56,172.16.2.198,172.16.3.177

wsrep_node_name=pxc1
wsrep_node_address=172.16.2.56

wsrep_sst_method=xtrabackup-v2
wsrep_sst_auth=sstuser:P@ssw0rd!

pxc_strict_mode=ENFORCING

binlog_format=ROW
default_storage_engine=InnoDB
innodb_autoinc_lock_mode=2

Используйте ту же конфигурацию для второго и третьего узлов, за исключением переменных wsrep_node_name и wsrep_node_address.

Для второго узла используйте:

wsrep_node_name=pxc2
wsrep_node_address=172.16.2.198

Для третьего узла используйте:

wsrep_node_name=pxc3
wsrep_node_address=172.16.3.177

Как и обычный MySQL, Percona XtraDB Cluster имеет множество настраиваемых параметров, и те, которые мы показали, являются минимальными настройками для запуска кластера. Мы настраиваем имя и IP-адрес узла, адрес кластера и пользователя, который будет использоваться для внутренней связи между узлами. Более подробную информацию вы можете найти в документации.

На данный момент у нас настроены все узлы, но процесс mysqld не запущен ни на одном узле. PXC требует, чтобы вы запустили один узел в кластере в качестве контрольной точки для других, прежде чем другие узлы смогут присоединиться и сформировать кластер. Этот узел должен быть запущен в режиме начальной загрузки (bootstrap). Начальная загрузка — это начальный шаг для введения одного сервера в качестве основного компонента, чтобы другие могли использовать его в качестве точки отсчета для синхронизации данных.

Запустите первый узел с помощью следующей команды:

# systemctl start mysql@bootstrap

Прежде чем добавлять другие узлы в новый кластер, подключитесь к узлу, который вы только что запустили, создайте пользователя для передачи моментальных снимков состояния (SST) и предоставьте ему необходимые привилегии. Учетные данные должны соответствовать тем, которые указаны в конфигурации wsrep_sst_auth, которую вы установили ранее:

mysql> CREATE USER 'sstuser'@'localhost' IDENTIFIED BY 'P@ssw0rd!';
mysql> GRANT RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON .
    -> TO 'sstuser'@'localhost';
mysql> FLUSH PRIVILEGES;

После этого вы можете регулярно инициализировать другие узлы:

# systemctl start mysql

Чтобы убедиться, что кластер работает нормально, мы можем выполнить несколько проверок, таких как создание базы данных на первом узле, создание таблицы и вставка некоторых данных на втором узле и получение некоторых строк из этой таблицы на третьем узле. Во-первых, давайте создадим базу данных на первом узле (pxc1):

mysq> CREATE DATABASE learning_mysql;
Query ok, 1 row affected (0.01 sec)

На втором узле (pxc2) создайте таблицу и вставьте некоторые данные:

mysql> USE learning_mysql;
Database changed
mysql> CREATE TABLE example (node_id INT PRIMARY KEY, node_name VARCHAR(30));
Query ok, 0 rows affected (0.05 sec)
mysql> INSERT INTO learning_mysql.example VALUES (1, "Vinicius1");
Query OK, 1 row affected (0.02 sec)

Затем извлеките несколько строк из этой таблицы на третьем узле:

mysql> SELECT * FROM learning_mysql.example;
+---------+-----------+
| node_id | node_name |
+---------+-----------+
|       1 | Vinicius1 |
+---------+-----------+
1 row in set (0.00 sec)

Другое, более элегантное решение — проверка глобальных переменных состояния wsrep_%, в частности wsrep_cluster_size и wsrep_cluster_status:

mysql> SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 3     |
+--------------------+-------+
1 row in set (0.00 sec)
mysql> SHOW GLOBAL STATUS LIKE 'wsrep_cluster_status';
+----------------------+---------+
| Variable_name        | Value   |
+----------------------+---------+
| wsrep_cluster_status | Primary |
+----------------------+---------+
1 row in set (0.00 sec)

Вывод этих команд говорит нам о том, что кластер имеет три узла и находится в первичном состоянии (может принимать операции чтения и записи).

Вы можете рассмотреть возможность использования ProxySQL в дополнение к кластеру Galera, чтобы обеспечить прозрачность приложения (см. главу 15).

Целью этой главы было просто познакомить вас с различными топологиями, чтобы вы знали, что они существуют. Обслуживание и оптимизация кластера — сложная тема, которая выходят за рамки этой книги.

Глава 14
MySQL в облаке

«Не беспокойтесь, это в облаке» — фраза, которую мы часто слышим. Это напоминает нам историю о женщине, которая переживала, что после того, как ее iPhone утонул в унитазе, она потеряла все свои семейные фотографии и фотографии из путешествий. К ее удивлению, когда она купила новый телефон, устройство «восстановило» все фотографии. Она использовала решение для резервного копирования iCloud от Apple для резервного копирования содержимого своего устройства в облако. (Другим сюрпризом, возможно, был счет за подписку на услуги, который она не осознавала, что платила.)

Как компьютерные инженеры, мы не можем позволить себе роскошь рисковать в отношении того, будут ли восстановлены наши данные или нет. Облачное хранилище — это масштабируемое и надежное решение. В этой главе мы рассмотрим несколько вариантов использования MySQL в облаке, которые есть у компаний. Они варьируются от вариантов «база данных как услуга» (DBaaS), которые легко масштабируются и обеспечивают автоматическое резервное копирование и функции высокой доступности, до более традиционных вариантов, таких как инстансы EC2, которые обеспечивают более детальный контроль. Как правило, начинающие компании, основной бизнес которых не связан с технологиями, предпочитают использовать варианты DBaaS, поскольку их проще реализовать и с ними проще работать. С другой стороны, компании, которым требуется более строгий контроль над своими данными, могут предпочесть использовать экземпляр EC2 или собственную облачную инфраструктуру.

База данных как услуга (DBaaS)

DBaaS — это вариант аутсорсинга, когда компании платят поставщику облачных услуг за запуск и обслуживание облачной базы данных для них. Оплата обычно производится за использование, и владельцы данных могут получать доступ к данным своих приложений по своему усмотрению. DBaaS предоставляет те же функции, что и стандартная реляционная или нереляционная база данных. Часто это хорошее решение для компаний, пытающихся избежать настройки, обслуживания и обновления своих баз данных и серверов (хотя это не всегда так). DBaaS существует в сфере программного обеспечения как услуги (SaaS), такой как платформа как услуга (PaaS) и инфраструктура как услуга (IaaS), где такие продукты, как базы данных, становятся услугами.

Amazon RDS для MySQL/MariaDB

Наиболее популярным DBaaS является Amazon RDS для MySQL. Начало работы с RDS похоже на настройку нового автомобиля на веб-сайте. Вы выбираете основной продукт и добавляете нужные параметры, пока он не будет выглядеть так, как вам нравится, а затем запускаете. На рисунке 14-1 показаны доступные продукты. В этом случае мы перейдем на MySQL (версия MariaDB имеет аналогичные настройки для развертывания).

Рисунок 14-1. Выбор продукта

Мы также можем выбрать версию — здесь мы выбрали 8.0.21. Далее нам нужно установить главного пользователя (похожего на root) и его пароль. Убедитесь, что вы выбрали надежный пароль, особенно если вы откроете свою базу данных для всего мира. На рисунке 14-2 показано, как определить имя пользователя и пароль для главного пользователя.

Рисунок 14-2. Настройка имени пользователя и пароля главного пользователя

Далее идет размер экземпляра, который напрямую влияет на окончательную цену. Мы выберем конфигурацию верхнего уровня, чтобы дать вам представление о том, насколько дорогостоящим может быть использование DBaaS. На рисунке 14.3 показаны доступные классы экземпляров; есть несколько вариантов с разной стоимостью.

Рисунок 14-3. Выбор класса экземпляра

Еще один параметр, который может напрямую повлиять на выставление счетов, — это параметры хранилища. Естественно, более высокая производительность (больше операций ввода-вывода в секунду) и больший объем хранилища приводят к более высокой стоимости. Рисунок 14-4 иллюстрирует выбор. Вы также можете выбрать, следует ли включить автомасштабирование.

Рисунок 14-4. Настройка размера хранилища и его производительности IOPS

Следующий вариант — важный выбор: хотите ли вы использовать развертывание в нескольких зонах доступности или нет? Вариант с несколькими зонами доступности — это высокая доступность. При предоставлении инстанса БД в нескольких зонах доступности Amazon RDS автоматически создает основной инстанс БД и синхронно реплицирует данные в резервный инстанс в другой зоне доступности (AZ). Зоны доступности физически разделены и имеют независимую инфраструктуру, что повышает общую доступность.

Если вы не хотите использовать развертывание в нескольких зонах доступности, RDS установит один экземпляр. В случае сбоя он запустит новый и перемонтирует свой том данных. Этот процесс занимает некоторое время, в течение которого ваша база данных будет недоступна. Даже крупные облачные провайдеры не являются пуленепробиваемыми, и могут случиться катастрофы, поэтому всегда рекомендуется иметь резервный сервер. На рисунок 14-5 показано, как настроить реплику.

Рисунок 14-5. Настройка резервной реплики

Следующая часть — настройка общей сетевой конфигурации. Мы рекомендуем настроить RDS для использования частной сети, к которой могут получить доступ только серверы приложений и IP-адреса разработчиков. На рисунке 14-6 показаны параметры сети.

Рисунок 14-6. Настройка сетевых параметров

Наконец, увы, приходят сметные расходы. На рисунке 14-7 показано, сколько вы будете платить в месяц за настроенные варианты.

Рисунок 14-7. Счет может достигать звезд при определенных конфигурациях!

Google Cloud SQL для MySQL

Google Cloud SQL предлагает управляемые службы баз данных, сопоставимые с Amazon RDS (и Azure), но с небольшими отличиями. Варианты Google Cloud для MySQL более просты, потому что вариантов меньше. Например, вы не можете выбрать между MySQL и MariaDB или выбрать минорную версию MySQL (только основную версию). Как показано на рисунке 14-8, вы можете начать работу либо с создания нового экземпляра, либо с переноса существующей базы данных в Google Cloud.

Рисунок 14-8. Google Cloud SQL

При создании нового экземпляра вам необходимо указать несколько параметров. Первый шаг – выбор товара. На рисунке 14-9 показаны параметры, доступные для MySQL.

Рисунок 14-9. Выбор продукта

После выбора MySQL вам нужно будет указать имя экземпляра, пароль root, версию базы данных и местоположение. Рисунок 14-10 показывает, как настроить эти параметры.

Рисунок 14-10. Настройка базовой конфигурации

Далее следуют настройки, которые могут повлиять на производительность, и, конечно же, стоимость — здесь важно найти правильный баланс. На рисунке 14-11 показаны доступные варианты хранения, памяти и ЦП.

Рисунок 14-11. Настройка типа машины и хранилища

Теперь экземпляр готов к запуску в Google Cloud.

Azure SQL

Последним из трех ведущих поставщиков облачных услуг является Azure SQL. На рисунке 14.12 показаны продукты баз данных, доступные в Azure. Вам нужно выбрать «Azure Database for MySQL servers».

Рисунок 14-12. Выбор MySQL в Azure

Azure предлагает два варианта: использовать простой сервер или более надежное решение с высокой доступностью в настройках. Рисунок 14-13 показывает разницу между двумя вариантами.

Рисунок 14-13. Выбор одного сервера или гибкого сервера

За этими выборами следуют аналогичные конфигурации в отношении производительности и стоимости услуг. На рисунке 14-14 показаны параметры управляемых служб MySQL.

Рисунок 14-14. Настройка нашего экземпляра управляемой службы MySQL

Amazon Aurora

Amazon Aurora — это совместимое с MySQL и PostgreSQL решение для реляционной базы данных, предоставляемое Amazon по коммерческой лицензии. Он предлагает функции, аналогичные MySQL, а также несколько дополнительных функций, разработанных Amazon.

Две из этих особенностей заслуживают упоминания. Во-первых, это Aurora Parallel Query (PQ), функция, которая распараллеливает некоторые операции ввода-вывода и вычисления, связанные с обработкой запросов с интенсивным использованием данных.

Aurora PQ работает, выполняя полное сканирование таблицы (уровень хранилища выполняет параллельное чтение). Когда мы используем параллельный запрос, запрос не использует буферный пул InnoDB. Вместо этого он переносит обработку запросов на уровень хранилища и распараллеливает ее.

Преимущество заключается в том, что перемещение обработки ближе к данным снижает сетевой трафик и задержку. Однако эта функция не является панацеей и работает не во всех случаях — она лучше всего подходит для аналитических запросов, которым необходимо обрабатывать большие объемы данных.

Функция PQ доступна не для всех экземпляров AWS. Для экземпляров, поддерживающих эту функцию, их класс экземпляра определяет количество параллельных запросов, которые могут быть активны в данный момент времени. Следующие экземпляры поддерживают функцию PQ:

Другой примечательной особенностью является глобальная база данных Amazon Aurora, предназначенная для приложений с глобальным охватом. Это позволяет одной базе данных Aurora охватывать несколько регионов AWS с быстрой репликацией, обеспечивающей глобальное чтение с малой задержкой и аварийное восстановление после сбоев в масштабах региона. Глобальная база данных Aurora использует репликацию на основе хранилища с использованием выделенной инфраструктуры Amazon в своих центрах обработки данных по всему миру.

Облачные экземпляры MySQL

Облачный экземпляр (cloud instance) — это не что иное, как виртуальный сервер. У разных поставщиков облачных услуг они называются по-разному: экземпляры Amazon Elastic Compute Cloud (EC2), экземпляры Google Compute Engine и виртуальные машины Azure.

Все они предлагают различные типы экземпляров в соответствии с бизнес-потребностями пользователя, начиная от неглубоких базовых конфигураций и заканчивая удивительно расширенными возможностями. Например, тип машины Compute Engine m2-megamem-416 — это монстр с 416 ЦП и 5888 ГБ ОЗУ.

Процесс установки MySQL для этих экземпляров является стандартным, описанным в главе 1. В этом случае наиболее значительным преимуществом использования облачных экземпляров по сравнению с решениями DBaaS является свобода настройки MySQL и операционной системы в соответствии с вашими потребностями без ограничений, присущих управляемым базам данных.

MySQL в Kubernetes

Самый последний вариант, доступный для развертывания экземпляров MySQL, — это Kubernetes. Kubernetes и платформа OpenShift добавили способ управления контейнерными системами, включая кластеры баз данных. Управление осуществляется контроллерами, объявленными в конфигурационных файлах. Эти контроллеры обеспечивают автоматизацию создания объектов, таких как контейнер или группа контейнеров, называемых pod, для прослушивания определенного события и выполнения задачи.

Эта автоматизация усложняет архитектуру на основе контейнеров и приложений с отслеживанием состояния, таких как базы данных. Оператор Kubernetes — это особый тип контроллера, введенный для упрощения сложных развертываний. Оператор расширяет API Kubernetes с помощью настраиваемых ресурсов.

О том, как работает Kubernetes, написано много хороших книг. Чтобы сделать этот раздел максимально кратким, мы обсудим важные компоненты, относящиеся к оператору Percona Kubernetes. Для быстрого ознакомления с Kubernetes вы можете ознакомиться с документацией от Linux Foundation. На рисунке 14-15 показаны компоненты Percona XtraDB Cluster в Kubernetes.

Рисунок 14-15. Компоненты кластера Percona XtraDB в Kubernetes

В следующем разделе описывается, как развернуть оператор Percona Kubernetes для кластера Percona XtraDB, который считается готовым к работе. Есть и другие операторы. Например:

Развертывание Percona XtraDB Cluster в Kubernetes

В этом разделе описаны этапы развертывания кластера Kubernetes в Google Cloud с использованием Google Cloud SDK и Percona Kubernetes Operator для PXC:

  1. Установка Google Cloud SDK.

    SDK предоставляет инструменты и библиотеки для взаимодействия с продуктами и сервисами Google Cloud. Загрузите бинарный файл, подходящий для вашей платформы, и установите его. Вот пример для macOS:

    $ wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/ \
         google-cloud-sdk-341.0.0-darwin-x86_64.tar.gz
    $ tar -xvf google-cloud-sdk-341.0.0-darwin-x86_64.tar.gz
    $ cd google-cloud-sdk/
    $ ./install.sh
  2. Установите kubectl через gcloud.

    Установив gcloud, установите компонент kubectl с помощью следующей команды:

    $ gcloud components install kubectl
  3. Создайте кластер Kubernetes.

    Для создания кластера Kubernetes сначала необходимо пройти аутентификацию в сервисе Google Cloud:

    $ gcloud auth login

    После аутентификации создайте кластер. Команда принимает множество параметров, но в данном случае мы рассмотрим основы создания кластера Kubernetes:

    $ gcloud container clusters create --machine-type n1-standard-4 \
        --num-nodes 3 --zone us-central1-b --project support-211414 \
        --cluster-version latest vinnie-k8

    Используемые здесь параметры — это лишь малая часть всего доступного — вы можете увидеть все параметры, запустив gcloud container clusters --help. Для этого случая мы только что запросили кластер с тремя узлами экземпляров типа n1-standard-4.

    Этот процесс может занять некоторое время, особенно если узлов много. Вывод будет выглядеть следующим образом:

    Creating cluster vinnie-k8 in us-central1-b... Cluster is being
    health-checked (master is healthy)...done.
    Created [https://container.googleapis.com/v1/projects/support-211414/
    zones/us-central1-b/clusters/vinnie-k8].
    To inspect the contents of your cluster, go to:
    https://console.cloud.google.com/kubernetes/workload_/gcloud/
    us-central1-b/vinnie-k8?project=support-211414
    kubeconfig entry generated for vinnie-k8.
    +-----------+---------------+------------------+---------------+...
    | NAME      | LOCATION      | MASTER_VERSION   | MASTER_IP     |...
    +-----------+---------------+------------------+---------------+...
    | vinnie-k8 | us-central1-b | 1.19.10-gke.1000 | 34.134.67.128 |...
    +-----------+---------------+------------------+---------------+...
    ...+---------------------------------+-----------+---------+
    ...| MACHINE_TYPE NODE_VERSION       | NUM_NODES | STATUS  |
    ...+---------------------------------+-----------+---------+
    ...| n1-standard-4 1.19.10-gke.1000  | 3         | RUNNING |
    ...+---------------------------------+-----------+---------+

    И мы можем проверить модули нашего кластера Kubernetes в Google Cloud:

    $ kubectl get nodes
    NAME                                       STATUS   ROLES    AGE
    VERSION
    gke-vinnie-k8-default-pool-376c2051-5xgz   Ready    <none>   62s
    v1.19.10-gke.1000
    gke-vinnie-k8-default-pool-376c2051-w2tk   Ready    <none>   61s
    v1.19.10-gke.1000
    gke-vinnie-k8-default-pool-376c2051-wxd7   Ready    <none>   62s
    v1.19.10-gke.1000

    Также можно использовать интерфейс Google Cloud для развертывания кластера, как показано на рисунке 14-16.

    Рисунок 14-16. В главном меню выберите Kubernetes Engine, затем Clusters.

    Чтобы создать новый кластер, выберите опцию CREATE, показанную в верхней части рисунка 14-17.

    Рисунок 14-17. Создайте кластер Kubernetes, нажав CREATE

Последним шагом является установка оператора PXC. В документации по развертыванию оператора есть очень подробные инструкции. Мы будем следовать рекомендуемым здесь шагам.

Сначала настройте Cloud Identity and Access Management (Cloud IAM) для управления доступом к кластеру. Следующая команда даст вам возможность создавать роли и привязки ролей:

$ kubectl create clusterrolebinding cluster-admin-binding --clusterrole \
cluster-admin --user $(gcloud config get-value core/account)

Оператор return подтверждает создание:#############???

clusterrolebinding.rbac.authorization.k8s.io/cluster-admin-binding created

Затем создайте пространство имен и установите контекст для пространства имен:

$ kubectl create namespace learning-mysql
$ kubectl config set-context $(kubectl config current-context) \
--namespace=learning-mysql

Теперь клонируйте репозиторий и перейдите в каталог:

$ git clone -b v1.8.0 \
https://github.com/percona/percona-xtradb-cluster-operator
$ cd percona-xtradb-cluster-operator

Разверните оператор с помощью следующей команды:

$ kubectl apply -f deploy/bundle.yaml

Это должно вернуть следующее подтверждение:

customresourcedefinition.apiextensions.k8s.io/perconaxtradbclusters.pxc.percona.com created
customresourcedefinition.apiextensions.k8s.io/perconaxtradbclusterbackups.pxc.percona.com created
customresourcedefinition.apiextensions.k8s.io/perconaxtradbclusterrestores.pxc.percona.com created
customresourcedefinition.apiextensions.k8s.io/perconaxtradbbackups.pxc.percona.com created
role.rbac.authorization.k8s.io/percona-xtradb-cluster-operator created
serviceaccount/percona-xtradb-cluster-operator created
rolebinding.rbac.authorization.k8s.io/service-account-percona-xtradb-cluster-operator created
deployment.apps/percona-xtradb-cluster-operator created

Оператор запущен, и вы можете убедиться в этом, выполнив:

$ kubectl get pods

Теперь создайте кластер Percona XtraDB:

$ kubectl apply -f deploy/cr.yaml

Этот шаг может занять некоторое время. После этого вы увидите все запущенные поды:

$ kubectl get pods
NAME                               READY  STATUS    RESTARTS   AGE
cluster1-haproxy-0                 2/2    Running   0          4m54s
cluster1-haproxy-1                 2/2    Running   0          3m15s
cluster1-haproxy-2                 2/2    Running   0          2m52s
cluster1-pxc-0                     3/3    Running   0          4m54s
cluster1-pxc-1                     3/3    Running   0          3m16s
cluster1-pxc-2                     3/3    Running   0          105s
percona-xtradb-cluster-operator-   1/1    Running   0          7m18s
77bfd8cdc5-d7zll

На предыдущих этапах оператор сгенерировал несколько секретных данных, включая пароль для пользователя root, который понадобится вам для доступа к кластеру. Чтобы получить сгенерированные секретные данные, выполните следующую команду:

$ kubectl get secret my-cluster-secrets -o yaml

Вы увидите такой вывод:

apiVersion: v1
data:
  clustercheck: UFZjdjk0SU4xWGtBSTR2VlVJ
  monitor: ZWZja01mOWhBTXZ4bTB0bUZ4eQ==
  operator: Vm10R0IxbHA4cVVZTkxqVVI4Mg==
  proxyadmin: VXVFbkx1S3RmUTEzVlNOd1c=
  root: eU53aWlKT3ZXaXJaeG16OXJK
  xtrabackup: V3VNNWRnWUdIblVWaU1OWGY=
...
secrets/my-cluster-secrets
  uid: 9d78c4a8-1926-4b7a-84a0-43087a601066
type: Opaque

Фактический пароль имеет кодировку base64, поэтому вам нужно будет выполнить следующую команду, чтобы получить пароль root:

$ echo 'eU53aWlKT3ZXaXJaeG16OXJK' | base64 --decode
yNwiiJOvWirZxmz9rJ

Теперь, когда у вас есть пароль, для проверки подключения к кластеру вы можете создать клиентский под (pod):

$ kubectl run -i --rm --tty percona-client --image=percona:8.0 \
--restart=Never -- bash -il

Затем подключитесь к MySQL:

$ mysql -h cluster1-haproxy -uroot -pyNwiiJOvWirZxmz9rJ

Обратите внимание, что оператор поставляется с HAProxy, который является балансировщиком нагрузки (мы обсудим балансировку нагрузки в следующей главе).

Глава 15
Балансировка нагрузки MySQL

Существуют различные способы подключения к MySQL. Например, для выполнения теста записи создается соединение, выполняется оператор, а затем соединение закрывается. Чтобы избежать затрат на открытие соединения каждый раз, когда оно необходимо, была разработана концепция пула соединений (connection pool). Пул соединений — это метод создания и управления пулом соединений, готовых для использования любым потоком приложения.

Распространяя концепцию высокой доступности, обсуждавшуюся в главе 13, на соединения с целью повышения отказоустойчивости производственной системы, можно использовать балансировщики нагрузки (load balancers) для подключения к кластеру базы данных. Балансировка нагрузки и высокая доступность MySQL позволяют поддерживать бесперебойную работу приложения (или с незначительными простоями). По сути, если исходный сервер или один из узлов кластера базы данных выходит из строя, клиенту просто нужно подключиться к другому узлу базы данных, и он может продолжать обслуживать запросы.

Балансировщики нагрузки были созданы для обеспечения прозрачности для клиентов при подключении к инфраструктуре MySQL. Таким образом, приложению не нужно знать топологию MySQL; используете ли вы классическую репликацию, групповую репликацию или кластер Galera, не имеет значения. Балансировщик нагрузки предоставит онлайн-узел, где можно будет читать и писать запросы. Наличие надежной архитектуры MySQL и надлежащего балансировщика нагрузки может помочь администраторам баз данных избежать бессонных ночей.

Балансировка нагрузки с помощью драйверов приложений

Чтобы подключить приложение к MySQL, вам нужен драйвер. Драйвер — это адаптер, используемый для подключения приложения к системе другого типа. Это похоже на подключение видеокарты к компьютеру; вам может потребоваться загрузить и установить драйвер, чтобы он работал с вашим приложением.

Современные драйверы MySQL из широко используемых языков программирования поддерживают пул соединений, балансировку нагрузки и отработку отказа. Примеры включают драйвер JDBC для MySQL (MySQL Connector/J) и драйвер PDO_MYSQL, который реализует интерфейс PHP Data Objects (PDO) для обеспечения доступа из PHP к базам данных MySQL.

Упомянутые нами драйверы базы данных созданы для обеспечения прозрачности для клиентов при подключении к автономному серверу MySQL или настройкам репликации MySQL. Мы не будем показывать вам, как использовать их в коде, потому что это выходит за рамки этой книги; однако вы должны знать, что добавление библиотеки драйверов облегчает разработку кода, поскольку драйвер абстрагирует значительный объем работы от разработчика.

Но для других топологий, таких как установка кластера, такая как Galera Cluster для MySQL или MariaDB, драйверы JDBC и PHP не знают о внутренней информации о состоянии Galera. Например, узел-донор Galera может находиться в режиме только для чтения, пока он помогает другому узлу повторно синхронизироваться (если используется метод SST mysqldump или rsync), или он может находиться в неосновном состоянии, если происходит разделение. Другое решение — использовать балансировщик нагрузки между клиентами и кластером базы данных.

Балансировщик нагрузки ProxySQL

ProxySQL — это прокси SQL. ProxySQL реализует протокол MySQL, и благодаря этому он может делать то, что не могут другие прокси. Вот некоторые из его преимуществ:

ProxySQL работает как демон, за которым следит процесс мониторинга. Процесс отслеживает работу демона и перезапускает его в случае сбоя, чтобы свести к минимуму время простоя. Демон принимает входящий трафик от клиентов MySQL и перенаправляет его на внутренние серверы MySQL.

Прокси предназначен для непрерывной работы без необходимости перезапуска. Большинство конфигураций можно выполнить во время выполнения с помощью запросов, аналогичных операторам SQL в административном интерфейсе ProxySQL. К ним относятся параметры среды выполнения, группировка серверов и параметры, связанные с трафиком.

Хотя обычно ProxySQL устанавливается на автономном узле между приложением и базой данных, это может повлиять на производительность запросов из-за дополнительной задержки из-за сетевых переходов. На рисунке 15-1 показан ProxySQL в качестве промежуточного уровня.

Рисунок 15-1. ProxySQL между приложением и MySQL

Чтобы уменьшить влияние на производительность (и избежать дополнительных переходов по сети), другим вариантом архитектуры является установка ProxySQL на серверах приложений. Затем приложение подключается к ProxySQL (действующему как сервер MySQL) на локальном хосте, используя сокет домена Unix, избегая дополнительной задержки. Он использует свои правила маршрутизации, чтобы связаться с реальными серверами MySQL и использовать их пул подключений. Приложение понятия не имеет, что происходит помимо его подключения к ProxySQL. Рисунок 15-2 показывает ProxySQL на том же сервере, что и приложение.

Рисунок 15-2. ProxySQL на том же сервере, что и приложение

Установка и настройка ProxySQL

Давайте посмотрим, как развернуть ProxySQL для конфигурации источник/реплика.

Разработчики инструмента предоставляют официальные пакеты для различных дистрибутивов Linux для всех релизов ProxySQL на своей странице релизов GitHub, поэтому мы загрузим оттуда последнюю версию пакета и установим ее.

Перед установкой в этом процессе мы будем использовать следующие экземпляры:

+---------------------------------------+----------------------+
| vinicius-grippa-default(mysql)        | 10.124.33.5 (eth0)   |
+---------------------------------------+----------------------+
| vinicius-grippa-node1(mysql)          | 10.124.33.169 (eth0) |
+---------------------------------------+----------------------+
| vinicius-grippa-node2(mysql)          | 10.124.33.130 (eth0) |
+---------------------------------------+----------------------+
| vinicius-grippa-node3(proxysql)       | 10.124.33.170 (eth0) |
+---------------------------------------+----------------------+

Для начала найдите подходящий дистрибутив для вашей операционной системы. В этом примере мы затем установим для CentOS 7. Сначала мы станем root'ом, установим клиент MySQL для подключения к ProxySQL и установим сам ProxySQL. Мы получаем URL-адрес со страницы загрузок и отправляем его в yum:

$ sudo su - root
# yum -y install https://repo.percona.com/yum/percona-release-latest.noarch.rpm
# yum -y install Percona-Server-client-57
# yum install -y https://github.com/sysown/proxysql/releases/download/v2.0.15/ \
     proxysql-2.0.15-1-centos7.x86_64.rpm

У нас выполнены все требования для запуска ProxySQL, но сервис не запускается автоматически после установки, поэтому запускаем его вручную:

# sudo systemctl start proxysql

Теперь ProxySQL должен работать с конфигурацией по умолчанию. Мы можем проверить это, выполнив эту команду:

# systemctl status proxysql

Вывод процесса ProxySQL в активном состоянии должен быть примерно таким:

 proxysql.service - High Performance Advanced Proxy for MySQL
   Loaded: loaded (/etc/systemd/system/proxysql.service; enabled; vendor...
   Active: active (running) since Sun 2021-05-23 18:50:28 UTC; 15s ago
  Process: 1422 ExecStart=/usr/bin/proxysql --idle-threads -c /etc/proxysql...
 Main PID: 1425 (proxysql)
   CGroup: /system.slice/proxysql.service
           ├─1425 /usr/bin/proxysql --idle-threads -c /etc/proxysql.cnf
           └─1426 /usr/bin/proxysql --idle-threads -c /etc/proxysql.cnf
May 23 18:50:27 vinicius-grippa-node3 systemd[1]: Starting High Performance...
May 23 18:50:27 vinicius-grippa-node3 proxysql[1422]: 2021-05-23 18:50:27...
May 23 18:50:27 vinicius-grippa-node3 proxysql[1422]: 2021-05-23 18:50:27...
May 23 18:50:27 vinicius-grippa-node3 proxysql[1422]: 2021-05-23 18:50:27...
May 23 18:50:28 vinicius-grippa-node3 systemd[1]: Started High Performance...

ProxySQL отделяет интерфейс приложения от интерфейса администратора. Это означает, что ProxySQL будет прослушивать два сетевых порта: интерфейс администратора будет прослушивать 6032, а приложение будет прослушивать 6033 (чтобы было легче запомнить, это обратный порт MySQL по умолчанию, 3306).

Затем ProxySQL необходимо связаться с узлами MySQL, чтобы проверить их состояние. Для этого ProxySQL необходимо подключиться к каждому серверу с выделенным пользователем.

Во-первых, мы собираемся создать пользователя на исходном сервере. Подключитесь к исходному экземпляру MySQL и выполните следующие команды:

mysql> CREATE USER 'proxysql'@'%' IDENTIFIED by '$3Kr$t';
mysql> GRANT USAGE ON *.* TO 'proxysql'@'%';

Далее мы настроим параметры ProxySQL для распознавания пользователя. Сначала подключаемся к ProxySQL:

# mysql -uadmin -padmin -h 127.0.0.1 -P 6032

А затем задаем параметры:

proxysql> UPDATE global_variables SET variable_value='proxysql'
       -> WHERE variable_name='mysql-monitor_username';
proxysql> UPDATE global_variables SET variable_value='$3Kr$t'
       -> WHERE variable_name='mysql-monitor_password';
proxysql> LOAD MYSQL VARIABLES TO RUNTIME;
proxysql> SAVE MYSQL VARIABLES TO DISK;

Теперь, когда мы установили пользователя в базе данных и ProxySQL, пришло время сообщить ProxySQL, какие серверы MySQL присутствуют в топологии:

proxysql> INSERT INTO mysql_servers(hostgroup_id, hostname, port)
       -> VALUES (10,'10.124.33.5',3306);
proxysql> INSERT INTO mysql_servers(hostgroup_id, hostname, port)
       -> VALUES (11,'10.124.33.169',3306);
proxysql> INSERT INTO mysql_servers(hostgroup_id, hostname, port)
       -> VALUES (11,'10.124.33.130',3306);
proxysql> LOAD MYSQL SERVERS TO RUNTIME;
proxysql> SAVE MYSQL SERVERS TO DISK;

Следующий шаг — определить, кто входит в группу записи и чтения. Серверы, присутствующие в группе записи, смогут получать операции DML, а запросы SELECT будут использовать серверы в группе чтения. В этом примере группа хостов 10 будет записывающим устройством, а группа хостов 11 будет читающим устройством:

proxysql> INSERT INTO mysql_replication_hostgroups
       -> (writer_hostgroup, reader_hostgroup) VALUES (10, 11);
proxysql> LOAD MYSQL SERVERS TO RUNTIME;
proxysql> SAVE MYSQL SERVERS TO DISK;

Далее, у ProxySQL должны быть пользователи, которые могут получить доступ к внутренним узлам для управления соединениями. Давайте создадим пользователя на внутреннем исходном сервере:

mysql> CREATE USER 'app'@'%' IDENTIFIED by '$3Kr$t';
mysql> GRANT ALL PRIVILEGES ON *.* TO 'app'@'%';

А теперь настроим ProxySQL с пользователем:

proxysql> INSERT INTO mysql_users (username,password,default_hostgroup)
       -> VALUES ('app','$3Kr$t',10);
proxysql> LOAD MYSQL USERS TO RUNTIME;
proxysql> SAVE MYSQL USERS TO DISK;

Следующий шаг самый захватывающий, потому что именно здесь мы определяем правила. Правила сообщат ProxySQL, куда отправлять запросы на запись и чтение, балансируя нагрузку на серверы:

proxysql> INSERT INTO mysql_query_rules
       -> (rule_id,username,destination_hostgroup,active,match_digest,apply)
       -> VALUES(1,'app',10,1,'^SELECT.*FOR UPDATE',1);
proxysql> INSERT INTO mysql_query_rules
       -> (rule_id,username,destination_hostgroup,active,match_digest,apply)
       -> VALUES(2,'app',11,1,'^SELECT ',1);
proxysql> LOAD MYSQL QUERY RULES TO RUNTIME;
proxysql> SAVE MYSQL QUERY RULES TO DISK;

ProxySQL имеет поток, отвечающий за подключение к каждому серверу, указанному в таблице mysql_servers, и проверку значения переменной read_only. Предположим, что реплика отображается в группе записи, например:

proxysql> SELECT * FROM mysql_servers;
+--------------+---------------+------+-----------+...
| hostgroup_id | hostname      | port | gtid_port |...
+--------------+---------------+------+-----------+...
| 10           | 10.124.33.5   | 3306 | 0         |...
| 11           | 10.124.33.169 | 3306 | 0         |...
| 11           | 10.124.33.130 | 3306 | 0         |...
+--------------+---------------+------+-----------+...
...+--------------+---------------+------+-----------+
...| status | weight | compression | max_connections |...
...+--------+--------+-------------+-----------------+...
...| ONLINE | 1      | 0           | 1000            |...
...| ONLINE | 1      | 0           | 1000            |...
...| ONLINE | 1      | 0           | 1000            |...
...+--------+--------+-------------+-----------------+...
...+---------------------+---------+----------------+---------+
...| max_replication_lag | use_ssl | max_latency_ms | comment |
...+---------------------+---------+----------------+---------+
...| 0                   | 0       | 0              |         |
...| 0                   | 0       | 0              |         |
...| 0                   | 0       | 0              |         |
...+---------------------+---------+----------------+---------+
3 rows in set (0.00 sec)

Поскольку мы не хотим, чтобы ProxySQL записывала данные на серверы-реплики, что могло бы привести к несогласованности данных, нам нужно установить опцию read_only на серверах-репликах, чтобы эти серверы обслуживали только запросы на чтение.

mysql> SET GLOBAL read_only=1;

Теперь мы готовы использовать наше приложение. Выполнение следующей команды должно вернуть имя хоста, с которого ProxySQL подключился:

$ mysql -uapp -p'$3Kr$t' -h 127.0.0.1 -P 6033 -e "select @@hostname;"
+-----------------------+
| @@hostname            |
+-----------------------+
| vinicius-grippa-node1 |
+-----------------------+

ProxySQL имеет гораздо больше возможностей и гибкости, чем мы показали здесь; наша цель в этом разделе состояла в том, чтобы просто представить инструмент, чтобы вы знали об этом параметре при выборе архитектуры.

Балансировщик нагрузки HAProxy

HAProxy расшифровывается как High Availability Proxy (прокси высокой доступности) и представляет собой балансировщик нагрузки TCP/HTTP. Он распределяет рабочую нагрузку по набору серверов, чтобы максимизировать производительность и оптимизировать использование ресурсов.

Чтобы расширить ваши знания об архитектуре MySQL и различных топологиях, в этом разделе мы настроим Percona XtraDB Cluster (Galera Cluster) с HAProxy вместо классической топологии репликации.

Варианты архитектуры аналогичны ProxySQL. HAProxy можно разместить вместе с приложением или в промежуточном слое. На рисунке 15-3 показан пример, когда HAProxy размещается на том же сервере, что и приложение.

Рисунок 15-3. HAProxy вместе с приложением

А на рисунке 15-4 показана топология с HAProxy на промежуточном уровне.

Рисунок 15-4. HAProxy на промежуточном уровне, работающем на выделенных серверах

Опять же, это архитектуры с разными плюсами и минусами. Пока в первом у нас нет лишнего хопа (что снижает латентность), мы добавляем дополнительную нагрузку на сервер приложений. Также вам необходимо настроить HAProxy на каждом сервере приложений.

С другой стороны, наличие HAProxy на промежуточном уровне упрощает управление и повышает доступность, поскольку приложение может подключаться к любому серверу HAProxy. Однако дополнительный переход увеличивает задержку.

Установка и настройка HAProxy

Распространенные операционные системы, такие как Red Hat/CentOS и Debian/Ubuntu, предоставляют пакет HAProxy, и вы можете установить его с помощью диспетчера пакетов. Процесс установки относительно прост.

Для Debian или Ubuntu используйте следующие команды:

# apt update
# apt install haproxy

Для Red Hat или CentOS используйте:

# sudo yum update
# sudo yum install haproxy

После установки HAProxy установит путь по умолчанию для файла конфигурации как /etc/haproxy/haproxy.cfg.

Перед запуском HAProxy нам нужно его настроить. Для этой демонстрации в нашем первом сценарии HAProxy будет расположен на том же сервере, что и приложение. Вот IP-адреса нашего трехузлового кластера Galera:

172.16.3.45/Port:3306
172.16.1.72/Port:3306
172.16.0.161/Port:3306

Давайте откроем наш файл /etc/haproxy/haproxy.cfg и посмотрим на него. Есть много параметров для настройки, разделенных на три раздела:

global

Раздел в файле конфигурации для параметров всего процесса.

defaults

Раздел в файле конфигурации для параметров по умолчанию

listen

Раздел в конфигурационном файле, определяющий полный прокси, включая его внешние и внутренние части.

В Таблице 15-1 показаны основные параметры HAProxy.

Таблица 15-1. Параметры HAProxy (со ссылками на документацию по HAProxy)
Параметр Описание
balance Определяет алгоритм балансировки нагрузки, который будет использоваться в серверной части
clitimeout Устанавливает максимальное время бездействия на стороне клиента
contimeout Устанавливает максимальное время ожидания успешной попытки подключения к серверу
daemon Переводит процесс в фоновый режим (рекомендуемый режим работы)
gid Изменяет идентификатор группы процесса на <number>
log Добавляет глобальный сервер системного журнала
maxconn Устанавливает максимальное количество одновременных подключений для каждого процесса равным <number>
mode Устанавливает режим работы или протокол экземпляра
option dontlognull Отключает регистрацию нулевых подключений
optiontcplog Включает расширенную регистрацию TCP-соединений с состоянием сеанса и таймерами

Чтобы заставить HAProxy работать, мы будем использовать следующий файл конфигурации на основе наших настроек:

global
    log /dev/log   local0
    log /dev/log   local1 notice
    maxconn 4096
    #debug
    #quiet
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    user        haproxy
    group       haproxy
    daemon

    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats

#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
     log     global
     mode    http
     option  tcplog
     option  dontlognull
     retries 3
     redispatch
     maxconn 2000
     contimeout      5000
     clitimeout      50000
     srvtimeout      50000

#---------------------------------------------------------------------
# round robin balancing between the various backends
#---------------------------------------------------------------------
listen mysql-pxc-cluster 0.0.0.0:3307
     mode tcp
     bind *:3307
     timeout client  10800s
     timeout server  10800s
     balance roundrobin
     option httpchk

     server vinicius-grippa-node2 172.16.0.161:3306 check port 9200
     inter 12000 rise 3 fall 3
     server vinicius-grippa-node1 172.16.1.72:3306 check port 9200 inter 12000

     rise 3 fall 3
     server vinicius-grippa-default 172.16.3.45:3306 check port 9200
     inter 12000 rise 3 fall 3

Чтобы запустить HAProxy, мы используем команду haproxy. Мы можем передать любое количество параметров конфигурации в командной строке. Чтобы использовать файл конфигурации, используйте параметр -f. Например, мы можем передать один файл конфигурации:

# sudo haproxy -f /etc/haproxy/haproxy.cfg

или несколько файлов конфигурации:

# sudo haproxy -f /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy-2.cfg

или каталог:

# sudo haproxy -f conf-dir

В этой конфигурации HAProxy будет балансировать нагрузку между тремя узлами. В этом случае он проверяет только, прослушивает ли процесс mysqld порт 3306, но не принимает во внимание состояние узла. Таким образом, он может отправлять запросы узлу, на котором работает mysqld, даже если он находится в состоянии JOINING или DISCONNECTED.

Чтобы проверить текущий статус узла, нам нужно что-то более сложное. Эта идея была взята из группы Google Codership.

Для реализации этой настройки нам понадобятся два скрипта:

Оба сценария доступны в виде двоичных файлов и исходных кодов Percona XtraDB.

Измените файл /etc/services, добавив следующую строку для каждого узла:

mysqlchk          9200/tcp               # mysqlchk

Если файл /etc/services не существует, скорее всего, xinetd не установлен.

Чтобы установить его для CentOS/Red Hat, используйте:

# yum install -y xinetd

Для Debian/Ubuntu используйте:

# sudo apt-get install -y xinetd

Далее нам нужно создать пользователя MySQL, чтобы скрипт мог проверить работоспособность узла. В идеале из соображений безопасности этот пользователь должен иметь минимальные необходимые привилегии:

mysql> CREATE USER 'clustercheckuser'@'localhost' IDENTIFIED BY 'clustercheckpassword!';
    -> GRANT PROCESS ON *.* TO 'clustercheckuser'@'localhost';

Чтобы проверить, как наш узел работает при проверке работоспособности, мы можем запустить следующую команду и посмотреть результат:

# /usr/bin/clustercheck

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Content-Length: 40

Percona XtraDB Cluster Node is synced.

Если мы сделаем это для всех узлов, мы будем готовы проверить, работает ли наша установка HAProxy. Самый простой способ сделать это — подключиться к нему и выполнить некоторые команды MySQL. Давайте запустим команду, которая извлекает имя хоста, с которого мы подключены:

# mysql -uroot -psecret -h 127.0.0.1 -P 3307 -e "select @@hostname"
+-----------------------+
| @@hostname            |
+-----------------------+
| vinicius-grippa-node1 |
+-----------------------+

Запуск этого во второй раз дает нам:

# mysql -uroot -psecret -h 127.0.0.1 -P 3307 -e "select @@hostname"
mysql: [Warning] Using a password on the command line interface can be insecure.
+-----------------------+
| @@hostname            |
+-----------------------+
| vinicius-grippa-node2 |
+-----------------------+

И в третий раз получаем:

# mysql -uroot -psecret -h 127.0.0.1 -P 3307 -e "select @@hostname"
mysql: [Warning] Using a password on the command line interface can be insecure.
+-------------------------+
| @@hostname              |
+-------------------------+
| vinicius-grippa-default |
+-------------------------+

Как вы можете видеть, наш HAProxy подключается в циклическом режиме. Если мы отключим один из узлов, HAProxy будет маршрутизировать только к оставшимся.

MySQL Router

MySQL Router отвечает за распределение трафика между членами кластера InnoDB. Это похожее на прокси решение для сокрытия топологии кластера от приложений, поэтому приложениям не нужно знать, какой член кластера является основным узлом, а какой — вторичным. Обратите внимание, что MySQL Router не будет работать с кластерами Galera; он был разработан только для InnoDB Cluster.

MySQL Router способен выполнять разделение чтения/записи, предоставляя различные интерфейсы. Обычная настройка состоит в том, чтобы иметь один интерфейс чтения/записи и один интерфейс только для чтения. Это поведение по умолчанию, которое также предоставляет два аналогичных интерфейса для использования X Protocol (используемого для операций CRUD и асинхронных вызовов).

Разделение чтения/записи выполняется с использованием концепции ролей: первичная для записи и вторичная для чтения. Это аналогично тому, как называются члены кластера. Кроме того, каждый интерфейс предоставляется через порт TCP, поэтому приложениям нужно знать только комбинацию IP:порт, используемую для записи, и комбинацию, используемую для чтения. Затем MySQL Router позаботится о подключениях к членам кластера в зависимости от типа трафика на сервер.

При работе в производственной среде экземпляры сервера MySQL, составляющие InnoDB Cluster, работают на нескольких хост-машинах как часть сети, а не на одной машине. Таким образом, как и в случае с ProxySQL и HAProxy, MySQL Router может быть средним уровнем в архитектуре.

На рисунке 15-5 показано, как работает производственный сценарий.

Рисунок 15-5. Производственное развертывание MySQL InnoDB Cluster

Теперь, чтобы начать наш пример, давайте посмотрим на членов MySQL, которые являются частью InnoDB Cluster:

mysql> SELECT member_host, member_port, member_state, member_role
    -> FROM performance_schema.replication_group_members;
+--------------+-------------+--------------+-------------+
| member_host  | member_port | member_state | member_role |
+--------------+-------------+--------------+-------------+
| 172.16.3.9   |        3306 | ONLINE       | SECONDARY   |
| 172.16.3.127 |        3306 | ONLINE       | SECONDARY   |
| 172.16.3.120 |        3306 | ONLINE       | PRIMARY     |
+--------------+-------------+--------------+-------------+
3 rows in set (0.00 sec)
mysql> SELECT cluster_name FROM mysql_innodb_cluster_metadata.clusters;
+--------------+
| cluster_name |
+--------------+
| cluster1     |
+--------------+
1 row in set (0.00 sec)

Теперь, когда у нас есть конфигурация узлов MySQL и имя кластера, мы можем приступить к настройке MySQL Router. В целях повышения производительности рекомендуется настроить MySQL Router в том же месте, что и приложение, предположим, что у нас есть экземпляр для каждого сервера приложений, поэтому мы разместим наш маршрутизатор на сервере приложений. Во-первых, мы собираемся определить версию MySQL Router, совместимую с нашей ОС:

# cat /etc/*release
CentOS Linux release 7.9.2009 (Core)

Теперь мы проверим страницу загрузки и установим ее с помощью yum:

# yum install -y https://dev.mysql.com/get/Downloads/MySQL-Router/mysql-
router-community-8.0.23-1.el7.x86_64.rpm
Loaded plugins: fastestmirror
mysql-router-community-8.0.23-1.el7.x86_64.rpm
Examining /var/tmp/yum-root-_ljdTQ/mysql-router-community-8.0.23-1.el7.x
86_64.rpm: mysql-router-community-8.0.23-1.el7.x86_64
Marking /var/tmp/yum-root-_ljdTQ/mysql-router-community-8.0.23-1.el7.x86
_64.rpm to be installed
Resolving Dependencies
--> Running transaction check
...
Running transaction
  Installing : mysql-router-community-8.0.23-1.el7.x86_64
  1/1
  Verifying : mysql-router-community-8.0.23-1.el7.x86_64
  1/1

Installed:
  mysql-router-community.x86_64 0:8.0.23-1.el7

Complete!

Теперь, когда MySQL Router установлен, нам нужно создать специальный каталог для его работы:

# mkdir /var/lib/mysqlrouter

Далее мы собираемся загрузить MySQL Router. Начальная загрузка настроит маршрутизатор для работы с MySQL InnoDB Cluster:

# mysqlrouter --bootstrap root@172.16.3.120:3306 \
    --directory /var/lib/mysqlrouter --conf-use-sockets \
    --account app_router --account-create always \
    --user=mysql

Please enter MySQL password for root:
# Bootstrapping MySQL Router instance at '/var/lib/mysqlrouter'...

Please enter MySQL password for app_router:
- Creating account(s)
- Verifying account (using it to run SQL queries that would be run by Router)
- Storing account in keyring
- Adjusting permissions of generated files
- Creating configuration /var/lib/mysqlrouter/mysqlrouter.conf

...

## MySQL Classic protocol

- Read/Write Connections: localhost:6446, /var/lib/mysqlrouter/mysql.sock
- Read/Only Connections: localhost:6447, /var/lib/mysqlrouter/mysqlro.sock

## MySQL X protocol

- Read/Write Connections: localhost:64460, /var/lib/mysqlrouter/mysqlx.sock
- Read/Only Connections: localhost:64470, /var/lib/mysqlrouter/mysqlxro.sock

В командной строке мы говорим маршрутизатору подключиться к пользователю root на нашем основном сервере (172.16.3.120) через порт 3306. Мы также говорим маршрутизатору создать файл сокета, чтобы мы могли подключиться с его помощью. Наконец, мы создаем нового пользователя (app_router) для использования в нашем приложении.

Давайте посмотрим на содержимое, созданное процессом начальной загрузки в нашем каталоге конфигурации (/var/lib/mysqlrouter):

# ls -l | awk '{print $9}'
data
log
mysqlrouter.conf
mysqlrouter.key
run
start.sh
stop.sh

Сгенерированный файл конфигурации MySQL Router (mysqlrouter.conf) выглядит примерно так:

# cat mysqlrouter.conf
# File automatically generated during MySQL Router bootstrap
[DEFAULT]
user=mysql
logging_folder=/var/lib/mysqlrouter/log
runtime_folder=/var/lib/mysqlrouter/run
...
[rest_routing]
require_realm=default_auth_realm

[rest_metadata_cache]
require_realm=default_auth_realm

В этом примере MySQL Router настроил четыре порта (два порта для чтения/записи с использованием обычного протокола MySQL и два порта для чтения/записи с использованием X Protocol) и четыре сокета. Порты добавляются по умолчанию, а сокеты были добавлены, потому что мы передали --conf-use-sockets. InnoDB Cluster с именем cluster1 является источником метаданных, а адресаты используют кэш метаданных InnoDB Cluster для динамической настройки информации о хосте.

Выполнив сценарий start.sh, мы можем запустить демон MySQL Router:

# ./start.sh
# PID 1684 written to '/var/lib/mysqlrouter/mysqlrouter.pid' logging facility initialized, switching logging to loggers specified in configuration

Теперь мы можем наблюдать за ходом процесса:

# ps -ef | grep -i mysqlrouter
root      1683     1  0 17:36 pts/0    00:00:00 sudo
ROUTER_PID=/var/lib/mysqlrouter/mysqlrouter.pid /usr/bin/mysqlrouter -c /var/lib/mysqlrouter/mysqlrouter.conf --user=mysql
mysql     1684  1683  0 17:36 pts/0    00:00:17 /usr/bin/mysqlrouter -c /var/lib/mysqlrouter/mysqlrouter.conf --user=mysql
root      1733  1538  0 17:41 pts/0    00:00:00 grep --color=auto -i mysqlrouter

И открываются порты:

# netstat -tulnp | grep -i mysqlrouter
tcp   0   0 0.0.0.0:64470   0.0.0.0:*   LISTEN   1684/mysqlrouter
tcp   0   0 0.0.0.0:8443    0.0.0.0:*   LISTEN   1684/mysqlrouter
tcp   0   0 0.0.0.0:64460   0.0.0.0:*   LISTEN   1684/mysqlrouter
tcp   0   0 0.0.0.0:6446    0.0.0.0:*   LISTEN   1684/mysqlrouter
tcp   0   0 0.0.0.0:6447    0.0.0.0:*   LISTEN   1684/mysqlrouter

Мы настроили MySQL Router с InnoDB Cluster, так что теперь мы можем протестировать это с соединениями для чтения и чтения/записи. Сначала подключимся к порту записи (6446):

# mysql -uroot -psecret -h 127.0.0.1 -P 6446 \
-e "create database learning_mysql;"
# mysql -uroot -psecret -h 127.0.0.1 -P 6446 \
-e "use learning_mysql; select database()"
+----------------+
| database()     |
+----------------+
| learning_mysql |
+----------------+

Как видите, в порту записи можно выполнять как чтение, так и запись.

Теперь мы проверим порт чтения (6447) с помощью оператора SELECT:

# mysql -uroot -psecret -h 127.0.0.1 -P 6447 \
    -e "use learning_mysql; select database()"
+----------------+
| database()     |
+----------------+
| learning_mysql |
+----------------+

Это работает, но давайте попробуем выполнить запись:

# mysql -uroot -psecret -h 127.0.0.1 -P 6447 \
    -e "create database learning_mysql_write;"
ERROR 1290 (HY000) at line 1: The MySQL server is running with the --super-read-only option so it cannot execute this statement

Таким образом, порт чтения принимает только чтение. Также можно увидеть, как маршрутизатор балансирует нагрузку при чтении:

# mysql -uroot -psecret -h 127.0.0.1 -P 6447 -e "select @@hostname"
+-----------------------+
| @@hostname            |
+-----------------------+
| vinicius-grippa-node1 |
+-----------------------+
# mysql -uroot -psecret -h 127.0.0.1 -P 6447 -e "select @@hostname"
insecure.
+-----------------------+
| @@hostname            |
+-----------------------+
| vinicius-grippa-node2 |
+-----------------------+

Таким образом, если в одном из узлов MySQL произойдет какое-либо время простоя, MySQL Router направит запросы на оставшиеся активные узлы.

Глава 16
Разные темы

Идея этой главы состоит в том, чтобы выйти за рамки устранения неполадок в запросе, перегруженной системе или настройке различных топологий MySQL. Мы хотим показать вам арсенал инструментов, доступных для вас, чтобы упростить повседневные задачи или исследовать сложные проблемы. Начнем с MySQL Shell.

MySQL Shell

MySQL Shell — это продвинутый клиент и редактор кода для MySQL. Он расширяет функциональность традиционного клиента MySQL, с которым работало большинство администраторов баз данных в MySQL 5.6 и 5.7. MySQL Shell поддерживает такие языки программирования, как Python, JavaScript и SQL. Он также расширяет функциональные возможности с помощью синтаксиса команд API. Например, можно настроить сценарии для администрирования кластера InnoDB. Из MySQL Shell вы также можете запускать и настраивать экземпляры песочницы MySQL.

Установка оболочки MySQL

Для поддерживаемых дистрибутивов Linux самый простой способ установить MySQL Shell — использовать репозиторий MySQL yum или apt. Давайте посмотрим, как установить его на Ubuntu и CentOS.

Установка MySQL Shell на Ubuntu 20.04 Focal Fossa

Установка MySQL Shell в Ubuntu относительно проста, поскольку она является частью обычных репозиториев.

Во-первых, нам нужно настроить репозиторий MySQL. Мы можем использовать эти команды для загрузки репозитория apt на наш сервер и его установки:

# wget https://dev.mysql.com/get/mysql-apt-config_0.8.16-1_all.deb
# dpkg -i mysql-apt-config_0.8.16-1_all.deb

После установки обновите информацию о нашем пакете:

# apt-get update

Затем выполните команду установки, чтобы установить MySQL Shell:

# apt-get install mysql-shell

Теперь мы можем запустить MySQL Shell с помощью командной строки:

# mysqlsh
MySQL Shell 8.0.23

Copyright (c) 2016, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
 MySQL  JS >

Установка MySQL Shell на CentOS 8

Чтобы установить MySQL Shell в CentOS 8, мы выполняем те же шаги, что и для Ubuntu, но сначала нам нужно убедиться, что пакет MySQL по умолчанию, присутствующий в CentOS 8, отключен:

# yum remove mysql-community-release -y
No match for argument: mysql-community-release
No packages marked for removal.
Dependencies resolved.
Nothing to do.
Complete!
# dnf erase mysql-community-release
No match for argument: mysql-community-release
No packages marked for removal.
Dependencies resolved.
Nothing to do.
Complete!

Далее мы собираемся настроить наш репозиторий yum. Нам нужно скачать правильную версию, соответствующую версии ОС со страницы загрузки:

# yum install \
    https://dev.mysql.com/get/mysql80-community-release-el8-1.noarch.rpm -y

После установки репозитория мы установим бинарный файл MySQL Shell:

# yum install mysql-shell -y

И мы можем убедиться, что установка работает, запустив ее:

# mysqlsh
MySQL Shell 8.0.23

Copyright (c) 2016, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
 MySQL  JS >

Развертывание изолированного кластера InnoDB с помощью MySQL Shell

MySQL Shell автоматизирует развертывание экземпляров песочницы с помощью AdminAPI, который предоставляет команду dba.deploySandboxInstance(port_number).

По умолчанию экземпляры песочницы размещаются в каталоге с именем $HOME/mysql-sandboxes/port. Давайте посмотрим, как изменить каталог:

# mkdir /var/lib/sandboxes
# mysqlsh
 MySQL  JS > shell.options.sandboxDir='/var/lib/sandboxes'
/var/lib/sandboxes

Предпосылкой для развертывания экземпляра песочницы является установка двоичных файлов MySQL. При необходимости просмотрите главу 1 для получения подробной информации. Вам нужно будет ввести пароль для пользователя root, чтобы завершить развертывание:

MySQL  JS > dba.deploySandboxInstance(3310)
A new MySQL sandbox instance will be created on this host in 
/var/lib/sandboxes/3310

Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks.

Please enter a MySQL root password for the new instance: ******

Deploying new MySQL instance...

Instance localhost:3310 successfully deployed and started.
Use shell.connect('root@localhost:3310') to connect to the instance.

Мы собираемся развернуть еще два экземпляра:

MySQL  JS > dba.deploySandboxInstance(3320)
MySQL  JS > dba.deploySandboxInstance(3330)

Следующим шагом является создание кластера InnoDB при подключении к начальному экземпляру MySQL Server. Исходный экземпляр — это экземпляр, к которому мы подключены через MySQL Shell и который мы хотим реплицировать на другие экземпляры. В этом примере все экземпляры песочницы являются пустыми экземплярами, поэтому мы можем выбрать любой экземпляр. В производственной настройке начальным экземпляром будет тот, который содержит существующий набор данных для репликации на другие экземпляры в кластере.

Мы используем эту команду для подключения MySQL Shell к начальному экземпляру, в данном случае к порту 3310:

MySQL  JS > \connect root@localhost:3310
Creating a session to root@localhost:3310
Please provide the password for root@localhost:3310:
Save password for root@localhost:3310? [Y]es/[N]o/Ne[v]er (default No): Y
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 12
Server version: 8.0.21 Source distribution
No default schema selected; type \use <schema> to set one.

Впоследствии мы будем использовать метод createCluster() для создания InnoDB Cluster с текущим подключенным экземпляром в качестве исходного:

MySQL localhost:3310 ssl  JS > var cluster = dba.createCluster('learning_mysql')

A new InnoDB cluster will be created on instance 'localhost:3310'.

Validating instance configuration at localhost:3310...
NOTE: Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters
for use within the same host.

This instance reports its own address as 127.0.0.1:3310

Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using
'127.0.0.1:33101'. Use the localAddress option to override.

Creating InnoDB cluster 'learning_mysql' on '127.0.0.1:3310'...

Adding Seed Instance...
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.

Как видно из выходных данных, три экземпляра способны поддерживать базу данных в оперативном режиме при отказе одного сервера, поэтому мы развернули три экземпляра песочницы.

Следующим шагом является добавление вторичных экземпляров в наш learning_mysql InnoDB Cluster. Любые транзакции, которые были выполнены начальным экземпляром, повторно выполняются каждым вторичным экземпляром по мере его добавления.

Исходный экземпляр в этом примере был недавно создан, поэтому он почти пуст. Следовательно, необходимо реплицировать небольшое количество данных из исходного экземпляра во вторичные экземпляры. Если необходимо реплицировать данные, MySQL будет использовать плагин клонирования (обсуждаемый в разделе «Создание реплики с помощью плагина Clone») для автоматической настройки экземпляров.

Давайте добавим один вторичный объект, чтобы увидеть процесс в действии. Чтобы добавить второй экземпляр в кластер InnoDB:

MySQL  localhost:3310 ssl  JS >  cluster.addInstance('root@localhost:3320')
...
* Waiting for clone to finish...
NOTE: 127.0.0.1:3320 is being cloned from 127.0.0.1:3310
** Stage DROP DATA: Completed
** Clone Transfer
    FILE COPY ############################################################
    100% Completed
    PAGE COPY ############################################################
    100% Completed
    REDO COPY ############################################################
    100% Completed

NOTE: 127.0.0.1:3320 is shutting down...

* Waiting for server restart... ready
* 127.0.0.1:3320 has restarted, waiting for clone to finish...
** Stage RESTART: Completed
* Clone process has finished: 59.62 MB transferred in about 1 second
(~59.62 MB/s)

State recovery already finished for '127.0.0.1:3320'

The instance '127.0.0.1:3320' was successfully added to the cluster

Затем добавьте третий экземпляр:

MySQL  localhost:3310 ssl  JS > cluster.addInstance('root@localhost:3320')

На данный момент мы создали кластер с тремя экземплярами: первичным и двумя вторичными. Мы можем увидеть статус, выполнив следующую команду:

MySQL  localhost:3310 ssl  JS > cluster.status()
{
    "clusterName": "learning_mysql",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "127.0.0.1:3310",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "127.0.0.1:3310": {
                "address": "127.0.0.1:3310",
                "mode": "R/W",
                "readReplicas": {},
                "replicationLag": null,
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.21"
            },
            "127.0.0.1:3320": {
                "address": "127.0.0.1:3320",
                "mode": "R/O",
                "readReplicas": {},
                "replicationLag": null,
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.21"
            },
            "127.0.0.1:3330": {
                "address": "127.0.0.1:3330",
                "mode": "R/O",
                "readReplicas": {},
                "replicationLag": null,
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.21"
            }
        },
        "topologyMode": "Single-Primary"
    },
    "groupInformationSourceMember": "127.0.0.1:3310"

Предполагая, что MySQL Router уже установлен (см. «MySQL Router»), единственным необходимым шагом является его загрузка с указанием местоположения сервера метаданных InnoDB Cluster.

Мы наблюдаем загрузку маршрутизатора:

# mysqlrouter --bootstrap root@localhost:3310 --user=mysqlrouter

Please enter MySQL password for root:
# Bootstrapping system MySQL Router instance...

- Creating account(s) (only those that are needed, if any)
...

## MySQL Classic protocol

- Read/Write Connections: localhost:6446
- Read/Only Connections: localhost:6447
...

Утилиты MySQL Shell

Как мы уже говорили, MySQL Shell — это мощный продвинутый клиент и редактор кода для MySQL. Среди его многочисленных функций есть утилиты для создания логического дампа и логического восстановления всего экземпляра базы данных, включая пользователей. Преимущество, по сравнению, например, с mysqldump, заключается в том, что утилита обладает возможностью распараллеливания, что значительно повышает скорость дампа и восстановления.

Вот утилиты для выполнения процесса дампа и восстановления:

util.dumpInstance()

Дамп всего экземпляра базы данных, включая пользователей

util.dumpSchemas()

Дамп набора схем

util.loadDump()

Загрузка дампа в целевую базу данных

util.dumpTables()

Загрузить определенные таблицы и представления

Давайте подробнее рассмотрим каждый из них по очереди.

util.dumpInstance()

Утилита dumpInstance() создаст дамп всех баз данных, присутствующих в каталоге данных MySQL (см. «Содержимое каталога MySQL»). Она исключит схемы information_schema, mysql_, ndbinfo, performance_schema и sys при создании дампа.

Существует также опция пробного запуска, которая позволяет вам проверять схемы и просматривать проблемы совместимости, а затем запускать дамп с соответствующими параметрами совместимости, применяемыми для устранения проблем. Давайте попробуем это сейчас — мы рассмотрим возможные ошибки и посмотрим варианты утилиты дампа.

Чтобы запустить дамп, выполните следующую команду:

MySQL  JS > shell.connect('root@localhost:48008');
MySQL  localhost:48008 ssl  JS > util.dumpInstance("/backup",
                               > {ocimds: true, compatibility:
                                   dryRun: true})
Acquiring global read lock
Global read lock acquired
Gathering information - done
All transactions have been started
Locking instance for backup
...
NOTE: Database test had unsupported ENCRYPTION option commented out
ERROR: Table 'test'.'sbtest1' uses unsupported storage engine MyISAM
(fix this with 'force_innodb' compatibility option)
Compatibility issues with MySQL Database Service 8.0.23 were found.
Please use the 'compatibility' option to apply compatibility adaptations
to the dumped DDL.
Util.dumpInstance: Compatibility issues were found (RuntimeError)

Если для параметра ocimds задано значение true, утилита дампа проверит словарь данных и индексный словарь. Параметры шифрования в операторах CREATE TABLE закомментированы в файлах DDL, чтобы гарантировать, что все таблицы расположены в каталоге данных MySQL и используют шифрование схемы по умолчанию. strip_restricted_grants удаляет определенные привилегии, ограниченные службой базы данных MySQL, которые могут вызвать ошибку в процессе создания пользователя. dryRun (пробный запуск) не требует пояснений: он выполнит только проверку, и никакие данные фактически не будут сброшены.

Итак, у нас есть таблица MyISAM в базе данных test. Опция пробного запуска явно выдает ошибку.

Чтобы исправить эту ошибку, мы собираемся использовать параметр force_innodb, который преобразует все неподдерживаемые механизмы в InnoDB в операторе CREATE TABLE:

MySQL  localhost:48008 ssl  JS > util.dumpInstance("backup",
                               > {ocimds: true, compatibility:
                               > ["strip_restricted_grants","force_innodb"],
                               dryRun: true})

Теперь пробный прогон не выдает никаких ошибок, и исключений нет. Давайте запустим команду dumpInstance(), чтобы сделать резервную копию экземпляра. Целевой каталог должен быть пустым перед экспортом. Если каталог еще не существует в своем родительском каталоге, утилита создает его.

Мы собираемся обрабатывать дамп параллельно. Для этого мы будем использовать опцию threads и установим значение 10 потоков:

MySQL  localhost:48008 ssl  JS > util.dumpInstance("/backup",
                               > {ocimds: true, compatibility:
                               > ["strip_restricted_grants","force_innodb"],
                               > threads : 10 })

Если мы посмотрим на последнюю часть вывода, то увидим:

1 thds dumping - 100% (10.00K rows / ~10.00K rows), 0.00 rows/s, 0.00 B/s uncompressed, 0.00 B/s

uncompressed
Duration: 00:00:00s
Schemas dumped: 1
Tables dumped: 10
Uncompressed data size: 1.88 MB
Compressed data size: 598.99 KB
Compression ratio: 3.1
Rows written: 10000
Bytes written: 598.99 KB
Average uncompressed throughput: 1.88 MB/s
Average compressed throughput: 598.99 KB/s

Если бы мы использовали mysqldump, у нас был бы один файл. Как мы видим здесь, в каталоге резервных копий есть несколько файлов:

@.done.json
@.json
@.post.sql
@.sql
test.json
test@sbtest10@@0.tsv.zst
test@sbtest10@@0.tsv.zst.idx
test@sbtest10.json
test@sbtest10.sql
...
test@sbtest1@@0.tsv.zst
test@sbtest1@@0.tsv.zst.idx
test@sbtest1.json
test@sbtest1.sql
test@sbtest9@@0.tsv.zst
test@sbtest9@@0.tsv.zst.idx
test@sbtest9.json
test@sbtest9.sql
test.sql

Давайте посмотрим на них:

util.dumpSchemas()

Эта утилита похожа на dumpInstance(), но позволяет указать схемы для дампа. Она поддерживает те же параметры:

MySQL localhost:48008 ssl JS > util.dumpSchemas(["test"],"/backup",
                             > {ocimds: true, compatibility:
                             > ["strip_restricted_grants","force_innodb"],
                             > threads : 10 , dryRun: true})

Если мы хотим указать несколько схем, мы можем сделать это, запустив:


MySQL  localhost:48008 ssl  JS > util.dumpSchemas(["test","percona",
                                 "learning_mysql"],"/backup",
                               > {ocimds: true, compatibility:
                               > ["strip_restricted_grants","force_innodb"],
                               > threads : 10 , dryRun: true})
util.dumpTables()

Если мы хотим извлечь более детализированные данные, например определенные таблицы, мы можем использовать утилиту dumpTables(). Опять же, большим преимуществом по сравнению с mysqldump является возможность параллельного извлечения данных из MySQL:


MySQL  localhost:48008 ssl  JS > util.dumpTables("test", [ "sbtest1",
                               > "sbtest2" ],"/backup",
                               > {ocimds: true, compatibility:
                               > ["strip_restricted_grants","force_innodb"],
                               > threads : 2 , dryRun: true})
util.loadDump(url[, options])

Мы видели все утилиты для извлечения данных, но осталась одна: та, которая загружает данные в MySQL.

Функция loadDump() обеспечивает потоковую передачу данных в удаленное хранилище, параллельную загрузку таблиц или фрагментов таблиц и отслеживание состояния выполнения. Она также предоставляет возможности возобновления и сброса, а также возможность одновременной загрузки во время создания дампа.

Обратите внимание, что эта утилита использует оператор LOAD DATA LOCAL INFILE, поэтому нам необходимо глобально включить параметр local_infile при импорте.

Утилита loadDump() проверяет, установлено ли для системной переменной sql_require_primary_key значение ON, и если да, возвращает ошибку, если в файлах дампа есть таблица без первичного ключа:

MySQL  localhost:48008 ssl  JS > util.loadDump("/backup",
                               > {progressFile :"/backup
                               > restore.json",threads :12})

Последняя часть вывода будет примерно такой:

[Worker006] percona@sbtest7@@0.tsv.zst: Records: 400000 Deleted: 0 Skipped: 0 Warnings: 0
[Worker007] percona@sbtest4@@0.tsv.zst: Records: 400000 Deleted: 0 Skipped: 0 Warnings: 0
[Worker002] percona@sbtest13@@0.tsv.zst: Records: 220742 Deleted: 0 Skipped: 0 Warnings: 0
Executing common postamble SQL

23 chunks (5.03M rows, 973.06 MB) for 23 tables in 3 schemas were loaded in 1 min 24 sec (avg throughput 11.58 MB/s)
0 warnings were reported during the load.

Обязательно ознакомьтесь с предупреждениями, указанными в конце, на случай, если они появятся.

Flame Graphs

Цитируя Брендана Грегга, определение того, почему процессоры заняты, является рутинной задачей для анализа производительности, которая часто включает в себя профилирование трассировки стека (stack traces). Профилирование путем выборки с фиксированной частотой — это грубый, но эффективный способ увидеть, какие пути кода являются горячими (занятыми на ЦП). Обычно это работает путем создания прерывания по времени, которое собирает текущий счетчик программы, адрес функции или всю трассировку стека и переводит их во что-то удобочитаемое при печати сводного отчета. Flame graphs — это тип визуализации выборочных трассировок стека, который позволяет быстро идентифицировать пути горячего кода.

Трассировка стека (также называемая обратной трассировкой стека (stack backtrace или stack traceback)) — это отчет об активных кадрах стека в определенный момент времени во время выполнения программы. Существует множество инструментов для сбора трассировки стека. Эти инструменты также известны как профилировщики процессора (CPU profilers). Профилировщик процессора, который мы собираемся использовать, — perf.

perf — это инструмент профилировщика для систем на базе Linux 2.6+, который абстрагирует аппаратные различия ЦП в измерениях производительности Linux и предоставляет простой интерфейс командной строки. perf основан на интерфейсе perf_events, экспортированном последними версиями ядра Linux.

perf_events — это инструмент наблюдения, ориентированный на события, который помогает решать сложные задачи по повышению производительности и устранению неполадок. Вопросы, на которые можно ответить, включают:

Обратите внимание, что в этой книге мы только поверхностно касаемся возможностей perf. Мы настоятельно рекомендуем посетить веб-сайт Брендана Грегга, который содержит гораздо более подробную информацию о perf и других профилировщиках ЦП.

Для создания графиков пламени (flame graphs) нам нужно начать собирать отчет о трассировке стека с perf на сервере MySQL. Эту операцию необходимо выполнить на хосте MySQL. Соберем данные за 60 секунд:

# perf record -a -g -F99 -p $(pgrep -x mysqld) -- sleep 60;
# perf report > /tmp/perf.report;
# perf script > /tmp/perf.script;

И если мы проверим каталог /tmp, мы увидим файлы perf:

# ls -l /tmp/perf*
-rw-r--r-- 1 root root 502100 Feb 13 22:01 /tmp/perf.report
-rw-r--r-- 1 root root 7303290 Feb 13 22:01 /tmp/perf.script

Следующий шаг не нужно выполнять на хосте MySQL; мы можем скопировать файлы на другой хост Linux или даже на macOS.

Для создания графиков пламени мы можем использовать репозиторий Брендана на GitHub. В этом примере мы клонируем репозиторий Flame Graph в каталог, где находится наш отчет о производительности:

# git clone https://github.com/brendangregg/FlameGraph
# ./FlameGraph/stackcollapse-perf.pl ./perf.script > perf.report.out.folded
# ./FlameGraph/flamegraph.pl ./perf.report.out.folded > perf.report.out.svg

Мы создали файл с именем perf.report.out.svg. Этот файл можно открыть в любом браузере для визуализации. На рисунке 16-1 показан пример графика пламени.

Рисунок 16-1. Пример графа пламени

Графики пламени показывают совокупность образцов по оси X и глубину стека по оси Y. Каждая функция (кадр стека) рисуется в виде прямоугольника, ширина которого зависит от количества выборок; поэтому чем больше полоса, тем больше процессорного времени тратится на эту функцию. Ось X охватывает коллекцию трассировки стека, но не показывает течение времени, поэтому порядок слева направо не имеет особого значения. Порядок выполняется в алфавитном порядке на основе имен функций, от корня до листа каждого стека.

Созданный файл является интерактивным, поэтому мы можем исследовать, на что тратится процессорное время ядра. В предыдущем примере операция INSERT потребляет 44% процессорного времени, как вы можете видеть на рисунке 16.2.

Рисунок 16-2. 44% процессорного времени используется для операции INSERT

Сборка MySQL из исходников

Как объяснялось в главе 1, дистрибутив MySQL доступен для большинства распространенных операционных систем. Некоторые компании также скомпилировали свои собственные версии MySQL (например, Facebook), которые работали на движке RocksDB и интегрировали его в MySQL. RocksDB — это встраиваемое постоянное хранилище ключей и значений для быстрого хранения, которое имеет несколько преимуществ по сравнению с InnoDB в отношении эффективности использования пространства.

Несмотря на свои преимущества, RocksDB не поддерживает репликацию или слой SQL. Это побудило команду Facebook создать MyRocks, проект с открытым исходным кодом, который интегрирует RocksDB в качестве механизма хранения MySQL. С MyRocks можно использовать RocksDB в качестве внутреннего хранилища и при этом пользоваться всеми функциями MySQL. Проект Facebook имеет открытый исходный код и доступен на GitHub.

Еще одним мотивом для компиляции MySQL является возможность настраивать ее сборку. Например, для очень конкретной проблемы мы всегда можем попытаться отладить MySQL, чтобы собрать дополнительную информацию. Для этого нам нужно настроить MySQL с параметром -DWITH_DEBUG=1.

Сборка MySQL для Ubuntu Focal Fossa и процессоров ARM

Поскольку процессоры ARM в настоящее время набирают обороты (особенно благодаря чипу Apple M1), мы покажем вам, как скомпилировать MySQL для Ubuntu Focal Fossa, работающего на ARM.

Во-первых, мы собираемся создать наши каталоги. Мы создадим один каталог для исходного кода, другой для скомпилированных двоичных файлов и третий для библиотеки boost:

# cd /
# mkdir compile
# cd compile/
# mkdir build
# mkdir source
# mkdir boost
# mkdir basedir
# mkdir /var/lib/mysql

Далее нам нужно установить дополнительные пакеты Linux, необходимые для компиляции MySQL:

# apt-get -y install dirmngr
# apt-get update -y
# apt-get -y install cmake
# apt-get -y install lsb-release wget
# apt-get -y purge eatmydata || true
# apt-get -y install psmisc pkg-config
# apt-get -y install libsasl2-dev libsasl2-modules libsasl2-modules-ldap || \
    apt-get -y install libsasl2-modules libsasl2-modules-ldap libsasl2-dev
# apt-get -y install dh-systemd || true
# apt-get -y install curl bison cmake perl libssl-dev gcc g++ libaio-dev \
    libldap2-dev libwrap0-dev gdb unzip gawk
# apt-get -y install lsb-release libmecab-dev libncurses5-dev libreadline-dev \
    libpam-dev zlib1g-dev
# apt-get -y install libldap2-dev libnuma-dev libjemalloc-dev libeatmydata \
    libc6-dbg valgrind libjson-perl libsasl2-dev
# apt-get -y install libmecab2 mecab mecab-ipadic
# apt-get -y install build-essential devscripts libnuma-dev
# apt-get -y install cmake autotools-dev autoconf automake build-essential \
    devscripts debconf debhelper fakeroot
# apt-get -y install libcurl4-openssl-dev patchelf
# apt-get -y install libeatmydata1
# apt-get install libmysqlclient-dev -y
# apt-get install valgrind -y

Эти пакеты связаны с флагами CMake, которые мы будем запускать. Если мы удалим или добавим определенные флаги, некоторые пакеты не нужно будет устанавливать (например, если мы не хотим компилировать с помощью Valgrind, нам не нужен этот пакет).

Далее мы загрузим исходный код. Для этого воспользуемся репозиторием MySQL на GitHub:

# cd source
# git clone https://github.com/mysql/mysql-server.git

Вывод будет примерно таким:

Cloning into 'mysql-server'...
remote: Enumerating objects: 1639611, done.
remote: Total 1639611 (delta 0), reused 0 (delta 0), pack-reused 1639611
Receiving objects: 100% (1639611/1639611), 3.19 GiB | 42.88 MiB/s, done.
Resolving deltas: 100% (1346714/1346714), done.
Updating files: 100% (32681/32681), done.

Чтобы проверить, какую версию мы будем компилировать, мы можем запустить следующее:

# cd mysql-server/
# git branch

Затем мы перейдем в наш каталог сборки и запустим CMake с выбранными нами флагами:

# cd /compile/build
# cmake ../source/mysql-server/ -DBUILD_CONFIG=mysql_release \
     -DCMake_BUILD_TYPE=${CMake_BUILD_TYPE:-RelWithDebInfo} \
     -DWITH_DEBUG=1 \
     -DFEATURE_SET=community \
     -DENABLE_DTRACE=OFF \
     -DWITH_SSL=system \
     -DWITH_ZLIB=system \
     -DCMake_INSTALL_PREFIX="/compile/basedir/" \
     -DINSTALL_LIBDIR="lib/" \
     -DINSTALL_SBINDIR="bin/" \
     -DWITH_INNODB_MEMCACHED=ON \
     -DDOWNLOAD_BOOST=1 \
     -DWITH_VALGRIND=1 \
     -DINSTALL_PLUGINDIR="plugin/" \
     -DMYSQL_DATADIR="/var/lib/mysql/" \
     -DWITH_BOOST="/compile/boost/"

Вот что делает каждый из них:

После того, как мы запустим CMake, мы собираемся скомпилировать MySQL с помощью команды make. Для оптимизации процесса компиляции мы будем использовать параметр -j, который указывает, сколько потоков мы собираемся использовать для компиляции MySQL. Поскольку в нашем случае у нас есть 16 ядер ARM, мы собираемся использовать 15 потоков (оставив один для действий ОС):

# make -j 15
# make install

Этот процесс может занять некоторое время, и он очень подробный. После его завершения мы можем увидеть бинарники в каталоге baseir:

# ls -l /compile/basedir/bin

Обратите внимание, что мы не найдем двоичный файл mysqld в каталоге /compile/build/bin/, вместо этого мы увидим mysqld-debug. Это связано с параметром DWITH_DEBUG, который мы установили ранее:

# /compile/build/bin/mysqld-debug --version
/compile/build/bin/mysqld-debug  Ver 8.0.23-debug-valgrind for Linux on aarch64 (Source distribution)

Теперь мы можем протестировать наш двоичный файл. Для этого мы собираемся вручную создать каталоги и настроить разрешения:

# mkdir /var/log/mysql/
# mkdir /var/run/mysqld/
# chown ubuntu: /var/log/mysql/
# chown ubuntu: /var/run/mysqld/

Затем добавим эти настройки в /etc/my.cnf:

[mysqld]
pid-file      = /var/run/mysqld/mysqld.pid
socket        = /var/run/mysqld/mysqld.sock
datadir       = /var/lib/mysql
log-error     = /var/log/mysql/error.log

Далее мы собираемся инициализировать словарь данных MySQL:

# /compile/basedir/bin/mysqld-debug --defaults-file=/etc/my.cnf --initialize \
--user ubuntu

Теперь MySQL готов к запуску:

# /compile/basedir/bin/mysqld-debug --defaults-file=/etc/my.cnf --user ubuntu &

Будет создан временный пароль, и мы можем извлечь его из журнала ошибок:

# grep "A temporary password" /var/log/mysql/error.log
2021-02-14T16:55:25.754028Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: yGldRKoRf0%T

Теперь мы можем подключиться с помощью клиента MySQL по нашему выбору:

# mysql -uroot -p'yGldRKoRf0%T'
mysql: [Warning] Using a password on the command line interface can be
insecure. Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.23-debug-valgrind

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

Анализ сбоев MySQL

Мы говорим, что MySQL аварийно завершает работу, когда процесс mysqld завершает работу без соответствующей команды завершения работы. MySQL может аварийно завершать работу по разным причинам, включая следующие:

Процесс MySQL может получать ряд сигналов от Linux. К числу наиболее распространенных относятся следующие:

Signal 15 (SIGTERM)

Вызывает отключение сервера. Это похоже на выполнение оператора SHUTDOWN без необходимости подключения к серверу (для завершения работы требуется учетная запись с привилегией SHUTDOWN). Например, следующие две команды приводят к обычному завершению работы:

# systemctl stop mysql
# kill -15 -p $(pgrep -x mysqld)
Signal 1 (SIGHUP)

Заставляет сервер перезагружать таблицы разрешений и очищать таблицы, журналы, кэш потоков и кэш хоста. Эти действия похожи на различные формы оператора FLUSH:

mysql> FLUSH LOGS;

или:

# kill -1 -p $(pgrep -x mysqld)
Signal 6 (SIGABRT)

Бывает, потому что что-то пошло не так. Он обычно используется libc и другими библиотеками для прерывания программы в случае критических ошибок. Например, glibc отправляет SIGABRT, если обнаруживает двойное освобождение или другое повреждение кучи. SIGABRT запишет подробности сбоя в журнал ошибок MySQL, например:

18:03:28 UTC - mysqld got signal 6 ;
Most likely, you have hit a bug, but this error can also be caused by...
Thread pointer: 0x7fe6b4000910
Attempting backtrace. You can use the following information to find out
where mysqld died. If you see no messages after this, something went
terribly wrong...
stack_bottom = 7fe71845fbc8 thread_stack 0x46000
/opt/mysql/8.0.23/bin/mysqld(my_print_stacktrace(unsigned char const*...
/opt/mysql/8.0.23/bin/mysqld(handle_fatal_signal+0x323) [0x1032cc3]
/lib64/libpthread.so.0(+0xf630) [0x7fe7244e5630]
/lib64/libc.so.6(gsignal+0x37) [0x7fe7224fa387]
/lib64/libc.so.6(abort+0x148) [0x7fe7224fba78]
/opt/mysql/8.0.23/bin/mysqld() [0xd52c3d]
/opt/mysql/8.0.23/bin/mysqld(MYSQL_BIN_LOG::new_file_impl(bool...
/opt/mysql/8.0.23/bin/mysqld(MYSQL_BIN_LOG::rotate(bool, bool*)+0x35)...
/opt/mysql/8.0.23/bin/mysqld(MYSQL_BIN_LOG::rotate_and_purge(THD*...
/opt/mysql/8.0.23/bin/mysqld(handle_reload_request(THD*, unsigned...
/opt/mysql/8.0.23/bin/mysqld(signal_hand+0x2ea) [0xe101da]
/opt/mysql/8.0.23/bin/mysqld() [0x25973dc]
/lib64/libpthread.so.0(+0x7ea5) [0x7fe7244ddea5]
/lib64/libc.so.6(clone+0x6d) [0x7fe7225c298d]

Trying to get some variables.
Some pointers may be invalid and cause the dump to abort.
Query (0): Connection ID (thread ID): 0
Status: NOT_KILLED

The manual page at http://dev.mysql.com/doc/mysql/en/crashing.html
contains information that should help you find out what is causing
the crash.
2021-02-14T18:03:29.120726Z mysqld_safe mysqld from pid file...
Signal 11 (SIGSEGV)

Указывает на ошибку сегментации, ошибку шины или нарушение прав доступа. Как правило, это попытка доступа к памяти, к которой процессор не может физически обратиться, или нарушение прав доступа. Когда MySQL получает SIGSEGV, будет создан дамп ядра, если настроен параметр core-file.

Signal 9 (SIGKILL)

Заставляет процесс немедленно завершиться (убивает его). Это, наверное, самый известный сигнал. В отличие от SIGTERM и SIGINT, этот сигнал нельзя перехватить или проигнорировать, а процесс-получатель не может выполнить какую-либо очистку после получения этого сигнала. Помимо вероятности повреждения данных MySQL, SIGKILL также заставит MySQL выполнить процесс восстановления при перезапуске, чтобы привести его в рабочее состояние. В следующем примере показано, как вручную отправить сигнал SIGKILL процессу MySQL:

# kill -9 -p $(pgrep -x mysqld)

Кроме того, процесс Linux OOM выполняет SIGKILL для завершения процесса MySQL.

Давайте попробуем проанализировать сбой, когда MySQL получил сигнал 11:

11:47:47 UTC - mysqld got signal 11 ;
Most likely, you have hit a bug, but this error can also be caused by...
Build ID: Not Available
Server Version: 8.0.22-13 Percona Server (GPL), Release 13, Revision 6f7822f
Thread pointer: 0x7f0e46c73000
Attempting backtrace. You can use the following information to find out
where mysqld died. If you see no messages after this, something went
terribly wrong...
stack_bottom = 7f0e664ecd10 thread_stack 0x46000
/usr/sbin/mysqld(my_print_stacktrace(unsigned char const*, unsigned...
/usr/sbin/mysqld(handle_fatal_signal+0x3c3) [0x1260d33]
/lib/x86_64-linux-gnu/libpthread.so.0(+0x128a0) [0x7f0e7acd58a0]
/usr/sbin/mysqld(Item_splocal::this_item()+0x14) [0xe36ad4]
/usr/sbin/mysqld(Item_sp_variable::val_str(String*)+0x20) [0xe38e60]
/usr/sbin/mysqld(Arg_comparator::compare_string()+0x27) [0xe5c127]
/usr/sbin/mysqld(Item_func_ne::val_int()+0x30) [0xe580e0]
/usr/sbin/mysqld(Item::val_bool()+0xcc) [0xe3ddbc]
/usr/sbin/mysqld(sp_instr_jump_if_not::exec_core(THD*, unsigned int*)+0x2d)...
/usr/sbin/mysqld(sp_lex_instr::reset_lex_and_exec_core(THD*, unsigned int*...
/usr/sbin/mysqld(sp_lex_instr::validate_lex_and_execute_core(THD*, unsigned...
/usr/sbin/mysqld(sp_head::execute(THD*, bool)+0x5c7) [0x1068e37]
/usr/sbin/mysqld(sp_head::execute_trigger(THD*, MYSQL_LEX_CSTRING const&...
/usr/sbin/mysqld(Trigger::execute(THD*)+0x10b) [0x12288cb]
/usr/sbin/mysqld(Trigger_chain::execute_triggers(THD*)+0x18) [0x1229c98]
/usr/sbin/mysqld(Table_trigger_dispatcher::process_triggers(THD*...
/usr/sbin/mysqld(fill_record_n_invoke_before_triggers(THD*, COPY_INFO*...
/usr/sbin/mysqld(Sql_cmd_update::update_single_table(THD*)+0x1e98) [0x11ec138]
/usr/sbin/mysqld(Sql_cmd_update::execute_inner(THD*)+0xd5) [0x11ec5f5]
/usr/sbin/mysqld(Sql_cmd_dml::execute(THD*)+0x6c0) [0x116f590]
/usr/sbin/mysqld(mysql_execute_command(THD*, bool)+0xaf8) [0x110e588]
/usr/sbin/mysqld(mysql_parse(THD*, Parser_state*, bool)+0x4ec) [0x111327c]
/usr/sbin/mysqld(dispatch_command(THD*, COM_DATA const*...
/usr/sbin/mysqld(do_command(THD*)+0x204) [0x1116554]
/usr/sbin/mysqld() [0x1251c20]
/usr/sbin/mysqld() [0x2620e84]
/lib/x86_64-linux-gnu/libpthread.so.0(+0x76db) [0x7f0e7acca6db]
/lib/x86_64-linux-gnu/libc.so.6(clone+0x3f) [0x7f0e78c95a3f]
Trying to get some variables.
Some pointers may be invalid and cause the dump to abort.
Query (7f0e46cb4dc8): update table1 set c2_id='R', c3_description='testing...
Connection ID (thread ID): 111
Status: NOT_KILLED
Please help us make Percona Server better by reporting any
bugs at https://bugs.percona.com/

Трассировка стека анализируется сверху вниз. Из сбоя видно, что это Percona Server v8.0.22. Далее мы видим, что поток создается на уровне ОС в этот момент:

/lib/x86_64-linux-gnu/libpthread.so.0(+0x76db) [0x7f0e7acca6db]

Продолжая движение вверх по стеку, путь кода входит в MySQL и начинает выполнять команду:

/usr/sbin/mysqld(do_command(THD*)+0x204)...

И путь кода, который дает сбой, — это функция Item_splocal:

/usr/sbin/mysqld(Item_splocal::this_item()+0x...

Немного изучив код MySQL, мы обнаруживаем, что Item_splocal является частью кода хранимой процедуры. Если мы посмотрим на конец трассировки стека, то увидим запрос:

Query (7f0e46cb4dc8): update table1 set c2_id='R', c3_description='testing...

Триггеры также могут использовать путь к хранимой процедуре, если они содержат переменные. Если мы проверим, есть ли в этой таблице триггеры, мы увидим следующее:

CREATE DEFINER=`root`@`localhost` TRIGGER `table1_update_trigger`
BEFORE UPDATE ON `table1` FOR EACH ROW BEGIN
DECLARE vc1_id VARCHAR(2);
SELECT c2_id FROM table1 WHERE c1_id = new.c1_id INTO vc1_id;
IF vc1_id <> P THEN
INSERT INTO table1_hist(
c1_id,
c2_id,
c3_description)
VALUES(
old.c1_id,
old.c2_id,
new.c3_description);
END IF;
END
;;

Со всей этой информацией мы можем создать тестовый пример и сообщить об ошибке:

USE test;

CREATE TABLE `table1` (
  `c1_id` int primary key auto_increment,
  `c2_id` char(1) NOT NULL,
  `c3_description` varchar(255));

CREATE TABLE `table1_hist` (
  `c1_id` int,
  `c2_id` char(1) NOT NULL,
  `c3_description` varchar(255));
  insert into table1 values (1, T, test crash);

delimiter ;;

CREATE DEFINER=`root`@`localhost` TRIGGER `table1_update_trigger`
BEFORE UPDATE ON `table1` FOR EACH ROW BEGIN
DECLARE vc1_id VARCHAR(2);
SELECT c2_id FROM table1 WHERE c1_id = new.c1_id INTO vc1_id;
IF vc1_id <> P THEN
INSERT INTO table1_hist(
c1_id,
c2_id,
c3_description)
VALUES(
old.c1_id,
old.c2_id,
new.c3_description);
END IF;
END
;;

Чтобы воспроизвести его, мы запускаем несколько команд одновременно в одной таблице, пока не произойдет ошибка:

$ mysqlslap --user=msandbox --password=msandbox \
    --socket=/tmp/mysql_sandbox37515.sock \
    --create-schema=test --port=37515 \
    --query="update table1 set c2_id='R',
    *c3_description='testing crash' where c1_id=1" \
    --concurrency=50 --iterations=200

Эту ошибку относительно легко воспроизвести, и мы рекомендуем вам протестировать ее. Вы можете найти более подробную информацию об этой ошибке в системе Percona Jira.

Также мы можем видеть, что Oracle исправила ошибку в версии 8.0.23 благодаря примечаниям к выпуску:

Prepared statements involving stored programs could cause heap-use-after-free memory problems (Bug #32131022, Bug #32045681, Bug #32051928).

Иногда ошибки нелегко воспроизвести, и их исследование может быть очень утомительным. С этим возникают проблемы даже у опытных инженеров, особенно при расследовании утечек памяти. Мы надеемся, что мы пробудили ваше любопытство к расследованию сбоев.

Об авторах

Винисиус Гриппа (Vinicius Grippa) — старший инженер службы поддержки, работающий в Percona, и старший партнер Oracle. Винисиус имеет степень бакалавра компьютерных наук и уже 13 лет работает с базами данных. Он имеет опыт проектирования баз данных для критически важных приложений и за последние несколько лет стал специалистом по экосистемам MySQL и MongoDB. Работая в команде поддержки, он помог клиентам Percona с сотнями различных случаев с широким спектром сценариев и сложностей. Винисиус также активно участвует в сообществе ОС, участвуя в виртуальных комнатах, таких как Slack, выступая на встречах и выступая на конференциях в Европе, Азии, Северной и Южной Америке.

Сергей Кузьмичев — старший инженер службы поддержки Percona. Ему нравится разгадывать хорошие технические загадки, работать с базами данных и создавать надежные системы. Сергей часто пишет код и отправляет отчеты об ошибках в проекты с открытым исходным кодом, а также пишет сообщения в блогах о MySQL и других базах данных с открытым исходным кодом. До прихода в Percona Сергей почти десять лет работал администратором баз данных и DevOps-инженером.

Колофон

Животные на обложке Learning MySQL — синие пятнистые вороны (Euploea midamus), бабочки, обитающие в Индии и Юго-Восточной Азии.

Этот вид бабочек в основном черный и темно-коричневый, с белыми пятнами на груди и внешней части крыльев. Синие пятна на их крыльях создаются преломленным светом как структурный цвет и на самом деле не являются цветом поверхности крыла.

Как и у многих видов бабочек, гусеницы синих пятнистых ворон питаются в основном листьями и стеблями одной группы растений; в их случае семейство Apocynaceae/dogbane. Их яйца откладываются на нижней стороне растений этого семейства. Гусеницы проходят через пять возрастов, прежде чем окукливаются и становятся взрослыми. Затем взрослые бабочки питаются нектаром многих цветов, иногда собираясь в большом количестве, чтобы питаться на одном растении. Они используют свой хоботок, чтобы направить цветочный нектар в рот.

Внутренние мембраны крыльев бабочки покрыты тонким слоем мелких перекрывающихся чешуек. Края этих крошечных чешуек рассеивают и преломляют свет, создавая переливчатость, а также синий цвет (чешуйки также выполняют функциональную функцию, защищая от воды и помогая рассеивать тепло). Исследования показывают, что крылья бабочек (таких как Euploea) имеют свои собственные четкие узоры чешуи. Некоторые бабочки из рода Euploea также были изучены на предмет их ультра-черной окраски.

Часто встречается сине-пятнистая ворона, и, хотя в некоторых частях ее ареала вид находится под защитой, IUCN не оценивал ее охранный статус. Многие животные на обложках O'Reilly находятся под угрозой исчезновения; все они важны для мира.

Цветная иллюстрация на обложке выполнена Карен Монтгомери на основе черно-белой гравюры из Музея живой природы Чарльза Найта (1844). Шрифты обложки — Gilroy Semibold и Guardian Sans. Текстовый шрифт — Adobe Minion Pro; шрифт заголовка — Adobe Myriad Condensed; а шрифт кода — Ubuntu Mono Далтона Маага.