¿Cómo fuerzo a un registro a tener un valor verdadero para una columna booleana y a todos los demás un valor falso?

20

Quiero exigir que solo un registro en una tabla se considere el valor "predeterminado" para otras consultas o vistas que puedan acceder a esa tabla.

Básicamente, quiero garantizar que esta consulta siempre devolverá exactamente una fila:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

¿Cómo haría eso en SQL?

emaynard
fuente
1
"Quiero garantizar que esta consulta siempre devolverá exactamente una fila" - ¿siempre? ¿Qué pasa cuando PostalCodesestá vacío? Si una fila ya tiene una propiedad que debería tener, ¿se evitará que se establezca en falso a menos que otra fila (si existe) se establezca en verdadero dentro de la misma instrucción SQL? ¿Pueden las filas cero tener la propiedad entre los límites de la transacción? ¿Debería forzarse la última fila de la tabla a tener la propiedad y evitar que se elimine? La experiencia me dice que "garantizar exactamente una fila" tiende a significar algo diferente en la realidad, a menudo simplemente "como máximo una fila".
día

Respuestas:

8

Editar: esto está orientado a SQL Server antes de que supiéramos MySQL

Hay 4 5 formas de hacer esto: en orden de más deseado a menos deseado

Sin embargo, no sabemos para qué es RDBMS, por lo que no todos pueden aplicarse

  1. La mejor manera es un índice filtrado. Esto usa DRI para mantener la unicidad.

  2. Columna calculada con unicidad (ver la respuesta de Jack Douglas) (agregado por edit 2)

  3. Una vista indexada / materializada que es como un índice filtrado usando DRI

  4. Disparador (según otras respuestas)

  5. 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

Tenga en cuenta que el requisito de "un valor predeterminado" está en la tabla, no en el procedimiento almacenado. El procedimiento almacenado tendrá los mismos problemas de concurrencia que una restricción de verificación con un udf

Nota: preguntado en tantas veces:

gbn
fuente
gbn: parece que la solución de índice filtrado / DRI es SQL 2008, así que parece que
pasaré
10

Nota: Esta respuesta se dio antes de que quedara claro que el autor de la pregunta quería algo específico de MySQL. Esta respuesta está sesgada hacia SQL Server.

Aplique una restricción CHECK a la tabla que llama a un UDF y se asegura de que su valor de retorno sea <= 1. El UDF solo puede contar las filas de la tabla DONDE isDefault = TRUE. Esto asegurará que no haya más de 1 fila predeterminada en la tabla.

Debe agregar un mapa de bits o un índice filtrado en la isDefaultcolumna (según su plataforma) para asegurarse de que este UDF se ejecute muy rápidamente.

Advertencias

  • Las restricciones de verificación tienen ciertas limitaciones . Es decir, se invocan para cada fila que se modifica, incluso si esas filas aún no se han confirmado en una transacción. Entonces, si comienza una transacción, establezca una nueva fila como predeterminada y luego desactive la fila anterior, su restricción de verificación se quejará. Esto se debe a que se ejecutará después de que haga que la nueva fila sea la predeterminada, descubra que ahora hay dos valores predeterminados y falle. La solución es asegurarse de desarmar la fila predeterminada primero antes de establecer una nueva predeterminada, incluso si está haciendo este trabajo en una transacción.
  • Como señaló gbn , las UDF pueden devolver resultados inconsistentes si está utilizando el aislamiento de instantáneas en SQL Server. Sin embargo, un UDF en línea no se encuentra con este problema, y ​​dado que un UDF que solo cuenta es inlineable, este no es un problema aquí.
  • La fuerza del poder de procesamiento de un motor de base de datos está en su capacidad para trabajar en conjuntos de datos a la vez. Con una restricción de verificación respaldada por UDF, el motor se limitará a aplicar este UDF en serie a cada fila que se modifique. En su caso de uso, es poco probable que realice actualizaciones masivas de la PostalCodestabla, sin embargo, esto sigue siendo una preocupación de rendimiento para aquellas situaciones en las que es probable una actividad basada en conjuntos.

Dadas todas estas advertencias, recomiendo usar la sugerencia de gbn de un índice único filtrado en lugar de una restricción de verificación.

Nick Chammas
fuente
4

Aquí hay un procedimiento almacenado (dialecto 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 ;

Para asegurarse de que su tabla esté limpia y que el procedimiento almacenado esté funcionando, 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;

En lugar de un procedimiento almacenado, ¿qué tal un disparador?

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;
RolandoMySQLDBA
fuente
3

Podría usar un disparador para aplicar las reglas. Cuando una instrucción UPDATE o INSERT establece isDefault en True, el SQL en el desencadenador puede establecer todas las demás filas en False.

Tendrás que considerar otras situaciones al aplicar esto. Por ejemplo, ¿qué debería suceder si más de una fila y los conjuntos ACTUALIZAR o INSERTAR son predeterminados a verdaderos? ¿Qué reglas se aplicarán para evitar romper la regla?

Además, ¿es posible la condición donde no hay un defecto? Qué sucederá cuando una actualización establezca isDefault de True a False.

Una vez que defina las reglas, puede construirlas en el activador.

Luego, como se indica en otra respuesta, desea aplicar una restricción de verificación para ayudar a hacer cumplir las reglas.

bobs
fuente
3

A continuación se configura una tabla y datos de ejemplo:

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

Si crea un procedimiento almacenado para establecer el indicador IsDefault y eliminar los permisos para cambiar la tabla base, puede aplicar esto con la consulta a continuación. Se requeriría un escaneo de tabla cada vez.

DECLARE @Id INT
SET @Id = 10

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

Dependiendo del tamaño de la tabla, un índice en IsDefault (DESC) puede generar una o ambas consultas a continuación, evitando un análisis completo.

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)

Si no puede eliminar los permisos de la tabla base, necesita hacer cumplir la integridad por otros medios y está utilizando SQL2008, puede utilizar un índice único filtrado:

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

Para MySQL que no tiene índices filtrados, puede hacer algo como lo siguiente. Explota el hecho de que MySQL permite múltiples NULL en índices únicos y utiliza una tabla dedicada y una clave foránea para simular un tipo de datos que solo puede tener NULL y 1 como valores.

Con esta solución, en lugar de verdadero / falso, solo usaría 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`))*/

Creo que lo único que puede salir mal es si alguien agrega una fila a la DefaultFlagtabla. Creo que esto no es una gran preocupación, y siempre puedes arreglarlo con permisos si realmente quieres estar seguro.

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

Básicamente, esto te estaba dando problemas porque colocaste el campo en el lugar equivocado y eso es todo.

Tenga una tabla 'Defaults', con PostalCode, que contenga la identificación que está buscando. En general, también es bueno nombrar sus tablas de acuerdo con un registro, por lo que el nombre de su tabla inicial sería mejor llamado 'Código postal'. Los valores predeterminados serán una tabla de una sola fila, hasta que necesite especializar sus valores predeterminados en alguna otra configuración (como el historial).

La consulta final que desea es la siguiente:

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