Як змусити один запис мати справжнє значення для булевого стовпчика, а всі інші - помилкове значення?


20

Я хочу застосувати, що лише один запис у таблиці вважається значенням "за замовчуванням" для інших запитів чи представлень, які можуть отримати доступ до цієї таблиці.

В основному, я хочу гарантувати, що цей запит завжди буде повертати рівно один рядок:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

Як би я це зробив у SQL?


1
"Я хочу гарантувати, що цей запит завжди поверне рівно один рядок" - завжди? А як щодо того, коли PostalCodesпорожній? Якщо в рядку вже є властивість, слід запобігати встановленню значення false, якщо інший рядок (якщо такий існує) встановлений на true у тому ж самому операторі SQL? Чи можуть нульові рядки мати властивість між межами транзакцій? Чи слід змушувати останній рядок у таблиці мати властивість та не можна його видаляти? Досвід говорить про те, що "гарантія рівно одного ряду" має на увазі щось інше, часто просто "щонайменше один ряд".
день, коли

Відповіді:


8

Редагувати: це спрямовано на SQL Server, перш ніж ми знали MySQL

Існує 4 5 способів зробити це: в порядку найбільш бажаного до найменшого бажаного

Ми не знаємо, що це за RDBMS, якщо для цього не всі можуть застосовуватися

  1. Найкращий спосіб - відфільтрований індекс. Це використовує DRI для підтримки унікальності.

  2. Обчислена колонка з унікальністю (див. Відповідь Джека Дугласа) (додано редакцією 2)

  3. Індексований / матеріалізований вигляд, який схожий на відфільтрований індекс за допомогою DRI

  4. Тригер (відповідно до інших відповідей)

  5. Перевірте обмеження за допомогою АДС. Це не безпечно для одночасності та ізоляції знімків. Дивіться один два три чотири

Зауважте, що вимога до "одного за замовчуванням" є в таблиці, а не в збереженій процедурі. Збережена процедура матиме ті ж проблеми паралельності, що і обмеження чека з udf

Примітка: запитували на SO багато разів:


gbn - схоже, що відфільтрований індекс / рішення DRI є SQL 2008, тільки так виглядає, що я
перейду

10

Примітка. Ця відповідь була надана ще до того, як зрозуміло, що запитуючий бажає чогось конкретного MySQL. Ця відповідь є упередженою щодо SQL Server.

Застосуйте обмеження CHECK до таблиці, яка викликає UDF і переконається, що її повернене значення <= 1. UDF може просто підраховувати рядки в таблиці WHERE isDefault = TRUE. Це забезпечить наявність у таблиці не більше 1 рядка за замовчуванням.

Ви повинні додати растровий або відфільтрований індекс у isDefaultстовпчик (залежно від вашої платформи), щоб переконатися, що цей UDF працює дуже швидко.

Коваджі

  • Перевірки обмежень мають певні обмеження . А саме, вони викликаються для кожного зміненого рядка, навіть якщо ці рядки ще не були здійснені в транзакції. Отже, якщо ви розпочали транзакцію, встановите нову рядок за замовчуванням, а потім скасуєте стару рядок, ваше обмеження чека буде скаржитися. Це тому, що він буде виконуватися після того, як ви зробите нову рядок за замовчуванням, виявіть, що зараз є два за замовчуванням, і не вдасться. Вирішенням цього є переконатися, що ви спочатку скасуєте рядок за замовчуванням, перш ніж встановити новий за замовчуванням, навіть якщо ви виконуєте цю роботу під час транзакції.
  • Як зазначав gbn , UDF можуть повертати непослідовні результати, якщо ви використовуєте ізоляцію знімків у SQL Server. Однак вбудований АДС не стикається з цим питанням, і оскільки 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;

3

Ви можете використовувати тригер для застосування правил. Коли для оператора UPDATE або INSERT встановлено значення "За замовчуванням" - "True", SQL в тригері може встановити для всіх інших рядків значення False.

При застосуванні цього вам доведеться враховувати інші ситуації. Наприклад, що повинно статися, якщо для декількох рядків у наборах UPDATE або INSERT встановлено значення "За замовчуванням"? Які правила застосовуватимуться, щоб уникнути порушення правила?

Також, чи можлива умова, коли немає за замовчуванням? Що станеться, коли оновлення встановить параметр 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 як значення.

З цим рішенням замість істинного / хибного ви просто використовуватимете 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таблиці. Я думаю, що це не викликає особливих проблем, і ви завжди можете це виправити за допомогою дозволів, якщо дійсно хочете бути в безпеці.


0
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

Це по суті створювало проблеми, тому що ви ставите поле в неправильне місце, і це все.

Майте таблицю "За замовчуванням", у якій знаходиться PostalCode, який містить ідентифікатор, який ви шукаєте. Загалом, також добре називати свої таблиці відповідно до одного запису, так що назва вашої початкової таблиці краще буде називатися "PostalCode". Значення за замовчуванням буде таблицею з одним рядком, поки вам не потрібно буде спеціалізуватися за замовчуванням на іншій конфігурації (наприклад, історії).

Кінцевий запит, який потрібно, такий:

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.