it-swarm-eu.dev

Jak implementovat 'výchozí' příznak, který lze nastavit pouze na jeden řádek

Například s tabulkou podobnou této:

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

Nezáleží na tom, zda je příznak implementován jako char(1), bit nebo cokoli. Chci jen vynutit omezení, které lze nastavit pouze na jeden řádek.

SQL Server 2008 - Filtrovaný jedinečný index

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

SQL Server 2000, 2005:

Můžete využít skutečnosti, že v jedinečném indexu je povolen pouze jeden null:

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

pro rok 2000 možná budete potřebovat SET ARITHABORT ON (díky @ info za tuto informaci)

Věštec:

Protože Oracle neindexuje položky, ve kterých jsou všechny indexované sloupce nulové, můžete použít jedinečný index založený na funkcích:

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

Tento index bude vždy indexovat pouze jeden řádek.

Znáte-li tuto skutečnost indexu, můžete implementovat bitový sloupec trochu jinak:

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

Zde budou možné hodnoty pro sloupec chkY a NULL. Pouze jeden řádek může mít hodnotu Y.

14
Vincent Malgrat

Myslím, že se jedná o případ správné strukturování databázových tabulek. Aby to bylo konkrétnější, pokud máte osobu s více adresami a chcete, aby byla výchozí, myslím, že byste měli uložit ID adresy výchozí adresy do tabulky osob, neměli byste mít výchozí sloupec v tabulce adres:

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

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

Můžete nastavit DefaultAddressID na null, ale tímto způsobem struktura vynutí vaše omezení.

13
Decker97

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

Omezení kontroly jsou v MySQL ignorována, takže musíme považovat null nebo false za false a true za true. Maximálně jeden řádek může mít chk=true

Můžete považovat za vylepšení přidat spouštěč pro změnu false na true na insert/update jako řešení pro nedostatek omezení kontroly - IMO to však není vylepšení.

Doufal jsem, že budu moci použít znak (0), protože to je

je také docela pěkné, když potřebujete sloupec, který může mít pouze dvě hodnoty: Sloupec, který je definován jako CHAR (0) NULL, zabírá pouze jeden bit a může přijímat pouze hodnoty NULL a ''

Bohužel, alespoň u MyISAM a InnoDB

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

--Upravit

od MySQL to nakonec není dobré řešení, boolean je synonymum pro tinyint(1) , a tak umožňuje nenulové hodnoty než 0 nebo 1. Je to je možné, že bit by byla lepší volbou

SQL Server:

Jak to udělat:

  1. Nejlepší způsob je filtrovaný index. Používá DRI
    SQL Server 2008+

  2. Počítačový sloupec s jedinečností. Používá DRI
    Viz odpověď Jacka Douglase. SQL Server 2005 a starší

  3. Indexované/materializované zobrazení, které je jako filtrovaný index. Používá DRI
    Všechny verze.

  4. Spoušť. Používá kód, nikoli DRI.
    Všechny verze

Jak to udělat:

  1. Zkontrolujte omezení pomocí UDF. Toto není bezpečné pro izolaci souběžnosti a snímků.
    Viz OneTwoThreeFour
10
gbn

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"

--Upravit

nebo (mnohem lépe), použijte jedinečný částečný index :

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"

Možné přístupy využívající široce implementované technologie:

1) Zrušte u stolu oprávnění „spisovatele“. Vytvořte procedury CRUD, které zajistí vynucení omezení na hranici transakce.

2) 6NF: zrušte sloupec CHAR(1). Přidejte referenční tabulku omezenou, aby její mohutnost nepřesáhla jednu:

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

Změňte sémantiku aplikace tak, aby byla považována za „výchozí“ řádek v nové tabulce. Tuto logiku lze zapouzdřit pomocí pohledů.

3) Vypusťte sloupec CHAR(1). Přidejte sloupec seq integer. Vložte jedinečné omezení pro seq. Změňte sémantiku aplikace tak, aby uvažovaná „výchozí“ byla řádek, kde hodnota seq je jedna nebo hodnota seq největší/nejmenší nebo podobná hodnota. Tuto logiku lze zapouzdřit pomocí pohledů.

6
onedaywhen

Tento druh problému je dalším důvodem, proč jsem se zeptal na tuto otázku:

Nastavení aplikace v databázi

Pokud máte v databázi tabulku nastavení aplikace, můžete mít záznam, který by odkazoval na ID jednoho záznamu, který chcete považovat za „speciální“. Pak byste jen hledali, co je ID z tabulky nastavení, takže nepotřebujete celý sloupec pouze pro jednu nastavenou položku.

6
CenterOrbit

Pro ty, kteří používají MySQL, je zde vhodná uložená procedura:

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 ;

Chcete-li se ujistit, že je tabulka čistá a zda uložená procedura funguje, za předpokladu, že je výchozí hodnota ID 200, spusťte tyto kroky:

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;

Tady je spouštěč, který také pomáhá:

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 ;

Chcete-li se ujistit, že je tabulka čistá a že spouštěč funguje, za předpokladu, že je výchozí hodnota ID 200, spusťte tyto kroky:

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;

Pokusit se !!!

5
RolandoMySQLDBA

V SQL Server 2000 a více můžete použít indexovaná zobrazení k implementaci složitých (nebo vícerozměrných) omezení, jako jsou ta, která požadujete.
Také Oracle má podobnou implementaci pro zhmotněné pohledy s odloženými omezeními kontroly.

Viz můj příspěvek zde.

4
spaghettidba

Standardní přechodný SQL-92, široce implementovaný např. SQL Server 2000 a vyšší:

Z tabulky zrušíte práva pro spisovatele. Vytvořte dva pohledy pro WHERE chk = 'Y' a WHERE chk = 'N', včetně WITH CHECK OPTION. Pro WHERE chk = 'Y' view, zahrňte podmínku vyhledávání v tom smyslu, že její mohutnost nesmí překročit jednu. Udělte oprávnění „spisovatel“ na zobrazení.

Příklad kódu pro zobrazení:

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

Zde je řešení pro MySQL a MariaDB pomocí virtuálních sloupců, které jsou o něco elegantnější. Vyžaduje MySQL> = 5.7.6 nebo 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)

Vytvořte virtuální sloupec, který je NULL, pokud si nepřejete vynutit jedinečný rozpor:

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

(Pro MySQL použijte STORED místo 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           |
+------+------+-------------+
3

Standardní PLNÝ SQL-92: použijte poddotaz v omezení CHECK, který není široce implementován např. podporováno v Access2000 (ACE2007, Jet 4.0, cokoli) a výše, pokud je v ANSI-92 Query Mode .

Příklad kódu: poznámka CHECK omezení v aplikaci Access jsou vždy na úrovni tabulky. Protože příkaz CREATE TABLE V otázce používá omezení na úrovni řádku CHECK, je třeba jej mírně změnit přidáním čárky:

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
onedaywhen

Procházel jsem pouze odpověďmi, takže jsem mohl vynechat podobnou odpověď. Cílem je použít generovaný sloupec, který je buď p.k, nebo konstanta, která neexistuje jako hodnota pro p.k.

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 to platí v SQL2003 (protože jste hledali agnostické řešení). DB2 to umožňuje, není si jistý, kolik dalších dodavatelů to přijalo.

0
Lennart