Как реализовать уникальность, где порядок полей не имеет значения - PullRequest
9 голосов
/ 14 марта 2012

Я думаю, что следующий пример лучше объяснит ситуацию.Допустим, у нас есть следующая структура таблицы:

-------------------------------------
Member1   int      NOT NULL (FK)(PK)
Member2   int      NOT NULL (FK)(PK)
-------------------------------------
Statust   char(1)  NOT NULL

Вот содержимое таблицы для таблицы:

Member1    Member2    Status
----------------------------
  100        105        A

Мой вопрос заключается в том, как реализовать уникальность, чтобы следующий оператор INSERTбудет FAIL на основе этой строки в таблице.

INSERT status_table (Member1,Member2,Status) VALUES(105,100,'D');

По сути, я пытаюсь смоделировать отношения между двумя членами.Поле Status одинаково, независимо от того, есть у нас (100,105) или (105,100).

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

Ответы [ 8 ]

6 голосов
/ 14 марта 2012

Если вы можете убедиться, что все приложения / пользователи хранят идентификаторы членов в порядке наименьшего к величине (наименьший MemberID в Member1 и наибольший в Member2), тогда вы можете просто добавить ограничение Check:

ALTER TABLE Status_table
  ADD CONSTRAINT Status_table_Prevent_double_pairs
    CHECK (Member1 < Member2)

Если вы не хотите этого делать или хотите, чтобы эта дополнительная информация была сохранена (потому что вы храните (просто пример), что "участник 100 пригласил (понравился, убил), что угодно) член 150 ", а не наоборот), тогда вы могли бы использовать подход @ Tegiri, немного изменив (умножение двух достаточно больших целых было бы проблемой переполнения в противном случае):

CREATE TABLE Status_table
( Member1 INT NOT NULL
, Member2 INT NOT NULL
, Status CHAR(1) NOT NULL
, MemberOne  AS CASE WHEN Member1 < Member2 THEN Member1 ELSE Member2 END
          --- a computed column
, MemberTwo  AS CASE WHEN Member1 < Member2 THEN Member2 ELSE Member1 END
          --- and another one
, PRIMARY KEY (Member1, Member2)
, UNIQUE (MemberOne, MemberTwo)
, ...                                    --- FOREIGN KEY details, etc 
) ;
3 голосов
/ 15 марта 2012

Вот альтернативный способ взглянуть на это. Вы могли бы фактически применить правило, согласно которому взаимоотношения всегда выражаются наличием двух строк (A, B) и (B, A) вместо одной.

CREATE TABLE MutualRelationship
 (Member1 INT NOT NULL,
  Member2 INT NOT NULL,
  Status CHAR(1),
 PRIMARY KEY (Member1, Member2),
 UNIQUE (Member1, Member2, Status),
 FOREIGN KEY (Member2, Member1, Status) REFERENCES MutualRelationship (Member1, Member2, Status));

INSERT INTO MutualRelationship (Member1, Member2, Status)
VALUES
(100,105,'A'),
(105,100,'A');
3 голосов
/ 14 марта 2012

Модель базы данных дает сбой, потому что у вас есть две сущности {Member1, Member2}, которые, говоря, что не имеет значения, какой, вы говорите, являются одной и той же сущностью {Member}.Другими словами, у вас есть один факт в двух местах, один из главных грехов проектирования реляционных баз данных.

Высокоуровневым решением было бы лучшее моделирование характера отношений.Примером может служить брак двух человек.Вместо того, чтобы «Жених и невеста состоят в браке» и суетой, которая указана первой, у вас будет «Брак #xyz между (содержит) участников А и В».Итак, таблица Marriage с первичным ключом, таблица MarriageMember с внешним ключом для Marriage, внешний ключ для «Person» и первичный ключ для обоих столбцов.Позволяет иметь более двух участников, что может быть полезно, если вы находитесь в истории Хайнлайна.

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


Дополнения

Согласно комментариям ниже, если вы моделируете пользователей, которыеданный участник связан с, то у вас возникает ситуация «Участник связан с другими участниками».Здесь Member1 является «основным» членом, а Member2 является другим членом, с которым связан «этот» член.(И это различие необходимо для двух столбцов-членов.) Таким образом, если отношение является двунаправленным, вам понадобятся две записи, чтобы охватить оба элемента: «Член A связан с членом B» и «Член B связан с Участником A».Это, конечно, будет применяться с первичным ключом {Member1, Member2}, так как статус, по-видимому, не имеет значения (есть только одно отношение, а не несколько в зависимости от состояния).

2 голосов
/ 14 марта 2012

Здесь вы можете найти отрывок из «Симметричных функций» в книге «Шаблоны проектирования SQL».

Рассмотрим базу данных инвентаря коробок

table Boxes (
   length integer,
   width  integer,
   height integer
)

Размеры коробки в реальном мире, однако, обычно не указываются в каком-либо конкретном порядке. Выбор размеров, которые становятся длиной, шириной и высотой, по существу, произвольный. Что если мы хотим идентифицировать коробки в соответствии с их размерами? Например, мы хотели бы иметь возможность сказать, что поле с длинами = 1, шириной = 2 и высотой = 3 совпадает с полем с длинами = 3, шириной = 1 и высотой = 2. Кроме того, как насчет объявления уникального размерного ограничения? Точнее говоря, мы не допустим двух коробок одинакового размера.

Аналитическому уму не составило бы труда признать, что суть проблемы - это порядок столбцов. Значения столбцов длины, ширины и высоты можно поменять местами, чтобы сформировать другую допустимую запись! Поэтому, почему бы нам не ввести 3 псевдостолбца, скажем, A, B и C, таких, что

A ≤ B ≤ C

Тогда единственное ограничение на A, B, C должно удовлетворять нашему требованию! Он может быть реализован как уникальный индекс, основанный на функции, при условии, что мы можем выражать A, B, C аналитически в терминах длины, ширины и высоты. Кусок торта: A - это наибольшая длина, ширина, высота; C является наименьшим из них, но как мы выражаем B? Ну, ответ легко написать

B = least (greatest (length,width),
           greatest (width,height),
           greatest (height,length) )

хотя это трудно объяснить.

Математическая перспектива, как обычно, многое проясняет. Рассмотрим кубическое уравнение

Если мы знаем корни x1, x2, x3, тогда кубический полином может быть учтен, так что мы имеем

Объединяя оба уравнения, мы выражаем коэффициенты a, b, c через корни x1, x2, x3

Рисунок 4.1: Форма графа многочлена y=(x-x1)(x-x2)(x-x3) полностью определяется корнями x1, x2 и x3. Обмен ими ни на что не влияет.

Функции -x1-x2-x3, x1x2+x2x3+x3x1, -x1x2x3 симметричны. Перестановка x1, x2, x3 не влияет на значения a, b, c. Другими словами, порядок среди корней кубического уравнения не имеет значения: формально мы говорим о множестве корней, а не о списке корней1. Это именно тот эффект, который мы хотим в нашем примере с Boxes. Симметричные функции, переписанные по длине, ширине, высоте:

length+width+height
length*width+width*height+height*length
length*width*height

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

Наше последнее решение поразительно похоже на предыдущее, где наибольший оператор играет роль умножения, а наименьший оператор выступает в качестве сложения. Можно даже предложить решение, которое представляет собой сочетание двух

least(length,width,height)
least(length+width,width+height,height+length)
length+width+height

Читатель может проверить, что эти три функции снова симметричны2. Последний шаг - запись нашего решения в формальном SQL

.
table Boxes (
   length integer,
   width  integer,
   height integer
);

create unique index b_idx on Boxes(
   length + width + height,
   length * width + width * height + height * length,
   length * width * height
);

Симметричные функции обеспечивают основу для изящного решения. Однако на практике проблему часто можно решить путем перепроектирования схемы. В примере с базой данных инвентаризации ящиков даже не требуется редизайн схемы: нам просто нужно изменить практику вставки неограниченных записей (length,width,height) и потребовать

length ≥ width ≥ height
2 голосов
/ 14 марта 2012

Один из способов избежать триггера - использовать вычисляемый UNIQUE столбец для Member1 и Member2:

create table test (Member1 int not null, Member2 int not null, Status char(1)
, bc as abs(binary_checksum(Member1))+abs(binary_checksum(Member2)) PERSISTED UNIQUE)

INSERT INTO test values(123, 456, 'A'); --succeeds
INSERT INTO test values(123, 789, 'B'); --succeeds
INSERT INTO test values(456, 123, 'D'); --fails with the following error:
--Msg 2627, Level 14, State 1, Line 1
--Violation of UNIQUE KEY constraint 'UQ__test__3213B1084A8F946C'. Cannot insert duplicate key in object 'dbo.test'
1 голос
/ 15 марта 2012

Небольшое отклонение от решения @ ypercube заключается в создании индексированного представления и перемещении уникального ограничения в представление.Вот полный сценарий, демонстрирующий подход:

/* the reference table (almost irrelevant for the tests,
   but added to make the environment closer to the one in the question) */
CREATE TABLE dbo.Members (
  ID int IDENTITY CONSTRAINT PK_Members PRIMARY KEY,
  Name varchar(50)
);

GO

/* the table to add the constraint on */
CREATE TABLE dbo.Data (
  Member1 int CONSTRAINT FK_Data_Member1 FOREIGN KEY REFERENCES dbo.Members (ID),
  Member2 int CONSTRAINT FK_Data_Member2 FOREIGN KEY REFERENCES dbo.Members (ID),
  Statust char(1),
  CONSTRAINT PK_Data PRIMARY KEY (Member1, Member2)
);

GO

/* the indexed view that the constraint will actually be applied to */
CREATE VIEW dbo.DataView
WITH SCHEMABINDING  /* required with indexed views */
AS
SELECT
  /* the column definitions are practically identical to ypercube's */
  Member1 = CASE WHEN Member1 > Member2 THEN Member2 ELSE Member1 END,
  Member2 = CASE WHEN Member1 > Member2 THEN Member1 ELSE Member2 END
FROM dbo.Data

GO

/* finally, the constraint itself */
CREATE UNIQUE CLUSTERED INDEX UQ_DataView ON dbo.DataView (Member1, Member2);

GO

/* preparing the stage: adding some data to the reference table */
INSERT INTO dbo.Members (Name)
SELECT 'Member A' UNION ALL
SELECT 'Member B' UNION ALL
SELECT 'Member C';

GO

/* the first two rows should and do insert into the target table without issues */
INSERT INTO dbo.Data (Member1, Member2, Statust) VALUES (3, 1, 'A');
INSERT INTO dbo.Data (Member1, Member2, Statust) VALUES (2, 3, 'A');

GO

/* and this one fails, which demonstrates the constraint in work */
INSERT INTO dbo.Data (Member1, Member2, Statust) VALUES (1, 3, 'B');

GO

/* cleaning up */
DROP VIEW dbo.DataView;
DROP TABLE dbo.Data;
DROP TABLE dbo.Members;

Подробнее об индексированных представлениях в MSDN:

1 голос
/ 14 марта 2012

Не могу придумать лучшего способа дополнить существующее уникальное ограничение, кроме триггера. например,

CREATE TRIGGER dbo.StatusTable_PreventDualUniques
ON dbo.status_table
INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (
      SELECT 1 FROM inserted AS i 
        INNER JOIN dbo.status_table AS s
        ON i.Member1 = s.Member1 AND i.Member2 = s.Member2
        OR i.Member2 = s.Member1 AND i.Member1 = s.Member2
    )
    BEGIN
        RAISERROR('Duplicate detected', 11, 1);
    END
    ELSE
    BEGIN
        INSERT dbo.status_table(Member1, Member2, Status)
            SELECT Member1, Member2, Status
            FROM inserted;
    END
END

Теперь, вне головы, это касается только однорядных вставок. Логика может стать немного более сложной, если вам нужно обрабатывать многострочные вставки, поскольку вам нужно проверять наличие дубликатов как в inserted, так и между inserted и базовой таблицей. Это также не обрабатывает высокий параллелизм на уровне изоляции по умолчанию (например, другая транзакция вставляет дублирующую строку между проверкой и вставкой). Но это должно быть началом.

(Вам также понадобится один для UPDATE ...)

0 голосов
/ 14 марта 2012

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

...