Урок 03

Типы данных MySQL

Отличия от PostgreSQL

Главное:

  • Меньше типов — нет массивов, UUID, JSONB, range.
  • AUTO_INCREMENT вместо IDENTITY.
  • DATETIME / TIMESTAMP — оба без явной таймзоны (TIMESTAMP хранит UTC, DATETIME — как есть).
  • ENUM — встроенный, но с оговорками.
  • JSON — не бинарный, индексируется слабее.

Целые числа

ТипРазмерSignedUnsigned
TINYINT1 байт-128 … 1270 … 255
SMALLINT2±32 7670 … 65 535
MEDIUMINT3±8.4 млн0 … 16.7 млн
INT4±2.1 млрд0 … 4.3 млрд
BIGINT8±9.2 квинтиллиона0 … 18.4 квинтиллиона

UNSIGNED — только положительные. Полезно для id, счётчиков.

Автоинкремент:

id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY

Правило:

  • id — BIGINT UNSIGNED AUTO_INCREMENT.
  • Флаги — TINYINT(1) (MySQL не имеет настоящего BOOLEAN, только алиас на TINYINT(1)).
  • Кол-во — INT UNSIGNED.

⚠️ INT(11) — не ограничивает значения, только ширину вывода.

Дробные числа

ТипОсобенности
FLOAT4 байта, ~7 знаков
DOUBLE8 байт, ~15 знаков
DECIMAL(M,D)точно, M — всего цифр, D — после точки

Деньги — только DECIMAL:

price DECIMAL(10, 2) NOT NULL
-- 12345678.90

⚠️ FLOAT / DOUBLE для денег — потеря точности.

Строки

ТипОсобенности
CHAR(n)фикс. длина, 0–255, добивает пробелами
VARCHAR(n)перем. длина, до 65 535
TINYTEXTдо 255 байт
TEXTдо 65 535 байт
MEDIUMTEXTдо 16 MB
LONGTEXTдо 4 GB
BLOBбинарные (аналоги TEXT)
VARBINARYбинарные

Правило:

  • CHAR — фикс. (страны, коды).
  • VARCHAR(n) — обычные строки (email, имя).
  • TEXT — длинные (описание, статьи).

Индексы:

  • VARCHAR — индексируется целиком (или префиксом).
  • TEXT — только префиксный индекс: INDEX (bio(100)).

Дата и время

ТипОсобенности
DATEтолько дата, 3 байта
TIMEтолько время
DATETIMEдата и время, без таймзоны, 8 байт
TIMESTAMPдата и время, в UTC, 4 байта
YEARгод

Ключевое отличие:

  • DATETIME — хранит как есть.
  • TIMESTAMP — конвертирует в UTC при записи и обратно в таймзону сессии при чтении.

⚠️ TIMESTAMP — диапазон 1970–2038. Для дат вне — только DATETIME.

Правило:

  • created_at, updated_at — TIMESTAMP.
  • Даты рождения, будущие события — DATETIME.

Значения по умолчанию:

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

Что даёт: автообновление без триггеров.

Миллисекунды:

created_at DATETIME(3)   -- с точностью до миллисекунд
created_at DATETIME(6)   -- микросекунды

BOOLEAN

MySQL не имеет настоящего BOOLEAN.

is_active BOOLEAN
-- на самом деле: TINYINT(1)

Значения:

  • 0 — false.
  • 1 — true.
  • NULL — неизвестно.

Проверка:

SHOW COLUMNS FROM users LIKE 'is_active';
-- tinyint(1)

ENUM

status ENUM('pending', 'paid', 'shipped', 'cancelled') NOT NULL DEFAULT 'pending'

Плюсы:

  • Валидация на уровне БД.
  • Компактное хранение (1–2 байта).

Минусы:

  • Тяжело менять — ALTER TABLE пересоздаёт.
  • Нельзя удалить значение просто.
  • Порядок важен (для ORDER BY).

Правило: для изменяемых — lookup-таблица. Для стабильных — ENUM.

SET

Набор значений:

tags SET('php', 'laravel', 'mysql', 'devops')

Хранит до 64 значений в битовой маске.

⚠️ Не нормализовано, редко используется. Лучше — связующая таблица или JSON.

JSON

data JSON

Функции:

-- Достать поле
SELECT data->>'$.theme' FROM users;
SELECT JSON_EXTRACT(data, '$.theme') FROM users;

-- Проверить наличие
SELECT * FROM users WHERE JSON_CONTAINS_PATH(data, 'one', '$.theme');

-- Вложенный поиск
SELECT * FROM users WHERE data->>'$.prefs.lang' = 'ru';

-- Установить
UPDATE users SET data = JSON_SET(data, '$.theme', 'dark') WHERE id = 1;

Индекс (MySQL 8.0.17+):

CREATE INDEX idx_users_theme
ON users ((CAST(data->>'$.theme' AS CHAR(20))));

-- Или виртуальная колонка
ALTER TABLE users
ADD COLUMN theme VARCHAR(20)
GENERATED ALWAYS AS (data->>'$.theme') VIRTUAL;

CREATE INDEX idx_users_theme ON users (theme);

Когда:

  • Динамические поля.
  • Метаданные.
  • Не для бизнес-логики — там колонки.

⚠️ JSON в MySQL слабее, чем JSONB в PostgreSQL. Нет GIN-индексов, нет оператора ?.

BLOB и бинарные

avatar BLOB,
data VARBINARY(255)

⚠️ Файлы — на диск или в S3, не в БД. Исключение — миниатюры, хеши.

Гео-типы

location POINT,
area POLYGON

Индексы:

CREATE SPATIAL INDEX idx_location ON places (location);

Функции:

ST_Distance_Sphere(point1, point2)
ST_Contains(polygon, point)

Пример: таблица пользователей

CREATE TABLE users (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(100) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    balance DECIMAL(12, 2) NOT NULL DEFAULT 0.00,
    metadata JSON,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    PRIMARY KEY (id),
    UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Приведение типов

SELECT CAST('123' AS UNSIGNED);
SELECT CAST(123 AS CHAR);
SELECT CONVERT('2026-01-01', DATE);

⚠️ MySQL приводит типы неявно — источник багов.

WHERE varchar_col = 123   -- приведёт varchar к числу

Правило: типы в сравнении — одинаковые. Не полагаться на неявное приведение.

Размер строки

Лимит 65 535 байт на строку (без TEXT/BLOB).

Проблема:

-- Ошибка, если VARCHAR(20000) + VARCHAR(20000) + ...
CREATE TABLE t (
    a VARCHAR(20000) utf8mb4,  -- 80 KB
    b VARCHAR(20000) utf8mb4   -- 80 KB
);

Решение: TEXT для длинных, вынести в отдельную таблицу.

⚠️ utf8mb4 = 4 байта на символ. VARCHAR(255) = 1020 байт.

Частые ошибки

1. INT(11) думают = 11 цифр:

Это ширина вывода, не ограничение

Решение: для ограничения — CHECK или DECIMAL.

2. FLOAT для денег:

0.1 + 0.2 != 0.3

Решение: DECIMAL(10,2).

3. VARCHAR(255) для emoji:

utf8 (не mb4) режет 4-байтовые символы

Решение: utf8mb4 везде.

4. TIMESTAMP для дат > 2038:

Y2K38 проблема

Решение: DATETIME.

5. AUTO_INCREMENT на INT при большом объёме:

Переполнение на 2.1 млрд

Решение: BIGINT UNSIGNED.

6. TEXT без префикса в индексе:

CREATE INDEX idx ON t (body);   -- ошибка

Решение: CREATE INDEX idx ON t (body(100));.

7. ENUM для изменяемых значений:

ALTER TABLE пересоздаёт таблицу

Решение: lookup-таблица.

8. JSON там, где нужны колонки:

Нет FK, слабые индексы

Решение: бизнес-данные — колонки.

Практика

1. Создайте таблицу products:

CREATE TABLE products (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    sku VARCHAR(64) NOT NULL,
    name VARCHAR(255) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INT UNSIGNED NOT NULL DEFAULT 0,
    attributes JSON,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (id),
    UNIQUE KEY uq_products_sku (sku),
    KEY idx_products_price (price)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. Вставьте товар с JSON-атрибутами:

INSERT INTO products (sku, name, price, attributes)
VALUES ('SKU001', 'Товар', 100.50, '{"color": "red", "size": "M"}');

3. Запросите по JSON:

SELECT * FROM products WHERE attributes->>'$.color' = 'red';

4. Проверьте тип is_active:

SHOW COLUMNS FROM products LIKE 'is_active';

Итог

  • id — BIGINT UNSIGNED AUTO_INCREMENT.
  • Деньги — DECIMAL.
  • Строки — VARCHAR (обычные), TEXT (длинные).
  • Дата — TIMESTAMP (created/updated), DATETIME (остальное).
  • BOOLEAN = TINYINT(1).
  • JSON — есть, но слабее PostgreSQL.
  • ENUM — для стабильных.
  • utf8mb4 — всегда.
  • INT(11) — не ограничение.

Дальше — создание таблиц.