BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Bob';
COMMIT; -- фиксация, если все хорошо
ROLLBACK; -- откат, если что-то пошло не так
ACID описывает требования к транзакционной системе, обеспечивающие наиболее надёжную и предсказуемую её работу. Требования ACID были в основном сформулированы в конце 70-х годов Джимом Греем.
Атомарность гарантирует, что никакая транзакция не будет зафиксирована в системе частично. Будут либо выполнены все её подоперации, либо не выполнено ни одной.
Транзакция, достигающая своего нормального завершения и, фиксирующая свои результаты, сохраняет согласованность базы данных.
Другими словами, каждая успешная транзакция по определению фиксирует только допустимые результаты.
Во время выполнения транзакции параллельные транзакции не должны оказывать влияние на её результат.
Независимо от проблем на нижних уровнях (к примеру, обесточивание системы или сбои в оборудовании) изменения, сделанные успешно завершённой транзакцией, должны остаться сохранёнными после возвращения системы в работу.
В простейшем случае журнализация изменений заключается в последовательной записи всех изменений, выполняемых в базе данных.
Записывается следующая информация:
Журнал содержит отметки начала и завершения транзакции, и отметки принятия контрольной точки.
Общий алгоритм:
Минимальное время транзакции не меньше времени сброса данных на диск.
| Тип | Устройство | IOPS | Интерфейс |
|---|---|---|---|
| HDD | 7,200 об/мин SATA-диски | ~75-100 | SATA 3 Гбит/с |
| HDD | 10,000 об/мин SATA-диски | ~125-150 | SATA 3 Гбит/с |
| HDD | 10,000 об/мин SAS-диски | ~140 | SAS |
| HDD | 15,000 об/мин SAS-диски | ~175-210 | SAS |
| SSD | Intel X25-M G2 MLC | ~8 600 | SATA 3 Гбит/с |
| SSD | OCZ Vertex 3 | ~60 000 (4K) | SATA 6 Гбит/с |
| SSD | OCZ Vertex 3 MAX IOPS | ~75 000 (4K) | SATA 6 Гбит/с |
| SSD | OCZ Vertex 4 | ~120 000 IOPS (4K) | SATA 6 Гбит/с |
| SSD | OCZ RevoDrive 3 X2 | ~200 000 IOPS (4K) | PCIe |
| SSD | OCZ Z-Drive R4 CloudServ | ~500 000 IOPS | PCIe |
На базе журнала транзакций так же реализуется ряд дополнительных возможностей:
| id | name | notes |
|---|---|---|
| 1 | Alice | Great at programming |
| 2 | Bob | Always talking to alice |
| 3 | Eve | Listens to everyone's conversations |
| id | name | notes |
|---|---|---|
| 1 | Alice |
read
Great at programming
|
| 2 | Bob | Always talking to alice |
| 3 | Eve | Listens to everyone's conversations |
| id | name | notes |
|---|---|---|
| 1 | Alice | Great at programming |
|
~ update
2
|
Bob | Always talking to alice |
| 3 | Eve | Listens to everyone's conversations |
| id | name | notes |
|---|---|---|
| 1 | Alice | Great at programming |
|
~ update
2
|
Bob | Working very hard |
| 3 | Eve | Listens to everyone's conversations |
| id | name | notes |
|---|---|---|
| 1 | Alice | Great at programming |
| 2 | Bob | Working very hard |
| 3 | Eve | Listens to everyone's conversations |
|
+ insert
4
|
Dave | Very promising new-hire |
| id | name | notes |
|---|---|---|
| 1 | Alice | Great at programming |
| 2 | Bob | Working very hard |
|
- delete
3
|
Eve | Listens to everyone's conversations |
| 4 | Dave | Very promising new-hire |
| id | name | notes |
|---|---|---|
| 1 | Alice | Great at programming |
| 2 | Bob | Working very hard |
| 4 | Dave | Very promising new-hire |
| id | name | notes |
|---|---|---|
| 1 | Alice |
read
Great at programming
|
|
+ update
2
|
Bob | Working very hard |
|
- delete
3
|
Eve | Listens to everyone's conversations |
|
+ insert
4
|
Dave | Very promising new-hire |
MVCC – механизм, позволяющий каждой транзакции видеть свой «слепок» (snapshot) базы данных на определенный момент времени, хотя данные на текущий момент уже могли измениться.
В PostgreSQL MVCC обеспечивается тем, что данные не удаляются, старые версии строк остаются с отметками об окончании их актуальности, параллельно заводятся новые версии строк. Специальный процесс VACUUM удаляет старые версии строк.
|
TXID: 103
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
| 101 | 0 | 2 | Bob | Always talk to alice |
| 102 | 0 | 3 | Eve | Listens to everyone's conversations |
|
TXID: 103
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
|
- update
101
|
0 | 2 | Bob | Always talk to alice |
| 102 | 0 | 3 | Eve | Listens to everyone's conversations |
|
TXID: 103
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
|
- update
101
|
103 | 2 | Bob | Always talk to alice |
| 102 | 0 | 3 | Eve | Listens to everyone's conversations |
|
TXID: 103
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
|
- update
101
|
103 | 2 | Bob | Always talk to alice |
| 102 | 0 | 3 | Eve | Listens to everyone's conversations |
|
+ update
103
|
0 | 2 | Bob | Working very hard |
|
TXID: 104
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
| 101 | 103 | 2 | Bob | Always talk to alice |
| 102 | 0 | 3 | Eve | Listens to everyone's conversations |
| 103 | 0 | 2 | Bob | Working very hard |
|
TXID: 104
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
| 101 | 103 | 2 | Bob | Always talk to alice |
| 102 | 0 | 3 | Eve | Listens to everyone's conversations |
| 103 | 0 | 2 | Bob | Working very hard |
|
+ insert
104
|
0 | 4 | Dave | Very promising new-hire |
|
TXID: 105
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
| 101 | 103 | 2 | Bob | Always talk to alice |
| 102 | 0 | 3 | Eve | Listens to everyone's conversations |
| 103 | 0 | 2 | Bob | Working very hard |
| 104 | 0 | 4 | Dave | Very promising new-hire |
|
TXID: 105
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
| 101 | 103 | 2 | Bob | Always talk to alice |
|
- delete
102
|
105 | 3 | Eve | Listens to everyone's conversations |
| 103 | 0 | 2 | Bob | Working very hard |
| 104 | 0 | 4 | Dave | Very promising new-hire |
|
TXID: 106
xmin
|
xmax | id | name | notes |
|---|---|---|---|---|
| 100 | 0 | 1 | Alice | Great at programming |
| 101 | 103 | 2 | Bob | Always talk to alice |
| 102 | 105 | 3 | Eve | Listens to everyone's conversations |
| 103 | 0 | 2 | Bob | Working very hard |
| 104 | 0 | 4 | Dave | Very promising new-hire |
| Уровень изоляции | Потерянное обновление | «Грязное» чтение | Неповторяющееся чтение | Фантомное чтение | Аномалии сериализации |
|---|---|---|---|---|---|
| Read uncommited | не возможно | допускается | возможно | возможно | возможно |
| Read commited | не возможно | не возможно | возможно | возможно | возможно |
| Repeatable read | не возможно | не возможно | не возможно | допускается | возможно |
| Serializable | не возможно | не возможно | не возможно | не возможно | не возможно |
Потерянное обновление происходит в случае перезатирания изменений другой транзакцией до завершения транзакции, сделавшей изменения.
| Транзакция 1 | Транзакция 2 |
|---|---|
|
|
|
Чтение данных, добавленных или изменённых еще не завершенной транзакцией.
| Транзакция 1 | Транзакция 2 |
|---|---|
|
|
|
|
|
|
|
При повторном чтении в рамках одной транзакции ранее прочитанные данные оказываются изменёнными.
| Транзакция 1 | Транзакция 2 |
|---|---|
|
|
|
|
|
|
|
Ситуация, когда при повторном чтении в рамках одной транзакции одна и та же выборка дает разные множества строк.
От неповторяющегося чтения оно отличается тем, что результат повторного обращения к данным изменился не из-за изменения/удаления самих этих данных, а из-за появления новых (фантомных) данных.
| Транзакция 1 | Транзакция 2 |
|---|---|
|
|
|
|
|
|
|
Ситуация, когда параллельное выполнение транзакций приводит к результату, невозможному при последовательном выполнении тех же транзакций.
| Транзакция 1 | Транзакция 2 |
|---|---|
|
|
|
|
|
|
Описанные аномалии и уровни изоляции транзакции является частью стандарта SQL:92.
На самом деле аномалий гораздо больше.
Когда создавался стандарт, ожидалось, что решать проблему аномалий будут блокировки. Но со временем на смену блокировочным протоколам управления транзакциями пришел протокол изоляции на основе снимков (Snapshot Isolation).
В PostgreSQL реализован многоверсионный вариант такого протокола. Что автоматически делает невозможным "грязное чтение"
| Уровень изоляции | Потерянное обновление | «Грязное» чтение | Неповторяющееся чтение | Фантомное чтение | Аномалии сериализации |
|---|---|---|---|---|---|
| Read uncommited | не возможно | допускается | возможно | возможно | возможно |
| Read commited | не возможно | не возможно | возможно | возможно | возможно |
| Repeatable read | не возможно | не возможно | не возможно | допускается | возможно |
| Serializable | не возможно | не возможно | не возможно | не возможно | не возможно |
Более высокий уровень изоляции транзакций уменьшает количество аномалий за счет увеличения количества блокировок и вероятности отката транзакции.
CREATE TABLE test1 (a int);
CREATE PROCEDURE transaction_test1()
AS $$
BEGIN
FOR i IN 0..9 LOOP
INSERT INTO test1 (a) VALUES (i);
IF i % 2 = 0 THEN
RAISE NOTICE 'i=%, txid=% will be committed', i, txid_current();
COMMIT;
ELSE
RAISE NOTICE 'i=%, txid=% will be rolledback', i, txid_current();
ROLLBACK;
END IF;
END LOOP;
END
$$
LANGUAGE PLPGSQL;
В PostgreSQL хранимые процедуры доступны с версии PostgreSQL 11.
В более старых версиях PostgreSQL можно довольствоваться только функциями.
CREATE [ OR REPLACE ] PROCEDURE
name ( [ [ argmode ] [ argname ]
argtype [ { DEFAULT | = } default_expr ] [, ...] ] )
{ LANGUAGE lang_name
| TRANSFORM { FOR TYPE type_name } [, ... ]
| [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
| AS 'obj_file', 'link_symbol'
} ...
DROP FUNCTION IF EXISTS add(integer, integer);
CREATE FUNCTION add(integer, integer) RETURNS integer
AS 'select $1 + $2;'
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
CREATE OR REPLACE FUNCTION inc(i integer) RETURNS integer AS $$
BEGIN
RETURN i + 1;
END;
$$ LANGUAGE plpgsql;
SELECT inc(42), add(2, 3);
CREATE FUNCTION python_max (a integer, b integer)
RETURNS integer
AS $$
if a > b:
return a
return b
$$ LANGUAGE plpythonu;
CREATE FUNCTION perl_max (integer, integer) RETURNS integer AS $$
my ($x, $y) = @_;
if (not defined $x) {
return undef if not defined $y;
return $y;
}
return $x if not defined $y;
return $x if $x > $y;
return $y;
$$ LANGUAGE plperl;
IF boolean-expression THEN
statements
[ ELSIF boolean-expression THEN
statements
...]]
[ ELSE
statements ]
END IF;
LOOP
-- some computations
EXIT WHEN count > 100;
CONTINUE WHEN count < 50;
-- some computations for count IN [50 .. 100]
END LOOP;
WHILE amount_owed > 0 LOOP
-- some computations here
END LOOP;
WHILE NOT done LOOP
-- some computations here
END LOOP;
FOR i IN 1..10 LOOP
-- i will take on the values 1,2,3,4,5,6,7,8,9,10 within the loop
END LOOP;
FOR i IN REVERSE 10..1 LOOP
-- i will take on the values 10,9,8,7,6,5,4,3,2,1 within the loop
END LOOP;
FOR i IN REVERSE 10..1 BY 2 LOOP
-- i will take on the values 10,8,6,4,2 within the loop
END LOOP;
CREATE FUNCTION cs_refresh_mviews() RETURNS integer AS $$
DECLARE
mviews RECORD;
BEGIN
RAISE NOTICE 'Refreshing materialized views...';
FOR mviews IN SELECT * FROM cs_materialized_views ORDER BY sort_key LOOP
-- Now "mviews" has one record from cs_materialized_views
RAISE NOTICE 'Refreshing view %s ...', quote_ident(mviews.mv_name);
EXECUTE format('TRUNCATE TABLE %I', mviews.mv_name);
EXECUTE format('INSERT INTO %I %s', mviews.mv_name, mviews.mv_query);
END LOOP;
RAISE NOTICE 'Done refreshing materialized views.';
RETURN 1;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE foo (fooid INT, foosubid INT, fooname TEXT);
INSERT INTO foo VALUES (1, 2, 'three');
INSERT INTO foo VALUES (4, 5, 'six');
CREATE OR REPLACE FUNCTION get_all_foo() RETURNS SETOF foo AS
$BODY$
DECLARE
r foo%rowtype;
BEGIN
FOR r IN
SELECT * FROM foo WHERE fooid > 0
LOOP
-- can do some processing here
RETURN NEXT r; -- return current row of SELECT
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;
SELECT * FROM get_all_foo();
CREATE [ CONSTRAINT ] TRIGGER name
{ BEFORE | AFTER | INSTEAD OF }{ event [ OR ... ] }
ON table_name
[ FROM referenced_table_name ]
[ NOT DEFERRABLE | [ DEFERRABLE ]
[ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ]
[ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( condition ) ]
EXECUTE PROCEDURE function_name ( arguments )
where event can be one of:
INSERT
UPDATE [ OF column_name [, ... ] ]
DELETE
TRUNCATE
CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
BEGIN
--
-- Create a row in emp_audit to reflect the operation performed on emp,
-- make use of the special variable TG_OP to work out the operation.
--
IF (TG_OP = 'DELETE') THEN
INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;
RETURN NEW;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$emp_audit$ LANGUAGE plpgsql;
CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
FOR EACH ROW EXECUTE PROCEDURE process_emp_audit();
CREATE TABLE IF NOT EXISTS payment
(
id SERIAL NOT NULL PRIMARY KEY,
amount BYTEA NOT NULL,
currency CHAR(3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
);
CREATE TRIGGER modify_payment_updated_at
BEFORE UPDATE
ON payment
FOR EACH ROW
EXECUTE PROCEDURE public.moddatetime(updated_at);
CREATE
[ OR REPLACE ]
[ TEMP | TEMPORARY ]
[ RECURSIVE ]
VIEW name [ ( column_name [, ...] ) ]
[ WITH ( view_option_name [= view_option_value] [, ... ] ) ]
AS query
[ WITH [ CASCADED | LOCAL ] CHECK OPTION ];
CREATE VIEW v AS
SELECT a.id, b.id FROM a, b;
-- ERROR: column "id" specified more than once
-- SQL-состояние: 42701
CREATE VIEW v (a_id, b_id) AS
SELECT a.id, b.id FROM a,b;
CREATE VIEW v AS
SELECT a.id a_id, b.id b_id FROM a,b;
CREATE TABLE films (
imdb varchar(16) PRIMARY KEY,
title varchar(40) NOT NULL,
kind varchar(10)
);
CREATE OR REPLACE VIEW comedies AS
SELECT *
FROM films
WHERE kind = 'Comedy'
WITH CASCADED CHECK OPTION;
INSERT INTO comedies (imdb, title, kind)
VALUES ('tt0114709', 'Toy Story 1', 'Animation');
-- ERROR: new row violates check option for view "comedies"
-- DETAIL: Failing row contains (tt0114709, Toy Story 1, Animation).
INSERT INTO comedies (imdb, title, kind)
VALUES ('tt1156398', 'Zombieland', 'Comedy');
CREATE MATERIALIZED VIEW my_matview AS SELECT * FROM mytab;
CREATE TABLE my_matview AS SELECT * FROM mytab;
Разница состоит в том, что материализованное представление впоследствии нельзя будет изменить непосредственно, а запрос, создающий материализованное представление, сохраняется точно так же, как запрос представления, и получить актуальные данные в материализованном представлении можно так.
REFRESH MATERIALIZED VIEW my_matview;
Это ряд правил, согласно которым сортируются и сравниваются данные.
Простая сортировка обычно даёт не тот результат, который ожидается пользователем.
| ucs_basic (C) | en_US.utf8 (ISO-14651) | ru_RU.utf8 |
|---|---|---|
| _ель | ґвалт | джаз |
| Есенин | джаз | ґвалт |
| джаз | ель | ель |
| ель | _ель | _ель |
| жаркое | Есенин | Есенин |
| ёжик | ёжик | ёжик |
| ґвалт | жаркое | жаркое |
В разных языках разные преобразования в верхний/нижний регистр
SELECT
LOWER('WINDOWS' COLLATE "en_US.utf8"),
UPPER('linux' COLLATE "en_US.utf8")
| lower | upper |
|---|---|
| windows | LINUX |
SELECT
LOWER('WINDOWS' COLLATE "tr_TR.utf8"),
UPPER('linux' COLLATE "tr_TR.utf8")
| lower | upper |
|---|---|
| wındows | LİNUX |
В PostgreSQL поддержка COLLATION реализована на базе C-функции strcoll_l.
Это порождает ряд проблем:
С PostgreSQL 10 (окт 2019) появилась поддержка COLLATION на базе библиотеки ICU.
CREATE TABLE person (
id SERIAL PRIMARY KEY,
name TEXT COLLATE "ru_RU.utf8",
...
);
CREATE UNIQUE INDEX uniq_name ON person (LOWER(name));
SELECT * FROM person WHERE LOWER(name) = LOWER('Jack Sparrow');
Проблемы:
CREATE EXTENSION IF NOT EXISTS citext;
CREATE TABLE person (
id SERIAL PRIMARY KEY,
name CITEXT COLLATE "ru_RU.utf8",
...
);
CREATE UNIQUE INDEX uniq_name ON person (name);
SELECT * FROM person WHERE name = 'Jack Sparrow';