skip to content

How does Snowflake's COPY INTO avoid reloading a file, and when does that protection lapse?

level: middleimportance: must knowfreq 68%

answer

  1. The table remembers which files it already read
  2. That memory is not permanent
  3. One COPY option deliberately switches it off
  4. 64 days, then the file looks new again
  5. FORCE = TRUE ignores load metadata

basics

~20 s

Snowflake records every file a COPY INTO loads in per-table load metadata and skips files it has already loaded. That metadata expires after 64 days, and FORCE = TRUE ignores it entirely — both paths can produce duplicate rows.

solid answer

~50 s

Each table keeps **load metadata**: the name of every file loaded into it, when it loaded, and a content signature (ETag/last-modified). A repeated `COPY INTO` over the same staged files therefore loads nothing and reports the files as already loaded — bulk loading is idempotent by default. Two things break that. First, the load metadata **expires after 64 days**; a file older than that which is still sitting in the stage can be loaded a second time, so long-lived stages plus wide `PATTERN` globs are a real duplicate-row source. Second, `FORCE = TRUE` deliberately bypasses the check. Snowflake does not enforce primary keys, so duplicates are not caught downstream — they just sit in the table. The defensive patterns are narrow, dated stage paths per load, `PURGE = TRUE` or a lifecycle rule to expire staged files, and `COPY_HISTORY` for auditing what actually landed.

code

sql · 16 lines
sql
-- dry run: parse and report errors, load nothing
COPY INTO orders FROM @raw/orders/dt=2026-08-20/
  FILE_FORMAT = (FORMAT_NAME = my_csv)
  VALIDATION_MODE = RETURN_ERRORS;

-- real load, abort on any bad row
COPY INTO orders FROM @raw/orders/dt=2026-08-20/
  FILE_FORMAT = (FORMAT_NAME = my_csv)
  ON_ERROR = ABORT_STATEMENT
  PURGE = TRUE;

-- what actually landed
SELECT file_name, row_count, status, error_count
FROM TABLE(INFORMATION_SCHEMA.COPY_HISTORY(
       TABLE_NAME => 'ORDERS',
       START_TIME => DATEADD(hour, -24, CURRENT_TIMESTAMP())));

go deeper

for a junior

Know that re-running the same COPY INTO over unchanged staged files loads nothing, and that FORCE = TRUE turns that safety off.

for a middle

Explain the mechanism — per-table load metadata keyed on file name plus content signature, 64-day retention — and what ON_ERROR values do to both the table and that metadata.

for a senior

Show how you design the pipeline so duplicates are impossible: dated immutable stage paths, purge or lifecycle expiry, staging-plus-MERGE when the source can re-deliver rows, and COPY_HISTORY as the audit trail.

for a principal

Own the correctness contract with upstream producers: immutable file naming, no in-place overwrites, agreed late-arrival and replay semantics, and a documented procedure for reloads that pairs FORCE with a targeted delete.

## The mechanism When `COPY INTO <table>` runs, Snowflake writes an entry into the target table's **load metadata** for every file it loaded: file path, load timestamp, row counts, and a content signature derived from the file's ETag and last-modified time. On the next `COPY`, each candidate file is checked against that metadata. If the path and signature match an entry, the file is skipped and reported as `LOAD_SKIPPED`/already loaded rather than re-read. That is why the canonical bulk-load loop — drop files into a stage, run the same `COPY` statement on a schedule — is safe to retry. A `COPY` that fails halfway, or a job that fires twice because an orchestrator retried it, does not duplicate data. ```sql COPY INTO orders FROM @raw/orders/ FILE_FORMAT = (FORMAT_NAME = my_csv); -- second run over the same files: 0 rows loaded, files reported as already loaded ``` ## Where it lapses **Expiry.** Load metadata for a table is retained for 64 days. After that, the entry for an old file is gone; if that file is still visible in the stage and still matches the `COPY`'s path or `PATTERN`, it is loaded again — a silent duplicate. This bites teams who never clean their stage and point `COPY` at a broad prefix. **FORCE.** `FORCE = TRUE` tells `COPY` to ignore load metadata and load everything it sees. It exists for genuine reloads (you truncated the table and want the same files back), and it is the single most common way people duplicate a fact table by accident. **Modified files.** If a file is overwritten at the same path with different content, the changed signature makes it a new file and it loads again — appending, not replacing. Snowflake does not diff contents. **No key enforcement.** Snowflake accepts `PRIMARY KEY` in DDL but does not enforce it. Nothing downstream stops a duplicate; you find it with a `GROUP BY ... HAVING COUNT(*) > 1` after the fact. ## Error handling interacts with this `ON_ERROR` decides what a bad row does to the load: - `ABORT_STATEMENT` (the default for bulk `COPY`) — the whole statement fails and nothing is committed, so load metadata records nothing. - `CONTINUE` — bad rows are skipped, good rows load, and the file is marked loaded. The rejected rows are gone unless you go looking for them. - `SKIP_FILE`, `SKIP_FILE_<n>`, `SKIP_FILE_<n>%` — the file is abandoned once the error count or percentage threshold is reached, and it is not marked as loaded, so a later run will retry it. Snowpipe defaults to `SKIP_FILE` instead, which matters when you compare the two paths. To see what went wrong, `VALIDATE(table_name, job_id => '_last')` returns the errors from a prior `COPY`, and `VALIDATION_MODE = RETURN_ERRORS` performs a dry run that parses the files and reports problems **without loading anything**. `VALIDATION_MODE` cannot be combined with a transformation-style `COPY`. ## Auditing what landed `INFORMATION_SCHEMA.COPY_HISTORY(TABLE_NAME => 'ORDERS', START_TIME => ...)` is a table function over recent loads; `SNOWFLAKE.ACCOUNT_USAGE.COPY_HISTORY` is the account-wide view with a longer retention and a latency of some hours. Both give file name, row counts, errors and status, and both are how you prove whether a suspect file was loaded once or twice. ## Designing so it cannot go wrong 1. **Write files into dated, immutable paths** — `@raw/orders/dt=2026-08-20/part-0001.csv.gz` — and point each `COPY` at that day's prefix, not at the table root. The load then cannot see anything from 64 days ago. 2. **Expire staged files** with `PURGE = TRUE` (internal stages) or a bucket lifecycle rule (external), so old files physically cannot be re-read. 3. **Never write producers that overwrite a path in place.** New content means a new file name. 4. **Treat `FORCE = TRUE` as a manual, reviewed operation**, usually paired with truncating or deleting the affected partition first. 5. **Land into a staging table, then MERGE** into the target on a business key when the source genuinely can re-deliver rows. Idempotency then comes from your key rather than from file names. ## What interviewers are actually testing They want to know whether you believe "Snowflake dedupes for me" without qualification. The right answer is: yes, by **file identity**, for 64 days, unless you tell it not to — and never by row content.

  • A producer overwrote yesterday's file at the same stage path with corrected rows. What does the next COPY do?
    It loads the file again and **appends** the corrected rows next to the original ones. The changed ETag/last-modified makes the file look new to load metadata, and `COPY` has no notion of replacing prior content. To correct data you must delete or rebuild the affected partition yourself, or land into a staging table and `MERGE` on a business key.
  • How do ON_ERROR = CONTINUE and SKIP_FILE differ in what ends up in the table and in load metadata?
    `CONTINUE` loads every good row, discards the bad ones, and marks the file as loaded — so a retry will not bring the rejected rows back. `SKIP_FILE` abandons the whole file once the threshold is hit and does **not** mark it loaded, so the same file is retried on the next run. `CONTINUE` favours availability, `SKIP_FILE` favours completeness.
  • How would you make an ingestion pipeline idempotent when the source can re-deliver the same business rows in different files?
    Stop relying on file identity. Load every file into a staging table (duplicates allowed), then `MERGE` into the target on a natural or hash business key, taking the newest version by an ingestion or event timestamp. File-level dedupe protects against replaying a file; only a key-based merge protects against the same row arriving twice under two names.

saying these in an interview costs you the question

  • Assuming COPY INTO deduplicates rows by content
  • Believing load metadata is retained forever
  • Using FORCE = TRUE routinely to 'make sure it loads'
  • Expecting a declared PRIMARY KEY to block duplicate rows
  • Thinking overwriting a staged file replaces the rows already loaded

context