Як реалізувати прапор 'за замовчуванням', який можна встановити лише в одному рядку


31

Наприклад, з подібною до цієї таблиці:

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

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


натхненний цим питанням, яке обмежується MySQL
Джеком Дугласом

2
Спосіб формулювання питання говорить про те, що використання таблиці має бути неправильною відповіддю. Але іноді (в більшості випадків?) Додавання іншої таблиці є хорошою ідеєю. І додавання таблиці - це повністю агностична база даних.
Майк Шеррілл 'Відкликання котів'

Відповіді:



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обмеження, якщо не хочете нулів (мені не було зрозуміло із специфікацій запитання). Лише один рядок може мати значення "Y" у будь-якому випадку.
Вінсент Мальграт

+1 Я бачу, що ти маєш на увазі - ти маєш рацію, що це не потрібно (але, можливо, це трохи акуратніше, особливо якщо він поєднується з a default)?
Джек Дуглас

2
@jack: ваше зауваження змусило мене зрозуміти, що є ще простіша можливість, якщо ви приймаєте, що стовпець може бути або, Yабо nullдивіться моє оновлення.
Вінсент Малграт

1
Варіант 2 має додаткову вигоду, індекс буде крихітним, оскільки nullпропускається s - за рахунок певної ясності, можливо
Джек Дуглас

13

Я думаю, що це випадок правильного структурування таблиць вашої бази даних. Щоб зробити це більш конкретним, якщо у вас є людина з кількома адресами, і ви хочете, щоб одна була за замовчуванням, я думаю, ви повинні зберігати addressID адреси за замовчуванням у таблиці особи, а не мати стовпця за замовчуванням у адресній таблиці:

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

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

Ви можете зробити DefaultAddressID змінним, але таким чином структура нав'язує ваше обмеження.


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'

--edit

все-таки це не гарне рішення, оскільки в MySQL booleanє синонімомtinyint(1) і тому дозволяє ненульові значення, ніж 0 або 1. Можливо, це bitбув би кращий вибір


Це може відповісти на мій коментар до відповіді RolandoMySQLDBA: чи можемо ми мати рішення MySQL з DRI?
gbn

Це трохи негарно , хоча через null, false, true- мені цікаво , якщо є що - то акуратніше ...
Джек Дуглас

@Jack - +1 для приємного спробу чистого підходу DRI в MySQL.
RolandoMySQLDBA

Я б радив уникати використання помилок тут, оскільки унікальне обмеження дозволило б надати лише одне таке хибне значення. Якщо null являє собою 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. Перевірте обмеження за допомогою АДС. Це не безпечно для одночасності та ізоляції знімків.
    Дивіться один два три чотири

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"

--edit

або (набагато краще), використовуйте унікальний частковий індекс :

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

Така проблема є ще однією причиною, чому я попросив це запитання:

Налаштування програми в базі даних

Якщо у вашій базі даних є таблиця налаштувань додатків, у вас може бути запис, який би посилався на ідентифікатор однієї записи, яку ви хочете вважати «спеціальною». Тоді ви просто подивіться, що таке ідентифікатор у таблиці налаштувань, і таким чином вам не потрібен цілий стовпець лише для одного пункту, який встановлюється.


Це чудова пропозиція: вона більше відповідає нормалізованому дизайну, працює з будь-якою платформою бази даних і її найлегше реалізувати.
Nick Chammas

+1, але зауважте, що "цілий стовпець" може не використовувати фізичного простору залежно від RDBMS :)
Джек Дуглас,

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значення найбільше / найменше значення або подібне. Можливо, використовуйте види, щоб інкапсулювати цю логіку.


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;

Спробувати !!!


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

навіть якщо ваш RDBMS підтримує це, він буде серіалізуватися як божевільний, тому якщо у вас більше одного користувача, у вас можуть виникнути проблеми
Джек Дуглас,

якщо кілька користувачів одночасно змінюють, їм доведеться стояти в черзі (серіалізувати) - іноді це нормально, часто це не так (думайте, великі 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, якщо ви не хочете застосовувати унікальний контракт:

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           |
+------+------+-------------+

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'
               ));

1
не добре в жодних RDBMS, якими я користувався ... нюансів багато
Джек Дуглас

0

Я лише проглядав відповіді, тому, можливо, пропустив подібну відповідь. Ідея полягає у використанні згенерованого стовпця, який є або pk, або константою, яка не існує як значення для pk

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 дозволяє це, не знаючи, скільки інших постачальників, які приймають його.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.