PostgreSQL Tables and Data Types
Seven columns, each a deliberate choice: identity rather than serial, text rather than varchar(n), numeric for money, timestamptz for time, and native array and jsonb types that MySQL has no equivalent for.
Schemas and Data Guide 6 of 47 Beginner
- OSUbuntu 26.04 LTS (resolute)
- PostgreSQL18.6-0ubuntu0.26.04.1
- Server timezoneEtc/UTC
- TimeAbout 15 min
- Reviewed27 August 2026
Written against the versions above. `GENERATED ALWAYS AS IDENTITY` is the SQL-standard form and has been available since PostgreSQL 10. `serial` still works and is still what most older tutorials show, but it leaves a sequence that outlives the column.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| db-a01 | 192.168.0.81 | Ubuntu 26.04 LTS | Primary / Source / Replica Set Member 1 | 2 Core | 4 GB | 50 GB |
Before you start
- The
appdbdatabase from the foundations track.
-
Create a table with types chosen on purpose
Every column here is a decision worth defending.
bigint GENERATED ALWAYS AS IDENTITYrather thanserial. Identity is the SQL standard, the sequence it owns is dropped with the column, andALWAYSrefuses a manually supplied value - which the constraints guide demonstrates.bigintbecause running out ofintat two billion is a genuinely awful day.textrather thanvarchar(80). In PostgreSQL they are the same type underneath with no performance difference, so a length limit is a business rule. Add one with aCHECKwhen you actually have a rule; do not invent 80 as a guess you cannot easily change later.numeric(10,2)for money, exact by definition, neverrealordouble.text[]andjsonbare native types with no MySQL equivalent - an array column and a binary JSON column, both indexable and both queryable in SQL. The last guide in this track uses them properly.timestamptz, always, for reasons the timestamps guide makes unarguable.bash Example session psql -d appdb -c "CREATE TABLE customers (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, email text NOT NULL UNIQUE, credit numeric(10,2) NOT NULL DEFAULT 0, tags text[] NOT NULL DEFAULT '{}', profile jsonb NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now())"CREATE TABLEExpected result
CREATE TABLE.Success conditionYou have a table whose types were chosen rather than defaulted to.
-
Read the table back
\dis psql's table description and is more informative than MySQL'sDESCRIBE: it shows types, nullability, defaults and the indexes and constraints underneath.Two things to notice. The identity column's default reads as
generated always as identityrather than a sequence name - the sequence is owned and hidden. And theUNIQUEon email has become a named index,customers_email_key, following PostgreSQL's automatic naming convention: table, column, suffix. That name appears verbatim in the error message when it fires, which is what makes those errors so easy to act on.bash Example session psql -d appdb -c "\d customers" Table "public.customers" Column | Type | Collation | Nullable | Default------------+--------------------------+-----------+----------+------------------------------ id | bigint | | not null | generated always as identity name | text | | not null | email | text | | not null | credit | numeric(10,2) | | not null | 0 tags | text[] | | not null | '{}'::text[] profile | jsonb | | not null | '{}'::jsonb created_at | timestamp with time zone | | not null | now()Indexes: "customers_pkey" PRIMARY KEY, btree (id) "customers_email_key" UNIQUE CONSTRAINT, btree (email)Expected resultSeven columns with their types and defaults, plus the primary key and unique indexes.
Success conditionYou can read a table's full definition including its constraints.
-
Insert rows and get something back
RETURNINGis the feature to take away from this step. The insert returns the generated ids in the same round trip - noLAST_INSERT_ID(), no second query, and it works for multi-row inserts where a last-id function cannot.It works on
UPDATEandDELETEtoo, which makes "change this and tell me what you changed" a single atomic statement.TABLE customersis shorthand forSELECT * FROM customers. The array and jsonb columns display as{vip,eu}and{"tier": "gold", ...}- stored as structured values, not as strings that happen to look like them.bash Example session psql -d appdb -c "INSERT INTO customers (name, email, credit, tags, profile) VALUES ('Ada Lovelace','ada@example.com',120.50,'{vip,eu}','{\"tier\":\"gold\",\"seats\":3}'), ('Grace Hopper','grace@example.com',0,'{us}','{\"tier\":\"silver\",\"seats\":1}'), ('Zoe Washburne','zoe@example.com',75.25,'{apac}','{\"tier\":\"gold\",\"seats\":9}') RETURNING id, name" id | name----+--------------- 1 | Ada Lovelace 2 | Grace Hopper 3 | Zoe Washburne(3 rows) INSERT 0 3psql -d appdb -c "CREATE TABLE customers (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, email text NOT NULL UNIQUE, credit numeric(10,2) NOT NULL DEFAULT 0, tags text[] NOT NULL DEFAULT '{}', profile jsonb NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now())"CREATE TABLEExpected resultThree ids returned, then the full rows including arrays and jsonb.
Success conditionYou can insert and read back generated values in one statement.
Troubleshooting
Coming from MySQL and looking for
AUTO_INCREMENT.Why: PostgreSQL uses sequences, exposed as
serialor identity columns.Fix:
GENERATED ALWAYS AS IDENTITYfor new tables.serialworks but leaves an orphaned sequence if the column is dropped.Wondering whether
varchar(n)is faster thantext.Why: A habit carried from other databases.
Fix:It is not. They are the same storage;
varchar(n)only adds a length check. Usetextplus aCHECKwhen you have a real rule.LAST_INSERT_ID()does not exist.Why: It is a MySQL function.
Fix:
RETURNING id, which is better - it works for multi-row inserts.