我有一个供应商表,其中包含以下架构:
| SupplierId | SupplierName |
-----------------------------
| 1 | Good Company |
我还有一对多的表 SupplierActivityHours ,其中包含以下架构:
| Id | SupplierId | Day (enum) | OpenHour | CloseHour |
----------------------------------------------------------------------
| 1 | 1 | 0 | 05:00:00.0000000 | 15:00:00.0000000 |
| 2 | 1 | 1 | 05:00:00.0000000 | 15:00:00.0000000 |
| 3 | 1 | 2 | 05:00:00.0000000 | 16:00:00.0000000 |
我创建了一个设置页面,供应商可以更新其数据。
问题是,更新活动时间表的正确方法是什么?
供应商可能会删除天数,添加天数,更新现有天数。
我想到了以下几个选项:
BTW,我正在使用ASP.NET MVC Core,MS SQL和Dapper.NET(如果它以某种方式重要)。
有没有更好的选择我不知道?谢谢大家!
我目前正在阅读" MERGE",试着看看它是否可以解决我的问题。
答案 0 :(得分:2)
是的,您可以这样使用MERGE:
create type SupplierActivityHoursType as table
(
[Id] [int] NOT NULL,
[SupplierId] [int] NULL,
[Day] [int] NULL,
[OpenHour] [datetime] NULL,
[CloseHour] [datetime] NULL
)
go
CREATE PROCEDURE UpdateSupplierActivityHours
@SupplierActivityHours dbo.SupplierActivityHoursType readonly
AS
BEGIN
merge SupplierActivityHours as t
using (select Id, SupplierId, [Day], OpenHour, CloseHour
from @SupplierActivityHours) as s
on t.Id = s.Id, t.SupplierId = s.SupplierId
when matched then
update set t.[Day] = s.[Day], t.OpenHour = s.OpenHour, t.CloseHour = s.CloseHour
when not matched by target then
insert (SupplierId, [Day], OpenHour, CloseHour)
values (s.SupplierId, s.[Day], s.OpenHour, s.CloseHour)
when not matched by source then
delete;
END
因此,要更新所有供应商数据,您需要将您的小时表传递给UpdateSupplierActivityHours
SP。
当在SupplierActivityHours
中找到记录时,它会被更新,当它在@SupplierActivityHours中找到但在SupplierActivityHours
表中找不到时,它将被插入,如果在@SupplierActivityHours
中找不到但在SupplierActivityHours
中找到它将被删除。
MERGE语句允许在一个事务中有效地执行所有这些插入,更新和删除。
答案 1 :(得分:0)
虽然你说“擦除/更新”方法效率不高,但最容易实现。
当您有多个用户编辑相同的数据时,您必须阻止其他人编辑表格或告诉他们“上次编辑获胜”;即编辑将被覆盖。
这对您的用户来说是否有问题取决于您,并且取决于同时编辑数据的潜在用户数量 - 您可能会发现这绝不是问题。
如果您担心丢失编辑,可以实施数据的审核日志并记录所有更改。
答案 2 :(得分:0)
此表架构和模式是我考虑将代理Id
作为主键删除并考虑使用SupplierId, Day
作为主键的少数几个实例之一。此版本的merge
过程无论如何都会忽略Id
。除非您还要通过SupplierId, Day
之外的其他内容获取或按行设置查询,否则我认为不需要Id
。
最后,我建议你做一些你对Id
感到满意的事情。
表格设置:
create table dbo.Suppliers (
SupplierId int primary key
, SupplierName nvarchar(64)
);
insert into dbo.Suppliers values
(1, 'Good Company');
create table dbo.SupplierActivityHours (
Id int not null
, SupplierId int not null
, [Day] tinyint not null
, OpenHour time(0) not null
, CloseHour time(0) not null
, constraint pk_SupplierActivityHours primary key clustered (Id)
, constraint fk_SupplierActivityHours_Suppliers foreign key (SupplierId)
references Suppliers(SupplierId)
);
create unique nonclustered index
uix_SupplierActivityHours_SupplierId_Day
on dbo.SupplierActivityHours (SupplierId, [Day])
include (OpenHour, CloseHour);
/* If the Supplier can have multiple open/close per day,
move OpenHour from the include() to the index key. */
insert into dbo.SupplierActivityHours values
(1,1,0,'05:00:00.0000000','15:00:00.0000000')
, (2,1,1,'05:00:00.0000000','15:00:00.0000000')
, (3,1,2,'05:00:00.0000000','16:00:00.0000000');
表格类型:
create type dbo.udt_SupplierActivityHours as table (
/* You will not have an ID for new rows, so no need for it here */
SupplierId int not null
, [Day] tinyint not null
, OpenHour time(0) not null
, CloseHour time(0) not null
, unique (SupplierId,[Day]) /* unnamed unique constraint */
--, primary key clustered (SupplierId, [Day]) /* instead of unique constraint */
/* If the Supplier can have multiple open/close per day,
add OpenHour to the unique constraint or primary key. */
);
合并程序:
go
create procedure dbo.SupplierActivityHours_Merge (
@SupplierActivityHours dbo.udt_SupplierActivityHours readonly
) as
begin;
set nocount, xact_abort on; /* you should always include this. */
begin try
begin tran
merge SupplierActivityHours with (holdlock) as t
/* with (holdlock) prevents race conditions in merge */
using (select SupplierId, [Day], OpenHour, CloseHour
from @SupplierActivityHours) as s
on t.SupplierId = s.SupplierId
and t.[Day] = s.[Day]
when matched and (
t.OpenHour != s.OpenHour
or t.CloseHour != s.CloseHour
)
then
update set
t.OpenHour = s.OpenHour
, t.CloseHour = s.CloseHour
when not matched by target
then insert (SupplierId, [Day], OpenHour, CloseHour)
values (s.SupplierId, s.[Day], s.OpenHour, s.CloseHour)
when not matched by source then
delete
/* check output for testing: */
--output $action, deleted.*, inserted.*, s.*
;
commit tran
end try
begin catch;
if @@trancount > 0
begin;
rollback transaction;
throw; /* or other error handling */
end;
end catch;
end;
go
在上面的表类型中,我包含了一个唯一的约束来防止重复。唯一约束将产生一些开销。如果这个开销是一个问题,并且您有其他安全措施来防止传入数据出现问题,请将其删除。
唯一约束的替代方法是使用群集在表类型上的主键,这也会产生开销。有一些方法可以通过保证已经从应用程序中订购的数据添加到数据中来减轻一些开销,但它非常复杂。
为什么你应该总是包括set nocount, xact_abort on;
- Erland Sommarskog
使用merge
时应注意的事项:
MERGE
Statement - Aaron Bertrand Merge
- sqlteam MERGE
Bug - Paul White Merge
Statement (LCK_M_RS_U locks) - Kendra Little merge
statements the right way - David Stein 表值参数参考:
以下是如何执行三部分操作而不是merge
的示例。希望这看起来不像你想要的那么复杂:
create type dbo.udt_ActivityHours as table (
[Day] tinyint not null
, OpenHour time(0) not null
, CloseHour time(0) not null
, unique ([Day])
-- If the Supplier can have multiple open/close per day,
-- add OpenHour to the unique constraint or primary key
);
go
create procedure dbo.SupplierActivityHours_Set_BySupplierId (
@SupplierId int not null
, @ActivityHours dbo.udt_ActivityHours readonly
) as
begin;
set nocount, xact_abort on; -- you should always use these.
begin try
begin tran
/* delete */
delete sah
from dbo.SupplierActivityHours sah
where sah.SupplierId = @SupplierId
and not exists (
select 1
from @ActivityHours ah
where ah.[Day] = sah.[Day]
);
/* update */
update sah set
sah.OpenHour = ah.OpenHour
, sah.CloseHour = ah.CloseHour
from SupplierActivityHours sah
inner join @ActivityHours ah on sah.[Day] = ah.[Day]
and sah.SupplierId = @SupplierId
where sah.OpenHour != ah.OpenHour
or sah.CloseHour != ah.CloseHour;
/* insert */
insert into dbo.SupplierActivityHours
(SupplierId, [Day], OpenHour, CloseHour)
select @SupplierId, [Day], OpenHour, CloseHour
from @ActivityHours ah
where not exists (
select 1
from dbo.SupplierActivityHours sah
where sah.SupplierId = @SupplierId
and ah.[Day] = sah.[Day]
);
commit tran;
end try
begin catch;
if @@trancount > 0
begin;
rollback transaction;
throw;
end;
end catch;
end;
go
TVP似乎会在rextester上造成死锁,因此rextester使用临时表替身。 (tvp deadlock example)
rextester:http://rextester.com/DCUPNF63408
答案 3 :(得分:0)
首先,让我们回想一下SQL是一个relational set-based theory
,因此它代表数据库,其解决方案对于一切的关系价值是有意义的。
这也使我们从整体上思考,包括涉及的网络流量以及对数据库的影响,碎片等。我们的设计没有浪费任何东西。
此外,Schemas
中的SQL Server
相当于tablespace
中的Oracle
,或者像房屋城市中的住房协会。模式具有自己的安全性并包含多个表。表包含多个列。
你的桌子设计
/*---------------------------------
| DIMENSION - dbo.Suppliers
*--------------------------------*/
CREATE TABLE dbo.Suppliers (Supplier_ID INT NOT NULL
, Supplier_Name NVARCHAR(100) NOT NULL)
/*---------------------------------
| FACT - dbo.ActivityHours_Suppliers
*--------------------------------*/
CREATE TABLE dbo.ActivityHours_Suppliers
( Row_ID INT IDENTITY(1,1) NOT NULL
/* At best a database key, but your design makes it useless.*/
, Supplier_ID INT NOT NULL
/* Value, whether you have a constraint or not comes from dbo.Suppliers*/
, Day_enum INT
/* Is this a Durable Key?*/
, OpenHour TIME NOT NULL
, CloseHour TIME NOT NULL)
因此,您希望在以下情况下反映此FACT表中的更改:
UPDATE
ActivityHours_Suppliers表值OpenHour,CloseHour INSERT
ActivityHours_Suppliers表Day_enum,OpenHour,CloseHour?DELETE
,其中Day_enum是非法的。
- 虽然后一个问题应该在它到达数据库之前处理。观察和问题:
如果是,您刚刚创建了durable key。如果不是,请考虑使用其他列以允许将来进行区分。虽然,我希望这与客户控制分开
在一天的开始?间隔?在一天结束?住(可能不是)?如果不是在当天结束时或开始之前,您是否允许在CloseHour中使用NULL值?这可能会使更新更容易。
临时表是更好的长期解决方案,可以在系统上更轻松地管理插入/更新,因为您使用的是基于集合的解决方案。
特别是如果您不打算实时计划,在登陆数据库之前,临时表将充当另一个缓冲区。
也许通过GUI使用的GUI引用了DIM表。从中创建一个新的供应商SEPARATE。你希望这永远不会有错。
你可以....可以使用两个表,一个用于更新,一个用于插入,但由于这是一个临时表,这有关系吗?你是否正在为这些表做任何事情,需要两个单独的表?
您的StagingTable会保留以下值:
Supplier_ID, Day_enum, OpenHour, CloseHour
。
如果您认为每次都插入和删除了大量行,那么请考虑其优势在于对表行进行大量修改。
如果您的金额不同,那么合并MERGE和CTE可能是合适的(这是一个简单的检查)IF <someNumber> < (SELECT COUNT(1) FROM TempTable)
/*---------------------------------
| UPDATING FIELDS
*--------------------------------*/
WITH CTE AS (SELECT Supplier_ID, Day_enum, OpenHour, CloseHour
FROM TempTable)
UPDATE A
SET OpenHour = B.OpenHour
, CloseHour = C.CloseHour
FROM MYTABLE A
INNER JOIN CTE B ON B.Supplier_ID = A.Supplier_ID
AND B.Day_enum = B.Day_enum
/*---------------------------------
| INSERTING FIELDS
*--------------------------------*/
;WITH CTE AS (SELECT Supplier_ID, Day_enum
FROM dbo.Suppliers
--use this as an opportunity to filter the rows or not at all)
INSERT INTO MYTABLE (Supplier_ID, Day_enum, OpenHour, CloseHour)
SELECT Supplier_ID, Day_enum, OpenHour, CloseHour
FROM TempTable
WHERE NOT EXISTS (SELECT 1
FROM CTE
WHERE ROWS = TempTable.ROWS)
(NOT)EXISTS返回true或false值,因此我们不需要在SELECT语句中指定列。 或者你可以限制对部分的影响......也许是区域,州,城市,如下所示
;WITH CTE AS (SELECT A.Supplier_ID, A.Day_enum, A.OpenHour, A.CloseHour
FROM TempTable A
INNER JOIN dbo.Suppliers B ON B.Supplier_ID = A.Supplier_ID
AND B.Day_enum = A.Day_Enum
WHERE A.Day_enum > (somenumber)
-- A.Day_enum IS NOT NULL
)
更好的是,考虑使用子查询替换SupplierTable,该子查询将DIM表限制为这些部分,区域,如下所示
INNER JOIN (SELECT Supplier_ID, Day_enum
FROM dbo.Suppliers
WHERE Region_ID = 15
AND Region_ID = 20) AS B ON B.Supplier_ID = A.Supplier_ID
AND B.Day_enum = A.Day_Enum
WHERE A.Day_enum IS NOT NULL
/ *您甚至可能会发现这种类型的JOIN对您的数据库* /
有效SELECT A.Supplier_ID, A.Day_enum, A.OpenHour, A.CloseHour
FROM MYINSERT A
LEFT OUTER JOIN dbo.ActivityHours_Suppliers B ON A.Supplier_ID = B.Supplier_ID
AND A.Day_enum = B.Day_enum
WHERE A.Day_enum IS NOT NULL
这将返回左侧匹配的每一行,并过滤掉每一行为NULL,因此只提供需要插入的行。
最后,请始终进行测试,因为表格的大小和范围可以决定什么是真正的解决方案。
我希望我能给你很多考虑并帮助你提供长期解决方案!
来源: KimballGroup(07/2012)。 Durable Super-Natural Keys