【问题标题】:Possible to insert with a Table Parameter, and also retrieve identity values?可以使用表参数插入,还可以检索标识值吗?
【发布时间】:2012-08-19 06:47:02
【问题描述】:

我正在尝试使用高性能表参数方法 (http://www.altdevblogaday.com/2012/05/16/sql-server-high-performance-inserts/) 插入记录,我很好奇是否可以检索我插入的每条记录的标识值。

目前,答案似乎是否定的 - 我插入数据,然后检索身份值,但它们不匹配。具体来说,它们大约有 75% 的时间不匹配,并且它们不会以不可预知的方式匹配。以下是一些复制此问题的代码:

// Create a datatable with 100k rows
DataTable dt = new DataTable();
dt.Columns.Add(new DataColumn("item_id", typeof(int)));
dt.Columns.Add(new DataColumn("comment", typeof(string)));
for (int i = 0; i < 100000; i++) {
    dt.Rows.Add(new object[] { 0, i.ToString() });
}

// Insert these records and retrieve back the identity
using (SqlConnection conn = new SqlConnection("Data Source=localhost;Initial Catalog=testdb;Integrated Security=True")) {
    conn.Open();
    using (SqlCommand cmd = new SqlCommand("proc_bulk_insert_test", conn)) {
        cmd.CommandType = CommandType.StoredProcedure;

        // Adding a "structured" parameter allows you to insert tons of data with low overhead
        SqlParameter param = new SqlParameter("@mytable", SqlDbType.Structured);
        param.Value = dt;
        cmd.Parameters.Add(param);
        SqlDataReader dr = cmd.ExecuteReader();

        // Set all the records' identity values
        int i = 0;
        while (dr.Read()) {
            dt.Rows[i].ItemArray = new object[] { dr.GetInt32(0), dt.Rows[i].ItemArray[1] };
            i++;
        }
        dr.Close();
    }

    // Do all the records' ID numbers match what I received back from the database?
    using (SqlCommand cmd = new SqlCommand("SELECT * FROM bulk_insert_test WHERE item_id >= @base_identity ORDER BY item_id ASC", conn)) {
        cmd.Parameters.AddWithValue("@base_identity", (int)dt.Rows[0].ItemArray[0]);
        SqlDataReader dr = cmd.ExecuteReader();
        DataTable dtresult = new DataTable();
        dtresult.Load(dr);
    }
}

使用此 SQL 服务器脚本定义数据库:

CREATE TABLE bulk_insert_test (
    item_id int IDENTITY (1, 1) NOT NULL PRIMARY KEY,
    comment varchar(20)
)
GO

CREATE TYPE bulk_insert_table_type AS TABLE ( item_id int, comment varchar(20) )
GO

CREATE PROCEDURE proc_bulk_insert_test
    @mytable bulk_insert_table_type READONLY
AS

DECLARE @TableOfIdentities TABLE (IdentValue INT)

INSERT INTO bulk_insert_test (comment)
OUTPUT Inserted.item_id INTO @TableOfIdentities(IdentValue)
SELECT comment FROM @mytable

SELECT * FROM @TableOfIdentities

问题是:proc_bulk_insert_test 返回的值与插入原始记录的顺序不同。因此,我无法以编程方式为每条记录分配从OUTPUT 语句返回的item_id 值。

似乎唯一有效的解决方案是 SELECT 返回我刚刚插入的整个记录​​列表,但坦率地说,我更喜欢任何可以减少通过 SQL Server 网卡传输的数据量的解决方案。有没有人在检索身份值的同时对大型插入有更好的解决方案?

编辑:让我尝试进一步澄清这个问题。问题是我希望我的 C# 程序了解 SQL Server 分配给我刚刚插入的数据的标识值。顺序不是必需的;但我希望能够在 C# 中获取任意记录集,使用快速表参数方法插入它们,然后在 C# 中分配它们自动生成的 ID 号,而无需将整个表重新查询回内存。

鉴于这是一个人工测试集,我试图将其压缩为尽可能小的可读代码。让我描述一下我用来解决这个问题的方法:

  1. 在我的原始代码中,在此示例来自的应用程序中,我将使用 1500 万条单独的插入语句插入大约 1500 万行,并在每次插入后检索标识值。这行得通,但速度很慢。
  2. 我修改了代码,使用高性能表参数进行插入。然后我将在 C# 中处理所有对象,并从数据库中读回整个对象。但是,原始记录有几十列,其中包含大量 varchar 和十进制值,因此这种方法虽然速度快且有效,但网络流量非常密集。
  3. 我现在开始研究是否可以使用表参数插入,同时要求 SQL Server 只报告标识值。我尝试了scope_identity()OUTPUT,但到目前为止都没有成功。

基本上,如果 SQL Server 始终按照我提供的顺序插入记录,这个问题就会得到解决。是否可以让 SQL Server 插入记录完全按照表值参数插入中提供的顺序?

EDIT2:这种方法似乎与 Cade Roux 下面引用的非常相似:

http://www.sqlteam.com/article/using-the-output-clause-to-capture-identity-values-on-multi-row-inserts

但是,在文章中,作者使用了一个神奇的唯一值“ProductNumber”,将插入的信息从“输出”值连接到原始表值参数。如果我的表没有神奇的唯一值,我正在尝试弄清楚如何做到这一点。

【问题讨论】:

  • 您在表上有什么类型的索引.. 有没有办法执行存储过程来获取 MaxId 表示最后更新行的标识列..?我希望我能正确理解你的问题..?基本上返回@@IDENTITY
  • 我最初用 scope_identity() 尝试过这个 - 结果也是不可预测的。在 100k 行中,似乎会以错误的顺序插入 10,000 行的随机片段。
  • @DJKRAZE - 整个表定义在上面的 SQL 脚本中;我正是使用该脚本在空白数据库上创建了该表。如果您想自己测试,请随意尝试:)
  • marc_s 感谢您的澄清..

标签: c# sql-server performance


【解决方案1】:

您没有在此指定ORDER BYSELECT * FROM @TableOfIdentities,因此无法保证订单。如果您希望它们以相同的顺序发送,请对插入的数据执行 INNER JOIN,其中 ORDER BY 与发送行的顺序相匹配。

【讨论】:

  • 如果我在select * from @tableofidentities 中加入order by 子句,不会影响插入,对吗?如果 SQL Server 决定以随机顺序插入数据,如何确保身份以匹配顺序返回?
  • @TedSpence 没有。事实上,为了提高效率,INSERT 可以并行完成。假设您以已知顺序从客户端加载 TVP,假设它将成为 NATURAL_KEY 列。然后SELECT i.IdentValue FROM @TableOfIdentities AS i INNER JOIN bulk_insert_test AS d ON d.item_id = i.IdentValue ORDER BY d.NATURAL_KEY
  • @TedSpence 如果您的客户处于一个订单中(即一个数组),但没有特别可识别和可重现/可定义的订单,并且无法通过自然键引用它们,您需要取回身份到数组中的这些数据结构上,除非您将该索引器发送到数据库中以保存在表中然后返回,否则您有点不走运。
  • @TedSpence 如果你放弃批量插入,你可以控制顺序但你失去了效率。另一种加载方式是使用来自客户端的 SqlBulkCopy,但我认为它没有任何特殊的返回处理。我仍然对在客户端中更新大量身份的重要性感到有些困惑。
  • 假设我在一个月内每天收到 10,000 种产品、3000 家商店的销售数据。我的代码尝试尽可能快地批量插入这些数据,但我们希望通过使用高性能整数(或 bigint)外键引用来保留每个产品、每个商店及其日常数据之间的连接。
【解决方案2】:

您的 TVP 是一个无序集,就像一张普通的桌子一样。只有当您这样指定时,它才有顺序。您不仅无法在此处指示实际订单,而且您还只是在最后执行了 SELECT * 而没有 ORDER BY。你期望这里的顺序是什么?您已经有效地告诉 SQL Server,您不在乎。也就是说,我实现了您的代码,并且以正确的顺序恢复行没有问题。我稍微修改了该过程,以便您实际上可以分辨哪个标识值属于哪个注释:

DECLARE @TableOfIdentities TABLE (IdentValue INT, comment varchar(20))

INSERT INTO bulk_insert_test (comment)
OUTPUT Inserted.item_id, Inserted.comment 
INTO @TableOfIdentities(IdentValue, comment)
SELECT comment FROM @mytable

SELECT * FROM @TableOfIdentities

然后我使用这段代码调用它(我们不需要所有的 C#):

DECLARE @t bulk_insert_table_type;
INSERT @t VALUES(5,'foo'),(2,'bar'),(3,'zzz');
SELECT * FROM @t;

EXEC dbo.proc_bulk_insert_test @t;

结果:

1   foo
2   bar
3   zzz

如果您想确保输出按照身份分配的顺序(不一定与您的无序 TVP 具有相同的“顺序”),您可以将 ORDER BY item_id 添加到过程中的最后一个选择中。

如果您想插入到目标表中,以便您的身份值按照对您来说很重要的顺序排列,那么您有几个选择:

  • 向 TVP 添加一列并将顺序插入该列,然后使用游标按该顺序遍历行,并一次插入一个。仍然比为每一行调用整个过程更有效,恕我直言。

  • 在您的 TVP 中添加一个指示顺序的列,并在插入时使用 ORDER BY。这无法保证,但相对可靠,尤其是在您使用 MAXDOP 1 消除并行性问题时。

无论如何,您似乎在 ORDER 上放置了很多相关性。您的订单实际上意味着什么?如果您想为订单添加一些含义,则不应使用 IDENTITY 列。

【讨论】:

  • 如果我理解正确,您的解决方案要求我回读我刚刚插入的整个数据集 - 这基本上就是我当前的解决方案所做的。是否有可能只获取身份值?
  • 是的,但是身份值本身意味着什么?你打算怎么处理他们?
  • 无论如何我只添加了那些,所以你可以看到哪个身份值属于哪个表,而不是作为“解决方案”的一部分 - 我仍然认为我不明白这个问题,因为我认为有关于顺序的一些误解。
  • 我要求的是一般知识。我的应用程序经常使用标识值来匹配关系表。目前,我必须将表值参数(以获取高性能插入)与复杂的存储过程(以保留标识值和关系数据)混合。但是,我的一些应用程序必须解析 10-30 GB 的数据并将文件 A 中的记录与文件 B 中的记录匹配;我希望能够在 C# 中通过使用快速插入和检索身份号码来完成这一切。
  • 你最好根据自然的、可识别的值进行匹配,而不是依赖于任何关于顺序的假设。除非您在循环中插入单独的行,否则无法保证如何分配标识值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多