简体   繁体   中英

Partial unique index for global and local uniqueness

Suppose I have a system that stores a company's departments and its locations. Each location has its own specific departments, but there are also global departments that are at every location.

In order to store a department, I use a table that stores a name and the location ID. If the location ID is null , then the department is global.

My issue is with the uniqueness of the name. This uniqueness is dependent on the location ID. The name generally needs to be unique within its own location, ie a simple UNIQUE ("name", "locationId") would suffice. However, the name needs to be globally unique should the location be null . This is not covered by the constraint above. So, my next attempt was using two partial constraints:

CREATE UNIQUE INDEX "locationId_and_name" ON "activities" ("locationId", "name") WHERE "locationId" IS NOT NULL
CREATE UNIQUE INDEX "global_name" ON "activities" ("name") WHERE "locationId" IS NULL

This works, except for the cases where there is already a global department and one tries to create a new location bound department with the same name. For example, the following rows would be allowed to exist at the same time, even though they should not be: ('Name', 'location-1'), ('Name', NULL) .

Is there any way that I could set up the constraints that it works properly?

For clarification, allowed are:

  • ('Name', 'location-1'), ('Name', 'location-2')
  • ('Name 1', 'location-1'), ('Name 2', 'location-1')
  • ('Name 1', 'location-1'), ('Name 2', null)

And not allowed are:

  • ('Name', 'location-1'), ('Name', 'location-1')
  • ('Name 1', 'location-1'), ('Name 1', NULL)
  • ('Name 1', NULL), ('Name 1', null)

The logic is too complex to be implemented with indexes alone. Leave the first index with the columns reordered and use a trigger instead of the second index.

create unique index "name_and_locationId" 
    on "activities" ("name", "locationId") 
    where "locationId" is not null;

create or replace function activities_trigger()
returns trigger language plpgsql as $$
begin
    if 
        new."locationId" is not null and exists(
            select from activities 
            where "name" = new."name" and "locationId" is null)
        or
        new."locationId" is null and exists(
            select from activities 
            where "name" = new."name")
    then raise exception '"%" is a global department.', new."name";
    end if;
    return new;
end $$;

create trigger activities_trigger
before insert or update on activities
for each row execute procedure activities_trigger();

Note that reordering the columns in the index will cause it to support queries in the trigger.

Test it in db<>fiddle.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM