Знай свой мессенджер

Современные мессенджеры шагнули далеко вперёд и помимо их основной функции, передачи сообщений, обладают множеством других. Но в этом посте, я бы хотел сконцентрироваться на действиях выполняемых непосредственно над сообщениями. Про них, как показывает опыт, многие либо не знают, либо не понимают, в следствие чего пользуются ими неправильно. Если это не вы, можете дальше не читать. А с остальными мы разберём некоторые из таких действий на примере мессенджера Телеграм.

Короткий тап (клик ПКМ) на сообщении показывает контекстное меню с возможными вариантами действия:

telegram context menu

Все они довольно очевидные, но давайте пройдёмся по каждому.

Reply (ответить)

Используйте когда вы хотите ответить на одно из ранее написанных сообщений и не потерять с ним связь.

example of reply

Очень часто когда люди хотят «ответить», они используют «переслать», что, конечно, неправильно. Читать такие ответы неудобно, т. к. сначала ты видишь ответ (комментарий), и только затем своё сообщение. Кстати, в ВК хоть и есть оба варианта, но выглядят они совершенно одинаково:

vk examples of replay and forwarding

Лайфхак: если тапнуть (кликнуть) на отвечаемое сообщение, чат промотается прямо к нему.

Copy (копировать)

Просто копирует сообщение в буфер обмена.

Forward (переслать)

Используйте когда хотите переслать сообщение из одного чата в другой, возможно сопроводив его комментарием. В рамках одного чата тоже бывает нужно переслать сообщение, но требуется это, как правило, не часто.

example of forwarding

Edit (редактировать)

Современный мессенджер — это не «аська» из двухтысячных. Не поверите, но в нём можно отредактировать уже отправленное сообщение (в случае ошибки или опечатки), а не писать исправления позже, сопровождая их звёздочкой. В Телеграме можно редактировать ваши сообщения в течении суток или около того.

Delete (удалить)

Кроме привычного удаления вашего сообщения из личной истории, вы можете удалить его и из истории вашего собеседника. Для этого отметьте галочкой пункт «Delete for …» во время удаления. Возможность удалять сообщения на другой строне, как и редактирование, действует некоторое время, после чего удалить сообщение из истории собеседника не получится.

example of deletion

Удалить сообщение соостоящие только из фото чуть сложнее, для этого нужно тапнуть не на самой фотографии я рядом с ней, дальше появится знакомое контекстное меню.

Лайфхак: удаление сообщения на другой стороне не гарантирует вам того, что собеседник его не увидит. Если он пользуется мобильной версией клиента, то всё ещё сможет прочитать его в уведомлениях.

example of deletion

Бонус

Во многих современных мессенджерах, если не во всех, присутствует возможность отправлять сообщения самому себе. Казалось бы, что это очень странная возможность, но в дейсвительности она очень полезна.

screen with saved messages

С недавних пор чат с самим собой в Телеграме переименовался в Saved Massages (сохранённые сообщения), что отражает его суть как нельзя лучше. Я часто скидываю туда сообщения к которым хочу вернуться спустя некоторое время, пишу короткие заметки и пр. И обязательно удаляю их, когда они больше мне не нужны. Чтобы чат с Saved Massages не затерялся среди других, его можно закрепить: в списке чатов долгий тап на чате, который вы хотите закрепить, дальше «Pin to top» (закрепить сверху).

Вы (программируете) сидите в своей зоне комфорта? Пожалуйста, не надо

Перевод статьи Are you (programming) in your comfort zone? Please don’t.


Я работаю программистом более пяти лет и всё это время использовал только Java стек технологий, не выходя за его пределы. Как следствие, я чувствую себя вполне уверенно с любым Java кодом. Совсем недавно я решил поиграться с JavaScript, а именно с node.js. JS во многих отношениях похож на Java, но он другой. Сквозь призму Java-разработчика, JS виделся мне грязненьким инструментом для фронтенда. Но использование node.js изменило моё отношение к нему и я начал ценить этот язык. Более того, чем больше я писал на JS, тем больше получал от этого удовольствия. В данный момент я использую MEAN стек на работе. И это офигенно увлекательно.

Когда я только начал писать на node.js, то испытывал дискомфорт и беспокойство. Мне было сложно выражать свои мысли на новом языке. Код, который я писал, не следовал хорошим практикам или каким-либо архетектурным стандартам. Всё это было за пределами моей зоны комфорта.

У меня для вас плохая новость: у вас тоже есть своя зона комфорта.

Зона комфорта

Зона комфорта — место, в котором вы чувствуете себя в безопасности. Комфортно. Имеено здесь большинство людей привыкли действовать (работать).

Повседневная деятельность, к которой вы привыкли, обычно не вызывает чувства беспокойства или неудобства. Она — часть вашей зоны комфорта, включающей навыки и умения, которые вы приобрели. И вы остаётесь весьма продуктивны находясь в вашей зоне комфорта.

Почему вам следует выйти из зоны комфорта

Вы чувствуете себя в безопасности. Вы продуктивны. Вы не чувствуете беспокойства или стресса. А затем вам становится скучно. Вы не ощущаете вызова. Ведь зона комфорта состоит из того, что мы уже умеем делать легко. Находясь в ней, вы не можете прогрессировать или прокачивать навыки.

Для программиста отсутствие движения вперёд означает движение назад.

Наибольшей пользой от выхода из зоны комфорта является ваш личный рост. Всякий раз, когда вы решаете задачу, находящуюся за пределами вашей зоны комфорта, и успешно её завершаете, ваша уверенность возрастает. Вы чувствуете, что достигли чего-то значимого.

За пределами зоны комфорта

Прямо за границей зоны комфорта лежит зона обучения. Навыки и умения находящиеся в ней почти досягаемы. Они не так далеко, чтобы заставить вас паниковать, но в то же время не слишком близко, чтобы быть легкодоступными.

Поскольку человек может прогрессировать лишь в зоне обучения, очень важно уметь нащупывать её. Для одних она может быть широкой, для других узкой. Но, как только вы начнёте испытывать себя (бросать себе вызов), эта зона начнёт расширяться.

Как понять, что вы продвигаетесь

Когда вы не знаете, где начинается ваша зона обучения, попробуйте испытать себя. Текущая задача должна быть такой, чтобы заинтересовать вас и вам было не скучно ее решать, но в то же время сложность задачи не должна вгонять вас в ступор. Если эти условия выполняются, вы в зоне обучения.

Вот несколько примеров задач, которые вы можете попробовать:

  • изучите другую IDE (если вы пользуйтесь Eclipse, попробуйте Visual Studio или IntelliJ);
  • используйте разные ОС (если привыкли к Windows, попробуйте Linux, если привыкли к ОС основанным на Debian, попробуйте Fedora);
  • используйте командную строку вместо пользовательского интерфейса;
  • используйте vim вместо notepad/sublime;
  • изучите язык программирования отличный от вашего (например JS, если вы пишите на Java);
  • изучите язык программирования с другой парадигмой/философией (если вы привыкли к ООП языкам, попробуйте функциональные, например Clojure);
  • если вы читаете только техническую литературу, почитайте что-нибудь из художественной.

Держитесь подальше от зоны паники

Сразу за пределами зоны обучения лежит зона паники. Как и в зоне комфорта вы не способны прогрессировать в ней. Задачи в зоне паники насктолько сложные, что вы не знаете как к ним подступиться. Вас охватывает беспокойство, вы не можете нормально думать. Или становитесь неуверенными и впадаете в ступор.

Например, если вы всю свою программерскую жизнь писали только на Java, используя ООП, и понятия не имеете как подойти к Haskell, попробуйте для начала .net. Или начните использовать лямбды и изучите элементы функционального программирования доступные в Java.

Оставайтесь в зоне обучения

Когда вы действуете в зоне обучения, текущие навыки будут всё более привычными и начнут перемещаться в зону комфорта. Как только это произошло, задачи, бывшие когда-то в зоне паники, перейдут в зону обучения и цикл повторится.

Желаю успехов в вашем обучении!


Мысли автора показались мне весьма близкими потому что в моей жизни сейчас происходят похожие перемены. Пробую писать на JS и Clojure после нескольких лет использования только Java. Надеюсь перевод статьи вам понравился, и вы воспользуетесь её рекомендациями на практике.

Синтаксис Clojure — префиксная запись

Несмотря на обилие круглых скобок, которое зачастую вызывает отторжение у программистов, язык Clojure имеет весьма простой синтаксис. И если присмотреться, то окажется, что и скобок не намного больше, чем в других языках, а иногда даже меньше. Сегодня мы поговорим о постфиксной записи повсеместно используемой в Clojure. Но сперва, давайте вспомним какие вообще бывают записи в программировании.

Привычная всем инфиксная запись: в ней операторы ставятся между операндов

2 + 2 * 2, object.method()

Префиксная запись (она же польская нотация): оператор стоит вначале, а затем перечисляются операнды

(+ (* 2 2) 2), ++i

При условии, что арность операторов известна заранее, скобки в первом выражении можно опустить

+ * 2 2 2

Постфиксная запись

i++

И, наконец, функциональная запись

cos(90), substr("spam!", 1, 3)

В таких языках как Ruby, Python и Java встречаются все 4 вида записей. Такое многообразие может затруднять чтение и понимание кода. К тому же, нужно запоминать приоритет выполнения операторов. В Clojure используется только префиксная запись. Сперва это непривычно и, возможно, даже «больно». Но стоит помнить, что нечто болезненное может со временем оказаться полезным 1.

Для более наглядной картины, ниже, я привожу сравнительную таблицу вызовов функций.

Выражение на Сlojure Эквивалент на Java
(not k) !k
(inc a) a++, ++a, a += 1, a + 1
(/ (+ x y) 2) (x + y) / 2
(instance? java.util.List al) al instanceof java.util.List
(if (not a) (inc b) (dec b)) !a ? b + 1 : b - 1
(Math/pow 2 10) Math.pow(2, 10)
(.someMethod someObj "foo" (.otherMethod otherObj 0)) someObj.SomeMethod("foo", otherObj.otherMethod(0))

С одной стороны, синтаксис вызовов на Java крайне многообразен:

  • в нём есть инфиксные операторы, но любой нетривиальный код требует явно определять порядок их выполнения;
  • унарные операторы имеют как префиксный так и постфиксный варианты;
  • вызов статических методов сопровождается префиксом…;
  • но в то же время вызов методов инстанса сопровождается множественными инфиксными формами: сначала указывается целевой объект (он будет присвоен переменной this внутри тела вызываемого метода), затем сам вызываемый метод с формальными параметрами.

С другой стороны, вызовы выражений на Clojure следуют единственному тпростому правилу: на первой позиции всегда стоит оператор (или функция), на остальных перечислены параметры передаваемые этому оператору. Инфиксная, функциональная и постфиксная записи, а также сложные правила определяющие приоритет вычисления — здесь отсутствуют. Такой синтаксис проще читать, использовать и изучать.


  1. Думай как математик. Как решать любые проблемы быстрее и эффективнее, Барбара Оакли. 2-е издание. — «Альпина Паблишер», 2016. — 132 c. 

8 способов сделать SQL запросы понятнее

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. — Martin Fowler

Вступление

За последние пять лет мне довелось поработать в трёх разных компаниях, но ни в одной из них я не встречал SQL запросы, которые выглядели бы опрятно и легко читались (не считая редких исключений). Как правило попадаются запросы написанные как попало: в них случайным образом скачут отступы и меняется регистр, они плохо структурированы и непоследовательны, зачастую ещё и написаны не очень эффективно. Открывая такой запрос приходится потратить немалое время, чтобы начать хоть что-то в нём понимать. А через месяц, встретив этот же запрос снова, придётся опять в нём разбираться. Многие люди вообще относятся к сиквелу как к второсорному языку, не проявляя к нему никакого уважения. Но уважения они не проявляют не только к языку, но и к другим разработчикам, которым в будущем приходится читать и поддерживать такие запросы.

Ниже я привожу 8 простых правил основанных на личном опыте. Следуя им, ваши запросы будут легко читаемыми и простыми для понимания другими разработчиками.

1. Никакого капса

В давние времена, когда редакторы кода не имели возможности подсвечивать синтаксис, было принято писать ключевые слова заглавными буквами. С тех пор эта привычка крепко укоренилась в головах некоторых разработчиков и они продолжают следовать этой традиции. Некоторые пошли ещё дальше, и стали писать капсом вообще всё: имена таблиц, стобцов и пр. На деле же, любой современный редактор кода имеет подстветку ситнаксиса (в т. ч. и для встроенных языков, если вы пишете запрос внутри другого языка). SELECT, FROM, WHERE навряд ли помогут вам понять суть запроса, а вот внимание на себя отвлекать однозначно будут. За 5 лет что я пишу SQL запросы, я редко встречал те которые можно назвать «образцовыми»: где все ключевые слова выделены капсом, а не ключевые нет. Зато смешивание этих стилей попадается сплошь и рядом.

2. Перенос строк

Тут я выделяю понятия остновных ключевых слов и второстепенных (вложенных). Так, например, select, from и where являются основными, join, on, and второстепенными. Основные слова выравнены по левому краю, второстепенные в зависимости от уровня вложенности сдвигаются вправо:

select ...
from table1
join table2
  on
  and
where ...
and ...

3. Отступы

В SQL предпочтительней использовать отступы из 2 пробелов. Запросы с такими отсупами выглядят опрятней и компактней в сравнении с другими вариантами.

4. Перечисления

В тех случая когда столбцов в запросе несколько, предпочтительней писать каждый из них с новой строки:

select
  id,
  name,
  type_id
  ...

5. Соединения

Тема соединений таблиц всегда была очень болезненной. Когда-то и я перечислял имена таблиц через запятую, а все соединения наряду с предикатами делал в блоке where используя (+) вместо left join. Но, такая запись трудна для восприятия человеком, читающим этот запрос. Основных аргументов в её пользу, которые мне доводилось слышать, два: 1) ANSI соединения в оракле работают медленнее (на данный момент это уже не актуально); 2) глазам не приходится бегать по всему запросу, т. к. все условия находятся в одном месте. Аргумент 2 не выдерживает никакой критики, скорее это закостенелая привычка от которой сложно изавиться людям давно использующим такой синтаксис. Когда все условия собраны в where, это больше похоже на месиво в котором чёрт ногу сломит. Напротив, при использовании ANSI соединений, запрос выглядит опрятным, каждое такое соединение и его уточнение сосредоточено сразу под именем таблицы, а в блоке where находится лишь окончательный предикат глядя на который становится видно саму суть.

...
from objects file
  join parameters path
    on path.object_id = file.id
    and path.attr_id = 123
where file.type_id = 404;

Обратите внимание, что в условии соединения столбец текущей таблицы стоит слева, а столбец внешней таблицы стоит справа от знака равенства.

Нередко авторы запросов заключают в скобки условия соединений:

...
from objects file
  join parameters path
    on (path.object_id = file.id
    and path.attr_id = 123)
where file.type_id = 404;

Сути это не меняет, а определённый шум вносит, поэтому лучше обойтись без них.

6. Алиасы для таблиц

Они должны обозначать, то что выбирается из таблицы. Алиасы лучше чем комментарии рядом с идентификаторами, т. к. они позволяют в любом месте запроса сразу понять о чём идёт речь, в случае с комментарием вам придётся искать его и держать в уме связь между id и именем.

Плохо:

select
  obj.name,
  p.value as message
from objects obj
  join params p
    on p.object_id = obj.id
    and p.attr_id = 995 -- content
where obj.type_id = 110; -- mail

Хорошо:

select
  mail.name,
  content.value as message
from objects mail
  join params content
    on content.object_id = mail.id
    and content.attr_id = 995
where mail.type_id = 110;

Во втором случае также можно добавить комментарии рядом с идентификатороми, но как по мне, это излишне.

7. Запятые

Кто-то переносит запятую в перечислении на новую строку:

select
    field1
  , field2
  , field3

Я не сторонник такого подхода. Конечно, это вносит определённое удобство при добавлении новых стобцов в запрос, но выглядит уродско.

8. Скобки

Парные скобки должны быть либо на одной строке, либо закрывающая скобка выровнена по началу блока к котором она принадлежит:

select name
from table1
where field1 in (..., ..., ...,)
  and field2 in (
    ...,
    ...,
    ...
  )

Также обратите внимание, что если в where сразу же идёт какое-то перечисление, то такой предикат следует перенести на новую строчку, чтобы правило скобок не было нарушено:

select
  ...
from table1
where
  field1 in (
    ...
  )
  and ...;

Ещё вариант:

select
  ...,
  (
    select foo
    from table
    where ...
  ) as bar,
  ...

Ещё несколько примеров

select
  id,
  name,
  parent_id
from objects descriptor
where type_id = 666
order by name;
select
  id,
  name,
  type_id
from objects descriptor
where
  type_id in (
    ...
  )
  and name like 'zek%';
with
  table1 as (
    ...
  ),
  table2 as (
    ...
  )
select
  ...
from table1
  join table2
    on table2.fieldA = table1.fieldA
where table1.fieldB = 'blah';

Вывод

Несомненно, этот список не явлется исчерпывающим, и вы сами с легкостью можете его расширить. Вероятно с какими-то пунктами вы даже не согласитесь. Цель этой заметки заключается в том, чтобы обратить внимание на тот код который мы пишем, не важно на каком языке, важно как мы это делаем. Спасибо.

Кирилл Мокевнин о stateful и stateless

Основная часть

  • 00:14:05. Главный тезис вебинара: как только вам приходится, что-то удалять или изменять, сложность процесса возрастает на порядки.

  • 00:16:00. Всё что является изменяемым состоянием, то что с ним связано, должно быть изолировано. Это уменьшает влияние на другие части системы и позволяет проще масшатбировать те части системы, которые не являются стейтфул.

  • 00:24:10. Что является состоянием конкретного сервера?

  • 00:26:25. Пример с обновлением софта на сервере.

  • 00:32:35. Стейтфул — это сложно. Идеально, если вы делегируете стейтфул сервера другим компаниям, которые решат за вас эти проблемы. Например Амазон.

  • 00:34:40. Пример неправильного подхода в решении проблемы изменяемости.

  • 00:37:00. Вагрант не просто изолирует ваше окружение от хост машины, фишка Вагранта в том, что вы легко можете экспереминтировать с состоянием. В случае краха довольно просто вернуться к первоначальному стостоянию. Конечно, предполагается, что вы также пользуетесь системой конфигурции, например Энсиблом.

  • 00:37:50. Главная фишка докера «имьютебл ифрастракче». Докер не заменяет систем конфигурации, но решает проблему обновления софта.

  • 00:45:50. Обратная сторона докера, в том, что докер-имаджы могут собираться довольно долго. Это приводит к тому, что хотфикс невозможен. Тем не менее докер нужно использовать уже прямо сейчас. В этом нет ничего сложного.

  • 00:50:25. Про деплой и выкатывание нового сервиса.

  • 00:56:40. В деплое самое главное быстрый и крайне простой откат.

  • 01:01:00. Откатывать базу данных нельзя никогда.

  • 01:03:45. База, как минимум, должна уметь рабтать с текущей, предыдущей и новой версиями кода.

  • 01:09:55. В большинстве типичных проектов можно обойтись без стейджинга.

  • 01:11:20. Резюме по базам данных.

  • 01:14:05. И всё-таки из базы можно удалять всё, что раньше трёх последних версий.

  • 01:20:00. Что делать, если нужно часть инфраструктуры перенести на другой сервис?

  • 01:23:00. Что делать, если структуру данных в очереди нужно изменить?

  • 01:30:10. Микросервисы и состояние.

Вопросы из чата

  • 01:32:20. Почему Докер не может заменить Вагрант?

Допольнительно

Бонус

01:19:35. Это не значит что мы не косячим. Мы реально раздолбаи. Если вы видите какие-то косяки и баги на Хекслете, это значит, что мы просто ленивые.

Коротко о TDD

Читаю книгу Роберта Мартина «Чистый код». Добрался до главы про тестирование. Она хоть и затрагивает тему поверхностно, всё же содержит в себе немало хороших рекомендаций. Ниже те, что я выписал.

Три закона TDD

  1. Не пишите код продукта, пока не напишете отказной модульный тест.
  2. Не пишите модульный тест в объёме большем, чем необходимо для отказа. Невозможность компиляции является отказом.
  3. Не пишите код продукта в объёме большем, чем необходимо для прохождения текущего отказного теста.

Чистота тестов

Тестовый код не менее важен, чем код продукта. Не считайте его «кодом второго сорта». К написанию тестового кода следует относиться вдумчиво, внимательно и отвественно. Тестовый код должен быть таким же чистым как и код продукта.

Какими отличительыми признаками характеризуется чистый тест? Тремя: удобочитаемостью, удобочитаемостью и удобочитаемостью. Вероятно, удобочитаемость в модульных тестах играет ещё более важную роль, чем в коде продукта. Что делает тестовый код удобочитаемым? То же, что делает удобочитаемым любой другой код: ясность, простота и выразительность. В тестовом коде необходимо передать максимум информации минимумом выразительных средств.

Хорошо написанный тест имеет понятную и простую структуру состоящую из трёх частей: построение, операция, проверка. Первая часть строит тестовые данные, вторая часть выполняет с ними операции, а третья часть проверяет, что операция привела к ожидаемым результатам.

Двойной стандарт

Код тестового API подчиняется несколько иным техническим стандартам, чем код продукта. Он также должен быть простым, локаничным и выразительным, но от него не требуется такая эффективность. В конце концов, тестовый код работает в тестовой среде, а не в среде реальной эксплуатации продукта, а эти среды весьма заметно различаются по своим потребностям.

Одна концепция на тест

В тестовой функции должна проверяться только одна концепция, а количество дириктив asserts должно быть минимальным.

Исследуем блокировки в PostgreSQL

Сегодня предлагаю вам вольный перевод весьма увлекательной и забавной статьи «Exploring Query Locks in Postgres».

Понимание того как работают блокировки является ключом к написанию правильных запросов способных выполняться параллельно. Чтобы узнать как работают блокировки и увидеть, что происходит внутри базы данных, давайте рассмотрим наглядный пример.

Песочница

Для начала создадим «песочницу»:

create database sandbox;

create table toys (
  id serial not null,
  name character varying(36),
  usage integer not null default 0,
  constraint toys_pkey primary key (id)
);

insert into toys(name) values('car'),('digger'),('shovel');

Откроем два терминала, в каждом из них подключимся к только что созданной базе данных sandbox. Чтобы не путаться, дадим им имена. Пусть это будут Алиса и Боб. Изменить подсказку командной строки можно с помощью команды \set:

\set PROMPT1 '[Alice] %/> '

Первой появляется Алиса и осматривает игрушки:

[Alice] sandbox> begin;
BEGIN
[Alice] sandbox> select * from toys;

 id |  name  | usage
----+--------+-------
  1 | car    |     0
  2 | digger |     0
  3 | shovel |     0
(3 rows)

Обратите внимание, что оператор begin начинает транзакцию явно. В этом случае она будет продолжаться до тех пор, пока мы не зафиксируем её, сделаем commit, или не откатим, сделаем rollback.

Если бы Боб сейчас посмотрел на игрушки, он увидел бы то же самое:

[Bob] sandbox> begin;
BEGIN
[Bob] sandbox> select * from toys;

 id |  name  | usage
----+--------+-------
  1 | car    |     0
  2 | digger |     0
  3 | shovel |     0
(3 rows)

Таким образом параллельное выполнение двух операторов select не мешает работе каждого из них. Именно такого поведения мы ожидаем от надёжной и высокопроизводительной базы данных.

pg_lock

Однако, транзакции Алисы и Боба до сих пор открыты. Чтобы посмотреть какие блокировки были установлены, откроем третий терминал и назовём его Ева:

\set PROMPT1 '[Eve] %/> '
select
  lock.locktype,
  lock.relation::regclass,
  lock.mode,
  lock.transactionid as tid,
  lock.virtualtransaction as vtid,
  lock.pid,
  lock.granted
from pg_catalog.pg_locks lock
  left join pg_catalog.pg_database db
    on db.oid = lock.database
where (db.datname = 'sandbox' or db.datname is null)
  and not lock.pid = pg_backend_pid()
order by lock.pid;
  locktype  | relation  |      mode       | tid | vtid  |  pid  | granted
------------+-----------+-----------------+-----+-------+-------+---------
 relation   | toys_pkey | AccessShareLock |     | 6/268 | 45265 | t
 relation   | toys      | AccessShareLock |     | 6/268 | 45265 | t
 virtualxid |           | ExclusiveLock   |     | 6/268 | 45265 | t
 relation   | toys_pkey | AccessShareLock |     | 1/282 | 45263 | t
 relation   | toys      | AccessShareLock |     | 1/282 | 45263 | t
 virtualxid |           | ExclusiveLock   |     | 1/282 | 45263 | t
(6 rows)

Представление pg_lock показывает активные блокировки. Условие (db.datname = 'sandbox' or db.datname is null) оставляет только те записи которые относятся к «песочнице», а условие not pid = pg_backend_pid() исключает записи текущей сессии. Наконец, чтобы колонка relation стала более информативной было использовано приведение типа к regclass.

Посмотрим на пятую строку:

  locktype  | relation  |      mode       | tid | vtid  |  pid  | granted
------------+-----------+-----------------+-----+-------+-------+---------
 relation   | toys      | AccessShareLock |     | 1/282 | 45263 | t

Виртуальной транзакцией 1/282, на таблицу toys наложена блокировка AccessShareLock, при этом блокировка считается выданной (is granted). Пока всё идёт хорошо, Боб и Алиса счастливы, ведь они оба видят — игрушки можно взять.

Обратите внимание, каждая транзакция удерживает блокировку ExclusiveLock на своей виртуальной транзакции virtualxid.

Алиса решает взять машинку:

[Alice] sandbox> update toys set usage = usage + 1 where id = 1;
UPDATE 1

Никаких проблем. Посмотрим как выглядит таблица блокировок теперь:

   locktype    | relation  |       mode       |   tid    | vtid  |  pid  | granted
---------------+-----------+------------------+----------+-------+-------+---------
 relation      | toys_pkey | AccessShareLock  |          | 6/268 | 45265 | t
 relation      | toys      | AccessShareLock  |          | 6/268 | 45265 | t
 virtualxid    |           | ExclusiveLock    |          | 6/268 | 45265 | t
 relation      | toys_pkey | AccessShareLock  |          | 1/282 | 45263 | t
 relation      | toys_pkey | RowExclusiveLock |          | 1/282 | 45263 | t
 relation      | toys      | AccessShareLock  |          | 1/282 | 45263 | t
 relation      | toys      | RowExclusiveLock |          | 1/282 | 45263 | t
 virtualxid    |           | ExclusiveLock    |          | 1/282 | 45263 | t
 transactionid |           | ExclusiveLock    | 24273800 | 1/282 | 45263 | t
(9 rows)

transactionid

В таблице toys на записи с машинкой теперь стоит блокировка RowExclusiveLock. Также появился реальный идентификатор транзакции transactionid на котором удерживается блокировка ExclusiveLock. Такой идентификатор появляется у каждой транзакции потенциально меняющей состояние базы данных.

MVCC

Поскольку транзакция Алисы не зафиксирована, Боб видит прежние данные:

[Bob] sandbox> select * from toys;

 id |  name  | usage
----+--------+-------
  1 | car    |     0
  2 | digger |     0
  3 | shovel |     0
(3 rows)

Мы не знаем, будет ли Алиса фиксировать или откатывать свою транзакцию. Следовательно, Боб видит содержимое таблицы неизменным.

Для того, чтобы каждый пользователь видел согласованное стостояние базы данных, постгрес использует механизм управления конкурентным доступом с помощью многоверсионности MVCC (Multi Version Concurrency Control).

Блокирующие запросы

Допустим Боб тоже хочет поиграть машинкой (типичная ситуация для детей). Боб выполняет следующий запрос:

[Bob] sandbox> update toys set usage = usage + 1 where id = 1;

но ничего не происходит. Ему нужно подождать пока Алиса завершит свою транзакцию. Снова посмотрим в таблицу блокировок:

   locktype    | relation  |       mode       |   tid    | vtid  |  pid  | granted
---------------+-----------+------------------+----------+-------+-------+---------
 relation      | toys_pkey | AccessShareLock  |          | 6/268 | 45265 | t
 relation      | toys_pkey | RowExclusiveLock |          | 6/268 | 45265 | t
 relation      | toys      | AccessShareLock  |          | 6/268 | 45265 | t
 relation      | toys      | RowExclusiveLock |          | 6/268 | 45265 | t
 virtualxid    |           | ExclusiveLock    |          | 6/268 | 45265 | t
 relation      | toys_pkey | AccessShareLock  |          | 1/282 | 45263 | t
 relation      | toys_pkey | RowExclusiveLock |          | 1/282 | 45263 | t
 relation      | toys      | AccessShareLock  |          | 1/282 | 45263 | t
 relation      | toys      | RowExclusiveLock |          | 1/282 | 45263 | t
 virtualxid    |           | ExclusiveLock    |          | 1/282 | 45263 | t
 transactionid |           | ExclusiveLock    | 24273800 | 1/282 | 45263 | t
 tuple         | toys      | ExclusiveLock    |          | 6/268 | 45265 | t
 transactionid |           | ExclusiveLock    | 24273801 | 6/268 | 45265 | t
 transactionid |           | ShareLock        | 24273800 | 6/268 | 45265 | f
(14 rows)

Теперь у Боба тоже есть transactionid и он просит выдать ему ShareLock на transactionid Алисы — «Мам, я тоже хочу поиграть машинкой». Поскольку две блокировки конфликтуют друг c другом, запрос Боба не удовлетворён (is not granted). Он будет висеть в таком состоянии до тех пор, пока Алиса не снимет ExclusiveLock, завершив свою транзакцию.

pg_stats_activity

pg_stat_activity ещё одно интересное представление (view) из pg_catalog’а. Оно показывает запросы выполняющиеся в данный момент:

select query, state, waiting, pid
from pg_stat_activity
where datname = 'sandbox'
  and not (state = 'idle' or pid = pg_backend_pid());
                      query                      |        state        | waiting |  pid
-------------------------------------------------+---------------------+---------+-------
 update toys set usage = usage + 1 where id = 1; | active              | t       | 45265
 update toys set usage = usage + 1 where id = 1; | idle in transaction | f       | 45263
(2 rows)

Мы видим, что запрос Алисы простаивает в ожидании поддтверждения транзакции (idle in transaction), в то время как запрос Боба активен и подвис (is waiting).

Чтобы увидеть кто кого заблокировал, объединим два запроса в один:

select
  bda.pid as blocked_pid,
  bda.query as blocked_query,
  bga.pid as blocking_pid,
  bga.query as blocking_query
from pg_catalog.pg_locks bdl
  join pg_stat_activity bda
    on bda.pid = bdl.pid
  join pg_catalog.pg_locks bgl
    on bgl.pid != bdl.pid
    and bgl.transactionid = bdl.transactionid
  join pg_stat_activity bga
    on bga.pid = bgl.pid
where not bdl.granted
  and bga.datname = 'sandbox';
 blocked_pid |                  blocked_query                  | blocking_pid |                 blocking_query
-------------+-------------------------------------------------+--------------+-------------------------------------------------
       45265 | update toys set usage = usage + 1 where id = 1; |        45263 | update toys set usage = usage + 1 where id = 1;
(1 row)

Если бы Алиса решила откатить или зафиксировать свою транзакцию, блокировка ExclusiveLock была бы снята и Боб получил бы ShareLock. После этого он мог бы зафиксировать свою транзакцию, и запись в таблице была бы обновлена независимо от решения Алисы.

[Alice] sandbox> rollback;
ROLLBACK

[Bob] sandbox> commit;
COMMIT
[Bob] sandbox> select * from toys;

 id |  name  | usage
----+--------+-------
  2 | digger |     0
  3 | shovel |     0
  1 | car    |     1
(3 rows)

Конечно, если бы Боб и Алиса решили играть разными игрушками, конфликтной ситуации между ними не возникло бы вообще.

Явные блокировки

Другая типичная ситуация для детей, когда один из них хочет забрать все игрушки без реальной необходимости:

[Alice] sandbox> begin;
BEGIN
[Alice] sandbox> lock table toys in access exclusive mode;
LOCK TABLE

Хотя Алиса и не взяла ни одной игрушки, Боб всё равно должен ждать.

[Bob] sandbox> begin; update toys set usage = usage + 1 where id = 2;
BEGIN

Таблица блокировок теперь выглядит так:

  locktype  | relation |        mode         | tid | vtid  |  pid  | granted
------------+----------+---------------------+-----+-------+-------+---------
 virtualxid |          | ExclusiveLock       |     | 6/284 | 45265 | t
 virtualxid |          | ExclusiveLock       |     | 1/294 | 45263 | t
 relation   | toys     | RowExclusiveLock    |     | 6/284 | 45265 | f
 relation   | toys     | AccessExclusiveLock |     | 1/294 | 45263 | t
(4 rows)

Поскольку Алиса удерживает AccessExclusiveLock без изменения состояния базы данных, то она не получила свой transactionid. У Боба его тоже нет, потому что он не получил RowExclusiveLock на таблицу toys. В этой ситуации, запрос для отображения блокировок который мы использовали ранее, нам не поможет, т. к. он использует объединение по transactionid.

 blocked_pid | blocked_query | blocking_pid | blocking_query
-------------+---------------+--------------+----------------
(0 rows)

Таким образом, Ева думает, что всё хорошо, в то время как следующий запрос:

select pid, query, now() - query_start as waiting_duration
from pg_catalog.pg_stat_activity
where datname = 'sandbox'
  and waiting;
  pid  |                      query                      | waiting_duration
-------+-------------------------------------------------+------------------
 35929 | update toys set usage = usage + 1 where id = 2; | 00:01:34.519518
(1 row)

показывает обратное. Обратите внимание на столбец waiting_duration, он рассчитывается как разница между now() и query_start. Благодаря этому, видно сколько времени запрос уже висит.

Сделав объединение по столбцам relation и locktype, мы снова можем видеть кто кого блокирует:

select
  bgl.relation::regclass,
  bda.pid as blocked_pid,
  bda.query as blocked_query,
  bdl.mode as blocked_mode,
  bga.pid AS blocking_pid,
  bga.query as blocking_query,
  bgl.mode as blocking_mode
from pg_catalog.pg_locks bdl
  join pg_stat_activity bda
    on bda.pid = bdl.pid
  join pg_catalog.pg_locks bgl
    on bdl.pid != bgl.pid
    and bgl.relation = bdl.relation
    and bgl.locktype = bdl.locktype
  join pg_stat_activity bga
    on bga.pid = bgl.pid
where not bdl.granted
  and bga.datname = 'sandbox';
 relation | blocked_pid |                  blocked_query                  |   blocked_mode   | blocking_pid |              blocking_query               |    blocking_mode
----------+-------------+-------------------------------------------------+------------------+--------------+-------------------------------------------+---------------------
 toys     |       35929 | update toys set usage = usage + 1 where id = 2; | RowExclusiveLock |        35937 | lock table toys in access exclusive mode; | AccessExclusiveLock
(1 row)

Алисе было сказано, что некрасиво делать явную блокировку без видимой на то причины. Она фиксирует свою транзакцию без каких-либо изменений, а Боб может взять игрушку.

[Alice] sandbox> commit;
COMMIT

UPDATE 1
[Bob] sandbox>

Но его транзакция всё ещё открыта. Если мы посмотрим в таблицу блокировок, то увидим следующее:

   locktype    | relation  |       mode       |  tid  | vtid |  pid  | granted
---------------+-----------+------------------+-------+------+-------+---------
 relation      | toys_pkey | RowExclusiveLock |       | 4/51 | 35929 | t
 virtualxid    |           | ExclusiveLock    |       | 4/51 | 35929 | t
 relation      | toys      | RowExclusiveLock |       | 4/51 | 35929 | t
 transactionid |           | ExclusiveLock    | 19307 | 4/51 | 35929 | t
(4 rows)

Лишь после того как Боб получил RowExclusiveLock, к его транзакции был добавлен transactionid. Боб рад и делает коммит:

[Bob] sandbox> commit;
COMMIT

RowExclusiveLock

Поскольку Алиса не знает какую игрушку она хочет взять, а ставить явную блокировку ей не разрешили, она пробует другой подход:

[Alice] sandbox> begin; select * from toys for update;
BEGIN

 id |  name  | usage
----+--------+-------
  2 | digger |     1
  3 | shovel |     0
  1 | car    |     1
(3 rows)

На детском языке это бы звучало примерно так: «Хочу видеть все игрушки и может быть я возьму одну, но пока не знаю какую. А до тех пор я не хочу чтобы кто-то другой прикасался к ним».

Тем временем Боб хочет взять лопатку, но конечно не может этого сделать, его транзакция подвисает:

[Bob] sandbox> begin; update toys set usage = usage + 1 where id = 3;
BEGIN

Ева видит следующую ситуацию:

   locktype    | relation  |       mode       |  tid  | vtid |  pid  | granted
---------------+-----------+------------------+-------+------+-------+---------
 transactionid |           | ShareLock        | 19309 | 4/55 | 35929 | f
 relation      | toys      | RowExclusiveLock |       | 4/55 | 35929 | t
 virtualxid    |           | ExclusiveLock    |       | 4/55 | 35929 | t
 transactionid |           | ExclusiveLock    | 19310 | 4/55 | 35929 | t
 tuple         | toys      | ExclusiveLock    |       | 4/55 | 35929 | t
 relation      | toys_pkey | RowExclusiveLock |       | 4/55 | 35929 | t
 relation      | toys      | RowShareLock     |       | 5/17 | 35937 | t
 virtualxid    |           | ExclusiveLock    |       | 5/17 | 35937 | t
 relation      | toys_pkey | AccessShareLock  |       | 5/17 | 35937 | t
 transactionid |           | ExclusiveLock    | 19309 | 5/17 | 35937 | t
(10 rows)

Боб совершенно ясно хочет изменить состояние базы данных поэтому он получил transactionid равный 19310, но снова вынужден ждать получения ShareLock на транзакцию Алисы с номером 19309.

Объединяем блокировки и активности

Пришло время объединить таблицу блокировок и таблицу активности вместе, так, чтобы всегда видеть кто кого заблокировал:

select
  coalesce(bgl.relation::regclass::text, bgl.locktype) as locked_item,
  now() - bda.query_start as waiting_duration,
  bda.pid as blocked_pid,
  bda.query as blocked_query,
  bdl.mode as blocked_mode,
  bga.pid as blocking_pid,
  bga.query as blocking_query,
  bgl.mode as blocking_mode
from pg_catalog.pg_locks bdl
  join pg_stat_activity bda
    on bda.pid = bdl.pid
  join pg_catalog.pg_locks bgl
    on bgl.pid != bdl.pid
    and (bgl.transactionid = bdl.transactionid
      or bgl.relation = bdl.relation and bgl.locktype = bdl.locktype)
  join pg_stat_activity bga
    on bga.pid = bgl.pid
    and bga.datid = bda.datid
where not bdl.granted
  and bga.datname = current_database();
  locked_item  | waiting_duration | blocked_pid |                  blocked_query                  | blocked_mode | blocking_pid |         blocking_query         | blocking_mode
---------------+------------------+-------------+-------------------------------------------------+--------------+--------------+--------------------------------+---------------
 transactionid | 00:03:32.330397  |       35929 | update toys set usage = usage + 1 where id = 3; | ShareLock    |        35937 | select * from toys for update; | ExclusiveLock
(1 row)

Для оценки времени блокирования запроса был добавлен столбец waiting_duration, а также функция current_database() используемая в условии.

Ева не может запомнить этот длиннющий запрос и создаёт представление:

create view lock_monitor as (
  select
    coalesce(bgl.relation::regclass::text, bgl.locktype) as locked_item,
    now() - bda.query_start as waiting_duration,
    bda.pid as blocked_pid,
    bda.query as blocked_query,
    bdl.mode as blocked_mode,
    bga.pid as blocking_pid,
    bga.query as blocking_query,
    bgl.mode as blocking_mode
  from pg_catalog.pg_locks bdl
    join pg_stat_activity bda
      on bda.pid = bdl.pid
    join pg_catalog.pg_locks bgl
      on bgl.pid != bdl.pid
      and (bgl.transactionid = bdl.transactionid
        or bgl.relation = bdl.relation and bgl.locktype = bdl.locktype)
    join pg_stat_activity bga
      on bga.pid = bgl.pid
      and bga.datid = bda.datid
  where not bdl.granted
    and bga.datname = current_database()
);

С помощью него, она легко узнает что задумали её дети:

[eve] sandbox> select * from lock_monitor;

  locked_item  | waiting_duration | blocked_pid |                  blocked_query                  | blocked_mode | blocking_pid |         blocking_query         | blocking_mode
---------------+------------------+-------------+-------------------------------------------------+--------------+--------------+--------------------------------+---------------
 transactionid | 00:06:19.986426  |       35929 | update toys set usage = usage + 1 where id = 3; | ShareLock    |        35937 | select * from toys for update; | ExclusiveLock
(1 row)

Выпив чашку чая и успокоившись, Ева решает почитать руководство по явным блокировкам в постгресе, узнать какие бывают виды блокировок и то, как они конфликтуют друг с другом.