<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
БАЗИ ДАНИХ · РУШІЇ Й СУБД

InnoDB, MyISAM і MEMORYMySQL storage engines

У MySQL рядки таблиці читає й пише рушій зберігання, і в кожної таблиці він свій. Від нього залежить, де лежать дані, кого чекає UPDATE, чи спрацює ROLLBACK і що лишиться від таблиці після збою.

18 хв читання Junior → Senior
01 · ЩО ТАКЕ РУШІЙ

Один сервер, різні способи зберігати таблицю

MySQL складається з двох шарів. Сервер розбирає SQL, будує план і виконує його, а самі рядки читає й пише . Рушій задають кожній таблиці окремо, рядком ENGINE=InnoDB у CREATE TABLE, тож в одній базі можуть жити таблиці з різною поведінкою. Від рушія залежать транзакції, рівень блокування, зовнішні ключі й те, що буде після збою.

СЛОВНИКРушій зберігання. Частина MySQL, яка фізично зберігає рядки й індекси таблиці. Список доступних показує SHOW ENGINES, рушій конкретної таблиці видно в SHOW CREATE TABLE.

З MySQL 5.5 рушій за замовчуванням InnoDB. До того ним був MyISAM, і в старих базах його таблиці ще трапляються. InnoDB зберігає таблицю як : рядки лежать прямо в листках B-tree первинного ключа. MyISAM, як і PostgreSQL, кладе рядки в , окремо від індексів.

СЛОВНИККластерний індекс. Індекс, у листках якого лежать самі рядки. В InnoDB це первинний ключ: таблиця і є цим деревом, а вторинні індекси зберігають значення PK як адресу рядка.
СЛОВНИККупа. Файл з рядками в порядку вставки, без сортування. Індекси лежать окремо й зберігають фізичну адресу рядка: зсув у файлі .MYD у MyISAM, ctid (сторінка, позиція) у PostgreSQL.

Надійність InnoDB тримається на . Зміну спершу дописують у журнал, і COMMIT повертається, коли журнал на диску; файли таблиці оновлюються пізніше. Після збою InnoDB програє журнал і відкочує незакомічені транзакції. У MyISAM журналу немає: перерваний UPDATE лишає таблицю наполовину зміненою, і нижче видно це на справжньому збої.

СЛОВНИКRedo log. Журнал змін InnoDB. Запис у нього послідовний і дешевий, тож COMMIT не чекає, поки оновляться сторінки таблиці. Після збою за журналом відновлюють те, що встигли закомітити. У PostgreSQL ту саму роль грає WAL.

PostgreSQL і SQLite мають по одному способу зберігання, тож вибір рушія це тема MySQL і MariaDB. Але наслідки кластерного індексу для вибору первинного ключа варто знати всім, хто пише міграції.

02 · ХТО КОГО ЧЕКАЄ

Блокування рядка проти блокування таблиці

Найпомітніша на живому сайті різниця. InnoDB блокує лише рядки, які змінює, MyISAM на час запису блокує всю таблицю. Поки на таблиці лічильника переглядів іде довгий UPDATE, на MyISAM у черзі стоїть кожен інший запис, навіть у зовсім інший рядок.

СЦЕНАРІЙ
Сесія A Сесія B t0
BEGIN
BEGIN
t1
UPDATE … WHERE id = 1
UPDATE … WHERE id = 2
t2
🔒 рядок 1
🔒 рядок 2
t3
COMMIT
COMMIT
ПАРАЛЕЛЬНОInnoDB блокує лише рядки, які змінює. Дві сесії оновлюють різні рядки й не заважають одна одній: UPDATE у B виконується одразу, поки A ще не закомітила.
Сесія A Сесія B t0
UPDATE … WHERE id = 1 (3 с)
t1
🔒 уся таблиця
UPDATE … WHERE id = 2
t2
🔒 уся таблиця
⏳ Waiting for table level lock
t3
готово
UPDATE виконано
ЧЕРГАMyISAM блокує на запис усю таблицю. Поки довгий UPDATE у A не закінчився, B чекає, хоча її рядок ніхто не чіпає. BEGIN і ROLLBACK тут нічого не дають: транзакцій у MyISAM немає.
SHOW PROCESSLIST · MySQL 8.0
state                          info
User sleep                     UPDATE users_m SET visits = visits + SLEEP(3) WHERE id = 1
Waiting for table level lock   UPDATE users_m SET visits = visits + 1 WHERE id = 2

Сценарії підготовлені заздалегідь. Стан Waiting for table level lock знято з SHOW PROCESSLIST на MySQL 8.0.46.

03 · ПОРІВНЯННЯ

InnoDB, MyISAM і MEMORY поруч

Виберіть два-три рушії. PostgreSQL тут для порівняння: у нього один спосіб зберігання, купа з окремими індексами, як у MyISAM, але з транзакціями й журналом, як в InnoDB.

Порівняння рушіїв MySQL і PostgreSQL
InnoDB MyISAM MEMORY PostgreSQL
Транзакції ✓ ✗ ROLLBACK ігнорує ✗ ✓
Блокування на запис рядок уся таблиця уся таблиця рядок
Зовнішні ключі ✓ ✗ мовчки ігнорує ✗ ✓
Після збою ✓ redo log і відкат ✗ marked as crashed ✗ порожня після рестарту ✓ WAL
Де лежать рядки у листках PRIMARY KEY .MYD окремо, індекси в .MYI в оперативній памʼяті, на диску лише опис купа, індекси окремо
COUNT(*) без WHERE читає найменший індекс ✓ лічильник у метаданих ✓ лічильник у метаданих перебирає таблицю або індекс
Повнотекстовий індекс ✓ FULLTEXT ✓ FULLTEXT ✗ ✓ tsvector і GIN
Коли брати Майже завжди. Рушій за замовчуванням з MySQL 5.5, на ньому тримаються транзакції, зовнішні ключі й Laravel-міграції. Лише старі таблиці, які ще не перевели. Нових на ньому не створюйте. Невеликі довідники й проміжні дані, які легко відновити з іншого джерела. Не для даних, які шкода втратити. Для порівняння: один спосіб зберігання для всіх таблиць, а тимчасові дані без журналу дає UNLOGGED.
СХЕМА ЗБЕРІГАННЯ
дерево PRIMARY KEY
листок PK 1–49
id дані рядка
10 Kvitka Soft, …
20 Dnipro Labs, …
листок PK 50+
id дані рядка
50 Lviv Pay, …
60 Odesa Cloud, …

Дані лежать прямо в листках дерева первинного ключа, у порядку ключа. Пошук за PK одразу дає рядок. Вторинний індекс зберігає значення PK, і пошук за ним проходить два дерева, тому PK варто робити коротким і зростаючим.

companies.MYI · індекс
id адреса
10 @0x00
20 @0x8f
50 @0x1a
адреса →
companies.MYD · дані
@ рядок
0x00 Kvitka Soft, …
0x1a Lviv Pay, …
0x8f Dnipro Labs, …

Індекс в одному файлі, дані в іншому, у порядку вставки. Індекс зберігає адресу рядка у файлі даних, і первинний ключ тут нічим не відрізняється від будь-якого іншого індексу.

companies_pkey · індекс
id ctid
10 (0,1)
20 (0,3)
50 (0,2)
ctid →
companies · купа
ctid рядок
(0,1) Kvitka Soft, …
(0,2) Lviv Pay, …
(0,3) Dnipro Labs, …

Схоже на MyISAM: купа й окремі індекси, які ведуть на ctid, тобто номер сторінки й позицію в ній. UPDATE у PostgreSQL пише нову версію рядка з новим ctid, а стару прибирає VACUUM.

Перевірено на MySQL 8.0.46: зовнішній ключ на MyISAM мовчки перетворюється на звичайний KEY, ROLLBACK на MyISAM дає попередження 1196 і нічого не відкочує, MEMORY відмовляє в TEXT і JSON помилкою 1163, а після рестарту сервера порожня. COUNT(*) без WHERE на MyISAM і MEMORY EXPLAIN показує як Select tables optimized away, на InnoDB як перебір найменшого індексу.

04 · ПЕРВИННИЙ КЛЮЧ

Кластерний індекс любить зростаючий ключ

В InnoDB таблиця відсортована за первинним ключем. Зростаючий ключ дописує рядки в кінець дерева, і сторінки заповнюються щільно. Випадковий ключ вставляє кожен рядок у середину: сторінки діляться, лишаються напівпорожніми, і в памʼяті доводиться тримати все дерево, а не лише його край.

Друга ціна прихована у вторинних індексах. Кожен із них зберігає значення PK, тож CHAR(36) замість BIGINT робить важчим кожен індекс таблиці.

PRIMARY KEY таблиці vacancies в InnoDB
id BIGINT UNSIGNED AUTO_INCREMENT найкраще Вісім байтів, кожне нове значення більше за попереднє. 500 000 рядків вставились за 5,3 с, файл таблиці 52 МБ.
id BINARY(16) · UUID_TO_BIN(UUID(), 1) або UUIDv7 працює Значення ростуть у часі, вставка теж у кінець дерева: 6,5 с і 64 МБ. Ключ удвічі ширший за BIGINT, але його можна згенерувати ще до INSERT, у застосунку.
id CHAR(36) · випадковий UUID v4 дорого Вставка в випадкові місця дерева: 17,4 с і 120 МБ на тих самих рядках. Індекс на company_id важчий утричі, 41,6 МБ проти 16,5 МБ, бо носить 36 символів ключа в кожному записі.
email VARCHAR(255) PRIMARY KEY обережно Природний ключ довгий і змінюється. Зміна PK в InnoDB переносить рядок в інше місце дерева й переписує його запис у кожному вторинному індексі.
без PRIMARY KEY ні InnoDB однаково кластеризує таблицю, але за прихованим ключем GEN_CLUST_INDEX, яким не можна шукати. sql_require_primary_key = ON забороняє такі таблиці: ERROR 3750.

Час і розмір виміряно на MySQL 8.0.46: 500 000 рядків одним INSERT … SELECT у таблицю з вторинним індексом на company_id. Розмір це файл .ibd.

05 · ЦІНА КЛЮЧА

Ті самі рядки, різний ключ

Перевірка з матриці вище на справжніх базах: по 500 000 вакансій у таблицях, які відрізняються лише первинним ключем. В InnoDB data_length це сама таблиця, тобто дерево первинного ключа, а index_length вторинні індекси.

У PostgreSQL порядок вставки купі байдужий, тож таблиця більша лише на ширину ключа. Але індекс первинного ключа на випадкових UUID теж росте вшир і вставка повільніша вдвічі.

MySQL 8.0 PostgreSQL 16
1CREATE TABLE pk_bigint (
2 id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
3 company_id INT, title VARCHAR(200), KEY (company_id));
4CREATE TABLE pk_uuid4 (
5 id CHAR(36) PRIMARY KEY,
6 company_id INT, title VARCHAR(200), KEY (company_id));
7-- pk_uuid_ord: id BINARY(16), UUID_TO_BIN(UUID(), 1)
8
9SELECT table_name,
10 ROUND(data_length / 1048576, 1) AS data_mb,
11 ROUND(index_length / 1048576, 1) AS index_mb
12FROM information_schema.tables
13WHERE table_name LIKE 'pk_%';
-- INSERT 500 000 рядків: 5,3 с · 17,4 с · 6,5 с
table_name    data_mb   index_mb
pk_bigint        27.6       16.5
pk_uuid4         67.8       41.6
pk_uuid_ord      31.6       22.6

Випадковий UUID удвічі збільшив і саму таблицю, і вторинний індекс. Числа з information_schema це оцінка за статистикою, файли .ibd на диску: 52, 120 і 64 МБ.

1CREATE TABLE pk_bigint (id bigserial PRIMARY KEY,
2 company_id int, title varchar(200));
3CREATE TABLE pk_uuid4 (id uuid PRIMARY KEY,
4 company_id int, title varchar(200));
5-- id = gen_random_uuid() для pk_uuid4
6
7SELECT relname, pg_size_pretty(pg_relation_size(oid))
8FROM pg_class
9WHERE relname LIKE 'pk_%';
-- INSERT 500 000 рядків: 2,1 с · 4,7 с
relname          pg_size_pretty
pk_bigint        32 MB
pk_bigint_pkey   11 MB
pk_uuid4         36 MB
pk_uuid4_pkey    19 MB

Купа майже однакова, різниця в індексі первинного ключа. Тип uuid займає 16 байтів, а не 36 символів, як CHAR(36) у MySQL. Функція uuidv7() зʼявилась у PostgreSQL 18, до того зростаючий UUID генерують у застосунку.

06 · ПІСЛЯ ЗБОЮ

Що лишається від перерваного UPDATE

Дві таблиці по два мільйони рядків з hits = 0, InnoDB і MyISAM. Обидві запускають UPDATE … SET hits = hits + 1 на всю таблицю, а через 2,5 секунди сервер падає по kill -9. Після запуску InnoDB відкотила незакомічений UPDATE повністю, а в MyISAM лишилось те, що встигли записати: п’ята частина рядків змінена, решта ні.

Така таблиця MyISAM формально ціла, CHECK TABLE її пропускає. Зіпсовані тут дані, і жоден журнал не скаже, які рядки встигли оновитись.

MySQL 8.0 PostgreSQL 16
1UPDATE views_innodb SET hits = hits + 1; -- сесія 1
2UPDATE views_myisam SET hits = hits + 1; -- сесія 2
3-- через 2,5 с: kill -9 mysqld, потім запуск
4
5SELECT hits, COUNT(*) FROM views_innodb GROUP BY hits;
6SELECT hits, COUNT(*) FROM views_myisam GROUP BY hits;
-- views_innodb
hits   COUNT(*)
0      2000000
-- views_myisam
hits   COUNT(*)
1      400142
0      1599858

-- error.log
[ERROR] [MY-013134] [Server] Table './lab/views_myisam'
  is marked as crashed and should be repaired

InnoDB відновлювалась 11 секунд: програвала redo log і відкочувала UPDATE. MyISAM лише позначила таблицю як пошкоджену, і сервер перевірив її при першому зверненні, бо в Ubuntu myisam_recover_options = BACKUP.

1UPDATE views_pg SET hits = hits + 1;
2-- через 2,5 с: pg_ctl stop -m immediate, потім запуск
3
4SELECT hits, count(*) FROM views_pg GROUP BY hits;
hits | count
-----+---------
   0 | 2000000

LOG:  database system was not properly shut down;
      automatic recovery in progress
LOG:  redo starts at 0/1EEFD0A8
LOG:  redo done at 0/27C91FA0 … elapsed: 0.89 s
LOG:  database system is ready to accept connections

PostgreSQL програє WAL і бачить, що транзакція не закомічена: нові версії рядків лишаються в таблиці, але невидимі, їх прибере VACUUM. Відкочувати нічого не треба, тому відновлення швидке.

07 · ДАНІ В ПАМʼЯТІ

Швидко і до першого рестарту

MEMORY тримає таблицю в оперативній памʼяті, на диску лишається лише її опис. Індекс за замовчуванням HASH: точний пошук миттєвий, а діапазон чи ORDER BY по ньому не працюють. Розмір обмежує max_heap_table_size, 16 МБ за замовчуванням, і TEXT, BLOB чи JSON у такій таблиці не бувають.

Внутрішні тимчасові таблиці для GROUP BY і сортувань MySQL 8.0 тримає вже не в MEMORY, а в TempTable. Тож MEMORY лишилась для рідкісних випадків: довідник, який завантажують при старті, або проміжні дані важкого звіту. У PostgreSQL ту саму нішу займають UNLOGGED-таблиці, але працюють вони інакше.

MySQL 8.0 PostgreSQL 16
1CREATE TABLE rates (id INT PRIMARY KEY, code CHAR(3),
2 rate DECIMAL(10,4)) ENGINE=MEMORY;
3
4CREATE TABLE notes (id INT PRIMARY KEY, body TEXT)
5 ENGINE=MEMORY;
6
7-- max_heap_table_size = 64 КБ, вставка 10 000 рядків
8INSERT INTO rates SELECT …;
9SELECT COUNT(*) FROM rates;
10
11EXPLAIN SELECT * FROM rates WHERE id BETWEEN 10 AND 20;
12-- рестарт сервера
13SELECT COUNT(*) FROM rates;
ERROR 1163 (42000): The used table type doesn't support BLOB/TEXT columns
ERROR 1114 (HY000): The table 'rates' is full
COUNT(*): 1088
type: ALL   key: NULL   rows: 1088   Extra: Using where
COUNT(*): 0

INSERT, який не вліз, не відкотився: 1088 рядків лишились, бо транзакцій у MEMORY немає. Діапазон за HASH-індексом став перебором. Індекс USING BTREE це виправляє, але рестарт однаково спорожнить таблицю.

1CREATE UNLOGGED TABLE rates (code char(3) PRIMARY KEY,
2 rate numeric(10,4));
3INSERT INTO rates VALUES ('USD', 41.5), ('EUR', 44.8);
4
5-- pg_ctl restart -m fast
6SELECT count(*) FROM rates;
7-- pg_ctl stop -m immediate, потім запуск
8SELECT count(*) FROM rates;
count: 2   -- після звичайного рестарту
count: 0   -- після збою

UNLOGGED-таблиця лежить на диску, але не пише WAL, тому запис швидший. Звичайний рестарт її не чіпає, а після збою PostgreSQL її очищає: без журналу він не може довести, що вона ціла. На репліки вона теж не потрапляє.

08 · РУШІЙ У LARAVEL

Рушій і ключ у міграції

Зазвичай рушій у міграціях не згадують зовсім: у config/database.php для mysql стоїть engine => null, і таблиця отримує default_storage_engine сервера, тобто InnoDB. Явно рушій пишуть, коли переводять старі таблиці або коли сервер налаштований інакше.

php create_vacancies_table.php Рушій таблиці
1Schema::create('vacancies', function (Blueprint $table) {
2 $table->engine('InnoDB'); // ENGINE = InnoDB у CREATE TABLE
3 $table->id();
4 $table->string('title');
5});
ЩО ПЕРЕВІРЯЄ
engine() дописує ENGINE до CREATE TABLE лише на MySQL і MariaDB, інші бази його ігнорують. Те саме для всіх таблиць робить engine у config/database.php.
КОЛИ БРАТИ
Коли default_storage_engine сервера не InnoDB або треба підкреслити, що таблиця свідомо на іншому рушії.
php create_vacancies_table.php Зростаючий ключ
1$table->id(); // bigint unsigned auto_increment primary key
2
3// або UUID, який генерує застосунок:
4$table->uuid('id')->primary(); // char(36) у MySQL
5// і в моделі: use HasUuids; → Str::uuid7()
ЩО ПЕРЕВІРЯЄ
id() дає BIGINT, найкращий кластерний ключ. HasUuids у Laravel 13 генерує UUIDv7: на початку час, тож вставка теж іде в кінець дерева. Старий трейт HasVersion4Uuids бере Str::orderedUuid(), який теж починається з часу. Випадковий v4 дає лише Str::uuid().
КОЛИ БРАТИ
id() за замовчуванням. UUID, коли ідентифікатор потрібен до запису в базу або його не можна вгадати в URL.
php create_vacancies_table.php Переведення з MyISAM
1public function up(): void
2{
3 // Перебудовує таблицю повністю, запис чекає до кінця.
4 DB::statement('ALTER TABLE legacy_stats ENGINE = InnoDB');
5}
ЩО ПЕРЕВІРЯЄ
Зміна рушія це повне копіювання таблиці: ALGORITHM=INPLACE тут не підтримується, MySQL відповідає ERROR 1845. На великій таблиці це хвилини, протягом яких запис стоїть.
КОЛИ БРАТИ
Старі таблиці MyISAM. Великі переводять у вікно обслуговування або через pt-online-schema-change чи gh-ost.
php create_vacancies_table.php Тимчасова таблиця
1Schema::create('import_rows', function (Blueprint $table) {
2 $table->temporary(); // CREATE TEMPORARY TABLE
3 $table->string('source_id');
4 $table->json('payload');
5});
ЩО ПЕРЕВІРЯЄ
Тимчасова таблиця живе до кінця зʼєднання й видна лише йому. У MySQL 8.0 вона InnoDB (default_tmp_storage_engine), тож JSON у ній можна, на відміну від MEMORY.
КОЛИ БРАТИ
Проміжні дані імпорту чи звіту в межах однієї команди. Між запитами вони не живуть.
09 · ЯК У PHP UKRAINE

Сайт на PostgreSQL, міграції готові до InnoDB

Сайт працює на PostgreSQL 17 у контейнері з docker-compose.yml, тож рушіїв MySQL на проді немає. Але міграції написані так, що на MySQL отримали б хороший кластерний ключ. $table->id() на PostgreSQL дає bigserial з окремим індексом _pkey, а на InnoDB дав би зростаючий BIGINT, найкращий кластерний ключ. З нього починаються майже всі таблиці, а рядкові первинні ключі мають лише службові: cache і cache_locks з ключем key, sessions і job_batches з випадковим id, password_reset_tokens з email і packages з назвою пакета. На InnoDB саме вони вставлялися б у випадкові місця дерева, але це невеликі таблиці.

php 0001_01_01_000001_create_cache_table.php фрагмент
1Schema::create('cache', function (Blueprint $table) {
2 $table->string('key')->primary();← рядковий PK
3 $table->mediumText('value');
4 $table->bigInteger('expiration')->index();← прибирання простроченого
5});
6
7Schema::create('cache_locks', function (Blueprint $table) {
8 $table->string('key')->primary();
9 $table->string('owner');
10 $table->bigInteger('expiration')->index();
11});
config/database.phpПідключення mysql має engine => null: рушій бере default_storage_engine сервера. За замовчуванням (DB_CONNECTION) стоїть sqlite, але .env.example і deploy/.env.docker.example перемикають на pgsql.
database/migrations/2026_09_02_000002_create_vacancies_table.php$table->id() і жодного UUID: вакансії, компанії й відгуки мають зростаючі ключі.
docker-compose.ymlСервіс db це образ postgres:17-alpine з томом phpukraine-db. Резервну копію щодня робить pg_dump з deploy/server-setup.sh.
10 · ТИПОВІ ПОМИЛКИ

Що ламається через рушій

✕ 1

Транзакція на таблиці MyISAM

DB::transaction() на MyISAM нічого не гарантує. ROLLBACK повертає лише попередження 1196, Some non-transactional changed tables couldn't be rolled back, а вставлені рядки лишаються.

Натомість: Перевірте рушій: SELECT table_name, engine FROM information_schema.tables WHERE table_schema = DATABASE(). Таблиці MyISAM переведіть на InnoDB.

✕ 2

Зовнішній ключ, якого немає

MyISAM приймає FOREIGN KEY без помилки й без попередження, але створює звичайний індекс. Каскадне видалення не працює, сирітські рядки накопичуються.

Натомість: Після міграції дивіться SHOW CREATE TABLE: справжній ключ має рядок CONSTRAINT … FOREIGN KEY. Обидві таблиці звʼязку мають бути InnoDB.

✕ 3

Випадковий UUID як первинний ключ InnoDB

Str::uuid() дає UUID v4. Кожна вставка в випадкове місце дерева: таблиця й індекси вдвічі більші, вставка в рази повільніша, і в buffer pool доводиться тримати все дерево.

Натомість: id() або зростаючий UUID: HasUuids генерує UUIDv7, Str::orderedUuid() теж починається з часу. Зберігайте його в BINARY(16), якщо таблиця велика.

✕ 4

MEMORY як кеш, який не можна втратити

Таблиця MEMORY спорожніє після кожного рестарту чи падіння сервера, а при переповненні частина INSERT лишається записаною без відкату.

Натомість: Для кешу беріть Cache у Laravel з драйвером database чи Redis, для проміжних даних тимчасову таблицю InnoDB.

✕ 5

COUNT(*) на великій таблиці InnoDB

Після переходу з MyISAM SELECT COUNT(*) FROM views раптом повільний: MyISAM брав число з метаданих, InnoDB рахує рядки, бо кожна транзакція може бачити свою кількість. На двох мільйонах рядків 0,36 с проти 0,01 с.

Натомість: Якщо потрібна приблизна цифра, беріть table_rows з information_schema.tables. Якщо точна й часто, тримайте лічильник в окремій таблиці або в кеші.

11 · СЛОВНИК
Рушій зберігання
Частина MySQL, яка фізично зберігає рядки й індекси таблиці. Список доступних показує SHOW ENGINES, рушій конкретної таблиці видно в SHOW CREATE TABLE.
Кластерний індекс
Індекс, у листках якого лежать самі рядки. В InnoDB це первинний ключ: таблиця і є цим деревом, а вторинні індекси зберігають значення PK як адресу рядка.
Купа
Файл з рядками в порядку вставки, без сортування. Індекси лежать окремо й зберігають фізичну адресу рядка: зсув у файлі .MYD у MyISAM, ctid (сторінка, позиція) у PostgreSQL.
Redo log
Журнал змін InnoDB. Запис у нього послідовний і дешевий, тож COMMIT не чекає, поки оновляться сторінки таблиці. Після збою за журналом відновлюють те, що встигли закомітити. У PostgreSQL ту саму роль грає WAL.
12 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з SQL →

Laravel виконає ROLLBACK, але MyISAM транзакцій не підтримує: усе, що встигли записати, лишиться в таблиці, а MySQL поверне лише попередження 1196. Помилки не буде, тож це легко не помітити.

InnoDB зберігає рядки в листках дерева первинного ключа, відсортованими за ним. Випадкове значення вставляє рядок у середину дерева: сторінки діляться й лишаються напівпорожніми, а кешувати доводиться все дерево. До того ж кожен вторинний індекс зберігає PK, і широкий ключ робить важчими всі індекси. Рішення: BIGINT AUTO_INCREMENT або зростаючий UUID, наприклад v7.

Листок первинного ключа і є рядком, тож пошук за PK проходить одне дерево. Вторинний індекс зберігає значення PK, і після нього база спускається ще й деревом первинного ключа. Якщо запиту вистачає колонок вторинного індексу разом з PK, другий спуск не потрібен: це покриваючий індекс.

InnoDB програє redo log, щоб відновити закомічене, і відкотить незакомічене за undo log: таблиця буде такою, як до UPDATE. PostgreSQL програє WAL, а нові версії рядків від незакоміченої транзакції просто лишаться невидимими до VACUUM. MyISAM журналу не має: частина рядків буде оновлена, частина ні, а таблицю позначать як пошкоджену й перевірятимуть REPAIR TABLE чи автоматично через myisam_recover_options.