05.02.2011

SQL для начинающих. Часть 2

Представляю Вашему вниманию вольный перевод статьи SQL for Beginners Part 2

Для каждого веб-разработчика важно уметь взаимодействовать с базами данных. Во второй части мы продолжаем изучение языка SQL и применяем свои навыки к СУБД MySQL. Мы познакомимся с индексами, типами данных и более сложными запросами.

Что вам нужно

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

Если Вы хотите выполнять приведенные примеры на своем сервере, сделайте следующее:

  1. Откройте консоль MySQL и авторизуйтесь.
  2. Создайте базу "my_first_db" с помощью запроса CREATE, если она не была создана ранее.
  3. Смените базу, используя оператор USE.



Индексы

Индексы (или ключи) обычно используются для повышения скорости выполнения операторов, которые выбирают данные (такие как SELECT) из таблиц.

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

Основные доводы в пользу индексации столбцов базы данных:

  • Почти каждая таблица имеет первичный ключ (PRIMARY KEY), обычно это столбец "id".
  • Если в столбце предполагается хранить уникальные значения, он должен иметь уникальный индекс (UNIQUE).
  • Если нужен частый поиск по столбцу (его использование в предложении WHERE), он должен иметь обычный индекс (INDEX).
  • Если столбец используется для взаимосвязи с другой таблицей, он должен быть по возможности внешним ключом (FOREIGN KEY) или обычным индексом.

Первичный ключ (PRIMARY KEY)

Почти у всех таблиц есть первичный ключ, обычно это целое число с опцией автоинкремента (AUTO_INCREMET).

Если Вы вспомните, в первой части, мы создали поле user_id в таблице пользователей (users) и он был первичным ключом. В веб-приложениях, мы можем обратиться ко всем пользователям по их номеру id.

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

Давайте рассмотрим простой запрос, который создает таблицу для хранения списка штатов США:

CREATE TABLE states (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(20)
);

Можно записать его так:

CREATE TABLE states (
    id INT AUTO_INCREMENT,
    name VARCHAR(20),
    PRIMARY KEY (id)
);

Уникальный ключ (UNIQUE)

Поскольку мы предполагаем, что имя штата уникально, нам надо немного изменить предыдущий запрос:

CREATE TABLE states (
    id INT AUTO_INCREMENT,
    name VARCHAR(20),
    PRIMARY KEY (id),
    UNIQUE (name)
);

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

CREATE TABLE states (
    id INT AUTO_INCREMENT,
    name VARCHAR(20),
    PRIMARY KEY (id),
    UNIQUE state_name (name)
);

Теперь индекс называется "state_name", а не "name".

Индекс (INDEX)

Допустим мы хотим добавить столбец для хранения года, в котором каждый штат вошел в состав США.

CREATE TABLE states (
    id INT AUTO_INCREMENT,
    name VARCHAR(20),
    join_year INT,
    PRIMARY KEY (id),
    UNIQUE (name),
    INDEX (join_year)
);

Я добавил столбец "join_year" и проиндексировал его. Этот тип индекса не накладывает ограничение на уникальность.

Вы можете использовать KEY вместо INDEX.

CREATE TABLE states (
    id INT AUTO_INCREMENT,
    name VARCHAR(20),
    join_year INT,
    PRIMARY KEY (id),
    UNIQUE (name),
    KEY (join_year)
);

Подробнее о производительности

Добавление индекса снижает производительность запросов INSERT и UPDATE. Потому что каждый раз при вставке новых данных в таблицу, данные индекса тоже обновляются, что требует выполнения дополнительной работы. Увеличение производительности для запросов SELECT обычно перевешивают эти недостатки. Однако не надо добавлять индексы к каждому столбцу таблицы, не подумав о том какие запросы будут выполняться.

Пример таблицы

Перед тем как идти дальше, я хотел бы создать таблицу с некоторыми данными.

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

CREATE TABLE states (
  id INT AUTO_INCREMENT,
  name VARCHAR(20),
  join_year INT,
  population INT,
  PRIMARY KEY (id),
  UNIQUE (name),
  KEY (join_year)
);

INSERT INTO states VALUES
(1, 'Alabama', 1819, 4661900),
(2, 'Alaska', 1959, 686293),
(3, 'Arizona', 1912, 6500180),
(4, 'Arkansas', 1836, 2855390),
(5, 'California', 1850, 36756666),
(6, 'Colorado', 1876, 4939456),
(7, 'Connecticut', 1788, 3501252),
(8, 'Delaware', 1787, 873092),
(9, 'Florida', 1845, 18328340),
(10, 'Georgia', 1788, 9685744),
(11, 'Hawaii', 1959, 1288198),
(12, 'Idaho', 1890, 1523816),
(13, 'Illinois', 1818, 12901563),
(14, 'Indiana', 1816, 6376792),
(15, 'Iowa', 1846, 3002555),
(16, 'Kansas', 1861, 2802134),
(17, 'Kentucky', 1792, 4269245),
(18, 'Louisiana', 1812, 4410796),
(19, 'Maine', 1820, 1316456),
(20, 'Maryland', 1788, 5633597),
(21, 'Massachusetts', 1788, 6497967),
(22, 'Michigan', 1837, 10003422),
(23, 'Minnesota', 1858, 5220393),
(24, 'Mississippi', 1817, 2938618),
(25, 'Missouri', 1821, 5911605),
(26, 'Montana', 1889, 967440),
(27, 'Nebraska', 1867, 1783432),
(28, 'Nevada', 1864, 2600167),
(29, 'New Hampshire', 1788, 1315809),
(30, 'New Jersey', 1787, 8682661),
(31, 'New Mexico', 1912, 1984356),
(32, 'New York', 1788, 19490297),
(33, 'North Carolina', 1789, 9222414),
(34, 'North Dakota', 1889, 641481),
(35, 'Ohio', 1803, 11485910),
(36, 'Oklahoma', 1907, 3642361),
(37, 'Oregon', 1859, 3790060),
(38, 'Pennsylvania', 1787, 12448279),
(39, 'Rhode Island', 1790, 1050788),
(40, 'South Carolina', 1788, 4479800),
(41, 'South Dakota', 1889, 804194),
(42, 'Tennessee', 1796, 6214888),
(43, 'Texas', 1845, 24326974),
(44, 'Utah', 1896, 2736424),
(45, 'Vermont', 1791, 621270),
(46, 'Virginia', 1788, 7769089),
(47, 'Washington', 1889, 6549224),
(48, 'West Virginia', 1863, 1814468),
(49, 'Wisconsin', 1848, 5627967),
(50, 'Wyoming', 1890, 532668);

GROUP BY: Группировка данных

Предложение GROUP BY группирует результирующий набор данных. Пример:

GROUP BY

Итак, что произошло? В таблице у нас 50 строк, а запрос вернул только 34. Так произошло, потому что результат сгруппирован по столбцу "join_year". Другими словами, мы видим только уникальные позиции столбца join_year. Некоторые штаты имеют одно и тоже значение join_year, поэтому мы получили менее 50 строк.

Например, присутствует только одно значение с 1787 годом, хотя в эту группу входят 3 штата:

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

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

COUNT(*): Подсчет строк

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

Например, мы можем использовать ее для подсчета количества штатов для каждого join_year:



Группировка всего

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

Количество строк во всей таблице:

COUNT(*)

Количество строк удовлетворяющие условию WHERE:

COUNT(*) и WHERE

MIN(), MAX() и AVG()

Эти функции возвращают минимальное, максимальное и среднее значения:

MIN(), MAX() и AVG()

GROUP_CONCAT()

Эта функция объединяет все значения внутри группы в одну строку с разделителем.

В первом запросе с GROUP BY только один штат для каждого года. Используя данную функцию, мы можем видеть все имена в каждой группе:

GROUP_CONCAT()

Запрос:

SELECT GROUP_CONCAT(name SEPARATOR ', '), join_year
FROM states GROUP BY join_year;

SUM()

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

SUM()

IF() & CASE: Управление потоком выполнения

Как и в других языках программирования, в SQL есть конструкции для управления потоком выполнения.

IF()

Эта функция принимает три аргумента. Первый аргумент условие, второй аргумент выполняется когда условие истинно, третий - когда ложно.

IF()

Вот более практический пример использования функции SUM():

SELECT
    SUM(
        IF(population > 5000000, 1, 0)
    ) AS big_states,

    SUM(
        IF(population <= 5000000, 1, 0)
    ) AS small_states
FROM states;

Первая функция SUM() подсчитывает количество больших штатов (население более 5 миллионов), вторая - количество маленьких штатов. Условие IF() вызывается внутри функции SUM() и возвращает 1 или 0 в зависимости от условия.

Результат выполнения запроса:

SUM() и IF()

CASE (Выбор)

CASE работает подобно условию switch-case в других языках программирования.

Предположим, что мы хотим разделить штаты на три возможные категории.

SELECT
COUNT(*),
CASE
    WHEN population > 5000000 THEN 'big'
    WHEN population > 1000000 THEN 'medium'
    ELSE 'small' END
    AS state_size
FROM states GROUP BY state_size;

Как Вы видите, мы можем применить GROUP BY для значений, возвращаемых после условия CASE. Вот что произошло:

CASE

HAVING: Условие для скрытых полей

Условие HAVING применяется для "скрытых" полей, таких как результат, возвращаемый агрегирующей функцией. Обычно оно используется совместно с GROUP BY.

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

SELECT COUNT(*), join_year FROM states GROUP BY join_year;

Результат - 34 строки.

Теперь, скажем, что нас интересуют только строки с количеством больше 1. Мы не можем использовать условие WHERE для этого:

На помощь приходит HAVING:

HAVING

Помните, что такая возможность доступна не во всех СУБД.

Подзапросы

Можно использовать результат одного запроса в другом.

В этом примере мы получим штат с наибольшим населением:

SELECT * FROM states WHERE population = (
    SELECT MAX(population) FROM states
);

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

Подзапрос

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

SELECT * FROM states ORDER BY population DESC LIMIT 1;

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

В этом примере мы получим штат, который был присоединен к Союзу последним:

SELECT * FROM states WHERE join_year = (
    SELECT MAX(join_year) FROM states
);

На этот раз мы получили 2 строки в результате. Если бы мы использовали ORDER BY ... LIMIT 1, то получили бы совсем другой результат.



IN()

Возможно Вы захотите использовать несколько резульатов, возвращаемых внутренним запросом.

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

SELECT * FROM states WHERE join_year IN (
    SELECT join_year FROM states
    GROUP BY join_year
    HAVING COUNT(*) > 1
) ORDER BY join_year;

IN()

Подробнее о подзапросах

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

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

UNION: Совмещение данных

Используя запрос UNION, можно объединять результаты нескольких напросов SELECT.

В этом примере объединяются штаты, название которых начинается с буквы "N", со штатами с большим населением:

(SELECT * FROM states WHERE name LIKE 'n%')
UNION
(SELECT * FROM states WHERE population > 10000000);

UNION

Обратите внимание, что штат New York принадлежит крупным и начинается с буквы "N". Однако в списке он встречается один раз, т.к. дубликаты удаляются автоматически.

Так же прелесть запросов UNION заключается в том, что их можно использовать для объединения запросов к разным таблицам.

Например, у нас есть таблицы employees (сотрудники), managers (менеджеры) и customers (клиенты). В каждой таблице есть поле с адресом электронной почты. Если мы хотим получить все E-mail адреса в одном запросе, то можем поступить следующим образом:

(SELECT email FROM employees)
UNION
(SELECT email FROM managers)
UNION
(SELECT email FROM customers WHERE subscribed = 1);

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

INSERT Продолжение

Мы уже говорили о запросе INSERT в предыдущей статье. После того как мы рассмотрели индексы, мы можем поговорить о дополнительных возможностях запросов INSERT.

INSERT ... ON DUPLICATE KEY UPDATE

Это наиболее часто используемое условие. Сначала запрос пытается выполнить INSERT, и если запрос терпит неудачу в следствии дублирования первичного (PRIMARY KEY) или уникального (UNIQUE KEY) ключа, то выполняется запрос UPDATE.

Давайте сначала создадим тестовую таблицу.

Это таблица для хранения продуктов. Поле "stock" хранит количество продуктов доступных на складе.

Теперь попробуем вставить уже существующее значение в таблицу и посмотрим что произойдет.

Мы получили ошибку.

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



REPLACE INTO

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

REPLACE INTO

Обратите внимание, т.к. вставляется совершенно новая строка, поле автоинкремента увеличивается на единицу.

INSERT IGNORE

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

INSERT IGNORE

Нет никаких ошибок и обновленных строк.

Типы данных

Каждый столбец в таблице должен быть определенного типа. Мы уже использовали типы INT, VARCHAR и DATE, но не останавливались на них подробно. Также мы рассмотрим еще несколько типов данных.

Начнем с числовых типов данных. Я разделяю из на две группы: Целые и дробные.

Целые

Столбец с типом целые может хранить только натуральные числа (без десятичной точки). По-умолчанию они могут быть положительными или отрицательными. Если выбрана опция UNSIGNED, то могут храниться только положительные числа.

MySQL поддерживает 5 типов целых чисел разных размеров и диапазонов:

Целые типы данных

Дробные числовые типы данных

Эти типы могут хранить дробные числа: FLOAT, DOUBLE и DECIMAL.

FLOAT занимает 4 байта, DOUBLE занимает 8 байт и аналогичен предыдущему. DOUBLE более точный.

DECIMAL(M,N) имеет изменяемую точность. M максимальное число цифр, N - число цифр после десятичной точки.

Например, DECIMAL(13,4) имеет 9 знаков до запятой и 4 после.

Строковые типы данных

По названию можно догадаться, что в них можно хранить строки.

CHAR(N) может хранить N символов и имеет фиксированную величину. Например, CHAR(50) должен всегда содержать 50 символов в каждой строке во всем столбце. Максимально возможное значениен 255 символов

VARCHAR(N) работает также, но диапазон может меняться. N - обозначает максимальное значение. Если хранимая строка короче N символов, то она будет занимать меньше места на жестком диске. Максимально возможное значениен 65535 символов.

Разновидности типа TEXT больше подходят для длинных строк. TEXT имеет ограничение в 65535 символов, MEDIUMTEXT в 16.7 миллионов, и LONGTEXT в 4.3 миллиарда символов. MySQL обычно хранит их в отдельных хранилищах на сервере, для того что бы главное хранилище было по возможности меньше и быстрее.

Тип DATE (Дата)

Тип DATE хранит даты и показывает их в формате "YYYY-MM-DD", но не хранит информацию о времени. Имеет диапазон от 1001-01-01 до 9999-12-31.

Тип DATETIME содержит дату и время и имеет формат "YYYY-MM-DD HH:MM:SS". Имеет диапазон от "1000-01-01 00:00:00" до "9999-12-31 23:59:59". Занимает 8 байт.

TIMESTAMP работает как DATETIME с некоторыми отличаями. Он занимает только 4 байта и имеет диапазон "1970-01-01 00:00:01" UTC до "2038-01-19 03:14:07" UTC. Например, он не очень подходит для хранения дат рождения.

Тип TIME хранит только время, а YEAR только год.

Другое

Другие типы данных, поддерживаемые MySQL. Посмотреть их список можно здесь. Так же обратите внимание на размеры хранимых данных каждого типа.

Заключение

Спазибо за чтение статьи. SQL - это важный язык и инструмент в арсенале веб-разработчика.