Skip to main content

Embedding a million SQL Server rows with sentence-transformers and writing the vectors back

François Pacull
François Pacull, ARPE.IO Developer
2026-10-30 · 26 min · Tutorial · ADBC

Microsoft SQL Server, Apache Arrow ADBC, Architecture & Performance, Polars and Sentence Transformers logos

SQL Server 2025 has a native VECTOR type, so the embeddings of a text column can be stored next to the rows they describe and searched in T-SQL. It can also compute the embeddings itself, with AI_GENERATE_EMBEDDINGS and an external model, but we tried it in a previous post, with the larger nomic-embed-text model on a local Ollama server and a GPU, and found it rather slow: 3 min 20 s for 25,000 Wikipedia articles with SQL Server 2025 RTM, for a single SELECT … INTO that reads the text, calls the model and writes the vectors to a new table.

Here, we compute the embeddings outside the database instead. We read the text column of a one-million-row table, embed it on a GPU with sentence-transformers, a library maintained by Hugging Face, and write the vectors back into the database with ArpeMSSQL, our ADBC driver for SQL Server. ArpeMSSQL maps VECTOR(n) to the Apache Arrow type fixed_size_list<float32>[n], for reads and for bulk ingest. Most of the other Python clients we tried send each vector to SQL Server as JSON text, such as '[0.0123, -0.0456, ...]', which the server then parses; mssql-python sends float32 values, but from one Python list per vector. With ArpeMSSQL, the NumPy array is wrapped as Arrow without a copy, and the embeddings are sent as binary float32 values, the same bytes as in the array.

The model takes most of the time: 353 s to embed the narratives, against 2.0 s to read them and 7.8 s to write the vectors back.

Data​

We use the Consumer Complaint Database of the US Consumer Financial Protection Bureau (CFPB), whose complaints include a free-text narrative when the consumer agreed to publish it. The CFPB stopped publishing these narratives in August 2026, and the current download no longer has the column. Previously published narratives remain available in the narratives archive of the CFPB's FOIA reading room. Its first three exports cover December 2011 to October 2022 and hold 1,139,480 complaints with a narrative. We kept the first 1,000,000 by complaint ID, received between March 19, 2015 and June 13, 2022. Personal details are already redacted as "XXXX" in the text, and we left them in place.

The rows were loaded into a SQL Server 2025 table, dbo.complaints, with ArpeMSSQL's bulk ingest. The embeddings will go into a second table, dbo.complaint_embeddings, with one row per complaint and the same primary key:

Here are the first three complaints, with a few of the columns and the narrative cut at 50 characters:

complaint_iddate_receivedproductcompanynarrative
12901552015-03-19MortgageOcwen Financial CorporationIn XXXX XXXX, we submitted a complaint to you ( ca…
12901572015-03-19Debt collectionNavient Solutions, LLC.I wrote to XXXX, asking them to stop calling. XXXX…
12901812015-03-19Debt collectionDean Morris, L.L.P.Lawyers at this law firm ( XXXX ) sent me a letter…

The narratives total 1.04 billion characters, 1,039 on average and 32,394 at most.

VECTOR(384) stores 384 float32 values per row.

Setup​

The driver is installed and registered with the ADBC driver manager by a one-line script, as described in the documentation. The script takes the path of a licence file. You can get a free 30-day trial licence from the trial page, which also gives the install command:

curl -fsSL https://raw.githubusercontent.com/arpe-io/adbc-drivers/main/install.sh \
| sh -s -- arpemssql --license /path/to/your.lic

This is the Linux command, used for this post. On Windows, the equivalent is a PowerShell one-liner:

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/arpe-io/adbc-drivers/main/install.ps1))) arpemssql -License C:\path\to\your.lic

On the Python side, we need the ADBC driver manager, Polars, PyArrow, and sentence-transformers with a CUDA build of PyTorch:

pip install adbc-driver-manager "polars>=2.0" pyarrow sentence-transformers torch

Imports​

import adbc_driver_manager.dbapi as dbapi
import numpy as np
import polars as pl
import pyarrow as pa
import torch
from sentence_transformers import SentenceTransformer

Package versions​

Used in this post:

Python : 3.14.2
OS : Linux
adbc_driver_manager : 1.12.0
ArpeMSSQL : 0.7.8
polars : 2.0.0
pyarrow : 26.0.0
numpy : 2.5.4
torch : 2.14.1 (CUDA 13.0)
sentence-transformers : 6.2.0
SQL Server : 2025 (RTM-CU7) 17.0.4065

The following packages are not needed for the pipeline itself, but they were used for the comparisons quoted below:

pyodbc : 5.3.0
ODBC Driver 18 : 18.7
mssql-python : 1.16.0
arrow-odbc : 10.6.1
connectorx : 0.4.6

SQL Server runs in a Docker container on the same laptop, an Intel i9-12900H with 32 GB of RAM and an NVIDIA RTX 3070 Ti Laptop GPU with 8 GB of memory. All connections use TLS. The complaints database uses the FULL recovery model, inherited from the model database, so the bulk loads are fully logged.

Unless stated otherwise, timings are the best of three runs. The writes of the million vectors varied more from run to run, so they were repeated 10 times with ArpeMSSQL and Polars, 5 times with mssql-python and 3 times with pyodbc, and we give the median as well as the best run.

Reading the narratives​

pl.read_database accepts an ADBC connection:

uri = (
"sqlserver://sa:<password>@localhost:1435/"
"?database=complaints&encrypt=true&TrustServerCertificate=true"
)

with dbapi.connect(driver="arpemssql", db_kwargs={"uri": uri}) as conn:
df = pl.read_database(
"SELECT complaint_id, narrative FROM dbo.complaints ORDER BY complaint_id", conn
)
1,000,000 rows x 2 columns in 2.01 s

complaint_id arrives as Int32 and narrative as String. The ORDER BY matters for the read-back check below, which compares the vectors in key order: without it, SQL Server does not guarantee any row order.

SQL Server sends NVARCHAR as UTF-16, so about 2 GB of text cross the wire. The driver converts it to UTF-8 while building its Arrow buffers, and Polars uses these buffers without copying them, as shown in What is copied.

Embedding​

This step is only here to produce the vectors. The post is about moving the data out of SQL Server and back in, so the choice of model and the handling of long texts (truncated here, without chunking) are not discussed.

We use all-MiniLM-L6-v2, a tiny model that maps a text to 384 dimensions, loaded in half precision on the GPU:

model = SentenceTransformer(
"sentence-transformers/all-MiniLM-L6-v2",
device="cuda",
model_kwargs={"dtype": torch.float16},
)
model.max_seq_length = 256

embeddings = model.encode(df["narrative"].to_list(), batch_size=256).astype(np.float32)
1,000,000 narratives in 352.9 s (2,834 texts/s)
(1000000, 384) float32

With the model in half precision, encode returns a float16 array. A VECTOR(384) column holds float32 values, and ArpeMSSQL maps it to fixed_size_list<float32>[384], so the array has to be in float32 before it is written back. The conversion with astype is exact, since every float16 value is also a float32 value, but it does not add any precision: the vectors keep the accuracy of half precision. It doubles the size of the array, from 732 MiB to 1,465 MiB (1 MiB = 2^20 bytes).

This time covers the encode call only, in a single run. PyTorch allocated 0.63 GB of GPU memory at most, and a batch size of 512 was not faster.

The model truncates its input at 256 tokens. The narratives have a median of 155 tokens, and the 29.5% that are longer are embedded from their beginning only. The output is normalised: every vector has a length of 1, within the rounding of half precision.

The text is copied at this step: to_list() builds one Python string per narrative, about 1 GB in total, for the tokenizer.

Writing the vectors back​

The Arrow type for VECTOR(384) is a fixed-size list of 384 float32 values. Without null values, its memory layout is a single buffer of 384 × 1,000,000 float32 values, with no offsets. NumPy is not Arrow, but a contiguous float32 array of shape (1,000,000, 384) is exactly that buffer, so Arrow can point to it instead of copying it. This requires float32, the type the driver expects for VECTOR(384), and a contiguous array; a strided slice, such as every other row, would need a copy. The Polars type Array(Float32, 384) has the same layout, so the DataFrame below is built on the memory of the NumPy array, without a copy. The target table is created beforehand:

CREATE TABLE dbo.complaint_embeddings (
complaint_id INT NOT NULL PRIMARY KEY,
embedding VECTOR(384) NOT NULL
)
pf = pl.DataFrame(
{"complaint_id": df["complaint_id"], "embedding": embeddings},
schema={"complaint_id": pl.Int32, "embedding": pl.Array(pl.Float32, 384)},
)

with dbapi.connect(driver="arpemssql", db_kwargs={"uri": uri}, autocommit=True) as conn:
pf.write_database("dbo.complaint_embeddings", conn, if_table_exists="append", engine="adbc")
1,000,000 rows in 7.82 s (127,881 rows/s)

With the ADBC engine, write_database hands the DataFrame to the driver's adbc_ingest through the Arrow C stream interface, without going through PyArrow, and the column reaches the driver as fixed_size_list<float32>[384]. The driver uses SQL Server's bulk load protocol, the one behind bcp and SqlBulkCopy, and the vectors travel as float32. For 100,000 rows, the loopback interface carried 156 MB, for 154 MB of float32 payload.

7.82 s is the best of 10 runs. The median is 9.25 s, and the runs ranged from 7.8 to 14.3 s.

This spread comes from the server. In a later series of 10 runs, the driver always took 6.2 to 7.3 s to send the rows, but SQL Server then needed between 1.8 and 7.5 s to finish the load (best 8.21 s, median 9.47 s). We then switched the database to the SIMPLE recovery model and ran 10 more writes: the final step took 1.1 to 1.7 s, for a best of 7.37 s and a median of 7.48 s. We did not check whether these loads were minimally logged. All the other timings in this post use FULL.

As a reference, we wrote the same million vectors with pyodbc and the Microsoft ODBC Driver 18. pyodbc has no way to send a list of floats as a parameter, but SQL Server converts a JSON array string, such as '[0.0123, -0.0456, ...]', into a VECTOR on insert, so each vector is serialised with json.dumps and sent as text. The serialisation takes 94.4 s and the insert 234.4 s, 328.8 s in total. That is 42 times the 7.8 s of Polars and ArpeMSSQL, and 30 times for the insert alone. The pyodbc inserts varied little, from 234.4 to 245.0 s.

NumPy embeddings to SQL Server VECTOR, time

Time to write 1,000,000 VECTOR(384) rows: pyodbc with JSON text, including the json.dumps calls, and Polars with ArpeMSSQL, as Arrow float32 values.

The JSON text is 7,320 characters per vector on average, 4.8 times the 1,536 bytes of float32 values, and SQL Server has to parse every number. During the insert, the SQL Server container used one CPU core at 100%, and almost nothing while the client was building the next batch of JSON strings. The pyodbc code, in the appendix, uses fast_executemany, a bounded VARCHAR(8000) parameter type (the longest JSON string is 7,513 characters) and a single commit at the end. With autocommit=True, every row is committed separately: 100,000 rows were still not written after 10 minutes.

We looked for other Python clients that write VECTOR straight from the float32 values in memory, the NumPy array or the Polars column built on it, without converting each vector to Python objects or JSON text. With the versions above, as of October 2026, we are not aware of any other than ArpeMSSQL. Microsoft's mssql-python driver also sends float32 values with its bulkcopy method, from Python lists, in 16.2 s for the same million rows (median 17.3 s over 5 runs). The other tools we tried are listed in the appendix.

What is copied​

Arrow is a memory layout, and Polars has its own implementation of it, in Rust. When the driver and Polars use the same layout for a column, Polars keeps the driver's buffers on a read instead of copying them, and the driver reads the buffers of the Polars DataFrame on a write. We checked which bytes are shared by comparing the buffer addresses on both sides:

StepColumnArrow typeSharedNew
Read, driver → Polarscomplaint_idint324.0 MB0
Read, driver → Polarsnarrativestring → string_view1,039.9 MB16.0 MB
Write, NumPy → driverembeddingfixed_size_list<float32>[384]1,536.0 MB0
Write, Polars → drivercomplaint_idint3204.0 MB

The 1,536 MB (1,465 MiB) of vectors go from the NumPy array, through the Polars DataFrame, to the driver without a copy. The narrative text is not copied either. Polars stores strings as string_view, with one 16-byte view per value that points into the driver's buffer, so only the views are new: 16 MB for a million rows. A string of 12 bytes or less is stored inside its view, and so copied, but only 57 narratives are that short.

The only column copied in full is complaint_id on the write side, 4 MB. The reason is not its type, but the number of memory blocks it is made of. A Polars column can be a list of separate blocks, called chunks, and the two columns of the DataFrame we write do not come from the same place:

complaint_id [blk 1][blk 2][blk 3] ... [blk 32] 32 blocks, 4 MB in total
embedding [ one block ] 1 block, 1,536 MB

complaint_id comes from the read: the driver returned the result in 32 record batches, and Polars kept the memory of each batch, which is the zero-copy read shown in the first row of the table. embedding comes from the NumPy array, a single contiguous block. Before exporting a DataFrame as an Arrow stream, Polars turns every column into a single block. The embeddings already are one, so nothing happens to them. The 32 pieces of complaint_id are copied into a new 4 MB buffer. Arrow does not require this merge: Polars rechunks the DataFrame before every export, and here it costs 4 MB.

Zero-copy here is about the hand-off between the driver and Polars. On the network side, data is always moved: on a read, the driver decodes the UTF-16 text from SQL Server into UTF-8 as it builds its Arrow buffers, and on a write, it copies the float32 values from the Arrow buffers into its network packets.

Reading them back​

The vectors come back from SQL Server in the same Arrow type:

with dbapi.connect(driver="arpemssql", db_kwargs={"uri": uri}) as conn:
cur = conn.cursor()
cur.execute("SELECT complaint_id, embedding FROM dbo.complaint_embeddings ORDER BY complaint_id")
result = cur.fetch_arrow_table()

result.schema
complaint_id: int32 not null
embedding: fixed_size_list<item: float>[384] not null
child 0, item: float
1,000,000 rows in 1.48 s (675,236 rows/s)

The first three rows of dbo.complaint_embeddings, the same complaints as in the Data section, rounded to four decimals:

complaint_idembedding
1290155[-0.0955, -0.0340, -0.0320, …, -0.0261] (384 values)
1290157[-0.0781, 0.0134, 0.0469, …, -0.0197] (384 values)
1290181[0.0030, 0.0701, -0.0562, …, -0.0513] (384 values)

The values of the fixed-size list form one float32 buffer, which reshapes into a NumPy array of the original shape. The ORDER BY in the query puts the rows in the order of the embeddings array, so the two can be compared row by row:

read_back = result["embedding"].combine_chunks().values.to_numpy().reshape(-1, 384)
np.array_equal(read_back, embeddings)
True

The round trip is exact, bit for bit. combine_chunks() copies the values into one buffer; it is only needed for this check. In Polars, pl.from_arrow(result) gives an Array(Float32, shape=(384,)) column.

The other Python clients we tried receive VECTOR values as JSON text. SQL Server writes each float32 value with 8 significant digits, as in -9.5520020e-002, and some float32 values need 9 to be restored exactly. Over 100,000 vectors read with pyodbc, 1.48% of the values differed from the originals, all by one unit in the last place, and 99.69% of the vectors had at least one such value. The lowest cosine similarity between a vector and its original was 0.99999976, so search results are not affected, but an exact comparison fails.

Similarity search in T-SQL​

With the vectors in the table, a similarity search no longer needs the model: the query vector can be the embedding of a complaint that is already stored. We look for the 10 complaints closest to complaint 2708543, "My Bank over charge me twice for Overdraft fee which is not…", with VECTOR_DISTANCE:

query = """
DECLARE @q VECTOR(384) = (SELECT embedding FROM dbo.complaint_embeddings WHERE complaint_id = 2708543);

WITH nearest AS (
SELECT TOP (10) e.complaint_id, VECTOR_DISTANCE('cosine', e.embedding, @q) AS distance
FROM dbo.complaint_embeddings AS e
ORDER BY distance
)
SELECT c.complaint_id, c.date_received, c.company, n.distance, LEFT(c.narrative, 60) AS narrative
FROM nearest AS n
JOIN dbo.complaints AS c ON c.complaint_id = n.complaint_id
ORDER BY n.distance
"""

with dbapi.connect(driver="arpemssql", db_kwargs={"uri": uri}) as conn:
cur = conn.cursor()
cur.execute(query)
hits = pl.from_arrow(cur.fetch_arrow_table())

The first five rows of the result:

complaint_iddate_receivedcompanydistancenarrative
27085432017-10-21BANK OF AMERICA, NATIONAL ASSOCIATION0.0000My Bank over charge me twice for Overdraft fee which is not…
21741592016-10-22SUNTRUST BANKS, INC.0.1171Bank charged me overdraft fees despite the fact that the acc…
17847712016-02-11U.S. BANCORP0.1375My bank charged me an overdraft fee which was n't warranted
21230312016-09-21BANK OF AMERICA, NATIONAL ASSOCIATION0.1407I have been charged excessive overdraft fees by BANK OF AMER…
55780892022-05-18TRUIST FINANCIAL CORPORATION0.1459The bank has charged me 4 overdraft fees when my account was…

The complaint itself comes first, at distance 0. The query takes 0.058 s, measured from Python, fetch included. There is no vector index on the table, so this is an exact search. SQL Server scans the 1,000,000 vectors in parallel on the 20 logical CPUs, keeps the 10 smallest distances, and only then reads the narratives of these 10 rows. Both tables were already in SQL Server's memory, and the query read nothing from disk.

The order of these steps matters. With LEFT(c.narrative, 60) and the distance in the same SELECT, the plan we got joined the two tables first and read the narrative of every row before keeping the 10 smallest distances: 8.8 million logical reads instead of 201,640, and 3.0 s instead of 0.058 s, for the same 10 rows. The CTE itself does not force anything, since SQL Server inlines it; what limits the join to 10 rows is the TOP (10) inside it, and a derived table works the same way.

The search can be combined with ordinary SQL filters, here one company and one year, by joining complaints inside the nearest step:

WITH nearest AS (
SELECT TOP (10) e.complaint_id, VECTOR_DISTANCE('cosine', e.embedding, @q) AS distance
FROM dbo.complaint_embeddings AS e
JOIN dbo.complaints AS c ON c.complaint_id = e.complaint_id
WHERE c.company = 'WELLS FARGO & COMPANY'
AND c.date_received >= '2019-01-01' AND c.date_received < '2020-01-01'
ORDER BY distance
)
complaint_iddate_receiveddistancenarrative
33910482019-09-300.2285The bank assessed a {$35.00} overdraft fee and is refusing t…
33522802019-08-240.2492I was charged three overdraft fees because the bank stacked …
31149492019-01-020.2639I was charged an overdraft fee for a debit card transaction …

This query takes 0.036 s. SQL Server scans the complaints table to apply the filter, which keeps 2,859 rows, then fetches only their vectors by primary key: it computes 2,859 distances instead of a million. The scan is there because company and date_received have no index; on a larger table, an index on these two columns would avoid it.

To search with a free-text question instead of a stored complaint, the question has to be embedded with the same model first; this is shown in the appendix.

Wrap-up​

With ArpeMSSQL, the million embeddings go back into a VECTOR(384) column in 7.8 s, as float32 values taken from Polars without a copy, against 328.8 s for JSON text through pyodbc. Reading the narratives takes 2.0 s and reading the vectors back 1.5 s, so the 353 s of embedding on the GPU is where the time goes.

Driver downloads, for Linux (x64) and Windows (x64), are at github.com/arpe-io/adbc-drivers. The binaries download freely and run once a licence is present. A 30-day trial licence for ArpeMSSQL can be downloaded for free here; for a full licence, contact us at sales@arpe.io or see arpe.io.

Appendix: other ways to write the vectors​

All of them write straight into dbo.complaint_embeddings, without a staging table.

Without Polars, the same driver call takes a PyArrow table. FixedSizeListArray.from_arrays wraps the flat float32 buffer of the NumPy array, without a copy either:

vectors = pa.FixedSizeListArray.from_arrays(pa.array(embeddings.reshape(-1)), 384)
table = pa.table({"complaint_id": df["complaint_id"].to_arrow(), "embedding": vectors})

with dbapi.connect(driver="arpemssql", db_kwargs={"uri": uri}, autocommit=True) as conn:
cur = conn.cursor()
cur.adbc_ingest("complaint_embeddings", table, mode="append", db_schema_name="dbo")

Its best of 10 runs is 7.87 s and its median 8.91 s, against 7.82 s and 9.25 s through Polars: the gap is much smaller than the spread between runs, 7.9 to 13.4 s here.

With pyodbc, the JSON strings are built and inserted in chunks of 50,000 rows, in one transaction:

import json
import pyodbc

odbc_conn_str = (
"DRIVER={ODBC Driver 18 for SQL Server};SERVER=localhost,1435;DATABASE=complaints;"
"UID=sa;PWD=<password>;Encrypt=yes;TrustServerCertificate=yes"
)
ids = df["complaint_id"].to_list()

with pyodbc.connect(odbc_conn_str) as conn: # autocommit is off by default
cur = conn.cursor()
cur.fast_executemany = True
cur.setinputsizes([(pyodbc.SQL_INTEGER, 0, 0), (pyodbc.SQL_VARCHAR, 8000, 0)])
for start in range(0, len(ids), 50_000):
rows = list(zip(
ids[start:start + 50_000],
map(json.dumps, embeddings[start:start + 50_000].tolist()),
))
cur.executemany("INSERT INTO dbo.complaint_embeddings VALUES (?, ?)", rows)
conn.commit()

With mssql-python, bulkcopy takes an iterable of tuples, and each vector is a Python list of floats:

import mssql_python

conn_str = "Server=localhost,1435;Database=complaints;UID=sa;PWD=<password>;Encrypt=yes;TrustServerCertificate=yes"
rows = zip(df["complaint_id"].to_list(), map(np.ndarray.tolist, embeddings))

with mssql_python.connect(conn_str, autocommit=True) as conn:
conn.cursor().bulkcopy("dbo.complaint_embeddings", rows, timeout=0)

This took 16.2 s, the conversion of each vector to a list included. Its Arrow variant, bulkcopy_arrow, rejects a fixed_size_list column for a VECTOR target.

arrow-odbc does not support fixed-size lists for insertion, so we sent JSON text with insert_into_table. 10,000 rows took 84.8 s, and we did not run the full million.

VECTOR support​

What each client did with a VECTOR(384) column, on 10 rows, with the versions listed above:

ToolRead returnsWrite as float32Write as JSON text
ArpeMSSQLfixed_size_list<float32>[384]yes, from Arrowyes, from 0.7.9
pyodbcstr (JSON)noyes
arrow-odbcstring (JSON)noyes
mssql-pythonstr (JSON), large_string with cursor.arrow()yes, bulkcopy from Python listsyes
connectorxstring (JSON)read onlyread only

Every successful write was checked against the original float32 values and was exact. ArpeMSSQL 0.7.8, used for the timings in this post, accepted JSON text for a VECTOR column only up to 4,000 characters per value, about 180 dimensions with json.dumps, so not for the 384 dimensions used here, at 7,320 characters on average. Version 0.7.9 lifts this limit: JSON text of any length loads into a VECTOR column, but the server still parses every number, so the float32 path remains much faster. The VECTOR type is recent, and support in client libraries is still being added, so this table may soon be out of date.

Appendix: searching with a question​

Here the query vector comes from a question, embedded in Python with the same model. It is bound as a parameter, a one-row Arrow record batch with a fixed_size_list<float32>[384] column, which the driver sends as a VECTOR value. The query is the same as above, with ? in place of @q:

question = "my bank charged me an overdraft fee twice for the same transaction"
q = model.encode([question]).astype(np.float32)
params = pa.record_batch({"q": pa.FixedSizeListArray.from_arrays(pa.array(q.reshape(-1)), 384)})

query = """
WITH nearest AS (
SELECT TOP (10) e.complaint_id, VECTOR_DISTANCE('cosine', e.embedding, ?) AS distance
FROM dbo.complaint_embeddings AS e
ORDER BY distance
)
SELECT c.complaint_id, c.date_received, c.company, n.distance, LEFT(c.narrative, 60) AS narrative
FROM nearest AS n
JOIN dbo.complaints AS c ON c.complaint_id = n.complaint_id
ORDER BY n.distance
"""

with dbapi.connect(driver="arpemssql", db_kwargs={"uri": uri}) as conn:
cur = conn.cursor()
cur.execute(query, params)
hits = pl.from_arrow(cur.fetch_arrow_table())

The first five rows of the result:

complaint_iddate_receivedcompanydistancenarrative
27085432017-10-21BANK OF AMERICA, NATIONAL ASSOCIATION0.0895My Bank over charge me twice for Overdraft fee which is not…
21741592016-10-22SUNTRUST BANKS, INC.0.1422Bank charged me overdraft fees despite the fact that the acc…
19011412016-04-28TD BANK US HOLDING COMPANY0.1551I overdrawn my account with a transaction and it posted and …
22077372016-11-14SUNTRUST BANKS, INC.0.1640My bank processed the same check twice, causing an overdraft…
40595992021-01-10NAVY FEDERAL CREDIT UNION0.1642I was being double charged for overdraft fees on several occ…

The query takes 0.064 s, and 0.039 s with the Wells Fargo 2019 filter. The model was already loaded on the GPU, and the time to embed the question is not included.

Try ArpeMSSQL Now

Free 30-day trial