1つのレコードにブール値列のtrue値を強制し、他のすべての値にfalse値を強制するにはどうすればよいですか?


20

テーブル内の1つのレコードのみが、そのテーブルにアクセスする可能性のある他のクエリまたはビューの「デフォルト」値と見なされるように強制します。

基本的に、このクエリが常に正確に1行を返すことを保証したいと思います。

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

SQLでこれを行うにはどうすればよいですか?


1
「このクエリが常に正確に1行を返すことを保証したい」-常に?PostalCodes空の場合はどうですか?行にすでにプロパティが必要な場合、同じSQLステートメント内で別の行(存在する場合)がtrueに設定されていない限り、falseに設定されないようにしますか?トランザクションの境界の間にゼロ行のプロパティを設定できますか?テーブルの最後の行にプロパティを設定し、削除されないようにする必要がありますか?経験上、「正確に1行を保証する」とは、実際には異なるものを意味する傾向があり、多くの場合は単に「多くても1行」という意味です。
いつか

回答:


8

編集:これは、MySQLを知る前にSQL Serverを対象としています。

これを行うには4つの 5つの方法があります。

ただし、すべてのRDBMSが当てはまるわけではありません

  1. 最適な方法は、フィルター選択されたインデックスです。これは、DRIを使用して一意性を維持します。

  2. 一意性を持つ計算列(ジャックダグラスの回答を参照)(編集2で追加)

  3. DRIを使用したフィルター選択されたインデックスのようなインデックス付き/マテリアライズドビュー

  4. トリガー(他の回答による)

  5. UDFを使用して制約を確認します。これ、並行性とスナップショットの分離にとって安全はありません。参照してください。一つ 二つ つの

「1つのデフォルト」の要件は、ストアドプロシージャではなくテーブルにあることに注意してください。ストアドプロシージャには、udfを使用したチェック制約と同じ同時実行の問題があります。

注:何度も聞かれました:


GBN -私はオプション2を試すにスキップしますようにフィルターインデックス/ DRIソリューションのように見えますが、唯一のルックスので、SQL 2008である
emaynard

10

注:この回答は、質問者がMySQL固有の何かを望んでいることが明らかになる前に与えられました。この答えはSQL Serverに偏っています。

UDFを呼び出すテーブルにCHECK制約を適用し、その戻り値が1以下であることを確認します。UDFはテーブルWHEREの行をカウントするだけです。これにより、テーブルにデフォルトの行が1つしか存在しないことが保証されます。isDefault = TRUE

このUDFが非常に高速に実行されるように、ビットマップまたはフィルター処理されたインデックスをisDefault(プラットフォームに応じて)列に追加する必要があります。

注意事項

  • チェック制約には特定の 制限があります。つまり、トランザクションでまだコミットされていない場合でも、変更されたすべての行に対して呼び出されます。そのため、トランザクションを開始し、新しい行をデフォルトに設定してから古い行の設定を解除すると、チェック制約がエラーになります。これは、新しい行をデフォルトにした後に実行され、現在2つのデフォルトがあることを発見し、失敗するためです。その場合の解決策は、トランザクションでこの作業を行っている場合でも、新しいデフォルトを設定する前に、デフォルトの行を最初に設定解除するようにすることです。
  • GBNは 指摘あなたがSQL Serverでスナップショットアイソレーションを使用している場合、UDFは一貫性のない結果を返すことがあります。ただし、インラインUDFではこの問題は発生しません。また、カウントのみを行うUDFはインライン対応であるため、ここでは問題になりません。
  • データベースエンジンの処理能力の強みは、一連のデータを一度に処理できることです。UDF-backedチェック制約を使用すると、エンジンは、変更されたすべての行にこの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がisDefaultを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 

フィールドを間違った場所に置いたので、これは本質的に問題を引き起こしていました。それがすべてです。

探しているIDを保持するPostalCodeを含む「デフォルト」テーブルがあります。一般に、1つのレコードに従ってテーブルに名前を付けることも良いので、最初のテーブルの名前は 'PostalCode'と呼ばれる方が良いでしょう。デフォルトは、他の設定(履歴など)に対してデフォルトを特化する必要があるまで、単一行のテーブルになります。

最終的なクエリは次のとおりです。

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.