하나의 레코드가 부울 열에 대해 실제 값을 갖도록하고 다른 레코드에 모두 거짓 값을 갖도록하려면 어떻게해야합니까?


20

테이블에서 하나의 레코드 만 해당 테이블에 액세스 할 수있는 다른 쿼리 나 뷰의 "기본"값으로 간주되도록하고 싶습니다.

기본적 으로이 쿼리가 항상 정확히 하나의 행을 반환하도록 보장하고 싶습니다.

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

SQL에서 어떻게해야합니까?


1
"이 쿼리가 항상 정확히 하나의 행을 반환하도록 보장하고 싶습니다"-항상? PostalCodes비어있는 경우 는 어떻습니까? 행에 이미 속성이 있어야하는 경우 동일한 SQL 문 내에서 다른 행 (있는 경우)이 true로 설정되어 있지 않으면 false로 설정되지 않습니까? 트랜잭션 경계 사이에 0 개의 행이 속성을 가질 수 있습니까? 테이블의 마지막 행에 속성이 있어야하고 삭제되지 않아야합니까? 경험에 따르면 "정확히 하나의 행을 보장"하는 것은 실제로는 단순히 "최대 하나의 행"과는 다른 것을 의미하는 경향이 있습니다.
onedaywhen

답변:


8

편집 : 이것은 우리가 MySQL을 알기 전에 SQL Server에 맞춰졌습니다.

이 작업을 수행하는 방법 에는 4 가지가 있습니다.

우리는 RDBMS가 무엇인지 알지 못한다.

  1. 가장 좋은 방법은 필터링 된 인덱스입니다. 이것은 DRI를 사용하여 고유성을 유지합니다.

  2. 고유 한 계산 열 (Jack Douglas의 답변 참조) (편집 2로 추가)

  3. DRI를 사용한 필터링 된 인덱스와 같은 인덱스 / 구체화 된 뷰

  4. 트리거 (다른 답변에 따라)

  5. UDF로 제한 조건을 점검하십시오. 이것은 안전하지 동시성 및 스냅 숏 격리합니다. 하나 참조

"하나의 기본값"에 대한 요구 사항은 저장 프로 시저가 아니라 테이블에 있습니다. 저장 프로 시저에는 udf의 검사 제약 조건과 동일한 동시성 문제가 있습니다.

참고 : SO에 여러 번 질문했습니다.


gbn-필터링 된 인덱스 / DRI 솔루션이 SQL 2008 인 것처럼 보이므로 옵션 2를 시도하지 않는 것 같습니다.
emaynard

10

참고 :이 답변은 asker가 MySQL에 특정한 것을 원하기 전에 제공되었습니다. 이 답변은 SQL Server에 편향되어 있습니다.

UDF 를 호출 하고 리턴 값이 <= 1인지 확인 하는 테이블에 CHECK 제한 조건을 적용 하십시오. UDF는 WHERE 테이블의 행을 계수 할 수 있습니다 . 이렇게하면 테이블에 기본 행이 1 개를 넘지 않아야합니다.isDefault = TRUE

이 UDF가 매우 빠르게 실행되도록 하려면 열에 비트 맵 또는 필터링 된 인덱스를 추가해야합니다 isDefault(플랫폼에 따라 다름).

경고

  • 점검 제한 조건에는 특정 제한 사항이 있습니다. 즉, 행이 아직 트랜잭션에서 커미트되지 않은 경우에도 수정 된 모든 행에 대해 호출됩니다. 따라서 트랜잭션을 시작하고 새 행을 기본값으로 설정 한 다음 이전 행을 설정 해제하면 검사 제한 조건이 불평합니다. 새 행을 기본값으로 설정하고 두 가지 기본값이 있으며 실패하면 실행되기 때문입니다. 해결 방법은 트랜잭션에서이 작업을 수행하더라도 새 기본값을 설정하기 전에 먼저 기본 행을 설정 해제하는 것입니다.
  • 으로 GBN는 지적 이 SQL 서버에서 스냅 숏 격리를 사용하는 경우, UDF는 일관되지 않은 결과를 반환 할 수 있습니다. 그러나 인라인 UDF는이 문제에 부딪치지 않으며 카운트 만 수행하는 UDF는 인라인 가능하므로 여기서는 문제가되지 않습니다.
  • 데이터베이스 엔진의 처리 능력의 강점은 한 번에 데이터 집합을 처리 할 수있는 능력에 있습니다. UDF 지원 점검 제한 조건을 사용하면 엔진은이 UDF를 수정 된 모든 행에 직렬로 적용하도록 제한됩니다. 사용 사례에서 PostalCodes테이블 에 대한 대량 업데이트를 수행 할 가능성은 없지만 세트 기반 활동이 발생할 가능성이있는 상황에서는 성능 문제가 남아 있습니다.

이러한 모든 단점을 감안할 때 , 체크 제한 대신 필터링 된 고유 인덱스에 대한 gbn의 제안 을 사용하는 것이 좋습니다 .


4

다음은 저장 프로 시저 (MySQL Dialect)입니다.

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

트리거를 사용하여 규칙을 적용 할 수 있습니다. UPDATE 또는 INSERT 문이 isDefault를 True로 설정하면 트리거의 SQL이 다른 모든 행을 False로 설정할 수 있습니다.

이것을 적용 할 때 다른 상황을 고려해야합니다. 예를 들어, 둘 이상의 행이 있고 UPDATE 또는 INSERT가 Default로 True 인 경우 어떻게됩니까? 규칙을 어 기지 않기 위해 어떤 규칙이 적용됩니까?

또한 기본값이없는 조건이 가능합니까? 업데이트가 isDefault를 True에서 False로 설정하면 어떻게됩니까?

규칙을 정의하면 트리거에서 규칙을 작성할 수 있습니다.

그런 다음 다른 답변에 표시된대로 규칙을 적용하는 데 도움이되는 검사 제한 조건을 적용하려고합니다.


3

다음은 예제 테이블 및 데이터를 설정합니다.

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[MyTable]') AND type in (N'U'))
DROP TABLE [dbo].[MyTable]
GO

CREATE TABLE dbo.MyTable
(
    [id] INT IDENTITY(1,1) PRIMARY KEY CLUSTERED
    , [IsDefault] BIT DEFAULT 0
)
GO

INSERT dbo.MyTable DEFAULT VALUES
GO 100

IsDefault 플래그를 설정하기 위해 저장 프로 시저를 만들고 기본 테이블을 변경할 수있는 권한을 제거하는 경우 아래 쿼리를 사용하여이를 적용 할 수 있습니다. 매번 테이블 스캔이 필요합니다.

DECLARE @Id INT
SET @Id = 10

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END 
GO

테이블의 크기에 따라 IsDefault (DESC)의 인덱스는 전체 쿼리를 피하기 위해 아래 쿼리 중 하나 또는 둘 다를 초래할 수 있습니다.

DECLARE @Id INT
SET @Id = 10

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END
FROM
    dbo.MyTable
WHERE
    [id] = @Id
OR  IsDefault = 1

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END
FROM
    dbo.MyTable
WHERE
    [id] = @Id
OR  ([id] != @Id AND IsDefault = 1)

기본 테이블에서 권한을 제거 할 수없고 다른 방법으로 무결성을 강화해야하며 SQL2008을 사용하는 경우 필터링 된 고유 인덱스를 사용할 수 있습니다.

CREATE UNIQUE INDEX IX_MyTable_IsDefault  ON dbo.MyTable (IsDefault) WHERE IsDefault = 1

1

필터링 된 인덱스가없는 MySQL의 경우 다음과 같은 작업을 수행 할 수 있습니다. MySQL은 고유 인덱스에 여러 NULL을 허용한다는 사실을 이용하고 전용 테이블과 외래 키를 사용하여 NULL과 1 만 값으로 가질 수있는 데이터 유형을 시뮬레이션합니다.

이 솔루션을 사용하면 true / false 대신 1 / NULL을 사용합니다.

CREATE TABLE DefaultFlag (id TINYINT UNSIGNED NOT NULL PRIMARY KEY);
INSERT INTO DefaultFlag (id) VALUES (1);

CREATE TABLE PostalCodes (
    ID INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    Zip VARCHAR (32) NOT NULL,
    isDefault TINYINT UNSIGNED NULL DEFAULT NULL,
    CONSTRAINT FOREIGN KEY (isDefault) REFERENCES DefaultFlag (id),
    UNIQUE KEY (isDefault)
);

INSERT INTO PostalCodes (Zip, isDefault) VALUES ('123', 1   );
/* Only one row can be default */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('abc', 1   );
/* ERROR 1062 (23000): Duplicate entry '1' for key 'isDefault' */

/* MySQL allows multiple NULLs in unique indexes */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('456', NULL);
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('789', NULL);

/* only NULL and 1 are admitted in isDefault thanks to foreign key */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('789', 2   );
/* ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`PostalCodes`, CONSTRAINT `PostalCodes_ibfk_1` FOREIGN KEY (`isDefault`) REFERENCES `DefaultFlag` (`id`))*/

내가 잘못 생각할 수있는 유일한 것은 누군가가 DefaultFlag테이블에 행을 추가하는 것 입니다. 나는 이것이 큰 관심사가 아니라고 생각하며, 정말로 안전하고 싶다면 항상 권한으로 해결할 수 있습니다.


0
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

필드를 잘못된 위치에 놓기 만하면 문제가 발생합니다. 그게 전부입니다.

PostalCode가있는 'Defaults'테이블을 사용하면 찾고있는 ID가 있습니다. 일반적으로 하나의 레코드에 따라 테이블 이름을 지정하는 것이 좋으므로 초기 테이블 이름은 'PostalCode'라고하는 것이 좋습니다. 기본값은 다른 구성 (예 : 내역)에 대해 기본값을 특수화해야 할 때까지 단일 행 테이블이됩니다.

원하는 최종 쿼리는 다음과 같습니다.

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.