postgres generated columns virtual columns default - ghdrako/doc_snipets GitHub Wiki
Generowanie wartości kolumn
- wartości wyliczanie po wstawieniu
DEFAULT
- Identity (v10) –
GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY
- Expression (v12) –
GENERATED ALWAYS AS ( generation_expr ) STORED
Kolumna domyślnie przyjmuje wartość NULL, jeśli podczas wstawiania wiersza nie podano żadnej wartości. Istnieją jednak trzy przypadki, w których silnik może podstawić inną wartość. Najbardziej znanym jest klauzula DEFAULT
. W tym przypadku wstawiona wartość odpowiada wartości określonej w tej klauzuli, jeśli dla kolumny nie określono żadnej wartości. Jeżeli określono wartość, to zastępuje ona wartość domyślną. Poniższy przykład to pokazuje
CREATE TABLE t2 (c1 integer, c2 integer, c3 integer DEFAULT 10);
INSERT INTO t2 (c1, c2, c3) VALUES (1, 2, 3);
INSERT INTO t2 (c1) VALUES (2);
SELECT * FROM t2;
c1 | c2 | c3
----+----+----
1 | 2 | 3
2 | | 10
Klauzuli DEFAULT nie można stosować z klauzulami złożonymi, w tym z klauzulami zawierającymi zapytania. Zazwyczej do rejestrowania daty wprowadzenia rekordu:
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
Począwszy od PostgreSQL 12, możliwe jest użycie GENERATED ALWAYS AS (wyrażenie) STORED
. Umożliwia to uzyskanie obliczonej wartości dla kolumny, wartości, której nie można przeciążyć ani przy wstawianiu, ani przy aktualizacji (ale która jest przechowywana na dysku).
Jako przykład weźmiemy tabelę kapitanów i dodamy do niej kolumnę zawierającą zmodyfikowaną wersję numeru karty kredytowej jako jej wartość:
ALTER TABLE capitaines ADD COLUMN num_cc_anon text GENERATED ALWAYS AS (substring(num_cartecredit, 0, 10) || '******') STORED;
SELECT nom, num_cartecredit, num_cc_anon FROM capitaines;
nom | num_cartecredit | num_cc_anon
----------------+------------------+-----------------
Robert Surcouf | 1234567890123456 | 123456789******
Haddock | |
INSERT INTO capitaines VALUES (2, 'Joseph Pradere-Niquet', 40, '9876543210987654', '44000', 'Lundi', 'test');
ERROR: cannot insert into column "num_cc_anon" DETAIL: Column "num_cc_anon" is a generated column.
INSERT INTO capitaines VALUES (2, 'Joseph Pradere-Niquet', 40, '9876543210987654', '44000', 'Lundi');
SELECT nom, num_cartecredit, num_cc_anon FROM capitaines;
nom | num_cartecredit | num_cc_anon
-----------------------+------------------+-----------------
Robert Surcouf | 1234567890123456 | 123456789******
Haddock | |
Joseph Pradere-Niquet | 9876543210987654 | 987654321******
GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY
pozwala na uzyskanie kolumny typu id, znacznie lepszej niż to, co oferuje pseudotyp seryjny. Jeśli określono opcję ALWAYS, wartość ta nie jest edytowalna.
ALTER TABLE capitaines ADD COLUMN id2 integer GENERATED ALWAYS AS IDENTITY;
SELECT nom, id2 FROM capitaines;
nom | id2
-----------------------+-----
Robert Surcouf. | 1
Haddock | 2
Joseph Pradere-Niquet | 3
INSERT INTO capitaines (nom) VALUES ('Tom Souville');
SELECT nom, id2 FROM capitaines;
nom | id2
-----------------------+-----
Robert Surcouf | 1
Haddock | 2
Joseph Pradere-Niquet | 3
Tom Souville | 4
Typ szeregowy jest zastępowany typem liczb całkowitych i sekwencją, jak pokazano w poniższym przykładzie. Jest to problem, o ile deklaracja składająca się z tworzenia tabeli daje wynik podstawy, a zatem w eksporcie danych.
CREATE TABLE tserial(s serial);
Table "public.tserial"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+------------------------------------
s | integer | | not null | nextval('tserial_s_seq'::regclass) 84
There are two kinds of generated columns:
- stored (Postgres 12+)
- virtual (not implemented in Postgres
CREATE TABLE people (
...,
height_cm numeric,
height_in numeric GENERATED ALWAYS AS (height_cm / 2.54) STORED
);
CREATE TABLE tbl (
int1 int
, int2 int
, product bigint GENERATED ALWAYS AS (int1 * int2) STORED
);
A generated column cannot be written to directly. In INSERT or UPDATE commands, a value cannot be specified for a generated column, but the keyword DEFAULT may be specified.
Generated column (Postgres 10+)
The SERIAL
data type is unique to PostgreSQL and has some known problems, so starting at version 10, the SERIAL
datatype was softly deprecated in favor of identity
columns:
GENERATED BY DEFAULT
- we can set autoganarated column manualy and cause mess (duplicate value error)
CREATE TABLE sale (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
sold_at TIMESTAMPTZ,
amount INT
);
db=# INSERT INTO sale (sold_at, amount) VALUES (now(), 1000);
INSERT 0 1
db=# SELECT * FROM sale;
id │ sold_at │ amount
────┼───────────────────────────────┼────────
1 │ 2021-09-25 10:11:57.771121+03 │ 1000
db=# INSERT INTO sale (id, sold_at, amount) VALUES (2, now(), 1000);
INSERT 0 1
db=# INSERT INTO sale (sold_at, amount) VALUES (now(), 1000);
ERROR: duplicate key value violates unique constraint "sale_pkey"
DETAIL: Key (id)=(2) already exists.
GENERATED ALWAYS
- not allowed set manulally
CREATE TABLE sale (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sold_at TIMESTAMPTZ,
amount INT
);
db=# INSERT INTO sale (sold_at, amount) VALUES (now(), 1000);
INSERT 0 1
db=# INSERT INTO sale (id, sold_at, amount) VALUES (2, now(), 1000);
ERROR: cannot insert into column "id"
DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.
In the error message, PostgreSQL is kind enough to offer a solution for when you actually do want to set the value for an identity column explicitly:
db=# INSERT INTO sale (id, sold_at, amount)
OVERRIDING SYSTEM VALUE VALUES (2, now(), 1000);
INSERT 0 1
By adding the OVERRIDING SYSTEM VALUE to the INSERT command you explicitly instruct PostgreSQL to allow you to set the value of an identity column. You still have to handle a possible unique constraint violation.
Differences between a column with a default and a generated column.
default | generated column |
---|---|
evaluated once when the row is first inserted if no other value was provided | a generated column is updated whenever the row changes and cannot be overridden |
may not refer to other columns of the table | a generation expression would normally do so |
can use volatile functions, for example random() or functions referring to the current time | volatile functions are not allowed |
CREATE TABLE people (
...,
height_cm numeric,
height_in numeric GENERATED ALWAYS AS (height_cm / 2.54) STORED
);
CREATE TABLE tab_gen
(
username CHARACTER VARYING(20),
userpass CHARACTER VARYING(20),
userhash CHARACTER VARYING(50) GENERATED ALWAYS AS (md5(username||userpass)) STORED
);
'STORED' is required to signify that the column will be computed on write and will be stored on disk.
Equivalent using triggers - worse performance
CREATE OR REPLACE FUNCTION fn_gen_hash()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NEW.userhash IS NOT NULL AND TG_OP='INSERT')
THEN
RAISE EXCEPTION 'cannot insert into the column "userhash". It''s a generated column';
ELSEIF (TG_OP='UPDATE' AND NEW.userhash <> OLD.userhash)
THEN
RAISE EXCEPTION 'cannot update the column "userhash". It''s a generated column';
END IF;
NEW.userhash=md5(NEW.username||NEW.userpass);
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER trg_gen_hash
BEFORE INSERT OR UPDATE
ON tab_trigger
FOR EACH ROW
EXECUTE FUNCTION fn_gen_hash();