Эта статья задумана мной как сборник некоторых интересных моментов по использованию и оптимизации SQL запросов в БД MySQL, на мой взгляд, плохо освещенных в интернете. Так, из статьи вы узнаете о конструкции with rollup, и о том, как переписать подзапросы in и not in на join'ы, а так же обновление и удаление данных в нескольких таблицах — одним запросом, и многое другое. Начнем по порядку.
Одни из самых распространённых подзапросов являются запросы с in и not in. Причём, мы знаем, что в MySQL join запросы отрабатывают быстрее за счёт эффективного построения плана выполнения запроса оптимизатором (для других БД это утверждение может быть диаметрально противоположным), поэтому попробуем переписать классические подзапросы на join. Как мы это будем делать? Для начала уясним то, что in запросы выводят все значения, которые присутствуют в обоих таблицах, а значит такому запросу будет однозначно соответствовать внутренний inner join. Запросы с not in наоборот выводят все значения, которые не присутствуют в подзапросе, а значит им уже будет соответствовать внешний outer join. Итак, начало положено, попробуем с этим что-нибудь сделать.
Для примера буду использовать тестовую БД world, которую можно взять с официального сайта mysql здесь
В БД world есть таблицы Country (страны) и CountryLanguage (официальные языки). Поставим себе задачу найти все страны, в которых говорят хотя бы на одном языке. Говоря другими словами, мы ищем территории с населением, у которого есть официальные языки. Напишем привычный in подзапрос:
На заметку, этот запрос можно переписать ещё и так:
Теперь, исходя из предположения выше, перепишем подзапрос на inner join:
Почти получилось, но у нас произошло дублирование данных, которое убираем через опцию distinct. Конечный вариант для всех полей таблицы получится таким:
Отлично! Подзапрос in успешно переписан на join.
Теперь немного сложнее — перепишем not in на outer join. Цель запроса — все территории, на которых не проживают люди и нет официальных языков. Снова вначале привожу стандартный not in подзапрос:
И показываю его же для not exists:
Как и в первом случае, перепишем на left join:
В результате получим, как и в первом случае, дублирование данных, и, конечно же, строки, которым не нашлось парного значения во второй таблице. Именно эти строки дают решение поставленной задачи, поэтому просто убираем все парные строки:
Такими нехитрыми преобразованиями мы смогли немного помочь оптимизатору запросов.
Бывают редкие случаи, когда нам нужно написать подзапрос, в котором сравнение происходит не по одному, а нескольким столбцам, однако писать так было бы явно НЕправильно:
Для этих целей существует регламентированный SQL стандартом запрос, отрабатываемый всеми базами данных:
Такой запрос называется «конструктором строк» и может быть подчёркнут функцией ROW(). В этом случае мы бы написали:
Несмотря на свою привлекательность, конструктор строк имеет ряд ограничений:
1. Подзапрос должен возвращать одну строку, а не несколько
2. Вы не можете использовать операторы сравнения <, > или <>, хотя это ограничение можно обойти специальными словами all, any, in или exists
Стоит обратить внимание, что такую конструкцию можно использовать не только для подзапросов, но и в скалярных выражениях:
Правда, на практике, конструктор запросов не очень эффективен для скалярных выражений, поэтому перепишем запрос к нормальному виду:
Возможно, кого-то удивит такой заголовок, но почему бы и нет? Начнём с обновления данных. Официальная документация говорит про следующий синтаксис:
Скорее всего, вы сделаете запрос вида:
С другой стороны, никто не мешает сделать запрос, который обновит данные сразу в двух, трёх и более таблицах:
Правда, он вряд ли будет иметь смысл, но, тем не менее, такое возможно.
С операцией удаления ещё интереснее обстоят дела. Официальная документация декларирует такой синтаксис:
Либо
Что соответствует запросам вида:
В этих двух запросах удаление происходит из таблицы t1, а t2 используется для создания условия выборки данных.
И как вы уже догадались, для удаления данных одновременно из двух таблиц делаем так:
Возможно те, кто сталкивался с такими промышленными БД как Oracle или SQL Server при чтении заголовка вскрикнут: «Ну надо же!», — но, увы, я сразу остужу их пламенные возгласы. С версии MySQL 4.1.1, когда появился модификатор with rollup, эта тема не продвинулась ни на миллиметр, поэтому никаких кубов данных вы не сможете построить встроенными средствами данной БД.
Для тех, кто не в курсе, что означает модификатор with rollup кратко поясню, что он используется для создания отчетов, содержащих подытоги и окончательное итоговое значение. В примерах, буду снова использовать базу world.
Предположим, что нам нужно получить суммарное и среднее число проживающих людей на всех географических территориях (регионах), а также на континентах и во всём мире. Если решать в лоб, получим следующие запросы:
Суммарное и среднее число проживающих людей на всех географических территориях (регионах):
Суммарное и среднее число проживающих людей на всех континентах:
Суммарное и среднее число проживающих людей во всём мире:
Вместо выполнения этих запросов и последующего сложного объединения результатов, можно выполнить всего один:
Обратите внимание, что в некоторых строках в не агрегирующих колонках стоит NULL, что указывает на то, что данная строка является подытогом. Например, строку
нужно читать как в Южной Америке суммарное население составляет 345780000 человек, а среднее значение 24698571.4286
А вот строка
Является окончательным итогом по отношению к численности населения на всём земном шаре.
Положительный эффект модификатора with rollup заключается в том, что проход по записям происходит один раз! Кстати, эта функциональность очень удобна при выводе какой-либо статистики на сайте (программе). Если вас заинтересовала данная функциональность или остались вопросы, то за подробностями прошу в официальную документацию
«--i-am-a-dummy» в разговорной форме переводится как – «я — болван». Эта опция является синонимом опции --safe-updates, которая создана для новичков и накладывает ряд ограничений для того, чтобы вашей любимой БД не стало плохо от ваших действий:
1. Запросы update и delete выполняются только при наличии условия where или limit
2. select одной таблицы (без join'ов и подзапросов) возвращает только первые 1000 строк, если явно не указан limit
3. select запросы с join или подзапросом обрабатывает только первые 1 000 000 строк
Для переопределения этих ограничений выполните следующий запрос со своими параметрами:
Для просмотра текущих настроек:
Также возможно параметризировать настройки при запуске клиента в шелле
И на закуску. После БД, отличных от MySQL, меня всегда удивляло, почему MySQL в однострочном комментарии, выглядящем как двойное тире, обязательно после себя требует пробел, табуляцию или другой управляющий символ, хотя по стандарту обязательного управляющего символа не должно быть. Согласитесь, когда пишешь какой-то запрос и нужно быстро закомментировать часть кода, уж очень долго ставить такое количество символов.
Что я имею ввиду. В MySQL мы пишем так:
(с пробелом перед SELECT), а в других БД:
(без управляющего символа).
Разгадка оказалась очень простой. Дело в том, что если вы напишите такой небрежный запрос
то первое тире будет трактоваться минусом, а второе -1, но никак не комментарием. Если после двух тире поставить управляющий символ, то только в этом случае мы получим комментарий. Подробнее об этом комментарии здесь
Переписываем подзапросы с in и not in на join'ы
Одни из самых распространённых подзапросов являются запросы с in и not in. Причём, мы знаем, что в MySQL join запросы отрабатывают быстрее за счёт эффективного построения плана выполнения запроса оптимизатором (для других БД это утверждение может быть диаметрально противоположным), поэтому попробуем переписать классические подзапросы на join. Как мы это будем делать? Для начала уясним то, что in запросы выводят все значения, которые присутствуют в обоих таблицах, а значит такому запросу будет однозначно соответствовать внутренний inner join. Запросы с not in наоборот выводят все значения, которые не присутствуют в подзапросе, а значит им уже будет соответствовать внешний outer join. Итак, начало положено, попробуем с этим что-нибудь сделать.
Для примера буду использовать тестовую БД world, которую можно взять с официального сайта mysql здесь
В БД world есть таблицы Country (страны) и CountryLanguage (официальные языки). Поставим себе задачу найти все страны, в которых говорят хотя бы на одном языке. Говоря другими словами, мы ищем территории с населением, у которого есть официальные языки. Напишем привычный in подзапрос:
На заметку, этот запрос можно переписать ещё и так:
Теперь, исходя из предположения выше, перепишем подзапрос на inner join:
Почти получилось, но у нас произошло дублирование данных, которое убираем через опцию distinct. Конечный вариант для всех полей таблицы получится таким:
Отлично! Подзапрос in успешно переписан на join.
Теперь немного сложнее — перепишем not in на outer join. Цель запроса — все территории, на которых не проживают люди и нет официальных языков. Снова вначале привожу стандартный not in подзапрос:
И показываю его же для not exists:
Как и в первом случае, перепишем на left join:
В результате получим, как и в первом случае, дублирование данных, и, конечно же, строки, которым не нашлось парного значения во второй таблице. Именно эти строки дают решение поставленной задачи, поэтому просто убираем все парные строки:
Такими нехитрыми преобразованиями мы смогли немного помочь оптимизатору запросов.
Сравнение строк в подзапросах
Бывают редкие случаи, когда нам нужно написать подзапрос, в котором сравнение происходит не по одному, а нескольким столбцам, однако писать так было бы явно НЕправильно:
Для этих целей существует регламентированный SQL стандартом запрос, отрабатываемый всеми базами данных:
Такой запрос называется «конструктором строк» и может быть подчёркнут функцией ROW(). В этом случае мы бы написали:
Несмотря на свою привлекательность, конструктор строк имеет ряд ограничений:
1. Подзапрос должен возвращать одну строку, а не несколько
2. Вы не можете использовать операторы сравнения <, > или <>, хотя это ограничение можно обойти специальными словами all, any, in или exists
Стоит обратить внимание, что такую конструкцию можно использовать не только для подзапросов, но и в скалярных выражениях:
Правда, на практике, конструктор запросов не очень эффективен для скалярных выражений, поэтому перепишем запрос к нормальному виду:
Обновление и удаление данных одновременно из нескольких таблиц.
Возможно, кого-то удивит такой заголовок, но почему бы и нет? Начнём с обновления данных. Официальная документация говорит про следующий синтаксис:
Скорее всего, вы сделаете запрос вида:
С другой стороны, никто не мешает сделать запрос, который обновит данные сразу в двух, трёх и более таблицах:
Правда, он вряд ли будет иметь смысл, но, тем не менее, такое возможно.
С операцией удаления ещё интереснее обстоят дела. Официальная документация декларирует такой синтаксис:
Либо
Что соответствует запросам вида:
В этих двух запросах удаление происходит из таблицы t1, а t2 используется для создания условия выборки данных.
И как вы уже догадались, для удаления данных одновременно из двух таблиц делаем так:
Немного про OLAP. Модификатор WITH ROLLUP
Возможно те, кто сталкивался с такими промышленными БД как Oracle или SQL Server при чтении заголовка вскрикнут: «Ну надо же!», — но, увы, я сразу остужу их пламенные возгласы. С версии MySQL 4.1.1, когда появился модификатор with rollup, эта тема не продвинулась ни на миллиметр, поэтому никаких кубов данных вы не сможете построить встроенными средствами данной БД.
Для тех, кто не в курсе, что означает модификатор with rollup кратко поясню, что он используется для создания отчетов, содержащих подытоги и окончательное итоговое значение. В примерах, буду снова использовать базу world.
Предположим, что нам нужно получить суммарное и среднее число проживающих людей на всех географических территориях (регионах), а также на континентах и во всём мире. Если решать в лоб, получим следующие запросы:
Суммарное и среднее число проживающих людей на всех географических территориях (регионах):
Суммарное и среднее число проживающих людей на всех континентах:
Суммарное и среднее число проживающих людей во всём мире:
Вместо выполнения этих запросов и последующего сложного объединения результатов, можно выполнить всего один:
Обратите внимание, что в некоторых строках в не агрегирующих колонках стоит NULL, что указывает на то, что данная строка является подытогом. Например, строку
нужно читать как в Южной Америке суммарное население составляет 345780000 человек, а среднее значение 24698571.4286
А вот строка
Является окончательным итогом по отношению к численности населения на всём земном шаре.
Положительный эффект модификатора with rollup заключается в том, что проход по записям происходит один раз! Кстати, эта функциональность очень удобна при выводе какой-либо статистики на сайте (программе). Если вас заинтересовала данная функциональность или остались вопросы, то за подробностями прошу в официальную документацию
Опция запуска --i-am-a-dummy
«--i-am-a-dummy» в разговорной форме переводится как – «я — болван». Эта опция является синонимом опции --safe-updates, которая создана для новичков и накладывает ряд ограничений для того, чтобы вашей любимой БД не стало плохо от ваших действий:
1. Запросы update и delete выполняются только при наличии условия where или limit
2. select одной таблицы (без join'ов и подзапросов) возвращает только первые 1000 строк, если явно не указан limit
3. select запросы с join или подзапросом обрабатывает только первые 1 000 000 строк
Для переопределения этих ограничений выполните следующий запрос со своими параметрами:
Для просмотра текущих настроек:
Также возможно параметризировать настройки при запуске клиента в шелле
Эстетичный комментарий
И на закуску. После БД, отличных от MySQL, меня всегда удивляло, почему MySQL в однострочном комментарии, выглядящем как двойное тире, обязательно после себя требует пробел, табуляцию или другой управляющий символ, хотя по стандарту обязательного управляющего символа не должно быть. Согласитесь, когда пишешь какой-то запрос и нужно быстро закомментировать часть кода, уж очень долго ставить такое количество символов.
Что я имею ввиду. В MySQL мы пишем так:
(с пробелом перед SELECT), а в других БД:
(без управляющего символа).
Разгадка оказалась очень простой. Дело в том, что если вы напишите такой небрежный запрос
то первое тире будет трактоваться минусом, а второе -1, но никак не комментарием. Если после двух тире поставить управляющий символ, то только в этом случае мы получим комментарий. Подробнее об этом комментарии здесь
Комментариев нет:
Отправить комментарий