PostgreSQL: предотвратить блокировку самостоятельного обновления таблицы с помощью левого соединения

Я на PostgreSQL 9.3. Я единственный, кто работает с базой данных, и мой код последовательно запускает запросы для модульных тестов.

В большинстве случаев следующий запрос UPDATE выполняется без проблем, но иногда он создает блокировки на сервере PostgreSQL. А то запрос вроде никогда не заканчивается, хотя обычно занимает всего 3 сек. Я должен уточнить, что запрос выполняется в контексте модульного теста, т.е. данные точно такие же, независимо от того, происходит ли блокировка или нет. Код — это единственный процесс, который обновляет данные.

Я знаю, что могут возникнуть проблемы с блокировкой PostgreSQL при использовании запроса на обновление для самообновляющейся таблицы. И больше всего, когда используется LEFT JOIN.

Я также знаю, что запрос LEFT JOIN можно заменить запросом NOT EXISTS для UPDATE, но в моем случае LEFT JOIN намного быстрее, потому что данных для обновления мало, а NOT EXISTS должен посетить почти все кандидаты строк.

Итак, мой вопрос: какие команды PostgreSQL (например, явная блокировка LOCK для таблицы) или параметры (например, SELECT FOR UPDATE) я должен использовать, чтобы обеспечить выполнение моего запроса без бесконечной блокировки.

Запрос:

-- for each places of scenario #1 update all owners that
-- are different from scenario #0
UPDATE t_territories AS upt
SET id_owner = diff.id_owner
FROM (
    -- list of owners in the source that are different from target
    SELECT trg.id_place, src.id_owner
    FROM t_territories AS trg
    LEFT JOIN t_territories AS src
       ON  (src.id_scenario = 0)
       AND (src.id_place = trg.id_place)
    WHERE (trg.id_scenario = 1)
    AND (trg.id_owner IS DISTINCT FROM src.id_owner)
    -- FOR UPDATE -- bug SQL : FOR UPDATE cannot be applied to the nullable side of an outer join
) AS diff
WHERE (upt.id_scenario = 1)
AND (upt.id_place = diff.id_place)

Структура таблицы:

CREATE TABLE t_territories
(
  id_scenario integer NOT NULL,
  id_place integer NOT NULL,
  id_owner integer,
  CONSTRAINT t_territories_pk PRIMARY KEY (id_scenario, id_place),
  CONSTRAINT t_territories_fkey_owner FOREIGN KEY (id_owner)
      REFERENCES t_owner (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE RESTRICT
)

person Skrol29    schedule 29.01.2018    source источник
comment
Один только сеанс никогда не может быть заблокирован. Проверьте pg_locks из второго сеанса. Также проверьте, выполняется ли запрос, т. е. потребляет ли процесс процессорное время.   -  person Laurenz Albe    schedule 29.01.2018
comment
IIUC, ваш подзапрос может выдавать более одного значения для каждого id_place, вызывая повторяющиеся обновления для {id_scenario,id_place} (и ваш trg-кортеж в подзапросе ссылается на точно такой же кортеж, что и upt в целевом RTE)   -  person joop    schedule 29.01.2018
comment
Но если вы намерены установить upt.id_owner в NULL, если не существует соответствующей строки с id_scenario=1, было бы проще обновить IMO из скалярного (коррелированного) подзапроса.   -  person joop    schedule 29.01.2018
comment
@joop «ваш подзапрос может дать более одного значения для каждого id_place» Я так не думаю, потому что (id_scenario, id_place) является первичным ключом, поэтому подзапрос получает только список id_place для сценария № 1. И самое главное, в большинстве случаев запрос выполняется нормально с точно таким же (это сценарий модульного тестирования).   -  person Skrol29    schedule 29.01.2018
comment
@LaurenzAlbe: блокировка эффективно происходит, когда есть другой сеанс, но только сеанс pgAdmin III для браузера базы данных и запрос SELECT, который завершен, и оба находятся в состоянии idle. Но это запросы SELECT и плюс есть неактивные. Вот информация о блокировках от pg_locks и pgAdmin 4. Для меня не очень полезная. imgur.com/a/JBYha imgur.com/a/kUIeu   -  person Skrol29    schedule 29.01.2018
comment
@ Skrol29 Нет замков, которые блокируют (granted = false). Может быть, заявление просто занимает много времени. С другой стороны, возможно, вы смотрели на pg_locks, когда ничего не было заблокировано.   -  person Laurenz Albe    schedule 29.01.2018


Ответы (1)


Я думаю, что ваш запрос был заблокирован другим запросом. Вы можете найти этот запрос по

SELECT
  COALESCE(blockingl.relation::regclass::text,blockingl.locktype) as locked_item,
  now() - blockeda.query_start AS waiting_duration, blockeda.pid AS blocked_pid,
  blockeda.query as blocked_query, blockedl.mode as blocked_mode,
  blockinga.pid AS blocking_pid, blockinga.query as blocking_query,
  blockingl.mode as blocking_mode
FROM pg_catalog.pg_locks blockedl
JOIN pg_stat_activity blockeda ON blockedl.pid = blockeda.pid
JOIN pg_catalog.pg_locks blockingl ON(
  ( (blockingl.transactionid=blockedl.transactionid) OR
  (blockingl.relation=blockedl.relation AND blockingl.locktype=blockedl.locktype)
  ) AND blockedl.pid != blockingl.pid)
JOIN pg_stat_activity blockinga ON blockingl.pid = blockinga.pid
  AND blockinga.datid = blockeda.datid
WHERE NOT blockedl.granted
AND blockinga.datname = current_database()

Этот запрос я нашел здесь http://big-elephants.com/2013-09/exploring-query-locks-in-postgres/

Также можно использовать ACCESS EXCLUSIVE LOCK, чтобы предотвратить любой запрос на чтение и запись таблицы t_territories.

LOCK t_territories IN ACCESS EXCLUSIVE MODE;

Подробнее о блокировках здесь https://www.postgresql.org/docs/9.1/static/explicit-locking.html

person Eugenij    schedule 29.01.2018
comment
К сожалению, ваш запрос не возвращает строк, когда происходит блокировка, и я вижу это с помощью pg_locks и pgAdmin 4. Запрос ничего не возвращает, даже если я удаляю предложение WHERE. - person Skrol29; 29.01.2018