[Perf] Bypass SqlBulkCopy Graph Column Mapping if Not Copying Graph Tables - #4535
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves SqlBulkCopy performance by avoiding SQL Graph column-alias resolution work (temp table creation + sys catalog scans + extra resultset parsing) when the bulk copy operation isn’t using SQL Graph alias destination columns ($edge_id, $to_id, $from_id, $node_id). This targets a regression observed in perf suites where bulk-copy setup/teardown dominated runtime.
Changes:
- Conditionally generate/execute the
#Column_Aliasestemp table + population queries only when destination column mappings reference SQL Graph alias names. - Make cached-metadata reuse aware of whether the column-alias resultset is required/present, and guard alias-application logic on resultset presence.
- Update ManualTests statistics expectations to reflect the reduced query/DDL/DML work in the common non-graph pathway.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs | Updates expected SqlConnection statistics counters to match the optimized non-graph bulk copy behavior. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs | Bypasses graph alias temp-table/query generation unless needed; adds result-count checks for cached metadata and alias resultset handling. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs:156
- The comment implies the initial query may return the column-alias result set based on destination table shape, but the new behavior is actually conditional on whether column mappings reference SQL Graph alias names (see ShouldResolveColumnAliases()). Clarifying this prevents confusion when reading stats/result-set expectations.
// The initial query will return three tables, and may return a fourth for column aliases.
|
I think test failures are related to changes here, please take a look. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4535 +/- ##
==========================================
- Coverage 64.78% 62.99% -1.79%
==========================================
Files 288 283 -5
Lines 44418 67467 +23049
==========================================
+ Hits 28774 42499 +13725
- Misses 15644 24968 +9324
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
edwardneal
left a comment
There was a problem hiding this comment.
A few comments - one nit and one slightly more substantial.
Do you have a before/after benchmark? I agree that making a change to the SQL statement would improve performance, but if something isn't a Graph table then the result set should have zero rows and have no direct performance impact.
|
@edwardneal we do have a before/after benchmark and this does make a marked improvement (ie, restores performance to pre-graph column mapping levels). Unfortunately, it's an internal benchmark dashboard. But, trust me, it's way better 😃 |
|
@priyankatiwari08 feedback has been addressed |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs:530
- When alias resolution is not needed (_localColumnMappingsResolveAliases == false), the initial query still creates #Column_Aliases and always returns the column-alias result set (empty). This keeps an extra result set/SELECT in the metadata round-trip, which can still show up in connection statistics and reduces the benefit of bypassing graph-alias work. Consider omitting both the temp-table creation and the final SELECT/DROP when alias resolution is not required; AnalyzeTargetAndCreateUpdateBulkCommand already guards on internalResults.Count.
string selectColumnAliasesQuery = """
SELECT [Canonical_Column_Name], [Aliased_Column_Name]
FROM #Column_Aliases
ORDER BY [Canonical_Column_Id] ASC
DROP TABLE #Column_Aliases
""";
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs:2401
- WriteRowSourceToServerCommon adds an extra foreach over SqlBulkCopyColumnMappingCollection just to reset MappedDestinationColumn. Since SqlBulkCopyColumnMappingCollection derives from CollectionBase, this enumerates via a non-generic IEnumerator and can allocate; it also doubles the traversal immediately before another foreach. This can be folded into a single indexed loop to avoid the extra enumeration and keep the per-operation overhead lower.
foreach (SqlBulkCopyColumnMapping bulkCopyColumn in _localColumnMappings)
{
bulkCopyColumn.MappedDestinationColumn = null;
}
|
Can you link the sqlclient-perf-pr run that shows the improvements against main? |
mdaigle
left a comment
There was a problem hiding this comment.
Looks good. Would still like to see the perf run numbers :)
Adds coverage for milestone items that closed after the initial draft: - #4529 leaked-connection reclamation in ChannelDbConnectionPool - #4535 SqlBulkCopy graph column alias mapping bypass - #4521 / #4496 Entra ID tenant parsing for multi-segment STSURL authorities - #4540 async key store provider APIs in the AzureKeyVaultProvider Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c485150e-f46e-4c55-8d06-e0491a10e7e8
* Add release notes for 7.1.0-preview3 Adds release notes for Microsoft.Data.SqlClient 7.1.0-preview3 and its four aligned companion packages, updates the per-version README index tables, and adds the corresponding CHANGELOG entry. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c7425f3e-f6b8-439b-b74f-e25f8406fbf6 * Capture remaining closed 7.1.0-preview3 milestone items in release notes Adds coverage for milestone items that closed after the initial draft: - #4529 leaked-connection reclamation in ChannelDbConnectionPool - #4535 SqlBulkCopy graph column alias mapping bypass - #4521 / #4496 Entra ID tenant parsing for multi-segment STSURL authorities - #4540 async key store provider APIs in the AzureKeyVaultProvider Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c485150e-f46e-4c55-8d06-e0491a10e7e8 * Add #4439 and #4445 to 7.1.0-preview3 release notes Both are user-facing fixes merged into the preview3 milestone that were not yet captured in the release notes or CHANGELOG: - #4439: DateOnly values in sql_variant TVP columns were sent as datetime instead of date, overflowing for values outside the datetime range. - #4445: ServerCertificate pin validation was skipped when the platform reported no TLS policy errors, and an unloadable certificate file fell back to host-name validation instead of failing closed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8c691319-4c66-40f6-a88e-b453937052f1 * Add #4474 to 7.1.0-preview3 cross-platform build notes PR #4474 (Remove OS-Specific Builds) removed OS-specific build targets and output paths, and rewrote the MDS nuspec to source a single OS-agnostic assembly for both the win and unix runtime folders. Verified the nuspec change is src-path-only: all 52 file entries have identical target= values before and after, so the produced package layout is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8c691319-4c66-40f6-a88e-b453937052f1 * Clarify #4536 allocation fix applies to the default async read path Addresses review feedback on PR #4565: the PacketData node-reuse fix is not gated behind UseCompatibilityAsyncBehaviour or any other AppContext switch, and the perf validation was measured on the default path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8c691319-4c66-40f6-a88e-b453937052f1 * Scope preview3 release notes to customer-facing changes Remove entries with no customer-visible effect, and reframe entries that led with implementation detail rather than customer impact. Removed: - #3862 ConnectionCapabilities consolidation - internal refactor; the GetSchema("DataTypes") fix it enables is deferred to a later PR. - #3700/#3741 SSRP scaffolding - adds no parsing code and no behavior change. - #4517 CodeQL findings - PKCS#1 half is suppression comments for declared false positives; the SHA-1 removal is a no-op in practice. - #4421 Extensions.Azure APIScan remediation - no public API change and managed-identity behavior preserved exactly; the removed assignment was already a no-op. Extensions.Azure now has no Changed section. Promoted: - #4504 counter fixes moved from a pool V2 sub-bullet into Fixed. These affect the default pool that customers use without opting in, and the previous text understated them as two fixes limited to Count. Reframed: - Cross-platform build collapsed to the one customer-visible outcome (trimming on Linux/macOS); package contents were always unchanged. - #4528 now leads with the allocation regression rather than call-site count. - AKV cache fixes now lead with symptoms (unbounded signature cache growth, duplicate CryptographyClient per key) rather than GetOrCreate/GetOrAdd. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8c691319-4c66-40f6-a88e-b453937052f1 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c7425f3e-f6b8-439b-b74f-e25f8406fbf6 Copilot-Session: c485150e-f46e-4c55-8d06-e0491a10e7e8 Copilot-Session: 8c691319-4c66-40f6-a88e-b453937052f1
Description
This addresses a performance regression in the DataTypeReader / Async perf suites, indirectly.
In #3677, SQL Graph column alias mapping support was added. While this works great for SQL Graph tables, it was a bit over eager and invoked the entire graph table mapping behavior even when the tables involved did not contain SQL Graph tables. Thus, every bulk copy implicitly generated mapping tables, etc. This dramatically decreased perf in scenarios where simple tables were being bulk copied.
To resolve this, we introduce a mechanism to bypass the graph column alias table generation when the table does not contain any of the graph alias columns (
$edge_id,$to_id,$from_id,$node_id). This returns performance in the DataTypeReader/Async perf suites to pre-#3677 levels.It is worth nothing that this was discovered as an artifact of the way the DataTypeReader/Async perf suites are implemented. Although the suite aims to verify perf of reading various data types from a SqlDataReader, majority of the time spent running the test is spent doing setup and teardown steps - ie, SqlBulkCopy of data into the test table. So although this change doesn't actually impact the SqlDataReader performance, it does resolve the perf suite regression, and improve perf of SqlBulkCopy in the most common pathways.
🤖
Issues
N/A
Testing
Local comparison of perf suite before and after these changes suggest a significant improvement.