{"id":233210,"date":"2026-10-08T00:25:06","date_gmt":"2026-10-08T07:25:06","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/java\/?p=233210"},"modified":"2026-10-08T01:31:04","modified_gmt":"2026-10-08T08:31:04","slug":"faster-by-design-performance-engineering-in-the-microsoft-jdbc-driver-for-sql-server","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/java\/faster-by-design-performance-engineering-in-the-microsoft-jdbc-driver-for-sql-server\/","title":{"rendered":"Faster by Design: Performance engineering in the Microsoft JDBC Driver for SQL Server"},"content":{"rendered":"<p class=\"isSelectedEnd\">A database driver sits on the critical path of many applications. When a frequently executed path performs unnecessary work, even a small amount of overhead can compound across millions of operations.<\/p>\n<p class=\"isSelectedEnd\">Over the past several releases, we have systematically profiled frequently executed paths across the Microsoft JDBC Driver for SQL Server, identifying opportunities to improve efficiency without changing application behavior. What began with ResultSet processing expanded into bulk copy operations, parameter metadata, Always Encrypted, and authentication.<\/p>\n<p class=\"isSelectedEnd\">The work followed a consistent principle: <strong>measure where the cost is, understand why it exists, and remove unnecessary work while preserving correctness and compatibility.<\/strong><\/p>\n<p class=\"isSelectedEnd\">We focused on several critical execution paths:<\/p>\n<ul data-spread=\"false\">\n<li><strong>ResultSet:<\/strong> More efficient value processing and reduced decoding overhead<\/li>\n<li><strong>Memory:<\/strong> Reduced allocation pressure across frequently executed paths<\/li>\n<li><strong>Bulk copy:<\/strong> More efficient execution and broader use of the bulk path<\/li>\n<li><strong>Always Encrypted:<\/strong> More effective reuse of cached key material<\/li>\n<li><strong>Authentication:<\/strong> Reduced contention in highly concurrent workloads<\/li>\n<\/ul>\n<h3><strong>Faster ResultSet Processing<\/strong><\/h3>\n<p>The investigation began with customer workloads returning large, sparse result sets containing a mix of SQL Server data types. Profiling showed that network I\/O was often not the primary bottleneck. Instead, significant cost came from allocations, temporary objects, and repeated work while materializing values from the TDS stream.<\/p>\n<p>The JDBC driver uses lazy decoding, keeping values in their wire representation until applications request them through getter methods. This model supports adaptive buffering, streaming access, Always Encrypted, server cursors, and updatable result sets. Rather than redesigning this architecture, we focused on shortening the existing hot paths and eliminating work that was not necessary to produce the requested value.<\/p>\n<ul>\n<li><strong>Early NULL detection:<\/strong> SQL Server exposes NULL information through the NBCROW bitmap before value materialization begins. We moved the NULL check earlier in the path, allowing a known NULL value to return immediately without initializing decoding machinery or processing a payload whose result was already known. For sparse result sets, this avoids repeating unnecessary decoding work across NULL columns.<\/li>\n<li><strong>Lower allocation pressure:<\/strong> Profiling showed that short-lived decoder objects were repeatedly created during ResultSet traversal. Where decoder state could safely be reused, we now reuse it rather than recreate it for each value. This reduces transient allocations and associated garbage-collection work while preserving existing decoding behavior.<\/li>\n<li><strong>Primitive-first numeric and temporal decoding:<\/strong> Common numeric, MONEY, and temporal values could pass through intermediate representations before reaching their final JDBC form. The updated implementation uses a more direct, primitive-first path for common values while retaining the existing paths for values that require arbitrary precision. This shortens the path from the TDS representation to the value returned to the application.<\/li>\n<li><strong>Streamlined string retrieval:<\/strong> Ordinary character values do not require the same processing path as large streaming values. We simplified the common <code>getString()<\/code> path through reusable buffers and more direct value construction while preserving existing behavior for large objects, streaming access, and adaptive buffering.<\/li>\n<li><strong>Lean hot paths:<\/strong> Profiling also uncovered smaller costs around packet-boundary handling, row-state initialization, and diagnostic logging checks. These operations are individually inexpensive, but their frequency makes them significant on hot paths. Removing unnecessary work in these areas complements the larger decoding and allocation improvements.<\/li>\n<\/ul>\n<p>Together, these changes shorten common ResultSet execution paths by reducing unnecessary decoding, intermediate representations, and transient allocations, while preserving the existing lazy-decoding model and application-visible behavior.<\/p>\n<h3><strong>More Efficient Bulk Copy<\/strong><\/h3>\n<p>Customer workloads and community contributions highlighted similar opportunities in bulk-copy-based batch inserts. The focus here was broader than reducing conversion overhead: we wanted to give applications more control over bulk execution, keep eligible operations on the bulk-copy path, and avoid unnecessary transformations before values reach SQL Server.<\/p>\n<ul>\n<li><strong>Fine-grained control over bulk-copy execution:<\/strong> The driver already supports routing eligible batched inserts through SQL Server bulk copy with <code>useBulkCopyForBatchInsert=true<\/code>. We extended this capability with connection properties that expose relevant Bulk Copy API options while allowing applications to continue using the standard <code>PreparedStatement<\/code> programming model. This gives applications control over behaviors such as constraint checking, triggers, identity handling, NULL handling, table locking, encrypted-value modifications, and batch size without requiring changes to the application-level insert API.\n<pre><span style=\"font-size: 10pt;\"><code class=\"language-java\">bulkCopyForBatchInsertAllowEncryptedValueModifications=true\r\nbulkCopyForBatchInsertCheckConstraints=true\r\nbulkCopyForBatchInsertFireTriggers=true\r\nbulkCopyForBatchInsertKeepIdentity=true\r\nbulkCopyForBatchInsertKeepNulls=true\r\nbulkCopyForBatchInsertTableLock=true\r\nbulkCopyForBatchInsertBatchSize=&lt;application-defined size&gt;\r\n<\/code><\/span><\/pre>\n<p>For eligibility requirements, known limitations, and supported data types, see <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/connect\/jdbc\/use-bulk-copy-api-batch-insert-operation?view=sql-server-ver17\">Using bulk copy API for batch insert operation<\/a>.<\/li>\n<li><strong>Keeping eligible operations on the bulk-copy path:<\/strong> We also found cases where otherwise eligible batch inserts could fall back to row-by-row execution. In particular, statements involving MONEY and temporal SQL types could leave the bulk-copy path even when those values could be handled by bulk copy. We updated the bulk-copy conversion layer to handle these types directly, allowing more supported operations to remain on the intended high-throughput execution path.<\/li>\n<li><strong>Preserving string representation:<\/strong> Community reports also identified unnecessary conversions when <code>sendStringParametersAsUnicode=false<\/code>, including character-encoding issues with accented characters. The updated implementation keeps values in their string representation until the final destination-encoding stage, avoiding redundant transformations while preserving the expected encoding behavior.<\/li>\n<\/ul>\n<p>Together, these changes make bulk-copy-based batch inserts more configurable, keep more eligible workloads on the bulk execution path, and reduce unnecessary conversion work while preserving the <code>PreparedStatement<\/code> programming model.<\/p>\n<p><span data-ccp-props=\"{&quot;335559739&quot;:30}\"><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>For Spark workloads:<\/strong><\/p>Applications focused on high-throughput ingestion can evaluate increasing the JDBC <code data-start=\"3828\" data-end=\"3840\">packetSize<\/code> to <code data-start=\"3844\" data-end=\"3851\">32767<\/code> to reduce network-transfer overhead. Validate the setting with representative workloads, as the impact depends on network conditions, batch size, partitioning, and the overall deployment environment. <\/div><\/span><\/p>\n<h3><strong>Better Parameter Metadata<\/strong><\/h3>\n<p>Parameter metadata can influence SQL Server query optimization. When applications do not provide parameter sizing information, the driver may generate parameter declarations that are broader than the application\u2019s actual data model, which can affect plan selection and execution efficiency.<\/p>\n<p><strong>Explicit parameter sizing:<\/strong> Applications can provide expected parameter metadata through <code>defineParameterType()<\/code> or the length-aware <code>setObject()<\/code> overload. Both APIs allow applications to communicate domain-specific sizing information without requiring additional metadata round trips.<\/p>\n<p><strong>Define parameter metadata, then set the value:<\/strong><\/p>\n<pre><span style=\"font-size: 10pt;\"><code class=\"language-java\">pstmt.defineParameterType(parameterIndex, Types.VARCHAR, expectedLength);\r\npstmt.setString(parameterIndex, value);\r\n<\/code><\/span><\/pre>\n<p><strong>Set the value with an expected-length hint:<\/strong><\/p>\n<pre><span style=\"font-size: 10pt;\"><code class=\"language-java\">pstmt.setObject(parameterIndex, value, Types.VARCHAR, expectedLength);\r\n<\/code><\/span><\/pre>\n<p><code>defineParameterType()<\/code> associates the SQL type and expected length with the prepared-statement parameter, allowing the metadata to be reused across subsequent executions and batch operations. The <code>scaleOrLength<\/code> argument of <code>setObject()<\/code> provides the expected-length hint at the point where the value is bound. For character and binary parameters, this hint influences the generated parameter declaration rather than limiting the value itself.<\/p>\n<p>These APIs give applications finer control over parameter declarations while preserving the existing <code>PreparedStatement<\/code> programming model. The metadata also remains compatible with the driver&#8217;s existing Always Encrypted requirements, allowing parameter sizing improvements without introducing additional metadata lookups or changing application behavior. <span data-contrast=\"auto\">For the API details, see the <\/span><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/connect\/jdbc\/reference\/setobject-method-sqlserverpreparedstatement?view=sql-server-ver17\"><span data-contrast=\"none\">setObject method documentation<\/span><\/a><span data-contrast=\"auto\"> on Microsoft Learn.<\/span><span data-ccp-props=\"{&quot;335559739&quot;:30}\">\u00a0<\/span><\/p>\n<h3><strong>Improving Always Encrypted Performance<\/strong><\/h3>\n<p>A community investigation identified repeated key-provider work in enclave-enabled Always Encrypted scenarios. Profiling showed that, unlike non-enclave encrypted operations, enclave-enabled paths could perform additional provider lookups even when reusable key material was already available.<\/p>\n<ul>\n<li><strong>Shared key-cache path:<\/strong> We extended the existing symmetric-key cache infrastructure to enclave-enabled key retrieval. When eligible key material is already cached, the driver can reuse it instead of repeating the provider lookup. This reduces unnecessary key-provider operations for secure-enclave workloads while preserving the existing cache semantics and security boundaries.<\/li>\n<\/ul>\n<h3><strong>Reducing Authentication Contention<\/strong><\/h3>\n<p>Profiling of highly concurrent workloads also identified contention during service-principal token acquisition. Requests that only needed a valid cached token could still encounter synchronization associated with token refresh coordination.<\/p>\n<p><strong>An upcoming release will optimize this path by checking the token cache before entering refresh synchronization.<\/strong> When a valid cached token is available, the request can return it without waiting for refresh coordination. The update will also narrow the scope of refresh coordination so that unrelated authentication operations are less likely to block one another.<\/p>\n<p>This change is intended to reduce unnecessary synchronization in highly concurrent authentication workloads while preserving existing token acquisition and refresh semantics.<\/p>\n<h3><strong>A Common Engineering Pattern<\/strong><\/h3>\n<p>Across the driver, these improvements follow a consistent engineering pattern: profile real workloads, identify where unnecessary work occurs, and remove it without changing application-visible behavior.<\/p>\n<p>The same approach appears at different layers of the driver: detecting NULL values before decoder initialization, avoiding unnecessary intermediate representations, keeping eligible operations on the bulk-copy path, reusing cached key material, and avoiding synchronization when a valid authentication token is already available.<\/p>\n<p>The broader lesson is that driver performance does not always require a new algorithm or architectural redesign. Significant gains can come from understanding existing execution paths deeply enough to ensure that each path performs only the work required to produce the correct result.<\/p>\n<h3><strong>Availability and Further Work<\/strong><\/h3>\n<p>The latest Microsoft JDBC Driver for SQL Server is available through Maven Central as <code>com.microsoft.sqlserver:mssql-jdbc<\/code>. Review the driver documentation for workload-specific configuration options, and report reproducible issues or performance findings through <span data-contrast=\"none\">the <\/span><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\"><span data-contrast=\"none\">Microsoft JDBC Driver for SQL Server GitHub repository<\/span><\/a><span data-contrast=\"none\">.<\/span><\/p>\n<p>Customer workloads, profiling traces, and community contributions shaped many of these optimizations and continue to guide future work. The effort does not end here; we are continuing to explore additional opportunities, including memory-segment APIs and further bulk-copy improvements.<\/p>\n<h3><strong>Further Reading<\/strong><\/h3>\n<p>The following pull requests provide additional implementation details for the optimizations discussed in this article.<\/p>\n<table>\n<thead>\n<tr>\n<th>Area<\/th>\n<th>Pull request<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>ResultSet processing<\/strong><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2600\">PR #2600<\/a><\/td>\n<td>Early return for NULL values<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2974\">PR #2974<\/a><\/td>\n<td>ResultSet read-path optimizations<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2975\">PR #2975<\/a><\/td>\n<td>Decimal and numeric decoder optimization<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2991\">PR #2991<\/a><\/td>\n<td>Additional decoder and buffer optimizations<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2955\">PR #2955<\/a><\/td>\n<td>Fine-grained logging optimization<\/td>\n<\/tr>\n<tr>\n<td><strong>Bulk copy<\/strong><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2555\">PR #2555<\/a><\/td>\n<td>Expose bulk-copy batch properties<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2670\">PR #2670<\/a><\/td>\n<td>Keep supported values on the bulk path<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2735\">PR #2735<\/a><\/td>\n<td>Preserve string representation through bulk copy<\/td>\n<\/tr>\n<tr>\n<td><strong>Parameter metadata<\/strong><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2960\">PR #2960<\/a><\/td>\n<td>Add <code>defineParameterType()<\/code> support<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/3026\">PR #3026<\/a><\/td>\n<td>Support parameter-length hints through <code>setObject()<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>Always Encrypted<\/strong><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2964\">PR #2964<\/a><\/td>\n<td>Route enclave key lookup through the symmetric-key cache<\/td>\n<\/tr>\n<tr>\n<td><strong>Authentication<\/strong><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2982\">PR #2982<\/a><\/td>\n<td>Check token cache before synchronization<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><a href=\"https:\/\/github.com\/microsoft\/mssql-jdbc\/pull\/2983\">PR #2983<\/a><\/td>\n<td>Partition authentication synchronization<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n","protected":false},"excerpt":{"rendered":"<p>A database driver sits on the critical path of many applications. When a frequently executed path performs unnecessary work, even a small amount of overhead can compound across millions of operations. Over the past several releases, we have systematically profiled frequently executed paths across the Microsoft JDBC Driver for SQL Server, identifying opportunities to improve [&hellip;]<\/p>\n","protected":false},"author":195730,"featured_media":227205,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[844,1],"tags":[248,856,855,857,860],"class_list":["post-233210","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure","category-java","tag-java","tag-jdbc","tag-mssql-jdbc-driver","tag-performance-engineering","tag-sql-server"],"acf":[],"blog_post_summary":"<p>A database driver sits on the critical path of many applications. When a frequently executed path performs unnecessary work, even a small amount of overhead can compound across millions of operations. Over the past several releases, we have systematically profiled frequently executed paths across the Microsoft JDBC Driver for SQL Server, identifying opportunities to improve [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/posts\/233210","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/users\/195730"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/comments?post=233210"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/posts\/233210\/revisions"}],"predecessor-version":[{"id":233213,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/posts\/233210\/revisions\/233213"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/media\/227205"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/media?parent=233210"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/categories?post=233210"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/java\/wp-json\/wp\/v2\/tags?post=233210"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}