Slide 1

Slide 1 text

DmitryTsepelev Fall Saint P 2021 meetup Задача: нужно сбросить всем пользователям флаг подтверждения почты 1

Slide 2

Slide 2 text

DmitryTsepelev Fall Saint P 2021 meetup 2 UPDATE users SET email_conf i rmed = FALSE;

Slide 3

Slide 3 text

DmitryTsepelev Fall Saint P 2021 meetup 3 А что если данных много? 🤔 SELECT pg_size_pretty(pg_total_relation_size('users')); - - 200 Gb

Slide 4

Slide 4 text

DmitryTsepelev Fall Saint P 2021 meetup Обновляем в цикле 4 User.in_batches do |batch| batch.update_all(email_conf i rmed: false) end SELECT users.id FROM users WHERE users.id > 0 ORDER BY users.id ASC LIMIT 1000; UPDATE users SET email_conf i rmed = FALSE WHERE users.id IN ( . . . ); SELECT users.id FROM users WHERE users.id > 1000 ORDER BY users.id ASC LIMIT 1000; UPDATE users SET email_conf i rmed = FALSE WHERE users.id IN ( . . . );

Slide 5

Slide 5 text

DmitryTsepelev Fall Saint P 2021 meetup 5 Ой, места на диске не осталось SELECT pg_size_pretty(pg_total_relation_size('users')); - - 400 Gb

Slide 6

Slide 6 text

DmitryTsepelev Fall Saint P 2021 meetup 6 @dmitrytsepelev

Slide 7

Slide 7 text

Как и зачем MVCC мешает нам пользоваться базой данных и почему это не плохо

Slide 8

Slide 8 text

DmitryTsepelev Fall Saint P 2021 meetup 8

Slide 9

Slide 9 text

DmitryTsepelev Fall Saint P 2021 meetup БД и разработчики • есть таблички с данными; • данные можно изменять и запрашивать; • таблички можно объединять (JOINs) в запросах; • таблички связаны с помощью foreign keys; • есть constraints для валидации данных. 9

Slide 10

Slide 10 text

DmitryTsepelev Fall Saint P 2021 meetup Делаем биллинг def handle_payment(user, payment) payment.update(processed: true) subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; 10

Slide 11

Slide 11 text

ACID

Slide 12

Slide 12 text

DmitryTsepelev Fall Saint P 2021 meetup ACID • Atomicity • Consistency • Isolation • Durability 12

Slide 13

Slide 13 text

DmitryTsepelev Fall Saint P 2021 meetup Делаем биллинг def handle_payment(user, payment) payment.update(processed: true) subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end 13 А вдруг user.update выбросит исключение? 🤔

Slide 14

Slide 14 text

DmitryTsepelev Fall Saint P 2021 meetup Atomicity (доделываем биллинг) def handle_payment(user, payment) ActiveRecord : : Base.transaction do payment.update(processed: true) subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end end BEGIN; UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; COMMIT; 14

Slide 15

Slide 15 text

DmitryTsepelev Fall Saint P 2021 meetup Транзакция • переводит БД из одного состояния в другое; • может содержать несколько операций; • фиксируется только если все операции прошли успешно; • фиксация изменений происходит одновременно; • работает независимо от других транзакций. 15

Slide 16

Slide 16 text

DmitryTsepelev Fall Saint P 2021 meetup Спин–офф: локи def handle_payment(user, payment) user.with_lock do return if payment.reload.processed? # на случай если платеж уже запроцессили payment.update(processed: true) subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end end BEGIN; SELECT * FROM payments WHERE id = ? FOR UPDATE; UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; COMMIT; 16

Slide 17

Slide 17 text

DmitryTsepelev Fall Saint P 2021 meetup ACID • Atomicity • Consistency • Isolation • Durability 17

Slide 18

Slide 18 text

DmitryTsepelev Fall Saint P 2021 meetup Целостность и консистентность • целостные данные соответствуют ограничениям БД (UNIQUE, NOT NULL, …); • корректные данные соответствуют всем ограничениям приложения; • БД не всегда может гарантировать корректность. 18

Slide 19

Slide 19 text

DmitryTsepelev Fall Saint P 2021 meetup ACID • Atomicity • Consistency • Isolation • Durability 19

Slide 20

Slide 20 text

DmitryTsepelev Fall Saint P 2021 meetup Isolation 20 BEGIN; UPDATE payments SET processed = true WHERE id = 1; SELECT processed FROM payments WHERE id = 1; - - true COMMIT; BEGIN; SELECT processed FROM payments WHERE id = 1; - - false COMMIT; BEGIN; SELECT processed FROM payments WHERE id = 1; - - true

Slide 21

Slide 21 text

DmitryTsepelev Fall Saint P 2021 meetup ACID • Atomicity • Consistency • Isolation • Durability 21

Slide 22

Slide 22 text

Как БД хранит данные

Slide 23

Slide 23 text

DmitryTsepelev Fall Saint P 2021 meetup Как хранятся данные на диске 23 users id name 1 John 2 Jane 42 (< 1GB) users forks Database orders id user_id 1 2 123 (< 1GB) orders forks Disk

Slide 24

Slide 24 text

DmitryTsepelev Fall Saint P 2021 meetup Как хранятся данные на диске 24 users id name 1 John 2 Jane Disk 42 (< 1GB) users forks Database orders id user_id 1 2 123 (1GB) orders forks 123.1 (< 1GB) Fork fi le page 1 (8 kB) page 2 (8 kB) page 3 (8 kB)

Slide 25

Slide 25 text

DmitryTsepelev Fall Saint P 2021 meetup Tuple • tuple = header + user data; • tuple почти никогда не обновляется (БД может, мы нет); • DELETE = UPDATE xmax; • UPDATE = INSERT + DELETE. 25 xmin xmax ctid infomask id name 123 125 (3, 15) 111 1 Jon 123 0 (4, 1) 111 2 Jane 125 0 (3, 15) 111 1 Jonh

Slide 26

Slide 26 text

DmitryTsepelev Fall Saint P 2021 meetup Tuple header 26 BEGIN; SELECT txid_current(); ┌──────────────┐ │ txid_current │ ├──────────────┤ │ 8249614 │ └──────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 1 │ [email protected] │ (0,1) │ 8249614 │ 0 │ └────┴──────────────────┴───────┴─────────┴──────┘

Slide 27

Slide 27 text

DmitryTsepelev Fall Saint P 2021 meetup Tuple header 27 CREATE EXTENSION pageinspect; SELECT (t_infomask & 256) > 0 AS xmin_commited, * FROM heap_page_items(get_raw_page('users', 0)); ┌───────────────┬─────────┬────────┬──────────┬────────┬─────────────┬────────────┬────────┬────────┬───────┬────────┐ │ xmin_commited │ t_xmin │ t_xmax │ t_f i eld3 │ t_ctid │ t_infomask2 │ t_infomask │ t_hoff │ t_bits │ t_oid │ t_data │ ├───────────────┼─────────┼────────┼──────────┼────────┼─────────────┼────────────┼────────┼────────┼───────┼────────┤ │ f │ 8249614 │ 0 │ 11 │ (0,1) │ 2 │ 2050 │ 24 │ ¤ │ ¤ │ . . . │ └───────────────┴─────────┴────────┴──────────┴────────┴─────────────┴────────────┴────────┴────────┴───────┴────────┘ COMMIT; ┌───────────────┬─────────┬────────┬──────────┬────────┬─────────────┬────────────┬────────┬────────┬───────┬────────┐ │ xmin_commited │ t_xmin │ t_xmax │ t_f i eld3 │ t_ctid │ t_infomask2 │ t_infomask │ t_hoff │ t_bits │ t_oid │ t_data │ ├───────────────┼─────────┼────────┼──────────┼────────┼─────────────┼────────────┼────────┼────────┼───────┼────────┤ │ t │ 8249614 │ 0 │ 11 │ (0,1) │ 2 │ 2050 │ 24 │ ¤ │ ¤ │ . . . │ └───────────────┴─────────┴────────┴──────────┴────────┴─────────────┴────────────┴────────┴────────┴───────┴────────┘

Slide 28

Slide 28 text

DmitryTsepelev Fall Saint P 2021 meetup Update tuple 28 BEGIN; SELECT txid_current(); ┌──────────────┐ │ txid_current │ ├──────────────┤ │ 8249618 │ └──────────────┘ UPDATE users SET email = '[email protected]' WHERE id = 1; SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────────┼───────┼─────────┼──────┤ │ 1 │ [email protected] │ (0,2) │ 8249618 │ 0 │ └────┴──────────────────────┴───────┴─────────┴──────┘

Slide 29

Slide 29 text

DmitryTsepelev Fall Saint P 2021 meetup Update tuple 29 SELECT t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('users', 0)); ┌─────────┬─────────┬────────┐ │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8249616 │ 8249618 │ (0,2) │ │ 8249619 │ 0 │ (0,2) │ └─────────┴─────────┴────────┘

Slide 30

Slide 30 text

DmitryTsepelev Fall Saint P 2021 meetup Delete tuple 30 BEGIN; SELECT txid_current(); ┌──────────────┐ │ txid_current │ ├──────────────┤ │ 8249620 │ └──────────────┘ DELETE FROM users WHERE id = 1; SELECT t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('users', 0)); ┌─────────┬─────────┬────────┐ │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8249616 │ 8249619 │ (0,2) │ │ 8249619 │ 8249621 │ (0,2) │ └─────────┴─────────┴────────┘

Slide 31

Slide 31 text

Multi Version Concurrency Control

Slide 32

Slide 32 text

DmitryTsepelev Fall Saint P 2021 meetup MVCC • чтение не блокирует запись; • запись не блокирует чтение; • каждая транзакция видит свой «слепок» данных. 32

Slide 33

Slide 33 text

DmitryTsepelev Fall Saint P 2021 meetup Snapshot • физическая копия данных не делается; • «видимые» данные определяются исходя из условий. 33 SELECT * FROM users WHERE xmin < = txid_current() AND (xmax = 0 OR txid_current() < xmax); - - - ^^ ^^ - - - запись еще не удалена запись удалена позже

Slide 34

Slide 34 text

DmitryTsepelev Fall Saint P 2021 meetup Snapshot T1 T2 T3 T4 S1 S2 S3 • S1: 🤷 • S2: T1 • S3: T1, T2 34

Slide 35

Slide 35 text

DmitryTsepelev Fall Saint P 2021 meetup Snapshot 35 BEGIN; SELECT txid_current(); ┌──────────────┐ │ txid_current │ ├──────────────┤ │ 8257515 │ └──────────────┘ SELECT txid_current_snapshot(); ┌──────────────────────────────┐ │ txid_current_snapshot │ ├──────────────────────────────┤ │ 8257515 : 8257515 : 8257515 │ └──────────────────────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 2 │ [email protected] │ (0,3) │ 8257515 │ 0 │ └────┴──────────────────┴───────┴─────────┴──────┘ xmin xmax список активных транзакций

Slide 36

Slide 36 text

DmitryTsepelev Fall Saint P 2021 meetup Savepoints 36 BEGIN; SELECT txid_current(); ┌──────────────┐ │ txid_current │ ├──────────────┤ │ 8257515 │ └──────────────┘ SELECT txid_current_snapshot(); ┌──────────────────────────────┐ │ txid_current_snapshot │ ├──────────────────────────────┤ │ 8257515 : 8257515 : 8257515 │ └──────────────────────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 2 │ [email protected] │ (0,3) │ 8257516 │ 0 │ └────┴──────────────────┴───────┴─────────┴──────┘

Slide 37

Slide 37 text

DmitryTsepelev Fall Saint P 2021 meetup Savepoints • механизм для "вложенных" транзакций; • позволяет откатить часть изменений, не откатывая всю транзакцию; • txid_current возвращает идентификатор главной транзакции! 37 BEGIN; SAVEPOINT s1; INSERT INTO users (email) VALUES ('[email protected]'); ROLLBACK TO s1;

Slide 38

Slide 38 text

DmitryTsepelev Fall Saint P 2021 meetup Где кроме PSQL используется MVCC? • MySQL (InnoDB/Falcon/Archive storage) • MariaDB (XtraDB/PBXT) • MongoDB (WiredTiger storage) • Oracle (> 4) • CouchDB • Couchbase • … 38

Slide 39

Slide 39 text

Спецэффекты

Slide 40

Slide 40 text

DmitryTsepelev Fall Saint P 2021 meetup Аномалии изоляции • Dirty Read • Nonrepeatable Read • Phantom Read • Serialization Anomaly 40

Slide 41

Slide 41 text

DmitryTsepelev Fall Saint P 2021 meetup Уровни изоляции транзакций 41 Isolation Level Dirty Read Nonrepeatable Reads Phantom Reads Serialization Anomaly Read Uncommited (not PG) (not PG) x x Read Commited x x x Repeatable Read (not PG) x Serializable

Slide 42

Slide 42 text

DmitryTsepelev Fall Saint P 2021 meetup Как работают уровни изоляции? 42 • в Read Committed снепшот создается перед каждым запросом; • в Repeatable Read и Serializable снепшот создается только один раз.

Slide 43

Slide 43 text

DmitryTsepelev Fall Saint P 2021 meetup Как работает Serializable 43 • гарантируется, что результат работы транзакций не зависит от порядка выполнения; • для полностью корректной работы нужно обходить граф зависимостей; • упрощенно: ищем ситуации, когда одна транзакция изменила строку, а другая — прочитала предыдущую версию этой строки; • простой подход может вызывать ложно–положительные срабатывания.

Slide 44

Slide 44 text

DmitryTsepelev Fall Saint P 2021 meetup Почему не использовать Serializable всегда? 44 • придется использовать его для ВСЕХ транзакций; • придется всегда ловить исключение и перезапускать операцию; • Serializable работает медленнее других уровней.

Slide 45

Slide 45 text

DmitryTsepelev Fall Saint P 2021 meetup 45 INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ ⏱ SELECT * FROM users; ❓ ❓ ❓

Slide 46

Slide 46 text

DmitryTsepelev Fall Saint P 2021 meetup INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ ⏱ SELECT * FROM users; ┌────┬─────────────────────┐ │ id │ email │ ├────┼─────────────────────┤ │ 2 │ [email protected] │ │ 3 │ [email protected] │ │ 1 │ [email protected] │ └────┴─────────────────────┘ Проблема: нет явного порядка 46

Slide 47

Slide 47 text

DmitryTsepelev Fall Saint P 2021 meetup INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ UPDATE users SET email = '[email protected]' WHERE id = 1; SELECT * FROM users; ┌────┬─────────────────────┐ │ id │ email │ ├────┼─────────────────────┤ │ 2 │ [email protected] │ │ 3 │ [email protected] │ │ 1 │ [email protected] │ └────┴─────────────────────┘ Проблема: нет явного порядка 47

Slide 48

Slide 48 text

DmitryTsepelev Fall Saint P 2021 meetup SELECT t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('users', 0)); ┌─────────┬─────────┬────────┐ │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8257533 │ 8257536 │ (0,4) │ │ 8257534 │ 0 │ (0,2) │ │ 8257535 │ 0 │ (0,3) │ │ 8257536 │ 0 │ (0,4) │ └─────────┴─────────┴────────┘ Проблема: нет явного порядка 48

Slide 49

Slide 49 text

DmitryTsepelev Fall Saint P 2021 meetup Проблема: нет явного порядка 49

Slide 50

Slide 50 text

DmitryTsepelev Fall Saint P 2021 meetup Фича: определяем была ли запись создана в upsert 50 INSERT INTO users (name, email) VALUES ('John', '[email protected]'); INSERT INTO users (name, email) VALUES ('John!', '[email protected]'), ('Jane', '[email protected]') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING id, (xmax = '0') AS inserted, name AS new_name; ┌─────────┬──────────┬──────────┐ │ id │ inserted │ new_name │ ├─────────┼──────────┼──────────┤ │ 1 │ f │ John! │ │ 2 │ t │ Jane │ └─────────┴──────────┴──────────┘

Slide 51

Slide 51 text

DmitryTsepelev Fall Saint P 2021 meetup Фича: определяем была ли запись создана в upsert 51 select *, xmin, xmax, ctid from users; ┌─────────┬──────────────────┬───────┬─────────┬─────────┬───────┐ │ id │ email │ name │ xmin │ xmax │ ctid │ ├─────────┼──────────────────┼───────┼─────────┼─────────┼───────┤ │ 1 │ [email protected] │ John! │ 8843025 │ 8843025 │ (0,2) │ │ 2 │ [email protected] │ Jane │ 8843025 │ 0 │ (0,3) │ └─────────┴──────────────────┴───────┴─────────┴─────────┴───────┘ 🤔🤔🤔

Slide 52

Slide 52 text

DmitryTsepelev Fall Saint P 2021 meetup Фича: определяем была ли запись создана в upsert 52 select *, xmin, xmax, ctid from users; ┌─────────┬──────────────────┬───────┬─────────┬─────────┬───────┐ │ id │ email │ name │ xmin │ xmax │ ctid │ ├─────────┼──────────────────┼───────┼─────────┼─────────┼───────┤ │ 1 │ [email protected] │ John! │ 8843025 │ 8843025 │ (0,2) │ │ 2 │ [email protected] │ Jane │ 8843025 │ 0 │ (0,3) │ └─────────┴──────────────────┴───────┴─────────┴─────────┴───────┘ 🤔🤔🤔 В некоторых случаях xmax может означать всего лишь лок: https://stackover fl ow.com/a/39204667

Slide 53

Slide 53 text

DmitryTsepelev Fall Saint P 2021 meetup 53 INSERT INTO users (email) SELECT random() : : text FROM generate_series(1, 1000000); SELECT pg_size_pretty(pg_total_relation_size('users')); - - 72 MB UPDATE users SET email = 'something'; SELECT pg_size_pretty(pg_total_relation_size('users')); ❓ ❓ ❓

Slide 54

Slide 54 text

DmitryTsepelev Fall Saint P 2021 meetup Проблема: большой апдейт может раздуть таблицу 54 INSERT INTO users (email) SELECT random() : : text FROM generate_series(1, 1000000); SELECT pg_size_pretty(pg_total_relation_size('users')); - - 72 MB UPDATE users SET email = 'something'; SELECT pg_size_pretty(pg_total_relation_size('users')); - - 135 MB

Slide 55

Slide 55 text

DmitryTsepelev Fall Saint P 2021 meetup Проблема: удаление данных из таблицы не освобождает место на диске 55 VACUUM users; SELECT pg_size_pretty( pg_total_relation_size('users') ); - - 135 MB VACUUM FULL users; SELECT pg_size_pretty( pg_total_relation_size('users') ); - - 64 MB

Slide 56

Slide 56 text

DmitryTsepelev Fall Saint P 2021 meetup Проблема: ID транзакций могут «кончиться» • тип ID транзакции XID, 32 бита (~4 000 000 000 значений); • больший тип увеличит место для хранения заголовка тапла; • БД начинает счет заново при достижении максимального значения; • VACUUM «замораживает» (freeze) старые таплы. 56

Slide 57

Slide 57 text

DmitryTsepelev Fall Saint P 2021 meetup Выводы • MVCC нужен для обеспечения изоляции транзакций; • на низком уровне данные не обновляются, а записываются; • реализация MVCC иногда приводит к неожиданным сайд– эффектам. 57

Slide 58

Slide 58 text

DmitryTsepelev Fall Saint P 2021 meetup Куда пойти дальше • Цикл статей про MVCC: • 🇬🇧 https://postgrespro.com/blog/pgsql/5967856; • 🇷🇺 https://habr.com/ru/company/postgrespro/blog/442804/; • The Internals of PostgreSQL — http://www.interdb.jp/pg/; • Serializable Snapshot Isolation in PostgreSQL — https://arxiv.org/pdf/ 1208.4179.pdf; • Database Internals — https://www.databass.dev. 58

Slide 59

Slide 59 text

evl.ms/blog @dmitrytsepelev @evilmartians evl.ms/telegram Спасибо! @dmitrytsepelev 59