I've started to use the QueryStore to monitor my App and one of the things I noticed is the unexpected high memory usage for what I thought a simple operation:
Which would mean the statement uses almost 600MB of RAM each time it is executed?
The RowCount for each execution in that timeframe is a lot less than 100. The Statement itself is run every 5 seconds.
The Table is partitioned and based on a ColumnStore Index and has no other Indexes or Primary Key/Identity and has around 750k Rows:
CREATE TABLE [DataLink].[LogEntry](
[AppInstanceId] [bigint] NOT NULL,
[LoggedOnUtc] [datetime2](7) NOT NULL,
[CategoryName] [nvarchar](256) NOT NULL,
[EventCode] [int] NOT NULL,
[EventName] [nvarchar](256) NULL,
[LogLevel] [int] NOT NULL,
[ScopeJson] [nvarchar](max) NULL,
[StateJson] [nvarchar](max) NULL,
[ExceptionJson] [nvarchar](max) NULL,
[Message] [nvarchar](max) NULL
) ON [PSCH_Logging_DataLink_LogEntry_Daily7Of9]([LoggedOnUtc])
CREATE CLUSTERED COLUMNSTORE INDEX [CIX_LogEntry]
ON [DataLink].[LogEntry] WITH (DROP_EXISTING = OFF, COMPRESSION_DELAY = 0, DATA_COMPRESSION = COLUMNSTORE)
ON [PSCH_Logging_DataLink_LogEntry_Daily7Of9]([LoggedOnUtc])
The Code which triggers the inserts:
using var conn = connInfo.Main.GetConnection(DatabaseLoginType.User);
await conn.OpenAsync(ct).CAf();
using var sqlBulkCopy = new SqlBulkCopy((SqlConnection)conn, SqlBulkCopyOptions.CheckConstraints | SqlBulkCopyOptions.FireTriggers, null);
foreach(var toWriteItemGroup in toWriteItems.GroupBy(x => x.SchemaName)) {
...
dataReader.Init(toWriteItemGroup, tableInfo.ColumnMappings.Length);
sqlBulkCopy.DestinationTableName = $"{schemaName}.LogEntry";
sqlBulkCopy.ColumnMappings.Clear();
for(int i = 0; i < tableInfo.ColumnMappings.Length; i++) sqlBulkCopy.ColumnMappings.Add(i, tableInfo.ColumnMappings[i]);
await sqlBulkCopy.WriteToServerAsync(dataReader, ct).CAf();
...
}
Any idea why the memory usage is so high and what I could do to fix that?
Edit4
I did some tests by changing and manually compiling Microsoft.Data.SqlClient. The changes I made included adding ROWS_PER_BATCH and/or KILOBYTES_PER_BATCH to the with options of the "insert bulk" statement. None of the options changed how much memory was used, the former changed the row count estimate though: https://www.brentozar.com/pastetheplan/?id=HkKjc9HIC
It doesn't seem like "insert bulk" can be optimized for low row counts.
Edit3
Here is a short example with which I can reproduce the issue.
It contains a script "Script.sql" which needs to be executed first to setup the table and add some data. After that run the program with "dotnet run" (or use an IDE).
Since I can't upload files here, I've uploaded it to github gist: https://gist.github.com/DvdKhl/d042ed05e3237136265295cb39ecb4f4
The Script will:
- Create a view, which shows PartitionInfo of the table
- Create (or recreate) the Table and it configuration
- Insert 700k rows (100k per partition)
- Rebuild the index
- Output the Partition Info
- Commented out is
- Event Session (Extended Event) to capture the query plan
- Another insert Statement
- Cleanup
The Program will:
- Open a connection on "localhost" for the DB "main"
- Create a Dummy DataReader (Change Count to change the inserted row count)
- Setup SqlBulkCopy like above
- Call WriteToServerAsync to insert the rows
This results in the following queryplan: https://www.brentozar.com/pastetheplan/?id=B1v_8bGLC
Edit2
Like Suggested by Denis Rubashkin I've set BatchSize and an order hint:
sqlBulkCopy.BatchSize = toWriteItemGroup.Count();
sqlBulkCopy.ColumnOrderHints.Add("LoggedOnUtc", SortOrder.Ascending);
BatchSize doesn't seem to change anything (the estimate stays the same).
It looks like ROWS_PER_BATCH is not used even though sqlBulkCopy.BatchSize is set in the code, which might be the main problem.
When the Order hint is added, the query does not show up in the QueryStore.
Using extended events to get the queryplan shows an "Excessive Grant" Warning though. So I'm not sure it helped.
The option KILOBYTES_PER_BATCH looks interesting but it does not seem like I can set it in the C# code.
WithOrderHint / WithoutOrderHint (Different table but exactly the same problem)
Edit:
Queryplan: https://www.brentozar.com/pastetheplan/?id=SJGpBktH0