
DATA 351: Data Management with SQL
January 26, 2026
Part 1: Importing Data with \COPY
Part 2: Exporting Data with \COPY
Imagine you have 65,000 survey responses…
This is:
Solution: Bulk import with \COPY
Today we will work with some Dunder Mifflin employee data again.
| employee_id | full_name | department | salary_usd |
|---|---|---|---|
| 1 | Michael Scott | Management | 75000.00 |
| 2 | Dwight Schrute | Sales | 62000.00 |
| 3 | Pam Beesly | Reception | 42000.00 |
| 4 | Jim Halpert | Sales | 61000.00 |
I have added a few more employees and columns so we can practice more SQL:
| employee_id | full_name | department | salary_usd | performance_rating | years_experience |
|---|---|---|---|---|---|
| 1 | Michael Scott | Management | 75000.00 | 3.2 | 15 |
| 2 | Dwight Schrute | Sales | 62000.00 | 4.8 | 12 |
| 3 | Pam Beesly | Reception | 42000.00 | 3.9 | 8 |
| 4 | Jim Halpert | Sales | 61000.00 | 4.1 | 10 |
| 5 | Angela Martin | Accounting | 52000.00 | 4.5 | 11 |
| 6 | Kevin Malone | Accounting | 48000.00 | 2.1 | 9 |
| 7 | Oscar Martinez | Accounting | 54000.00 | 4.7 | 13 |
| 8 | Stanley Hudson | Sales | 58000.00 | 3.0 | 20 |
Step 1: Create a file named employees_import.csv, I recommend in your Downloads directory for now.
Copy this exact content (hover over the data and click the copy button that appearsto the right):
employee_id,full_name,department,email,hire_date,salary_usd,is_manager,performance_rating,years_experience,commission_rate,last_login
1,Michael Scott,Management,michael.scott@dundermifflin.com,2005-03-24,75000.00,true,3.2,15,,2026-01-15 09:12:00
2,Dwight Schrute,Sales,dwight.schrute@dundermifflin.com,2006-04-12,62000.00,false,4.8,12,0.08,2026-01-16 08:01:00
3,Pam Beesly,Reception,pam.beesly@dundermifflin.com,2007-07-02,42000.00,false,3.9,8,,
4,Jim Halpert,Sales,jim.halpert@dundermifflin.com,2005-10-05,61000.00,false,4.1,10,0.07,2026-01-16 08:03:00
5,Angela Martin,Accounting,angela.martin@dundermifflin.com,2006-08-15,52000.00,false,4.5,11,,2026-01-15 07:45:00
6,Kevin Malone,Accounting,kevin.malone@dundermifflin.com,2007-02-28,48000.00,false,2.1,9,,2026-01-16 09:30:00
7,Oscar Martinez,Accounting,oscar.martinez@dundermifflin.com,2005-06-01,54000.00,false,4.7,13,,2026-01-16 08:15:00
8,Stanley Hudson,Sales,stanley.hudson@dundermifflin.com,2004-11-20,58000.00,false,3.0,20,0.05,2026-01-16 08:55:00Why a “safe” location?
PostgreSQL needs permission to read your file. Some folders are restricted.
macOS / Linux:
Windows (PowerShell):
Open your terminal and connect:
You should see a prompt like:
postgres=#
Warning
command not found: psql or something similar, psql is not in your PATH. Check out the resource on Canvas → Week 2 Lesson Plan → Adding PSQL to your PATH.psql: could not connect to server: No such file or directory or something similar, you are not connected to the database or there are credential issues. Try psql -U postgres -h localhost instead.At the postgres=# prompt, run:
You should see:
You are now connected to database "office_db" as user "postgres".
Note
The \c command connects you to the new database.
Now we need a table that matches our CSV columns exactly. Copy this exact content (hover over the data and click the copy button that appears to the right):
CREATE TABLE employees (
employee_id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL,
department TEXT NOT NULL,
email TEXT,
hire_date DATE NOT NULL,
salary_usd NUMERIC(10,2) NOT NULL,
is_manager BOOLEAN NOT NULL,
performance_rating NUMERIC(2,1),
years_experience INTEGER,
commission_rate NUMERIC(3,2),
last_login TIMESTAMP
);Verify with \d employees to see the structure.

Each column definition has:
salary_usd)NUMERIC(10,2))NOT NULL)The moment of truth!
macOS / Linux:
Windows:
You should see: COPY 8
That means 8 rows were imported successfully!
Count the rows:
| count |
|---|
| 8 |
View all the data:
Take a moment to verify the data looks correct.
\COPYLet’s break this down:
| Part | Meaning |
|---|---|
\COPY |
Client-side copy command |
employees |
Target table name |
FROM '/tmp/...' |
Source file path |
FORMAT csv |
File is comma-separated |
HEADER true |
First row is column names |
\COPY vs COPY: What’s the Difference?| Feature | \COPY |
COPY |
|---|---|---|
| Runs on | Your computer (client) | Database server |
| File location | Your filesystem | Server filesystem |
| Permissions | Your user permissions | postgres user permissions |
| Best for | Development, small files | Production, large files |
Rule of thumb: Use \COPY in this class. It is safer and easier.
Write queries to answer (work with a neighbor if your system is acting up):
Take 3 minutes, then we will review.
1. Sales department count:
| count |
|---|
| 3 |
2. Michael’s email:
| michael.scott@dundermifflin.com |
3. Highest performance rating:
| full_name | performance_rating |
|---|---|
| Dwight Schrute | 4.8 |
Real-world scenarios where you need to get data OUT of PostgreSQL:
\COPY Command for ExportJust like importing, we use \COPY for exporting:
You should see: COPY 8
That means 8 rows were exported successfully!
\COPY for Export| Part | Meaning |
|---|---|
\COPY |
Client-side copy command |
(SELECT * FROM employees) |
Query to export (must be in parentheses) |
TO '/tmp/...' |
Destination file path |
FORMAT csv |
Output as comma-separated values |
HEADER true |
Include column names as first row |
Export an entire table:
Export a query result:
Note
When exporting a query, you MUST wrap it in parentheses.
Let’s export just the Sales team with their commission information:
macOS / Linux:
Windows:
Check the file contents (from your terminal, not psql):
macOS / Linux:
Windows (PowerShell):
full_name,salary_usd,commission_rate
Dwight Schrute,62000.00,0.08
Jim Halpert,61000.00,0.07
Stanley Hudson,58000.00,0.05
Common options for \COPY ... TO:
| Option | Description | Example |
|---|---|---|
FORMAT csv |
Comma-separated values | Most common |
FORMAT text |
Tab-separated (default) | For TSV files |
HEADER true |
Include column headers | Usually want this |
DELIMITER ';' |
Custom delimiter | European CSV format |
NULL 'NA' |
Represent NULL as ‘NA’ | For R compatibility |
QUOTE '"' |
Character for quoting strings | Default is double quote |
Some European systems use semicolons because commas are decimal separators:
Result:
full_name;salary_usd
Michael Scott;75000.00
Dwight Schrute;62000.00
Export summary statistics, not raw data:
This exports a 4-row summary instead of all 8 employee records.
By default, NULL exports as empty string. You can customize this:
full_name,commission_rate
Michael Scott,N/A
Dwight Schrute,0.08
Pam Beesly,N/A
Useful when the receiving system needs explicit NULL markers.
Create a CSV export that shows:
Only include employees who are not managers and make more than $50,000.
Export to /tmp/employee_report.csv (or C:\Users\Public\ on Windows).
Take 4 minutes.
full_name,department,salary_usd
Dwight Schrute,Sales,62000.00
Jim Halpert,Sales,61000.00
Stanley Hudson,Sales,58000.00
Oscar Martinez,Accounting,54000.00
Angela Martin,Accounting,52000.00
1. Forgetting parentheses around SELECT:
2. File permission issues:
\COPY vs COPY for ExportJust like with import, there are two versions:
| Feature | \COPY ... TO |
COPY ... TO |
|---|---|---|
| Runs on | Your computer (client) | Database server |
| File location | Your filesystem | Server filesystem |
| Permissions | Your user permissions | postgres user permissions |
| Best for | Development, sharing files | Server backups, ETL |
Tip
Rule of thumb: Use \COPY in this class. The file ends up on YOUR machine.
A typical data export workflow:

| Task | Command |
|---|---|
| Import entire CSV | \COPY table FROM 'file.csv' WITH (FORMAT csv, HEADER true) |
| Export entire table | \COPY table TO 'file.csv' WITH (FORMAT csv, HEADER true) |
| Export query result | \COPY (SELECT ...) TO 'file.csv' WITH (FORMAT csv, HEADER true) |
Note
Remember:
FROM = bringing data INTO = sending data OUTWe covered:
\COPYWhat questions do you have?
