Как реализовать флаг «по умолчанию», который может быть установлен только в одной строке

31

Например, с таблицей, подобной этой:

create table foo(bar int identity, chk char(1) check (chk in('Y', 'N')));

Не имеет значения, реализован ли флаг как a char(1), a bitили как угодно. Я просто хочу иметь возможность применять ограничение, которое может быть установлено только в одной строке.

Джек Дуглас
источник
вдохновлен этим вопросом, который ограничивается MySQL
Джек Дуглас
2
Формулировка вопроса предполагает, что использование таблицы должно быть неправильным ответом. Но иногда (чаще всего?) Добавление другой таблицы - хорошая идея. И добавление таблицы совершенно не зависит от базы данных.
Майк Шеррилл 'Cat Recall'

Ответы:

31

SQL Server 2008 - отфильтрованный уникальный индекс

CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
Марк Стори-Смит
источник
16

SQL Server 2000, 2005:

Вы можете воспользоваться тем, что в уникальном индексе допускается только один нуль:

create table t( id int identity, 
                chk1 char(1) not null default 'N' check(chk1 in('Y', 'N')), 
                chk2 as case chk1 when 'Y' then null else id end );
create unique index u_chk on t(chk2);

для 2000, вам может понадобиться SET ARITHABORT ON(спасибо @gbn за эту информацию)

Джек Дуглас
источник
14

Oracle:

Поскольку Oracle не индексирует записи, в которых все проиндексированные столбцы равны нулю, вы можете использовать уникальный индекс на основе функций:

create table foo(bar integer, chk char(1) not null check (chk in('Y', 'N')));
create unique index idx on foo(case when chk='Y' then 'Y' end);

Этот индекс будет индексировать только одну строку максимум.

Зная этот факт, вы также можете реализовать битовый столбец немного иначе:

create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);

Здесь возможные значения для столбца chkбудут Yи NULL. Только одна строка может иметь значениеY.

Винсент Малграт
источник
чк нужно not nullограничение?
Джек Дуглас
@jack: Вы можете добавить not nullограничение, если не хотите иметь значения NULL (это не было понятно из спецификации вопроса). В любом случае только одна строка может иметь значение «Y».
Винсент Малграт
+1 Понятно, что ты имеешь в виду - ты прав, в этом нет необходимости (но, может быть, немного аккуратнее, особенно в сочетании с default)?
Джек Дуглас
2
@jack: Ваше замечание заставило меня понять, что возможна еще более простая возможность, если вы согласитесь с тем, что столбец может быть либо, Yлибо null, смотрите мое обновление.
Винсент Малграт
1
Вариант 2 имеет дополнительное преимущество: индекс будет крошечным при пропуске nulls - возможно, за счет некоторой ясности
Джек Дуглас
13

Я думаю, что это случай правильного структурирования таблиц вашей базы данных. Чтобы сделать его более конкретным, если у вас есть человек с несколькими адресами, и вы хотите, чтобы он был по умолчанию, я думаю, вам следует сохранить addressID адреса по умолчанию в таблице person, а не иметь столбец по умолчанию в таблице адресов:

Person
-------
PersonID
Name
etc.
DefaultAddressID (fk to addressID)

Address
--------
AddressID
Street
City, State, Zip, etc.

Вы можете сделать DefaultAddressID обнуляемым, но таким образом структура обеспечивает соблюдение вашего ограничения.

Decker97
источник
12

MySQL:

create table foo(bar serial, chk boolean unique);
insert into foo(chk) values(null);
insert into foo(chk) values(null);
insert into foo(chk) values(false);
insert into foo(chk) values(true);

select * from foo;
+-----+------+
| bar | chk  |
+-----+------+
|   1 | NULL |
|   2 | NULL |
|   3 |    0 |
|   4 |    1 |
+-----+------+

insert into foo(chk) values(true);
ERROR 1062 (23000): Duplicate entry '1' for key 2
insert into foo(chk) values(false);
ERROR 1062 (23000): Duplicate entry '0' for key 2

Проверочные ограничения игнорируются в MySQL , поэтому мы должны рассматривать nullили falseкак ложные и trueистинные. Максимум 1 строка может иметьchk=true

Вы можете считать это улучшение , чтобы добавить триггер изменения falseв trueна вставки / обновления в качестве обходного для отсутствия проверки ограничения - ИМО это не улучшение , хотя.

Я надеялся, что смогу использовать char (0), потому что это

Также неплохо, когда вам нужен столбец, который может принимать только два значения: столбец, определенный как CHAR (0), NULL занимает только один бит и может принимать только значения NULL и ''

К сожалению, с MyISAM и InnoDB по крайней мере, я получаю

ERROR 1167 (42000): The used storage engine can't index column 'chk'

--редактировать

В конце концов, это не очень хорошее решение, поскольку в MySQL booleanэто синонимtinyint(1) , и поэтому допускает ненулевые значения, отличные от 0 или 1. Возможно, это bitбыл бы лучший выбор.

Джек Дуглас
источник
Это может ответить на мой комментарий к ответу RolandoMySQLDBA: можем ли мы иметь решения MySQL с DRI?
ГБН
Это немного некрасиво из-за того null, что false, true- я действительно задаюсь вопросом, есть ли что-то более аккуратное ...
Джек Дуглас
@Jack - +1 для хорошей попытки в подходе чистого DRI в MySQL.
RolandoMySQLDBA
Я бы посоветовал избегать использования false здесь, поскольку ограничение уникальности позволило бы предоставить только одно такое ложное значение. Если null представляет собой false, то его следует использовать повсеместно - предотвращение false может быть применено, если доступна дополнительная проверка (например, JSR-303 / hibernate-validator).
Стив Чемберс
1
В последних версиях MySQL / MariaDB реализованы виртуальные столбцы, которые, как мне кажется, позволяют использовать несколько более элегантное решение, описанное ниже по адресу dba.stackexchange.com/a/144847/94908
MattW.
10

SQL Server:

Как это сделать:

  1. Лучший способ - фильтрованный индекс. Использует DRI
    SQL Server 2008+

  2. Вычисляем столбец с уникальностью. Использует DRI
    Смотрите ответ Джека Дугласа. SQL Server 2005 и ранее

  3. Индексированное / материализованное представление, похожее на отфильтрованный индекс. Использует DRI
    Все версии.

  4. Курок. Использует код, а не DRI.
    Все версии

Как не сделать это:

  1. Проверьте ограничение с помощью UDF. Это не безопасно для параллелизма и изоляции моментальных снимков.
    Смотреть один два три четыре
ГБН
источник
10

PostgreSQL:

create table foo(bar serial, chk char(1) unique check(chk='Y'));
insert into foo default values;
insert into foo default values;
insert into foo(chk) values('Y');

select * from foo;
 bar | chk
-----+-----
   1 |
   2 |
   3 | Y

insert into foo(chk) values('Y');
ERROR:  duplicate key value violates unique constraint "foo_chk_key"

--редактировать

или (намного лучше) используйте уникальный частичный индекс :

create table foo(bar serial, chk boolean not null default false);
create unique index foo_i on foo(chk) where chk;
insert into foo default values;
insert into foo default values;
insert into foo(chk) values(true);

select * from foo;
 bar | chk
-----+-----
   1 | f
   2 | f
   3 | t
(3 rows)

insert into foo(chk) values(true);
ERROR:  duplicate key value violates unique constraint "foo_i"
Джек Дуглас
источник
6

Такая проблема является еще одной причиной, почему я задал этот вопрос:

Настройки приложения в базе данных

Если у вас есть таблица настроек приложения в вашей базе данных, у вас может быть запись, которая будет ссылаться на идентификатор одной записи, которую вы хотите считать «особой». Затем вы просто посмотрите, какой идентификатор у вашей таблицы настроек, и вам не понадобится целый столбец для установки только одного элемента.

CenterOrbit
источник
Это отличное предложение: оно больше соответствует нормальному дизайну, работает с любой платформой баз данных и проще в реализации.
Ник Чаммас
+1, но учтите, что «целый столбец» может не использовать какое-либо физическое пространство в зависимости от вашей РСУБД :)
Джек Дуглас,
6

Возможные подходы с использованием широко применяемых технологий:

1) Отменить привилегии «писателя» на столе. Создайте процедуры CRUD, обеспечивающие применение ограничения на границах транзакции.

2) 6NF: опустить CHAR(1)столбец. Добавьте таблицу ссылок, ограниченную, чтобы гарантировать, что ее количество элементов не может превышать единицу:

alter table foo ADD UNIQUE (bar);

create table foo_Y
(
 x CHAR(1) DEFAULT 'x' NOT NULL UNIQUE CHECK (x = 'x'), 
 bar int references foo (bar)
);

Измените семантику приложения так, чтобы рассматриваемым «по умолчанию» была строка в новой таблице. Возможно использовать представления для инкапсуляции этой логики.

3) Оставьте CHAR(1)колонку. Добавьте seqцелочисленный столбец. Установите уникальное ограничение seq. Измените семантику приложения так, чтобы рассматриваемое «значение по умолчанию» представляло собой строку, в которой seqзначение равно единице, либо seqзначение наибольшее / наименьшее значение или аналогичное. Возможно использовать представления для инкапсуляции этой логики.

onedaywhen
источник
5

Для тех, кто использует MySQL, есть подходящая хранимая процедура:

DELIMITER $$
DROP PROCEDURE IF EXISTS SetDefaultForZip;
CREATE PROCEDURE SetDefaultForZip (NEWID INT)
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;

    SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
    IF FOUND_TRUE = 1 THEN
        SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
        IF NEWID <> OLDID THEN
            UPDATE PostalCode SET isDefault = FALSE WHERE ID = OLDID;
            UPDATE PostalCode SET isDefault = TRUE  WHERE ID = NEWID;
        END IF;
    ELSE
        UPDATE PostalCode SET isDefault = TRUE WHERE ID = NEWID;
    END IF;
END;
$$
DELIMITER ;

Чтобы убедиться, что ваша таблица чистая и хранимая процедура работает, предполагая, что ID 200 является значением по умолчанию, выполните следующие действия:

ALTER TABLE PostalCode DROP INDEX isDefault_ndx;
UPDATE PostalCodes SET isDefault = FALSE;
ALTER TABLE PostalCode ADD INDEX isDefault_ndx (isDefault);
CALL SetDefaultForZip(200);
SELECT ID FROM PostalCodes WHERE isDefault = TRUE;

Вот триггер, который также помогает:

DELIMITER $$
CREATE TRIGGER postalcodes_bu BEFORE UPDATE ON PostalCodes FOR EACH ROW
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;
    IF NEW.isDefault = TRUE THEN
        SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
        IF FOUND_TRUE = 1 THEN
            SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
            UPDATE PostalCodes SET isDefault = FALSE WHERE ID = OLDID;
        END IF;
    END IF;
END;
$$
DELIMITER ;

Чтобы убедиться, что ваша таблица чистая и триггер работает, предполагая, что ID 200 является значением по умолчанию, выполните следующие действия:

DROP TRIGGER postalcodes_bu;
ALTER TABLE PostalCode DROP INDEX isDefault_ndx;
UPDATE PostalCodes SET isDefault = FALSE;
ALTER TABLE PostalCode ADD INDEX isDefault_ndx (isDefault);
DELIMITER $$
CREATE TRIGGER postalcodes_bu BEFORE UPDATE ON PostalCodes FOR EACH ROW
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;
    IF NEW.isDefault = TRUE THEN
        SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
        IF FOUND_TRUE = 1 THEN
            SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
            UPDATE PostalCodes SET isDefault = FALSE WHERE ID = OLDID;
        END IF;
    END IF;
END;
$$
DELIMITER ;
UPDATE PostalCodes SET isDefault = TRUE WHERE ID = 200;
SELECT ID FROM PostalCodes WHERE isDefault = TRUE;

Попробуйте!

RolandoMySQLDBA
источник
3
Нет ли решения на основе DRI для MySQL? Только код? Мне любопытно, потому что я начинаю использовать MySQL все больше и больше ...
gbn
4

В SQL Server 2000 и более поздних версиях вы можете использовать индексированные представления для реализации сложных (или многостоловых) ограничений, подобных тем, которые вы запрашиваете.
Также Oracle имеет аналогичную реализацию для материализованных представлений с отложенными проверочными ограничениями.

Смотрите мой пост здесь .

spaghettidba
источник
Не могли бы вы дать немного больше «мяса» в этом ответе, например, небольшой фрагмент кода? Прямо сейчас это просто пара общих идей и ссылка.
Ник Чаммас
Было бы немного трудно привести пример здесь. Если вы нажмете на ссылку, вы найдете «мясо», которое вы ищете.
Спагеттидба
3

Стандартный переходный SQL-92, широко применяемый, например, SQL Server 2000 и выше:

Отзовите привилегии «писателя» из таблицы. Создайте два представления для WHERE chk = 'Y'и WHERE chk = 'N'соответственно, в том числе WITH CHECK OPTION. Для WHERE chk = 'Y'представления включите условие поиска, чтобы его мощность не превышала единицу. Предоставьте «писательские» привилегии на представления.

Пример кода для просмотров:

CREATE VIEW foo_chk_N
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'N' 
WITH CHECK OPTION

CREATE VIEW foo_chk_Y
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'Y' 
       AND 1 >= (
                 SELECT COUNT(*)
                   FROM foo AS f2
                  WHERE f2.chk = 'Y'
                )
WITH CHECK OPTION
onedaywhen
источник
даже если ваша СУБД поддерживает это, она будет сериализована как сумасшедшая, поэтому, если у вас более одного пользователя, у вас могут возникнуть проблемы
Джек Дуглас,
если несколько пользователей изменяют одновременно, им придется стоять в очереди (сериализовать) - иногда это нормально, часто нет (например, тяжелые OLTP или длинные транзакции).
Джек Дуглас
3
Спасибо за разъяснение. Я должен сказать, что если несколько пользователей часто устанавливают единственную строку по умолчанию, тогда выбор дизайна (столбец флага в той же таблице) сомнителен.
понедельник,
3

Вот решение для MySQL и MariaDB с использованием виртуальных столбцов, которое немного элегантнее. Требуется MySQL> = 5.7.6 или MariaDB> = 5.2:

MariaDB [db]> create table foo(bar varchar(255), chk boolean);

MariaDB [db]> describe foo;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| bar   | varchar(255) | YES  |     | NULL    |       |
| chk   | tinyint(1)   | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

Создайте виртуальный столбец со значением NULL, если вы не хотите применять ограничение Unique:

MariaDB [db]> ALTER table foo ADD checked_bar varchar(255) as (IF(chk, bar, null)) PERSISTENT UNIQUE;

(Для MySQL используйте STOREDвместо PERSISTENT.)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.01 sec)

MariaDB [salt_dev]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
ERROR 1062 (23000): Duplicate entry 'a' for key 'checked_bar'

MariaDB [db]> insert into foo(bar, chk) values('b', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> select * from foo;
+------+------+-------------+
| bar  | chk  | checked_bar |
+------+------+-------------+
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    1 | a           |
| b    |    1 | b           |
+------+------+-------------+
MattW.
источник
1

Стандарт FULL SQL-92: использовать подзапрос в CHECKограничении, который не реализован широко, например поддерживается в Access2000 (ACE2007, Jet 4.0 и т. Д.) И выше в режиме запросов ANSI-92 .

Пример кода: примечание CHECKограничения в Access всегда на уровне таблицы. Поскольку в CREATE TABLEутверждении в вопросе используется CHECKограничение на уровне строк , его необходимо слегка изменить, добавив запятую:

create table foo(bar int identity, chk char(1), check (chk in('Y', 'N')));

ALTER TABLE foo ADD 
   CHECK (1 >= (
                SELECT COUNT(*) 
                  FROM foo AS f2 
                 WHERE f2.chk = 'Y'
               ));
onedaywhen
источник
1
не годится ни в одной СУРБД, которую я использовал ... предостережения предостаточно
Джек Дуглас
0

Я только просмотрел ответы, поэтому мог пропустить подобный ответ. Идея состоит в том, чтобы использовать сгенерированный столбец, который является либо ПК, либо константой, которая не существует в качестве значения для ПК.

create table foo 
(  bar int not null primary key
,  chk char(1) check (chk in('Y', 'N'))
,  some_name generated always as ( case when chk = 'N' 
                                        then bar 
                                        else -1 
                                   end )
, unique (somename)
);

AFAIK это действительно в SQL2003 (так как вы искали агностическое решение). DB2 допускает это, не уверенный, сколько других поставщиков, которые принимают это.

Леннарт
источник