PostgreSQL CheatSheet
PostgreSQL is:
- open sourced
- supports both relational and non-relational (JSON) queries
- for dynamic websites and web applications
Why are there schemas in each database?
- To better organize tables, create modules, and use RBAC
- Public is the default schema
- Sometimes putting different organizations into different databases is not as efficient as putting them into different schemas of the same database. An example would be a SAAS company with hunderds or thousands of clients.
Connection and Basic Operations
# use psql to connect on command line:
$ psql -h localhost -p 5432 -U ps_user inno_wiki
# Get version
inno_wiki=# select version();
# Get current connection info
test_db=# \conninfo
You are connected to database "test_db" as user "ps_user" on host "localhost" (address "::1") at port "5432".
Database Operations
- create database options:
- owner
- tablespace
(set the default tablespace / physical storage locations on the server) - template
"template1" is the default; "template0" is a pristine template with only essential system objects - encoding
sets encoding, default is "SQL_ASCII" - locale
sets locale - connection limit
max number of concurrent connections to the database. default is 115 (15 for super users and 100 for regular users) - allow_connections
enables to disables connections to the database
- owner
# List all databases
inno_wiki=# \list
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+---------+----------+-----------------+------------+------------+--------+-----------+---------------------
ccj | ps_user | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
dng | ps_user | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
...
# Switch to a different database
inno_wiki=# \c ccj
You are now connected to database "ccj" as user "ps_user".
# Create a database
ccj=# create database test_db;
CREATE DATABASE
# See the owner
ccj=# \list
# See current connections
test_db=# SELECT * FROM pg_stat_activity WHERE datname = 'test_db';
datid | datname | pid | leader_pid | usesysid | usename | application_name | client_addr | client_hostname | client_port | backend_start | xact_start | query_start | state_change | wait_event_type | wait_event | state | backend_xid | backend_xmin | query_id | query | backend_type
-------+---------+------+------------+----------+---------+------------------+-------------+-----------------+-------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+-----------------+------------+--------+-------------+--------------+----------+-----------------------------------------------------------+----------------
16728 | test_db | 8532 | | 10 | ps_user | psql | ::1 | | 48814 | 2024-12-20 21:17:42.485645+00 | 2024-12-20 21:40:30.649138+00 | 2024-12-20 21:40:30.649138+00 | 2024-12-20 21:40:30.649141+00 | | | active | | 898 | | SELECT * FROM pg_stat_activity WHERE datname = 'test_db'; | client backend
(1 row)
# Alter a database
test_db=# alter database test_db with connection limit = 120;
ALTER DATABASE
# Rename a database
test_db=# alter database test_db rename to test_db2;
ERROR: current database cannot be renamed
# needs to connect to other database
test_db=# \c ccj
You are now connected to database "ccj" as user "ps_user".
ccj=# alter database test_db rename to test_db2;
ALTER DATABASE
# Delete a database
ccj=# drop database if exists test_db2
ccj-# ;
DROP DATABASE
ccj=# drop database if exists test_db;
NOTICE: database "test_db" does not exist, skipping
DROP DATABASE
# Copy a database
ccj=# create database ccj2 with template ccj;
CREATE DATABASE
Schema Operations
- to access an object in a schema, you need to qualify the object by using schema_name.object_name
- database and schemas is a 1-to-N relation
- default schema is the public schema
# get current schema
ccj=# select current_schema();
current_schema
----------------
public
(1 row)
# List schemas
ccj=# \dn
# show search path (the path postgresql uses to search for objects within a database). $user represents the schema that has the same name as the current user
ccj=# SHOW search_path;
search_path
-----------------
"$user", public
(1 row)
# Create Schema
ccj=# create schema sales;
CREATE SCHEMA
# new schema is not in search path
ccj=# SHOW search_path;
search_path
-----------------
"$user", public
(1 row)
# add sales schema to search paths:
ccj=# set search_path to sales, public;
SET
ccj=# SHOW search_path;
search_path
---------------
sales, public
(1 row)
# to give user access to resources in schema that they do not own:
GRANT USAGE ON SCHEMA schema_name TO role_name;
# to allow user to create objects in schemas they do not own:
GRANT CREATE ON SCHEMA schema_name TO user_name;
Table Operations
# List tables
inno_wiki=# \dt
List of relations
Schema | Name | Type | Owner
--------+------------------------+-------+---------
public | admin_menu | table | ps_user
public | admin_operation_log | table | ps_user
public | admin_permissions | table | ps_user
public | admin_role_menu | table | ps_user
public | admin_role_permissions | table | ps_user
public | admin_role_users | table | ps_user
...
```sql
-- Inheritance
create table cities (
name text,
population real,
altitude int
);
create table capitals (
state char(2)
) inherits (cities); -- muliple inheritance is possible
-- Result:
SELECT name, altitude
FROM cities
WHERE altitude > 500;
+----------+----------+
|name | altitude |
+----------+----------+
|Las Vegas | 2174 |
+----------+----------+
|Mariposa | 1953 |
+----------+----------+
|Madison | 845 |
+----------+----------+
SELECT name, altitude
FROM ONLY cities
WHERE altitude > 500;
+----------+----------+
|name | altitude |
+----------+----------+
|Las Vegas | 2174 |
+----------+----------+
|Mariposa | 1953 |
+----------+----------+
SELECT * FROM capitals;
+----------+----------+-----------+
|name | altitude | state |
+----------+----------+-----------+
| Madison | 845 | Wisconsin |
+----------+----------+-----------+
-- Array Type
create table sal_emp (
name text,
pay_by_quarter integer[],
schedule text[][]
);
-- array can be defined as a properly formatted string, or using ARRAY keyword
insert into sal_emp values
('Bill1', '{10000, 10000, 10000, 10000}', '{{"meeting", "lunch"}, {"training", "presentation"}}'),
('Carol1', '{20000, 25000, 25000, 25000}', '{{"breakfast", "consulting"}, {"meeting", "lunch"}}'),
('Bill2', ARRAY[10000, 10000, 10000, 10000], ARRAY[['meeting', 'lunch'], ['training', 'presentation']]),
('Carol2', ARRAY[20000, 25000, 25000, 25000], ARRAY[['breakfast', 'consulting'], ['meeting', 'lunch']]);
-- array is 1-indexed. Select all records where 1st item in pay_by_quarter != 2nd item in pay_by_quarter
select name from sal_emp where pay_by_quarter[1] <> pay_by_quarter[2];
name
-------
Carol
(1 row)
-- select the first 2 items -> first item from schedule
select schedule[1:2][1:1] from sal_emp where name = 'Bill';
schedule
------------------------
{{meeting},{training}}
(1 row)
-- select the first 2 items -> 2 items from schedule, [2] => [:2]
SELECT schedule[1:2][2] FROM sal_emp WHERE name = 'Bill';
schedule
-------------------------------------------
{{meeting,lunch},{training,presentation}}
(1 row)
-- select the first 2 items -> second and rest items from schedule
SELECT schedule[:2][2:] FROM sal_emp WHERE name = 'Bill';
schedule
------------------------
{{lunch},{presentation}}
(1 row)
-- select all items -> first item from schedule
SELECT schedule[:][1:1] FROM sal_emp WHERE name = 'Bill';
schedule
------------------------
{{meeting},{training}}
(1 row)
-- Get array dimensions
SELECT array_dims(schedule) FROM sal_emp WHERE name = 'Carol';
array_dims ( dimension )
------------
[1:2][1:2]
(1 row)
-- Get array upper bound
SELECT array_upper(schedule, 1) FROM sal_emp WHERE name = 'Carol'; -- or array_lower
array_upper ( upper and lower bound of a specified array dimension )
-------------
2
(1 row)
-- Get array length. array_upper = array_length if indexing starts at 1 (array_lower = 1), but sometimes array_lower != 1, in that case, array_upper != array_length
SELECT array_length(schedule, 1) FROM sal_emp WHERE name = 'Carol';
array_length ( length of a specified array dimension )
--------------
2
(1 row)
-- Get number of total elements
SELECT cardinality(schedule) FROM sal_emp WHERE name = 'Carol';
cardinality ( total number of elements across all dimensions )
-------------
4
(1 row)
-- Update array. Can update whole array, or just an item in array
update sal_emp set pay_by_quarter = '{25000,25000,27000,27000}' where name = 'Carol';
update sal_emp set pay_by_quarter = array[25000,25000,27000,27000] where name = 'Carol';
update sal_emp set pay_by_quarter[4] = 15000 where name = 'Bill';
update sal_emp set pay_by_quarter[1:2] = '{27000,27000}' where name = 'Carol';
-- JSONB and JSON. JSONB is generally preferred, but JSON can be more suitable in some situations: need to preserve white space / formatting in logging, legacy systems. We'll use JSONB instead.
create table users (
id serial primary key,
profile jsonb
);
-- "->" return the json object; "-->" returns the json object and convert it to string; "@>" checks wheather one document contains another
-- get all profiles where country is USA, and extract the name field as text and return them
SELECT profile->>'name' AS name FROM users WHERE profile @> '{"country": "USA"}';
-- "->" can be chained:
select '{"name": "Alice", "agent": {"bot": true} }'::jsonb -> 'agent' -> 'bot';
-- returns true
select '{"name": "Alice", "agent": {"bot": true} }'::jsonb @> '{"agent": {"bot": false}}';
-- returns false
select '{"name": "Alice", "agent": {"bot": true} }'::jsonb @> '{"agent": {"bot": true}}';
-- return true
select '{"name": "Alice", "agent": {"bot": true} }'::jsonb -> 'agent' ->> 'bot' is not null;
select '{"name": "Alice", "agent": {"bot": true} }'::jsonb -> 'agent' ? 'bot';
-- jsonb_each: expands into a set of key-value pairs
select jsonb_each( '{"name": "Alice", "agent": {"bot": true} }'::jsonb );
-- Result:
jsonb_each
1 (name,"Alice")
2 (agent,"{"bot": true}")
-- jsonb_object_keys: extract the keys
select jsonb_object_keys( '{"name": "Alice", "agent": {"bot": true} }'::jsonb );
-- Result:
jsonb_object_keys
1 name
2 agent
-- jsonb_extract_path: returns a JSON object that is traversed by a “path”
select jsonb_extract_path( '{"name": "Alice", "agent": {"bot": true} }'::jsonb, 'agent', 'bot');
-- Result:
jsonb_extract_path
1 true
-- jsonb_pretty: prettify
select jsonb_pretty( '{"name": "Alice", "agent": {"bot": true} }'::jsonb );
-- Result:
{
"name": "Alice",
"agent": {
"bot": true
}
}
-- select all users whose metadata contains '{"country": "Peru"}'
SELECT * FROM users WHERE metadata @> '{"country": "Peru"}';
-- select all users whose metadata.country is peru
SELECT * FROM users WHERE metadata->>'country' = 'Peru';
-- select all the metadata.country from users
SELECT metadata->>'country' FROM users;
-- select all the users whose metadata.country is not null
SELECT * FROM users WHERE metadata->>'country' IS NOT NULL;
-- select all users whose metadata.company.name is Mozilla
SELECT * FROM users WHERE metadata->'company'->>'name' = "Mozilla";
-- select all users who has '{"company":{"name": "Mozilla"}}' in the metadata
SELECT * FROM users WHERE metadata @> '{"company":{"name": "Mozilla"}}';
-- Remembering @> operator checks containment in a JSONB column, you can query on an array like {"x": ["a", "b", "c"]"} by just passing {"x":["a"]} to the WHERE clause:
SELECT * FROM users WHERE metadata @> '{"companies": ["Mozilla"]}';
-- select all users whose metadata.countries are in (‘Chad’, ‘Japan’)
SELECT * FROM users WHERE metadata->>'countries' IN ('Chad', 'Japan');
-- Insert into all users
UPDATE users SET metadata = '{"country": "India"}';
-- Use || operator to concatenate the actual data with the new data. It will update or insert the value
UPDATE users SET metadata = metadata || '{"country": "Egypt"}';
-- To remove an attribute
UPDATE users SET metadata = metadata - 'country';
-- Creating a GIN index on the JSONB column can significantly improve query performance, especially for large datasets:
CREATE INDEX idx_user_profile ON users USING GIN (profile);
-- Example: Implementing a Real-Time Analytics System
-- Consider a scenario where a company tracks user interactions on its website. Storing this data in a JSONB column allows for flexible and efficient analysis. For example, counting the number of users who clicked a particular item:
SELECT COUNT(*) FROM user_interactions WHERE event_data @> '{"event": "item_click", "item_id": 123}';
-- This query extracts the email address from a nested contact object and filters users who have 'English' in their language preferences.
SELECT id, profile->'contact'->>'email' AS email FROM users WHERE profile->'preferences'->'languages' ? 'English';
-- This updates the phone number in the user’s profile, demonstrating efficient data modification without rewriting the entire JSON structure.
UPDATE users SET profile = jsonb_set(profile, '{contact,phone}', '"123-456-7890"') WHERE id = 1;
-- Insert value into jsonb
UPDATE users SET profile = jsonb_insert(profile, '{contact,phone2}', '"123-456-7891"') WHERE id = 1;
-- Remove value from jsonb
UPDATE users SET profile = jsonb_remove(profile, '{contact,phone2}') WHERE id = 1;
-- HStore vs JSONB
-- The main differences between them are that: hstore stores key-value pairs as a flat string and only supports basic data types such as strings and null values. JSONB stores JSON data in a binary format and supports nested data structures, arrays, and complex data types.
-- Case sensitive search (be default)
select * from cars where model like '%d';
-- Case insensitive search
select * from cars where model ilike '%d';
-- Between
select * from cars where yaer between 1970 and 1980;
-- in and not in
select * from customers where country in ('Norway', 'Sweden');
select * from customers where country not in ('USA', 'Canada');
-- distince
select distinct country from customers;
-- limit and offset
select * from customers order by name limit 10 offset 20;
-- alias
select customer_name as COMPANY from customers;
-- Group by, Having
SELECT COUNT(customer_id), country
FROM customers
GROUP BY country
HAVING COUNT(customer_id) > 5;
-- Exists
select customer.customer_name
from customers
where exists (
select order_id
from orders
where customer_id = customers.customer_id
);
-- Any and All
select product_name
from products
where product_id = ANY (
select product_id
from order_details
where quantity > 120
);
select product_name
from products
where product_id = ALL (
select product_id
from order_details
where quantity > 10
);
-- Case
select product_name,
case
when price < 10 then 'Low price product'
when price > 50 then 'High price product'
else
'Normal product'
end as "price category"
from products;
-- Views
create view myview as
select name, temp_lo, temp_hi, prcp, date, location
from weather, cities
where city = name;
-- then call the view:
select * from myview;
-- Primary Key
create table users (
id serial primary key,
profile jsonb
);
-- Foreigh Key
create table cities (
name varchar(80) primary key,
location point
);
create table weather (
city varchar(80) references cities(name),
temp_lo int,
tmep_hi int,
prcp real,
date date,
);
-- Transactions
BEGIN;
update accounts set balance = balance - 100.00 where name = 'Alice';
savepoint my_savepoint1;
update accounts set balance = balance + 100.00 where name = 'Bob';
rollback to savepoint my_savepoint1;
update accounts set balance = balance + 100.00 where name = 'Casy';
commit;
| MySQL | PostgreSQL | |
|---|---|---|
| case sensitive in where clause? | No | Yes |
| primary key definition | AUTO_INCREMENT NOT NULL PRIMARY KEY | SERIAL NOT NULL PRIMARY KEY |
| case insensitive search | default | ILIKE '%d' |
| case sensitive search | SELECT * FROM your_table WHERE column COLLATE utf8mb4_bin='SearchTerm'; |
default |
| limit and offset | select * from products limit 20, 10; select * from products limit 10 offset 20; |
select * from products limit 10 offset 20; |
| Group By | select count (customer_id), country from customers group by country; |
selecct count (customer_id), country from customers group by country; |
| min, max, sum, price | same | same |
| (window functions) | ||
| ACID | only with InnoDB | Full Compliance |
| Concurrency Control (MVCC) | only with InnoDB | Full Compliance |
| Indexes | B-Tree and R-Tree | trees, expression indexes, partial indexes, hash indexes |
| Data Types | Pure relational | object-relational, json, array, xml |
| Views | basic | materialized views |
| Stored Procedures | supported | same, but also supports other languages |
| Triggers | only AFTER and BEFORE for Insert, Update, Delete | same, but also INSTEAD OF trigger |
| Performance | better if frequent reads | better if frequent writes |
| Write Performance | Uses locks to prevent concurrent writes | Supports concurrent writes |
| Read Performance | Single process for multiple users | Each user gets a process with 10MB memory |
| if, ifnull | supported | not supported, use case statement |
SQL
-- Create Table
create table cars (
brand varchar(255),
model varchar(255),
year int
);
-- Drop Table
drop table cars;
-- Truncat Table (delete all records)
truncate table cars;
-- Add Column
alter table cars add color varchar(255);
-- Change Column
alter table cars alter column year type varchar(4);
alter table cars alter column color type varchar(30);
-- Drop Column
alter table cars drop column color;
-- CRUD
insert into cars (brand, model, year) values ('Ford', 'Mustang', 1964);
select * from cars;
select brand, year from cars;
update cars set color='red' where brand='Ford';
delete from cars where brand = 'Ford';
-- Others
select * from cars where model like '%d';
select * from cars where model like '_ustang';
select * from cars where year between 1970 and 1980;
select * from cars where brand in ('Ford', 'Audi', 'BMW');
select * from cars where brand not in ('Ford', 'Audi', 'BMW');
select distinct brand from cars;
select * from cars limit 10 offset 20;
select brand as CAR_BRAND from cars
select count(brand), brand
from cars
group by brand
having count(brand) > 5;
#### Roles and RBAC Management
- Roles are not namespaced, they are unique with in a single database
```sql
# List all roles:
inno_wiki=# select * from pg_roles;
rolname | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolconnlimit | rolpassword | rolvaliduntil | rolbypassrls | rolconfig | oid
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+-------------+---------------+--------------+-----------+-------
pg_database_owner | f | t | f | f | f | f | -1 | ******** | | f | | 6171
pg_read_all_data | f | t | f | f | f | f | -1 | ******** | | f | | 6181
pg_write_all_data | f | t | f | f | f | f | -1 | ******** | | f | | 6182
pg_monitor | f | t | f | f | f | f | -1 | ******** | | f | | 3373
...
# Create role:
ccj=# create role ccj_owner;
CREATE ROLE
ccj=# create role ccj_login login password 'testPassword';
CREATE ROLE
# list all roles
ccj=# select rolname from pg_roles;
# list all roles created by current logged in account:
ccj=# \du
List of roles
Role name | Attributes
-----------+------------------------------------------------------------
ccj_login |
ccj_owner | Cannot login
ps_user | Superuser, Create role, Create DB, Replication, Bypass RLS
test
# Create supoeruser role
ccj=# create role inno_owner superuser login password 'innoPass1';
CREATE ROLE
# Create role with database creation permission
ccj=# create role dba createdb login password 'dbaPass1';
CREATE ROLE
# Create role with expiration
ccj=# create role inno_temp login password 'innoPass1' valid until '2025-01-01';
CREATE ROLE
# give connect database access to role
ccj=# grant connect on database inno to inno_temp;
# Alter role with connection limit
ccj=# alter role ccj_login connection limit 1000;
ALTER ROLE
ccj=# \du
List of roles
Role name | Attributes
------------+------------------------------------------------------------
ccj_login | 1000 connections
ccj_owner | Cannot login
dba | Create DB
inno_owner | Superuser
inno_temp | Password valid until 2025-01-01 00:00:00+00
ps_user | Superuser, Create role, Create DB, Replication, Bypass RLS
test |
# Grant Privileges to roles:
# privileges include: select, insert, update, delete, truncate, or all
# Grant all priviledges to user
ccj=# grant all on admin_operation_log to ccj_owner;
GRANT
# Grant all priviledges on all tables in a schema to a role:
ccj=# grant all on all tables in schema public to ccj_owner;
GRANT
# Revoke a privilege:
ccj=# revoke select on all tables in schema public from ccj_owner;
REVOKE
# alter role
alter role calf superuser;
alter role calf nosuperuser;
# Drop role
DROP ROLE [IF EXISTS] target_role;
-- execute these statements in the database that contains
-- the object owned by the target role
REASSIGN OWNED BY target_role TO another_role;
DROP OWNED BY target_role;
-- drop the role
DROP ROLE target_role;
https://neon.tech/postgresql/postgresql-administration/postgresql-backup-database