Cách triển khai cờ 'mặc định' chỉ có thể được đặt trên một hàng


31

Ví dụ: với một bảng tương tự như sau:

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

Không có vấn đề gì nếu cờ được thực hiện như một char(1), một bithoặc bất cứ điều gì. Tôi chỉ muốn có thể thực thi các ràng buộc mà nó chỉ có thể được đặt trên một hàng duy nhất.


lấy cảm hứng từ câu hỏi này được giới hạn ở MySQL
Jack Douglas

2
Cách câu hỏi được diễn đạt cho thấy rằng sử dụng bảng phải là câu trả lời sai. Nhưng đôi khi (hầu hết các lần?) Thêm một bảng khác là một ý tưởng tốt. Và thêm một bảng là hoàn toàn cơ sở dữ liệu bất khả tri.
Mike Sherrill 'Nhớ lại mèo'

Câu trả lời:


31

SQL Server 2008 - Chỉ mục duy nhất được lọc

CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'

16

Máy chủ SQL 2000, 2005:

Bạn có thể tận dụng thực tế là chỉ có một null được phép trong một chỉ mục duy nhất:

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

cho năm 2000, bạn có thể cần SET ARITHABORT ON(nhờ @gbn cho thông tin này)


14

Oracle:

Vì Oracle không có mục chỉ mục trong đó tất cả các cột được lập chỉ mục là null, nên bạn có thể sử dụng chỉ mục duy nhất dựa trên chức năng:

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

Chỉ số này sẽ chỉ bao giờ chỉ mục một hàng tối đa.

Biết thực tế chỉ số này, bạn cũng có thể triển khai cột bit hơi khác:

create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);

Ở đây các giá trị có thể cho cột chksẽ là YNULL. Chỉ một hàng tối đa có thể có giá trịY.


Chk cần một sự not nullràng buộc?
Jack Douglas

@jack: Bạn có thể thêm một not nullràng buộc nếu bạn không muốn null (Nó không rõ ràng với tôi từ thông số câu hỏi). Chỉ một hàng có thể có giá trị 'Y' trong mọi trường hợp.
Vincent Malgrat

+1 Tôi hiểu ý của bạn - bạn nói đúng là không cần thiết (nhưng có lẽ gọn gàng hơn một chút, đặc biệt nếu kết hợp với a default)?
Jack Douglas

2
@jack: nhận xét của bạn khiến tôi nhận ra rằng một khả năng thậm chí còn đơn giản hơn nếu bạn chấp nhận rằng cột có thể Yhoặc null, xem cập nhật của tôi.
Vincent Malgrat

1
Tùy chọn 2 có thêm lợi ích, chỉ số sẽ rất nhỏ khi nullbị bỏ qua - với chi phí rõ ràng có lẽ
Jack Douglas

13

Tôi nghĩ rằng đây là một trường hợp cấu trúc các bảng cơ sở dữ liệu của bạn một cách chính xác. Để làm cho cụ thể hơn, nếu bạn có một người có nhiều địa chỉ và bạn muốn một địa chỉ mặc định, tôi nghĩ bạn nên lưu địa chỉID của địa chỉ mặc định trong bảng người, không có cột mặc định trong bảng địa chỉ:

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

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

Bạn có thể làm cho DefaultAddressID trở nên vô hiệu, nhưng theo cách này cấu trúc thực thi ràng buộc của bạn.


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

Kiểm tra các ràng buộc được bỏ qua trong MySQL vì vậy chúng tôi phải xem xét nullhoặc falselà sai và truelà đúng. Nhiều nhất 1 hàng có thể cóchk=true

Bạn có thể coi đó là một cải tiến để thêm một kích hoạt để thay đổi falsethành truechèn / cập nhật như một cách giải quyết cho việc thiếu ràng buộc kiểm tra - IMO mặc dù đó không phải là một cải tiến.

Tôi hy vọng có thể sử dụng char (0) vì nó

cũng khá hay khi bạn cần một cột chỉ có thể nhận hai giá trị: Một cột được xác định là CHAR (0) NULL chỉ chiếm một bit và chỉ có thể lấy các giá trị NULL và ''

Thật không may, với MyISAM và InnoDB ít nhất, tôi nhận được

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

--chỉnh sửa

Rốt cuộc đây không phải là một giải pháp tốt vì trên MySQL, booleanlà một từ đồng nghĩatinyint(1) và vì vậy cho phép các giá trị không null hơn 0 hoặc 1. Có thể đó bitsẽ là một lựa chọn tốt hơn


Điều này có thể trả lời nhận xét của tôi cho câu trả lời của RolandoMySQLDBA: chúng ta có thể có giải pháp MySQL với DRI không?
gbn

Đó là một chút xấu xí mặc dù vì null, false, true- tôi làm ngạc nhiên nếu có một cái gì đó gọn gàng ...
Jack Douglas

@Jack - +1 để thử tốt cách tiếp cận DRI thuần túy trong MySQL.
RolandoMySQLDBA

Tôi khuyên bạn nên tránh sử dụng sai ở đây vì ràng buộc duy nhất sẽ chỉ cho phép một giá trị sai như vậy được cung cấp. Nếu null đại diện cho false thì nó nên được sử dụng một cách nhất quán xuyên suốt - việc tránh sai có thể được thi hành nếu xác thực bổ sung có sẵn (ví dụ: JSR-303 / hibernate-validator).
Steve Chambers

1
Các phiên bản gần đây của MySQL / MariaDB triển khai các cột ảo mà tôi tin rằng cho phép một giải pháp thanh lịch hơn một chút được nêu dưới đây tại dba.stackexchange.com/a/144847/94908
MattW.

10

Máy chủ SQL:

Làm thế nào để làm nó:

  1. Cách tốt nhất là một chỉ mục được lọc. Sử dụng DRI
    SQL Server 2008+

  2. Cột tính toán với tính độc đáo. Sử dụng DRI
    Xem câu trả lời của Jack Douglas. SQL Server 2005 trở về trước

  3. Một khung nhìn được lập chỉ mục / cụ thể hóa giống như một chỉ mục được lọc. Sử dụng DRI
    Tất cả các phiên bản.

  4. Cò súng. Mã sử ​​dụng, không phải DRI.
    Tất cả các phiên bản

Làm thế nào để không làm điều đó:

  1. Kiểm tra ràng buộc với UDF. Điều này không an toàn cho cách ly đồng thời và chụp nhanh.
    Xem một hai ba bốn

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"

--chỉnh sửa

hoặc (tốt hơn nhiều), sử dụng một chỉ mục một phần duy nhất :

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

Loại vấn đề này là một lý do khác tại sao tôi hỏi câu hỏi này:

Cài đặt ứng dụng trong cơ sở dữ liệu

Nếu bạn có một bảng cài đặt ứng dụng trong cơ sở dữ liệu của mình, bạn có thể có một mục tham chiếu ID của một bản ghi mà bạn muốn được coi là 'đặc biệt'. Sau đó, bạn sẽ tìm kiếm ID từ bảng cài đặt của mình, theo cách này, bạn không cần toàn bộ cột cho chỉ một mục được đặt.


Đây là một gợi ý tuyệt vời: Nó phù hợp hơn với thiết kế chuẩn hóa, hoạt động với mọi nền tảng cơ sở dữ liệu và dễ thực hiện nhất.
Nick Chammas

+1 nhưng lưu ý rằng "toàn bộ cột" có thể không sử dụng bất kỳ không gian vật lý nào tùy thuộc vào RDBMS của bạn :)
Jack Douglas

6

Phương pháp tiếp cận có thể sử dụng các công nghệ được triển khai rộng rãi:

1) Thu hồi các đặc quyền của 'nhà văn' trên bàn. Tạo các thủ tục CRUD để đảm bảo ràng buộc được thi hành tại các ranh giới giao dịch.

2) 6NF: thả CHAR(1)cột. Thêm một bảng tham chiếu bị ràng buộc để đảm bảo số lượng thẻ của nó không thể vượt quá một:

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

Thay đổi ngữ nghĩa ứng dụng sao cho 'mặc định' được coi là hàng trong bảng mới. Có thể sử dụng các khung nhìn để gói gọn logic này.

3) Thả CHAR(1)cột. Thêm một seqcột số nguyên. Đặt một ràng buộc duy nhất trên seq. Thay đổi ngữ nghĩa ứng dụng sao cho 'mặc định' được coi là hàng trong đó seqgiá trị là một hoặc seqgiá trị là giá trị lớn nhất / nhỏ nhất hoặc tương tự. Có thể sử dụng các khung nhìn để gói gọn logic này.


5

Đối với những người sử dụng MySQL, đây là một Quy trình được lưu trữ thích hợp:

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 ;

Để đảm bảo bảng của bạn sạch sẽ và quy trình được lưu trữ đang hoạt động, giả sử ID 200 là mặc định, hãy chạy các bước sau:

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;

Đây là một Kích hoạt cũng giúp:

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 ;

Để đảm bảo bảng của bạn sạch sẽ và trình kích hoạt đang hoạt động, giả sử ID 200 là mặc định, hãy chạy các bước sau:

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;

Hãy thử một lần !!!


3
Không có giải pháp dựa trên DRI cho MySQL? Chỉ có mã? Tôi tò mò vì tôi bắt đầu sử dụng MySQL ngày càng nhiều ...
gbn

4

Trong SQL Server 2000 trở lên, bạn có thể sử dụng Chế độ xem được lập chỉ mục để triển khai các ràng buộc phức tạp (hoặc nhiều bảng) giống như các ràng buộc bạn yêu cầu.
Ngoài ra, Oracle có một triển khai tương tự cho các khung nhìn cụ thể hóa với các ràng buộc kiểm tra hoãn lại.

Xem bài viết của tôi ở đây .


Bạn có thể cung cấp thêm một chút "thịt" trong câu trả lời này, như một đoạn mã ngắn không? Ngay bây giờ nó chỉ là một vài ý tưởng chung và một liên kết.
Nick Chammas

Nó sẽ là một chút khó khăn để phù hợp với một ví dụ ở đây. Nếu bạn nhấp vào liên kết, bạn sẽ tìm thấy "thịt" bạn đang tìm kiếm.
spaghettidba

3

Chuyển tiếp tiêu chuẩn SQL-92, được triển khai rộng rãi, ví dụ SQL Server 2000 trở lên:

Thu hồi các đặc quyền của 'nhà văn' từ bảng. Tạo hai khung nhìn cho WHERE chk = 'Y'WHERE chk = 'N'tương ứng, bao gồm WITH CHECK OPTION. Đối với WHERE chk = 'Y'chế độ xem, bao gồm một điều kiện tìm kiếm cho hiệu ứng mà số lượng thẻ của nó không thể vượt quá một. Cấp đặc quyền 'nhà văn' trên quan điểm.

Mã ví dụ cho các khung nhìn:

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

ngay cả khi RDBMS của bạn hỗ trợ điều này, nó sẽ tuần tự hóa như điên, vì vậy nếu bạn có nhiều hơn một người dùng, bạn có thể gặp sự cố
Jack Douglas

nếu nhiều người dùng đang sửa đổi đồng thời họ sẽ phải xếp hàng (tuần tự hóa) - đôi khi điều này là ổn, thường thì không (nghĩ rằng OLTP nặng hoặc giao dịch dài).
Jack Douglas

3
Cảm ơn đã làm rõ. Tôi phải nói rằng nếu nhiều người dùng thường xuyên đặt hàng mặc định duy nhất thì lựa chọn thiết kế (cột cờ trong cùng một bảng) là nghi vấn.
onedaywhen

3

Đây là một giải pháp cho MySQL và MariaDB bằng cách sử dụng các cột ảo thanh lịch hơn một chút. Nó yêu cầu MySQL> = 5.7.6 hoặc 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)

Tạo một cột ảo là NULL nếu bạn không muốn thực thi biện pháp chống độc đáo:

MariaDB [db]> ALTER table foo ADD checked_bar varchar(255) as (IF(chk, bar, null)) PERSISTENT UNIQUE;

(Đối với MySQL, sử dụng STOREDthay vì 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 tiêu chuẩn: sử dụng truy vấn con trong một CHECKràng buộc, không được triển khai rộng rãi, ví dụ như được hỗ trợ trong Access2000 (ACE2007, Jet 4.0, bất cứ điều gì) và ở trên khi ở Chế độ truy vấn ANSI-92 .

Mã ví dụ: các CHECKràng buộc ghi chú trong Access luôn ở mức bảng. Vì CREATE TABLEcâu lệnh trong câu hỏi sử dụng CHECKràng buộc mức hàng , nên nó cần được sửa đổi một chút bằng cách thêm dấu phẩy:

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
không tốt trong bất kỳ RDBMS nào tôi đã sử dụng ... hãy cẩn thận
Jack Douglas

0

Tôi chỉ lướt qua các câu trả lời, vì vậy tôi có thể đã bỏ lỡ một câu trả lời tương tự. Ý tưởng là sử dụng một cột được tạo là pk hoặc hằng số không tồn tại làm giá trị cho 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 điều này hợp lệ trong SQL2003 (vì bạn đang tìm kiếm một giải pháp bất khả tri). DB2 cho phép nó, không chắc có bao nhiêu nhà cung cấp khác chấp nhận nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.