PostgreSQL

psql Basics — The PostgreSQL Terminal

Thirdy Gayares
10 min read

🎯 What You Will Learn

Every serious PostgreSQL engineer lives inside psql — the terminal client that ships with Postgres itself. GUIs are nice, pero when you SSH into a production server at 2 AM, psql is the only tool that's guaranteed to be there. Let's make it feel like home.

  • Connect with -U, -d, -h, -p flags or a connection URI
  • Decode the prompt — and escape the dreaded shop-# continuation trap
  • Explore any database with meta-commands: \l, \dt, \d, \du, and friends
  • Run multi-line SQL, edit queries in your editor with \e, re-run with \g
  • Read wide rows with expanded display (\x) and tame the pager
  • Run .sql files and import/export CSV with \copy
  • Work faster: tab completion, \timing, \watch, and shell escapes
psqlPostgreSQL 16TerminalBeginner

Prerequisites: PostgreSQL 16 installed locally (any recent version works — psql has been stable for decades). We'll practice against the same tiny web-shop database used across this whole PostgreSQL series, so what you learn here carries straight into Create Database & Tables and SELECT Queries.

Why Learn psql When GUIs Exist?

Fair question. pgAdmin, DBeaver, DataGrip, TablePlus — they're all good tools, and I use them too. So why bother with a terminal client from the 90s? Three reasons na natutunan ko the hard way:

  • It's always there. psql ships with every PostgreSQL installation. Docker container? It's there. Bare Ubuntu server with no desktop? There. Emergency SSH session into a box you've never touched? Still there. Your GUI is not.
  • It's scriptable. Anything you type interactively can become a shell script, a cron job, or a CI step. psql -f migrate.sql in a deploy pipeline is a real production pattern; "click the run button in pgAdmin" is not.
  • It's faster. Once your fingers know \dt and \d customers, you'll answer "what columns does this table have?" before a GUI finishes loading its sidebar tree.
💡
Mental model: think of psql as two languages in one prompt. Anything starting with a backslash (\dt, \x, \q) is a meta-command — an instruction to the psql client, executed instantly, no semicolon needed. Everything else is SQL, sent to the PostgreSQL server only when you type ;. Keeping those two languages separate in your head removes 90% of beginner confusion.

Ang deal natin: 10 minutes here, and the terminal stops being scary. Tara na.

Connecting: Flags, URIs, and Passwords

1Check that psql is installed
terminal
psql --version
psql (PostgreSQL) 16.3
2Connect with flags

The four flags you'll use daily: -U (user), -d (database), -h (host), and -p (port). Locally, host and port usually have sane defaults, so this is the typical form:

terminal
psql -U postgres -d postgres
psql (16.3)
Type "help" for help.

postgres=#

Connecting to a remote server just means spelling everything out:

terminal
psql -h db.example.com -p 5432 -U app_user -d shop
FlagMeaningDefault if omitted
-URole (user) to connect asYour OS username
-dDatabase to connect toSame as the user name
-hServer hostLocal socket / localhost
-pServer port5432
-WForce a password promptPrompt only when required
3Or connect with a connection URI

Everything above can be packed into a single URI — the same format your app frameworks (FastAPI + SQLAlchemy, Django, Prisma) use in DATABASE_URL. Learning it here pays off everywhere:

terminal
psql postgres://postgres:mypassword@localhost:5432/shop
psql (16.3)
Type "help" for help.

shop=#
4Handle passwords without typing them every time

For one-off scripts you can pass the password through the PGPASSWORD environment variable:

terminal
PGPASSWORD=mypassword psql -h localhost -U postgres -d shop
⚠️
OK for learning — careful in production. PGPASSWORD=... on the command line can leak into your shell history and, on shared machines, into the process list. The cleaner habit is a ~/.pgpass file: one host:port:database:user:password line per server, then chmod 600 ~/.pgpass so only you can read it. psql picks it up automatically — no password prompt, nothing leaked.
~/.pgpass
# host:port:database:user:password
localhost:5432:shop:postgres:mypassword
5Verify where you actually landed

Lost? \conninfo tells you exactly which database, user, host, and port you're on. Make this a reflex before running anything destructive:

psql
\conninfo
You are connected to database "shop" as user "postgres" on host "localhost" (address "127.0.0.1") at port "5432".

One more thing before we explore — create the web-shop database this series uses. Full walkthrough with explanations lives in Create Database & Tables; here's the compact version so this post is runnable on its own:

psql
CREATE DATABASE shop;
\c shop
setup_shop.sql
CREATE TABLE customers (
  customer_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  full_name   TEXT NOT NULL,
  email       TEXT NOT NULL UNIQUE,
  city        TEXT,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE products (
  product_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name       TEXT NOT NULL,
  category   TEXT NOT NULL,
  price      NUMERIC(10,2) NOT NULL CHECK (price >= 0),
  stock      INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE orders (
  order_id    INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_id INTEGER NOT NULL REFERENCES customers (customer_id),
  status      TEXT NOT NULL DEFAULT 'pending',
  ordered_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
  order_item_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_id      INTEGER NOT NULL REFERENCES orders (order_id) ON DELETE CASCADE,
  product_id    INTEGER NOT NULL REFERENCES products (product_id),
  quantity      INTEGER NOT NULL CHECK (quantity > 0),
  unit_price    NUMERIC(10,2) NOT NULL
);

INSERT INTO customers (full_name, email, city) VALUES
  ('Maria Santos',   '[email protected]',   'Manila'),
  ('Juan dela Cruz', '[email protected]',  'Cebu'),
  ('Ana Reyes',      '[email protected]',      'Davao'),
  ('Carlo Garcia',   '[email protected]',   'Quezon City'),
  ('Liza Mendoza',   '[email protected]',   'Makati');

INSERT INTO products (name, category, price, stock) VALUES
  ('Mechanical Keyboard', 'Accessories', 2499.00, 15),
  ('USB-C Hub',           'Accessories', 1299.00, 30),
  ('27-inch Monitor',     'Displays',   10995.00,  8),
  ('HD Webcam',           'Accessories', 1899.00, 20),
  ('Laptop Stand',        'Accessories',  899.00, 25),
  ('Wireless Mouse',      'Accessories',  749.00, 40);

INSERT INTO orders (customer_id, status) VALUES
  (1, 'paid'),
  (2, 'pending'),
  (3, 'shipped');

INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
  (1, 1, 1,  2499.00),
  (1, 2, 2,  1299.00),
  (2, 3, 1, 10995.00),
  (3, 5, 1,   899.00);
Checkpoint: paste the whole block into psql (or save it as setup_shop.sql — we'll run it as a file in Section 7). You should see four CREATE TABLE lines followed by INSERT 0 5, INSERT 0 6, INSERT 0 3, and INSERT 0 4. If yes, your playground is ready.

The Prompt, Decoded (and the Trap Inside It)

That little string before your cursor is a status display, hindi lang decoration. It tells you which database you're in, who you are, and — most importantly — whether psql thinks your statement is finished.

PromptWhat it means
shop=#Connected to database shop as a superuser. Ready for a new statement.
shop=>Connected to shop as a regular (non-superuser) role. Also ready.
shop-#Continuation: your previous line did NOT end a statement. psql is still waiting.
shop(#You have an unclosed parenthesis somewhere above.
shop'#You have an unclosed single quote — psql thinks you are still inside a string.
shop"#You have an unclosed double quote (an identifier).

The one that traps every beginner is shop-#. You type a query, press Enter, and… nothing happens. Walang error, walang result. Ganito ang itsura:

psql
SELECT full_name, city FROM customers
shop=# SELECT full_name, city FROM customers
shop-#
⚠️
The missing-semicolon trap: psql did not ignore you — it's waiting. SQL statements only execute when they end with ;. The -# prompt means "your statement is still open." Beginners retype the whole query here and accidentally stack two half-statements into one buffer, which then explodes with a syntax error that mentions neither of their queries. Huwag mag-panic — read the prompt first.

Three clean ways out of the continuation prompt:

  • Finish it: just type ; and press Enter — the buffered statement runs.
  • Abandon it: press Ctrl+C — psql cancels the input and gives you a fresh shop=#.
  • Clear the buffer: type \r (reset) — same effect, and it works even when you're stuck inside an unclosed quote.
shop-# ;
   full_name    |    city
----------------+-------------
 Maria Santos   | Manila
 Juan dela Cruz | Cebu
 Ana Reyes      | Davao
 Carlo Garcia   | Quezon City
 Liza Mendoza   | Makati
(5 rows)
💡
Why meta-commands don't need semicolons: a backslash command like \dt is handled by the psql client itself, line by line — it never travels to the server, so there's no statement to terminate. Only real SQL needs the ;.

Exploring: The Meta-Command Family

This is where psql outruns every GUI. A handful of two-keystroke commands answer the questions you ask a hundred times a day. Let's tour them against our shop database.

1List all databases with \l
psql
\l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 shop      | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(4 rows)

(template0 and template1 are the blueprints Postgres clones when you run CREATE DATABASE — leave them alone.)

2Switch databases with \c
psql
\c shop
You are now connected to database "shop" as user "postgres".
3List tables with \dt
psql
\dt
          List of relations
 Schema |    Name     | Type  |  Owner
--------+-------------+-------+----------
 public | customers   | table | postgres
 public | order_items | table | postgres
 public | orders      | table | postgres
 public | products    | table | postgres
(4 rows)
4Describe one table with \d

This is the meta-command I use most. Columns, types, nullability, defaults, indexes, and every constraint — one screen:

psql
\d customers
                                        Table "public.customers"
   Column    |           Type           | Collation | Nullable |           Default
-------------+--------------------------+-----------+----------+------------------------------
 customer_id | integer                  |           | not null | generated always as identity
 full_name   | text                     |           | not null |
 email       | text                     |           | not null |
 city        | text                     |           |          |
 created_at  | timestamp with time zone |           | not null | now()
Indexes:
    "customers_pkey" PRIMARY KEY, btree (customer_id)
    "customers_email_key" UNIQUE CONSTRAINT, btree (email)
Referenced by:
    TABLE "orders" CONSTRAINT "orders_customer_id_fkey" FOREIGN KEY (customer_id) REFERENCES customers(customer_id)

Add a + for the verbose version — \d+ also shows storage details and, at the end, check constraints, comments, and the table's access method:

psql
\d+ products
                                                   Table "public.products"
   Column   |     Type      | Collation | Nullable |           Default            | Storage  | Stats target | Description
------------+---------------+-----------+----------+------------------------------+----------+--------------+-------------
 product_id | integer       |           | not null | generated always as identity | plain    |              |
 name       | text          |           | not null |                              | extended |              |
 category   | text          |           | not null |                              | extended |              |
 price      | numeric(10,2) |           | not null |                              | main     |              |
 stock      | integer       |           | not null | 0                            | plain    |              |
Indexes:
    "products_pkey" PRIMARY KEY, btree (product_id)
Check constraints:
    "products_price_check" CHECK (price >= 0::numeric)
Referenced by:
    TABLE "order_items" CONSTRAINT "order_items_product_id_fkey" FOREIGN KEY (product_id) REFERENCES products(product_id)
Access method: heap
5Roles, schemas, and functions: \du, \dn, \df
psql
\du
                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
 thirdy    | Create DB
psql
\dn
      List of schemas
  Name  |       Owner
--------+-------------------
 public | pg_database_owner
(1 row)
psql
\df
                       List of functions
 Schema | Name | Result data type | Argument data types | Type
--------+------+------------------+---------------------+------
(0 rows)

Empty for now — walang user-defined functions pa tayo. After you work through PostgreSQL Functions, this list starts filling up, and \df becomes how you find them again.

💡
Pattern to remember: \d means "describe." The letter after it picks the object type — \dt tables, \dv views, \di indexes, \df functions, \du users (roles), \dn schemas (namespaces). Add + to any of them for extra detail. You can also filter with a pattern: \dt order* lists only tables starting with "order".

Running SQL: Multi-Line, \e, and \g

psql doesn't care about line breaks — only the ; ends a statement. That means you can format queries across multiple lines exactly like you'd write them in a file. The prompt keeps showing -# until you close the statement:

psql
SELECT c.full_name,
       count(o.order_id) AS total_orders
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.customer_id
GROUP BY c.full_name
ORDER BY total_orders DESC, c.full_name;
shop=# SELECT c.full_name,
shop-#        count(o.order_id) AS total_orders
shop-# FROM customers AS c
shop-# LEFT JOIN orders AS o ON o.customer_id = c.customer_id
shop-# GROUP BY c.full_name
shop-# ORDER BY total_orders DESC, c.full_name;
   full_name    | total_orders
----------------+--------------
 Ana Reyes      |            1
 Juan dela Cruz |            1
 Maria Santos   |            1
 Carlo Garcia   |            0
 Liza Mendoza   |            0
(5 rows)
1Edit the last query in a real editor with \e

Typo in a five-line query? Don't retype it. \e opens the current query buffer (or the last executed statement) in your $EDITOR. Fix it, save, quit — psql runs the edited statement the moment it ends with ;.

terminal
# pick your editor once, in your shell profile
export EDITOR=nano
psql
\e

Kung vi ang bumukas at hindi ka makalabas: press Esc, type :wq, Enter. (Everyone has been there. Walang judgment.)

2Re-run the last query with \g

\g executes whatever is in the query buffer again — perfect for "did my UPDATE change the count?" loops. Bonus: \g filename re-runs it and writes the result to a file instead of the screen.

psql
SELECT count(*) AS pending_orders FROM orders WHERE status = 'pending';
\g
 pending_orders
----------------
              1
(1 row)

 pending_orders
----------------
              1
(1 row)
💡
Semicolon vs \g: they do the same thing — send the buffer to the server. ; is what you'll type 99% of the time; \g exists so you can re-send without retyping, or redirect the output. Same engine, different trigger.

Wide Rows: Expanded Display and the Pager

Select a whole row from a table with many columns and the output wraps into unreadable soup. psql's fix is expanded display: \x flips the table 90 degrees so each column becomes its own line.

psql
\x
SELECT * FROM customers WHERE customer_id = 1;
Expanded display is on.
-[ RECORD 1 ]---------------------------------
customer_id | 1
full_name   | Maria Santos
email       | [email protected]
city        | Manila
created_at  | 2026-07-18 09:14:22.583127+08

\x is a toggle — run it again to go back to normal tables. Even better for daily work: \x auto lets psql decide, using normal tables when they fit your terminal and flipping to expanded only when a row is too wide. Set it and forget it.

psql
\x auto
Expanded display is used automatically.

The other surprise for beginners is the pager. When a result has more rows than your terminal, psql pipes it through less: your screen is "taken over," arrow keys scroll, and q brings you back. That's a feature, not a hang — pero if it annoys you during a demo or when copy-pasting results:

psql
\pset pager off
Pager usage is off.
⚠️
"psql froze!" — no, you're inside the pager. If the bottom of your screen shows : or (END), you're in less. Press q to exit. This one keystroke has rescued more beginners than any Stack Overflow answer.

Running .sql Files

Real work lives in files — schema definitions, seeds, migrations, reports. There are three ways to run them, and you'll use all three.

1From inside psql: \i

\i reads a file and executes every statement in it, echoing each result. Paths are relative to the directory where you started psql:

psql
\i setup_shop.sql
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 5
INSERT 0 6
INSERT 0 3
INSERT 0 4
2From the shell: psql -f

Same effect, no interactive session — this is the form that belongs in deploy scripts and CI pipelines:

terminal
psql -U postgres -d shop -f setup_shop.sql
3One-off commands: psql -c

For a single statement, skip the file entirely. -c runs one command and exits — perfect for shell scripts and health checks:

terminal
psql -U postgres -d shop -c "SELECT count(*) FROM products;"
 count
-------
     6
(1 row)
Production habit: for migration-style files, add -1 (run the whole file in a single transaction) and -v ON_ERROR_STOP=1 (abort on the first error instead of plowing ahead): psql -1 -v ON_ERROR_STOP=1 -d shop -f migrate.sql. Either the whole file applies or none of it does — no half-migrated schema at 2 AM.

Import & Export CSV with \copy

"Pa-export naman ng CSV" is a request you will get for the rest of your career. psql handles it natively — walang Excel plugin, walang GUI export wizard.

1Export a query result to CSV
psql
\copy (SELECT full_name, email, city FROM customers ORDER BY customer_id) TO 'customers.csv' WITH (FORMAT csv, HEADER)
COPY 5

Check the file without leaving psql (that's the \! shell escape — more in Section 9):

psql
\! cat customers.csv
full_name,email,city
Maria Santos,[email protected],Manila
Juan dela Cruz,[email protected],Cebu
Ana Reyes,[email protected],Davao
Carlo Garcia,[email protected],Quezon City
Liza Mendoza,[email protected],Makati
2Import a CSV into a table

Say marketing hands you two new products in a CSV:

new_products.csv
name,category,price,stock
Ring Light,Accessories,1499.00,12
Ergonomic Chair,Furniture,7995.00,5
psql
\copy products (name, category, price, stock) FROM 'new_products.csv' WITH (FORMAT csv, HEADER)
COPY 2

Verify — naming the columns instead of SELECT *, dahil alam na natin kung ano'ng hinahanap natin:

psql
SELECT product_id, name, price FROM products ORDER BY product_id DESC LIMIT 2;
 product_id |      name       |  price
------------+-----------------+---------
          8 | Ergonomic Chair | 7995.00
          7 | Ring Light      | 1499.00
(2 rows)
💡
\copy vs COPY — huge difference: the SQL command COPY runs on the server, so the file path must exist on the database server's disk and you need superuser-level file privileges. The meta-command \copy runs on the client — the file lives on your machine, and it works with any role that can read or write the table. For a remote database (which is almost always), \copy is the one you want.
⚠️
One-line rule: unlike SQL, a \copy command must fit on a single line — it's a meta-command, so the first newline ends it. If you break the query across lines, psql cuts it off mid-statement and you get a confusing parse error. Long export query? Wrap it in a view first, then \copy (SELECT * FROM my_view) TO ... — see PostgreSQL Views.

Quality of Life: The Tricks That Make psql Fast

These five habits are the difference between "I tolerate psql" and "psql is faster than my GUI."

  • Tab completion. psql completes keywords, table names, and column names. Type SELECT * FROM cus then press Tab — it becomes customers. It even completes meta-commands and file paths after \i.
  • History. Up/Down arrows walk through previous commands, exactly like your shell. Ctrl+R searches history incrementally, and \s prints the whole session history.
1Measure every query with \timing
psql
\timing
SELECT count(*) FROM order_items;
Timing is on.
 count
-------
     4
(1 row)

Time: 0.412 ms

It's a toggle, and it stays on for the session. Turn it on before you compare two versions of a query — gut feeling is not a benchmark.

2Auto-repeat a query with \watch

Run a query, then \watch N re-executes it every N seconds until you press Ctrl+C. Monitoring a batch job's progress, watching orders come in during a launch — this is the tool:

psql
SELECT status, count(*) FROM orders GROUP BY status;
\watch 5
          Fri 18 Jul 2026 10:15:04 (every 5s)

 status  | count
---------+-------
 paid    |     1
 pending |     1
 shipped |     1
(3 rows)

          Fri 18 Jul 2026 10:15:09 (every 5s)

 status  | count
---------+-------
 paid    |     2
 pending |     1
 shipped |     1
(3 rows)
3Escape to the shell with \!

Need to peek at a file, check disk space, or clear the screen without losing your session? \! runs any shell command and drops you right back at the prompt:

psql
\! ls -lh *.csv
-rw-r--r--  1 thirdy  staff   231B Jul 18 10:12 customers.csv
-rw-r--r--  1 thirdy  staff    89B Jul 18 10:14 new_products.csv
💡
Stuck? psql has built-in help, two flavors: \? lists every meta-command with a one-line description, and \h gives SQL syntax help — \h CREATE TABLE prints the full syntax diagram for that statement, straight from the docs, offline. Hindi mo kailangang i-Google ang word order ng ALTER TABLE kada beses.

Common Mistakes (Everyone Makes These)

Mistake #1: single vs double quotes. In PostgreSQL, 'single quotes' are for string values, and "double quotes" are for identifiers (table and column names). Coming from JavaScript or Python, where both quote strings, this bites hard:

psql
SELECT * FROM customers WHERE full_name = "Maria Santos";
ERROR:  column "Maria Santos" does not exist
LINE 1: SELECT * FROM customers WHERE full_name = "Maria Santos";
                                                  ^
⚠️
Read that error carefully: Postgres says column "Maria Santos" does not exist — because double quotes told it to look for a column with that name. Strings take single quotes, always:
psql
SELECT customer_id, full_name, city FROM customers WHERE full_name = 'Maria Santos';
 customer_id |  full_name   |  city
-------------+--------------+--------
           1 | Maria Santos | Manila
(1 row)

Mistake #2: running commands in the wrong database. You connect, type \dt, and psql claims your tables don't exist. Kinabahan ka na — but look at the prompt:

postgres=# \dt
Did not find any relations.
⚠️
Your tables didn't vanish — you're in the wrong database. The prompt says postgres=#, not shop=#. Every database is fully isolated; \dt only sees the one you're connected to. Check the prompt (or \conninfo), then \c shop and try again. This exact panic happens to every beginner at least once — usually on someone else's server.
postgres=# \c shop
You are now connected to database "shop" as user "postgres".
shop=# \dt
          List of relations
 Schema |    Name     | Type  |  Owner
--------+-------------+-------+----------
 public | customers   | table | postgres
 public | order_items | table | postgres
 public | orders      | table | postgres
 public | products    | table | postgres
(4 rows)

Mistake #3: not knowing how to leave. Classic. The canonical exit is \q, but modern psql (11+) is friendly — exit and quit also work at a fresh prompt, and Ctrl+D works everywhere. The catch: at a continuation prompt, exit is just more text added to your unfinished statement. psql even coaches you:

shop-# exit
Use \q to quit or press control-D (^D) to exit, or terminate your current statement with a semicolon first.
⚠️
Also — the semicolon habit cuts both ways. Forgetting ; strands you at -# (Section 3), but blindly typing ; to "fix" a stuck prompt executes whatever is buffered. If the buffer holds a half-typed DELETE FROM orders without its WHERE clause, that semicolon just deleted every order. When in doubt, Ctrl+C or \r to throw the buffer away, then start clean — and practice destructive statements inside a transaction (BEGIN;ROLLBACK;).

Best Practices + Meta-Command Cheat Sheet

Habits that make psql safe and fast:
  • Read the prompt before typing — it tells you the database and whether a statement is still open.
  • \conninfo before anything destructive — confirm you're on the server you think you're on.
  • \x auto + \timing at session start — readable rows and honest numbers, always.
  • Use ~/.pgpass instead of typing passwords or exporting PGPASSWORD.
  • Files over retyping: anything longer than three lines belongs in a .sql file run with \i or -f.
  • Wrap experiments in BEGIN; … ROLLBACK; — free undo for UPDATE and DELETE practice.

Print this table, tape it to your monitor — or just come back here. The full list is in \?, but these cover 95% of real sessions:

CommandWhat it does
\qQuit psql (exit and quit also work at a fresh prompt)
\conninfoShow current database, user, host, and port
\lList all databases in the cluster
\c dbnameConnect (switch) to another database
\dtList tables in the current database
\d table_nameDescribe a table: columns, types, indexes, constraints
\d+ table_nameVerbose describe: storage, check constraints, comments
\duList roles (users) and their attributes
\dnList schemas
\dfList user-defined functions
\x / \x autoToggle expanded (vertical) display / let psql decide
\eEdit the query buffer in $EDITOR, run on save
\gRe-run the last query (\g file.txt writes output to a file)
\rReset (clear) the query buffer — escape hatch for stuck prompts
\i file.sqlExecute a SQL file from inside psql
\copy ... TO/FROM ...Client-side CSV export/import (single line!)
\timingToggle per-query execution time display
\watch NRe-run the last query every N seconds (Ctrl+C stops)
\! commandRun a shell command without leaving psql
\sShow command history
\pset pager offStop piping long output through less
\? / \h SQLHelp for meta-commands / syntax help for any SQL statement

Practice Exercises

Five tasks, all against the shop database from Section 2. Bawal mag-GUI — psql only. Expected output is under each task so you can self-check.

1Confirm your connection

Connect to shop as postgres using flags, then prove where you are with a single meta-command.

You are connected to database "shop" as user "postgres" on host "localhost" (address "127.0.0.1") at port "5432".
2Find the CHECK constraint

Using only meta-commands (no SQL), find which products constraint stops negative prices. Hint: describe the table.

Check constraints:
    "products_price_check" CHECK (price >= 0::numeric)
3Multi-line query + re-run

Write a multi-line query counting customers per city, ordered by city (watch the -# prompts appear), then immediately run it a second time without retyping anything.

    city     | count
-------------+-------
 Cebu        |     1
 Davao       |     1
 Makati      |     1
 Manila      |     1
 Quezon City |     1
(5 rows)
4Expanded display

Turn on expanded display and show the full row for order 1, then switch to \x auto.

Expanded display is on.
-[ RECORD 1 ]--------------------------------
order_id    | 1
customer_id | 1
status      | paid
ordered_at  | 2026-07-18 09:20:41.117530+08
5Export to CSV

Export every product with price above 1400 — columns name and price, ordered by price descending — to expensive.csv with a header row, then print the file without leaving psql.

COPY 5
name,price
27-inch Monitor,10995.00
Ergonomic Chair,7995.00
Mechanical Keyboard,2499.00
HD Webcam,1899.00
Ring Light,1499.00
All five matched? Congrats — you can now connect, navigate, inspect, query, and export on any PostgreSQL server na maabot mo through a terminal. That is genuinely the daily toolkit; everything else in psql is a variation of what you just did.

What's Next

Recap: meta-commands talk to the psql client and run instantly; SQL talks to the server and needs a ;. Read the prompt, describe before you query, \x auto for wide rows, \copy for CSV, and \q to go home. Kabisado mo na ang terminal — time to put real SQL through it.

🚀 Recommended next reads:

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.