diff --git a/DuckDB.NET.Benchmarks/AppenderBenchmark.cs b/DuckDB.NET.Benchmarks/AppenderBenchmark.cs
new file mode 100644
index 0000000..36f918e
--- /dev/null
+++ b/DuckDB.NET.Benchmarks/AppenderBenchmark.cs
@@ -0,0 +1,50 @@
+using BenchmarkDotNet.Attributes;
+using DuckDB.NET.Data;
+
+namespace DuckDB.NET.Benchmarks;
+
+[MemoryDiagnoser]
+public class AppenderBenchmark
+{
+ private DuckDBConnection connection = null!;
+
+ [Params(1_000_000)]
+ public int RowCount { get; set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ connection = new DuckDBConnection("DataSource=:memory:");
+ connection.Open();
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ connection.Dispose();
+ }
+
+ [IterationSetup]
+ public void IterationSetup()
+ {
+ using var command = connection.CreateCommand();
+ command.CommandText = "DROP TABLE IF EXISTS bench; CREATE TABLE bench (a INTEGER, b BIGINT, c DOUBLE, d BOOLEAN);";
+ command.ExecuteNonQuery();
+ }
+
+ [Benchmark]
+ public void AppendRows()
+ {
+ using var appender = connection.CreateAppender("bench");
+
+ for (var i = 0; i < RowCount; i++)
+ {
+ appender.CreateRow()
+ .AppendValue(i)
+ .AppendValue((long)i)
+ .AppendValue((double)i)
+ .AppendValue(i % 2 == 0)
+ .EndRow();
+ }
+ }
+}
diff --git a/DuckDB.NET.Benchmarks/Benchmarks.csproj b/DuckDB.NET.Benchmarks/Benchmarks.csproj
new file mode 100644
index 0000000..319ca74
--- /dev/null
+++ b/DuckDB.NET.Benchmarks/Benchmarks.csproj
@@ -0,0 +1,34 @@
+
+
+
+ Exe
+ net10.0
+ net10.0
+ enable
+ enable
+ true
+ true
+ Full
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ PreserveNewest
+ runtimes\%(RecursiveDir)\%(FileName)%(Extension)
+
+
+
+
diff --git a/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs b/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs
new file mode 100644
index 0000000..a253796
--- /dev/null
+++ b/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs
@@ -0,0 +1,45 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace DuckDB.NET.Benchmarks;
+
+internal static class NativeLibraryLoader
+{
+ [ModuleInitializer]
+ public static void Init()
+ {
+ if (GetRid() is not { } rid)
+ {
+ return;
+ }
+
+ _ = NativeLibrary.TryLoad(Path.Join("runtimes", rid, "native", "duckdb"), Assembly.GetExecutingAssembly(), DllImportSearchPath.AssemblyDirectory, out _) ||
+ NativeLibrary.TryLoad(Path.Join("runtimes", rid, "native", "libduckdb"), Assembly.GetExecutingAssembly(), DllImportSearchPath.AssemblyDirectory, out _);
+ }
+
+ private static string? GetRid()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return Environment.Is64BitProcess ? "win-x64" : "win-x86";
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return RuntimeInformation.ProcessArchitecture switch
+ {
+ Architecture.X64 => "linux-x64",
+ Architecture.Arm64 => "linux-arm64",
+ _ => null,
+ };
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return "osx";
+ }
+
+ return null;
+ }
+}
diff --git a/DuckDB.NET.Benchmarks/Program.cs b/DuckDB.NET.Benchmarks/Program.cs
new file mode 100644
index 0000000..4e31a4d
--- /dev/null
+++ b/DuckDB.NET.Benchmarks/Program.cs
@@ -0,0 +1,15 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.Toolchains.InProcess.Emit;
+using DuckDB.NET.Benchmarks;
+
+// The repo's Directory.Build.props renames the output assembly to DuckDB.NET.Benchmarks
+// while the project file stays Benchmarks.csproj, so BenchmarkDotNet's default toolchain
+// can't locate the csproj. Run in-process to avoid the separate build/spawn step.
+var config = DefaultConfig.Instance
+ .AddJob(Job.Default.WithToolchain(InProcessEmitToolchain.Instance));
+
+BenchmarkSwitcher
+ .FromTypes([typeof(AppenderBenchmark)])
+ .Run(args, config);
diff --git a/DuckDB.NET.Data/DuckDBAppender.cs b/DuckDB.NET.Data/DuckDBAppender.cs
index d32e3ca..720d225 100644
--- a/DuckDB.NET.Data/DuckDBAppender.cs
+++ b/DuckDB.NET.Data/DuckDBAppender.cs
@@ -17,6 +17,7 @@ public class DuckDBAppender : IDisposable
private readonly DuckDBLogicalType[] logicalTypes;
private readonly DuckDBDataChunk dataChunk;
private readonly VectorDataWriterBase[] vectorWriters;
+ private DuckDBAppenderRow? currentRow;
internal DuckDBAppender(Native.DuckDBAppender appender, string qualifiedTableName)
{
@@ -60,7 +61,19 @@ public IDuckDBAppenderRow CreateRow()
}
rowCount++;
- return new DuckDBAppenderRow(qualifiedTableName, vectorWriters, rowCount - 1, dataChunk, nativeAppender);
+
+ // A single row is filled and ended before the next CreateRow() call, so reuse one instance
+ // instead of allocating a DuckDBAppenderRow per row.
+ if (currentRow is null)
+ {
+ currentRow = new DuckDBAppenderRow(qualifiedTableName, vectorWriters, rowCount - 1, dataChunk, nativeAppender);
+ }
+ else
+ {
+ currentRow.Reset(rowCount - 1);
+ }
+
+ return currentRow;
}
public void Clear()
diff --git a/DuckDB.NET.Data/DuckDBAppenderRow.cs b/DuckDB.NET.Data/DuckDBAppenderRow.cs
index 186ab69..1881cbe 100644
--- a/DuckDB.NET.Data/DuckDBAppenderRow.cs
+++ b/DuckDB.NET.Data/DuckDBAppenderRow.cs
@@ -7,7 +7,7 @@ public class DuckDBAppenderRow : IDuckDBAppenderRow
private int columnIndex = 0;
private readonly string qualifiedTableName;
private readonly VectorDataWriterBase[] vectorWriters;
- private readonly ulong rowIndex;
+ private ulong rowIndex;
private readonly DuckDBDataChunk dataChunk;
private readonly Native.DuckDBAppender nativeAppender;
@@ -21,6 +21,18 @@ internal DuckDBAppenderRow(string qualifiedTableName, VectorDataWriterBase[] vec
this.nativeAppender = nativeAppender;
}
+ ///
+ /// Re-targets this row instance at a new row index so the appender can reuse a single
+ /// instead of allocating one per row. The table name, vector
+ /// writers, data chunk and native appender are stable for the lifetime of the appender, so only
+ /// the row index and column cursor need to be reset.
+ ///
+ internal void Reset(ulong rowIndex)
+ {
+ this.rowIndex = rowIndex;
+ columnIndex = 0;
+ }
+
public void EndRow()
{
if (columnIndex < vectorWriters.Length)
diff --git a/DuckDB.NET.slnx b/DuckDB.NET.slnx
index 715e640..b40a6d6 100644
--- a/DuckDB.NET.slnx
+++ b/DuckDB.NET.slnx
@@ -1,4 +1,5 @@
+