Is there an existing issue for this?
Category of Bug / Issue
Profiler bug
Current Behavior
On a high-volume Snowflake account, execute-database-profiler exhausts client memory and the host process dies. On a VDI this took the whole session host down rather than producing a Python traceback.
The Snowflake extract has no streaming anywhere in its path. A single step's result set is fully buffered and then materialized two more times before it reaches disk:
-
Unbounded query. query_history.sql selects every query in SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY over a 90-day window — ~30 columns, no LIMIT, plus an ORDER BY START_TIME DESC.
-
Full buffer into Python objects. _BaseConnector.fetch calls result.fetchall() — no fetchmany, no cursor iteration, no server-side paging:
# src/databricks/labs/lakebridge/connections/database_manager.py
with Session(self.engine) as session, session.begin():
result = session.execute(text(query))
return FetchResult(list(result.keys()), result.fetchall())
-
Second copy into pandas. FetchResult.to_df() copies the whole thing again:
def to_df(self) -> pd.DataFrame:
return pd.DataFrame.from_records(self.rows, columns=list(self.columns))
-
Third materialization into DuckDB. PipelineClass._save_to_db then runs CREATE TABLE {step} AS SELECT * FROM _result_frame (or INSERT INTO ... SELECT *) against the frame.
Steps 2–4 are all live simultaneously, so peak RSS is roughly 2–3x the decoded result set. ACCOUNT_USAGE.QUERY_HISTORY on a busy account is easily tens of millions of rows over 90 days, and as Python row objects that is many GB before pandas takes its copy.
This is not specific to Snowflake — _BaseConnector.fetch is shared by the Oracle and Teradata connectors, and the MSSQL and Redshift connectors have their own fetchall() implementations with the same shape. Snowflake just hits it first because ACCOUNT_USAGE is the largest default extract.
Expected Behavior
A profiler extract should complete in bounded memory regardless of source-account size. Memory use should be a function of batch size, not result-set size.
Steps To Reproduce
- Configure the Snowflake profiler against an account with a high query volume (enough that 90 days of
ACCOUNT_USAGE.QUERY_HISTORY is in the tens of millions of rows).
- Run
databricks labs lakebridge execute-database-profiler.
- The
query_history step grows unboundedly in RSS and the process is killed by the OS / the host runs out of memory.
Relevant log output or Exception details
No Python traceback was captured — the host ran out of memory and the session
terminated before the interpreter could unwind.
Proposed approach
Sketching this rather than prescribing it, since it touches the shared pipeline framework:
- Add a batched fetch path to
DatabaseConnector (e.g. fetch_batches(query, size) -> Iterator[FetchResult]) backed by cursor.fetchmany() / SQLAlchemy's yield_per, keeping the existing fetch() for small steps.
- Have
_execute_sql_step / _save_to_db consume batches and append to DuckDB per batch inside a single transaction, so nothing larger than one batch is resident.
- Prefer
CREATE TABLE ... AS SELECT from the first batch, then INSERT subsequent batches, dropping the intermediate pandas copy where possible — DuckDB can ingest Arrow or record batches directly.
- Consider a configurable batch size with a conservative default.
Related: #2486 (configurable lookback window). That issue would reduce how often this is hit, but not fix it — the failure is a function of rows returned, not of the window, so a large enough account fails at 30 days too. The two are complementary.
Logs Confirmation
Sample Query
-- src/databricks/labs/lakebridge/resources/assessments/snowflake/query_history.sql
SELECT QUERY_ID, QUERY_TYPE, DATABASE_NAME, ... -- ~30 columns
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE START_TIME >= DATEADD('day', -90, CURRENT_TIMESTAMP())
ORDER BY START_TIME DESC;
Operating System
Windows
Version
latest via Databricks CLI
Is there an existing issue for this?
Category of Bug / Issue
Profiler bug
Current Behavior
On a high-volume Snowflake account,
execute-database-profilerexhausts client memory and the host process dies. On a VDI this took the whole session host down rather than producing a Python traceback.The Snowflake extract has no streaming anywhere in its path. A single step's result set is fully buffered and then materialized two more times before it reaches disk:
Unbounded query.
query_history.sqlselects every query inSNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORYover a 90-day window — ~30 columns, noLIMIT, plus anORDER BY START_TIME DESC.Full buffer into Python objects.
_BaseConnector.fetchcallsresult.fetchall()— nofetchmany, no cursor iteration, no server-side paging:Second copy into pandas.
FetchResult.to_df()copies the whole thing again:Third materialization into DuckDB.
PipelineClass._save_to_dbthen runsCREATE TABLE {step} AS SELECT * FROM _result_frame(orINSERT INTO ... SELECT *) against the frame.Steps 2–4 are all live simultaneously, so peak RSS is roughly 2–3x the decoded result set.
ACCOUNT_USAGE.QUERY_HISTORYon a busy account is easily tens of millions of rows over 90 days, and as Python row objects that is many GB before pandas takes its copy.This is not specific to Snowflake —
_BaseConnector.fetchis shared by the Oracle and Teradata connectors, and the MSSQL and Redshift connectors have their ownfetchall()implementations with the same shape. Snowflake just hits it first becauseACCOUNT_USAGEis the largest default extract.Expected Behavior
A profiler extract should complete in bounded memory regardless of source-account size. Memory use should be a function of batch size, not result-set size.
Steps To Reproduce
ACCOUNT_USAGE.QUERY_HISTORYis in the tens of millions of rows).databricks labs lakebridge execute-database-profiler.query_historystep grows unboundedly in RSS and the process is killed by the OS / the host runs out of memory.Relevant log output or Exception details
Proposed approach
Sketching this rather than prescribing it, since it touches the shared pipeline framework:
DatabaseConnector(e.g.fetch_batches(query, size) -> Iterator[FetchResult]) backed bycursor.fetchmany()/ SQLAlchemy'syield_per, keeping the existingfetch()for small steps._execute_sql_step/_save_to_dbconsume batches and append to DuckDB per batch inside a single transaction, so nothing larger than one batch is resident.CREATE TABLE ... AS SELECTfrom the first batch, thenINSERTsubsequent batches, dropping the intermediate pandas copy where possible — DuckDB can ingest Arrow or record batches directly.Related: #2486 (configurable lookback window). That issue would reduce how often this is hit, but not fix it — the failure is a function of rows returned, not of the window, so a large enough account fails at 30 days too. The two are complementary.
Logs Confirmation
--debuglsp-server.logunder USER_HOME/.databricks/labs/remorph-transpilers/<converter_name>/lib/lsp-server.logSample Query
Operating System
Windows
Version
latest via Databricks CLI