-1

Let's say I have two SQL Server tables. One contains "transient" data - records are inserted, updated, and deleted as the external data that populates the table changes.

I have another table that uses data in that table, and I need to make sure that one of the column values is found in that table when attempting to insert.

Table #1 - Widgets:

CREATE TABLE [dbo].[Widgets]
(
    [id] [int] NOT NULL,
    [widget_attr_1] [int] NULL,
    [widget_attr_2] [varchar](10) NOT NULL,

    CONSTRAINT [PK_Table_1] 
        PRIMARY KEY CLUSTERED ([id] ASC)
                    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
                          IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
                          ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

Table #2 - Transactions:

CREATE TABLE [dbo].[Transactions]
(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [widget_id] [int] NOT NULL,
    [transaction_data_1] [varchar](50) NOT NULL,

    PRIMARY KEY CLUSTERED ([id] ASC)
                WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
                      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
                      ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

So in the above example, I need to make sure widget_id is in dbo.Widgets or else throw an error. I can't use a foreign key from Transactions to Widgets, because the Transactions records are permanent, and when a delete of a Widget is attempted, it would fail, because it's referenced by the foreign key.

Can I use a CHECK constraint looks up the value in the Widgets table before inserting? Or maybe a trigger that looks up the value and throws an error if it doesn't exist? I can't figure out how either would work and what the possible performance effects would be. Looking for best practices here.

Thanks in advance!

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Eric Belair
  • 10,574
  • 13
  • 75
  • 116
  • 1
    A foreign key **is** best practice. Once you decided against that, you move outside of "best practice". You could use a check constraint involving a UDF for your "lookup" as well as a trigger - but which is better involves a more intimate knowledge of how rows come and go in your two tables and why checking only at insert time (but not afterwards) is a useful path. – SMor May 18 '21 at 15:55
  • 1
    What is the value of leaving `widget_id` in there when the corresponding Widget is deleted? Would a foreign key and cascade to set it to null on delete work for you? – Martin Smith May 18 '21 at 15:57
  • @SMor A foreign WOULD be best pratice, if it would work in this scenario, but, it will NOT work in this scenario. I wouldn't be able to delete rows from Widgets. – Eric Belair May 18 '21 at 16:01
  • @MartinSmith the Transactions data is permanent data that needs to persist after the Widget record is deleted. When the Widget record is deleted, it is later added to another table that has permanent data. The Transactions data is historical information that need to persist. That's the value of leaving it in there. – Eric Belair May 18 '21 at 16:03
  • So why not have the `transactions` table reference this permanent table instead with its foreign key? – Thom A May 18 '21 at 16:04
  • @EricBelair: If the purpose of Transactions is to maintain historical information then you should do one of two things. Either also put the widget attributes into the transaction table OR add an IsDeleted column to the widgets table that gets set when you want to delete the widget. Otherwise the transaction table becomes meaningless when a widget is deleted. – NotMe May 18 '21 at 16:05
  • If you knew the FK was best practice, then why ask? You have sufficient rep (and obvious experience) to implement or research such things. Want to know if there are performance implications? Then try them and evaluate the impact. Nothing is free! – SMor May 18 '21 at 16:08
  • In my opinion, the transaction becomes useless as soon as the row in the widget table is deleted already. You no longer have any idea what the transaction is for; thus it's meaningless. – Thom A May 18 '21 at 16:09
  • @Larnu, "So why not have the transactions table reference this permanent table instead with its foreign key?" because the data in the permanent table does not exist until some time AFTER the data is inserted into the Transactions table. – Eric Belair May 18 '21 at 16:23
  • @SMor I ask because a Foreign Key will NOT work in this scenario. when a row delete on Widgets is attempted, it would fail if a corresponding record was found in Transactions, which is permanent data. – Eric Belair May 18 '21 at 16:24
  • 1
    Then, perhaps, you should be populating your permanent `widgets` table first, @EricBelair , and then inserting data into your `transactions`. Then a `FOREIGN KEY` *will* work. The problem here appears to be the design, and thus you're needing to reinvent the wheel. – Thom A May 18 '21 at 16:25
  • Why not a *nullable* foreign key, then you can insert the transaction with a `null` for the `widget_id`, and update it afterwards, and use `on delete set null` to reset it back to null? – Charlieface May 18 '21 at 22:49

1 Answers1

3

A FOREIGN KEY is a type of CHECK CONSTRAINT. I would suggest you add an EXISTS clause to your INSERT statement, so that rows you don't want inserted aren't. In Pseudo SQL that would be:

INSERT INTO dbo.Transactions (widget_id,transaction_data_1)
SELECT widget_id,
       transaction_data_1
FROM (VALUES(...))V((widget_id,transaction_data_1)
WHERE EXISTS (SELECT 1
              FROM dbo.widgets w
              WHERE w.widget_id = v.widget_id);

If you need to have an error thrown, you could use a TRIGGER like the below, just note that if even 1 of the values doesn't exist in the table widgets then the entire INSERT will fail:

CREATE TRIGGER dbo.trg_fk_TranasctionWidget
ON dbo.Transactions
AFTER INSERT
AS
IF EXISTS (SELECT 1
           FROM inserted i
               LEFT JOIN dbo.widgets w ON i.widget_id = w.widget_id
           WHERE w.widget_id IS NULL)
    --Change the error number to one suitable for your environment
    THROW 73246, N'The INSERT statement conflict with the trigger constraint trg_fk_TranasctionWidget. The conflict occured on the table "dbo.Transactions", column "widget_id".', 16;
GO

Though, like @MartinSmith has commented it seems like really you should be looking at cascading.

Thom A
  • 88,727
  • 11
  • 45
  • 75
  • Excellent idea, but, I don't have access to change the insert statement. The insert occurs from an external app. I can only control things on my end. – Eric Belair May 18 '21 at 16:00
  • 1
    There are 2 solutions in this answer, @EricBelair . A `TRIGGER` is in there too. – Thom A May 18 '21 at 16:01
  • are there any risks/performance effects that I need to worry about with the trigger? – Eric Belair May 18 '21 at 16:04
  • There could well be a performance impact, yes @EricBelair; especially if `widget_id` in the table `dbo.widget` isn't indexed or the number of rows your inserting is very large . However,if you're worried about that use a `FOREIGN KEY`; as we've all told you it's exactly what you *really* need. – Thom A May 18 '21 at 16:06
  • a foreign key will not work, as i stated in my post. when row delete on Widgets is attempted, it would fail if a corresponding record was found in Transactions, which is permanent data. – Eric Belair May 18 '21 at 16:22
  • Then you need not worry about performance, @EricBelair . If you *are* worried about performance then you need to rethink the design and processes you have in its entirety so that you can implement a `FOREIGN KEY`. If you really don't want to implement one, then you are by definition accepting the performance costs that reinventing the wheel comes with. – Thom A May 18 '21 at 16:30
  • Hey @Larnu, I think I'm going to go with the Trigger option. Not the most elegant, but, it works. Thanks. Answer accepted. – Eric Belair May 18 '21 at 17:23
  • Yeah, this is likely the closest you are going to get without using a proper `CONSTRAINT`, @EricBelair . You *could* use an `INSTEAD OF INSERT` if you wanted. That would allow you to only "push" the valid rows to the table and not `THROW`, but invalid rows would be silently "lost" and if you are using `OUTPUT` clauses against the table you might get some unexpected behaviour. – Thom A May 18 '21 at 17:26