Уникальное ограничение SQL для нескольких таблиц

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

Например, у меня есть три таблицы: t_Analog, t_Discrete, t_Message.

CREATE TABLE t_Analog(
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    [Value] [float] NOT NULL,
    CONSTRAINT [uc_t_Analog] UNIQUE(AppName, ItemName)
)

CREATE TABLE t_Discrete(
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    [Value] [bit] NOT NULL,
    CONSTRAINT [uc_t_Discrete] UNIQUE(AppName, ItemName)
)

CREATE TABLE t_Message(
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    [Value] [nvarchar](256) NOT NULL,
    CONSTRAINT [uc_t_Message] UNIQUE(AppName, ItemName)
)

Моя цель — сделать AppName и ItemName уникальными для всех трех таблиц. Например, имя элемента Y в приложении X не может существовать как в аналоговых, так и в дискретных таблицах.

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

Если у вас есть какие-либо предложения по подходам к этому, я хотел бы услышать их!

---- НАЧАТЬ РЕДАКТИРОВАНИЕ 26 апреля 2012 г., 13:28 CST ----

Спасибо всем за ваши ответы!

Кажется, может быть причина изменить схему этой базы данных, и это нормально.

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

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

Изменение схемы, чтобы быть:

CREATE TABLE t_AllItems(
    [id] [bigint] IDENTITY(1,1) NOT NULL,
    [itemType] [int] NOT NULL,
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    CONSTRAINT [pk_t_AllItems] PRIMARY KEY CLUSTERED ( [id] )
    CONSTRAINT [uc_t_AllItems] UNIQUE([id], [AppName], [ItemName])
) ON [PRIMARY]

CREATE TABLE t_Analog(
    [itemId] [bigint] NOT NULL,
    [Value] [float] NOT NULL,
    FOREIGN KEY (itemId) REFERENCES t_AllItems(id)
)

CREATE TABLE t_Discrete(
    [itemId] [bigint] NOT NULL,
    [Value] [bit] NOT NULL,
    FOREIGN KEY (itemId) REFERENCES t_AllItems(id)
)

CREATE TABLE t_Message(
    [itemId] [bigint] NOT NULL,
    [Value] [nvarchar](256) NOT NULL,
    FOREIGN KEY (itemId) REFERENCES t_AllItems(id)
)

У меня только один вопрос по этому подходу. Обеспечивает ли это уникальность для подтаблиц?

Например, не может ли существовать «Элемент» с «id» 9 с таблицами t_Analog, имеющими «itemId» 9 со «значением» 9,3, и в то же время t_Message имеет «itemId» 9 со «Значением» "фу"?

Возможно, я не совсем понимаю этот подход с дополнительными таблицами, но я не против этого.

Пожалуйста, поправьте меня, если я ошибаюсь в этом.


person CoryC    schedule 26.04.2012    source источник
comment
Вы на правильном пути, но ограничения целостности недостаточно хороши. Например, идентификационный номер 100 может появиться в каждой таблице. Для более точного использования типа элемента см. этот ответ и этот ответ.   -  person Mike Sherrill 'Cat Recall'    schedule 27.04.2012
comment
Это не ограничение, но вы можете использовать CREATE SEQUENCE, чтобы получить уникальный номер для каждой записи: docs.microsoft.com/en-us/sql/t-sql/statements/ я думаю эта функция была добавлена ​​в версии 2012.   -  person John Gilmer    schedule 16.12.2019


Ответы (6)


Добавьте четвертую таблицу специально для тех значений, которые вы хотите сделать уникальными, а затем свяжите эти ключи из этой таблицы с другими, используя отношение «один ко многим». Например, у вас будет уникальная таблица с идентификатором, AppName и ItemName, чтобы составить ее 3 столбца. Затем свяжите эту таблицу с другими.

Как это сделать, вот хороший пример Создайте один для многие отношения с использованием SQL Server

EDIT: Я бы сделал так, но, учитывая потребности вашего сервера, вы можете изменить то, что необходимо:

CREATE TABLE AllItems(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [itemType] [int] NOT NULL,
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    CONSTRAINT [pk_AllItems] PRIMARY KEY CLUSTERED ( [id] ASC )
) ON [PRIMARY]

CREATE TABLE Analog(
    [itemId] [int] NOT NULL,
    [Value] [float] NOT NULL
)

CREATE TABLE Discrete(
    [itemId] [int] NOT NULL,
    [Value] [bit] NOT NULL
)

CREATE TABLE Message(
    [itemId] [bigint] NOT NULL,
    [Value] [nvarchar](256) NOT NULL
)

ALTER TABLE [Analog] WITH CHECK 
    ADD CONSTRAINT [FK_Analog_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Analog] CHECK CONSTRAINT [FK_Analog_AllItems]
GO

ALTER TABLE [Discrete] WITH CHECK 
    ADD CONSTRAINT [FK_Discrete_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Discrete] CHECK CONSTRAINT [FK_Discrete_AllItems]
GO

ALTER TABLE [Message] WITH CHECK 
    ADD CONSTRAINT [FK_Message_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Message] CHECK CONSTRAINT [FK_Message_AllItems]
GO

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

person John Sykor    schedule 26.04.2012
comment
Джон, я отредактировал свой пост, добавив схему, о которой, как мне кажется, вы говорите. Не могли бы вы проверить, чтобы убедиться, что я понимаю ваше предложение? Спасибо :) - person CoryC; 26.04.2012
comment
если вы посмотрите на мое редактирование, вы увидите, к чему я привык, но ваш синтаксис мне нравится - person John Sykor; 26.04.2012
comment
Вы на правильном пути, но этого недостаточно. Проблема в том, что нет никаких ограничений для предотвращения появления идентификатора 100 в каждой из этих таблиц. Для более точного использования типа элемента см. этот ответ и этот ответ. - person Mike Sherrill 'Cat Recall'; 27.04.2012
comment
Я бы предпочел просто компенсировать запросы вместо создания таких подробных таблиц. Например. Как только идентификатор используется в нашей основной таблице, он больше не может использоваться двумя другими нашими таблицами. По сути, я бы добавил флаг в первую таблицу, говорящий, что она используется, и если этот флаг установлен, он больше не доступен. Посмотрите также ответ Catcall, я просто не знаком с ним. - person John Sykor; 27.04.2012
comment
Кэтколл, твои ответы были именно тем, что мне было нужно. Спасибо Джон и Кэт за вашу помощь! - person CoryC; 02.05.2012

Хотя вы можете или не хотите изменять свою схему, как говорят другие ответы, индексированное представление может применить ограничение, о котором вы говорите:

CREATE VIEW v_Analog_Discrete_Message_UK WITH SCHEMABINDING AS
SELECT a.AppName, a.ItemName
FROM dbo.t_Analog a, dbo.t_Discrete b, dbo.t_Message c, dbo.Tally t
WHERE (a.AppName = b.AppName and a.ItemName = b.ItemName)
    OR (a.AppName = c.AppName and a.ItemName = c.ItemName)
    OR (b.AppName = c.AppName and b.ItemName = c.ItemName)
    AND t.N <= 2
GO
CREATE UNIQUE CLUSTERED INDEX IX_AppName_ItemName_UK
    ON v_Analog_Discrete_Message_UK (AppName, ItemName)
GO

Вам понадобится "Tally" или таблица чисел, либо вы должны создать ее иным образом. на лету, в стиле Celko:

-- Celko-style derived numbers table to 100k
select a.N + b.N * 10 + c.N * 100 + d.N * 1000 + e.N * 10000 + 1 as N
from (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) a
      , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) b
      , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) c
      , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) d
      , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) e
order by N
person Tim Lehner    schedule 26.04.2012
comment
Спасибо, я считаю это самым элегантным решением - person spinalfrontier; 15.06.2015
comment
Согласен - это самое элегантное решение. Однако вам не нужна полная итоговая таблица со 100 000 строк, достаточно двух строк. У вас есть WHERE ... tN ‹ = 2 в любом случае. Для этой цели у меня есть таблица с именем chk.TwoRows, содержащая только значения 1 и 2. - person Reversed Engineer; 11.04.2017
comment
К счастью, в этом случае MS SQL рассматривает NULL как значение с точки зрения уникальных ограничений (что, конечно, неверно), поэтому он работает даже для значений, дублирующихся между таблицами b и c, и в этом случае a.AppName, a.ItemName оба НУЛЕВЫЕ. Две ошибки делают право здесь - person Reversed Engineer; 11.04.2017
comment
P L U S _ O N E в любом случае - это действительно помогло - person Reversed Engineer; 11.04.2017
comment
Улучшенная генерация таблицы Tally: -- Таблица производных чисел в стиле Celko до 100 тыс. 1),(2),(3),(4),(5),(6),(7),(8),(9)) AS t(N)) AS a , (SELECT * FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) AS t(N)) AS b , ( ВЫБЕРИТЕ * ИЗ (ЗНАЧЕНИЯ (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)) AS t (N) ) AS c , (ВЫБРАТЬ * ИЗ (ЗНАЧЕНИЙ (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)) КАК t(N)) КАК d , (ВЫБРАТЬ * ИЗ (ЗНАЧЕНИЙ (0), (1), (2), (3), (4), (5), (6), (7), (8) ,(9)) AS t(N)) AS e ПОРЯДОК ПО N - person Dima; 30.04.2017
comment
это здорово и очень гибко - person Dave Cousineau; 31.05.2018

Одна мысль может состоять в том, чтобы объединить три таблицы:

CREATE TABLE t_Generic(
[AppName] [nvarchar](20) NOT NULL,
[ItemName] [nvarchar](32) NOT NULL,
[Type] [nvarchar](32) NOT NULL,
[AnalogValue] [Float] NULL,
[DiscreteValue] [bit] NULL,
[MessageValue] [nvarchar](256) NULL,
CONSTRAINT [uc_t_Generic] UNIQUE(AppName, ItemName)
)

Логика вашего приложения должна обеспечивать заполнение только одного значения, и вы можете использовать поле Type, чтобы отслеживать, к какому типу относится эта запись.

person Victor Bruno    schedule 26.04.2012

Вы также можете создать ограничение, которое имеет немного больше логики и проверяет все три таблицы.

Посмотрите здесь пример того, как это сделать с помощью функции.

person zimdanen    schedule 26.04.2012
comment
Зимданен, не могли бы вы привести пример того, как это ограничение будет проверяться на нескольких таблицах? - person CoryC; 26.04.2012
comment
Вот один из вариантов с функцией: stackoverflow.com/questions/2588072/ - person zimdanen; 26.04.2012
comment
zimdanen, это выглядит очень многообещающе, я могу использовать этот подход. - person CoryC; 26.04.2012
comment
Хотя эта ссылка может ответить на вопрос, лучше включить сюда основные части ответа и предоставить ссылку для справки. Ответы, содержащие только ссылки, могут стать недействительными, если связанная страница изменится. – Из обзора - person ice1000; 01.06.2018
comment
@ice1000: Ice1000: Это относится к ссылкам на SO? - person zimdanen; 06.06.2018

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

EG:

AppName – PrimaryKey - unique
ID – Foreign Key of either Discrete, Analog or message
Type – SMALLINT representing Discrete, analog or message.
person HeavenCore    schedule 26.04.2012

Вместо триггеров вставки и обновления я использовал для решения этой проблемы следующее:

CREATE TRIGGER tI_Analog ON t_Analog
INSTEAD OF INSERT
AS 
BEGIN
    SET NOCOUNT ON ;

    IF EXISTS (SELECT 1 FROM inserted AS I INNER JOIN t_Analog AS T
                   ON T.AppName = I.AppName AND T.ItemName = I.ItemName
               UNION ALL
               SELECT 1 FROM inserted AS I INNER JOIN t_Discrete AS T
                   ON T.AppName = I.AppName AND T.ItemName = I.ItemName
               UNION ALL
               SELECT 1 FROM inserted AS I INNER JOIN t_Message AS T
                   ON T.AppName = I.AppName AND T.ItemName = I.ItemName
              )
    BEGIN
        RAISERROR('Duplicate key', 16, 10) ;
    END
    ELSE
    BEGIN
        INSERT INTO t_Analog ( AppName, ItemName, Value )
        SELECT AppName, ItemName, Value FROM inserted ;
    END
END
GO

CREATE TRIGGER tU_Analog ON t_Analog
INSTEAD OF UPDATE
AS 
BEGIN
    SET NOCOUNT ON ;

    IF EXISTS (SELECT TOP(1) 1
                 FROM (SELECT T.AppName, T.ItemName, COUNT(*) AS numRecs
                         FROM
                            (SELECT I.AppName, I.ItemName
                               FROM inserted AS I INNER JOIN t_Analog AS T
                                 ON T.AppName = I.AppName AND T.ItemName = I.ItemName
                             UNION ALL
                             SELECT I.AppName, I.ItemName
                               FROM inserted AS I INNER JOIN t_Discrete AS T
                                 ON T.AppName = I.AppName AND T.ItemName = I.ItemName
                             UNION ALL
                             SELECT I.AppName, I.ItemName
                               FROM inserted AS I INNER JOIN t_Message AS T
                                 ON T.AppName = I.AppName AND T.ItemName = I.ItemName
                            ) AS T
                          GROUP BY T.AppName, T.ItemName
                        ) AS T
                WHERE T.numRecs > 1
              )
    BEGIN
        RAISERROR('Duplicate key', 16, 10) ;
    END
    ELSE
    BEGIN
        UPDATE T
           SET AppName = I.AppName
             , ItemName = I.ItemName
             , Value = I.Value
          FROM inserted AS I INNER JOIN t_Message AS T
            ON T.AppName = I.AppName AND T.ItemName = I.ItemName
        ;
    END
END
GO

Одно предупреждение с использованием вместо триггеров — когда задействовано поле идентификации. Этот триггер предотвращает правильную работу предложения OUTPUT команды INSERT INTO и переменной @@IDENTITY.

person Michael Erickson    schedule 29.06.2012