[英]Dapper Bulk Insert Returning Serial IDs
我試圖使用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:列”值“不存在”。 讀完這個問題之后 ,我想也許我必須傳遞一個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?
我注意到異常的大小與列名稱不匹配,但我確定我在表和列名稱周圍有引號,所以我不確定為什么它在“值”而不是“值”中表示例外。 只是想我會提到它,以防它以某種方式與錯誤有關,因為很容易忽略套管。
- 編輯 -
為了澄清,這是創建表的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”中插入數百個(在不久的將來可能有數千個)每秒行,所以等待這個循環迭代地將每個值發送到數據庫是很麻煩的(我假設,但是還沒有基准)耗時。 此外,我對可能會或可能不會導致其他插入的值執行額外的計算,其中我需要對“MyTable”條目的外鍵引用。
由於這些問題,我正在尋找一種替代方案,將單個語句中的所有值發送到數據庫,以減少網絡流量和處理延遲。 同樣,我還沒有對迭代方法進行基准測試......我正在尋找的是一種替代方法,它可以進行批量插入,因此我可以將兩種方法相互比較。
最終,我想出了四種不同的方法來解決這個問題。 我生成了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;
}
我感到震驚的是,這比最佳解決方案慢了3.3倍,但我希望在真實環境中會變得更糟,因為這個解決方案需要連續向服務器發送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"
的排序相"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的額外要求,我沒有選擇它,速度效益很小。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.