从具有多个基表的视图中删除记录的最有效方法?
Posted
技术标签:
【中文标题】从具有多个基表的视图中删除记录的最有效方法?【英文标题】:Most efficient way of deleting records from a view with multiple base tables? 【发布时间】:2020-04-17 08:48:20 【问题描述】:我需要编写一个 VB 函数,从视图的基表中删除所有已使用默认值/默认约束键值初始化的记录。
我的理解是,这样做的唯一方法是分别从每个基表中删除记录,但我不确定是否有比我尝试尝试的更简单、更有效的方法。如果可能的话,我想要一些指导/建议。
这是我能想到的唯一方法:
-
运行从视图返回基表名称的查询:
DECLARE @vn as nvarchar(max) = 'dbo.TABLE_NAME'
SELECT referenced_server_name, referenced_database_name, referenced_entity_name as SourceTable,referenced_minor_name as SourceColumn, referenced_minor_id as depnumber
FROM sys.dm_sql_referenced_entities (@vn, 'OBJECT')
where referenced_minor_name IS NOT NULL
ORDER BY referenced_entity_name, referenced_minor_id
-
运行
sp_helpconstraint N'<table_name>'
,它将为我提供所有默认值/默认约束类型的列表,以及我需要将默认值与每个表中的值进行比较并确定是否应该记录的列名被删除。
问题
有没有更简单/更有效的方法来尝试从视图中删除记录?
注意:我最终编写的完整功能已添加到下面的答案中,供任何对答案感兴趣的人使用
【问题讨论】:
您是在谈论构建一个适用于任何视图或特定视图的流程吗? @jmcilhinney 对于任何观点 【参考方案1】:我不确定是否有比我尝试的更简单、更有效的方法。如果可能的话,我想要一些指导/建议
TLDR;没有
您需要了解视图不是数据库中的数据;它是一个存储的 SQL 查询,每次您从视图中选择时都会运行该查询。
SQL Server 甚至可能会获取您的查询并将其与提供视图的查询混合并优化它们并运行它们,因此它甚至不一定会运行视图查询,获取所有百万条记录视图代表,然后通过它们寻找一个叫做君士坦丁堡 Ernhardt 的人 - SQL Server 可能认为最好静默透明地重写您提供的查询,因此它的计划和运行与您可能的想法完全不同 - 它这样做是为了每个查询都在一个称为优化的过程中。
你的观点是:
CREATE VIEW MyView AS
SELECT * FROM Person p JOIN Address a on p.AddressId = a.Id
你写:
SELECT * FROM MyView WHERE Name = 'Abc' and HouseName = 'def'
您可能认为它会这样做(从概念上讲,您是对的):
SELECT * FROM
(
SELECT * FROM Person p JOIN Address a on p.AddressId = a.Id
) x WHERE Name = 'Abc' and HouseName = 'def'
但它可能会被重写为:
SELECT * FROM Person p JOIN Address a on p.AddressId = a.Id WHERE Name = 'Abc' and HouseName = 'def'
所以现在已经解决了,您可以看到视图只是表顶部的查询,每次您从中选择时都会运行它 - 如何从查询中删除数据?
你不能,因为查询没有数据;他们从表中检索数据
“从视图中删除数据”的唯一方法是从视图从中选择数据的表中删除数据
您只能通过相关表上的DELETE
语句来做到这一点
您可以在视图上编写INSTEAD OF
触发器,然后从视图中删除,SQL Server 将运行触发器(从基础表中删除)。看起来您正在从视图中删除数据,但实际上,您只是在使用一种机制从基础表中删除数据,就像视图是将数据从这些表中拖出的机制一样。
您可以编写一个删除数据的存储过程,但同样,这只是从基础表中删除数据的一种机制
根据您的业务目标和以某种方式封装您的软件的愿望,选择您喜欢的任何方法。例如,过去我有一个我无法更改的软件(丢失了源代码或其他),并且它已硬连接到 SELECT FROM users
或 DELETE FROM users
- 我们希望该软件能够继续工作,即使用户表被重命名为成员。我们重命名了表,然后创建了一个名为 users
的视图,它刚刚做了 SELECT * FROM members
- 允许应用程序继续工作,读取数据。然后,当应用尝试对users
视图(应用仍然认为它是一个表)执行该操作时,我们创建了INSTEAD OF
触发器来更新和删除成员表中的数据
那么为什么这么难呢?好吧,从您的视图中出来的数据甚至可能不再与表格行相关。这是一个简单的版本:
CREATE VIEW MyView AS
SELECT MAX(SUBSTR(Name, 4, 999)) MaxFirstName FROM Person GROUP BY Gender
假设有两个人分别叫Mr Lee Smith
和Ms Lee Smith
,你已经获取了函数输出的最大值并得到了Lee Smith
,现在你想通过分析视图从person表中删除Lee Smith
并从 Person 表中删除... 什么?哪个记录?分组将所有记录混合在一起。一个人的最大姓名和另一个人的最小生日..
这是另一个例子,有点可笑但可以想象:
CREATE VIEW MyView AS
SELECT name as x FROM person
UNION
SELECT street FROM address
这很容易产生独特的价值“Penny Lane”——但它是一个人,还是一条路?如果我们从这个视图中删除 x = 'Penny Lane' 应该删除哪个
没有可以运行它的灵丹妙药,它会显示“此表使用这 3 个表”,因此您可以从中删除它们。这甚至都不是一个好的前提。您的视图可能会从一个数据表和一个查找表中进行选择,并且仅仅因为您要从用户表中删除唐纳德特朗普而从查找表中删除性别类型 1,这将是一个错误的选择
如果你想在视图上提供删除功能,你需要编写一些代码;没有一个自动的解决方案可以计算出应该删除哪些表中的哪些数据以及应该保留哪些数据。试想一下,分析一个连接 9 个表、混合连接样式、另外 8 个 VALUES 列表、3 次调用解析一些 json 的交叉应用表值函数、吊起几行生成的视图是多么困难那里有递归 CTE,还有一个枢轴......
绝对没有任何人会编写魔术按钮来将所有内容分开为“计算出基表列表以及应删除哪些数据以满足DELETE FROM MyView WHERE output_of_parsing_function = 'Hello'
【讨论】:
感谢您详尽详尽的回答。表/视图中的每一行都有一个具有唯一 id 的 id 列。这就是为什么可以做到这一点。我猜你可能没有正确理解我的问题。我已经写了我最终创建的函数作为这个线程的答案【参考方案2】:这是我最终使用的最后一个工作函数。 (欢迎提出更好的方法)
If DI.CommonDataFunctions.IsThisAView(BaseTableName, Globals.dif) Then
Dim viewColumnNames As New List(Of String)
Dim tableName As String
Dim viewTables As New List(Of String)
Dim isDefault
Dim primaryKeyName As String
//return table names from view
Dim qp As New List(Of SqlParameter)
qp.Add(New SqlParameter("@vn", $"dbo.BaseTableName"))
Dim sql As String = "
SELECT
referenced_entity_name as SourceTable,referenced_minor_name as SourceColumn
FROM
sys.dm_sql_referenced_entities (@vn, 'OBJECT')
WHERE
referenced_minor_name IS NOT NULL
ORDER BY
referenced_entity_name, referenced_minor_id"
Using dr As New DataReader(Globals.dif.GetDBDetails)
Dim constraintKeys As New Dictionary(Of String, String)()
Dim primaryKeyList As New List(Of Int32)
Dim table As String
dr.ExecuteReader(sql, qp)
Do While dr.Read
tableName = dr.Item("SourceTable").ToString.ToUpper.Trim
viewColumnNames.Add(dr.Item("SourceColumn").ToString.ToUpper.Trim)
If Not viewTables.Contains(tableName) Then
viewTables.Add(tableName)
End If
Loop
For Each table In viewTables
Dim columnName As String
Dim defaultConstraintValue
isDefault = True
table = table
dr.ExecuteReader("
SELECT Col.Column_Name from
INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab,
INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col
WHERE
Col.Constraint_Name = Tab.Constraint_Name
AND Col.Table_Name = Tab.Table_Name
AND Constraint_Type = 'PRIMARY KEY'
AND Col.Table_Name = '" + table + "'")
While dr.Read
primaryKeyName = dr.Item(0)
End While
//return default constraints
dr.ExecuteReader("
SELECT
ColumnName = c.name,
TableName = t.name,
df.definition
FROM
sys.default_constraints df
INNER JOIN
sys.tables t ON df.parent_object_id = t.object_id
INNER JOIN
sys.columns c ON c.object_id = df.parent_object_id AND df.parent_column_id = c.column_id
WHERE
t.Name = N'" + table + "'")
While dr.Read
defaultConstraintValue = dr.Item("definition").ToString
//delete "(( ))" Or "( )" from default constraint
If defaultConstraintValue.StartsWith("((") AndAlso defaultConstraintValue.EndsWith("))") Then
defaultConstraintValue = defaultConstraintValue.Substring(0, defaultConstraintValue.Length - 2)
defaultConstraintValue = defaultConstraintValue.Substring(2)
ElseIf defaultConstraintValue.StartsWith("(") AndAlso defaultConstraintValue.EndsWith(")") Then
defaultConstraintValue = defaultConstraintValue.Substring(0, defaultConstraintValue.Length - 1)
defaultConstraintValue = defaultConstraintValue.Substring(1)
End If
If defaultConstraintValue.StartsWith("'") AndAlso defaultConstraintValue.EndsWith("'") Then
defaultConstraintValue = defaultConstraintValue.Substring(0, defaultConstraintValue.Length - 1)
defaultConstraintValue = defaultConstraintValue.Substring(1)
If Not IsNumeric(defaultConstraintValue) Then
defaultConstraintValue = "'" + defaultConstraintValue + "'"
End If
End If
columnName = dr.Item("ColumnName").ToString.ToUpper.Trim
constraintKeys.Add(columnName, defaultConstraintValue)
End While
Next
Dim sql2 = "SELECT " + primaryKeyName + " FROM " + BaseTableName
If constraintKeys IsNot Nothing Then
Dim isFirstFilter = True
sql2 &= " WHERE "
For Each constraintKey In constraintKeys
If viewColumnNames.Contains(constraintKey.Key) AndAlso constraintKey.Key <> "FAMILY_UID" Then
If isFirstFilter = False Then
sql2 &= " And "
End If
If IsNumeric(constraintKey.Value) Then
Dim intConverted = CInt(constraintKey.Value)
sql2 &= constraintKey.Key + " = " + intConverted.ToString + " "
If isFirstFilter = True Then
isFirstFilter = False
End If
Else
sql2 &= constraintKey.Key + " = " + constraintKey.Value + " "
If isFirstFilter = True Then
isFirstFilter = False
End If
End If
End If
Next
End If
dr.ExecuteReader(sql2)
While dr.Read
primaryKeyList.Add(dr.Item(primaryKeyName))
End While
If primaryKeyList.Count > 0 Then
For Each table In viewTables
Dim isFirstFilter = True
Dim sql3 = "DELETE FROM " + table + " WHERE " + primaryKeyName + " IN ("
For Each primaryKey In primaryKeyList
sql3 &= primaryKey.ToString
If Not primaryKey = primaryKeyList(primaryKeyList.Count - 1) Then
sql3 &= ", "
End If
Next
sql3 &= ")"
Using CEx As New CommandExecutor(Globals.dif)
CEx.ExecuteNonQuery(sql3)
End Using
Next
End If
End Using
End If
【讨论】:
以上是关于从具有多个基表的视图中删除记录的最有效方法?的主要内容,如果未能解决你的问题,请参考以下文章