SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Data Ingestion and Loading

COPY INTO and Auto Loader: Incremental File Loading into Unity Catalog Tables (Databricks Data Engineer Associate)

15 min readDatabricks DEA · Data Ingestion and LoadingUpdated

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.

What you’ll learn
  • 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.

ClauseSelectsUse it whenLimits
PATTERN = 'orders_*.csv'Every file whose name matches the glob, now and in future runsA recurring load must pick one kind of file out of a shared folderNeeds a usable naming convention
FILES = ('a.csv', 'b.csv')Exactly the named filesTargeted backfills or reloads from a manifest with no shared naming patternAt 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 _metadata column exposes fields such as file_path, file_name and file_modification_time. It replaces input_file_name(), which is deprecated and not supported on Unity Catalog standard access mode.
  • Aggregation: global aggregates (for example a single SUM over everything loaded) are allowed, but GROUP BY is 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.

SettingWhat 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 pathPrivilege the user needs to read itNotes
/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 locationREAD FILES on the external locationBROWSE shows only metadata; WRITE FILES and CREATE EXTERNAL TABLE grant other things
A cloud URI with no external locationInline temporary credentials in the source clauseFor 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.schemaLocation is what enables inference and evolution: Auto Loader writes each schema version to a _schemas folder 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. Set cloudFiles.inferColumnTypes to 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.schemaHints takes 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.

ModeOn a new fieldStreamDefault when
addNewColumnsAdds it to the schema in the schema locationFails with UnknownFieldException; the next start continues with the new columnThe schema is inferred
rescueNever changes the schema; values go to _rescued_dataKeeps runningNever
failOnNewColumnsNever changes the schemaFails and will not restart until someone updates the schema or removes the fileNever
noneIgnores it; nothing is rescued unless rescuedDataColumn is setKeeps runningA 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 (CustomerID against customerId), 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.

ModeHow it is turned onSetup and permissionsFits
Directory listing (default)No optionsRead access onlyModest 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 itMany streams on one location, volume paths, very large directories
Legacy file notificationcloudFiles.useNotifications = trueEach stream creates its own notification and queue (for example an SNS topic and SQS queue on AWS), needing permission to create cloud resourcesOlder 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.

TriggerBehaviour
availableNow=TrueProcesses 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
continuousLow-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

OptionWhat 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.

Key takeaways
  • 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.

Test yourself on this topic
Practice now

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?