Warehouse recipes
Load exported data into Snowflake, Amazon Redshift, Databricks, DuckDB and Looker Studio — minimal, working setups from a cloud storage bucket, BigQuery or Google Sheets.
These recipes take data that a cloud storage bucket, BigQuery or Google Sheets destination delivers and put it where your team queries it. They are deliberately minimal — a starting point to adapt, not a full pipeline.
Before you start
Section titled “Before you start”- Use Parquet for bucket destinations feeding a warehouse. Its columns are typed, so nothing
needs parsing, and text arrives exactly as measured. (CSV works, but text that looks like a
formula carries a leading
'— see CSV.) - Know the layout. A bucket destination writes
<folder>/<dataset>/dt=<YYYY-MM-DD>/<run_id>.parquet, and a manifest at<folder>/_manifests/<run_id>.jsononce every file of the run is complete — see what gets written. The examples use the folderisearchfromand theposition_tracker.resultsdataset, whose folder isposition_tracker-results. - Read with your own credentials. The key you gave iSearchFrom only needs to write. Give your warehouse its own read access to the bucket.
- De-duplicate on
row_id. Delivery is at least once: a backfill or a retried run can deliver a row twice, always with the samerow_id. Every recipe below ends with a de-duplicated view or query — build your reports on that.
Snowflake
Section titled “Snowflake”Point an external stage at the folder, then load it with COPY INTO — by hand, on a schedule, or
continuously with Snowpipe.
CREATE FILE FORMAT isf_parquet TYPE = PARQUET;
-- Amazon S3. For Google Cloud Storage use STORAGE_PROVIDER = 'GCS' and gcs:// URLs,-- then grant the service account Snowflake creates read access to the bucket.CREATE STORAGE INTEGRATION isf_bucket TYPE = EXTERNAL_STAGE STORAGE_PROVIDER = 'S3' ENABLED = TRUE STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123456789012:role/snowflake-isearchfrom' STORAGE_ALLOWED_LOCATIONS = ('s3://your-bucket/isearchfrom/');
CREATE STAGE isf_stage URL = 's3://your-bucket/isearchfrom/' STORAGE_INTEGRATION = isf_bucket FILE_FORMAT = isf_parquet;
-- A table shaped like the files.CREATE TABLE position_tracker_results USING TEMPLATE ( SELECT ARRAY_AGG(OBJECT_CONSTRUCT(*)) FROM TABLE(INFER_SCHEMA( LOCATION => '@isf_stage/position_tracker-results/', FILE_FORMAT => 'isf_parquet', IGNORE_CASE => TRUE )));
-- Load. COPY INTO remembers the files it has loaded and skips them next time.COPY INTO position_tracker_results FROM @isf_stage/position_tracker-results/ FILE_FORMAT = (FORMAT_NAME = 'isf_parquet') MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE;Load by manifest. To load only complete runs, read each new manifest and pass its files to
COPY INTO. The manifest’s keys include your folder; drop it, since the stage already points there:
COPY INTO position_tracker_results FROM @isf_stage FILES = ('position_tracker-results/dt=2026-09-30/0199a7c2-5b1e-7c3a-9f10-3d2b8e6a4c11.parquet') FILE_FORMAT = (FORMAT_NAME = 'isf_parquet') MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE;Or load continuously with Snowpipe. Create a pipe over the same COPY INTO, then add an S3 event
notification for new objects under isearchfrom/position_tracker-results/ that targets the queue in
the pipe’s notification_channel (from SHOW PIPES). Snowpipe loads every file as it lands, including
files of a run that later fails — the de-duplicated view below takes care of those.
CREATE PIPE isf_position_tracker_results AUTO_INGEST = TRUE AS COPY INTO position_tracker_results FROM @isf_stage/position_tracker-results/ FILE_FORMAT = (FORMAT_NAME = 'isf_parquet') MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE;
CREATE VIEW position_tracker_results_dedup ASSELECT * FROM position_tracker_resultsQUALIFY ROW_NUMBER() OVER (PARTITION BY row_id ORDER BY measured_at) = 1;Amazon Redshift
Section titled “Amazon Redshift”Redshift loads Parquet with COPY … FORMAT PARQUET. It matches columns by position, so the table
must have the file’s columns in the file’s order — the dataset reference
order, or the columns list of the manifest.
CREATE TABLE isf_position_tracker_results ( row_id VARCHAR(MAX) NOT NULL, measured_at TIMESTAMP NOT NULL, -- UTC run_id VARCHAR(MAX), campaign_id VARCHAR(MAX), campaign_name VARCHAR(MAX), project_id VARCHAR(MAX), country VARCHAR(MAX), language VARCHAR(MAX), device VARCHAR(MAX), search_engine VARCHAR(MAX), city VARCHAR(MAX), keyword VARCHAR(MAX), domain VARCHAR(MAX), position BIGINT, page BIGINT, url VARCHAR(MAX), title VARCHAR(MAX), description VARCHAR(MAX));CREATE TABLE isf_position_tracker_results_staging (LIKE isf_position_tracker_results);
-- For each file listed in a new manifest:COPY isf_position_tracker_results_stagingFROM 's3://your-bucket/isearchfrom/position_tracker-results/dt=2026-09-30/0199a7c2-5b1e-7c3a-9f10-3d2b8e6a4c11.parquet'IAM_ROLE 'arn:aws:iam::123456789012:role/redshift-isearchfrom'FORMAT AS PARQUET;
-- Then fold the staged rows in, replacing any row already loaded.BEGIN;DELETE FROM isf_position_tracker_resultsUSING isf_position_tracker_results_staging sWHERE isf_position_tracker_results.row_id = s.row_id;INSERT INTO isf_position_tracker_results SELECT * FROM isf_position_tracker_results_staging;TRUNCATE isf_position_tracker_results_staging;COMMIT;- List columns (
string_array, e.g. inai_visibility.mentions) load into aSUPERcolumn when you addSERIALIZETOJSON:FORMAT AS PARQUET SERIALIZETOJSON. JSON columns arrive as text; useJSON_PARSEto turn them intoSUPER. - The bucket must be in the same AWS Region as the cluster.
- When a dataset gains a column, it is added at the end of the file. Add it to both tables with
ALTER TABLE … ADD COLUMNbefore the next load. The same applies if you change the column pick. - From CSV instead:
FORMAT AS CSV IGNOREHEADER 1 GZIP TIMEFORMAT 'auto'.
Databricks
Section titled “Databricks”Auto Loader picks up new files in the folder incrementally and remembers what it has loaded. With Unity Catalog, first create an external location for the bucket.
( spark.readStream.format("cloudFiles") .option("cloudFiles.format", "parquet") .option("cloudFiles.schemaLocation", "/Volumes/main/isearchfrom/_meta/position_tracker_results/schema") .option("cloudFiles.partitionColumns", "dt") .load("s3://your-bucket/isearchfrom/position_tracker-results/") .writeStream .option("checkpointLocation", "/Volumes/main/isearchfrom/_meta/position_tracker_results/checkpoint") .trigger(availableNow=True) .toTable("main.isearchfrom.position_tracker_results"))Run it as a scheduled job after your destination’s schedule. With availableNow, each run loads
what is new and stops. When a dataset gains a column, Auto Loader stops the stream once with a
schema change and picks the column up when the job runs again.
CREATE OR REPLACE VIEW main.isearchfrom.position_tracker_results_dedup ASSELECT * FROM main.isearchfrom.position_tracker_resultsQUALIFY ROW_NUMBER() OVER (PARTITION BY row_id ORDER BY measured_at) = 1;For Google Cloud Storage, use the gs:// path.
DuckDB
Section titled “DuckDB”DuckDB queries the bucket in place — no loading step. Good for analysis on a laptop and for quick checks of what was delivered.
INSTALL httpfs;LOAD httpfs;
CREATE SECRET isf ( TYPE s3, KEY_ID 'AKIA…', SECRET '…', REGION 'eu-central-1');
SELECT keyword, domain, position, measured_atFROM read_parquet( 's3://your-bucket/isearchfrom/position_tracker-results/*/*.parquet', hive_partitioning = true)WHERE dt >= '2026-09-01'QUALIFY row_number() OVER (PARTITION BY row_id ORDER BY measured_at) = 1ORDER BY measured_at DESC;- Other providers: for Cloudflare R2 use
TYPE r2withACCOUNT_IDandr2://paths; for Google Cloud StorageTYPE gcswith an HMAC key andgs://paths; for Wasabi, Backblaze B2, DigitalOcean Spaces and MinIO addENDPOINT 's3.eu-central-1.wasabisys.com'(the host, withouthttps://) and, where path-style is used,URL_STYLE 'path'. dtcomes from the folder names, so filtering on it reads only those days.- CSV files work the same way with
read_csv('…/*/*.csv.gz').
Looker Studio
Section titled “Looker Studio”Looker Studio doesn’t read files from a bucket; connect it to a BigQuery or Google Sheets destination instead.
From BigQuery — the better choice for anything beyond a small report:
-
In Looker Studio, Add data → BigQuery, then choose your project, the export’s dataset and a table such as
isf_position_tracker__checks. -
Use
measured_atas the date range dimension, so report date filters only scan those days of the day-partitioned table. -
To drop repeated rows, choose Custom query instead of a table:
SELECT * FROM `acme-analytics.isearchfrom.isf_position_tracker__checks`WHERE measured_at >= TIMESTAMP(PARSE_DATE('%Y%m%d', @DS_START_DATE))QUALIFY ROW_NUMBER() OVER (PARTITION BY row_id ORDER BY measured_at) = 1Tick Enable date range parameters so
@DS_START_DATEfollows the report’s date range.
From Google Sheets:
- Add data → Google Sheets, choose the export’s spreadsheet and a tab such as
position_tracker.checks, and keep Use first row as headers on. - Timestamps in Sheets are ISO-8601 text. If Looker Studio reads
measured_atas text, add a calculated fieldPARSE_DATETIME("%Y-%m-%dT%H:%M:%S", LEFT(measured_at, 19))and use it as the date.