Cómo implementar un indicador 'predeterminado' que solo se puede establecer en una sola fila


31

Por ejemplo, con una tabla similar a esta:

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

No importa si la bandera se implementa como a char(1), a bito lo que sea. Solo quiero poder imponer la restricción de que solo se puede establecer en una sola fila.


inspirado en esta pregunta que se limita a MySQL
Jack Douglas

2
La forma en que se formula la pregunta sugiere que usar una tabla debe ser la respuesta incorrecta. Pero a veces (¿la mayoría de las veces?) Agregar otra tabla es una buena idea. Y agregar una tabla es completamente independiente de la base de datos.
Mike Sherrill 'Cat Recall'

Respuestas:


31

SQL Server 2008: índice único filtrado

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

16

SQL Server 2000, 2005:

Puede aprovechar el hecho de que solo se permite un valor nulo en un índice único:

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

para 2000, puede que necesite SET ARITHABORT ON(gracias a @gbn por esta información)


14

Oráculo:

Dado que Oracle no indexa las entradas donde todas las columnas indexadas son nulas, puede usar un índice único basado en funciones:

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

Este índice solo indexará una sola fila como máximo.

Conociendo este hecho del índice, también puede implementar la columna de bits de manera ligeramente diferente:

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

Aquí los valores posibles para la columna chkserán Yy NULL. Solo una fila como máximo puede tener el valorY.


chk necesita una not nullrestricción?
Jack Douglas

@jack: puede agregar una not nullrestricción si no desea valores nulos (no me quedó claro a partir de las especificaciones de la pregunta). Solo una fila puede tener el valor 'Y' en cualquier caso.
Vincent Malgrat

+1 Entiendo lo que quieres decir: ¿tienes razón, no es necesario (pero quizás es un poco más ordenado, especialmente si se combina con un default)?
Jack Douglas

2
@jack: su comentario me hizo darme cuenta de que existe una posibilidad aún más simple si acepta que la columna puede ser Yo null, vea mi actualización.
Vincent Malgrat

1
La opción 2 tiene el beneficio adicional de que el índice será pequeño a medida que nullse omiten, a costa de cierta claridad, quizás
Jack Douglas

13

Creo que este es un caso de estructurar sus tablas de base de datos correctamente. Para hacerlo más concreto, si tiene una persona con varias direcciones y desea que una sea la predeterminada, creo que debe almacenar el addressID de la dirección predeterminada en la tabla de personas, no tener una columna predeterminada en la tabla de direcciones:

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

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

Puede hacer que el DefaultAddressID sea anulable, pero de esta manera la estructura aplica su restricció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

Comprobación se ignora en MySQL por lo que tenemos que considerar nullo falsecomo falso y truelo verdadero. Como máximo 1 fila puede tenerchk=true

Puede considerar una mejora agregar un disparador para cambiar falseal trueinsertar / actualizar como una solución alternativa por la falta de una restricción de verificación. Sin embargo, en mi opinión, no es una mejora.

Esperaba poder usar un char (0) porque

también es bastante bueno cuando necesita una columna que puede tomar solo dos valores: una columna que se define como CHAR (0) NULL ocupa solo un bit y puede tomar solo los valores NULL y ''

Desafortunadamente, con MyISAM e InnoDB al menos, obtengo

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

--editar

después de todo, esta no es una buena solución, ya que en MySQL booleanes sinónimo detinyint(1) , y por lo tanto, permite valores no nulos de 0 o 1. Es posible que bitsea ​​una mejor opción


Esto podría responder mi comentario a la respuesta de RolandoMySQLDBA: ¿podemos tener soluciones MySQL con DRI?
gbn

Es un poco feo, aunque debido a la null, false, true- me pregunto si hay algo más limpio ...
Jack Douglas

@Jack - +1 para un buen intento de enfoque DRI puro en MySQL.
RolandoMySQLDBA

Aconsejaría evitar el uso de falso aquí ya que la restricción única solo permitiría suministrar uno de esos valores falsos. Si nulo representa falso, entonces se debe usar de manera consistente en todo momento: se podría evitar evitar falso si hay una validación adicional disponible (por ejemplo, JSR-303 / hibernate-validator).
Steve Chambers

1
Las versiones recientes de MySQL / MariaDB implementan columnas virtuales que creo que permiten una solución un poco más elegante que se describe a continuación en dba.stackexchange.com/a/144847/94908
MattW.

10

Servidor SQL:

Cómo hacerlo:

  1. La mejor manera es un índice filtrado. Utiliza DRI
    SQL Server 2008+

  2. Columna calculada con singularidad. Utiliza DRI
    Ver la respuesta de Jack Douglas. SQL Server 2005 y antes

  3. Una vista indexada / materializada que es como un índice filtrado. Utiliza DRI
    Todas las versiones.

  4. Desencadenar. Utiliza código, no DRI.
    Todas las versiones

Cómo no hacerlo:

  1. Verifique la restricción con un UDF. Esto no es seguro para la concurrencia y el aislamiento de instantáneas.
    Ver uno dos tres cuatro

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"

--editar

o (mucho mejor), use un índice parcial único :

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

Este tipo de problema es otra razón por la que solicité esta pregunta:

Configuración de la aplicación en la base de datos

Si tiene una tabla de configuración de aplicaciones en su base de datos, puede tener una entrada que haga referencia a la ID del registro que desea que se considere 'especial'. Luego, solo debe buscar cuál es la ID de su tabla de configuración, de esta manera no necesita una columna completa para solo un elemento que se está configurando.


Esta es una gran sugerencia: es más acorde con el diseño normalizado, funciona con cualquier plataforma de base de datos y es más fácil de implementar.
Nick Chammas 01 de

+1 pero tenga en cuenta que "una columna completa" podría no usar ningún espacio físico dependiendo de su RDBMS :)
Jack Douglas

6

Posibles enfoques utilizando tecnologías ampliamente implementadas:

1) Revocar los privilegios de 'escritor' sobre la mesa. Cree procedimientos CRUD que garanticen que la restricción se aplique en los límites de la transacción.

2) 6NF: suelte la CHAR(1)columna. Agregue una tabla de referencia restringida para garantizar que su cardinalidad no pueda exceder uno:

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

Cambie la semántica de la aplicación para que el 'valor predeterminado' considerado sea la fila en la nueva tabla. Posiblemente use vistas para encapsular esta lógica.

3) Suelta la CHAR(1)columna. Agrega una seqcolumna entera. Poner una restricción única seq. Cambie la semántica de la aplicación para que el 'valor predeterminado' considerado sea la fila donde el seqvalor es uno o el seqvalor más grande / más pequeño o similar. Posiblemente use vistas para encapsular esta lógica.


5

Para aquellos que usan MySQL, aquí hay un procedimiento almacenado apropiado:

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 ;

Para asegurarse de que su tabla esté limpia y que el procedimiento almacenado funcione, suponiendo que la ID 200 sea la predeterminada, ejecute estos pasos:

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;

Aquí hay un disparador que también ayuda:

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 ;

Para asegurarse de que su tabla esté limpia y que el disparador funcione, suponiendo que la ID 200 sea la predeterminada, ejecute estos pasos:

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;

Darle una oportunidad !!!


3
¿No hay una solución basada en DRI para MySQL? Solo codigo? Tengo curiosidad porque estoy empezando a usar MySQL cada vez más ...
gbn

4

En SQL Server 2000 y versiones posteriores, puede usar Vistas indexadas para implementar restricciones complejas (o de varias tablas) como la que está solicitando.
Además, Oracle tiene una implementación similar para vistas materializadas con restricciones de verificación diferida.

Mira mi publicación aquí .


¿Podría proporcionar un poco más de "carne" en esta respuesta, como un pequeño fragmento de código? En este momento son solo un par de ideas generales y un enlace.
Nick Chammas

Sería un poco difícil poner un ejemplo aquí. Si hace clic en el enlace, encontrará la "carne" que está buscando.
spaghettidba

3

SQL-92 de transición estándar, ampliamente implementado, por ejemplo, SQL Server 2000 y superior:

Revocar los privilegios de 'escritor' de la tabla. Cree dos vistas para WHERE chk = 'Y'y WHERE chk = 'N'respectivamente, incluido WITH CHECK OPTION. Para la WHERE chk = 'Y'vista, incluya una condición de búsqueda para que su cardinalidad no pueda exceder una. Otorgue privilegios de 'escritor' en las vistas.

Código de ejemplo para las vistas:

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

incluso si su RDBMS admite esto, se serializará como loco, por lo que si tiene más de un usuario, puede tener un problema
Jack Douglas,

Si varios usuarios están modificando simultáneamente, tendrán que hacer cola (serializar); a veces esto está bien, a menudo no lo está (piense en OLTP pesado o transacciones largas).
Jack Douglas

3
Gracias por aclararlo. Debo decir que si varios usuarios configuran con frecuencia la única fila predeterminada, entonces la opción de diseño (columna de marca en la misma tabla) es cuestionable.
cuando el

3

Aquí hay una solución para MySQL y MariaDB usando columnas virtuales que es un poco más elegante. Requiere MySQL> = 5.7.6 o 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)

Cree una columna virtual que sea NULL si no desea aplicar la restricción única:

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

(Para MySQL, use en STOREDlugar de 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

SQL-92 COMPLETO estándar: use una subconsulta en una CHECKrestricción, no ampliamente implementada, por ejemplo, compatible con Access2000 (ACE2007, Jet 4.0, lo que sea) y superior cuando está en modo de consulta ANSI-92 .

Código de ejemplo: las CHECKrestricciones de nota en Access siempre son a nivel de tabla. Debido a que la CREATE TABLEdeclaración en la pregunta usa una CHECKrestricción de nivel de fila , debe modificarse ligeramente agregando una coma:

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
no es bueno en ningún RDBMS que he usado ... abundan las advertencias
Jack Douglas

0

Solo hojeé las respuestas, así que podría haber perdido una respuesta similar. La idea es utilizar una columna generada que sea pk o una constante que no exista como valor para 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 esto es válido en SQL2003 (ya que estaba buscando una solución agnóstica). DB2 lo permite, no estoy seguro de cuántos otros proveedores lo aceptan.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.