Как заставить одну запись иметь истинное значение для логического столбца, а все остальные - ложное значение?

20

Я хочу обеспечить, чтобы только одна запись в таблице считалась значением «по умолчанию» для других запросов или представлений, которые могут обращаться к этой таблице.

По сути, я хочу гарантировать, что этот запрос всегда будет возвращать ровно одну строку:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

Как бы я сделал это в SQL?

emaynard
источник
1
«Я хочу гарантировать, что этот запрос всегда будет возвращать ровно одну строку» - всегда? Как насчет того, когда PostalCodesпусто? Если у строки уже есть свойство, должно быть предотвращено присвоение ему значения false, если другая строка (если она существует) не установлена ​​в значение true в том же операторе SQL? Могут ли нулевые строки иметь свойство между границами транзакции? Должна ли последняя строка в таблице иметь свойство и не должна быть удалена? Опыт подсказывает мне, что «гарантия ровно в один ряд» в действительности означает нечто иное, часто просто «не более одного ряда».
понедельник,

Ответы:

8

Изменить: это связано с SQL Server, прежде чем мы знали MySQL

Есть 4 5 способов сделать это: от наиболее желаемого до наименее желаемого

Мы не знаем, что это за СУРБД, хотя и не все могут применяться

  1. Лучший способ - фильтрованный индекс. Это использует DRI для поддержания уникальности.

  2. Вычисляемый столбец с уникальностью (см. Ответ Джека Дугласа) (добавлено редактированием 2)

  3. Индексированное / материализованное представление, которое похоже на отфильтрованный индекс с использованием DRI

  4. Триггер (согласно другим ответам)

  5. Проверьте ограничение с помощью UDF. Это не безопасно для параллелизма и изоляции моментальных снимков. Смотреть один два три четыре

Обратите внимание, что требование для «одного значения по умолчанию» указано в таблице, а не в хранимой процедуре. Хранимая процедура будет иметь те же проблемы параллелизма, что и ограничение проверки с помощью udf

Примечание: много раз спрашивали об этом:

ГБН
источник
gbn - похоже, что решение с отфильтрованным индексом / DRI - это только SQL 2008, поэтому похоже, что я
перейду
10

Примечание. Этот ответ был дан до того, как стало ясно, что спрашивающий хочет что-то специфичное для MySQL. Этот ответ смещен в сторону SQL Server.

Примените ограничение CHECK к таблице, которая вызывает UDF и проверяет, что его возвращаемое значение <= 1. UDF может просто считать строки в таблице WHERE isDefault = TRUE. Это обеспечит наличие в таблице не более 1 строки по умолчанию.

Вы должны добавить в столбец растровое изображение или отфильтрованный индекс isDefault(в зависимости от вашей платформы), чтобы этот UDF работал очень быстро.

Предостережения

  • Проверочные ограничения имеют определенные ограничения . А именно, они вызываются для каждой измененной строки, даже если эти строки еще не были зафиксированы в транзакции. Таким образом, если вы начнете транзакцию, установите новую строку в качестве значения по умолчанию, а затем удалите старую строку, ваше ограничение проверки будет жаловаться. Это потому, что он будет выполнен после того, как вы сделаете новую строку по умолчанию, обнаружите, что теперь есть два значения по умолчанию, и потерпите неудачу. Решение состоит в том, чтобы сначала убрать строку по умолчанию перед установкой нового по умолчанию, даже если вы выполняете эту работу в транзакции.
  • Как указал gbn , пользовательские функции могут возвращать противоречивые результаты, если вы используете изоляцию моментальных снимков в SQL Server. Однако встроенный UDF не сталкивается с этой проблемой, и, поскольку UDF, который просто выполняет подсчет, является встроенным, это не является проблемой здесь.
  • Сила вычислительной мощности ядра базы данных заключается в его способности работать с наборами данных одновременно. При проверочном ограничении, поддерживаемом UDF, механизм будет ограничен применением этого UDF последовательно к каждой изменяемой строке. В вашем случае использования маловероятно, что вы будете выполнять массовые обновления PostalCodesтаблицы, однако это остается проблемой производительности в тех ситуациях, когда вероятно действие на основе набора.

Учитывая все эти предостережения, я рекомендую использовать предложение gbn отфильтрованного уникального индекса вместо проверочного ограничения.

Ник Чаммас
источник
4

Вот хранимая процедура (MySQL Dialect):

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 ;

Чтобы убедиться, что ваша таблица чистая и хранимая процедура работает, предполагая, что ID 200 является значением по умолчанию, выполните следующие действия:

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;

Вместо хранимой процедуры, как насчет триггера?

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 ;

Чтобы убедиться, что ваша таблица чистая и триггер работает, предполагая, что ID 200 является значением по умолчанию, выполните следующие действия:

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
источник
3

Вы можете использовать триггер, чтобы применить правила. Когда оператор UPDATE или INSERT устанавливает для isDefault значение True, SQL в триггере может установить для всех остальных строк значение False.

Вы должны учитывать другие ситуации при применении этого. Например, что должно произойти, если несколько строк и UPDATE или INSERT задают для isDefault значение True? Какие правила будут применяться, чтобы избежать нарушения правила?

Кроме того, возможно ли условие, когда нет дефолта? Что произойдет, когда обновление установит isDefault с True на False.

Как только вы определите правила, вы можете встроить их в триггер.

Затем, как указано в другом ответе, вы хотите применить проверочное ограничение, чтобы обеспечить соблюдение правил.

качается
источник
3

Ниже приведены пример таблицы и данных:

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

Если вы создадите хранимую процедуру для установки флага IsDefault и удалите разрешения на изменение базовой таблицы, вы можете применить это с помощью запроса ниже. Это потребует сканирования таблицы каждый раз.

DECLARE @Id INT
SET @Id = 10

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

В зависимости от размера таблицы, индекс IsDefault (DESC) может привести к тому, что один или оба приведенных ниже запроса будут избегать полного сканирования.

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)

Если вы не можете удалить разрешения из базовой таблицы, нужно обеспечить целостность другими средствами и использовать SQL2008, вы можете использовать отфильтрованный уникальный индекс:

CREATE UNIQUE INDEX IX_MyTable_IsDefault  ON dbo.MyTable (IsDefault) WHERE IsDefault = 1
Марк Стори-Смит
источник
1

Для MySQL, у которого нет отфильтрованных индексов, вы можете сделать что-то вроде следующего. Он использует тот факт, что MySQL допускает использование нескольких NULL в уникальных индексах, и использует выделенную таблицу и внешний ключ для имитации типа данных, который может иметь только NULL и 1 в качестве значений.

С этим решением вместо true / false вы бы просто использовали 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`))*/

Думаю, единственное, что может пойти не так, это если кто-то добавит строку в DefaultFlagтаблицу. Я думаю, что это не большая проблема, и вы всегда можете исправить это с разрешениями, если вы действительно хотите быть в безопасности.

djfm
источник
0
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

По сути, это создавало вам проблемы, потому что вы поставили поле не в том месте, и все.

У вас есть таблица «По умолчанию», в которой есть PostalCode, в которой содержится идентификатор, который вы ищете. В общем, также хорошо называть ваши таблицы в соответствии с одной записью, поэтому лучше будет называть имя вашей исходной таблицы «PostalCode». По умолчанию будет таблица из одной строки, пока вам не нужно будет специализировать ваши значения по умолчанию для какой-либо другой конфигурации (например, для истории).

Последний запрос, который вы хотите, выглядит следующим образом:

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
Кёнчог
источник