Skip to content

PERF: Keep fetchmany column metadata native and call-local - #796

Draft
Jahnvi Thakkar (jahnvi480) wants to merge 2 commits into
mainfrom
jahnvi/perf-small-fetch-native-metadata
Draft

Jahnvi Thakkar (jahnvi480) wants to merge 2 commits into
mainfrom
jahnvi/perf-small-fetch-native-metadata

Conversation

@jahnvi480

@jahnvi480 Jahnvi Thakkar (jahnvi480) commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

GitHub Issue: #554

ADO Task: AB#48323


Summary

Keep fetchmany() column metadata native within each call instead of building Python dictionaries and immediately unpacking them again in C++. Small batches repeat this setup frequently, so avoiding the roundtrip reduces driver-side work.

The implementation shares one checked description loop and lets the existing binding and batch code read native fields. Fresh ODBC descriptions, eager Unicode-name conversion, public metadata output and existing fetch behavior are unchanged. There is no cross-call cache or hidden prefetch; fetchall() and Arrow retain their existing setup.

flowchart LR
    subgraph Before
        B1["Fresh descriptions"] --> B2["Python dictionaries"] --> B3["C++ field extraction"] --> B4["Bind/fetch"]
    end
    subgraph After
        A1["Fresh descriptions"] --> A2["Native fields + owned Unicode names"] --> A3["Same bind/fetch"]
    end
Loading

Matched Windows x64/Python 3.13 OFF-Release measurements: 10,000 rows, 24 columns, 12 paired rounds and 9 drains per case. The table shows median fetch latency.

Operation Before After Median change
fetchmany(1) 722.44 ms 252.47 ms -65.05%
fetchall() control 47.02 ms 61.84 ms +31.52%

Separately instrumented counts for the fetchmany(1) drain, including EOF, show metadata dictionary emissions falling from 240,024 to 0, while ODBC column descriptions remain 240,024. Owned Python Unicode names still exist.

Draft, not merge-ready: same-build calibration was unstable and some controls were slower, so no-regression is not established. The wide-small-fetch result is promising, but it is not a universal-speedup claim.

Remove native metadata dictionary roundtrips while preserving eager Unicode names and fresh per-call descriptions. Add behavior and profiling regression coverage. Performance acceptance remains unresolved after the bounded local study.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 17, 2026 15:53

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.

🟡 Changes recommended

The declared performance acceptance and no-regression gates remain unresolved, including failed A/A stability results.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Refactors fetchmany() metadata handling to avoid Python dictionary round-trips while preserving public descriptions and existing fetch behavior.

Changes:

  • Adds call-local native metadata structures and shared description logic.
  • Adds comprehensive fetch, metadata, lifecycle, and profiling tests.
  • Documents the behavior change in the changelog.
File summaries
File Description
mssql_python/pybind/ddbc_bindings.cpp Uses native metadata for fetchmany().
tests/test_040_fetch_native_metadata.py Adds regression and profiling coverage.
CHANGELOG.md Documents the metadata refactor.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@jahnvi480 Jahnvi Thakkar (jahnvi480) changed the title REFACTOR: Keep fetchmany column metadata native and call-local PERF: Keep fetchmany column metadata native and call-local Sep 17, 2026
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

93%


🎯 Overall Coverage

83%


📈 Total Lines Covered: 8674 out of 10332
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/pybind/ddbc_bindings.cpp (93.4%): Missing lines 3043,3045-3046,3097,4256

Summary

  • Total: 76 lines
  • Missing: 5 lines
  • Coverage: 93%

mssql_python/pybind/ddbc_bindings.cpp

Lines 3039-3050

  3039 SQLULEN GetFetchColumnSize(const FetchColumnMetadata& column) {
  3040     return column.columnSize;
  3041 }
  3042 
! 3043 std::string GetFetchColumnName(const py::dict& column) {
  3044     return column["ColumnName"].cast<std::string>();
! 3045 }
! 3046 
  3047 std::string GetFetchColumnName(const FetchColumnMetadata& column) {
  3048     return column.name.cast<std::string>();
  3049 }

Lines 3093-3101

  3093 }  // namespace
  3094 
  3095 // Wrap SQLDescribeCol
  3096 SQLRETURN SQLDescribeCol_wrap(SqlHandlePtr StatementHandle, py::list& ColumnMetadata) {
! 3097     PERF_TIMER("SQLDescribeCol_wrap");
  3098     return DescribeColumns(StatementHandle, [&](FetchColumnMetadata column) {
  3099         ColumnMetadata.append(
  3100             py::dict("ColumnName"_a = column.name, "DataType"_a = column.dataType,
  3101                      "ColumnSize"_a = column.columnSize, "DecimalDigits"_a = column.decimalDigits,

Lines 4252-4260

  4252 
  4253     {
  4254         PERF_TIMER("FetchBatchData::cache_column_metadata");
  4255         for (SQLUSMALLINT col = 0; col < numCols; col++) {
! 4256             const auto& columnMeta = GetFetchColumnMetadata(columnNames, col);
  4257             columnInfos[col].dataType = GetFetchColumnType(columnMeta);
  4258             columnInfos[col].columnSize = GetFetchColumnSize(columnMeta);
  4259             columnInfos[col].isLob =
  4260                 std::find(lobColumns.begin(), lobColumns.end(), col + 1) != lobColumns.end();


📋 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.1%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 78.4%
mssql_python.pybind.connection.connection_pool.cpp: 82.3%
mssql_python.pybind.connection.connection.cpp: 82.5%
mssql_python.row.py: 83.4%
mssql_python.logging.py: 86.2%
mssql_python.pooling.py: 90.1%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copilot AI review requested due to automatic review settings September 21, 2026 08:51
@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown

PR Performance Report

No consistent slowdowns detected across all 2 environments.

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

Environment Status
Unix / SQL Server 2022 Completed
Unix / SQL Server 2025 Completed
Affected phases and call counts

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

Batched row fetching: ddbc::FetchBatchData::construct_rows +1.258 ms; ddbc::FetchBatchData +0.310 ms; ddbc::FetchBatchData::SQLFetchScroll_call +0.079 ms. Call changes: ddbc::SQLDescribeCol_wrap (added, removed, or intermittent).
Row fetching in batches of 100: no measured phase delta. Call changes: ddbc::SQLDescribeCol_wrap (added, removed, or intermittent).
Row fetching in batches of 10,000: py::fetchmany::cpp_call +9.370 ms; ddbc::FetchMany_wrap +8.466 ms; ddbc::FetchBatchData::SQLFetchScroll_call +0.865 ms. Call changes: ddbc::SQLDescribeCol_wrap (added, removed, or intermittent).

Unix / SQL Server 2025

Batched row fetching: no measured phase delta. Call changes: ddbc::SQLDescribeCol_wrap (added, removed, or intermittent).
Row fetching in batches of 100: no measured phase delta. Call changes: ddbc::SQLDescribeCol_wrap (added, removed, or intermittent).
Row fetching in batches of 10,000: ddbc::SQLBindColums +7.941 ms; py::fetchmany::diag_records +0.016 ms; ddbc::SQLGetAllDiagRecords +0.016 ms. Call changes: ddbc::SQLDescribeCol_wrap (added, removed, or intermittent).

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 10.460 ms 10.520 ms +1.2% no signal
SELECT queries 1.178 ms 1.250 ms -1.1% no signal
Row insertion 34.982 ms 34.561 ms -0.4% no signal
Executemany inserts 166.489 ms 156.828 ms -5.9% no signal
Fetch-all queries 174.066 ms 173.240 ms -0.3% no signal
Row-by-row fetching 61.224 ms 60.812 ms -0.7% no signal
Batched row fetching 164.630 ms 164.237 ms -0.0% no signal
Transaction commit and rollback 114.774 ms 115.477 ms +1.1% no signal
Arrow row fetching 95.539 ms 95.064 ms +1.3% no signal
100,000-row insertion 451.711 ms 450.808 ms -1.3% no signal
Row fetching in batches of 100 227.351 ms 207.234 ms -8.9% no signal
Row fetching in batches of 10,000 179.764 ms 190.750 ms +4.3% no signal
Repeated positional queries 43.011 ms 43.360 ms +1.4% no signal
Repeated named-parameter queries 45.214 ms 45.286 ms -1.4% no signal
Legacy 100,000-row insertion 358.184 ms 353.443 ms -0.7% no signal
Insertion with explicit input sizes 2422.678 ms 2390.242 ms -1.2% no signal
Joined aggregation queries 179.515 ms 181.588 ms +1.2% no signal
Large joined-result fetching 214.554 ms 218.509 ms -0.3% no signal
1.2-million-row fetching 5097.321 ms 5047.856 ms -0.4% no signal
Common table expression queries 5.470 ms 5.422 ms -1.4% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 98.899 ms 97.127 ms -1.3% no signal
SELECT queries 1.207 ms 1.172 ms -3.0% no signal
Row insertion 34.163 ms 34.194 ms -0.6% no signal
Executemany inserts 154.716 ms 153.368 ms -0.5% no signal
Fetch-all queries 175.210 ms 171.863 ms -2.1% no signal
Row-by-row fetching 61.129 ms 60.298 ms -1.4% no signal
Batched row fetching 169.362 ms 163.762 ms -3.2% no signal
Transaction commit and rollback 114.987 ms 113.574 ms -1.4% no signal
Arrow row fetching 95.150 ms 95.310 ms -1.8% no signal
100,000-row insertion 453.398 ms 448.709 ms -1.0% no signal
Row fetching in batches of 100 226.312 ms 205.387 ms -9.1% no signal
Row fetching in batches of 10,000 196.190 ms 193.145 ms +0.1% no signal
Repeated positional queries 41.799 ms 41.644 ms +0.4% no signal
Repeated named-parameter queries 44.291 ms 44.297 ms -1.2% no signal
Legacy 100,000-row insertion 362.062 ms 357.369 ms -1.3% no signal
Insertion with explicit input sizes 2376.421 ms 2461.094 ms -0.0% no signal
Joined aggregation queries 160.398 ms 160.601 ms +0.2% no signal
Large joined-result fetching 213.913 ms 212.442 ms -0.0% no signal
1.2-million-row fetching 5064.599 ms 5041.252 ms -0.5% no signal
Common table expression queries 5.310 ms 5.263 ms -0.2% no signal
Build, commits and measurement details

ADO build 176907

PR head: 5b65844ed831e1c50555ba4a0d977a3dd63f48bd
Base: c963ee1ebf11a96b2dc5275b465d2a36f3f55d77
Measured merge: bf36efc6398642f4e29c165c636677b9fda7f6eb

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4295.3; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5005.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.

Copilot review overview

🔵 Needs a closer look

The performance-sensitive native ODBC path has unstable calibration and no established no-regression result.

Review effort: Lite
Findings: None

@github-actions github-actions Bot added the pr-size: large Substantial code update label Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: large Substantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants