Come posso forzare un record ad avere un valore vero per una colonna booleana e tutti gli altri un valore falso?


20

Voglio imporre che un solo record in una tabella sia considerato il valore "predefinito" per altre query o viste che potrebbero accedere a quella tabella.

Fondamentalmente, voglio garantire che questa query restituisca sempre esattamente una riga:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

Come lo farei in SQL?


1
"Voglio garantire che questa query restituisca sempre esattamente una riga" - sempre? Che dire di quando PostalCodesè vuoto? Se una riga ha già una proprietà, dovrebbe essere impedita l'impostazione su false a meno che un'altra riga (se presente) sia impostata su true all'interno della stessa istruzione SQL? Le righe zero possono avere la proprietà tra i limiti della transazione? L'ultima riga della tabella deve essere forzata ad avere la proprietà e impedire che venga eliminata? L'esperienza mi dice che "garantire esattamente una riga" tende a significare qualcosa di diverso nella realtà, spesso semplicemente "al massimo una riga".
onedayquando il

Risposte:


8

Modifica: questo è orientato a SQL Server prima di conoscere MySQL

Ci sono 4 5 modi per farlo: nell'ordine da più desiderato a meno desiderato

Non sappiamo che cosa sia RDBMS se per questo, quindi non tutti potrebbero applicarsi

  1. Il modo migliore è un indice filtrato. Questo utilizza DRI per mantenere l'unicità.

  2. Colonna calcolata con unicità (vedi la risposta di Jack Douglas) (aggiunta dalla modifica 2)

  3. Una vista indicizzata / materializzata che è come un indice filtrato che utilizza DRI

  4. Trigger (come da altre risposte)

  5. Controllare il vincolo con un UDF. Questo non è sicuro per l'isolamento della concorrenza e delle istantanee. Vedi Uno Due Tre Quattro

Si noti che il requisito per "un valore predefinito" è presente nella tabella, non nella procedura memorizzata. La procedura memorizzata avrà gli stessi problemi di concorrenza di un vincolo di controllo con un udf

Nota: chiesto più volte su:


gbn - sembra che l'indice filtrato / la soluzione DRI sia solo SQL 2008, quindi sembra che
salterò

10

Nota: questa risposta è stata data prima che fosse chiaro chi voleva qualcosa di specifico per MySQL. Questa risposta è distorta verso SQL Server.

Applicare un vincolo CHECK alla tabella che chiama un UDF e assicurarsi che il suo valore di ritorno sia <= 1. L'UDF può semplicemente contare le righe nella tabella DOVE isDefault = TRUE. Ciò garantirà che non vi siano più di 1 righe predefinite nella tabella.

Si dovrebbe aggiungere un bitmap o filtrata indice sulla isDefaultcolonna (a seconda del platforom) per assicurarsi che questo UDF corre molto rapidamente.

Avvertenze

  • Controllare i vincoli hanno alcune limitazioni . Vale a dire, vengono invocati per ogni riga modificata, anche se tali righe non sono state ancora impegnate in una transazione. Pertanto, se si avvia una transazione, impostare una nuova riga come predefinita e quindi annullare l'impostazione della riga precedente, il vincolo del controllo si lamenterà. Questo perché verrà eseguito dopo aver impostato la nuova riga come predefinita, trovando che ora ci sono due impostazioni predefinite e non riescono. La soluzione quindi è assicurarti di disinserire la riga predefinita prima di impostare un nuovo valore predefinito, anche se stai facendo questo lavoro in una transazione.
  • Come sottolineato da gbn , gli UDF possono restituire risultati incoerenti se si utilizza l'isolamento di istantanee in SQL Server. Tuttavia, un UDF in linea non si imbatte in questo problema, e poiché un UDF che fa solo un conteggio è in-line, questo non è un problema.
  • La forza della potenza di elaborazione di un motore di database è nella sua capacità di lavorare su set di dati contemporaneamente. Con un vincolo di controllo supportato da UDF, il motore si limiterà ad applicare questo UDF in serie a ogni riga modificata. Nel tuo caso d'uso è improbabile che eseguirai aggiornamenti di massa alla PostalCodestabella, tuttavia ciò rimane un problema di prestazioni per quelle situazioni in cui è probabile un'attività basata su set.

Dati tutti questi avvertimenti, ti consiglio di usare il suggerimento di gbn di un indice univoco filtrato invece di un vincolo di controllo.


4

Ecco una procedura memorizzata (dialetto 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 ;

Per assicurarsi che la tabella sia pulita e che la procedura memorizzata funzioni, supponendo che ID 200 sia l'impostazione predefinita, eseguire questi passaggi:

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;

Invece di una stored procedure, che ne dici di un trigger?

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 ;

Per assicurarsi che la tabella sia pulita e che il trigger funzioni, supponendo che ID 200 sia l'impostazione predefinita, eseguire questi passaggi:

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;

3

È possibile utilizzare un trigger per applicare le regole. Quando un'istruzione UPDATE o INSERT imposta isDefault su True, l'SQL nel trigger può impostare tutte le altre righe su False.

Dovrai considerare altre situazioni durante l'applicazione. Ad esempio, cosa dovrebbe accadere se più di una riga in e UPDATE o INSERT imposta è Default su True? Quali regole verranno applicate per evitare di infrangere la regola?

Inoltre, è possibile la condizione in cui non esiste alcun valore predefinito? Cosa succederà quando un aggiornamento imposta isDefault da True a False.

Dopo aver definito le regole, è possibile crearle nel trigger.

Quindi, come indicato in un'altra risposta, si desidera applicare un vincolo di controllo per far rispettare le regole.


3

Di seguito viene impostata una tabella di esempio e dati:

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

Se si crea una procedura memorizzata per impostare il flag IsDefault e rimuovere le autorizzazioni per modificare la tabella di base, è possibile applicarla con la query seguente. Richiederebbe una scansione della tabella ogni volta.

DECLARE @Id INT
SET @Id = 10

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

A seconda della dimensione della tabella, un indice su IsDefault (DESC) potrebbe comportare una o entrambe le query sottostanti evitando una scansione completa.

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)

Se non è possibile rimuovere le autorizzazioni dalla tabella di base, è necessario imporre l'integrità con altri mezzi e si utilizza SQL2008, è possibile utilizzare un indice univoco filtrato:

CREATE UNIQUE INDEX IX_MyTable_IsDefault  ON dbo.MyTable (IsDefault) WHERE IsDefault = 1

1

Per MySQL che non ha indici filtrati, potresti fare qualcosa di simile al seguente. Sfrutta il fatto che MySQL consente NULL multipli in indici univoci e utilizza una tabella dedicata e una chiave esterna per simulare un tipo di dati che può avere solo NULL e 1 come valori.

Con questa soluzione, invece di vero / falso, useresti solo 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`))*/

L'unica cosa che può andare storta, penso, è se qualcuno aggiunge una riga alla DefaultFlagtabella. Penso che questo non sia un grosso problema e puoi sempre risolverlo con le autorizzazioni se vuoi davvero essere al sicuro.


0
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

Questo essenzialmente ti dava problemi perché hai messo il campo nel posto sbagliato e basta.

Avere una tabella 'Predefiniti', con PostalCode, che contiene l'id che stai cercando. In generale, è anche meglio nominare le tabelle in base a un record, quindi il nome della tabella iniziale sarebbe meglio chiamato "PostalCode". I valori predefiniti saranno una tabella a riga singola, fino a quando non sarà necessario specializzare i valori predefiniti rispetto ad altre configurazioni (come gli storici).

L'ultima query che desideri è la seguente:

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.