Дослідження швидкодії складеного первинного ключа в Postgres, MySQL та CockroachDB
Я довго працював з MySQL і за той час набув таке упередження стосовно повільності складеного первинного ключа (composite primary key), що уникав його використання навіть в Postgres.
Щоб позбутись упередження, я протестую на швидкодію вставки в наступні таблиці:
| Складений унікальний ключ | Складений первинний ключ |
|---|---|
CREATE TABLE user_favorite_companies_v1 ( id SERIAL, user_id INT NOT NULL, company_id INT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, favorite_state BOOLEAN NOT NULL, PRIMARY KEY (id), UNIQUE (user_id, company_id), FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (company_id) REFERENCES companies (id) ); | CREATE TABLE user_favorite_companies_v2 ( user_id INT NOT NULL, company_id INT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, favorite_state BOOLEAN NOT NULL, PRIMARY KEY (user_id, company_id), FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (company_id) REFERENCES companies (id) ); |
В кожну таблицю буду робити 20 вставок по мільйону рядків, результати виведу на графіку.
Вставка мільйонів на голому SQL в MySQL
Щоб протестувати перебудову індексу, вставлятиму перехрещуванням.
Для початку створимо таблиці й заповнимо їх даними, на основі яких будуватимемо вставки.
CREATE TABLE users ( id INT NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id) ) ENGINE = InnoDB; CREATE TABLE companies ( id INT NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id) ) ENGINE = InnoDB;
SET SESSION cte_max_recursion_depth = 1000000; INSERT INTO users (id, created_at) SELECT number, '2022-11-05 12:00:00' FROM ( WITH RECURSIVE numbers AS ( SELECT 1 AS number UNION ALL SELECT number + 1 FROM numbers WHERE number < 1000 * 1000 ) SELECT * FROM numbers ) AS generate_series; INSERT INTO companies (id, created_at) SELECT number, '2022-11-05 12:00:00' FROM ( WITH RECURSIVE numbers AS ( SELECT 1 AS number UNION ALL SELECT number + 1 FROM numbers WHERE number < 10 * 1000 ) SELECT * FROM numbers ) AS generate_series;
CREATE TABLE user_favorite_companies_v1 ( id INT AUTO_INCREMENT, user_id INT NOT NULL, company_id INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, favorite_state BOOLEAN NOT NULL, PRIMARY KEY (id), UNIQUE (user_id, company_id), FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (company_id) REFERENCES companies (id) ) ENGINE = InnoDB; | CREATE TABLE user_favorite_companies_v2 ( user_id INT NOT NULL, company_id INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, favorite_state BOOLEAN NOT NULL, PRIMARY KEY (user_id, company_id), FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (company_id) REFERENCES companies (id) ) ENGINE = InnoDB; |
[ [1, 11, 21, 31, /* ..., */ 91], [2, 12, 22, 32, /* ..., */ 92], [3, 13, 23, 33, /* ..., */ 93], [4, 14, 24, 34, /* ..., */ 94], /* ..., */ [9, 19, 29, 39, /* ..., */ 99], ];
SET SESSION profiling_history_size = 100; SET SESSION cte_max_recursion_depth = 1000000; SET SESSION profiling = 1; TRUNCATE user_favorite_companies_v1;
INSERT INTO user_favorite_companies_v1 (user_id, company_id, created_at, updated_at, favorite_state) SELECT number, 1 + (0 + number * 20) % 10000, '2022-11-05 12:00:00', '2022-11-05 12:00:00', TRUE FROM ( WITH RECURSIVE numbers AS ( SELECT 1 AS number UNION ALL SELECT number + 1 FROM numbers WHERE number < 1000 * 1000 ) SELECT * FROM numbers ) AS generate_series ON DUPLICATE KEY UPDATE updated_at = '2022-11-05 12:00:00', favorite_state = TRUE; SELECT COUNT(*) FROM user_favorite_companies_v1 WHERE (user_id, company_id) IN ( SELECT number, 1 + (0 + number * 20) % 10000 FROM ( WITH RECURSIVE numbers AS ( SELECT 1 AS number UNION ALL SELECT number + 1 FROM numbers WHERE number < 1000 * 1000 ) SELECT * FROM numbers ) AS generate_series );
-- (1 + number * 20) ...
-- (2 + number * 20) ...
-- (3 + number * 20) ...
-- ...
-- (18 + number * 20) ...
INSERT INTO user_favorite_companies_v1 (user_id, company_id, created_at, updated_at, favorite_state) SELECT number, 1 + (19 + number * 20) % 10000, '2022-11-05 12:00:00', '2022-11-05 12:00:00', TRUE FROM ( WITH RECURSIVE numbers AS ( SELECT 1 AS number UNION ALL SELECT number + 1 FROM numbers WHERE number < 1000 * 1000 ) SELECT * FROM numbers ) AS generate_series ON DUPLICATE KEY UPDATE updated_at = '2022-11-05 12:00:00', favorite_state = TRUE; SELECT COUNT(*) FROM user_favorite_companies_v1 WHERE (user_id, company_id) IN ( SELECT number, 1 + (19 + number * 20) % 10000 FROM ( WITH RECURSIVE numbers AS ( SELECT 1 AS number UNION ALL SELECT number + 1 FROM numbers WHERE number < 1000 * 1000 ) SELECT * FROM numbers ) AS generate_series );
TRUNCATE user_favorite_companies_v1; SHOW PROFILES;
Результати на графіку для INSERT-ів:

Результати на графіку для SELECT-ів:
Якщо хочете самостійно протестувати, то ось репозиторій й команди:
up: docker-compose up -d mysql-v1-test: cat ./console/mysql.v1.sql | docker exec -i -e MYSQL_PWD=mypass cpkr_mysql mysql --database=yaaws --user=myuser --verbose | tee ./console/mysql.v1.output mysql-v2-test: cat ./console/mysql.v2.sql | docker exec -i -e MYSQL_PWD=mypass cpkr_mysql mysql --database=yaaws --user=myuser --verbose | tee ./console/mysql.v2.output down: docker-compose down
Вставка мільйонів на голому SQL в Postgres
Для Postgres буде схожий тест, але для наглядності додам порівняння з MySQL (щоб побачити повну картину, потрібно зменшити зум):| Postgres | MySQL |
|---|---|
\timing | SET SESSION profiling_history_size = 100; SET SESSION cte_max_recursion_depth = 1000000; SET SESSION profiling = 1; |
TRUNCATE user_favorite_companies_v1; | TRUNCATE user_favorite_companies_v1; |
INSERT INTO user_favorite_companies_v1 ( user_id, company_id, created_at, updated_at, favorite_state ) SELECT generate_series, 1 + (0 + generate_series * 20) % 10000, '2022-11-05 12:00:00', '2022-11-05 12:00:00', TRUE FROM generate_series(1, 1000 * 1000) ON CONFLICT (user_id, company_id) DO UPDATE SET updated_at = '2022-11-05 12:00:00', favorite_state = TRUE; | INSERT INTO user_favorite_companies_v1 ( user_id, company_id, created_at, updated_at, favorite_state ) SELECT number, 1 + (0 + number * 20) % 10000, '2022-11-05 12:00:00', '2022-11-05 12:00:00', TRUE FROM ( WITH RECURSIVE numbers AS ( SELECT 1 AS number UNION ALL SELECT number + 1 FROM numbers WHERE number < 1000 * 1000 ) SELECT * FROM numbers ) AS generate_series ON DUPLICATE KEY UPDATE updated_at = '2022-11-05 12:00:00', favorite_state = TRUE; |
SELECT COUNT(*) FROM user_favorite_companies_v1 WHERE (user_id, company_id) IN ( SELECT generate_series, 1 + (0 + generate_series * 20) % 10000 FROM generate_series(1, 1000 * 1000) ); | SELECT COUNT(*) FROM user_favorite_companies_v1 WHERE (user_id, company_id) IN ( SELECT number, 1 + (0 + number * 20) % 10000 FROM ( WITH RECURSIVE numbers AS ( SELECT 1 AS number UNION ALL SELECT number + 1 FROM numbers WHERE number < 1000 * 1000 ) SELECT * FROM numbers ) AS generate_series ); |
SHOW PROFILES; |

Результати на графіку для SELECT-ів:
Результати для складеного первинного ключа трохи кращі й руйнують мою упередженість після MySQL.
Вставка мільйонів на голому SQL в CockroachDB
CockroachDB сумісний з PostgreSQL тому одразу до результатів.Результати на графіку для INSERT-ів:
Результати на графіку для SELECT-ів:
Якщо ви з CockroachDB графіків змогли зробити висновки то напишіть.
Порівняння різних БД на одному графіку
Результати на графіку для INSERT-ів:
Результати на графіку для SELECT-ів:

Postgres в цьому порівнянні — найпрогнозованіша БД.
Епілог
Почалось з того, що мій колега з Казакстану додав в проєкт міграцію в Postgres зі складеним первинним ключем, як результат — вирішив розібратись в темі краще.Дослідження починав з тестів на Go на паралельну вставку, але коли таблиця менше мільйона записів, то відчути відмінності складно, тому спробував для 10 мільйонів на чистому SQL, а потім вже для 20 мільйонів.
Вакансії
Мені подобається концепція, коли технічні фахівці шукають технічних фахівців. Отже, є вакансії в сфері кібербезпеки від компанії Cossack Labs:- Application Security Engineer ($2500–5000)
- Mobile Application Security Engineer ($2500–5000)
30 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівPostgres то є кацапська СУБД і сама крінжова зі всіх RDBMS
Навіщо Характерник тестує кацапські продукти ?
То с клікхаус прибіжить то с постгрес.
Стосовно ClickHouse відповідав раніше.
Це вдвічі смішно, бо українці виступають бесплатними тестувальниками для московських продуктів. Чому у українських девелоперів не кортять руки створити щось українське, а тільки тестувати московське ?
Створіть
Звісно створюю. Тут наприклад своя СУБД під капотом.
І самі технології на три голови вище кацапських.
Написав 12 рядочків коду, отримав сайт з 8тис\сек запитів на секунду
(це вже с генерацією сторінок)
dou.ua/...rums/topic/41300/#2535483
А у вас тут безсмістовного бойлєрплейту на три ERP.
Ото нема чим людям зайнятися.
Кацапи написали постгрес мамонта, а українці це ще як канхфєтку жують.
Хм, а є якісь пруфи? Бо судячи з www.postgresql.org/community/contributors аж 2 розробника з московії, що не тяне на
. Хоча не знайшов інфи де зареєстрована The PostgreSQL Global Development Group, але не думаю, що в рфії.
Стосовно PostgreSQL:
То було давно і неправда. Основні контрибьютори там РФ. Вона же взята як рекомендована база для РФ
Це ти напевно переплутав з Postgres Pro
Деякий вклад вони точно зробили (на скільки я памятаю json і повнотекстовий пошук), але це ще не дає права називати PostgreSQL москальською розробкою. А на рахунок крінжовості вимушений підтримати )
а можна приклади?
Там всюди треш. Візьміть нормальну MS SQL або Oracle вони на дві голови краще спроєктовані. І річ навіть не в оптимизаторі запросів, то взагалі рокет сцайнц та патенти.
А навіть банальний синтаксис SQL. Таке враження що пять студентів першокурсників ліпили свою першу мову програмування в сильсько господарському пту. Чого тільки коштує постійно переписувати Stored Procedure в залежності що вона повертає. Поліморфний сінтаксис ? Нє, на расєі нє слішалі. Це як би було в паскалі придумали десять string контейнерів і кожен по різному декларується в залежності від строки що там є. І всі ці string1,string2 stringN погано комбінуються між собою. Йопвашу, навіщо я це згадав. Расчлєньонка в сінтаксісі. Буеее.
MS SQL перевіряв вже після написання статті, результати виявились кращими ніж у PostgreSQL.
А ви не пробували зробити id не автоінкрементом?
Є репозиторій де ви можете протестувати варіанти й написати свої висновки.
Думал показалось, ан нет.
DDL user_favorite_companies_v1 содержит
PRIMARY KEY (id),
UNIQUE (user_id, company_id),
DDL user_favorite_companies_v2
PRIMARY KEY (user_id, company_id),
Не особо заметно по дизайну. Что PRIMARY в user_favorite_companies_v1 дается бесплатно ?
Тест вставки в такие таблицы будет не об этом, а скорей о том что будет если навести сверху композитного ключа (user_id, company_id) еще один (id)
По хорошему PRIMARY KEY (id) нужно убрать.
Второе. Если оцениваешь импакт, то оценивай так чтоб небыло других переменных.
Зачем FK здесь ?
они что бесплатные в плане оверхеда ? Я бы еще понял если б у тебя составной FK был. А так это непонятная составляющая в замерах.
Убери ненужное и разница будет более заметная.
PS. По большому вся разница ключ составной vs сингл — в пейдж сплит, который будет происходить всегда но при длинных ключай это проходит чаще. Особенно если для btree нужно будет выстроить новый левел.
Отчасти это нивелируется page fill index (у каждого вендора свое название)
и дополнительной сортировке нон лидинг аттрибутов
Відмінності й так помітні, але якщо у вас є бажання щось довести то ось репозиторій.
Статья как и анализ пестрит допущениями о которых ни слова.
Эт ваш труд.
А мой комментарий о нем выше.
Clustered and Secondary Indexes:
Это называется таблица как куча. Не имеет к топику отношения.
Clustered and Secondary Indexes:
Якщо приберу PRIMARY KEY з user_favorite_companies_v1 то user_favorite_companies_v1 й user_favorite_companies_v2 будуть однакові.
Ще з Oracle і MS SQL цікаво порівняти.
Я хотів охопити більше баз сумісних з Postgres (YugabyteDB та Neon), але там потрібно було повозитись з налаштуваннями в docker-compose, тому облишив.
Чисто цікаво перевірити чи це раптом не ритуальні дії, коли перед імпортом в базу великого об’єму данних з початку дропають усі індекси і ключі з тих таблиць і які ведеться імпорт, а потім їх відновлюють. Тобто чи це не пов’язане з міфами про старі DBMS движки та догмою і фольклором. Тобто давно вже не є актуальним. Як бачимо у випадку MySQL з innodb — ні не догма, все так і є. А там далі ще багато чього є для реальних дослідів і бенчмарків. New SWl — Claud Spanner, Tarantool etc.
Імпорти великих об’ємів даних роблять зазвичай рідко тому дешевше по часу залишити ритуальні дії (або згенерований через СУБД файл), ніж витрачати2-3 години на дослідження.
Є ще що досліджувати для написання статей, але на то потрібно виділити пару повних робочих днів.
Спасибо за новые для меня базы.
Иногда кажется базы любят клепать также как новые JS фреймворки.
Вообще неясно зачем это все, если есть MS SQL Server
Дешевле?)
Работаю с MS SQL уже лет 19, и только недавно начал понимать, что он не так уж плох по сравнению.
Я починав свою кар’єру з Delphi та MSSQL тому досить швидко розібрався як запустити MSSQL в Docker:
Провів тести, MSSQL виявився найшвидшим по вставці (всі вставки до 11 секунд), але ще треба пошукати як зробити красиво SELECT в MSSQL:
При спробі написати правильно функцію GENERATE_SERIES й зберегти коментар на DOU то отримую http.status_code=403 від CloudFlare.