Skip to content

PERF: avoid repeated text fetch preparation for fetchmany(1) - #891

Open
Jahnvi Thakkar (jahnvi480) wants to merge 2 commits into
mainfrom
jahnvi/single-row-fetch-optimization
Open

Jahnvi Thakkar (jahnvi480) wants to merge 2 commits into
mainfrom
jahnvi/single-row-fetch-optimization

Conversation

@jahnvi480

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

N/A

GitHub Issue: #554


Summary

Keep size-one text fetches on native completion so ordinary row-count updates do not rebuild the dispatch policy for every row. Bounded wide text reuses the established batch decoder; narrow text retains the batch reader. Preserve Row/RowMapping behavior, converter bytes, malformed-text handling, mutation checks and consumed-row completion. No prefetching, weak-state cache or stack-buffer changes.

Add exactly one PR performance workload: fetchmany_one_text, collecting 10,000 ordered INT/NVARCHAR rows with explicit fetchmany(1). It retains rows through EOF and validates values/types outside timing. Existing comparison thresholds and bulk controls are unchanged. Add one workload/report contract test and update the registry/documentation.

Correct the GC-sensitive plain-tuple count test after reproducing its original failure: collection of an older watched cursor during plan preparation legitimately invalidates the global epoch. Keep exact stable-path counts and separately test the required one-read fallback; production invalidation is unchanged.

Validation

  • python -m pre_commit run black-check --all-files --hook-stage pre-push: passed.
  • /fetchstudy/venv/bin/python /fetchstudy/validate_worker.py candidate-on tests: passed, 145 report contract tests. The import-safe runner executes pytest.main(["-q", "-ra", "--tb=short", "tests/test_036_profiler_ci.py"]) against the fresh candidate build.
  • New workload: one warmup pair and five fresh-process alternating measured pairs on private SQL Server 2022, Linux x86_64 / CPython 3.13, matching profiling-enabled Release builds. Main/candidate medians were 1767.565 / 22.495 ms; policy preparations were 10,001 / 2 and Python fallbacks 10,001 / 1 in every measured sample. These instrumented timings are not release-latency or pyodbc comparisons.
  • The unchanged driver patch also passed focused OFF 643 / ON 651 tests, a separate fault-build gate of 281 tests, and 1195 common OFF tests. Original failures and expected-negative baseline tests are retained in the investigation evidence.

Draft / remaining performance qualification

Measured base is 730cb2bec05c650b1dba069bf9cbbadb810291d9. Later main 01a18f76a58ccf8f23e2c3d3f88b88a37b81ac54 changes only the parameter helper and its tests; it was not folded into local measurements. CI must validate the current merge.

The separate profiling-OFF study demonstrates the mixed-text regression improvement but does not beat pyodbc or show repeatable fetchone gains. Adverse bulk-fetch controls remain unresolved: the second primary fetchall comparison is candidate/main 1.221, 95% CI [1.060, 1.410]; fetchmany(1000) is also adverse. Identical-binary replicas vary under shared-host contention, but this does not establish that the regressions are harmless. Keep this PR draft pending clean control measurements and cross-platform CI; no regression-free or merge-ready claim.

Preserve batch decoding while completing eligible size-one text fetches natively. Add one mixed-text performance-report workload and focused contract coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 12, 2026 07:45
@github-actions github-actions Bot added the pr-size: medium Moderate update size label Nov 12, 2026
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

PR Performance Report

✅ Performance improved

1 database task consistently improved across 2 measured environments. No consistent slowdowns were detected.

1 IMPROVEMENT 0 SLOWDOWNS 2/2 ENVIRONMENTS

Signal fingerprint

Database task Unix / SQL Server 2022 Unix / SQL Server 2025
10,000 INT/NVARCHAR rows / fetchmany(1) 98.2% faster 98.2% faster

Coverage: 2 of 2 environments completed. Advisory result; does not block merging.

Measured timings
Environment Database task Before After Change
Unix / SQL Server 2022 10,000 INT/NVARCHAR rows / fetchmany(1) 1377.119 ms 24.322 ms -98.2%
Unix / SQL Server 2025 10,000 INT/NVARCHAR rows / fetchmany(1) 1386.970 ms 25.098 ms -98.2%
Performance diagnostics

Phase times are inclusive diagnostics and must not be added together. They identify where measured time changed, not why it changed.

Unix / SQL Server 2022

10,000 INT/NVARCHAR rows / fetchmany(1): ddbc::NativeCursor::prepare -1130.411 ms; ddbc::NativeCursor::fallback -227.878 ms; py::fetchmany::cpp_call -64.121 ms. Call changes: ddbc::FetchBatchData (added, removed, or intermittent); ddbc::FetchBatchData::SQLFetchScroll_call (added, removed, or intermittent); ddbc::FetchBatchData::cache_column_metadata (added, removed, or intermittent).

Unix / SQL Server 2025

10,000 INT/NVARCHAR rows / fetchmany(1): ddbc::NativeCursor::prepare -1136.902 ms; ddbc::NativeCursor::fallback -230.206 ms; py::fetchmany::cpp_call -66.057 ms. Call changes: ddbc::FetchBatchData (added, removed, or intermittent); ddbc::FetchBatchData::SQLFetchScroll_call (added, removed, or intermittent); ddbc::FetchBatchData::cache_column_metadata (added, removed, or intermittent).

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 9.812 ms 9.944 ms +0.1% no signal
SELECT queries 0.987 ms 0.970 ms -1.3% no signal
Row insertion 30.023 ms 29.971 ms -0.3% no signal
Executemany inserts 154.728 ms 158.699 ms +2.5% no signal
Fetch-all queries 121.022 ms 120.266 ms -0.4% no signal
Row-by-row fetching 4.964 ms 4.977 ms -0.4% no signal
Batched row fetching 116.345 ms 115.847 ms -0.4% no signal
Transaction commit and rollback 103.556 ms 103.811 ms -0.1% no signal
Arrow row fetching 89.269 ms 89.615 ms -0.1% no signal
100,000-row insertion 343.911 ms 358.435 ms +3.1% no signal
Row fetching in batches of 100 119.768 ms 124.260 ms +4.4% no signal
Row fetching in batches of 10,000 123.392 ms 122.577 ms -1.0% no signal
Repeated positional queries 31.433 ms 31.462 ms +0.3% no signal
Repeated named-parameter queries 31.945 ms 32.327 ms -2.3% no signal
Legacy 100,000-row insertion 252.371 ms 254.145 ms +0.3% no signal
Insertion with explicit input sizes 391.167 ms 376.633 ms -3.7% no signal
Joined aggregation queries 177.388 ms 179.619 ms +1.5% no signal
Large joined-result fetching 185.440 ms 184.764 ms -1.1% no signal
1.2-million-row fetching 3450.378 ms 3463.033 ms +0.5% no signal
Common table expression queries 5.205 ms 5.106 ms -0.8% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.541 ms 1.501 ms -5.7% no signal
10,000 scalar values / fetchval() (debug disabled) 17.169 ms 16.738 ms -2.1% no signal
10,000 output-converted rows / fetchall() 18.932 ms 19.423 ms +1.4% no signal
10,000 output-converted rows / fetchmany(1000) 22.776 ms 22.753 ms +0.8% no signal
10,000 INT/NVARCHAR rows / fetchmany(1) 1377.119 ms 24.322 ms -98.2% consistent improvement
10,000 rows x 32 columns / dict(row._mapping) (warm) 31.216 ms 31.484 ms +0.3% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 98.548 ms 98.475 ms -0.1% no signal
SELECT queries 1.025 ms 1.021 ms +0.2% no signal
Row insertion 30.860 ms 30.511 ms -1.2% no signal
Executemany inserts 156.494 ms 163.282 ms +3.1% no signal
Fetch-all queries 122.070 ms 124.873 ms +1.3% no signal
Row-by-row fetching 5.020 ms 5.054 ms +0.7% no signal
Batched row fetching 117.423 ms 119.832 ms +2.2% no signal
Transaction commit and rollback 107.358 ms 106.499 ms -0.9% no signal
Arrow row fetching 90.865 ms 91.671 ms +1.6% no signal
100,000-row insertion 364.930 ms 350.575 ms -2.8% no signal
Row fetching in batches of 100 123.042 ms 128.854 ms +6.0% no signal
Row fetching in batches of 10,000 126.356 ms 140.367 ms +10.7% no signal
Repeated positional queries 31.997 ms 35.156 ms +0.4% no signal
Repeated named-parameter queries 32.065 ms 32.373 ms +2.0% no signal
Legacy 100,000-row insertion 258.775 ms 259.411 ms +0.4% no signal
Insertion with explicit input sizes 400.750 ms 384.993 ms -3.4% no signal
Joined aggregation queries 168.098 ms 181.181 ms +7.8% no signal
Large joined-result fetching 191.470 ms 195.262 ms +2.1% no signal
1.2-million-row fetching 3522.476 ms 3509.638 ms -0.4% no signal
Common table expression queries 5.198 ms 5.291 ms +2.9% no signal
256 KiB VARCHAR(MAX) / fetchall() 2.023 ms 2.056 ms -0.5% no signal
10,000 scalar values / fetchval() (debug disabled) 17.191 ms 17.285 ms +0.5% no signal
10,000 output-converted rows / fetchall() 20.235 ms 20.443 ms -1.5% no signal
10,000 output-converted rows / fetchmany(1000) 24.215 ms 25.313 ms +3.9% no signal
10,000 INT/NVARCHAR rows / fetchmany(1) 1386.970 ms 25.098 ms -98.2% consistent improvement
10,000 rows x 32 columns / dict(row._mapping) (warm) 31.450 ms 32.246 ms +2.9% no signal
Build and measurement details

ADO build 191387

PR head: aef97fa2ccab043c0a203a53adda4b787db95b49
Base: 444ae8c764272e55ded42c5f5a81bf35e8dbb327
Measured merge: 1e59a80a5388755780b3c94dcc6bb803f5d5e552

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4315.2; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5045.3; 5 paired comparisons and 1 warmup.

A consistent change requires more than 20% median paired movement, at least 1 ms between the median runtimes, and at least 80% of pairs exceeding the relative threshold in the same direction. A slowdown without enough pair agreement is reported as inconsistent.

The displayed change is the median of paired before-and-after ratios. It is not recalculated from the two displayed median runtimes.

Both revisions use profiling-enabled builds on the same agent and database, with alternating order and discarded warmups. Results are diagnostic and do not represent production-wheel latency.

Raw samples and logs are attached to the ADO run as profiler-* artifacts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Source-only review cannot establish cross-platform/live-SQL correctness of the native fetch changes or attribute the reported profiling-OFF bulk-fetch regressions without clean controls.

No open issues 🎉

What changed in this PR

This PR targets repeated dispatch preparation for text-bearing fetchmany(1) calls in the native driver, addressing part of the fetch-performance concerns raised in #554.

Changes:

  • Keep size-one text fetches on native completion, sharing batch decoding for bounded wide text and retaining the batch reader for narrow text.
  • Add regression coverage for dispatch counts, text boundaries, converter bytes, malformed text, and garbage-collection invalidation.
  • Add one 10,000-row INT/NVARCHAR workload with timing-boundary and report-contract tests, bringing the profiler registry to 26 tasks.

The reported instrumented gains do not establish release-latency improvements. Bulk-fetch regressions remain unresolved, and the PR remains draft pending clean control measurements and cross-platform validation.

File Description
tests/​test_single_row_pipeline.py Tests wide-text boundaries and updated native-path counts.
tests/​test_output_converter_bytes.py Extends converter and malformed-text coverage to fetchmany(1).
tests/​test_native_fetch_dispatch.py Tests stable dispatch reuse and garbage-collection invalidation.
tests/​test_036_profiler_ci.py Validates the new workload’s timing, retention, cleanup, and reporting.
mssql_python/​pybind/​native_fetch.hpp Completes size-one text reads natively without repeated policy preparation.
mssql_python/​pybind/​fetch_text.hpp Extracts the shared batch UTF-16 decoder.
mssql_python/​pybind/​ddbc_bindings.h Uses the shared decoder for bound wide-text batches.
mssql_python/​pybind/​ddbc_bindings.cpp Enables bounded wide-text single-row reads with batch decoding semantics.
mssql_python/​_native_fetch.py Updates dispatch documentation for size-one text fetches.
eng/​profiler_benchmarks/​workloads.py Adds and registers fetchmany_one_text.
eng/​profiler_benchmarks/​report.py Adds the new workload’s report label.
eng/​profiler_benchmarks/​README.md Documents workload boundaries and updates registry counts.

Review effort: Balanced

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

86%


🎯 Overall Coverage

85%


📈 Total Lines Covered: 11735 out of 13700
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/pybind/ddbc_bindings.cpp (50.0%): Missing lines 3556,3943-3945
  • mssql_python/pybind/ddbc_bindings.h (100%)
  • mssql_python/pybind/fetch_text.hpp (100%)
  • mssql_python/pybind/native_fetch.hpp (94.4%): Missing lines 741

Summary

  • Total: 37 lines
  • Missing: 5 lines
  • Coverage: 86%

mssql_python/pybind/ddbc_bindings.cpp

Lines 3552-3560

  3552             (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE));
  3553 }
  3554 
  3555 static inline bool CanUseSingleRowForFetchMany(SQLSMALLINT dataType, SQLULEN columnSize) {
! 3556     // Narrow text still needs the batch's platform/codec-specific conversion.
  3557     switch (dataType) {
  3558         case SQL_CHAR:
  3559         case SQL_VARCHAR:
  3560         case SQL_LONGVARCHAR:

Lines 3939-3949

  3939                                     text = OutputConverters::text(textData, textSize);
  3940                                 row.append(text ? text
  3941                                                 : batchText
  3942                                                       ? steal(FetchText::decode_batch_utf16_native(
! 3943                                                             textData, textSize))
! 3944                                                       : FetchText::from_utf16_native(textData,
! 3945                                                                                      textSize));
  3946                                 LOG("SQLGetData: Appended NVARCHAR string "
  3947                                     "length=%lu for column %d",
  3948                                     (unsigned long)numCharsInData, i);
  3949                             } else {

mssql_python/pybind/native_fetch.hpp

Lines 737-745

  737         ret = FetchMany_wrap(plan->handle, rows, 1, plan->charEncoding,
  738                              plan->wideEncoding, plan->charCtype, plan->at(Messages));
  739         if (!current(self, context, state, *plan, rt) ||
  740             (!SQL_SUCCEEDED(ret) && ret != SQL_NO_DATA))
! 741             return py::reinterpret_borrow<py::object>(self).attr("_complete_fetchmany")(rows, ret);
  742         if (PyList_GET_SIZE(rows.ptr()))
  743             values = py::reinterpret_borrow<py::list>(PyList_GET_ITEM(rows.ptr(), 0));
  744         else
  745             ret = SQL_NO_DATA;


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.performance_counter.hpp: 0.7%
mssql_python.pybind.logger_bridge.cpp: 57.9%
mssql_python.pybind.ddbc_bindings.h: 64.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python._native_fetch.py: 77.4%
mssql_python.pybind.ddbc_bindings.cpp: 80.5%
mssql_python.pybind.row_mapping_cache.hpp: 81.5%
mssql_python.pybind.connection.connection_pool.cpp: 82.4%
mssql_python.pybind.connection.connection.cpp: 82.5%
mssql_python.logging.py: 86.2%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as ready for review November 12, 2026 08:07
Copilot AI balanced review requested due to automatic review settings November 12, 2026 08:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Native fetch routing and decoding require live SQL Server and cross-platform validation, while unresolved profiling-OFF bulk-fetch slowdowns need clean control measurements and human review.

No open issues 🎉

Review effort: Balanced

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the PR for correctness, security, reliability, performance, test coverage, repository conventions, and applicable architecture and design specifications. No actionable issues were identified. The implementation is consistent with repository standards and the applicable approved design requirements.

Recommendation: Approve

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: medium Moderate update size

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants