COPY INTO and Auto Loader: Incremental File Loading into Unity Catalog Tables (Databricks Data Engineer Associate)
COPY INTO and Auto Loader are Databricks' two ways to load only new files from cloud object storage (S3, ADLS, GCS) or Unity Catalog volumes into Delta tables: COPY INTO is an idempotent SQL command that remembers which files it loaded into each table, and Auto Loader is a Structured Streaming source (cloudFiles) that discovers new files, infers and evolves their schema, and can run as a scheduled incremental batch. This lesson covers the COPY INTO options that control reloading, file selection, transformations, schema evolution, validation and Unity Catalog access, then Auto Loader's schema inference, evolution modes, rescued data, file detection modes and batch triggers.
On this page10 sections
- COPY INTO: idempotent incremental loading in SQL
- Reloading files with force
- Choosing which files to load: PATTERN and FILES
- Transforming data during the load with a SELECT
- Schema evolution and dry runs in COPY INTO
- Unity Catalog access for COPY INTO sources
- Auto Loader and schema inference
- Schema evolution modes and the rescued data column
- File detection modes: directory listing and file notification
- Running Auto Loader as an incremental batch, and file-handling options
- Explain how COPY INTO tracks loaded files per target table, and use force, PATTERN and FILES to control what loads
- Transform data during a COPY INTO load with a source SELECT and _metadata, and know why GROUP BY is not allowed
- Evolve a target schema with the right mergeSchema setting and check a load with VALIDATE before writing
- Authorise COPY INTO sources with READ VOLUME, READ FILES or temporary credentials
- Configure Auto Loader schema inference, schema hints, evolution modes and the rescued data column
- Choose between directory listing, managed file events and legacy file notifications
- Run Auto Loader as a scheduled incremental batch with availableNow and the file-handling options
COPY INTO: idempotent incremental loading in SQL
COPY INTO is a SQL command that loads files from a cloud storage path or Unity Catalog volume into a Delta table and skips every file it has already loaded into that table. Because of that file tracking, the command is retriable and idempotent: a rerun, whether scheduled or accidental, simply picks up the files that arrived since the last run and appends their rows; rows from earlier loads are left as they are.
COPY INTO main.bronze.orders
FROM '/Volumes/main/raw/orders/'
FILEFORMAT = CSV
FORMAT_OPTIONS ('header' = 'true')
COPY_OPTIONS ('mergeSchema' = 'true');
Three details of the file tracking decide most scenarios:
- History lives with the target table. The record of loaded files belongs to the Delta table being written. A second, new table loading the same path starts with an empty history and loads every file on its first run; nothing is shared between tables and nothing is keyed on the source path or the table's creation time.
- A modified file is still skipped. If a file with an already-loaded name is overwritten with new content, COPY INTO does not load it again; a newer modification time changes nothing.
- Deleting rows does not reset the history. Truncating the table or deleting rows leaves the record of loaded files in place, so the next normal run inserts zero rows.
COPY INTO suits SQL users loading thousands of files on a schedule. Databricks now marks it as a legacy path in places and recommends streaming tables over read_files for new SQL pipelines (see the lesson on ingestion patterns), but the exam guide still lists COPY INTO, and its behaviour is what this lesson teaches.
Reloading files with force
To load files that COPY INTO has already loaded, add COPY_OPTIONS ('force' = 'true'): it disables idempotency and loads every selected file regardless of history. That is the only switch that reloads; mergeSchema, inferSchema, parser modes and VALIDATE do not change which files count as loaded.
Because force ignores history on every run, it belongs in a one-off statement, not in a scheduled job. Left in a nightly job, it reloads the whole source folder each night and the table fills with repeated rows; removing the option restores skipping. Parser modes deal with unparsable records, not with duplicates, so they are no defence against a stray force.
Correcting a file that was replaced at the source
When a supplier overwrites an already-loaded file with corrected data under the same name, the normal job skips it. The clean fix has two steps: delete the old rows for that file (a column populated from _metadata.file_path at load time identifies them), then reload only that file with FILES = ('<name>') plus force. Scope the force to the one file: force on the whole path reloads everything under it, and the stale rows still have to be removed explicitly.
Choosing which files to load: PATTERN and FILES
COPY INTO reads every file under the source path unless you narrow it with one of two mutually exclusive clauses: PATTERN, a glob over file names, or FILES, an explicit list of file names. A statement cannot use both.
| Clause | Selects | Use it when | Limits |
|---|---|---|---|
PATTERN = 'orders_*.csv' | Every file whose name matches the glob, now and in future runs | A recurring load must pick one kind of file out of a shared folder | Needs a usable naming convention |
FILES = ('a.csv', 'b.csv') | Exactly the named files | Targeted backfills or reloads from a manifest with no shared naming pattern | At most 1,000 names per statement; split longer lists into several statements |
Neither clause chooses columns. Picking and renaming columns is the job of a SELECT in the FROM clause (next section), and the optional BY POSITION target clause maps headerless CSV columns to table columns by position; it does not filter files either.
Transforming data during the load with a SELECT
The FROM clause of COPY INTO can be a SELECT over the source path, so simple shaping happens as the files load: choosing a subset of columns, renaming with aliases, casting types and adding file metadata.
COPY INTO main.bronze.orders
FROM (
SELECT cust_no AS customer_id,
order_id,
CAST(order_ts AS TIMESTAMP) AS order_ts,
_metadata.file_path AS source_file
FROM '/Volumes/main/raw/orders/'
)
FILEFORMAT = CSV
FORMAT_OPTIONS ('header' = 'true');
- File metadata: the hidden
_metadatacolumn exposes fields such asfile_path,file_nameandfile_modification_time. It replacesinput_file_name(), which is deprecated and not supported on Unity Catalog standard access mode. - Aggregation: global aggregates (for example a single
SUMover everything loaded) are allowed, butGROUP BYis not supported. When the business needs totals per group, load the raw rows with COPY INTO, which keeps exactly-once file tracking, and aggregate in a separate query or materialized view over the bronze table. Options such as force or mergeSchema do not change what the source query may contain.
Schema evolution and dry runs in COPY INTO
COPY INTO has one option name, mergeSchema, in two places with two different jobs, and a VALIDATE clause that checks a load without writing it.
| Setting | What it does |
|---|---|
FORMAT_OPTIONS ('mergeSchema' = 'true') | Read side: infers and merges the schema across all the source files, so columns present in only some files are included |
COPY_OPTIONS ('mergeSchema' = 'true') | Write side: lets the target Delta table's schema evolve, adding new columns |
FORMAT_OPTIONS ('inferSchema' = 'true') | CSV only: infers column types instead of reading strings; it does not merge schemas or evolve the table |
COPY_OPTIONS ('force' = 'true') | Reloads already-loaded files; unrelated to schemas |
A new source field becomes a table column automatically only with the write-side setting. Since Databricks Runtime 11.3 LTS the target can also be a schemaless placeholder, created with CREATE TABLE IF NOT EXISTS main.raw.vendor_feed; and no column list. COPY INTO then derives the columns from the data, which needs both settings when files differ: read-side mergeSchema to union the columns from every file, and write-side mergeSchema to let the empty table take that schema.
VALIDATE: check without loading
VALIDATE (Databricks Runtime 10.4 LTS and above) runs the load's checks and returns a preview of rows, but writes nothing. It checks that the data parses, that its schema matches the table or could evolve to it, and that NOT NULL and CHECK constraints hold. VALIDATE ALL (the default when no count is given) checks all the data; VALIDATE 10 ROWS checks only that many rows, so a bad row further in can be missed. A validation run commits nothing to the table, so it records no files as loaded: the real run afterwards, without VALIDATE and without force, loads every file. Parser modes such as FAILFAST only change how a real, writing load treats malformed records.
Unity Catalog access for COPY INTO sources
COPY INTO into a Unity Catalog table reads its source through one of three routes, and each needs a different authorisation. Databricks recommends volumes.
| Source path | Privilege the user needs to read it | Notes |
|---|---|---|
/Volumes/<catalog>/<schema>/<volume>/... | READ VOLUME on the volume (plus USE CATALOG and USE SCHEMA) | Recommended; the grant covers nested folders. WRITE VOLUME is for adding or removing files. |
A cloud URI (s3://, abfss://, gs://) under an external location | READ FILES on the external location | BROWSE shows only metadata; WRITE FILES and CREATE EXTERNAL TABLE grant other things |
| A cloud URI with no external location | Inline temporary credentials in the source clause | For one-off loads; short-lived keys, scoped to the statement |
COPY INTO main.audit.findings
FROM 's3://auditor-share/batch-07/' WITH (
CREDENTIAL (AWS_ACCESS_KEY = '...', AWS_SECRET_KEY = '...',
AWS_SESSION_TOKEN = '...')
)
FILEFORMAT = JSON;
On Azure the inline credential is AZURE_SAS_TOKEN. SELECT is a table and view privilege and does not apply to volumes. Legacy workarounds such as DBFS mounts or keys in a cluster's Spark configuration sit outside Unity Catalog governance and expose the credentials to other users; an external volume is not an option either when no external location exists, because a volume must live inside one. The target table needs its own privileges as usual.
Auto Loader and schema inference
Auto Loader is the cloudFiles Structured Streaming source: it discovers new files in cloud storage or a volume and processes each one once, tracking them in the stream's checkpoint (checkpoints are covered in the ingestion patterns lesson). It can infer the table's schema and evolve it as files change.
(spark.readStream.format("cloudFiles")
.option("cloudFiles.format", "json")
.option("cloudFiles.schemaLocation", "/Volumes/main/raw/_meta/orders")
.option("cloudFiles.inferColumnTypes", "true")
.option("cloudFiles.schemaHints", "unit_price DECIMAL(10,2)")
.load("/Volumes/main/raw/orders/")
.writeStream
.option("checkpointLocation", "/Volumes/main/raw/_meta/orders_ckpt")
.option("mergeSchema", "true")
.trigger(availableNow=True)
.toTable("main.bronze.orders"))
cloudFiles.schemaLocationis what enables inference and evolution: Auto Loader writes each schema version to a_schemasfolder under it and reads it back on restart instead of inferring from scratch.- Strings by default: for JSON, CSV and XML, inference types every column (including nested JSON fields) as
STRING, to avoid type mismatches as files change. SetcloudFiles.inferColumnTypesto true to detect types. Parquet and Avro always use the types encoded in the files. - Sampling: inference reads a sample of the first files (by default up to 50 GB or 1,000 files). It does not retype columns later as more data arrives.
cloudFiles.schemaHintstakes SQL schema syntax for a few named columns and fixes their types while everything else is still inferred, for example a decimal where early files held only whole numbers. Passing a full schema with.schema()instead turns inference off for every column.
Schema evolution modes and the rescued data column
cloudFiles.schemaEvolutionMode decides what Auto Loader does when files contain fields the schema does not have. Its default depends on whether the schema is inferred or provided.
| Mode | On a new field | Stream | Default when |
|---|---|---|---|
addNewColumns | Adds it to the schema in the schema location | Fails with UnknownFieldException; the next start continues with the new column | The schema is inferred |
rescue | Never changes the schema; values go to _rescued_data | Keeps running | Never |
failOnNewColumns | Never changes the schema | Fails and will not restart until someone updates the schema or removes the file | Never |
none | Ignores it; nothing is rescued unless rescuedDataColumn is set | Keeps running | A schema is provided with .schema() |
The addNewColumns failure is by design: production jobs recover with task retries on the Lakeflow Jobs task (Lakeflow Spark Declarative Pipelines restart automatically), and the Delta write needs mergeSchema so the new column can land. Never "fix" it with a fresh checkpoint, which makes every existing file look new. addNewColumns never changes the type of an existing column; a preview mode, addNewColumnsWithTypeWidening, also widens types. When columns may change only after human review but ingestion must not stop or lose data, rescue is the fit.
What lands in _rescued_data
When Auto Loader infers a schema it adds a _rescued_data column by default. It captures, as a JSON blob that includes the source file path:
- fields missing from the schema;
- values whose type does not match (text in a numeric column, which would otherwise arrive as null);
- fields whose name differs only by case (
CustomerIDagainstcustomerId), which are rescued rather than added as a new column.
With a provided schema, rescue is off until you set the rescuedDataColumn option; that is how silently dropped values (new fields in none mode, or type mismatches) are caught without changing the table's columns. The rescued column cannot hold corrupt records that are not valid JSON at all; those are what _corrupt_record and the parser modes deal with. With rescue enabled, a type mismatch is no longer malformed, so FAILFAST does not fail on it and DROPMALFORMED does not drop it, but a cut-off, unparsable line still fails a FAILFAST stream.
File detection modes: directory listing and file notification
Auto Loader finds new files in one of two ways. Directory listing is the default: it lists the input path and needs only read access to the storage. File notification mode reacts to storage events instead of listing, which keeps discovery cheap when directories hold millions of files.
| Mode | How it is turned on | Setup and permissions | Fits |
|---|---|---|---|
| Directory listing (default) | No options | Read access only | Modest folders; slows and costs more as directories grow |
| Managed file events (recommended) | Enable file events on the Unity Catalog external location, then set cloudFiles.useManagedFileEvents to true (Databricks Runtime 14.3 LTS and above) | One notification setup per external location, managed by Databricks with the storage credential's permissions and shared by every stream reading it | Many streams on one location, volume paths, very large directories |
| Legacy file notification | cloudFiles.useNotifications = true | Each stream creates its own notification and queue (for example an SNS topic and SQS queue on AWS), needing permission to create cloud resources | Older setups; not supported on volume paths; limited to about 100 notification pipelines per S3 bucket |
When legacy notifications hit the per-bucket limit or the cloud team refuses per-stream queues, the answer is managed file events on the external location. A notification queue (cloudFiles.queueUrl) belongs to one stream: each message is consumed by a single reader, so a queue cannot be shared between streams. cloudFiles.backfillInterval adds periodic full listings to catch files that notifications missed; incremental listing (cloudFiles.useIncrementalListing) is deprecated; and cloudFiles.maxFilesPerTrigger changes batch size, not listing cost. A Lakeflow Jobs file arrival trigger is a different thing: it starts job runs when files land, rather than being how a stream discovers files.
Running Auto Loader as an incremental batch, and file-handling options
To run Auto Loader as a scheduled batch, use trigger(availableNow=True): the run processes every file that arrived before the query started, in as many micro-batches as the rate limits require, then stops so the job cluster can terminate.
| Trigger | Behaviour |
|---|---|
availableNow=True | Processes the backlog present at start in rate-limited micro-batches, then stops |
once=True (deprecated) | Processes the backlog in one batch, ignoring rate limits, then stops |
processingTime='1 hour' | Runs a micro-batch every interval and never stops |
continuous | Low-latency continuous processing that never stops; not supported by the cloudFiles source |
The default rate limit is 1,000 files per micro-batch (cloudFiles.maxFilesPerTrigger; cloudFiles.maxBytesPerTrigger caps bytes). A backlog of 2,500 files is therefore processed in several micro-batches in one run. A file that lands after the run started is not part of that run, but it is not marked as processed either: the next run picks it up as new.
File-handling options
| Option | What it does |
|---|---|
cloudFiles.includeExistingFiles (default true) | Whether files already in the path are processed. Read only on the stream's first start; changing it for a stream that resumes from an existing checkpoint has no effect. To ingest only new files, start with a new checkpoint and the option set to false. |
cloudFiles.allowOverwrites (default false) | By default a file path is processed once, so a file rewritten in place is ignored. True lets each changed version be picked up, appended as new rows. |
cloudFiles.cleanSource (Databricks Runtime 16.4 and above) | OFF (default), DELETE or MOVE processed files after cloudFiles.cleanSource.retentionDuration (default 30 days). MOVE sends them to cloudFiles.cleanSource.moveDestination, a cloud path or volume, which archives them and keeps the landing folder small. |
Choosing between COPY INTO, Auto Loader and streaming tables by volume and frequency, and triggered versus continuous scheduling in general, are covered in the lesson on ingestion patterns and choosing an ingestion method.
Tip. Questions are scenarios about a load that misbehaves or a requirement to meet: a rerun, a truncated table, a corrected file, a new field, a rename, a permission error, a slow directory listing or a scheduled batch. The options are usually real COPY INTO clauses or Auto Loader options that each do something sensible, so you need to know precisely what each one controls: file history versus schema versus parsing versus file discovery. Expect near-twins such as the two mergeSchema placements, READ VOLUME versus READ FILES, rescue versus addNewColumns versus none, managed file events versus legacy notifications, and availableNow versus the once trigger, and watch for the stated constraint (keep running, no schema change without review, no new cloud resources, archive rather than delete) that decides between workable options.
- COPY INTO skips files already loaded into the same target table, even if they were modified later; truncating the table does not reset that history, and a new table starts with none.
- COPY_OPTIONS ('force' = 'true') is the only way to reload loaded files; keep it out of scheduled jobs, and pair it with FILES to reload one corrected file.
- PATTERN (a glob) and FILES (at most 1,000 names) select files and cannot be combined; a source SELECT selects, renames and casts columns and can add _metadata.file_path, but GROUP BY is not supported.
- mergeSchema in FORMAT_OPTIONS merges source file schemas; in COPY_OPTIONS it evolves the target table. VALIDATE checks parsing, schema and constraints, writes nothing and records no files.
- Volumes need READ VOLUME, external-location paths need READ FILES, and unregistered buckets can be read once with inline temporary credentials.
- Auto Loader infers JSON, CSV and XML columns as strings unless cloudFiles.inferColumnTypes is true; schemaHints pin chosen columns, and schemaLocation stores every schema version.
- addNewColumns (default when inferring) fails then continues after a restart; rescue keeps running without changing the schema; failOnNewColumns needs manual action; none (default with a provided schema) drops new fields unless rescuedDataColumn is set.
- _rescued_data holds missing fields, type mismatches and case mismatches with the file path, but not corrupt records.
- Directory listing is the default and needs read access only; managed file events on the external location are the recommended notification mode and work with volumes.
- trigger(availableNow=True) processes the backlog present at start in micro-batches of up to 1,000 files by default, then stops; includeExistingFiles applies only on a first start.
Frequently asked questions
Does COPY INTO load the same file twice?
No. COPY INTO is idempotent: it records the files it has loaded into each target table and skips them on later runs, even if a file was modified after it was loaded. It reloads files only when COPY_OPTIONS ('force' = 'true') is set. The history belongs to the target table, so a different, new table loading the same path will load every file.
What is the difference between mergeSchema in FORMAT_OPTIONS and COPY_OPTIONS?
In FORMAT_OPTIONS, mergeSchema makes COPY INTO infer and merge the schema across all the source files it reads. In COPY_OPTIONS, it allows the target Delta table's schema to evolve, for example to add a new column. Loading files with differing columns into a schemaless placeholder table created with CREATE TABLE IF NOT EXISTS needs both.
How do I reload a single file with COPY INTO?
Run COPY INTO with FILES naming that file and COPY_OPTIONS ('force' = 'true'). Force disables the file-skipping behaviour and FILES limits the reload to that one file. If the file replaced an earlier version, delete the old rows first, for example using a source-file column captured from _metadata.file_path.
What does COPY INTO VALIDATE do?
VALIDATE runs COPY INTO's checks without writing to the table: it confirms the data parses, that its schema matches or could evolve to the table's, and that NOT NULL and CHECK constraints hold, and it returns a preview of rows. VALIDATE ALL checks all the data; VALIDATE n ROWS checks only n rows. Because nothing is committed, no files are recorded as loaded.
Why are all my Auto Loader columns strings?
For JSON, CSV and XML files, Auto Loader's schema inference types every column as a string by default, to avoid type mismatches as files change. Set cloudFiles.inferColumnTypes to true to infer real types, and use cloudFiles.schemaHints to fix the type of particular columns. Parquet and Avro files keep the types encoded in them.
What is the default Auto Loader schema evolution mode?
When Auto Loader infers the schema, cloudFiles.schemaEvolutionMode defaults to addNewColumns: on a new field it updates the stored schema and stops the stream, and the next start continues with the new column. When you provide a schema with .schema(), the default is none, which ignores new fields and rescues nothing unless the rescuedDataColumn option is set.
What is the difference between Auto Loader directory listing and file notification mode?
Directory listing, the default, lists the input path to find new files and needs only read access, but gets slower as directories grow. File notification mode uses storage events instead. Databricks recommends managed file events, enabled once on a Unity Catalog external location and used with cloudFiles.useManagedFileEvents; the legacy cloudFiles.useNotifications mode creates a queue per stream, needs permission to create cloud resources and does not support volumes.
How do I run Auto Loader as a batch job?
Use trigger(availableNow=True). Each run processes every file that arrived before it started, in micro-batches that respect cloudFiles.maxFilesPerTrigger (1,000 files by default) and maxBytesPerTrigger, then stops so the job cluster can terminate. Files that arrive during a run are picked up by the next one. The older once trigger is deprecated and ignores the rate limits.
Source
This lesson covers the "Data Ingestion and Loading" domain of the official Databricks DEA exam guide. Vendors revise their guides — check the source for the current version.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.