check constraint being printed without parenthesis - postgresql

I have this DDL:
CREATE TABLE checkout_value (
id BIGSERIAL PRIMARY KEY,
start_value INTEGER,
end_value INTEGER,
);
With an e-commerce in mind, I want to save several ranges of possible values, where future rules will be applied at checkout. examples:
values until $20
values from $400
between $30 and $300
This way, I want to allow one null value, but if both are not null, start_value should be smaller than end_value.
I though about triggers, but I'm trying to do this using a check constraint, this way:
CREATE TABLE checkout_value (
id BIGSERIAL PRIMARY KEY,
start_value INTEGER,
end_value INTEGER,
CHECK
(
(start_value IS NOT NULL AND end_value IS NULL)
OR
(start_value IS NULL AND end_value IS NOT NULL)
OR
(start_value IS NOT NULL AND end_value IS NOT NULL AND end_value > start_value)
)
);
this works! but when I run \d checkout_value, it prints without any parenthesis:
Check constraints:
"checkout_value_check" CHECK (start_value IS NOT NULL AND end_value IS NULL OR start_value IS NULL AND end_value IS NOT NULL OR start_value IS NOT NULL AND end_value IS NOT NULL AND end_value > start_value)
which, without parenthesis, would lead to an unwanted rule. Is this a bug at printing the details of the table? Is there an easier way to apply while documenting these rules in a more explicit way?

AND binds stronger than OR, so both versions are equivalent. PostgreSQL doesn't store the string, but the parsed expression.

Related

Create dependency statistics on not reference columns

Assume following tables:
CREATE TABLE main
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY
);
CREATE TABLE apple
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
main_id INTEGER NOT NULL REFERENCES main(id)
);
CREATE TABLE orange
(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
main_id INTEGER NOT NULL REFERENCES main(id)
);
CREATE TABLE main_history
(
id INTEGER NOT NULL,
history_valid_from TIMESTAMPTZ NOT NULL,
history_valid_to TIMESTAMPTZ
);
CREATE TABLE apple_history
(
id INTEGER NOT NULL,
main_id INTEGER NOT NULL REFERENCES main(id), -- main_history does not have a PK
history_valid_from TIMESTAMPTZ NOT NULL,
history_valid_to TIMESTAMPTZ
);
CREATE TABLE orange_history
(
id INTEGER NOT NULL,
main_id INTEGER NOT NULL REFERENCES main(id), -- main_history does not have a PK
history_valid_from TIMESTAMPTZ NOT NULL,
history_valid_to TIMESTAMPTZ
);
So there is a root (main) and two tables which reference on it. The referenced table hold max. 20 records (in 99% of the cases) for each main record.
The problem starts when I try to retreive the history:
SELECT * FROM main_history
LEFT JOIN apple_history ON apple_history.main_id = main_history.id AND
apple_history.history_valid_from <= '2021-12-20T10:46:52.482620Z' AND
(apple_history.history_valid_to IS NULL OR '2021-12-20T10:46:52.482620Z' < apple_history.history_valid_to)
LEFT JOIN orange_history ON orange_history.main_id = main_history.id AND
orange_history.history_valid_from <= '2021-12-20T10:46:52.482620Z' AND
(orange_history.history_valid_to IS NULL OR '2021-12-20T10:46:52.482620Z' < orange_history.history_valid_to)
WHERE
main_history.id IN (1,2,3,4) AND
main_history.history_valid_from <= '2021-12-20T10:46:52.482620Z' AND
(main_history.history_valid_to IS NULL OR '2021-12-20T10:46:52.482620Z' < main_history.history_valid_to)
The point is: I'm joining now over a non-referencing column. PostgreSQL overestimates the rows up to 20.000 times (see https://explain.dalibo.com/plan/oIF).
Then I tried to
SET enable_hashjoin = off; SET enable_mergejoin = off
because I knew that there are not that much rows --> then it went down to 100msec.
When I change the query to:
SELECT * FROM main
LEFT JOIN apple_history ON apple_history.main_id = main_history.id AND
apple_history.history_valid_from <= '2021-12-20T10:46:52.482620Z' AND
(apple_history.history_valid_to IS NULL OR '2021-12-20T10:46:52.482620Z' < apple_history.history_valid_to)
LEFT JOIN orange_history ON orange_history.main_id = main_history.id AND
orange_history.history_valid_from <= '2021-12-20T10:46:52.482620Z' AND
(orange_history.history_valid_to IS NULL OR '2021-12-20T10:46:52.482620Z' < orange_history.history_valid_to)
WHERE
main.id IN (1,2,3,4)
It can use the original statitics - everything works fine.
My question: Can I create manual statistics for JOINing not REFERENCESed columns? Goal: use main_history as main table in the query without forcing JOIN strategies
You need some indexes.
To replicate 100ms performance all you need to do is create an index on main_history (main_id, valid_from, valid_to) and you should be set.
But why, though? Ok, so when you query main_history WHERE main_history.Id IN (1,2,3,4) postgres
doesn't know how many rows will match
doesn't know where the rows are on disk/in memory
It has no choice but to scan the entire main_history table to find the rows you're looking for. And the same goes for apples_history/oranges_history. It cannot possibly know where the corresponding main_ids are, nor how many there are. Full scans are needed, and Hash-joins are the best choices (since data isn't ordered)
So to help it along, you create indexes on the referenced/referencing columns. They give count and order, and will help postgres choose the correct plan.

PostgreSQL - Multiple constraints

I want to add several CHECK CONSTRAINTS to a PostgreSQL 13 table. In natural language logic is : if a field contain a defined value, an other field must be filled. I have several scenarios to combine. When I add just one constraint it's ok, but when I want to accumulate them, CONSTRAINTS aren't respected and row can't be inserted.
Here is my table:
CREATE TABLE IF NOT EXISTS demo_table
(
uuid uuid NOT NULL DEFAULT uuid_generate_v4(),
id integer NOT NULL DEFAULT nextval('demo_table_id_seq'::regclass),
thematic character varying COLLATE pg_catalog."default",
field_a character varying COLLATE pg_catalog."default",
field_b character varying COLLATE pg_catalog."default",
CONSTRAINT demo_table_pkey PRIMARY KEY (uuid),
CONSTRAINT field_a_check CHECK (thematic::text ~~ 'A'::text AND field_a IS NOT NULL),
CONSTRAINT field_b_check CHECK (thematic::text ~~ 'B'::text AND field_b IS NOT NULL)
)
My expected logic is : when thematic like 'A', field_a can't be NULL or when thematic like 'B' field_b can't be NULL. With this settings I can't add rows because my CONSTRAINTS definitions never check both conditions (field_a IS NOT NULL and field_b IS NOT NULL).
I tried to define an unique CONSTRAINT as suggested in this post, but CHECK CONSTRAINT isn't respected either because parenthesis who isolate conditions aren't saved in the definition.
CREATE TABLE IF NOT EXISTS demo_table
(
uuid uuid NOT NULL DEFAULT uuid_generate_v4(),
id integer NOT NULL DEFAULT nextval('demo_table_id_seq'::regclass),
thematic character varying COLLATE pg_catalog."default",
field_a character varying COLLATE pg_catalog."default",
field_b character varying COLLATE pg_catalog."default",
CONSTRAINT demo_table_pkey PRIMARY KEY (uuid),
CONSTRAINT field_a_b_check CHECK (thematic::text ~~ 'A'::text AND field_a IS NOT NULL OR thematic::text ~~ 'B'::text AND field_b IS NOT NULL)
)
How to combine multiple CONSTRAINTS like (IF ... ) OR (IF ... ) OR (IF ...) ?
The problem with your approach is that your constraints are not full.
For example:
CONSTRAINT field_a_check CHECK (thematic::text ~~ 'A'::text AND field_a IS NOT NULL),
The constraint says "the record is ok if thematic contains 'A' and field_a is not empty". That means the record is not OK otherwise (if it does not contain 'A'). If you appended your checks with "OK otherwise" you could have several of them - no problem:
CONSTRAINT field_a_check CHECK (thematic::text ~~ 'A'::text AND field_a IS NOT NULL OR NOT thematic::text ~~ 'A'::text)
As to why the parenthesis are removed - it's because they are not needed. The AND operator has priority over OR, so the expressions are the same with or without parenthesis.
You are welcome to check the solution at db<>fiddle
You can do like this.
alter table table_1
add constraint ck_only_one check ((col1 is null and col2 is not null) or (col2 is null and col1 is not null));
Separation with parenthesis is to be given for better segregation.

Postgresql conditional check

I've a users table:
id
type
merchant_id
agent_id
...
I want to add a check constraint using the following conditions:
if type == 'MERCHANT' then merchant_id is not null
if type == 'AGENT' then agent_id is not null
How this constraint is implemented?
Update:
I forgot to mention an extra requirement. the user can only have an agent_id or merchant_id.
You may add the following check constraints to the create table statement:
CREATE TABLE users (
id INTEGER,
type VARCHAR(55),
merchant_id INTEGER,
agent_id INTEGER,
...,
CHECK ((type <> 'MERCHANT' OR merchant_id IS NOT NULL) AND
(type <> 'AGENT' OR agent_id IS NOT NULL))
)
You can check constraints in CREATE TABLE command:
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id SERIAL PRIMARY KEY,
type VARCHAR (50),
merchant_id INT (50),
CONSTRAINT if_attribute_then_field_is_not_null
CHECK ( (NOT attribute) OR (field IS NOT NULL) )
);
OR in Alter Table command:
ALTER TABLE users
ADD CONSTRAINT if_attribute_then_field_is_not_null
CHECK ( (NOT attribute) OR (field IS NOT NULL) )
);

A view that shows the name of the server, the id of the instance and the number of active sessions (a session is active if the end timestamp is null)

CREATE TABLE instances(
ser_name VARCHAR(20) NOT NULL,
id INTEGER NOT NULL ,
ser_ip VARCHAR(16) NOT NULL,
status VARCHAR(10) NOT NULL,
creation_ts TIMESTAMP,
CONSTRAINT instance_id PRIMARY KEY(id)
);
CREATE TABLE characters(
nickname VARCHAR(15) NOT NULL,
type VARCHAR(10) NOT NULL,
c_level INTEGER NOT NULL,
game_data VARCHAR(40) NOT NULL,
start_ts TIMESTAMP ,
end_ts TIMESTAMP NULL ,
player_ip VARCHAR(16) NOT NULL,
instance_id INTEGER NOT NULL,
player_username VARCHAR(15),
CONSTRAINT chara_nick PRIMARY KEY(nickname)
);
ALTER TABLE
instances ADD CONSTRAINT ins_ser_name FOREIGN KEY(ser_name) REFERENCES servers(name);
ALTER TABLE
instances ADD CONSTRAINT ins_ser_ip FOREIGN KEY(ser_ip) REFERENCES servers(ip);
ALTER TABLE
characters ADD CONSTRAINT chara_inst_id FOREIGN KEY(instance_id) REFERENCES instances(id);
ALTER TABLE
characters ADD CONSTRAINT chara_player_username FOREIGN KEY(player_username) REFERENCES players(username);
insert into instances values
('serverA','1','138.201.233.18','active','2020-10-20'),
('serverB','2','138.201.233.19','active','2020-10-20'),
('serverE','3','138.201.233.14','active','2020-10-20');
insert into characters values
('characterA','typeA','1','Game data of characterA','2020-07-18 02:12:12','2020-07-18 02:32:30','192.188.11.1','1','nabin123'),
('characterB','typeB','3','Game data of characterB','2020-07-19 02:10:12',null,'192.180.12.1','2','rabin123'),
('characterC','typeC','1','Game data of characterC','2020-07-18 02:12:12',null,'192.189.10.1','3','sabin123'),
('characterD','typeA','1','Game data of characterD','2020-07-18 02:12:12','2020-07-18 02:32:30','192.178.11.1','2','nabin123'),
('characterE','typeB','3','Game data of characterE','2020-07-19 02:10:12',null,'192.190.12.1','1','rabin123'),
('characterF','typeC','1','Game data of characterF','2020-07-18 02:12:12',null,'192.188.10.1','3','sabin123'),
('characterG','typeD','1','Game data of characterG','2020-07-18 02:12:12',null,'192.188.13.1','1','nabin123'),
('characterH','typeD','3','Game data of characterH','2020-07-19 02:10:12',null,'192.180.17.1','2','bipin123'),
('characterI','typeD','1','Game data of characterI','2020-07-18 02:12:12','2020-07-18 02:32:30','192.189.18.1','3','dhiraj123'),
('characterJ','typeD','3','Game data of characterJ','2020-07-18 02:12:12',null,'192.178.19.1','2','prabin123'),
('characterK','typeB','4','Game data of characterK','2020-07-19 02:10:12','2020-07-19 02:11:30','192.190.20.1','1','rabin123'),
('characterL','typeC','2','Game data of characterL','2020-07-18 02:12:12',null,'192.192.11.1','3','sabin123'),
('characterM','typeC','3','Game data of characterM','2020-07-18 02:12:12',null,'192.192.11.1','2','sabin123');
here I need a view that shows the name of the server, the id of the instance and the number of active sessions (a session is active if the end timestamp is null). do my code wrong or something else? i am starting to learn so hoping for positive best answers.
my view
create view active_sessions as
select i.ser_name, i.id, count(end_ts) as active
from instances i, characters c
where i.id=c.instance_id and c.end_ts = null
group by i.ser_name, i.id;
This does not do what you want:
where i.id = c.instance_id and c.end_ts = null
Nothing is equal to null. You need is null to check a value against null.
Also, count(end_ts) will always produce 0, as we know already that end_ts is null, which count() does not consider.
Finally, I would highly recommend using a standard join (with the on keyword), rather than an implicit join (with a comma in the from clause): this old syntax from decades ago should not be used in new code. I think that a left join is closer to what you want (it would also take in account instances that have no character at all).
So:
create view active_sessions as
select i.ser_name, i.id, count(c.nickname) as active
from instances i
left join characters c on i.id = c.instance_id and c.end_ts is null
group by i.ser_name, i.id;

Use a sequence to populate multiple columns with successive values

How can I use default constraints, triggers, or some other mechanism to automatically insert multiple successive values from a sequence into multiple columns on the same row of a table?
A standard use of a sequence in SQL Server is to combine it with default constraints on multiple tables to essentially get a cross-table identity. See for example the section "C. Using a Sequence Number in Multiple Tables" in the Microsoft documentation article "Sequence Numbers".
This works great if you only want to get a single value from the sequence for each row inserted. But sometimes I want to get multiple successive values. So theoretically I would create a sequence and table like this:
CREATE SEQUENCE DocumentationIDs;
CREATE TABLE Product
(
ProductID BIGINT NOT NULL IDENTITY(1, 1) PRIMARY KEY
, ProductName NVARCHAR(100) NOT NULL
, MarketingDocumentationID BIGINT NOT NULL DEFAULT ( NEXT VALUE FOR DocumentationIDs )
, TechnicalDocumentationID BIGINT NOT NULL DEFAULT ( NEXT VALUE FOR DocumentationIDs )
, InternalDocumentationID BIGINT NOT NULL DEFAULT ( NEXT VALUE FOR DocumentationIDs )
);
Unfortunately this will insert the same value in all three columns. This is by design:
If there are multiple instances of the NEXT VALUE FOR function specifying the same sequence generator within a single Transact-SQL statement, all those instances return the same value for a given row processed by that Transact-SQL statement. This behavior is consistent with the ANSI standard.
Increment by hack
The only suggestion I could find online was to use a hack where you have the sequence increment by the number of columns you need to insert (three in my contrived example) and manually add to the NEXT VALUE FOR function in the default constraint:
CREATE SEQUENCE DocumentationIDs START WITH 1 INCREMENT BY 3;
CREATE TABLE Product
(
ProductID BIGINT NOT NULL IDENTITY(1, 1) PRIMARY KEY
, ProductName NVARCHAR(100) NOT NULL
, MarketingDocumentationID BIGINT NOT NULL DEFAULT ( NEXT VALUE FOR DocumentationIDs )
, TechnicalDocumentationID BIGINT NOT NULL DEFAULT ( ( NEXT VALUE FOR DocumentationIDs ) + 1 )
, InternalDocumentationID BIGINT NOT NULL DEFAULT ( ( NEXT VALUE FOR DocumentationIDs ) + 2 )
)
This does not work for me because not all tables using my sequence require the same number of values.
One possible way using AFTER INSERT trigger is following.
Table definition need to be changed slighlty (DocumentationID columns should be defaulted to 0, or allowed to be nullable):
CREATE TABLE Product
(
ProductID BIGINT NOT NULL IDENTITY(1, 1)
, ProductName NVARCHAR(100) NOT NULL
, MarketingDocumentationID BIGINT NOT NULL CONSTRAINT DF_Product_1 DEFAULT (0)
, TechnicalDocumentationID BIGINT NOT NULL CONSTRAINT DF_Product_2 DEFAULT (0)
, InternalDocumentationID BIGINT NOT NULL CONSTRAINT DF_Product_3 DEFAULT (0)
, CONSTRAINT PK_Product PRIMARY KEY (ProductID)
);
And the trigger doing the job is following:
CREATE TRIGGER Product_AfterInsert ON Product
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM INSERTED)
RETURN;
CREATE TABLE #DocIDs
(
ProductID BIGINT NOT NULL
, Num INT NOT NULL
, DocID BIGINT NOT NULL
, PRIMARY KEY (ProductID, Num)
);
INSERT INTO #DocIDs (ProductID, Num, DocID)
SELECT
i.ProductID
, r.n
, NEXT VALUE FOR DocumentationIDs OVER (ORDER BY i.ProductID, r.n)
FROM INSERTED i
CROSS APPLY (VALUES (1), (2), (3)) r(n)
;
WITH Docs (ProductID, MarketingDocID, TechnicalDocID, InternalDocID)
AS (
SELECT ProductID, [1], [2], [3]
FROM #DocIDs d
PIVOT (MAX(DocID) FOR Num IN ([1], [2], [3])) pvt
)
UPDATE p
SET
p.MarketingDocumentationID = d.MarketingDocID
, p.TechnicalDocumentationID = d.TechnicalDocID
, p.InternalDocumentationID = d.InternalDocID
FROM Product p
JOIN Docs d ON d.ProductID = p.ProductID
;
END