Bagaimana cara memaksa satu rekaman untuk memiliki nilai sebenarnya untuk kolom boolean, dan semua yang lain nilai palsu?

20

Saya ingin menegaskan bahwa hanya satu catatan dalam tabel dianggap sebagai nilai "default" untuk kueri atau tampilan lain yang mungkin mengakses tabel itu.

Pada dasarnya, saya ingin menjamin bahwa permintaan ini akan selalu mengembalikan tepat satu baris:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

Bagaimana saya melakukannya dalam SQL?

emaynard
sumber
1
"Saya ingin menjamin bahwa permintaan ini akan selalu mengembalikan tepat satu baris" - selalu? Bagaimana dengan kapan PostalCodeskosong? Jika suatu baris sudah memiliki properti seharusnya, itu dicegah dari disetel ke false kecuali baris lain (jika ada) disetel ke true dalam pernyataan SQL yang sama? Bisakah nol baris memiliki properti di antara batas-batas transaksi? Haruskah baris terakhir dalam tabel dipaksa untuk memiliki properti dan dicegah agar tidak dihapus? Pengalaman mengatakan kepada saya bahwa "menjamin tepat satu baris" cenderung berarti sesuatu yang berbeda dalam kenyataan, seringkali hanya "paling banyak satu baris".
onedaywhen

Jawaban:

8

Sunting: ini ditujukan untuk SQL Server sebelum kita tahu MySQL

Ada 4 5 cara untuk melakukan ini: dalam urutan yang paling diinginkan hingga yang tidak diinginkan

Kami tidak tahu apa RDBMS ini jika untuk jadi tidak semua mungkin berlaku

  1. Cara terbaik adalah indeks yang difilter. Ini menggunakan DRI untuk mempertahankan keunikan.

  2. Kolom yang dihitung dengan keunikan (lihat jawaban Jack Douglas) (ditambah dengan edit 2)

  3. Tampilan terindeks / terwujud yang seperti indeks yang difilter menggunakan DRI

  4. Pemicu (sesuai jawaban lain)

  5. Periksa kendala dengan UDF. Ini tidak aman untuk konkurensi dan isolasi snapshot. Lihat Satu Dua Tiga Empat

Perhatikan bahwa persyaratan untuk "satu standar" ada di atas meja, bukan prosedur tersimpan. Prosedur tersimpan akan memiliki masalah konkurensi yang sama dengan kendala cek dengan udf

Catatan: sering ditanyakan SO:

gbn
sumber
gbn - sepertinya indeks / solusi DRI yang difilter adalah SQL 2008 hanya jadi sepertinya saya akan lewati untuk mencoba opsi 2.
emaynard
10

Catatan: Jawaban ini diberikan sebelum jelas penanya menginginkan sesuatu yang spesifik MySQL. Jawaban ini bias terhadap SQL Server.

Menerapkan batasan PERIKSA ke tabel yang memanggil UDF dan memastikan nilai kembalinya <= 1. UDF hanya dapat menghitung baris dalam tabel MANAisDefault = TRUE . Ini akan memastikan bahwa tidak ada lebih dari 1 baris default dalam tabel.

Anda harus menambahkan bitmap atau indeks yang difilter pada isDefaultkolom (tergantung platforom Anda) untuk memastikan UDF ini berjalan sangat cepat.

Peringatan

  • Periksa kendala memiliki batasan tertentu . Yaitu, mereka dipanggil untuk setiap baris yang dimodifikasi, bahkan jika baris-baris itu belum dilakukan dalam suatu transaksi. Jadi, jika Anda memulai transaksi, atur baris baru sebagai default, lalu hapus baris lama, kendala pemeriksaan Anda akan dikeluhkan. Itu karena itu akan dieksekusi setelah Anda menjadikan baris baru sebagai default, temukan sekarang ada dua default, dan gagal. Solusinya kemudian adalah memastikan Anda menghapus dulu baris default sebelum menetapkan default baru, bahkan jika Anda melakukan pekerjaan ini dalam transaksi.
  • Seperti yang ditunjukkan gbn , UDFs dapat mengembalikan hasil yang tidak konsisten jika Anda menggunakan isolasi snapshot di SQL Server. Namun, UDF inline tidak mengalami masalah ini, dan karena UDF yang hanya menghitung mampu inline, ini bukan masalah di sini.
  • Kekuatan kekuatan pemrosesan mesin basis data adalah kemampuannya untuk bekerja pada set data sekaligus. Dengan kendala pemeriksaan yang didukung UDF, mesin akan dibatasi untuk menerapkan UDF ini secara berurutan untuk setiap baris yang dimodifikasi. Dalam kasus penggunaan Anda, tidak mungkin bahwa Anda akan melakukan pembaruan massal untuk PostalCodestabel, namun ini tetap menjadi masalah kinerja untuk situasi-situasi di mana aktivitas berbasis set mungkin.

Dengan semua peringatan ini, saya sarankan menggunakan saran GBN tentang indeks unik yang difilter alih-alih kendala pemeriksaan.

Nick Chammas
sumber
4

Berikut ini adalah Prosedur Tersimpan (Dialek 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 ;

Untuk memastikan meja Anda bersih dan prosedur tersimpan berfungsi, dengan anggapan ID 200 adalah default, jalankan langkah-langkah ini:

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;

Alih-alih Prosedur Tersimpan, bagaimana dengan Pemicu?

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 ;

Untuk memastikan meja Anda bersih dan pemicu berfungsi, dengan asumsi ID 200 adalah default, jalankan langkah-langkah ini:

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
sumber
3

Anda bisa menggunakan pemicu untuk menerapkan aturan. Ketika pernyataan UPDATE atau INSERT menetapkan isDefault ke True, SQL di pemicu dapat mengatur semua baris lainnya ke False.

Anda harus mempertimbangkan situasi lain saat menerapkan ini. Misalnya, apa yang harus terjadi jika lebih dari satu baris masuk dan UPDATE atau INSERT menetapkan isDefault ke True? Aturan apa yang akan berlaku untuk menghindari melanggar aturan?

Juga, mungkinkah kondisi di mana tidak ada default? Apa yang akan terjadi ketika pembaruan menetapkan isDefault dari True ke False.

Setelah Anda menetapkan aturan, Anda bisa membuatnya di pelatuk.

Kemudian, seperti ditunjukkan dalam jawaban lain, Anda ingin menerapkan batasan pemeriksaan untuk membantu menegakkan aturan.

bobs
sumber
3

Berikut ini menyiapkan contoh tabel dan data:

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

Jika Anda membuat prosedur tersimpan untuk mengatur bendera IsDefault dan menghapus izin untuk mengubah tabel dasar, Anda bisa menerapkannya dengan kueri di bawah ini. Itu akan membutuhkan pemindaian tabel setiap kali.

DECLARE @Id INT
SET @Id = 10

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

Bergantung pada ukuran tabel, indeks pada IsDefault (DESC) dapat menghasilkan salah satu atau kedua pertanyaan di bawah ini menghindari pemindaian penuh.

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)

Jika Anda tidak dapat menghapus izin dari tabel dasar, perlu menegakkan integritas dengan cara lain dan menggunakan SQL2008, Anda dapat menggunakan indeks unik yang difilter:

CREATE UNIQUE INDEX IX_MyTable_IsDefault  ON dbo.MyTable (IsDefault) WHERE IsDefault = 1
Mark Storey-Smith
sumber
1

Untuk MySQL yang tidak memiliki indeks filter, Anda bisa melakukan sesuatu seperti berikut ini. Ini mengeksploitasi fakta bahwa MySQL memungkinkan beberapa NULL dalam indeks unik, dan menggunakan tabel khusus dan kunci asing untuk mensimulasikan tipe data yang hanya dapat memiliki NULL dan 1 sebagai nilai.

Dengan solusi ini, alih-alih benar / salah, Anda hanya akan menggunakan 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`))*/

Satu-satunya hal yang bisa salah, saya pikir, adalah jika seseorang menambahkan baris ke DefaultFlagmeja. Saya pikir ini bukan masalah besar, dan Anda selalu dapat memperbaikinya dengan izin jika Anda benar-benar ingin aman.

djfm
sumber
0
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

Ini pada dasarnya memberi Anda masalah karena Anda meletakkan bidang di tempat yang salah dan hanya itu.

Memiliki tabel 'Defaults', dengan PostalCode di dalamnya, yang berisi id yang Anda cari. Secara umum, ada baiknya untuk memberi nama tabel Anda sesuai dengan satu catatan, jadi nama tabel awal Anda akan lebih baik disebut 'Kode Pos'. Default akan menjadi tabel baris tunggal, sampai Anda perlu mengkhususkan standar Anda terhadap beberapa konfigurasi lain (seperti sejarah).

Permintaan akhir yang Anda inginkan adalah sebagai berikut:

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
Konchog
sumber