# PostgreSQL essentials

February 18, 2026 SQL Database Fundamentals

PostgreSQL is one of the most powerful and featurful opensource RDBMS available right now. It is flexible enough to meet the storage needs of most web applications. In this article walk through the PostgreSQL's SQL dialect essentials.

Connecting to Database

Through psql

$ psql -h <hostname> -p <port> -d <database> -U <username>

Connection string

postgresql://devuser:devpass@localhost:5432/dev?sslmode=disable

Exporting existing DB schema

pg_dump --schema-only --no-acl $DATABASE_URL > migrations/0000-init.sql

Comments

-- this style is used, as comments to document SQL statements
SELECT * FROM /* test */ users;

Databases

-- create an new database
CREATE DATABASE sample;

-- delete a database
DROP DATABASE sample;
# create new db using PostgreSQL shell
$ psql -h <Host> -p <Port> -U <User> <database> --command="CREATE DATABASE <db_name> WITH OWNER <User>"

Drop all content of database

DROP SCHEMA public CASCADE;
CREATE SCHEMA public;

Column Data types

TypeDescription
INTSigned four-byte integer
BIGINTSigned 64-Bit (i.e. 8-Byte) integer. Can be used to store money values in cents.
NUMERICA number type with very high precision, supporting floating point numbers without the hassle of rounding-off errors. The best choice for storing money values.
BOOLLogical Boolean (TRUE / FALSE)
CHAR (n)Fixed-length character string
VARCHAR (n)Variable-length character string
TEXTVariable-length character string
UUIDUniversally unique identifier. Takes up 128-Bits of storage.
DATECalendar date (year, month, day)
FLOAT8Double precision floating-point number (8 bytes). Avoid using in production because has precision problems
DECIMAL(15,2)Store amount to exactly 2-decimal points. Used by most general ledger software
INETIPv4 or IPv6 host address
INTERVALTime span
JSONTextual JSON data
SMALLINTSigned 2-byte (16-bit) integer
SERIALAuto Incrementing 4-byte (32-bit) integer. Should not be used as primary key.
BIGSERIALAuto Incrementing 8-byte (64-bit) integer. Can be used as primary keys.
TIMETime of day (no time zone)
TIMETZTime of day, including time zone
TIMESTAMPDate and time (no time zone)
TIMESTAMPTZDate and time, including time zone

Constraints

TypeDescription
NOT NULLColumn value is mandatory
UNIQUEThe value in the column must be unique
DEFAULTSpecify a default value in case a value is not provided by the user
PRIMARY KEYSet column and the primary key for the table
REFERENCES other_table(other_column)Add foreign key constraint
CHECKBoolean checks

Note: If we provide proper names to our constraints, we will be able to catch errors properly when they are thrown by the database driver in case of constraint violations.

Example: Product price and discounted price

CREATE TABLE
  products (
    product_id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,

    -- column level check
    price NUMERIC NOT NULL CHECK (price >= 0),
    discounted_price NUMERIC NOT NULL,

    -- table level check
    CONSTRAINT valid_discount CHECK (discounted_price < price)
  );

Example: Unique combinations

CREATE TABLE
  users (
    user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT,
    CONSTRAINT email_unique UNIQUE (email)
  );
CREATE TABLE
  user_profiles (
    user_id BIGSERIAL,
    profile_id BIGSERIAL,
    CONSTRAINT unique_user_id_profile_id UNIQUE (user_id, profile_id)
  );

Note: In this example we have applied a table level constraint which ensures that following

  • (1, 1) and (1, 2) is allowed
  • (1, 1) and (1, 1) in two separate rows is not allowed.

Tables

In SQL it is a convention that table names should be snake-case plurals.

-- create a new table within a database
CREATE TABLE
  users (
    user_id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    location POINT NOT NULL
  );

UUID

UUIDs can be used as unique identifiers for records. Here is how they work.

-- UUID as primary key, without default value
CREATE TABLE
  users (
    user_id UUID PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL
  );
-- UUID as primary key, with auto-generating UUIDs at insertion
CREATE TABLE
  users (
    user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL
  );
-- generating UUID at the time of insertion
INSERT INTO
  users (user_id, email)
VALUES
  (gen_random_uuid (), 'admin@site.com'),
  (gen_random_uuid (), 'user@site.com');

Why use UUID over BIGSERIAL as primary key?

  • First question to ask is whether the record key should be random or sequential.
  • If record key is going to be exposed to the users, they random keys are more secure. Sequential keys can lead to enumeration attacks.

On the other hand, sequential keys have certain advantages as well

  • They are faster to generate
  • They are quick to index
  • They take up less space than UUID

In summary, use UUID where security / entropy matters, otherwise default to using BIGSERIAL sequential keys.

Array Columns

-- table with array field
CREATE TABLE
  users (
    user_id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    phone_numbers VARCHAR(255) ARRAY,
    CONSTRAINT user_email_unique UNIQUE (email)
  );

-- inserting data into array field
INSERT INTO
  users (user_id, email, phone_numbers)
VALUES
  (
    1,
    'admin@site.com',
    '{"123123897", "39847928347", "53475934875"}'::text[]
  );
-- alternative syntax
phone_numbers TEXT[],

Insert Data into Table

-- insert multiple records into table
INSERT INTO
  users (name, email, dob, location)
VALUES
  (
    'User one',
    'user_one@site.com',
    DATE '1990-02-10',
    POINT '(30.44, 50.23)'
  ),
  (
    'User two',
    'user_two@site.com',
    DATE '1998-09-17',
    POINT '(30.44, 50.23)'
  );
-- insert into multiple tables
WITH password_insert AS (
  INSERT INTO password (hash)
  VALUES ('abc123223213')
  RETURNING id
)

INSERT INTO
  users (email, role, password_id)
  VALUES ('admin@site.com', 'ADMIN', (select id from password_insert));

Enumerations

CREATE TYPE
  user_role AS ENUM('ADMIN', 'USER');

CREATE TABLE
  users (
    user_id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    role user_role NOT NULL,
    password VARCHAR(255)
  );

INSERT INTO
  users (email, role)
VALUES
  ('admin@site.com', user_role 'ADMIN');
create table
  users (
    id uuid primary key,
    email varchar not null,
    role varchar(10),
    constraint role_enum check (role in ('ADMIN', 'USER', 'EMPLOYEE')),
    constraint email_unique unique (email)
  )

Domain types

Domain types can be used to extend existing types with additional checks etc.

CREATE DOMAIN rating AS FLOAT4 CHECK (
  VALUE >= 0
  AND VALUE <= 5
);

CREATE TABLE
  book (
    book_id BIGSERIAL,
    name TEXT,
    rating rating,
    PRIMARY KEY (book_id)
  );

-- insert dummy records
INSERT INTO
  book (book_id, name, rating)
VALUES
  (1, 'Foundation', 5);

Composite types

CREATE TYPE address as (street TEXT, city TEXT, zip INT);

CREATE TABLE
  users (
    email VARCHAR(255) UNIQUE NOT NULL,
    address address,
    PRIMARY KEY (email)
  );

INSERT INTO users (email, address)
VALUES ('admin@site.com', ROW('Main street', 'Lahore', 5400));
-- composite type fields can also be accessed as follows
INSERT INTO users (email, address.street, address.city, address.zip)
VALUES ('admin@site.com', 'Main street', 'Lahore', 5400);

Pagination

SELECT *
FROM users
LIMIT 2
OFFSET 0;

Note: OFFSET 0 will return the first page of results.

Note: The LIMIT keyword is supported by MySQL, PostgreSQL and SQLite but it may not be supported by other databases.

Example

SELECT u.*, COUNT(*) OVER() AS total_count FROM users u
WHERE u.deleted_at IS NULL
LIMIT 10
OFFSET 0;

Note: The above query will not take into account the LIMIT and OFFSET options when calculating the total_count value.

Relationships

One-to-one

Mandatory one-to-one: Adding relationship field to the parent entity with UNIQUE constraint ensures that the a child entity will always exists for the parent. Do note that this does not prevent orphan child entities.

E.g. In the following SQL schema, all users will have profiles but it is not necessary that all profiles will have associated users.

-- table schema
CREATE TABLE
  profiles (
    profile_id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255)
  );

CREATE TABLE
  users (
    user_id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    -- notice the unique constraint (1:1)
    profile_id BIGSERIAL UNIQUE NOT NULL,
    CONSTRAINT fk_user_profile FOREIGN KEY (profile_id) REFERENCES profiles (profile_id)
  );

-- insert dummy data
WITH
  profile_insert AS (
    INSERT INTO
      profiles (name)
    VALUES
      ('User One')
    RETURNING
      profile_id
  )
INSERT INTO
  users (email, profile_id)
VALUES
  (
    'user@site.com',
    (
      SELECT
        profile_id
      FROM
        profile_insert
    )
  );

-- fetch data through users table
SELECT
  users.user_id,
  users.email,
  profiles.name
FROM
  users
JOIN profiles ON users.profile_id = profiles.profile_id;

-- fetch data through profiles table
SELECT
  profiles.profile_id,
  users.user_id,
  users.email,
  profiles.name
FROM
  profiles
  JOIN users ON profiles.profile_id = users.profile_id
WHERE
  profiles.profile_id = 2;

Optional one-to-one: Adding relationship field to the child entity with UNIQUE constraint ensures that all child entities will be associated with parent entities. However, this does not mean that all parent entities will necessarily have associated child entities.

E.g. In the following SQL schema, it is ensured that all profiles will have associated users. However, it is possible that some users may not have associated profile.

CREATE TABLE
  users (
    user_id BIGSERIAL NOT NULL,
    email text UNIQUE NOT NULL,
    PRIMARY KEY (user_id)
  );

CREATE TABLE
  profiles (
    profile_id BIGSERIAL NOT NULL,
    title TEXT NOT NULL,
    -- notice the unique constraint (1:1)
    user_id BIGSERIAL UNIQUE NOT NULL,
    PRIMARY KEY (profile_id),
    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (user_id)
  );

-- insert dummy record
WITH
  user_insert AS (
    INSERT INTO
      users (email)
    VALUES
      ('user@site.com') RETURNING user_id
  )
INSERT INTO
  profiles (profile_id, title)
VALUES
  (
    (
      SELECT
        user_id
      from
        user_insert
    ),
    'Post one title'
  );

-- fetch from parent entity
SELECT
  users.user_id,
  users.email,
  profiles.title
FROM
  users
  JOIN profiles ON users.user_id = profiles.user_id
WHERE
  users.user_id = 1;

-- fetch from child entity
SELECT
  users.user_id,
  users.email,
  profiles.title
FROM
  profiles
  JOIN users ON profiles.user_id = users.user_id
WHERE
  users.user_id = 1;

One-to-many

-- table schema
CREATE TABLE
  users (
    user_id BIGSERIAL PRIMARY KEY,
    email VARCHAR(100)
  );

CREATE TABLE
  posts (
    post_id BIGSERIAL PRIMARY KEY,
    title VARCHAR(100) UNIQUE,
    -- notice user_id has *no unique constraint* (1:M)
    user_id BIGSERIAL NOT NULL,
    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (user_id)
  );

-- insert dummy data
INSERT INTO
  users (email)
VALUES
  ('admin@site.com');

WITH
  user_insert AS (
    INSERT INTO
      users (email)
    VALUES
      ('user@site.com')
    RETURNING
      user_id
  )
INSERT INTO
  posts (title, user_id)
VALUES
  (
    'post one',
    (
      SELECT
        user_id
      FROM
        user_insert
    )
  ),
  (
    'post two',
    (
      SELECT
        user_id
      FROM
        user_insert
    )
  );

-- fetch related records
SELECT
  users.user_id,
  users.email,
  posts.title
FROM
  users
  JOIN posts ON users.user_id = posts.user_id
WHERE
  users.user_id = 2;

Many-to-many

-- schena definitions
CREATE TABLE
  roles (
    role_id BIGSERIAL NOT NULL,
    role_name VARCHAR (100) NOT NULL,
    PRIMARY KEY (role_id)
  );

CREATE TABLE
  users (
    user_id BIGSERIAL NOT NULL,
    email VARCHAR (255) UNIQUE NOT NULL,
    password VARCHAR (255),
    PRIMARY KEY (user_id)
  );

-- TODO: update example to use compound ids, drop user_role_id column.
CREATE TABLE
  user_roles (
    user_role_id BIGSERIAL NOT NULL,
    user_id BIGSERIAL NOT NULL,
    role_id BIGSERIAL NOT NULL,
    PRIMARY KEY (user_role_id),
    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (user_id),
    CONSTRAINT fk_user_role FOREIGN KEY (role_id) REFERENCES roles (role_id)
  );

-- insert test data
INSERT INTO
  roles (role_name)
VALUES
  ('admin'),
  ('customer'),
  ('user');

INSERT INTO
  users (email)
VALUES
  ('admin_one@site.com'),
  ('admin_two@site.com'),
  ('user@site.com'),
  ('customer@site.com');

INSERT INTO
  user_roles (user_id, role_id)
VALUES
  (1, 1),
  (2, 1),
  (3, 3),
  (4, 2);

-- query: get all admins
SELECT
  users.user_id,
  users.email,
  roles.role_name
FROM
  users
  INNER JOIN user_roles ON users.user_id = user_roles.user_id
  INNER JOIN roles ON user_roles.role_id = roles.role_id
WHERE
  roles.role_name = 'admin';

Relationships and Delete cascades

When creating relationships, always question yourself which table is the parent and which table is the child. This is because we will very likely have delete cascade in our foreign relationships. Consider the following schema.

create table "user" (
  id uuid not null
  , email varchar(255) not null
  , password varchar(255) not null
  , created_at timestamp not null default now()
  , updated_at timestamp not null default now()
  , primary key (id)
  , constraint email_unique unique(email)
);

create index user_email_idx on "user"(email);

create table user_profile (
  id uuid not null
  , name varchar(255)
  , address text
  , city varchar(255)
  , country varchar(255)
  , user_id uuid not null
  , primary key (id)
  , constraint fk_user foreign key (user_id) references "user"(id) on delete cascade
  , constraint user_id_unique unique(user_id)
);

create index user_profile_user_id_idx on user_profile(user_id);

In this schema, user is the parent and user_profile is the child table. This is an optional one-to-one relation because a user may not have any associated user_profile.

Notice the user_id field with delete cascade in the user_profile table. This ensures that when a user is deleted, the associated user_profile is also deleted.

To sum up, is delete cascade is specified, the entry in child table will be deleted when the entry in the parent table is deleted.

JSON Aggregation

We can use this trick to fetch records of relationships as JSON array. This will make it incredibly easier to serialise returned data into native language of our choosing.

CREATE TABLE
  posts (
    post_id UUID DEFAULT gen_random_uuid(),
    title TEXT NOT NULL,
    PRIMARY KEY (post_id)
  );

CREATE TABLE
  comments (
    comment_id UUID DEFAULT gen_random_uuid(),
    text VARCHAR(255) NOT NULL,
    post_id UUID NOT NULL,
    PRIMARY KEY (comment_id),
    CONSTRAINT fk_post FOREIGN KEY (post_id) REFERENCES posts (post_id)
  );
SELECT
  posts.post_id,
  posts.title,
  jsonb_agg (
    jsonb_build_object (
      'comment_id',
      comments.comment_id,
      'text',
      comments.text
    )
  ) as comments
FROM
  posts
  JOIN comments ON posts.post_id = comments.post_id
GROUP BY
  posts.post_id,
  posts.title;

A better implementation

create table
  users (
    user_id uuid default gen_random_uuid (),
    email text not null,
    primary key (user_id),
    constraint email_unique unique (email)
  );

create table
  posts (
    post_id uuid default gen_random_uuid (),
    title text not null,
    user_id uuid not null,
    primary key (post_id),
    constraint fk_user foreign key (user_id) references users (user_id)
  );
select
  users.user_id,
  users.email,
  coalesce(
    jsonb_agg (posts) filter (where posts.user_id is not null),
    to_jsonb('[]'::text)
  ) as posts
from
  users
  left join posts on users.user_id = posts.user_id
group by
  users.user_id

Note: coalesce is a variadic function which returns the first non-null value it is provided. In the above example, the first argument to coalesce will be NULL when no posts are found against the user. The second argument to coalesce will be a non-null value, therefore it will be returned instead of NULL.

Transactions

ACID

ACID is an acronym that refers to the set of 4 key properties that define a transaction.

  • Atomicity: Each statement in a transaction (to read, write, update or delete data) is treated as a single unit. Either the entire statement is executed, or none of it is executed. This property prevents data loss and corruption from occurring in case of system failure.

  • Consistency: Ensures that transactions only make changes to tables in predefined, predictable ways. Transactional consistency ensures that corruption or errors in your data do not create unintended consequences for the integrity of your table.

  • Isolation: When multiple users are reading and writing from the same table all at once, isolation of their transactions ensures that the concurrent transactions don’t interfere with or affect one another. Each request can occur as though they were occurring one by one, even though they’re actually occurring simultaneously.

  • Durability: Ensures that changes to your data made by successfully executed transactions will be saved, even in the event of system failure.

-- table schema as some dummy data
CREATE TABLE
  accounts (
    account_id BIGSERIAL,
    balance BIGINT NOT NULL,
    PRIMARY KEY (account_id),
    CONSTRAINT balance_nonnegative CHECK (balance >= 0)
  );

INSERT INTO accounts (account_id, balance)
VALUES (1, 100000), (2, 20000), (3, 40500000);

Note: BIGINT is used to store currency values in cents.

BEGIN;
UPDATE accounts SET balance = balance - 100000 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100000 WHERE account_id = 2;
COMMIT;

Performance optimisations

Prepared statements

-- define the prepared statement
PREPARE
  users_insert AS
INSERT INTO
  users (name, email)
VALUES
  ($1, $2);
-- execute existing prepared statement
EXECUTE users_insert ('User one', 'user_one@site.com');

Note: Prepared statements are scoped to connection, this means that only the session which prepares the statement can execute it.

Indexing

A good rule of thumb is to create database indexes for everything that is referenced in the WHERE, HAVING and ORDER BY parts of your SQL queries.

-- create indexes on fields frequently used for querying tables
CREATE INDEX users_index ON users (email, name);
-- remove index if not required
DROP INDEX users_index;

Note: Indexes help with read speeds. However, they slow down write speeds because with every new records inserted, indexes have to be updated as well.

Partitioning

-- create the original table: will be partitioned by day (through timestamp)
CREATE TABLE
  events (
    event_id UUID NOT NULL,
    payload JSON NOT NULL,
    source INET NOT NULL,
    event_timestamp TIMESTAMP
  )
PARTITION BY
  RANGE (event_timestamp);

-- create subpartitions for single days
CREATE TABLE
  events_20230401 PARTITION OF events FOR
VALUES
FROM
  ('2023-04-01') TO ('2023-04-02');

CREATE TABLE
  events_20230402 PARTITION OF events FOR
VALUES
FROM
  ('2023-04-02') TO ('2023-04-03');

Caveats

  • The sub-partition tables in the above example will need to be created using a scheduler like CRON job etc.
  • If a query has to multiple partitions, it will be slower than if we had not used partitioning at all.
  • If a table is partitioned, it cannot have a global unique row identifier. However, row IDs can be unique within any single partition.

Stored Procedures

Procedures are reusable blocks of statements, which don’t return a value.

-- example table with some dummy data
CREATE TABLE
  accounts (
    account_id BIGSERIAL,
    balance BIGINT NOT NULL,
    PRIMARY KEY (account_id),
    CONSTRAINT balance_nonnegative CHECK (balance >= 0)
  );

INSERT INTO
  accounts (account_id, balance)
VALUES
  (1, 100),
  (2, 300),
  (3, 4000);
CREATE
OR
REPLACE
  PROCEDURE transfer_balance (
    from_account INTEGER,
    to_account INTEGER,
    amount BIGINT
  ) AS $$ BEGIN
  -- deduct from sender
UPDATE
  accounts
SET
  balance = balance - amount
WHERE
  account_id = from_account;

-- add to recepient
UPDATE
  accounts
SET
  balance = balance + amount
WHERE
  account_id = to_account;

COMMIT;
END;
$$ LANGUAGE plpgsql;

Note: The statements defined within the procedure body are executed as a single transaction. This means, when a procedure is called, it will either succeed entirely or fail entirely.

-- list out all stored procedures inside psql shell
db=# \df
-- execute a stored procedure
CALL transfer_balance (1, 2, 100);
-- drop procedure if not required
DROP PROCEDURE
  transfer_balance (
    from_account INTEGER,
    to_account INTEGER,
    amount BIGINT
  );

Stored functions

-- define schema
CREATE TABLE
  profiles (
    profile_id BIGSERIAL,
    name TEXT,
    address TEXT,
    PRIMARY KEY (profile_id)
  );

CREATE TABLE
  users (
    user_id BIGSERIAL,
    email TEXT UNIQUE,
    profile_id BIGSERIAL UNIQUE, -- one-to-one relation
    PRIMARY KEY (user_id),
    CONSTRAINT fk_profile FOREIGN KEY (profile_id) REFERENCES profiles (profile_id)
  );

-- insert dummy data
INSERT INTO
  profiles (profile_id, name, address)
VALUES
  (1, 'Admin', '12 Main street'),
  (2, 'Customer', '13 Street');

INSERT INTO
  users (user_id, email, profile_id)
VALUES
  (1, 'admin@site.com', 1),
  (2, 'customer@site.com', 2);
-- define stored functions and their return (composite) types
CREATE TYPE user_with_profile as (email TEXT, name TEXT, address TEXT);

CREATE
  FUNCTION get_user (id INT) RETURNS user_with_profile AS $$
SELECT
  users.email,
  profiles.name,
  profiles.address
FROM
  users
  JOIN profiles ON users.profile_id = profiles.profile_id
WHERE
  users.user_id = id
LIMIT
  1 $$ LANGUAGE SQL;

-- execute defined function
SELECT
  *
FROM
  get_user (1);

Note: If we are simply returning a row from an existing (single) table, we can put the name of the table as the function return type.

-- remove a defined function
DROP FUNCTION get_user (id INT);

Hashing passwords inside PostgreSQL

PostgreSQL hashing functions rely on pgcrypto extension, it needs to be manually enabled on the database.

-- enable extension on the current database
CREATE EXTENSION pgcrypto;
-- define a stored function for quickly hashing passwords
CREATE
OR
REPLACE
  FUNCTION hash_password (password VARCHAR(255)) RETURNS VARCHAR(255) AS $$ BEGIN
RETURN
  crypt (password, gen_salt ('bf'));

END;
$$ LANGUAGE plpgsql;
-- hash password at the time of record creation
INSERT INTO
  users (user_id, email, password)
VALUES
  (
    gen_random_uuid (),
    'admin@site.com',
    hash_password ('abc123123')
  );

Views

CREATE TABLE
  sites (
    site_id BIGSERIAL,
    site_name TEXT,
    PRIMARY KEY (site_id)
  );

CREATE TABLE
  orders (
    order_id BIGSERIAL,
    amount DECIMAL,
    PRIMARY KEY (order_id)
  );

CREATE TABLE
  site_orders (
    site_id BIGSERIAL,
    order_id BIGSERIAL,
    PRIMARY KEY (site_id, order_id),
    FOREIGN KEY (site_id) REFERENCES sites (site_id),
    FOREIGN KEY (order_id) REFERENCES orders (order_id)
  );
CREATE VIEW
  site_orders_view AS
SELECT
  sites.site_name,
  orders.order_id,
  orders.amount
FROM
  site_orders
  JOIN sites ON site_orders.site_id = sites.site_id
  JOIN orders on site_orders.order_id = orders.order_id
WHERE
  orders.amount > 100;
SELECT * FROM site_orders_view;

N+1 query problem

N+1 queries are a performance problem in which the application makes database queries in a loop, instead of making a single query that returns or modifies all the information at once. Each database connection takes some amount of time, so querying the database in a loop can be many times slower than doing it just once. This problem often occurs when you use an object-relational mapping (ORM) tool in web frameworks like Django or Ruby on Rails.

Optional filter parameters

SELECT * FROM people
WHERE
	employed = TRUE
	AND ($1 IS NULL OR age > $1)

In the above query, $1 is a nullable value for age. If we pass in a non-null value, the condition age > $1 will apply. Is we pass in a null value, only the first portion of the condition i.e. $1 IS NULL will evaluate and short-circuit the condition.

To-do

  • Type casting
  • Additional Features Link
  • Volatile
  • Triggers / Listen / Notify
  • Partitioning Link