Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1n9s38-0003rN-BQ for pgsql-performance@arkaria.postgresql.org; Tue, 18 Jan 2022 17:13:38 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1n9s37-0003Kt-3Q for pgsql-performance@arkaria.postgresql.org; Tue, 18 Jan 2022 17:13:37 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1n9s36-0003Kj-OP for pgsql-performance@lists.postgresql.org; Tue, 18 Jan 2022 17:13:36 +0000 Received: from mail-qv1-xf2e.google.com ([2607:f8b0:4864:20::f2e]) by magus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1n9s32-0008Qe-LW for pgsql-performance@lists.postgresql.org; Tue, 18 Jan 2022 17:13:36 +0000 Received: by mail-qv1-xf2e.google.com with SMTP id t7so22091720qvj.0 for ; Tue, 18 Jan 2022 09:13:32 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=message-id:date:mime-version:user-agent:to:content-language:from :subject:content-transfer-encoding; bh=g/yaMSDNE2TSPIl2RuzxGQI/hzCS8uRyDN5/+KndFeg=; b=DICgwBmD+dhjrg+eVqXZWO/z0+Tg+5RTI+F1DyHIFr4bpPWjYVFwTBKr0waMKfWbLk xW6YOkoDUJ1R2U/yLSwkBrj4/9nT959KK9ufWDCP62QdJtUWTPw6KO8WnMA9yL2LwO1p XeZrAVNaiGM9q2vGSjhM8HN7SI0n3rx1yzNZ5ZSkdZ6CYIBUIr7oZijRNU5OkX+I3gFF /GCpb07flEfZXS8H2hJ8KIll1gEjIBeqTyIDpQJeqbYVWfHTW7LZldLmaOIrjx5ujxpN 4c05iOw5eBWA8j2uc7JI7bF5huflI3oeIT8ZF/YuqS6vk11t+bb2U5JVIpTusI/PngYa WEgg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:message-id:date:mime-version:user-agent:to :content-language:from:subject:content-transfer-encoding; bh=g/yaMSDNE2TSPIl2RuzxGQI/hzCS8uRyDN5/+KndFeg=; b=4BCjhiEWSc4TW8h7LaKOIlq4qKOskzsDMn/GC9geNKLNKDAZzlbWJjRXqkmu4TSylT R2T2+1SYPwOHFVqgobWZQcCIohuq877HmMKtDpRagmGEokkF+HUhejeuJoXgbo6R/x4d spqSRoVhR33dMWb8NRRHt0wSCUf7qlzy6kznb98OfhaHhi/X1YpqDnAooIkB8cidayUh YoK0E3V/e9+q1v5zt2GjZlWLl2XPq9wXtl237Djg88+ihj6QzWHV0bngPF8IjFpy+FkI ZKE/JiviaojuerfQBe28bzoygl8/CPlotKyDf0tqDbKb9KeWHSTdQBvI/qpp4Z7/+K5k 21Nw== X-Gm-Message-State: AOAM532Ni10P6lz8uVV0K+8zYRDv2HYbiImZJfnQAnA5+uVaCNmYc8M3 7wDQMLmEDJuT6Ce77KhiHxDdt2bS56M= X-Google-Smtp-Source: ABdhPJzCwIwO1ubeIhmAhQ6HFT0/qM10e/5PQcsMfixfkASTsr5CV7XXp4Qr77XYOq4iZuGJO8JGBg== X-Received: by 2002:a05:6214:761:: with SMTP id f1mr24124464qvz.106.1642526010208; Tue, 18 Jan 2022 09:13:30 -0800 (PST) Received: from [192.168.2.100] (pool-72-79-19-93.nwrknj.east.verizon.net. [72.79.19.93]) by smtp.gmail.com with ESMTPSA id k6sm3725392qko.46.2022.01.18.09.13.28 for (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Tue, 18 Jan 2022 09:13:29 -0800 (PST) Message-ID: Date: Tue, 18 Jan 2022 12:13:27 -0500 MIME-Version: 1.0 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Thunderbird/91.4.0 To: "pgsql-performance@lists.postgresql.org" Content-Language: en-US From: Mladen Gogala Subject: Unique constraint blues Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk Postgres version is 13.5, platform is Oracle Linux 8.5, x86_64. Here is the problem: mgogala=# create table test1(col1 integer,col2 varchar(10)); CREATE TABLE mgogala=# alter table test1 add constraint test1_uq unique(col1,col2); ALTER TABLE mgogala=# insert into test1 values(1,null); INSERT 0 1 mgogala=# insert into test1 values(1,null); INSERT 0 1 mgogala=# select * from test1;  col1 | col2 ------+------     1 |     1 | (2 rows) So, my unique constraint doesn't work if one of the columns is null. Bruce Momjian to the rescue: https://blog.toadworld.com/2017/07/12/allowing-only-one-null Let's see what happens: mgogala=# truncate table test1; TRUNCATE TABLE mgogala=# alter table test1 drop constraint test1_uq; ALTER TABLE mgogala=# create unique index test1_uq on test1(col1,(col2 is null)) where col2 is null; CREATE INDEX mgogala=# insert into test1 values(1,null); INSERT 0 1 mgogala=# insert into test1 values(1,null); ERROR:  duplicate key value violates unique constraint "test1_uq" DETAIL:  Key (col1, (col2 IS NULL))=(1, t) already exists. So, this allows only a single NULL value, just what I wanted. However, there is a minor issue: this doesn't work for the general case: mgogala=# insert into test1 values(1,'test1'); INSERT 0 1 mgogala=# insert into test1 values(1,'test1'); INSERT 0 1 mgogala=# select * from test1;  col1 | col2 ------+-------     1 |     1 | test1     1 | test1 (3 rows) I can insert the same row twice, which defeats the purpose. So, let's make the 3d modification: mgogala=# truncate table test1; TRUNCATE TABLE mgogala=# drop index test1_uq; DROP INDEX mgogala=# create unique index test1_uq on test1(col1,coalesce(col2,'*** EMPTY ***')); Using "coalesce" enforces the constraint just the way I need: mgogala=# insert into test1 values(1,null); INSERT 0 1 mgogala=# insert into test1 values(1,null); ERROR:  duplicate key value violates unique constraint "test1_uq" DETAIL:  Key (col1, COALESCE(col2, '*** EMPTY ***'::character varying))=(1, *** EMPTY ***) already exists. mgogala=# insert into test1 values(1,'test1'); INSERT 0 1 mgogala=# insert into test1 values(1,'test1'); ERROR:  duplicate key value violates unique constraint "test1_uq" DETAIL:  Key (col1, COALESCE(col2, '*** EMPTY ***'::character varying))=(1, test1) already exists. mgogala=# Now comes the greatest mystery of them all: explain (analyze,verbose) select * from test1 where col1=1 and col2='test1';                                                    QUERY PLAN -------------------------------------------------------------------------------- ---------------------------------  Bitmap Heap Scan on mgogala.test1  (cost=1.70..7.52 rows=1 width=42) (actual ti me=0.023..0.024 rows=1 loops=1)    Output: col1, col2    Recheck Cond: (test1.col1 = 1)    Filter: ((test1.col2)::text = 'test1'::text)    Rows Removed by Filter: 1    Heap Blocks: exact=1    ->  Bitmap Index Scan on test1_uq  (cost=0.00..1.70 rows=6 width=0) (actual t ime=0.015..0.016 rows=2 loops=1)          Index Cond: (test1.col1 = 1)  Planning Time: 1.184 ms  Execution Time: 0.407 ms (10 rows) How come that the index is used for search without the "coalesce" function? The unique index is a function based index and, in theory, it shouldn't be usable for searches without the function. I don't understand why is this working. I am porting application from Oracle to Postgres and Oracle behaves like this: SQLcl: Release 21.3 Production on Tue Jan 18 11:39:43 2022 Copyright (c) 1982, 2022, Oracle.  All rights reserved. Connected to: Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production Version 19.13.0.0.0 Elapsed: 00:00:00.001 SQL> create table test1(col1 integer,col2 varchar2(10)); Table TEST1 created. Elapsed: 00:00:00.050 SQL> alter table test1 add constraint test1_uq unique(col1,col2); Table TEST1 altered. Elapsed: 00:00:00.139 SQL> insert into test1 values(1,null); 1 row inserted. Elapsed: 00:00:00.026 SQL> insert into test1 values(1,null); Error starting at line : 1 in command - insert into test1 values(1,null) Error report - ORA-00001: unique constraint (SCOTT.TEST1_UQ) violated Elapsed: 00:00:00.033 Oracle is rejecting the same row twice, regardless of whether it contains NULL values or not. As in  Postgres, the resulting index can be used for searches. However, Oracle index is not a function-based index because it doesn't contain the coalesce function. -- Mladen Gogala Database Consultant Tel: (347) 321-1217 https://dbwhisperer.wordpress.com