Когда UPDATE или DELETE трогает строку, он блокирует именно её. На
таблице при этом висит лёгкий ROW EXCLUSIVE, а сам замок на строку
живёт в данных: номер держащей транзакции пишется в поле xmax
заголовка кортежа, статус - в битах t_infomask. Поэтому блокировка
строки почти бесплатна по памяти и масштабируется на миллионы строк.
Четыре режима
| Режим | Сила | Когда |
|---|---|---|
| FOR KEY SHARE | слабейший | удержать строку от удаления и смены ключа |
| FOR SHARE | разделяемый | «никто не изменит, пока считаю» |
| FOR NO KEY UPDATE | средний | UPDATE неключевых колонок |
| FOR UPDATE | сильнейший | SELECT ... FOR UPDATE, DELETE |
Зачем четыре? Из-за внешних ключей. Вставка в дочернюю таблицу должна
удержать родителя от исчезновения, но не менять его. Слабый FOR KEY
SHARE блокирует только удаление и смену ключа родителя, пропуская
параллельный UPDATE его неключевых колонок. До версии 9.3 тут был
полный конфликт, и вставки в дочернюю таблицу сериализовались зря.
Где это видно
В pg_locks отдельной строки на заблокированный кортеж нет. Ждущая
транзакция видна косвенно - она стоит на locktype = 'transactionid',
ожидая завершения держателя:
SELECT pid, wait_event_type, wait_event
FROM pg_stat_activity WHERE wait_event_type = 'Lock';
Если одну строку держат несколько транзакций в режиме FOR SHARE,
одного xmax мало - заводится multixact (запись в pg_multixact со
списком участников). Его возраст отслеживает relminmxid, и он тоже
требует заморозки.
Главное про порядок
Брать строки в одном порядке во всех транзакциях (например, по возрастанию ключа) - так не возникает deadlocks. Сами блокировки строк - это часть тяжёлого механизма, верхний слой которого описан в relation-locks.