When working with PostgreSQL and .NET applications, choosing the right data insertion strategy can significantly impact performance, especially when dealing with large datasets. This article presents a comprehensive performance analysis comparing different PostgreSQL insertion methods using Npgsql, including single inserts, batch inserts, bulk copy operations, and prepared statements.
public class TestRecord
{
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public decimal Salary { get; set; }
public ProfileData ProfileData { get; set; }
}
public class ProfileData
{
public string Department { get; set; }
public string Position { get; set; }
public List<string> Skills { get; set; }
public Dictionary<string, object> Preferences { get; set; }
public DateTime LastLogin { get; set; }
public bool IsActive { get; set; }
}
| Method | Records/sec | Total Time (1M records) | Performance Ratio |
|---|---|---|---|
| Single Inserts | 3,786 | 264 seconds | 1x (baseline) |
| Prepared Statements | 4,986 | 200 seconds | 1.3x |
| Batch Inserts | 44,651 | 22 seconds | 11.8x |
| 🏆 Bulk Copy (COPY) | 119,246 | 8 seconds | 31.5x |
foreach (var record in testData)
{
await using var command = new NpgsqlCommand(@"
INSERT INTO performance_test (name, email, age, salary, profile_data)
VALUES (@name, @email, @age, @salary, @profile_data::jsonb)", connection);
command.Parameters.AddWithValue("@name", record.Name);
command.Parameters.AddWithValue("@email", record.Email);
command.Parameters.AddWithValue("@age", record.Age);
command.Parameters.AddWithValue("@salary", record.Salary);
command.Parameters.AddWithValue("@profile_data", JsonSerializer.Serialize(record.ProfileData));
await command.ExecuteNonQueryAsync();
}
await using var command = new NpgsqlCommand(@"
INSERT INTO performance_test (name, email, age, salary, profile_data)
VALUES (@name, @email, @age, @salary, @profile_data::jsonb)", connection);
await command.PrepareAsync();
foreach (var record in testData)
{
command.Parameters["@name"].Value = record.Name;
command.Parameters["@email"].Value = record.Email;
command.Parameters["@age"].Value = record.Age;
command.Parameters["@salary"].Value = record.Salary;
command.Parameters["@profile_data"].Value = JsonSerializer.Serialize(record.ProfileData);
await command.ExecuteNonQueryAsync();
}
const int batchSize = 1000;
for (int i = 0; i < testData.Count; i += batchSize)
{
var batch = testData.Skip(i).Take(batchSize).ToList();
var valueSets = string.Join(",", batch.Select((record, index) =>
$"(@name{index}, @email{index}, @age{index}, @salary{index}, @profile_data{index}::jsonb)"));
await using var command = new NpgsqlCommand($@"
INSERT INTO performance_test (name, email, age, salary, profile_data)
VALUES {valueSets}", connection);
// Add parameters for each record in the batch
for (int j = 0; j < batch.Count; j++)
{
var record = batch[j];
command.Parameters.AddWithValue($"@name{j}", record.Name);
command.Parameters.AddWithValue($"@email{j}", record.Email);
command.Parameters.AddWithValue($"@age{j}", record.Age);
command.Parameters.AddWithValue($"@salary{j}", record.Salary);
command.Parameters.AddWithValue($"@profile_data{j}", JsonSerializer.Serialize(record.ProfileData));
}
await command.ExecuteNonQueryAsync();
}
using var writer = await connection.BeginBinaryImportAsync(
"COPY performance_test (name, email, age, salary, profile_data) FROM STDIN (FORMAT BINARY)");
foreach (var record in testData)
{
await writer.StartRowAsync();
await writer.WriteAsync(record.Name);
await writer.WriteAsync(record.Email);
await writer.WriteAsync(record.Age);
await writer.WriteAsync(record.Salary);
await writer.WriteAsync(JsonSerializer.Serialize(record.ProfileData), NpgsqlDbType.Jsonb);
}
await writer.CompleteAsync();
The COPY command significantly outperforms all other methods:
✓
31.5x faster
than single inserts
✓
2.7x faster
than bulk inserts
✓
24x faster
than prepared statements
Working with JSONB data requires special consideration:
Serialization
Objects must be serialized to JSON strings
Type Casting
Use ::jsonb casting in SQL or NpgsqlDbType.Jsonb in binary operations
Binary Format
COPY command with binary format provides best performance for JSONB
⚡
Single Inserts
Low memory usage, poor performance
⚡
Bulk Inserts
Moderate memory usage, good performance
⚡
COPY Command
Higher memory usage, excellent performance
The performance analysis clearly demonstrates that PostgreSQL's COPY command is the optimal choice for bulk data insertion, offering 31.5x better performance than single inserts. However, the choice of insertion method should be based on your specific requirements:
Understanding these performance characteristics and implementing the appropriate method for your use case can significantly improve your application's data insertion performance and overall user experience.
This analysis was conducted using .NET 8, Npgsql driver, and PostgreSQL. Results may vary based on hardware configuration, network conditions, and database settings.
🏆 Winner: PostgreSQL COPY command delivers exceptional performance for bulk data insertion, making it the optimal choice for large-scale data operations.
Access the complete source code for all PostgreSQL insertion methods, performance benchmarks, test data generation, and detailed implementation examples used in this analysis.
View Source Code