SQLMentor // learn sql

External Tables

An external table looks like a regular Oracle table but its data lives in files on the operating system, not inside the database. You can SELECT * FROM ext_csv_data and get back rows parsed from a CSV file on disk โ€” without ever loading the data into the database.

External tables are Oracle's clean alternative to running SQL*Loader, file scraping scripts, or external ETL tools for many common load and read workloads.

What External Tables Are Good For

1. Loading data from CSV / flat files

The classic ETL pattern: dump from one system into a flat file, copy the file to an Oracle directory, define an external table over it, then INSERT INTO target SELECT * FROM ext_table. Simpler than SQL*Loader, no separate utility to run.

2. Querying log files without importing

Tail a CSV produced by some other process and run ad-hoc SQL against it. The file stays on disk; no storage cost inside the database.

3. Sharing data between databases

Database A exports a file using ORACLE_DATAPUMP, copies it to a directory accessible by database B, which then reads it via an external table. No DB link required.

4. Backing data into the database lazily

External tables can be the source of a materialised view that refreshes on demand โ€” incremental data loads without the SQL*Loader machinery.

The Two Access Drivers

Oracle ships two drivers for external tables:

Driver Reads/Writes Use case
ORACLE_LOADER Reads CSV, fixed-width, delimited text Plain text files
ORACLE_DATAPUMP Reads and writes Oracle's binary format Database-to-database transfer

ORACLE_LOADER is read-only (no INSERT into the external table). ORACLE_DATAPUMP can write โ€” useful for exporting query results into a file Oracle can re-read elsewhere.

Directory Objects

External tables can only read files inside a directory object โ€” a named alias for an OS path. Directories are how Oracle controls which OS paths the database may touch.

-- DBA creates the directory:
CREATE OR REPLACE DIRECTORY ext_data_dir AS '/u01/data/external';

-- Grant read access to a specific user:
GRANT READ ON DIRECTORY ext_data_dir TO hr_user;

-- For writing (Datapump or BAD/LOG files):
GRANT WRITE ON DIRECTORY ext_data_dir TO hr_user;

A directory is just a name pointing at a path. It does not create the OS folder โ€” the folder must already exist with appropriate permissions for the Oracle OS user.

Inspect directories:

SELECT directory_name, directory_path FROM all_directories;

Defining an External Table with ORACLE_LOADER

The minimal form for a CSV file:

CREATE TABLE ext_employees (
  employee_id   NUMBER,
  first_name    VARCHAR2(50),
  last_name     VARCHAR2(50),
  email         VARCHAR2(100),
  hire_date     DATE,
  salary        NUMBER
)
ORGANIZATION EXTERNAL (
  TYPE ORACLE_LOADER
  DEFAULT DIRECTORY ext_data_dir
  ACCESS PARAMETERS (
    RECORDS DELIMITED BY NEWLINE
    SKIP 1                              -- skip header row
    FIELDS TERMINATED BY ','
    OPTIONALLY ENCLOSED BY '"'
    MISSING FIELD VALUES ARE NULL
    (
      employee_id,
      first_name,
      last_name,
      email,
      hire_date DATE 'YYYY-MM-DD',
      salary
    )
  )
  LOCATION ('employees.csv')
)
REJECT LIMIT UNLIMITED;

Now SELECT * FROM ext_employees parses employees.csv from /u01/data/external and returns rows.

Key Access Parameters

Parameter Purpose
RECORDS DELIMITED BY NEWLINE One record per line (default)
RECORDS FIXED n Each record is exactly n bytes
FIELDS TERMINATED BY ',' Delimiter between fields
OPTIONALLY ENCLOSED BY '"' Strip surrounding quotes if present
MISSING FIELD VALUES ARE NULL Treat missing fields as NULL instead of error
SKIP n Skip the first n lines (headers)
LOAD WHEN field = value Load only matching rows
BADFILE / LOGFILE / DISCARDFILE Where to write rejected rows and logs
REJECT LIMIT n / UNLIMITED How many bad rows to tolerate

Field Type Conversions

The column types defined in the CREATE TABLE come from Oracle's type system; the source file is always text. Specify conversion patterns when needed:

(
  order_id      INTEGER EXTERNAL,
  order_date    DATE 'YYYY-MM-DD HH24:MI:SS',
  amount        DECIMAL EXTERNAL,
  description   CHAR(200)
)

INTEGER EXTERNAL and DECIMAL EXTERNAL mean "the file has the value as a text string; convert to numeric". Without EXTERNAL, ORACLE_LOADER expects the value in binary format.

Handling Bad Records

By default, parsing errors abort the query. Three files capture rejected data:

File Purpose
BADFILE Records that failed to parse
LOGFILE Summary of the load attempt and errors encountered
DISCARDFILE Records rejected by LOAD WHEN condition
CREATE TABLE ext_employees (
  ...
)
ORGANIZATION EXTERNAL (
  TYPE ORACLE_LOADER
  DEFAULT DIRECTORY ext_data_dir
  ACCESS PARAMETERS (
    RECORDS DELIMITED BY NEWLINE
    BADFILE      ext_data_dir : 'employees.bad'
    LOGFILE      ext_data_dir : 'employees.log'
    DISCARDFILE  ext_data_dir : 'employees.dis'
    FIELDS TERMINATED BY ','
    ( ... )
  )
  LOCATION ('employees.csv')
)
REJECT LIMIT 100;       -- tolerate up to 100 bad rows

Per row that fails: the parsed input goes to BADFILE, an error message to LOGFILE. Inspect these after every load.

Loading from External Table into a Real Table

The canonical ETL pattern:

-- Once: define the external table over the file format
CREATE TABLE ext_employees (...);

-- For each load:
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, salary)
SELECT employee_id, first_name, last_name, email, hire_date, salary
FROM   ext_employees;

COMMIT;

For large loads, use the APPEND hint to bypass the buffer cache and direct-path insert:

INSERT /*+ APPEND */ INTO employees SELECT * FROM ext_employees;

This is significantly faster but the rows aren't visible until COMMIT and the table is briefly locked.

Multiple Files in One External Table

You can union multiple files into one table:

ORGANIZATION EXTERNAL (
  ...
  LOCATION ('part1.csv', 'part2.csv', 'part3.csv')
)

The external table presents all three files as a single rowset. Useful when your source system splits a daily dump into multiple files.

You can also point to different directories:

LOCATION (
  data_dir_1 : 'jan.csv',
  data_dir_2 : 'feb.csv'
)

ORACLE_DATAPUMP โ€” Binary Read/Write

ORACLE_DATAPUMP is the binary equivalent. It can both read existing Datapump files and write a Datapump file from a SELECT statement.

Write a query result to a Datapump file:

CREATE TABLE emp_export
ORGANIZATION EXTERNAL (
  TYPE ORACLE_DATAPUMP
  DEFAULT DIRECTORY ext_data_dir
  LOCATION ('emp_export.dmp')
)
AS
SELECT * FROM employees WHERE department_id = 50;

This creates a file emp_export.dmp containing the selected rows in Datapump's binary format.

Read a Datapump file on another database:

-- On the destination database:
CREATE TABLE emp_import (
  employee_id NUMBER,
  first_name  VARCHAR2(50),
  ...
)
ORGANIZATION EXTERNAL (
  TYPE ORACLE_DATAPUMP
  DEFAULT DIRECTORY ext_data_dir
  LOCATION ('emp_export.dmp')
);

-- Query it:
SELECT * FROM emp_import;

Compared to ORACLE_LOADER, Datapump:

  • Reads/writes binary โ€” much faster, smaller files
  • Both directions supported
  • Less flexible for ad-hoc CSV ingestion

External Tables vs SQL*Loader

Both exist for loading flat files. The trade-offs:

Aspect External Tables SQL*Loader
Setup DDL inside SQL Separate utility
Querying Full SQL on the data None (write-only)
Joins, filters during load Use SELECT to filter Limited WHEN clauses
Parallel direct-path Yes (parallel insert) Yes
Conditional loading Use SELECT ... WHERE LOAD WHEN clauses
Multiple targets Multiple INSERTs from one SELECT Multiple INTO TABLE blocks
Operating environment DBA-controlled directories Client-side or server-side
Pre-12c Available since 9i Mature, decades old

Default to external tables in modern Oracle. SQL*Loader retains a niche when you need to load from a network-connected client without granting filesystem access on the server. External tables are easier to use, integrate with SQL, and are the recommended approach for new code.

Limitations

External tables can't do everything a regular table can:

  • Read-only with ORACLE_LOADER โ€” you can't INSERT, UPDATE, or DELETE through it
  • No indexes โ€” every query is a full scan of the underlying file
  • No constraints โ€” except NOT NULL on columns
  • Slower for repeated access โ€” every query re-reads and re-parses the file
  • No partitioning (though you can have multiple files as separate locations)
  • File must be on the database server's filesystem โ€” not a remote URL

For frequently-queried data, load it into a real table once and query the table. External tables are best for one-shot loads and infrequent queries.

Inspecting External Tables

-- Confirm a table is external and see its driver:
SELECT table_name, type_name, default_directory_name
FROM   user_external_tables;

-- See all access parameters:
SELECT * FROM user_external_tables WHERE table_name = 'EXT_EMPLOYEES';

-- See the file locations:
SELECT * FROM user_external_locations WHERE table_name = 'EXT_EMPLOYEES';

Worked Example โ€” Loading a Daily CSV with Validation

Requirements:

  • A vendor drops a daily CSV orders_YYYYMMDD.csv into /u01/incoming
  • Bad rows must be logged but not fail the load
  • Only rows with status = 'CONFIRMED' should be loaded
  • After load, the file should be archived

Setup (one-time, DBA):

CREATE OR REPLACE DIRECTORY incoming AS '/u01/incoming';
CREATE OR REPLACE DIRECTORY archive  AS '/u01/incoming/archive';
GRANT READ, WRITE ON DIRECTORY incoming TO etl_user;
GRANT WRITE       ON DIRECTORY archive  TO etl_user;

External table definition:

CREATE TABLE ext_orders (
  order_id    NUMBER,
  customer_id NUMBER,
  order_date  DATE,
  status      VARCHAR2(20),
  total       NUMBER
)
ORGANIZATION EXTERNAL (
  TYPE ORACLE_LOADER
  DEFAULT DIRECTORY incoming
  ACCESS PARAMETERS (
    RECORDS DELIMITED BY NEWLINE
    BADFILE     incoming : 'orders.bad'
    LOGFILE     incoming : 'orders.log'
    SKIP 1
    FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
    MISSING FIELD VALUES ARE NULL
    (
      order_id     INTEGER EXTERNAL,
      customer_id  INTEGER EXTERNAL,
      order_date   DATE 'YYYY-MM-DD',
      status       CHAR(20),
      total        DECIMAL EXTERNAL
    )
  )
  LOCATION ('orders_current.csv')
)
REJECT LIMIT 100;

Daily load procedure:

BEGIN
  -- Insert only confirmed orders into the real table
  INSERT /*+ APPEND */ INTO orders (order_id, customer_id, order_date, status, total)
  SELECT order_id, customer_id, order_date, status, total
  FROM   ext_orders
  WHERE  status = 'CONFIRMED';

  COMMIT;

  -- Audit log
  INSERT INTO load_audit (file_name, loaded_at, row_count)
  VALUES ('orders_current.csv', SYSDATE, SQL%ROWCOUNT);
  COMMIT;
END;
/

A scheduled job runs the procedure nightly. The vendor only needs to copy each day's file as orders_current.csv.

Common Errors

Error Cause Fix
ORA-29913: error in executing ODCIEXTTABLEOPEN callout File missing, wrong permissions, or directory not granted Verify the file exists, the OS user (typically oracle) can read it, and the user has READ on the directory
ORA-29400: data cartridge error / KUP-04040 Bad format spec โ€” wrong column types, missing field separator Check ACCESS PARAMETERS; compare your spec to a sample line of the file
KUP-04001: error opening file ... bad file BADFILE / LOGFILE directory not writable Grant WRITE on the directory; verify OS write permission
ORA-04043: object ext_data_dir does not exist Directory object not created or grant missing DBA creates the directory and grants READ to the user
Too many rows rejected; partial load Source file format changed Inspect BADFILE; update ACCESS PARAMETERS or fix the source
Slow performance on large files No way to add indexes; every query rescans the file Use external tables only for one-shot loads; for ongoing queries, load into a real table
ORA-30653: reject limit reached More bad rows than REJECT LIMIT allows Increase REJECT LIMIT or fix source data

Interview Corner

IQ ยท External Tables
When would you use an external table vs SQL*Loader?
โ–ถ Show answer

Default to external tables for any new project โ€” they're simpler, integrate with SQL, and offer the same performance via direct-path inserts.

External tables are better when:

  • The file is on the database server (or NFS-mounted)
  • You want to filter or transform during load: INSERT INTO target SELECT * FROM ext WHERE ...
  • You want to load into multiple tables from one source: INSERT ALL ... SELECT FROM ext
  • The load is part of a larger SQL/PL/SQL workflow
  • You want to query a file without committing to importing it
  • The schema or transformation logic might evolve

SQL*Loader still has a niche:

  • The file is on a client machine, not the database server, and you can't or don't want to grant filesystem access
  • You need very specific control over per-column conversions that external tables don't expose
  • Existing scripts use SQL*Loader and there's no benefit to rewriting

The Oracle documentation has moved firmly toward external tables. Most "use SQL*Loader" advice you find online predates 11g.

IQ ยท External Tables
An external table's CSV file just changed formats. How do you handle it?
โ–ถ Show answer

External tables don't store any data; the table definition is just metadata pointing at a file with a specific format. To handle a format change:

1. Drop and recreate the external table with the new format:

DROP TABLE ext_orders;

CREATE TABLE ext_orders (
  ...new schema or new positions...
)
ORGANIZATION EXTERNAL (
  ...new ACCESS PARAMETERS...
);

This is non-disruptive โ€” no actual data is lost because the external table held no data.

2. Use ALTER TABLE on the access parameters if the change is minor:

ALTER TABLE ext_orders ACCESS PARAMETERS (
  RECORDS DELIMITED BY NEWLINE
  FIELDS TERMINATED BY ';'                -- changed from ','
  ...
);

3. Switch the LOCATION if the file name pattern changed:

ALTER TABLE ext_orders LOCATION ('orders_v2.csv');

Important: if the column types change (new column added, column dropped), you must DROP and CREATE โ€” ALTER TABLE ADD COLUMN doesn't apply meaningfully to external tables because the file is read positionally.

Defensive practice: keep the external table's CREATE statement in version control alongside the format spec from the source system. When the vendor changes format, update both atomically.

IQ ยท External Tables
A query against an external table is extremely slow. What can you do?
โ–ถ Show answer

External tables have no indexes โ€” every query is a full file scan. Three options to speed things up:

1. Stop querying the external table directly.

If you're querying the file repeatedly, the file is the wrong storage. Load it once into a real table with appropriate indexes:

CREATE TABLE orders_local AS SELECT * FROM ext_orders;
CREATE INDEX orders_date_idx ON orders_local(order_date);
-- Now query orders_local instead

2. Use parallelism for the one-shot scan.

CREATE TABLE ext_orders (...) ORGANIZATION EXTERNAL (...) PARALLEL 8;
-- Or per-query:
SELECT /*+ PARALLEL(ext_orders 8) */ * FROM ext_orders WHERE ...;

Oracle splits the file scan across 8 parallel workers โ€” useful for one-time loads of multi-GB files.

3. Pre-filter the file with OS tools before defining the external table.

If you only need rows where status = 'CONFIRMED', run grep/awk first to produce a filtered file. External tables can't push predicates into the file; you must filter externally.

Anti-pattern to avoid: running 100 queries against the same external table in one day. Each query rescans the file. Either load it into a real table once or use a materialised view that refreshes from the external table.

Related Topics

  • DDL Commands โ€” CREATE TABLE ... ORGANIZATION EXTERNAL is the entry point
  • DML Commands โ€” INSERT INTO target SELECT * FROM ext_table is the standard load pattern
  • Performance โ€” INSERT /*+ APPEND */ and parallelism for large external loads
  • Partitioning โ€” EXCHANGE PARTITION paired with external tables for fastest ETL
  • Data Dictionary โ€” USER_EXTERNAL_TABLES and USER_EXTERNAL_LOCATIONS reference views