Bryan
12/10/2025, 7:18 PMMarvin
12/10/2025, 7:18 PMMarvin
12/10/2025, 7:24 PMSqlAlchemyConnector block.
Assumptions:
- You’re on Prefect 3.x
- You already have a SqlAlchemyConnector block for SQL Server
- You can create/use a SnowflakeConnector block (shown below if you don’t have one yet)
Option A (recommended first): Stream SQL Server -> chunked DataFrames -> write_pandas to Snowflake
- Very fast (Arrow-optimized)
- Memory bounded by chunk size (O(batch_size))
- No local files
from prefect import flow, task
from prefect_sqlalchemy import SqlAlchemyConnector
from prefect_snowflake.database import SnowflakeConnector
from sqlalchemy import text
from snowflake.connector.pandas_tools import write_pandas
import pandas as pd
def stream_sqlserver_as_dfs(sql_block_name: str, query: str, batch_size: int = 50_000):
"""
Stream rows out of SQL Server using server-side cursors and yield pandas DataFrames.
Memory stays bounded by batch_size; no local files used.
"""
sql = SqlAlchemyConnector.load(sql_block_name)
with sql.get_connection(begin=False) as conn:
result = conn.execution_options(stream_results=True).execute(text(query))
cols = result.keys()
batch = []
for row in result:
batch.append(tuple(row))
if len(batch) >= batch_size:
yield pd.DataFrame(batch, columns=cols)
batch = []
if batch:
yield pd.DataFrame(batch, columns=cols)
@task(retries=3, retry_delay_seconds=30)
def append_df_to_snowflake(sf_block_name: str, df: pd.DataFrame, table: str) -> int:
"""
Chunked, Arrow-accelerated write to Snowflake.
"""
sf = SnowflakeConnector.load(sf_block_name)
with sf.get_connection() as conn:
success, nchunks, nrows, _ = write_pandas(
conn,
df,
table_name=table,
database=sf.database,
schema=sf.schema_,
)
if not success:
raise RuntimeError("write_pandas reported failure")
return nrows
@flow(name="sqlserver-to-snowflake-streaming")
def migrate_sqlserver_to_snowflake(
sql_block_name: str = "sqlserver-block",
sf_block_name: str = "snowflake-connector",
source_query: str = "SELECT * FROM dbo.large_table ORDER BY id",
target_table: str = "STAGING.LARGE_TABLE",
batch_size: int = 50_000,
):
total = 0
for df in stream_sqlserver_as_dfs(sql_block_name, source_query, batch_size):
total += append_df_to_snowflake(sf_block_name, df, target_table)
return {"rows_loaded": total}
Tuning tips for Option A
- Start with batch_size 25k–100k depending on row width and available RAM (e.g., 50k is a solid default).
- Use ORDER BY on a stable key to make resuming idempotent (log last max ID).
- Size your Snowflake warehouse appropriately for bulk loads.
Option B (largest volumes, zero local disk): Stream -> write CSV chunks to cloud storage -> Snowflake COPY INTO
- Ideal when moving 100s of GB+ or when you want resilient, restartable bulk loads
- Uses virtually no local disk; memory bounded by chunk size
- Requires an external stage (e.g., S3/Azure/GCS) or internal stage with files uploaded via cloud SDK
Example with S3 external stage (no local files):
```
import io, csv, gzip, boto3
from prefect import flow, task
from prefect_sqlalchemy import SqlAlchemyConnector
from prefect_snowflake.database import SnowflakeConnector, snowflake_query
from sqlalchemy import text
def stream_to_s3_chunks(sql_block_name, query, bucket, prefix, batch_size=100_000, gzip_compress=True):
sql = SqlAlchemyConnector.load(sql_block_name)
s3 = boto3.client("s3")
with sql.get_connection(begin=False) as conn:
result = conn.execution_options(stream_results=True).execute(text(query))Marvin
12/10/2025, 7:24 PMWhy these work well
- SQL Server reading is truly streamed via `execution_options(stream_results=True)`, so you never materialize the full result in memory.
- Option A uses Snowflake’s Arrow-based `write_pandas` path for fast, chunked inserts with no local files.
- Option B pushes chunks straight to object storage and uses Snowflake’s highly optimized `COPY INTO`, which is resilient and restartable.
Optional: creating the SnowflakeConnector block (if you don’t have one yet)
from prefect_snowflake.credentials import SnowflakeCredentials
from prefect_snowflake.database import SnowflakeConnector
creds = SnowflakeCredentials(
account="abc12345.us-east-1",
user="etl_user",
password="***",
role="TRANSFORMER",
)
creds.save("sf-creds", overwrite=True)
sf = SnowflakeConnector(
credentials=creds,
database="ANALYTICS",
schema_="STAGING",
warehouse="COMPUTE_WH",
)
sf.save("snowflake-connector", overwrite=True)
```
Quick tuning checklist
- Use ORDER BY and log the last processed key to resume safely.
- Start with 25k–100k row batches; reduce for very wide rows.
- Size your Snowflake warehouse up during the load; scale down after.
- Add retries to the write tasks; Snowflake COPY is already resilient.Marvin
12/10/2025, 7:24 PM