Only one primary key per table (can be composite).
Foreign Key
Refers to the primary key in another table.
Enforces referential integrity.
Can define behavior on update/delete (CASCADE, SET NULL, RESTRICT).
4. Explain VIEW
View is a virtual table based on the result of a SQL query.
It does not store data itself, but it provides a saved query that you can treat like a table in your SQL
statements.
Why Use Views?
Simplify complex queries: You can encapsulate joins or filters into a view.
Improve readability: Abstract away complex logic.
Enhance security: Expose only specific columns or rows to certain users.
Reusability: Define logic once, use it anywhere.
Syntax:
CREATE VIEW view_name AS
SELECT column1, column2
FROM table_name
WHERE condition;
5. π What are CTEs (Common Table Expressions) in PostgreSQL?
CTE is a temporary result set that we can define within a query that is used to simplify the complex
statement.
A common table expression (CTE) allows you to create a temporary result set within a query. A CTE helps you
enhance the readability of a complex query by breaking it down into smaller and more reusable parts.
Syntax:
WITH cte_name AS (
SELECT column1, column2
FROM some_table
WHERE condition
)
SELECT *
FROM cte_name
WHERE another_condition;
Example:
WITH recent_orders AS (
SELECT * FROM orders WHERE order_date > CURRENT_DATE - INTERVAL '30 days'
)
SELECT customer_id, COUNT(*) AS total_orders
FROM recent_orders
GROUP BY customer_id;
Use Case β I: Find all employees whose salary is greater than the average salary of their department.
Create a CTE that calculates average salary per department.
Use that CTE to filter employees whose salary is higher than the average.
WITH dept_avg_salary AS (
SELECT
department_id,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
)
SELECT
e.employee_id,
e.name,
e.salary,
e.department_id
FROM employees e
JOIN dept_avg_salary d
ON e.department_id = d.department_id
WHERE e.salary > d.avg_salary;
Use Case β II: Find the highest paid employee(s) in each department.
Create a CTE to get the maximum salary per department.
Join this with the employee table to get full details of the top earners.
WITH dept_max_salary AS (
SELECT
department_id,
MAX(salary) AS max_salary
FROM employees
GROUP BY department_id
)
SELECT
e.employee_id,
e.name,
e.salary,
e.department_id
FROM employees e
JOIN dept_max_salary d
ON e.department_id = d.department_id
AND e.salary = d.max_salary;
6. π Explain Triggers and How to Create Them?
A PostgreSQL trigger is a database object that automatically executes a function in response to an event
such as insert, update, delete, truncate operation.
It is mainly used for data validation, automatic updates, and preventing invalid transactions, etc.
Use OLD to access the row before change.
Use NEW to access the row after change.
Trigger Components:
Trigger function
Trigger (Defines when the function is called and on which table/event)
Example:
Track every time an employeeβs salary is changed and store the old and new salary in a log table.
CREATE TABLE employees (
employee_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
department_id INT,
salary NUMERIC(10,2)
);
π This is your main table storing employee data.
π This table will log every salary change β who changed, what changed, and when.
CREATE OR REPLACE FUNCTION log_salary_change()
RETURNS TRIGGER AS $$
BEGIN
-- Only log if salary actually changed
IF NEW.salary IS DISTINCT FROM OLD.salary THEN
INSERT INTO salary_audit_log (employee_id, old_salary, new_salary)
VALUES (OLD.employee_id, OLD.salary, NEW.salary);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
π This function checks if the salary was changed and logs it to salary_audit_log.
CREATE TRIGGER trigger_salary_change
AFTER UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION log_salary_change();
π This connects the function to the employees table and ensures it's triggered after every UPDATE.
7. π Explain Functions in PostgreSQL
In PostgreSQL, functions (also called stored functions or user-defined functions) are reusable blocks of
code that perform operations and return results.
Functions can take input parameters, perform complex computations, interact with the database, and return a
single value or a set of rows.
Why Use Functions?
Code reusability
Better performance (precompiled and cached)
Abstraction and encapsulation of business logic
Cleaner and more maintainable SQL code
Can be used in SQL queries like built-in functions
πΉ Syntax of a Function in PostgreSQL
CREATE [OR REPLACE] FUNCTION function_name (
[parameter_name data_type, ...]
)
RETURNS return_type AS $$
BEGIN
-- function body (logic)
RETURN some_value;
END;
$$ LANGUAGE plpgsql;
πΉ Example 1: A Simple Function
CREATE OR REPLACE FUNCTION add_numbers(a INTEGER, b INTEGER)
RETURNS INTEGER AS $$
BEGIN
RETURN a + b;
END;
$$ LANGUAGE plpgsql;
Usage: SELECT add_numbers(5, 3); -- Output: 8
πΉ Example 2: Function Returning a Table (Set-returning function)
CREATE OR REPLACE FUNCTION get_users_by_role(role_name TEXT)
RETURNS TABLE(id INT, name TEXT) AS $$
BEGIN
RETURN QUERY
SELECT id, name FROM users WHERE role = role_name;
END;
$$ LANGUAGE plpgsql;
Usage: SELECT * FROM get_users_by_role('admin');
πΉ Function vs Stored Procedure
Functions must return a value.
Procedures (added in PostgreSQL 11+) do not need to return anything and are used with CALL.
π Explain Window Functions in PostgreSQL
β’ A window function performs a calculation across a set of rows related to the current row.
β’ This set of rows is called the window frame.
β’ Window functions are always used with the OVER() clause.
β’ You can group rows using the PARTITION BY clause (e.g., rank employees within each
department).
β’ You can sort rows within the window using the ORDER BY clause.
β’ Unlike aggregate functions, window functions do not collapse or remove rows β they keep all rows and add
calculated results.
1. Ranking Functions
Used to assign rank or sequence to rows within a partition.
RANK(): Assigns a unique sequential number to each row within the partition.
DENSE_RANK(): Similar to RANK(), but no gaps in ranking values.
ROW_NUMBER(): Assigns a unique sequential number to each row within the partition.
2. Aggregate Functions
Traditional aggregate functions used as window functions to retain row-level detail.
SUM() β Calculates the cumulative sum
AVG() β Returns the running average
MIN() / MAX() β Finds the minimum or maximum in the frame
COUNT() β Counts rows in the frame
3. Lead and Lag Functions
Used to look at another row's value in the result β either before (LAG) or after (LEAD) the current row.
LEAD(column, offset): Returns the value from a subsequent row.
LAG(column, offset): Returns the value from a previous row.
4. Window Frame Functions
These functions let you pick values from certain positions in a window frame β like the first, last, or nth
row β not just the current row.
FIRST_VALUE(column)
LAST_VALUE(column)
NTH_VALUE(column, n)
5. Percentile Functions
Percentile functions help you find values at a certain percentage in a dataset β like finding the median or
the value at the 90th percentile.
Useful in statistics, data analysis, and performance measurement.
PERCENT_RANK(): Relative rank of a row within the partition (between 0 and 1).
CUME_DIST(): Cumulative distribution value (percentage of rows less than or equal to the
current row).
PERCENTILE_CONT(p): Finds the value at a given percentile using linear
interpolation. Example: PERCENTILE_CONT(0.5) β Median (50th percentile)
PERCENTILE_DISC(p): Like PERCENTILE_CONT, but returns the closest matching actual row β no
interpolation. Example: PERCENTILE_DISC(0.9) β Value at 90th percentile (rounded up to
actual row)
6. Distribution Functions
Divides the result set or calculates distribution metrics.
NTILE(n): Splits the partition into n equal groups (tiles or buckets).
CUME_DIST(): Returns cumulative distribution β % of rows less than or equal to current row.
Examples:
-- Assigns a unique number to each row based on the order.
SELECT name, salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num
FROM employees;
-- Ranks rows with gaps if values are tied.
SELECT name, salary,
RANK() OVER (ORDER BY salary DESC) AS rank
FROM employees;
-- Ranks rows without gaps (no skipping numbers).
SELECT name, salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;
-- Splits rows into n roughly equal groups.
SELECT name, salary,
NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees;
-- Returns the first value in the window frame.
SELECT name, salary,
FIRST_VALUE(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS top_salary
FROM employees;
Partitioning allows you to divide a large table into smaller, more manageable pieces called partitions,
which can improve query performance and make maintenance easier.
Instead of storing all rows in one big table, PostgreSQL splits them into smaller child tables, based on
some rule (like date range, category, etc.).
Types of Partitioning
Type
Description
Range Partitioning
Rows are divided based on value ranges (e.g., dates).
List Partitioning
Rows are divided by a list of values (e.g., region = 'US').
Hash Partitioning
Rows are divided based on a hash of a column (even distribution).
Composite Partitioning
Combination of two types (e.g., range + list).
Example: Range Partitioning by Date
CREATE TABLE sales (
id SERIAL,
amount NUMERIC,
sale_date DATE
) PARTITION BY RANGE (sale_date);
CREATE TABLE sales_2024_q1 PARTITION OF sales
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE sales_2024_q2 PARTITION OF sales
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
Now, if you insert:
INSERT INTO sales (amount, sale_date) VALUES (500, '2024-02-15');
π It will automatically go into sales_2024_q1.
Example: List Partitioning by Region
CREATE TABLE customers (
id SERIAL,
name TEXT,
region TEXT
) PARTITION BY LIST (region);
CREATE TABLE customers_us PARTITION OF customers FOR VALUES IN ('US');
CREATE TABLE customers_uk PARTITION OF customers FOR VALUES IN ('UK');
π Explain Transaction in PostgreSQL?
A transaction is a sequence of one or more SQL operations executed as a single unit of work, which must
either completely succeed or completely fail.
Transactions ensure data integrity and consistency, especially in the presence of errors, concurrent access,
or system failures.
Transactions follow the ACID properties:
Atomicity: Guarantees that all parts of a transaction are completed successfully. If
any part fails, the entire transaction is rolled back (the "all or nothing" principle).
Consistency: A transaction brings the database from one valid state to another,
preserving data integrity. Consistency ensures the data stays accurate and reliable, before and after
each transaction.
Isolation: Transactions are isolated from each other until they are committed, avoiding
interference (donβt affect each other while theyβre running).
Durability: Once committed, changes are permanent even in case of a crash. This ensures
that the effects of the transaction are permanently recorded in the database.
There are three main commands in a transaction block. They are:
BEGIN: Starts a transaction block.
COMMIT: Commits the current transaction block, making all changes made within the block
permanent.
ROLLBACK: Cancels all changes made in the current transaction block, reverting to the
state before the transaction began.
Transfer $100 from Account A (id=1) to Account B (id=2). If any step fails (e.g., insufficient balance), roll
back to a safe point or cancel the whole transaction.
-- Step 1: Start the transaction
BEGIN;
-- Step 2: Check balance of Account A
SELECT balance FROM accounts WHERE id = 1;
-- Step 3: Savepoint before update
SAVEPOINT before_transfer;
-- Step 4: Deduct $100 from Account A
UPDATE accounts
SET balance = balance - 100
WHERE id = 1
AND balance >= 100;
-- Check how many rows were affected (simulate a check in an application)
-- If 0 rows affected, rollback to savepoint (insufficient funds)
-- For demo, assume no rows affected
ROLLBACK TO before_transfer;
-- Log the failure or notify user (done outside SQL)
-- Step 5: Proceed with a valid case (simulate proper balance now)
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
-- Step 6: Add $100 to Account B
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- Step 7: Finalize the transaction
COMMIT;
Code:
CREATE OR REPLACE FUNCTION transfer_money(from_id INT, to_id INT, amount NUMERIC)
RETURNS TEXT AS $$
DECLARE
rows_updated INT;
BEGIN
BEGIN
-- Start transaction implicitly in function
SAVEPOINT before_transfer;
-- Try to deduct amount
UPDATE accounts
SET balance = balance - amount
WHERE id = from_id AND balance >= amount;
GET DIAGNOSTICS rows_updated = ROW_COUNT;
IF rows_updated = 0 THEN
ROLLBACK TO SAVEPOINT before_transfer;
RETURN 'Insufficient funds';
END IF;
-- Credit the other account
UPDATE accounts
SET balance = balance + amount
WHERE id = to_id;
RETURN 'Transfer successful';
EXCEPTION WHEN OTHERS THEN
ROLLBACK;
RETURN 'Error occurred';
END;
END;
$$ LANGUAGE plpgsql;
Call the above function:
SELECT transfer_money(1, 2, 100);
Explain Indexing in PostgreSQL?
Indexing is a special technique used in databases to optimize data retrieval operations.
It will Improve performance when searching, filtering, joining, or ordering large datasets
It's similar to an index in a book β instead of scanning every page to find a topic, you can jump
directly to the right page using the index.
An index in PostgreSQL is a data structure (typically a B-tree, though others exist) that helps
PostgreSQL find rows efficiently without scanning the entire table.
Use indexes on columns used in WHERE, JOIN, ORDER BY, GROUP BY.
Types of Indexes:
B-tree (default):
This is the default index type in PostgreSQL
It is used for Equality and range comparisons (=, <, >, BETWEEN, ORDER BY)
Structure: Balanced tree (B-tree)
Queries like: SELECT * FROM users WHERE age > 30;
Default index type in PostgreSQL when you write: CREATE INDEX ON users(age);
Hash:
Hash indexes use a hash function to map values to locations in the index. These indexes are efficient
for exact matches but are not suited for range queries.
It is used for exact match query (=)
CREATE INDEX idx_hash ON books USING HASH (author); CREATE INDEX USING hash ON users(email); SELECT * FROM users WHERE email = 'john@example.com';
GiST (Generalized Search Tree):
GiST indexes are flexible and support a wide range of data types and search operations.
They are particularly useful for spatial and full-text search queries.
Best for: Spatial and range queries (<<, &<, &>, >>, @>, <@, ~=)
CREATE INDEX idx_gist ON books USING GIST (published_year);
SP-GiST:
SP-GiST is a specialized type of index in PostgreSQL designed to support partitioned search structures,
especially where data can be recursively split into non-overlapping regions β great for hierarchical,
spatial, or multidimensional data.
Best for: IP ranges, prefix searches, geometric data, etc
GIN:
GIN indexes are designed for handling complex data types such as arrays and full-text searches.
They are optimized for fast search operations but it can handle fast searches and typically consumes
more space.
Best for: Array and full-text searches (<@, @>, =, &&).
CREATE INDEX idx_gin ON books USING GIN (title);
BRIN:
BRIN indexes are suitable for large tables with ordered data.
They divide the table into blocks and store summarized information for each block, making them efficient
for range queries on sorted data.
Best for: Large datasets with ordered data, especially for range queries (<, <=, =, >=,
>).
CREATE INDEX idx_brin ON books USING BRIN (published_year);
Explain JOIN in PostgreSQL?
A JOIN is used to combine rows from two or more tables based on a related column between them.
This is commonly used in relational databases to retrieve data that is spread across multiple tables.
You can filter JOIN results with WHERE or ON conditions.
Types of JOINs in PostgreSQL
INNER JOIN
Returns only rows that have matching values in both tables.
SELECT *
FROM orders
INNER JOIN customers ON orders.customer_id = customers.id;
β Only matching records.
LEFT JOIN (or LEFT OUTER JOIN)
Returns all rows from the left table, and matched rows from the right table. If no match, NULLs are
returned.
SELECT *
FROM orders
LEFT JOIN customers ON orders.customer_id = customers.id;
β All from left table + matches from right.
RIGHT JOIN (or RIGHT OUTER JOIN)
Opposite of LEFT JOIN. Returns all rows from the right table and matched ones from the left.
SELECT *
FROM orders
RIGHT JOIN customers ON orders.customer_id = customers.id;
FULL JOIN (or FULL OUTER JOIN)
Returns rows when there is a match in either left or right table. NULLs are filled where there's no
match.
SELECT *
FROM orders
FULL JOIN customers ON orders.customer_id = customers.id;
CROSS JOIN
Returns the Cartesian product: all combinations of rows from both tables.
SELECT *
FROM products
CROSS JOIN categories;
β Be careful β can produce a huge result set.
SELF JOIN
A table is joined with itself using aliases (allowing you to compare data within the same table).
SELECT a.name AS employee, b.name AS manager
FROM employees a
JOIN employees b ON a.manager_id = b.id;
Explain MVCC in PostgreSQL?
MVCC (Multi-Version Concurrency Control) in PostgreSQL is a concurrency control method used to handle
simultaneous transactions without locking the entire database.
It allows multiple transactions to access the same data at the same time without interfering with each
other, ensuring consistency, isolation, and performance.
When a transaction modifies a row, PostgreSQL creates a new version of that row and tags it with the
transaction ID. Other transactions continue to see the previous version of the row until the modifying
transaction is committed. This approach ensures consistency and isolation without requiring read locks.
Key Concepts
Tuple: A version of the row in the database.
Transaction ID: A unique identifier for each transaction in the system.
Garbage Collection and VACUUM
Once a transaction is committed, older versions of the data that are no longer visible to any active
transactions are eventually cleaned up by a process called vacuuming.
Vacuuming: The process of cleaning up old, no longer needed versions of data.
How Does VACUUM Work?
Identifies dead tuples: Any tuple (row) with an xmax transaction ID older than the current
transaction is marked as dead.
Removes the dead tuples: It removes these tuples from storage, freeing up space.
Updates visibility maps: Ensures visibility information is properly updated for each page of the table.
Real-World Analogy
Think of a notebook of orders in a restaurant.
Each change to an order is written as a new line, not overwritten.
Eventually, the notebook gets full of outdated lines.
A cleaner (VACUUM) comes in and erases old, irrelevant lines to keep things tidy and fast to read.
Summary
MVCC lets PostgreSQL keep many versions of a row at the same time.
When you update a row, it doesnβt overwrite the old one β it creates a new copy.
The old version is marked as "dead", but still remains in the table temporarily.
This helps other users read the old version while a new one is being written β so reads and writes donβt
block each other.
Each version of the row is marked with a transaction ID, so PostgreSQL knows which version to show to
which user.
This system allows multiple users to work concurrently without locking the whole table.
Exception Handling in PostgreSQL
Exception handling is primarily managed using the PL/pgSQL language's BEGIN ... EXCEPTION
block.
Use BEGIN ... EXCEPTION to handle errors gracefully within functions, procedures, and DO
blocks.
You can catch specific exceptions like division_by_zero, or use a generic handler for others.
Use RAISE to throw errors or display custom messages.
Common Exception Types
division_by_zero
undefined_table
undefined_column
raise_exception
no_data_found
others (generic handler for any other exceptions)
Syntax for Exception Handling
BEGIN
-- Code that might throw an exception
EXCEPTION
WHEN exception_type THEN
-- Handling code
END;
Example: Handling Division by Zero
DO $$
DECLARE
result numeric;
BEGIN
BEGIN
result := 10 / 0;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Cannot divide by zero.';
result := NULL;
END;
RAISE NOTICE 'Result is: %', result;
END;
$$ LANGUAGE plpgsql;
Using RAISE for Custom Errors
DO $$
BEGIN
RAISE EXCEPTION 'This is a custom error message.';
END;
$$ LANGUAGE plpgsql;
Handling Multiple Exceptions
BEGIN
-- risky operation
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Handled division by zero.';
WHEN others THEN
RAISE NOTICE 'Some other error occurred.';
END;
16. How to Handle Backup and Restore in PostgreSQL?
How to Backup in PostgreSQL
Using pg_dump (Good for most cases)
This creates a file with all your database info. pg_dump -U your_username -d your_dbname -f backup.sql
β This saves everything into backup.sql.
Backup All Databases pg_dumpall -U your_username > all_databases.sql
β This backs up everything in your PostgreSQL server.
Copying the Data Folder (Advanced)
You can also back up the entire PostgreSQL data folder, but this is recommended for bigger systems.
How to Restore in PostgreSQL
Restore from pg_dump Backup
If you used pg_dump, run: psql -U your_username -d new_dbname -f backup.sql
β This restores everything into a new database.
Restore All Databases
If you used pg_dumpall, run: psql -U your_username -f all_databases.sql
β This brings back all your databases.
Quick Commands Summary
Action
Command
Backup one DB
pg_dump -U user -d db -f file.sql
Restore one DB
psql -U user -d db -f file.sql
Backup all DBs
pg_dumpall -U user > all.sql
Restore all DBs
psql -U user -f all.sql
17. What Is the pg_hba.conf file?
The pg_hba.conf file controls client authentication in PostgreSQL.
It specifies which clients are allowed to connect, what authentication methods they use, and which databases
they can access. This file is essential for securing your PostgreSQL server.
HBA stands for Host-Based Authentication.
This file tells PostgreSQL:
Which users
From which IP addresses or hosts
Using which authentication method (e.g., password, md5, peer)
Are allowed to access which databases
Typically found in the PostgreSQL data directory, for example: /etc/postgresql/15/main/pg_hba.conf
or /var/lib/pgsql/data/pg_hba.conf
19. How to Optimize Queries in PostgreSQL?
Use EXPLAIN to See Whatβs Happening
Before optimizing, run: EXPLAIN ANALYZE your_query_here;
It shows how PostgreSQL executes your query: indexes used, rows scanned, and more.
Create indexes on columns used in WHERE, JOIN, ORDER BY, or GROUP BY
Example: CREATE INDEX idx_user_email ON users(email);
Avoid over-indexing as it slows down writes (INSERT, UPDATE, DELETE).
Avoid SELECT *; select only necessary columns.
Example: SELECT id, name FROM users;
Fetching only what you need reduces I/O and memory usage.
Filter early using WHERE
Example: SELECT id FROM users WHERE status = 'active';
This helps PostgreSQL scan fewer rows.
Use LIMIT to reduce result size during development or pagination
Example: SELECT * FROM products LIMIT 50;
Do VACUUM and ANALYZE regularly
Keep statistics and table health fresh: VACUUM ANALYZE;
Optimize joins and subqueries
20. What are the Different Isolation Levels in PostgreSQL?
Read Uncommitted: Transactions can see uncommitted changes made by others.
Read Committed: A transaction sees only changes committed before it began.
Repeatable Read: A transaction sees a consistent snapshot, no new changes during its
execution.
Serializable: Ensures complete isolation; transactions appear to run sequentially.
21. What Is The WAL (Write-Ahead Logging) In PostgreSQL, And How Does It Work?
Write-Ahead Logging (WAL) is a method to ensure data integrity.
Before any changes are made to the database, changes are first recorded in a log (the WAL).
This log helps recover the database to a consistent state in case of a crash.
WAL works by writing changes to the log file before applying them to the database, ensuring safety even
during failures.
22. How to Perform Full-Text Search in PostgreSQL?
Full-text search allows efficient natural-language text searching, beyond simple pattern matching.
Instead of exact matches, it finds words based on lexical similarity, relevance, and ranking.
PostgreSQL provides two main data types for this:
tsvector: Represents a document optimized for text search.
tsquery: Represents a text query.
23. Extensions in PostgreSQL
PostgreSQL extensions are packages that enhance the database by adding new capabilities such as:
Data types
Functions
Operators
Indexing methods
Procedural languages
Custom utilities
Extensions are managed using the CREATE EXTENSION command and help keep PostgreSQL modular and
lightweight.
Full-text search: Built-in support for natural language text search
Advanced data types: Includes arrays, hstore, json,
jsonb, and more
Extensibility: Easily extend with user-defined types, operators, functions, and
extensions
MVCC: Multi-Version Concurrency Control for high-performance transaction handling
ACID Properties in PostgreSQL
PostgreSQL ensures data integrity and reliability using the ACID properties of
transactions:
A β Atomicity:
Ensures that all operations within a transaction are completed successfully. If one part fails, the
entire transaction is rolled back. No partial changes are saved.
C β Consistency:
Guarantees that a transaction brings the database from one valid state to another. All data must meet
the database rules (constraints, types, relationships) after the transaction.
I β Isolation:
Ensures that concurrent transactions do not interfere with each other. Each transaction is executed as
if it were the only one running. PostgreSQL uses MVCC to achieve this.
D β Durability:
Once a transaction is committed, it remains so even in case of a system crash. PostgreSQL uses
Write-Ahead Logging (WAL) to make sure data is safely stored.
These properties make PostgreSQL a reliable choice for critical applications where data accuracy and
consistency are essential.