How to better duplicate a set of data in SQL Server - sql

I have several related tables that I want to be able to duplicate some of the rows while updating the references.
I want to duplicate a row in Table1, and all of it's related rows from Table2 and Table3, and I'm trying to figure out an efficient way of doing it short of iterating through rows.
So for example, I have a table of baskets:
+----------+---------------+
| BasketId | BasketName |
+----------+---------------+
| 1 | Home Basket |
| 2 | Office Basket |
+----------+---------------+
Each basket has fruit:
+---------+----------+-----------+
| FruitId | BasketId | FruitName |
+---------+----------+-----------+
| 1 | 1 | Apple |
| 2 | 1 | Orange |
| 3 | 2 | Mango |
| 4 | 2 | Pear |
+---------+----------+-----------+
And each fruit has some properties:
+------------+---------+--------------+
| PropertyId | FruitId | PropertyText |
+------------+---------+--------------+
| 1 | 2 | Is juicy |
| 2 | 2 | Hard to peel |
| 3 | 1 | Is red |
+------------+---------+--------------+
For this example, my properties are specific to the individual fruit row, these "apple" properties aren't properties of all apples in all baskets, just for that specific apple in that specific basket.
What I want to do is duplicate a basket. So given basket 1, I want to create a new basket, duplicate the fruit rows it contains, and duplicate the properties pointing to those fruits. In the end I'm hoping to have data like so:
+----------+---------------+
| BasketId | BasketName |
+----------+---------------+
| 1 | Home Basket |
| 2 | Office Basket |
| 3 | Friends Basket|
+----------+---------------+
+---------+----------+-----------+
| FruitId | BasketId | FruitName |
+---------+----------+-----------+
| 1 | 1 | Apple |
| 2 | 1 | Orange |
| 3 | 2 | Mango |
| 4 | 2 | Pear |
| 5 | 3 | Apple |
| 6 | 3 | Orange |
+---------+----------+-----------+
+------------+---------+--------------+
| PropertyId | FruitId | PropertyText |
+------------+---------+--------------+
| 1 | 2 | Is juicy |
| 2 | 2 | Hard to peel |
| 3 | 1 | Is red |
| 4 | 6 | Is juicy |
| 5 | 6 | Hard to peel |
| 6 | 5 | Is red |
+------------+---------+--------------+
Duplicating the basket and it's fruit were pretty straightforward, but duplicating the properties of the fruit seems to me to lead to iterating over rows and I'm hoping there's a better solution in TSQL.
Any ideas?

Why dont you join on the FruitName to get a table with old and new FruitId's? Considering information would be added at the same time.... it may not be the best option but you wont be using any cycles.
INSERT INTO BASKET(BASKETNAME)
VALUES ('COPY BASKET')
DECLARE #iBasketId int
SET #iBasketId = ##SCOPE_IDENTITY;
insert into Fruit (BasketId, FruitName)
select #iBasketId, FruitName
from Fruit
where BasketId = #originalBasket
declare #tabFruit table (originalFruitId int, newFruitId int)
insert into #tabFruit (originalFruitId, newFruitId)
select o.FruitId, n.FruitId
from (SELECT FruitId, FruitName from Fruit where BasketId = #originalBasket) as o
join (SELECT FruitId, FruitName from Fruit where BasketId = #newBasket) as n
on o.FruitName = n.FruitName
insert into Property (FruitId, PropertyText)
select NewFruitId, PropertyText
from Fruit f join #tabFruit t on t.originalFruitId = f.FruitId

(ab)use MERGE with OUTPUT clause.
MERGE can INSERT, UPDATE and DELETE rows. In our case we need only to INSERT. 1=0 is always false, so the NOT MATCHED BY TARGET part is always executed. In general, there could be other branches, see docs. WHEN MATCHED is usually used to UPDATE; WHEN NOT MATCHED BY SOURCE is usually used to DELETE, but we don't need them here.
This convoluted form of MERGE is equivalent to simple INSERT, but unlike simple INSERT its OUTPUT clause allows to refer to the columns that we need.
I will write down the definitions of table explicitly. Each primary key in the tables is IDENTITY. I've configured foreign keys as well.
Baskets
CREATE TABLE [dbo].[Baskets](
[BasketId] [int] IDENTITY(1,1) NOT NULL,
[BasketName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Baskets] PRIMARY KEY CLUSTERED
(
[BasketId] ASC
)
Fruits
CREATE TABLE [dbo].[Fruits](
[FruitId] [int] IDENTITY(1,1) NOT NULL,
[BasketId] [int] NOT NULL,
[FruitName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Fruits] PRIMARY KEY CLUSTERED
(
[FruitId] ASC
)
ALTER TABLE [dbo].[Fruits] WITH CHECK
ADD CONSTRAINT [FK_Fruits_Baskets] FOREIGN KEY([BasketId])
REFERENCES [dbo].[Baskets] ([BasketId])
ALTER TABLE [dbo].[Fruits] CHECK CONSTRAINT [FK_Fruits_Baskets]
Properties
CREATE TABLE [dbo].[Properties](
[PropertyId] [int] IDENTITY(1,1) NOT NULL,
[FruitId] [int] NOT NULL,
[PropertyText] [varchar](50) NOT NULL,
CONSTRAINT [PK_Properties] PRIMARY KEY CLUSTERED
(
[PropertyId] ASC
)
ALTER TABLE [dbo].[Properties] WITH CHECK
ADD CONSTRAINT [FK_Properties_Fruits] FOREIGN KEY([FruitId])
REFERENCES [dbo].[Fruits] ([FruitId])
ALTER TABLE [dbo].[Properties] CHECK CONSTRAINT [FK_Properties_Fruits]
Copy Basket
At first copy one row in Baskets table and use SCOPE_IDENTITY to get the generated ID.
BEGIN TRANSACTION;
-- Parameter of the procedure. What basket to copy.
DECLARE #VarOldBasketID int = 1;
-- Copy Basket, one row
DECLARE #VarNewBasketID int;
INSERT INTO [dbo].[Baskets] (BasketName)
VALUES ('Friends Basket');
SET #VarNewBasketID = SCOPE_IDENTITY();
Copy Fruits
Then copy Fruits using MERGE and remember a mapping between old and new IDs in a table variable.
-- Copy Fruits, multiple rows
DECLARE #FruitIDs TABLE (OldFruitID int, NewFruitID int);
MERGE INTO [dbo].[Fruits]
USING
(
SELECT
[FruitId]
,[BasketId]
,[FruitName]
FROM [dbo].[Fruits]
WHERE [BasketId] = #VarOldBasketID
) AS Src
ON 1 = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT
([BasketId]
,[FruitName])
VALUES
(#VarNewBasketID
,Src.[FruitName])
OUTPUT Src.[FruitId] AS OldFruitID, inserted.[FruitId] AS NewFruitID
INTO #FruitIDs(OldFruitID, NewFruitID)
;
Copy Properties
Then copy Properties using remembered mapping between old and new Fruit IDs.
-- Copy Properties, many rows
INSERT INTO [dbo].[Properties] ([FruitId], [PropertyText])
SELECT
F.NewFruitID
,[dbo].[Properties].PropertyText
FROM
[dbo].[Properties]
INNER JOIN #FruitIDs AS F ON F.OldFruitID = [dbo].[Properties].FruitId
;
Check results, change rollback to commit once you confirmed that the code works correctly.
SELECT * FROM [dbo].[Baskets];
SELECT * FROM [dbo].[Fruits];
SELECT * FROM [dbo].[Properties];
ROLLBACK TRANSACTION;

I had the same need as the OP: cloning sql server tables where they are hierarchical sql server tables that contain foreign keys to one another. Or in other words, cloning sql server tables that have parent-child relationships.
Starting with #Tony_O 's answer/SQL, I converted it to my needs but discovered that the last line '..from Fruit f join..' should be '..from Property f join..'. Also, #newBasket should be #iBasketId.
So along with some other minor housekeeping fixes I found were needed for it to execute, plus using #Vladimir_Baranov 's DDL (with some missing parenthesis added), as well as making both of their SQL's object names consistent, since I had done the work I thought I would post it as a refinement of their work that will allow someone to to quickly test whether this solution solves their need. Just do 'find/replace' of the table names here with values from yours.
And note that if your Properties table has more fields than the single 'PropertyText' one in this example, just make sure to join on those additional fields as noted in the comment in the script.
-----------------
--create tables--
-----------------
--Baskets
CREATE TABLE [dbo].[Baskets](
[BasketId] [int] IDENTITY(1,1) NOT NULL,
[BasketName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Baskets] PRIMARY KEY CLUSTERED
(
[BasketId] ASC
)
)
--Fruits
CREATE TABLE [dbo].[Fruits](
[FruitId] [int] IDENTITY(1,1) NOT NULL,
[BasketId] [int] NOT NULL,
[FruitName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Fruits] PRIMARY KEY CLUSTERED
(
[FruitId] ASC
)
)
ALTER TABLE [dbo].[Fruits] WITH CHECK
ADD CONSTRAINT [FK_Fruits_Baskets] FOREIGN KEY([BasketId])
REFERENCES [dbo].[Baskets] ([BasketId])
ALTER TABLE [dbo].[Fruits] CHECK CONSTRAINT [FK_Fruits_Baskets]
--Properties
CREATE TABLE [dbo].[Properties](
[PropertyId] [int] IDENTITY(1,1) NOT NULL,
[FruitId] [int] NOT NULL,
[PropertyText] [varchar](50) NOT NULL,
CONSTRAINT [PK_Properties] PRIMARY KEY CLUSTERED
(
[PropertyId] ASC
)
)
ALTER TABLE [dbo].[Properties] WITH CHECK
ADD CONSTRAINT [FK_Properties_Fruits] FOREIGN KEY([FruitId])
REFERENCES [dbo].[Fruits] ([FruitId])
ALTER TABLE [dbo].[Properties] CHECK CONSTRAINT [FK_Properties_Fruits]
-------------------------
--Fill tables with data--
-------------------------
SET IDENTITY_INSERT [dbo].[Baskets] ON
GO
INSERT [dbo].[Baskets] ([BasketId], [BasketName]) VALUES (1, N'Home Basket')
GO
SET IDENTITY_INSERT [dbo].[Baskets] OFF
GO
SET IDENTITY_INSERT [dbo].[Fruits] ON
GO
INSERT [dbo].[Fruits] ([FruitId], [BasketId], [FruitName]) VALUES (1, 1, N'Apple')
GO
INSERT [dbo].[Fruits] ([FruitId], [BasketId], [FruitName]) VALUES (2, 1, N'Orange')
GO
SET IDENTITY_INSERT [dbo].[Fruits] OFF
GO
SET IDENTITY_INSERT [dbo].[Properties] ON
GO
INSERT [dbo].[Properties] ([PropertyId], [FruitId], [PropertyText]) VALUES (1, 2, N'is juicy')
GO
INSERT [dbo].[Properties] ([PropertyId], [FruitId], [PropertyText]) VALUES (2, 2, N'hard to peel')
GO
INSERT [dbo].[Properties] ([PropertyId], [FruitId], [PropertyText]) VALUES (3, 1, N'is red')
GO
SET IDENTITY_INSERT [dbo].[Properties] OFF
GO
--------------------------------------------------------------
-- Copy 'Home Basket' to new basket named 'COPY BASKET' --
-- i.e., Copy Basket (and all fruits and their fruit properties) having basket id
-- #origBasketId to a new basket with name 'COPY BASKET'.
--------------------------------------------------------------
DECLARE #originalBasket int
select #originalBasket = 1
begin tran
INSERT INTO BASKETS(BASKETNAME)
VALUES ('COPY BASKET')
DECLARE #newBasketId int
SET #newBasketId = SCOPE_IDENTITY();
insert into Fruits (BasketId, FruitName)
select #newBasketId, FruitName
from Fruits
where BasketId = #originalBasket
declare #tabFruit table (originalFruitId int, newFruitId int)
insert into #tabFruit (originalFruitId, newFruitId)
select o.FruitId, n.FruitId
from (SELECT FruitId, FruitName from Fruits where BasketId = #originalBasket) as o
join (SELECT FruitId, FruitName from Fruits where BasketId = #newBasketId) as n
on o.FruitName = n.FruitName --if your table equivalent to Fruits has other fields, match on those as well here.
insert into Properties (FruitId, PropertyText)
select NewFruitId, PropertyText
from Properties p join #tabFruit t on t.originalFruitId = p.FruitId
commit tran
---------------
--See results--
---------------
select *
from dbo.Baskets b inner join dbo.Fruits f on b.BasketId=f.BasketId
inner join properties p on p.FruitId=f.FruitId
order by b.BasketId, f.FruitId, p.PropertyId

Related

Error "duplicate key value violates unique constraint" while updating multiple rows

I created a table in PostgreSQL and Oracle as
CREATE TABLE temp(
seqnr smallint NOT NULL,
defn_id int not null,
attr_id int not null,
input CHAR(50) NOT NULL,
CONSTRAINT pk_id PRIMARY KEY (defn_id, attr_id, seqnr)
);
This temp table has primary key as (defn_id,attr_id,seqnr) as a whole!
Then I inserted the record in the temp table as
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (1,100,100,'test1');
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (2,100,100,'test2');
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (3,100,100,'test3');
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (4,100,100,'test4');
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (5,100,100,'test5');
in both Oracle and Postgres!
The table now contains:
seqnr | defn_id | attr_id | input
1 | 100 | 100 | test1
2 | 100 | 100 | test2
3 | 100 | 100 | test3
4 | 100 | 100 | test4
5 | 100 | 100 | test5
When I run the command:
UPDATE temp SET seqnr=seqnr+1
WHERE defn_id = 100 AND attr_id = 100 AND seqnr >= 1;
In case of ORACLE it is Updating 5 Rows and the O/p is
seqnr | defn_id | attr_id | input
2 | 100 | 100 | test1
3 | 100 | 100 | test2
4 | 100 | 100 | test3
5 | 100 | 100 | test4
6 | 100 | 100 | test5
But in case of PostgreSQL it is giving an error!
DETAIL: Key (defn_id, attr_id, seqnr)=(100, 100, 2) already exists.
Why does this happen and how can I replicate the same result in Postgres as Oracle?
Or how can the same result be achieved in Postgres without any errors?
UNIQUE an PRIMARY KEY constraints are checked immediately (for each row) unless they are defined DEFERRABLE - which is the solution you demand.
ALTER TABLE temp
DROP CONSTRAINT pk_id
, ADD CONSTRAINT pk_id PRIMARY KEY (defn_id, attr_id, seqnr) DEFERRABLE
;
Then your UPDATE just works.
db<>fiddle here
This comes at a cost, though. The manual:
Note that deferrable constraints cannot be used as conflict
arbitrators in an INSERT statement that includes an ON CONFLICT DO UPDATE clause.
And for FOREIGN KEY constraints:
The referenced columns must be the columns of a non-deferrable unique
or primary key constraint in the referenced table.
And:
When a UNIQUE or PRIMARY KEY constraint is not deferrable,
PostgreSQL checks for uniqueness immediately whenever a row is
inserted or modified. The SQL standard says that uniqueness should be
enforced only at the end of the statement; this makes a difference
when, for example, a single command updates multiple key values. To
obtain standard-compliant behavior, declare the constraint as
DEFERRABLE but not deferred (i.e., INITIALLY IMMEDIATE). Be aware
that this can be significantly slower than immediate uniqueness
checking.
See:
Constraint defined DEFERRABLE INITIALLY IMMEDIATE is still DEFERRED?
I would avoid a DEFERRABLE PK if at all possible. Maybe you can work around the demonstrated problem? This usually works:
UPDATE temp t
SET seqnr = t.seqnr + 1
FROM (
SELECT defn_id, attr_id, seqnr
FROM temp
WHERE defn_id = 100 AND attr_id = 100 AND seqnr >= 1
ORDER BY defn_id, attr_id, seqnr DESC
) o
WHERE (t.defn_id, t.attr_id, t.seqnr)
= (o.defn_id, o.attr_id, o.seqnr);
db<>fiddle here
But there are no guarantees as ORDER BY is not specified for UPDATE in Postgres.
Related:
UPDATE with ORDER BY

Audited table and foreign key

I have a database with multiples tables that must be audited.
As an example, I have a table of objects defined with an unique ID, a name and a description.
The name will always be the same. It is not possible to update it. "ObjectA" will always be "ObjectA".
As you see the name is not unique in the database but only in the logic.
The rows "from", "to" and "creator_id" are used to audit the changes. "from" is the date of the change, "to" is the date when a new row has been added and is null when it is the latest row. "creator_id" is the ID of the user that made the change.
+----+----------+--------------+----------------------+----------------------+------------+
| id | name | description | from | to | creator_id |
+----+----------+--------------+----------------------+----------------------+------------+
| 1 | ObjectA | My object | 2021-05-30T00:05:00Z | 2021-05-31T05:04:36Z | 18 |
| 2 | ObjectB | My desc | 2021-05-30T02:07:25Z | null | 15 |
| 3 | ObjectA | Super object | 2021-05-31T05:04:36Z | null | 20 |
+----+----------+--------------+----------------------+----------------------+------------+
Now I have another table that must have a foreign key to this object table based on the "unique" object name.
+----+---------+-------------+
| id | foo | object_name |
+----+---------+-------------+
| 1 | blabla | ObjectA |
| 2 | wawawa | ObjectB |
+----+---------+-------------+
How can I create this link between those 2 tables ?
I already tried to create another table with a uuid and add a column "unique_identifier" in the object table. The foreign key will be then linked to this uuid table and not the object table. The issue is that I have multiple tables with this problem and I will have to create the double number of table.
It is also possible to use the object ID as the FK instead of the name but it would mean that I must update every table with that FK with the new ID when updating an object.
By the SQL standard, a foreign key must reference either the primary key or a unique key of the parent table. If the primary key has multiple columns, the foreign key must have the same number and order of columns. Therefore the foreign key references a unique row in the parent table; there can be no duplicates.
Another solution is to use trigger, you can check the existence of the object in objects table before you insert into another table.
Update : Adding code
Prepare the tables and create trigger: (I have only included 3 columns in Objects table for simplicity. In trigger, I am just printing the error in else part, you could raise error suing RAISEERROR function to return the error to client)
Create table AuditObjects(id int identity (1,1),ObjectName varchar(20), ObjectDescription varchar(100) )
Insert into AuditObjects values('ObjectA','description ObjectA Test')
Insert into AuditObjects values('ObjectB','description ObjectB Test')
Insert into AuditObjects values('ObjectC','description ObjectC Test')
Insert into AuditObjects values('ObjectB','description ObjectB Test')
Insert into AuditObjects values('ObjectB','description ObjectB Test')
Insert into AuditObjects values('ObjectA','description ObjectA Test')
Create table ObjectTab2 (id int identity (1,1),foo varchar(200), ObjectName varchar(20))
go
CREATE TRIGGER t_CheckObject ON ObjectTab2 INSTEAD OF INSERT
AS BEGIN
Declare #errormsg varchar(200), #ObjectName varchar(20)
select #ObjectName = objectname from INSERTED
if exists(select 1 from AuditObjects where objectname = #ObjectName)
Begin
INSERT INTO ObjectTab2 (foo, Objectname)
Select foo, Objectname
from INSERTED
End
Else
Begin
Select #errormsg = 'Object '+objectname+ ' does not exists in AuditObjects table'
from Inserted
print(#errormsg)
End
END;
Now if you try to insert a row in ObjectTab2 with object name as "ObjectC", insert will be allowed as "objectC" is present in audit table.
Insert into ObjectTab2 values('blabla', 'ObjectC')
Select * from ObjectTab2
id foo ObjectName
----------- ------ --------------------
1 blabla ObjectC
However, if you try to enter "ObjectD", it will not make an insert and give error msg in output.
Insert into ObjectTab2 values('Inserting ObjectD', 'ObjectD')
Object ObjectD does not exists in AuditObjects table
Well its not what you asked for but give you the same functionality and results.
Can you not still go ahead with linking the two tables based on 'object name'. The only difference would be - when you join the two tables, you would get multiple records from table1 (the first table you were referencing). You may then add filter condition based on from and to, as per your requirements.
Post Edit -
What I meant is, you can still achieve the desired results without introducing Foreign Key in this scenario -
Let's call your tables - Table1 and Table2
--Below will give you all records from Table1
SELECT T2.*, T1.description, T1.creator_id, T1.from, T1.to
FROM TABLE2 T2
INNER JOIN TABLE1 T1 ON T2.OBJECT_NAME = T1.NAME;
--Below will give you ONLY those records from Table1 whose TO is null
SELECT T2.*, T1.description, T1.creator_id, T1.from, T1.to
FROM TABLE2 T2
INNER JOIN TABLE1 T1 ON T2.OBJECT_NAME = T1.NAME
WHERE T1.TO IS NULL;
I decided to go with an additional table to have this final design:
Table "Object"
+-------+--------------------------------------+---------+--------------+----------------------+----------------------+------------+
| id PK | identifier FK | name | description | from | to | creator_id |
+-------+--------------------------------------+---------+--------------+----------------------+----------------------+------------+
| 1 | 123e4567-e89b-12d3-a456-426614174000 | ObjectA | My object | 2021-05-30T00:05:00Z | 2021-05-31T05:04:36Z | 18 |
| 2 | 123e4567-e89b-12d3-a456-524887451057 | ObjectB | My desc | 2021-05-30T02:07:25Z | null | 15 |
| 3 | 123e4567-e89b-12d3-a456-426614174000 | ObjectA | Super object | 2021-05-31T05:04:36Z | null | 20 |
+-------+--------------------------------------+---------+--------------+----------------------+----------------------+------------+
Table "Object_identifier"
+--------------------------------------+
| identifier PK |
+--------------------------------------+
| 123e4567-e89b-12d3-a456-426614174000 |
| 123e4567-e89b-12d3-a456-524887451057 |
+--------------------------------------+
Table "foo"
+-------+--------+--------------------------------------+
| id PK | foo | object_identifier FK |
+-------+--------+--------------------------------------+
| 1 | blabla | 123e4567-e89b-12d3-a456-426614174000 |
| 2 | wawawa | 123e4567-e89b-12d3-a456-524887451057 |
+-------+--------+--------------------------------------+

How to add foreign key constraint to Table A (id, type) referencing either of two tables Table B (id, type) or Table C (id, type)?

I'm looking to use two columns in Table A as foreign keys for either one of two tables: Table B or Table C. Using columns table_a.item_id and table_a.item_type_id, I want to force any new rows to either have a matching item_id and item_type_id in Table B or Table C.
Example:
Table A: Inventory
+---------+--------------+-------+
| item_id | item_type_id | count |
+---------+--------------+-------+
| 2 | 1 | 32 |
| 3 | 1 | 24 |
| 1 | 2 | 10 |
+---------+--------------+-------+
Table B: Recipes
+----+--------------+-------------------+-------------+----------------------+
| id | item_type_id | name | consistency | gram_to_fluid_ounces |
+----+--------------+-------------------+-------------+----------------------+
| 1 | 1 | Delicious Juice | thin | .0048472 |
| 2 | 1 | Ok Tasting Juice | thin | .0057263 |
| 3 | 1 | Protein Smoothie | heavy | .0049847 |
+----+--------------+-------------------+-------------+----------------------+
Table C: Products
+----+--------------+----------+--------+----------+----------+
| id | item_type_id | name | price | in_stock | is_taxed |
+----+--------------+----------+--------+----------+----------+
| 1 | 2 | Purse | $200 | TRUE | TRUE |
| 2 | 2 | Notebook | $14.99 | TRUE | TRUE |
| 3 | 2 | Computer | $1,099 | FALSE | TRUE |
+----+--------------+----------+--------+----------+----------+
Other Table: Item_Types
+----+-----------+
| id | type_name |
+----+-----------+
| 1 | recipes |
| 2 | products |
+----+-----------+
I want to be able to have an inventory table where employees can enter inventory counts regardless of whether an item is a recipe or a product. I don't want to have to have a product_inventory and recipe_inventory table as there are many operations I need to do across all inventory items regardless of item types.
One solution would be to create a reference table like so:
Table CD: Items
+---------+--------------+------------+-----------+
| item_id | item_type_id | product_id | recipe_id |
+---------+--------------+------------+-----------+
| 2 | 1 | NULL | 2 |
| 3 | 1 | NULL | 3 |
| 1 | 2 | 1 | NULL |
+---------+--------------+------------+-----------+
It just seems very cumbersome, plus I'd now need to add/remove products/recipes from this new table whenever they are added/removed from their respective tables. (Is there an automatic way to achieve this?)
CREATE TABLE [dbo].[inventory] (
[id] [bigint] IDENTITY(1,1) NOT NULL,
[item_id] [smallint] NOT NULL,
[item_type_id] [tinyint] NOT NULL,
[count] [float] NOT NULL,
CONSTRAINT [PK_inventory_id] PRIMARY KEY CLUSTERED ([id] ASC)
) ON [PRIMARY]
What I would really like to do is something like this...
ALTER TABLE [inventory]
ADD CONSTRAINT [FK_inventory_sources] FOREIGN KEY ([item_id],[item_type_id])
REFERENCES {[products] ([id],[item_type_id]) OR [recipes] ([id],[item_type_id])}
Maybe there is no solution as I'm describing it, so if you have any ideas where I can maintain the same/similar schema, I'm definitely open to hearing them!
Thanks :)
Since your products and recipes are stored separately, and appear to mostly have separate columns, then separate inventory tables is probably the correct approach. e.g.
CREATE TABLE dbo.ProductInventory
(
Product_id INT NOT NULL,
[count] INT NOT NULL,
CONSTRAINT FK_ProductInventory__Product_id FOREIGN KEY (Product_id)
REFERENCES dbo.Product (Product_id)
);
CREATE TABLE dbo.RecipeInventory
(
Recipe_id INT NOT NULL,
[count] INT NOT NULL,
CONSTRAINT FK_RecipeInventory__Recipe_id FOREIGN KEY (Recipe_id)
REFERENCES dbo.Recipe (Recipe_id )
);
If you need all types combined, you can simply use a view:
CREATE VIEW dbo.Inventory
AS
SELECT Product_id AS item_id,
2 AS item_type_id,
[Count]
FROM ProductInventory
UNION ALL
SELECT recipe_id AS item_id,
1 AS item_type_id
[Count]
FROM RecipeInventory;
GO
IF you create a new item_type, then you need to amend the DB design anyway to create a new table, so you would just need to amend the view at the same time
Another possibility, would be to have a single Items table, and then have Products/Recipes reference this. So you start with your items table, each of which has a unique ID:
CREATE TABLE dbo.Items
(
item_id INT IDENTITY(1, 1) NOT NULL
Item_type_id INT NOT NULL,
CONSTRAINT PK_Items__ItemID PRIMARY KEY (item_id),
CONSTRAINT FK_Items__Item_Type_ID FOREIGN KEY (Item_Type_ID) REFERENCES Item_Type (Item_Type_ID),
CONSTRAINT UQ_Items__ItemID_ItemTypeID UNIQUE (Item_ID, Item_type_id)
);
Note the unique key added on (item_id, item_type_id), this is important for referential integrity later on.
Then each of your sub tables has a 1:1 relationship with this, so your product table would become:
CREATE TABLE dbo.Products
(
item_id BIGINT NOT NULL,
Item_type_id AS 2,
name VARCHAR(50) NOT NULL,
Price DECIMAL(10, 4) NOT NULL,
InStock BIT NOT NULL,
CONSTRAINT PK_Products__ItemID PRIMARY KEY (item_id),
CONSTRAINT FK_Products__Item_Type_ID FOREIGN KEY (Item_Type_ID)
REFERENCES Item_Type (Item_Type_ID),
CONSTRAINT FK_Products__ItemID_ItemTypeID FOREIGN KEY (item_id, Item_Type_ID)
REFERENCES dbo.Item (item_id, item_type_id)
);
A few things to note:
item_id is again the primary key, ensuring the 1:1 relationship.
the computed column item_type_id (as 2) ensuring all item_type_id's are set to 2. This is key as it allows a foreign key constraint to be added
the foreign key on (item_id, item_type_id) back to the items table. This ensures that you can only insert a record to the product table, if the original record in the items table has an item_type_id of 2.
A third option would be a single table for recipes and products and make any columns not required for both nullable. This answer on types of inheritance is well worth a read.
I think there is a flaw in your database design. The best way to solve your actual problem, is to have Recipies and products as one single table. Right now you have a redundant column in each table called item_type_id. That column is not worth anything, unless you actually have the items in the same table. I say redundant, because it has the same value for absolutely every entry in each table.
You have two options. If you can not change the database design, work without foreign keys, and make the logic layer select from the correct tables.
Or, if you can change the database design, make products and recipies exist in the same table. You already have a item_type table, which can identify item categorization, so it makes sense to put all items in the same table
you can only add one constraint for a column or pair of columns. Think about apples and oranges. A column cannot refer to both oranges and apples. It must be either orange or apple.
As a side note, this can be somehow achieved with PERSISTED COMPUTED columns, however It only introduces overhead and complexity.
Check This for Reference
You can add some computed columns to the Inventory table:
ALTER TABLE Inventory
ADD _recipe_item_id AS CASE WHEN item_type_id = 1 THEN item_id END persisted
ALTER TABLE Inventory
ADD _product_item_id AS CASE WHEN item_type_id = 2 THEN item_id END persisted
You can then add two separate foreign keys to the two tables, using those two columns instead of item_id. I'm assuming the item_type_id column in those two tables is already computed/constraint appropriately but if not you may want to consider that too.
Because these computed columns are NULL when the wrong type is selected, and because SQL Server doesn't check FK constraints if at least one column value is NULL, they can both exist and only one or the other will be satisfied at any time.

Snowflake: create a default field value that auto increments for each primary key, resets per primary key

I would like to create a table to house the following type of data
+--------+-----+----------+
| pk | ctr | name |
+--------+-----+----------+
| fish | 1 | herring |
| mammal | 1 | dog |
| mammal | 2 | cat |
| mammal | 3 | whale |
| bird | 1 | penguin |
| bird | 2 | ostrich |
+--------+----_+----------+
PK is the primary key string (100) not null
ctr is a field I want to auto increment by 1 for each pk row
I have tried the following
create or replace table schema.animals (
pk string(100) not null primary key,
ctr integer not null default ( select NVL(max(ctr),0) + 1 from schema.animals )
name string (1000) not null);
This produced the following error
SQL compilation error: error line 6 at position 52 aggregate functions
are not allowed as part of the specification of a default value
clause.
So i would have used the auto increment /identity property like so
AUTOINCREMENT | IDENTITY [ ( start_num , step_num ) | START num INCREMENT num ]
but it doesnt seem to be able to support the resetting per unique pk
looking for any suggestions on how to solve this, thanks for any help in advance
You cannot do this with an IDENTITY method. The suggested solution is to use INSTEAD OF trigger that will calculate ctr value on every row of INSERTED table. For example
CREATE TABLE dbo.animals (
pk nvarchar(100) NOT NULL,
ctr integer NOT NULL,
name nvarchar(1000) NOT NULL,
CONSTRAINT PK_animals PRIMARY KEY (pk, ctr)
)
GO
CREATE TRIGGER dbo.animals_before_insert ON dbo.animals INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO animals (pk, ctr, name)
SELECT
i.pk,
(ROW_NUMBER() OVER (PARTITION BY i.pk ORDER BY i.name) + ISNULL(a.max_ctr, 0)) AS ctr,
i.name
FROM inserted i
LEFT JOIN (SELECT pk, MAX(ctr) AS max_ctr FROM dbo.animals GROUP BY pk) a
ON i.pk = a.pk;
END
GO
INSERT INTO dbo.animals (pk, name) VALUES
('fish' , 'herring'),
('mammal' , 'dog'),
('mammal' , 'cat'),
('mammal' , 'whale'),
('bird' , 'pengui'),
('bird' , 'ostrich');
SELECT * FROM dbo.animals;
Result
pk ctr name
------- ----- ---------
bird 1 ostrich
bird 2 pengui
fish 1 herring
mammal 1 cat
mammal 2 dog
mammal 3 whale
Another method is to use scalar user-defined function as DEFAULT value but it is slow: the trigger fires once on all rows whereas the function is called on every row.
I have no idea why you would have a column called pk that is not the primary key. You cannot (easily) do what you want. I would recommend doing this as:
create or replace table schema.animals (
animal_id int identity primary key,
name string(100) not null primary key,
);
create view schema.v_animals as
select a.*, row_number() over (partition by name order by animal_id) as ctr
from schema.animals a;
That is, calculate ctr when you need to use it, rather than storing it in the table.

Is it possible to update an "order" column from within a trigger in MySQL?

We have a table in our system that would benefit from a numeric column so we can easily grab the 1st, 2nd, 3rd records for a job. We could, of course, update this column from the application itself, but I was hoping to do it in the database.
The final method must handle cases where users insert data that belongs in the "middle" of the results, as they may receive information out of order. They may also edit or delete records, so there will be corresponding update and delete triggers.
The table:
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`seq` int(11) unsigned NOT NULL,
`job_no` varchar(20) NOT NULL,
`date` date NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=7 DEFAULT CHARSET=latin1
And some example data:
mysql> SELECT * FROM test ORDER BY job_no, seq;
+----+-----+--------+------------+
| id | seq | job_no | date |
+----+-----+--------+------------+
| 5 | 1 | 123 | 2009-10-05 |
| 6 | 2 | 123 | 2009-10-01 |
| 4 | 1 | 123456 | 2009-11-02 |
| 3 | 2 | 123456 | 2009-11-10 |
| 2 | 3 | 123456 | 2009-11-19 |
+----+-----+--------+------------+
I was hoping to update the "seq" column from a t rigger, but this isn't allowed by MySQL, with an error "Can't update table 'test' in stored function/trigger because it is already used by statement which invoked this stored function/trigger".
My test trigger is as follows:
CREATE TRIGGER `test_after_ins_tr` AFTER INSERT ON `test`
FOR EACH ROW
BEGIN
SET #seq = 0;
UPDATE
`test` t
SET
t.`seq` = #seq := (SELECT #seq + 1)
WHERE
t.`job_no` = NEW.`job_no`
ORDER BY
t.`date`;
END;
Is there any way to achieve what I'm after other than remembering to call a function after each update to this table?
What about this?
CREATE TRIGGER `test_after_ins_tr` BEFORE INSERT ON `test`
FOR EACH ROW
BEGIN
SET #seq = (SELECT COALESCE(MAX(seq),0) + 1 FROM test t WHERE t.job_no = NEW.job_no);
SET NEW.seq = #seq;
END;
From Sergi's comment above:
http://dev.mysql.com/doc/refman/5.1/en/stored-program-restrictions.html - "Within a stored function or trigger, it is not permitted to modify a table that is already being used (for reading or writing) by the statement that invoked the function or trigger."