我正在尝试使用Dapper over Npgsql执行批量插入,返回新插入行的ID。在我的两个示例中都使用了以下insert语句:
var query = "INSERT INTO \"MyTable\" (\"Value\") VALUES (@Value) RETURNING \"ID\"";
首先,我尝试添加一个具有“Value”属性的对象数组:
var values = new[] {
new { Value = 0.0 },
new { Value = 0.5 }
};
var ids = connection.Query<int>(query, values);
但是,由于NpgsqlException失败:“错误:42703:列”值“不存在”。阅读this question之后,我想也许我必须传递一个DataTable对象而不是一个对象数组:
var dataTable = new DataTable();
dataTable.Columns.Add("Value", typeof(double));
dataTable.Rows.Add(0.0);
dataTable.Rows.Add(0.5);
var ids = connection.Query<int>(query, dataTable);
然而,这失败了完全相同的异常。如何通过Npgsql执行批量插入并从Dapper中获取生成的序列ID?
我注意到异常的大小与列名称不匹配,但我确定我在表名和列名周围都有引号,所以我不确定为什么它会说“value”而不是“Value” “在例外中。我以为我会提到它,以防它以某种方式与错误有关,因为很容易忽视套管。
- 编辑 -
为了澄清,这是创建表的SQL
CREATE TABLE "MyTable" (
"ID" SERIAL PRIMARY KEY,
"Value" DOUBLE PRECISION NOT NULL
);
使用上面定义的变量“query”和“values”,这是基于每行的代码:
var ids = new List<int>();
foreach (var valueObj in values) {
var queryParams = new DynamicParamaters();
queryParams.Add("Value", valueObj.Value);
ids.AddRange(connection.Query<int>(query, queryParams));
}
问题是我需要能够在“MyTable”中插入数百个(在不久的将来可能有数千个)每秒行,所以等待这个循环迭代地将每个值发送到数据库是很麻烦的(I假设,但还没有基准测试)耗时。此外,我对可能会或可能不会导致其他插入的值执行额外的计算,其中我需要对“MyTable”条目的外键引用。
由于这些问题,我正在寻找一种替代方法,将单个语句中的所有值发送到数据库,以减少网络流量和处理延迟。同样,我还没有对迭代方法进行基准测试......我正在寻找的是一种替代方法,它可以进行批量插入,因此我可以将这两种方法相互比较。
答案 0 :(得分:12)
最终,我想出了四种不同的方法来解决这个问题。我生成了500个随机值以插入到MyTable中,并为四种方法中的每一种计时(包括启动和回滚运行它的事务)。在我的测试中,数据库位于localhost上。但是,具有最佳性能的解决方案也只需要一次往返数据库服务器,因此我发现的最佳解决方案在部署到与数据库不同的服务器时仍然可以胜过替代方案。
请注意,变量connection
和transaction
在以下代码中使用,并假定为有效的Npgsql数据对象。另请注意,符号 Nx较慢表示操作花费的时间等于最佳解决方案乘以 N 。
方法#1(1,494ms =慢18.7倍):将数组展开为单个参数
public List<MyTable> InsertEntries(double[] entries)
{
// Create a variable used to dynamically build the query
var query = new StringBuilder(
"INSERT INTO \"MyTable\" (\"Value\") VALUES ");
// Create the dictionary used to store the query parameters
var queryParams = new DynamicParameters();
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Add a unique parameter for each id
var paramIdx = 0;
foreach (var entry in result)
{
var paramName = string.Format("value{1:D6}", paramIdx);
if (0 < paramIdx++) query.Append(',');
query.AppendFormat("(:{0})", paramName);
queryParams.Add(paramName, entry.Value);
}
query.Append(" RETURNING \"ID\"");
// Execute the query, and store the ids
var ids = connection.Query<int>(query, queryParams, transaction);
ids.ForEach((id, i) => result[i].ID = id);
// Return the result
return result;
}
我真的不确定为什么这是最慢的,因为它只需要一次往返数据库,但确实如此。
方法#2(267ms =慢3.3倍):标准循环迭代
public List<MyTable> InsertEntries(double[] entries)
{
const string query =
"INSERT INTO \"MyTable\" (\"Value\") VALUES (:val) RETURNING \"ID\"";
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Add each entry to the database
foreach (var entry in result)
{
var queryParams = new DynamicParameters();
queryParams.Add("val", entry.Value);
entry.ID = connection.Query<int>(
query, queryParams, transaction);
}
// Return the result
return result;
}
我感到震惊的是,这只比最佳解决方案慢2倍,但我希望在真实环境中会变得更糟,因为这个解决方案需要连续向服务器发送500条消息。但是,这也是最简单的解决方案。
方法#3(223ms =慢2.8倍):异步循环迭代
public List<MyTable> InsertEntries(double[] entries)
{
const string query =
"INSERT INTO \"MyTable\" (\"Value\") VALUES (:val) RETURNING \"ID\"";
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Add each entry to the database asynchronously
var taskList = new List<Task<IEnumerable<int>>>();
foreach (var entry in result)
{
var queryParams = new DynamicParameters();
queryParams.Add("val", entry.Value);
taskList.Add(connection.QueryAsync<int>(
query, queryParams, transaction));
}
// Now that all queries have been sent, start reading the results
for (var i = 0; i < result.Count; ++i)
{
result[i].ID = taskList[i].Result.First();
}
// Return the result
return result;
}
这越来越好,但仍然不是最优的,因为我们只能排队与线程池中可用线程一样多的插入。然而,这几乎与非线程方法一样简单,因此它是速度和可读性之间的良好折衷。
方法#4(134ms =慢1.7倍):批量插入
这种方法要求在运行下面的代码段之前定义以下Postgres SQL:
CREATE TYPE "MyTableType" AS (
"Value" DOUBLE PRECISION
);
CREATE FUNCTION "InsertIntoMyTable"(entries "MyTableType"[])
RETURNS SETOF INT AS $$
DECLARE
insertCmd TEXT := 'INSERT INTO "MyTable" ("Value") '
'VALUES ($1) RETURNING "ID"';
entry "MyTableType";
BEGIN
FOREACH entry IN ARRAY entries LOOP
RETURN QUERY EXECUTE insertCmd USING entry."Value";
END LOOP;
END;
$$ LANGUAGE PLPGSQL;
相关代码:
public List<MyTable> InsertEntries(double[] entries)
{
const string query =
"SELECT * FROM \"InsertIntoMyTable\"(:entries::\"MyTableType\")";
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Convert each entry into a Postgres string
var entryStrings = result.Select(
e => string.Format("({0:E16})", e.Value).ToArray();
// Create a parameter for the array of MyTable entries
var queryParam = new {entries = entryStrings};
// Perform the insert
var ids = connection.Query<int>(query, queryParam, transaction);
// Assign each id to the result
ids.ForEach((id, i) => result[i].ID = id);
// Return the result
return result;
}
我对这种方法有两个问题。首先,我必须硬编码MyTableType成员的顺序。如果该顺序发生变化,我必须修改此代码以匹配。第二个是我必须在将所有输入值发送到postgres之前将其转换为字符串(在实际代码中,我有多个列,所以我不能只更改数据库函数的签名以获取双倍precision [],除非我传入N个数组,其中N是MyTableType上的字段数。)
尽管存在这些陷阱,但这种情况越来越接近理想状态,只需要往返数据库。
- 开始编辑 -
从最初的帖子开始,我提出了四种额外的方法,这些方法都比上面列出的更快。我修改了 Nx较慢的数字,以反映下面的最快的方法。
方法#5(105ms =慢1.3倍)与#4相同,没有动态查询
此方法与方法#4 之间的唯一区别是对“InsertIntoMyTable”函数的以下更改:
CREATE FUNCTION "InsertIntoMyTable"(entries "MyTableType"[])
RETURNS SETOF INT AS $$
DECLARE
entry "MyTableType";
BEGIN
FOREACH entry IN ARRAY entries LOOP
RETURN QUERY INSERT INTO "MyTable" ("Value")
VALUES (entry."Value") RETURNING "ID";
END LOOP;
END;
$$ LANGUAGE PLPGSQL;
除了方法#4 的问题之外,其缺点是,在生产环境中,“MyTable”被分区。使用这种方法,每个目标分区需要一个方法。
方法#6(89ms = 1.1x较慢):使用数组参数插入语句
public List<MyTable> InsertEntries(double[] entries)
{
const string query =
"INSERT INTO \"MyTable\" (\"Value\") SELECT a.* FROM " +
"UNNEST(:entries::\"MyTableType\") a RETURNING \"ID\"";
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Convert each entry into a Postgres string
var entryStrings = result.Select(
e => string.Format("({0:E16})", e.Value).ToArray();
// Create a parameter for the array of MyTable entries
var queryParam = new {entries = entryStrings};
// Perform the insert
var ids = connection.Query<int>(query, queryParam, transaction);
// Assign each id to the result
ids.ForEach((id, i) => result[i].ID = id);
// Return the result
return result;
}
唯一的缺点是与方法#4 的第一个问题相同。也就是说,它将实现与"MyTableType"
的排序相结合。尽管如此,我发现这是我的第二个最喜欢的方法,因为它非常快,并且不需要任何数据库功能才能正常工作。
方法#7(80ms =非常慢):与#1相同,但没有参数
public List<MyTable> InsertEntries(double[] entries)
{
// Create a variable used to dynamically build the query
var query = new StringBuilder(
"INSERT INTO \"MyTable\" (\"Value\") VALUES");
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Add each row directly into the insert statement
for (var i = 0; i < result.Count; ++i)
{
entry = result[i];
query.Append(i == 0 ? ' ' : ',');
query.AppendFormat("({0:E16})", entry.Value);
}
query.Append(" RETURNING \"ID\"");
// Execute the query, and store the ids
var ids = connection.Query<int>(query, null, transaction);
ids.ForEach((id, i) => result[i].ID = id);
// Return the result
return result;
}
这是我最喜欢的方法。它只比最快的慢一点(即使有4000条记录,它仍然在1秒内运行),但不需要特殊的数据库功能或类型。我唯一不喜欢的是我必须对双精度值进行字符串化,只是由Postgres再次解析。最好以二进制形式发送值,这样它们就占用了8个字节而不是我为它们分配的大量20个字节。
方法#8(80ms):与#5相同,但在纯sql中
此方法与方法#5 之间的唯一区别是对“InsertIntoMyTable”函数的以下更改:
CREATE FUNCTION "InsertIntoMyTable"(
entries "MyTableType"[]) RETURNS SETOF INT AS $$
INSERT INTO "MyTable" ("Value")
SELECT a.* FROM UNNEST(entries) a RETURNING "ID";
$$ LANGUAGE SQL;
这种方法与#5一样,每个“MyTable”分区需要一个函数。这是最快的,因为可以为每个函数生成一次查询计划,然后重复使用。在其他方法中,必须解析,然后计划,然后执行查询。尽管这是最快的,但由于数据库方面对方法#7 的额外要求,我没有选择它,速度效益很小。