단일 행에서만 설정할 수있는 'default'플래그를 구현하는 방법


31

예를 들어 다음과 비슷한 테이블이 있습니다.

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

플래그가 char(1), a bit또는 무엇 으로 구현되는지는 중요하지 않습니다 . 단지 단일 행에서만 설정할 수있는 제약 조건을 적용하고 싶습니다.


이 질문에서 영감을 받아 MySQL로 제한되었습니다
Jack Douglas

2
질문의 표현은 테이블을 사용하는 것이 정답이 아니라는 것을 암시합니다. 그러나 때때로 (대부분?) 다른 테이블을 추가하는 것이 좋습니다. 그리고 테이블을 추가하는 것은 완전히 데이터베이스에 구애받지 않습니다.
Mike Sherrill 'Cat Recall'10

답변:


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은 모든 색인화 된 열이 널인 항목을 색인화하지 않으므로 함수 기반 고유 색인을 사용할 수 있습니다.

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될 것이다 YNULL . 최대 하나의 행만 값을 가질 수 있습니다Y.


chk는 not null제약이 필요 합니까?
Jack Douglas

@ jack : not nullnull을 원하지 않으면 제약 조건을 추가 할 수 있습니다 (질문 스펙에서 나에게 명확하지 않았습니다). 어쨌든 하나의 행만 'Y'값을 가질 수 있습니다.
Vincent Malgrat

+1 나는 당신이 무엇을 의미하는지 안다-당신이 옳지 않다고 생각 default한다.
Jack Douglas

2
@ jack : 귀하의 의견에 따르면 열이 Y또는 null일 수 있음을 수락하면 더 간단한 가능성을 알 수 있습니다 . 내 업데이트를 참조하십시오.
Vincent Malgrat

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 점검 제한 조건의 부족에 대한 해결 방법으로 삽입 / 업데이트에 - IMO는 그래도 개선되지 않습니다.

char (0)을 사용할 수 있기를 바랐습니다.

CHAR (0) NULL로 정의 된 열은 1 비트 만 차지하며 NULL 및 ''값만 사용할 수 있습니다.

불행히도 MyISAM과 InnoDB는 적어도

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

--편집하다

이것은 MySQL 이후로 좋은 해결책이 아니며에 boolean대한 동의어tinyint(1) 이므로 0 또는 1보다 null이 아닌 값을 허용 bit합니다. 더 나은 선택이 될 수 있습니다.


이것은 RolandoMySQLDBA의 답변에 대한 나의 의견에 대답 할 수 있습니다 : 우리는 DRI와 함께 MySQL 솔루션을 가질 수 있습니까?
gbn 14

, - 때문에 조금 추악하지만 null, 더 좋은 것이 있는지 궁금합니다 ...falsetrue
Jack Douglas

@Jack-MySQL의 순수한 DRI 접근 방식에 대한 좋은 시도를 위해 +1.
RolandoMySQLDBA

고유 한 제약 조건으로 그러한 잘못된 값 중 하나만 제공 할 수 있기 때문에 false 사용을 피하는 것이 좋습니다. null이 false를 나타내는 경우, 전체적으로 일관되게 사용해야합니다. 추가적인 검증이 가능한 경우 (예 : JSR-303 / 최대 절전 모드 검증) false를 피할 수 있습니다.
Steve Chambers

1
최신 버전의 MySQL / MariaDB는 dba.stackexchange.com/a/144847/94908
MattW

10

SQL 서버 :

그것을하는 방법 :

  1. 가장 좋은 방법은 필터링 된 인덱스입니다. DRI
    SQL Server 2008 이상 사용

  2. 고유 한 계산 열 DRI 사용
    Jack Douglas의 답변을 참조하십시오. 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

이런 종류의 문제는 내가이 Quiestion을 요구 한 또 다른 이유입니다.

데이터베이스의 응용 프로그램 설정

데이터베이스에 응용 프로그램 설정 테이블이있는 경우 '특별한'것으로 간주하려는 한 레코드의 ID를 참조하는 항목이있을 수 있습니다. 그런 다음 설정 테이블에서 ID가 무엇인지 검색하면 하나의 항목을 설정하기 위해 전체 열이 필요하지 않습니다.


이는 훌륭한 제안입니다. 표준화 된 디자인을 유지하고 모든 데이터베이스 플랫폼에서 작동하며 구현하기가 가장 쉽습니다.
Nick Chammas

+1이지만 "전체 열"은 RDBMS에 따라 실제 공간을 사용하지 않을 수 있습니다.
Jack Douglas

6

널리 구현 된 기술을 사용하는 가능한 접근 방식 :

1) 테이블에서 '작성자'권한을 취소하십시오. 트랜잭션 경계에서 제한 조건이 적용되도록 CRUD 프로 시저를 작성하십시오.

2) 6NF : CHAR(1)열을 삭제하십시오 . 카디널리티가 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)
);

고려 된 'default'가 새 테이블의 행이되도록 응용 프로그램 시맨틱을 변경하십시오. 이 논리를 캡슐화하기 위해 뷰를 사용할 수 있습니다.

3) CHAR(1)컬럼을 삭제하십시오 . seq정수 열을 추가하십시오 . 에 고유 제한 조건을 설정하십시오 seq. 고려되는 'default'가 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
MySQL 용 DRI 기반 솔루션이 없습니까? 코드 만? MySQL을 점점 더 많이 사용하기 시작했기 때문에 궁금합니다.
gbn

4

SQL Server 2000 이상에서는 인덱싱 된 뷰를 사용하여 원하는 것과 같은 복잡한 (또는 다중 테이블) 제약 조건을 구현할 수 있습니다.
또한 Oracle은 검사 제한 조건이 지연된 구체화 된 뷰에 대해 유사한 구현을 가지고 있습니다. 여기

내 게시물을 참조 하십시오 .


이 답변에서 짧은 코드 스 니펫과 같이 조금 더 "고기"를 제공 할 수 있습니까? 지금은 몇 가지 일반적인 아이디어와 링크 일뿐입니다.
Nick Chammas

여기에 예제를 맞추는 것이 약간 어려울 것입니다. 링크를 클릭하면 찾고있는 "고기"를 찾을 수 있습니다.
spaghettidba

3

SQL Server 2000 이상과 같이 널리 구현 된 표준 전환 SQL-92 :

테이블에서 'writer'권한을 취소하십시오. 두 가지 뷰를 생성 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
설명해 주셔서 감사합니다. 여러 사용자가 자주 기본 행을 설정하는 경우 디자인 선택 (같은 테이블의 플래그 열)에 의문이 생깁니다.
onedayhen '10

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 : ANSI-92 쿼리 모드CHECK 에서 Access2000 (ACE2007, Jet 4.0 등)에서 지원되는 등 널리 구현되지 않은 제약 조건 에서 하위 쿼리를 사용합니다 .

예제 코드 : CHECKAccess의 참고 제약 조건은 항상 테이블 수준입니다. 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에는 좋지 않다 ... 주의 사항
Jack Douglas

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는이를 허용하는 다른 벤더 수를 확실하지 않습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.