Now we’ll go full-on query performance pro mode using EXPLAIN ANALYZE and real plans. We’ll learn how PostgreSQL makes decisions, how to catch slow queries, and how your indexes make them 10x faster.
๐ Part 1: What is EXPLAIN ANALYZE?
EXPLAIN shows how PostgreSQL plans to execute your query.
ANALYZE runs the query and adds actual time, rows, loops, etc.
Syntax:
EXPLAIN ANALYZE
SELECT * FROM users WHERE username = 'bob';
โ๏ธ Example 1: Without Index
SELECT * FROM users WHERE username = 'bob';
If username has no index, plan shows:
Seq Scan on users
Filter: (username = 'bob')
Rows Removed by Filter: 9999
โ PostgreSQL scans all rows = Sequential Scan = slow!
โ Add Index:
CREATE INDEX idx_users_username ON users (username);
Now rerun:
EXPLAIN ANALYZE SELECT * FROM users WHERE username = 'bob';
You’ll see:
Index Scan using idx_users_username on users
Index Cond: (username = 'bob')
โ PostgreSQL uses B-tree index ๐ Massive speed-up!
๐ฅ Want even faster?
SELECT username FROM users WHERE username = 'bob';
If PostgreSQL shows:
Index Only Scan using idx_users_username on users
Index Cond: (username = 'bob')
๐ Index Only Scan! = covering index success! No heap fetch = lightning-fast.
โ ๏ธ Note: Index-only scan only works if:
Index covers all selected columns
Table is vacuumed (PostgreSQL uses visibility map)
If you still get Seq scan output like:
test_db=# EXPLAIN ANALYSE SELECT * FROM users where username = 'aman_chetri';
QUERY PLAN
-------------------------------------------------------------------------------------------------
Seq Scan on users (cost=0.00..1.11 rows=1 width=838) (actual time=0.031..0.034 rows=1 loops=1)
Filter: ((username)::text = 'aman_chetri'::text)
Rows Removed by Filter: 2
Planning Time: 0.242 ms
Execution Time: 0.077 ms
(5 rows)
even after adding an index, because PostgreSQL is saying:
๐ค “The table is so small (cost = 1.11), scanning the whole thing is cheaper than using the index.”
Also: Your query uses only SELECT username, which could be eligible for Index Only Scan, but heap fetch might still be needed due to visibility map.
๐ง Step-by-step Fix:
โ 1. Add Data for Bigger Table
If the table is small (few rows), PostgreSQL will prefer Seq Scan no matter what.
Try adding ~10,000 rows:
INSERT INTO users (username, email, phone_number)
SELECT 'user_' || i, 'user_' || i || '@mail.com', '1234567890'
FROM generate_series(1, 10000) i;
Then VACUUM ANALYZE users; again and retry EXPLAIN.
โ 2. Confirm Index Exists
First, check your index exists and is recognized:
\d users
You should see something like:
Indexes:
"idx_users_username" btree (username)
If not, add:
CREATE INDEX idx_users_username ON users(username);
โ 3. Run ANALYZE (Update Stats)
ANALYZE users;
This updates statistics โ PostgreSQL might not be using the index if it thinks only one row matches or the table is tiny.
โ 4. Vacuum for Index-Only Scan
Index-only scans require the visibility map to be set.
Run:
VACUUM ANALYZE users;
This marks pages in the table as “all-visible,” enabling PostgreSQL to avoid reading the heap.
โ 5. Force PostgreSQL to Consider Index
You can turn off sequential scan temporarily (for testing):
SET enable_seqscan = OFF;
EXPLAIN SELECT username FROM users WHERE username = 'bob';
You should now see:
Index Scan using idx_users_username on users ...
โ ๏ธ Use this only for testing/debugging โ not in production.
๐ก Extra Tip (optional): Use EXPLAIN (ANALYZE, BUFFERS)
EXPLAIN (ANALYZE, BUFFERS)
SELECT username FROM users WHERE username = 'bob';
This will show:
Whether heap was accessed
Buffer hits
Actual rows
๐ Summary
Step
Command
Check Index
\d users
Analyze table
ANALYZE users;
Vacuum for visibility
VACUUM ANALYZE users;
Disable seq scan for test
SET enable_seqscan = OFF;
Add more rows (optional)
INSERT INTO ...
๐จ How to catch bad index usage?
Always look for:
“Seq Scan” instead of “Index Scan” โ missing index
“Heap Fetch” โ not a covering index
“Rows Removed by Filter” โ inefficient filtering
“Loops: 1000+” โ possible N+1 issue
Common Pattern Optimizations
Pattern
Fix
WHERE column = ?
B-tree index on column
WHERE column LIKE 'prefix%'
B-tree works (with text_ops)
SELECT col1 WHERE col2 = ?
Covering index: (col2, col1) or (col2) INCLUDE (col1)
WHERE col BETWEEN ?
Composite index with range second: (status, created_at)
WHERE col IN (?, ?, ?)
Index still helps
ORDER BY col LIMIT 10
Index on col helps sort fast
โก Tip: Use pg_stat_statements to Find Slow Queries
Enable it in postgresql.conf:
shared_preload_libraries = 'pg_stat_statements'
Then run:
SELECT query, total_exec_time, calls
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5;
๐ฏ Find your worst queries & optimize them with new indexes!
๐งช Try It Yourself
Want a little lab setup to practice?
CREATE TABLE users (
user_id serial PRIMARY KEY,
username VARCHAR(220),
email VARCHAR(150),
phone_number VARCHAR(20)
);
-- Insert 100K fake rows
INSERT INTO users (username, email, phone_number)
SELECT
'user_' || i,
'user_' || i || '@example.com',
'999-000-' || i
FROM generate_series(1, 100000) i;
Then test:
EXPLAIN ANALYZE SELECT * FROM users WHERE username = 'user_5000';
Add INDEX ON username
Re-run, compare speed!
๐ฏ Extra Pro Tools for Query Performance
EXPLAIN ANALYZE โ Always first tool
pg_stat_statements โ Find slow queries in real apps
auto_explain โ Log slow plans automatically
pgBadger or pgHero โ Visual query monitoring
๐ฅ Now We Know:
โ How to read query plans โ When you’re doing full scans vs index scans โ How to achieve index-only scans โ How to catch bad performance early โ How to test and fix in real world
Let’s look into some of the features of sql data indexing. This will be super helpful while developing our Rails 8 Application.
๐ Part 1: What is a Covering Index?
Normally when you query:
SELECT * FROM users WHERE username = 'bob';
Database searches username index (secondary).
Finds a pointer (TID or PK).
Then fetches full row from table (heap or clustered B-tree).
Problem:
Heap fetch = extra disk read.
Clustered B-tree fetch = extra traversal.
๐ Covering Index idea:
โ If the index already contains all the columns you need, โ Then the database does not need to fetch the full row!
It can answer the query purely by scanning the index! โก
Boom โ one disk read, no extra hop!
โ๏ธ Example in PostgreSQL:
Suppose your query is:
SELECT username FROM users WHERE username = 'bob';
You only need username.
But by default, PostgreSQL indexes only store the index column (here, username) + TID.
โ So in this case โ already covering!
No heap fetch needed!
โ๏ธ Example in MySQL InnoDB:
Suppose your query is:
SELECT username FROM users WHERE username = 'bob';
Secondary index (username) contains:
username (indexed column)
user_id (because secondary indexes in InnoDB always store PK)
โฆ๏ธ So again, already covering! No need to jump to the clustered index!
๐ฏ Key point:
If your query only asks for columns already inside the index, then only the index is touched โ no second lookup โ super fast!
๐ Part 2: Real SQL Examples
โจ PostgreSQL
Create a covering index for common query:
CREATE INDEX idx_users_username_email ON users (username, email);
Now if you run:
SELECT email FROM users WHERE username = 'bob';
Postgres can:
Search index on username
Already have email in index
โ No heap fetch!
(And Postgres is smart: it checks index-only scan automatically.)
โจ MySQL InnoDB
Create a covering index:
CREATE INDEX idx_users_username_email ON users (username, email);
โ Now query:
SELECT email FROM users WHERE username = 'bob';
Same behavior:
Only secondary index read.
No need to touch primary clustered B-tree.
๐ Part 3: Tips to design smart Covering Indexes
โ If your query uses WHERE on col1 and SELECTcol2, โ Best to create index: (col1, col2).
โ Keep indexes small โ don’t add 10 columns unless needed. โ Avoid huge TEXT or BLOB columns in covering indexes โ they make indexes heavy.
โ Composite indexes are powerful:
CREATE INDEX idx_users_username_email ON users (username, email);
โ Can be used for:
WHERE username = ?
WHERE username = ? AND email = ?
etc.
โ Monitor index usage:
PostgreSQL: EXPLAIN ANALYZE
MySQL: EXPLAIN
โ Always check if Index Only Scan or Using Index appears in EXPLAIN plan!
๐ Quick Summary Table
Database
Normal Query
With Covering Index
PostgreSQL
B-tree โ Heap fetch (unless TID optimization)
B-tree scan only
MySQL InnoDB
Secondary B-tree โ Primary B-tree
Secondary B-tree only
Result
2 steps
1 step
Speed
Slower
Faster
๐ Great! โ Now We Know:
๐ง How heap fetch works! ๐ง How block lookup is O(1)! ๐ง How covering indexes skip heap fetch! ๐ง How to create super fast indexes for PostgreSQL and MySQL!
๐ฆพ Advanced Indexing Tricks (Real Production Tips)
Now it’s time to look into super heavy functionalities that Postgres supports for making our sql data search/fetch super fast and efficient.
1. ๐ฏ Partial Indexes (PostgreSQL ONLY)
โ Instead of indexing the whole table, โ You can index only the rows you care about!
Example:
Suppose 95% of users have status = 'inactive', but you only search active users:
SELECT * FROM users WHERE status = 'active' AND email = 'bob@example.com';
๐ Instead of indexing the whole table:
CREATE INDEX idx_active_users_email ON users (email) WHERE status = 'active';
โฆ๏ธ PostgreSQL will only store rows with status = 'active' in this index!
Advantages:
Smaller index = Faster scans
Less space on disk
Faster index maintenance (less updates/inserts)
Important:
MySQL (InnoDB) does NOT support partial indexes ๐ โ only PostgreSQL has this superpower.
2. ๐ฏ INCLUDE Indexes (PostgreSQL 11+)
โ Normally, a composite index uses all columns for sorting/searching. โ With INCLUDE, extra columns are just stored in index, not used for ordering.
Example:
CREATE INDEX idx_username_include_email ON users (username) INCLUDE (email);
Meaning:
username is indexed and ordered.
email is only stored alongside.
Now query:
SELECT email FROM users WHERE username = 'bob';
โ Index-only scan โ no heap fetch.
Advantages:
Smaller & faster than normal composite indexes.
Helps to create very efficient covering indexes.
Important:
MySQL 8.0 added something similar with INVISIBLE columns but it’s still different.
3. ๐ฏ Composite Index Optimization
โ Always order columns inside index smartly based on query pattern.
Golden Rules:
โ๏ธ Equality columns first (WHERE col = ?) โ๏ธ Range columns second (WHERE col BETWEEN ?) โ๏ธ SELECT columns last (for covering)
Example:
If query is:
SELECT email FROM users WHERE status = 'active' AND created_at > '2024-01-01';
Best index:
CREATE INDEX idx_users_status_created_at ON users (status, created_at) INCLUDE (email);
โฆ๏ธ status first (equality match) โฆ๏ธ created_at second (range) โฆ๏ธ email included (covering)
Bad Index: (wrong order)
CREATE INDEX idx_created_at_status ON users (created_at, status);
โ Will not be efficient!
4. ๐ฏ BRIN Indexes (PostgreSQL ONLY, super special!)
โ When your table is very huge (millions/billions of rows), โ And rows are naturally ordered (like timestamp, id increasing), โ You can create a BRIN (Block Range Index).
Example:
CREATE INDEX idx_users_created_at_brin ON users USING BRIN (created_at);
โฆ๏ธ BRIN stores summaries of large ranges of pages (e.g., min/max timestamp per 128 pages).
โฆ๏ธ Ultra small index size.
โฆ๏ธ Very fast for large range queries like:
SELECT * FROM users WHERE created_at BETWEEN '2024-01-01' AND '2024-04-01';
Important:
BRIN โ B-tree
BRIN is approximate, B-tree is precise.
Only useful if data is naturally correlated with physical storage order.
MySQL?
MySQL does not have BRIN natively. PostgreSQL has a big advantage here.
5. ๐ฏ Hash Indexes (special case)
โ If your query is always exact equality (not range), โ You can use hash indexes.
Example:
CREATE INDEX idx_users_username_hash ON users USING HASH (username);
Useful for:
Simple WHERE username = 'bob'
Never ranges (BETWEEN, LIKE, etc.)
โ ๏ธ Warning:
Hash indexes used to be “lossy” before Postgres 10.
Now they are safe, but usually B-tree is still better unless you have very heavy point lookups.
๐ PRO-TIP: Which Index Type to Use?
Use case
Index type
Search small ranges or equality
B-tree
Search on huge tables with natural order (timestamps, IDs)
BRIN
Only exact match, super heavy lookup
Hash
Search only small part of table (active users, special conditions)
Partial index
Need to skip heap fetch
INCLUDE / Covering Index
๐บ๏ธ Quick Visual Mindmap:
Your Query
โ
โโโ Need Equality + Range? โ B-tree
โ
โโโ Need Huge Time Range Query? โ BRIN
โ
โโโ Exact equality only? โ Hash
โ
โโโ Want Smaller Index (filtered)? โ Partial Index
โ
โโโ Want to avoid Heap Fetch? โ INCLUDE columns (Postgres) or Covering Index
๐ Now we Know:
๐ง Partial Indexes ๐ง INCLUDE Indexes ๐ง Composite Index order tricks ๐ง BRIN Indexes ๐ง Hash Indexes ๐ง How to choose best Index
MySQL InnoDB: Directly find the row inside the PK B-tree (no extra lookup).
โ MySQL is a little faster here because it needs only 1 step!
2. SELECT username FROM users WHERE user_id = 102; (Only 1 Column)
PostgreSQL: Might do an Index Only Scan if all needed data is in the index (very fast).
MySQL: Clustered index contains all columns already, no special optimization needed.
โ Both can be very fast, but PostgreSQL shines if the index is “covering” (i.e., contains all needed columns). Because index table has less size than clustered index of mysql.
3. SELECT * FROM users WHERE username = 'Bob'; (Secondary Index Search)
PostgreSQL: Secondary index on username โ row pointer โ fetch table row.
MySQL: Secondary index on username โ get primary key โ clustered index lookup โ fetch data.
โ Both are 2 steps, but MySQL needs 2 different B-trees: secondary โ primary clustered.
Consider the below situation:
SELECT username FROM users WHERE user_id = 102;
user_id is the Primary Key.
You only want username, not full row.
Now:
๐ต PostgreSQL Behavior
๐ In PostgreSQL, by default:
It uses the primary key btree to find the row pointer.
Then fetches the full row from the table (heap fetch).
๐ But PostgreSQL has an optimization called Index-Only Scan.
If all requested columns are already present in the index,
And if the table visibility map says the row is still valid (no deleted/updated row needing visibility check),
Then Postgres does not fetch the heap.
๐ So in this case:
If the primary key index also stores username internally (or if an extra index is created covering username), Postgres can satisfy the query just from the index.
โ Result: No table lookup needed โ Very fast (almost as fast as InnoDB clustered lookup).
๐ข Postgres primary key indexes usually don’t store extra columns, unless you specifically create an index that includes them (INCLUDE (username) syntax in modern Postgres 11+).
๐ MySQL InnoDB Behavior
In InnoDB: Since the primary key B-tree already holds all columns (user_id, username, email), It directly finds the row from the clustered index.
So when you query by PK, even if you only need one column, it has everything inside the same page/block.
โ One fast lookup.
๐ฅ Why sometimes Postgres can still be faster?
If PostgreSQL uses Index-Only Scan, and the page is already cached, and no extra visibility check is needed, Then Postgres may avoid touching the table at all and only scan the tiny index pages.
In this case, for very narrow queries (e.g., only 1 small field), Postgres can outperform even MySQL clustered fetch.
๐ก Because fetching from a small index page (~8KB) is faster than reading bigger table pages.
๐ฏ Conclusion:
โ MySQL clustered index is always fast for PK lookups. โ PostgreSQL can be even faster for small/narrow queries if Index-Only Scan is triggered.
๐ Quick Tip:
In PostgreSQL, you can force an index to include extra columns by using: CREATE INDEX idx_user_id_username ON users(user_id) INCLUDE (username); Then index-only scans become more common and predictable! ๐
Isn’t PostgreSQL also doing 2 B-tree scans? One for secondary index and one for table (row_id)?
When you query with a secondary index, like:
SELECT * FROM users WHERE username = 'Bob';
In MySQL InnoDB, I said:
Find in secondary index (username โ user_id)
Then go to primary clustered index (user_id โ full row)
Let’s look at PostgreSQL first:
โฆ๏ธ Step 1: Search Secondary Index B-tree on username.
It finds the matching TID (tuple ID) or row pointer.
TID is a pair (block_number, row_offset).
Not a B-tree! Just a physical pointer.
โฆ๏ธ Step 2: Use the TID to directly jump into the heap (the table).
The heap (table) is not a B-tree โ itโs just a collection of unordered pages (blocks of rows).
PostgreSQL goes directly to the block and offset โ like jumping straight into a file.
๐ Important:
Secondary index โ TID โ heap fetch.
No second B-tree traversal for the table!
๐ Meanwhile in MySQL InnoDB:
โฆ๏ธ Step 1: Search Secondary Index B-tree on username.
It finds the Primary Key value (user_id).
โฆ๏ธ Step 2: Now, search the Primary Key Clustered B-tree to find the full row.
Need another B-tree traversal based on user_id.
๐ Important:
Secondary index โ Primary Key B-tree โ data fetch.
Two full B-tree traversals!
Real-world Summary:
โฆ๏ธ PostgreSQL
Secondary index gives a direct shortcut to the heap.
One B-tree scan (secondary) โ Direct heap fetch.
โฆ๏ธ MySQL
Secondary index gives PK.
Then another B-tree scan (primary clustered) to find full row.
โ PostgreSQL does not scan a second B-tree when fetching from the table โ just a direct page lookup using TID.
โ MySQL does scan a second B-tree (primary clustered index) when fetching full row after secondary lookup.
Is heap fetch a searching technique? Why is it faster than B-tree?
๐ Let’s start from the basics:
When PostgreSQL finds a match in a secondary index, what it gets is a TID.
โฆ๏ธ A TID (Tuple ID) is a physical address made of:
Block Number (page number)
Offset Number (row slot inside the page)
Example:
TID = (block_number = 1583, offset = 7)
๐ต How PostgreSQL uses TID?
It directly calculates the location of the block (disk page) using block_number.
It reads that block (if not already in memory).
Inside that block, it finds the row at offset 7.
โฆ๏ธ No search, no btree, no extra traversal โ just:
Find the page (via simple number addressing)
Find the row slot
๐ Visual Example
Secondary index (username โ TID):
username
TID
Alice
(1583, 7)
Bob
(1592, 3)
Carol
(1601, 12)
โฆ๏ธ When you search for “Bob”:
Find (1592, 3) from secondary index B-tree.
Jump directly to Block 1592, Offset 3.
Done โ !
Answer:
Heap fetch is NOT a search.
It’s a direct address lookup (fixed number).
Heap = unordered collection of pages.
Pages = fixed-size blocks (usually 8 KB each).
TID gives an exact GPS location inside heap โ no searching required.
That’s why heap fetch is faster than another B-tree search:
No binary search, no B-tree traversal needed.
Only a simple disk/memory read + row offset jump.
๐ฟ B-tree vs ๐ Heap Fetch
Action
B-tree
Heap Fetch
What it does
Binary search inside sorted tree nodes
Direct jump to block and slot
Steps needed
Traverse nodes (root โ internal โ leaf)
Directly read page and slot
Time complexity
O(log n)
O(1)
Speed
Slower (needs comparisons)
Very fast (direct)
๐ฏ Final and short answer:
โฆ๏ธ In PostgreSQL, after finding the TID in the secondary index, the heap fetch is a direct, constant-time (O(1)) access โ no B-tree needed! โฆ๏ธ This is faster than scanning another B-tree like in MySQL InnoDB.
๐งฉ Our exact question:
When we say:
Jump directly to Block 1592, Offset 3.
We are thinking:
There are thousands of blocks.
How can we directly jump to block 1592?
Shouldn’t that be O(n) (linear time)?
Shouldn’t there be some traversal?
๐ต Here’s the real truth:
No traversal needed.
No O(n) work.
Accessing Block 1592 is O(1) โ constant time.
๐ Why?
Because of how files, pages, and memory work inside a database.
When PostgreSQL stores a table (the “heap”), it saves it in a file on disk. The file is just a long array of fixed-size pages.
Each page = 8KB (default in Postgres).
Each block = 1 page = fixed 8KB chunk.
Block 0 is the first 8KB.
Block 1 is next 8KB.
Block 2 is next 8KB.
…
Block 1592 = (1592 ร 8 KB) offset from the beginning.
โ So block 1592 is simply located at 1592 ร 8192 bytes offset from the start of the file.
โ Operating systems (and PostgreSQL’s Buffer Manager) know exactly how to seek to that byte position without reading everything before it.
Letโs walk through a real-world example using a schema we are already working on: a shopping app that sells clothing for women, men, kids, and infants.
Weโll look at how candidate keys apply to real tables like Users, Products, Orders, etc.
Here, a combination of order_id and product_id uniquely identifies a row โ i.e., what product was ordered in which order โ making it a composite candidate key, and weโve selected it as the primary key.
๐ Summary of Candidate Keys by Table
Table
Candidate Keys
Primary Key Used
Users
user_id, email, username
user_id
Products
product_id, sku
product_id
Orders
order_id, order_number
order_id
OrderItems
(order_id, product_id)
(order_id, product_id)
Let’s explore how to implement candidate keys in both SQL and Rails (Active Record). Since we are working on a shopping app in Rails 8, I’ll show how to enforce uniqueness and data integrity in both layers:
๐น 1. Candidate Keys in SQL (PostgreSQL Example)
Letโs take the Users table with multiple candidate keys (email, username, and user_id).
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL UNIQUE,
phone_number VARCHAR(20)
);
user_id: chosen as the primary key
email and username: candidate keys, enforced via UNIQUE constraints
If youโve already built a Rails 8 app using the default SQLite setup and now want to switch to PostgreSQL, hereโs a clean step-by-step guide to make the transition smooth:
1.๐ง Setup PostgreSQL in macOS
๐ท Step 1: Install PostgreSQL via Homebrew
Run the following:
brew install postgresql
This created a default database cluster for me, check the output. So you can skip the Step 3.
==> Summary
๐บ /opt/homebrew/Cellar/postgresql@14/14.17_1: 3,330 files, 45.9MB
==> Running `brew cleanup postgresql@14`...
==> postgresql@14
This formula has created a default database cluster with:
initdb --locale=C -E UTF-8 /opt/homebrew/var/postgresql@14
To start postgresql@14 now and restart at login:
brew services start postgresql@14
Or, if you don't want/need a background service you can just run:
/opt/homebrew/opt/postgresql@14/bin/postgres -D /opt/homebrew/var/postgresql@14
Sometimes Homebrew does this automatically. If not:
initdb /opt/homebrew/var/postgresql@<version>
Or a more general version:
initdb /usr/local/var/postgres
Key functions of initdb: Creates a new database cluster, Initializes the database cluster’s default locale and character set encoding, Runs a vacuum command.
In essence, initdb prepares the environment for a PostgreSQL database to be used and provides a foundation for creating and managing databases within that cluster
๐ท Step 4: Create a User and Database
PostgreSQL uses a role-based access control. Create a user with superuser privileges:
# createuser creates a new Postgres user
createuser -s postgres
createuser is a shell script wrapper around the SQL command CREATE USER via the Postgres interactive terminal psql. Thus, there is nothing special about creating users via this or other methods
Then switch to psql:
psql postgres
You can also create a database:
createdb <db_name>
๐ท Step 5: Connect and Use psql
psql -d <db_name>
Inside the psql shell, try:
\l -- list databases
\dt -- list tables
\q -- quit
Then go to http://localhost:3000 and confirm everything works.
7. Check psql manually (Optional)
psql -d your_app_name_development
Then run:
\dt -- view tables
\q -- quit
8. Update .gitignore
Note: If not already added /storage/*
Make sure SQLite DBs are not accidentally committed:
/storage/*.sqlite3
/storage/*.sqlite3-journal
After moving into PostgreSQL
I was getting an issue with postgres column, where I have the following data in the migration:
# migration
t.decimal :rating, precision: 1, scale: 1
# log
ActiveRecord::RangeError (PG::NumericValueOutOfRange: ERROR: numeric field overflow
12:44:36 web.1 | DETAIL: A field with precision 1, scale 1 must round to an absolute value less than 1.
12:44:36 web.1 | )
Value passed is: 4.3. I was not getting this issue in SqLite DB.
What does precision: 1, scale: 1 mean?
precision: Total number of digits (both left and right of the decimal).
scale: Number of digits after the decimal point
If you want to store ratings like 4.3, 4.5, etc., a good setup is:
t.decimal :rating, precision: 2, scale: 1
# revert and migrate for products table
โ rails db:migrate:down VERSION=2025031XXXXX -t
โ rails db:migrate:up VERSION=2025031XXXXXX -t
Then go to http://localhost:3000 and confirm everything works.
For a Ruby on Rails 8 application, the choice of database depends on your specific needs, but hereโs a breakdown of the best options and when to use each:
PostgreSQL (Highly Recommended)
Best overall choice for most Rails apps.
Why:
First-class support in Rails.
Advanced features like full-text search, JSONB support, CTEs, window functions.
Strong consistency and reliability.
Scales well vertically and horizontally (with tools like Citus).
Used by: GitHub, Discourse, Basecamp, Shopify.
Use if:
Youโre building a standard Rails web app or API.
You need advanced query features or are handling complex data types (e.g., JSON).
SQLite (For development/testing only)
Lightweight, file-based.
Fast and easy to set up.
But not recommended for production.
Use if:
Youโre building a quick prototype or local dev/testing app.
NOT for multi-user production environments.
MySQL / MariaDB
Also supported by Rails.
Can work fine for simpler applications.
Lacks some advanced features (like robust JSON support or full Postgres-style indexing).
Not the default in many modern Rails setups.
Use if:
Your team already has MySQL infrastructure or legacy systems.
You need horizontal scaling with Galera Cluster or similar setups.
Others (NoSQL like MongoDB, Redis, etc.)
Use Redis for caching and background job data (not as primary DB).
Use MongoDB or other NoSQL only if your data model really demands it (e.g., unstructured documents, event sourcing).
Recommendation Summary:
Use Case
Recommended DB
Production web/API app
PostgreSQL
Dev/prototyping/local testing
SQLite
Legacy systems/MySQL infrastructure
MySQL/MariaDB
Background jobs/caching
Redis
Special needs (e.g., documents)
MongoDB (with caution)
If you’re starting fresh or building something scalable and modern with Rails 8, go with PostgreSQL.
Letโs break that down:
๐ฌ What does “robust JSON support” mean?
PostgreSQL supports a special column type: json and jsonb, which lets you store structured JSON data directly in your database โ like hashes or objects.
Why it matters:
You can store dynamic data without needing to change your schema.
You can query inside the JSON using SQL (->, ->>, @>, etc.).
You can index parts of the JSON โ for speed.
๐ง Example:
You have a products table with a specs column that holds tech specs in JSON:
SELECT * FROM products WHERE specs->>'color' = 'black';
Or check if the JSON contains a value:
SELECT * FROM products WHERE specs @> '{"brand": "Libas"}';
You can even indexspecs->>'color' to make these queries fast.
๐ฌ What does “full Postgres-style indexing” mean?
PostgreSQL supports a wide variety of powerful indexing options, which improve query performance and flexibility.
โ๏ธ Types of Indexes PostgreSQL supports:
Index Type
Use Case
B-Tree
Default; used for most equality and range searches
GIN (Generalized Inverted Index)
Fast indexing for JSON, arrays, full-text search
Partial Indexes
Index only part of the data (e.g., WHERE active = true)
Expression Indexes
Index a function or expression (e.g., LOWER(email))
Covering Indexes (INCLUDE)
Fetch data directly from the index, avoiding table reads
B-Tree Indexes: B-tree indexes are more suitable for single-value columns.
When to Use GIN Indexes: When you frequently search for specific elements within arrays, JSON documents, or other composite data types.
Example for GIN Indexes: Imagine you have a table with a JSONB column containing document metadata. A GIN index on this column would allow you to quickly find all documents that have a specific author or belong to a particular category.
Why does this matter for our shopping app?
We can store and filter products with dynamic specs (e.g., kurtas, shorts, pants) without new columns.
Full-text search on product names/descriptions.
Fast filters: color = 'red' AND brand = 'Libas' even if those are stored in JSON.
Index custom expressions like LOWER(email) for case-insensitive login.
๐ฌ What are Common Table Expressions (CTEs)?
CTEs are temporary result sets you can reference within a SQL query โ like defining a mini subquery that makes complex SQL easier to read and write.
WITH recent_orders AS (
SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'
)
SELECT * FROM recent_orders WHERE total > 100;
Breaking complex queries into readable parts.
Re-using result sets without repeating subqueries.
In Rails (via with from gems like scenic or with_cte):
Window functions perform calculations across rows related to the current row โ unlike aggregate functions, they donโt group results into one row.
๐ง Example: Rank users by their score within each team:
SELECT
user_id,
team_id,
score,
RANK() OVER (PARTITION BY team_id ORDER BY score DESC) AS rank
FROM users;
Use cases:
Ranking rows (like leaderboards).
Running totals or moving averages.
Calculating differences between rows (e.g. โHow much did this order increase from the last?โ).
๐ค In Rails:
Window functions are available through raw SQL or Arel. Here’s a basic example:
User
.select("user_id, team_id, score, RANK() OVER (PARTITION BY team_id ORDER BY score DESC) AS rank")
CTEs and Window functions are fully supported in PostgreSQL, making it the go-to DB for any Rails 8 app that needs advanced querying.
JSONB Support
JSONB stands for “JSON Binary” and is a binary representation of JSON datathat allows for efficient storage and retrieval of complex data structures.
This can be useful when you have data that doesn’t fit neatly into traditional relational database tables, such as nested or variable-length data structures.
Absolutely โ storing JSON in a relational database (like PostgreSQL) can be super powerful when used wisely. It gives you schema flexibility without abandoning the structure and power of SQL. Here are real-world use cases for using JSON columns in relational databases:
Here are real-world use cases for using JSON columns in relational databases:
๐ง 1. Flexible Metadata / Extra Attributes
Let users store arbitrary attributes that don’t require schema changes every time.
A lightweight version that only declares attribute accessors for keys inside a JSON column. Doesnโt include serialization logic โ so you usually use it with a json/jsonb/text column that already works as a Hash.
๐ Example:
class User < ApplicationRecord
store_accessor :settings, :theme, :notifications
end
This gives you:
user.theme, user.theme=
user.notifications, user.notifications=
๐ค When to Use Each?
Feature
When to Use
store
When you need both serialization and accessors
store_accessor
When your column is already serialized (jsonb, etc.)
If you’re using PostgreSQL with jsonb columns โ it’s more common to just use store_accessor.
Querying JSON Fields
User.where("settings ->> 'theme' = ?", "dark")
Or if you’re using store_accessor:
User.where(theme: "dark")
๐ก But remember: youโll only be able to query these fields efficiently if youโre using jsonb + proper indexes.
๐ฅ Conclusion:
PostgreSQL can store, search, and index inside JSON fields natively.
This lets you keep your schema flexible and your queries fast.
Combined with its advanced indexing, itโs ideal for a modern e-commerce app with dynamic product attributes, filtering, and searching.
To install and set up PostgreSQL on macOS, you have a few options. The most common and cleanest method is using Homebrew. Hereโs a step-by-step guide:
I am working on a project where we face issues in an ancestral path data in PostgreSql DB. Working with hierarchical data in PostgreSQL often involves dealing with ancestry paths stored as delimited strings. This comprehensive guide explores how to extract specific values from ancestry columns and utilize them effectively in join operations, complete with practical examples, troubleshooting tips and how I fixed the issues.
PostgreSQL’s robust string manipulation capabilities make it ideal for handling complex hierarchical data structures. When working with ancestry values stored in text columns, you often need to extract specific parts of the hierarchy for data analysis, reporting, or joining operations.
This article demonstrates how to:
โจ Extract values from ancestry strings using regular expressions
๐ Perform efficient joins on extracted ancestry data
๐ก๏ธ Handle edge cases and avoid common pitfalls
โก Optimize queries for better performance
โ Problem Statement
๐ Scenario
Consider a projects table with an ancestry column containing hierarchical paths like:
Extract the last integer value from the ancestry path
Use this value in a JOIN operation to fetch parent project data
Handle edge cases like NULL values and malformed strings
๐๏ธ Understanding the Data Structure
๐ Table Structure
CREATE TABLE projects (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
ancestry TEXT, -- Stores parent hierarchy as "id1/id2/id3"
created_at TIMESTAMP DEFAULT NOW()
);
-- Sample data
INSERT INTO projects (id, name, ancestry) VALUES
(1, 'Root Project', NULL),
(2, 'Department A', '1'),
(3, 'Team Alpha', '1/2'),
(4, 'Task 1', '1/2/3'),
(5, 'Subtask 1A', '1/2/3/4');
๐งญ Ancestry Path Breakdown
Project ID
Name
Ancestry
Immediate Parent
1
Root Project
NULL
None (root)
2
Department A
1
1
3
Team Alpha
1/2
2
4
Task 1
1/2/3
3
5
Subtask 1A
1/2/3/4
4
๐ง Solution Overview
๐ฏ Core Approach
๐ Pattern Matching: Use regex to identify the last number in the ancestry string
โ๏ธ Value Extraction: Extract the matched value using regexp_replace()
๐ Type Conversion: Cast the extracted string to the appropriate numeric type
๐ Join Operation: Use the converted value in JOIN conditions
๐ Basic Query Structure
SELECT projects.*
FROM projects
LEFT OUTER JOIN projects AS parent_project
ON CAST(
regexp_replace(projects.ancestry, '.*\/(\d+)$', '\1')
AS BIGINT
) = parent_project.id
WHERE projects.ancestry IS NOT NULL;
๐ Regular Expression Deep Dive
๐ฏ Pattern Breakdown: .*\/(\d+)$
Let’s dissect this regex pattern:
.* -- Match any characters (greedy)
\/ -- Match literal forward slash
(\d+) -- Capture group: one or more digits
$ -- End of string anchor
๐ Pattern Matching Examples
Ancestry String
Regex Match
Captured Group
Result
"6/4/5/3"
5/3
3
โ 3
"1/2"
1/2
2
โ 2
"9"
No match
–
โ Original string
"abc/def"
No match
–
โ Original string
๐ง Alternative Regex Patterns
-- For single-level ancestry (no slashes)
regexp_replace(ancestry, '^(\d+)$', '\1')
-- For extracting first parent instead of last
regexp_replace(ancestry, '^(\d+)\/.*', '\1')
-- For handling mixed delimiters (/ or -)
regexp_replace(ancestry, '.*[\/\-](\d+)$', '\1')
๐ป Implementation Examples
๐ง Example 1: Basic Parent Lookup
-- Find each project with its immediate parent information
SELECT
p.id,
p.name AS project_name,
p.ancestry,
parent.id AS parent_id,
parent.name AS parent_name
FROM projects p
LEFT OUTER JOIN projects parent
ON CAST(
regexp_replace(p.ancestry, '.*\/(\d+)$', '\1')
AS BIGINT
) = parent.id
WHERE p.ancestry IS NOT NULL
ORDER BY p.id;
Expected Output:
id | project_name | ancestry | parent_id | parent_name
----+--------------+----------+-----------+-------------
2 | Department A | 1 | 1 | Root Project
3 | Team Alpha | 1/2 | 2 | Department A
4 | Task 1 | 1/2/3 | 3 | Team Alpha
5 | Subtask 1A | 1/2/3/4 | 4 | Task 1
๐ฏ Example 2: Handling Edge Cases
-- Robust query that handles all edge cases
SELECT
p.id,
p.name AS project_name,
p.ancestry,
CASE
WHEN p.ancestry IS NULL THEN 'Root Level'
WHEN p.ancestry !~ '.*\/(\d+)$' THEN 'Single Parent'
ELSE 'Multi-level'
END AS hierarchy_type,
parent.name AS parent_name
FROM projects p
LEFT OUTER JOIN projects parent ON
CASE
-- Handle multi-level ancestry
WHEN p.ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(p.ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
-- Handle single-level ancestry
WHEN p.ancestry ~ '^\d+$' THEN
CAST(p.ancestry AS BIGINT)
ELSE NULL
END = parent.id
ORDER BY p.id;
๐ Example 3: Aggregating Child Counts
-- Count children for each project
WITH parent_child_mapping AS (
SELECT
p.id AS child_id,
CASE
WHEN p.ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(p.ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
WHEN p.ancestry ~ '^\d+$' THEN
CAST(p.ancestry AS BIGINT)
ELSE NULL
END AS parent_id
FROM projects p
WHERE p.ancestry IS NOT NULL
)
SELECT
p.id,
p.name,
COUNT(pcm.child_id) AS direct_children_count
FROM projects p
LEFT JOIN parent_child_mapping pcm ON p.id = pcm.parent_id
GROUP BY p.id, p.name
ORDER BY direct_children_count DESC;
๐จ Common Errors and Solutions
โ Error 1: “invalid input syntax for type bigint”
-- โ Correct: Cast only the extracted value
CAST(
regexp_replace(projects.ancestry, '.*\/(\d+)$', '\1')
AS BIGINT
) = parent.id
โ Error 2: Unexpected Results with Single-Level Ancestry
Problem: Single values like "9" don’t match the pattern .*\/(\d+)$
Solution:
-- โ Handle both multi-level and single-level ancestry
CASE
WHEN ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
WHEN ancestry ~ '^\d+$' THEN
CAST(ancestry AS BIGINT)
ELSE NULL
END
โ Error 3: NULL Ancestry Values Causing Issues
Problem: NULL values can cause unexpected behaviour in joins
Solution:
-- โ Explicitly handle NULL values
WHERE ancestry IS NOT NULL
AND ancestry != ''
๐ก๏ธ Complete Error-Resistant Query
SELECT
p.id,
p.name AS project_name,
p.ancestry,
parent.id AS parent_id,
parent.name AS parent_name
FROM projects p
LEFT OUTER JOIN projects parent ON
CASE
WHEN p.ancestry IS NULL OR p.ancestry = '' THEN NULL
WHEN p.ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(p.ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
WHEN p.ancestry ~ '^\d+$' THEN
CAST(p.ancestry AS BIGINT)
ELSE NULL
END = parent.id
ORDER BY p.id;
โก Performance Considerations
๐ Indexing Strategies
-- Create index on ancestry for faster pattern matching
CREATE INDEX idx_projects_ancestry ON projects (ancestry);
-- Create partial index for non-null ancestry values
CREATE INDEX idx_projects_ancestry_not_null
ON projects (ancestry)
WHERE ancestry IS NOT NULL;
-- Create functional index for extracted parent IDs
CREATE INDEX idx_projects_parent_id ON projects (
CASE
WHEN ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
WHEN ancestry ~ '^\d+$' THEN
CAST(ancestry AS BIGINT)
ELSE NULL
END
) WHERE ancestry IS NOT NULL;
๐ Query Optimization Tips
๐ฏ Use CTEs for Complex Logic
WITH parent_lookup AS (
SELECT
id,
CASE
WHEN ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
WHEN ancestry ~ '^\d+$' THEN
CAST(ancestry AS BIGINT)
END AS parent_id
FROM projects
WHERE ancestry IS NOT NULL
)
SELECT p.*, parent.name AS parent_name
FROM parent_lookup p
JOIN projects parent ON p.parent_id = parent.id;
โก Consider Materialized Views for Frequent Queries
CREATE MATERIALIZED VIEW project_hierarchy AS
SELECT
p.id,
p.name,
p.ancestry,
CASE
WHEN p.ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(p.ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
WHEN p.ancestry ~ '^\d+$' THEN
CAST(p.ancestry AS BIGINT)
END AS parent_id
FROM projects p;
-- Refresh when data changes
REFRESH MATERIALIZED VIEW project_hierarchy;
๐ ๏ธ Advanced Techniques
๐ Extracting Multiple Ancestry Levels
-- Extract all ancestry levels as an array
SELECT
id,
name,
ancestry,
string_to_array(ancestry, '/') AS ancestry_array,
-- Get specific levels
split_part(ancestry, '/', 1) AS level_1,
split_part(ancestry, '/', 2) AS level_2,
split_part(ancestry, '/', -1) AS last_level
FROM projects
WHERE ancestry IS NOT NULL;
๐งฎ Calculating Hierarchy Depth
-- Calculate the depth of each project in the hierarchy
SELECT
id,
name,
ancestry,
CASE
WHEN ancestry IS NULL THEN 0
ELSE array_length(string_to_array(ancestry, '/'), 1)
END AS hierarchy_depth
FROM projects
ORDER BY hierarchy_depth, id;
๐ณ Building Complete Hierarchy Paths
-- Recursive CTE to build full hierarchy paths
WITH RECURSIVE hierarchy_path AS (
-- Base case: root projects
SELECT
id,
name,
ancestry,
name AS full_path,
0 AS level
FROM projects
WHERE ancestry IS NULL
UNION ALL
-- Recursive case: child projects
SELECT
p.id,
p.name,
p.ancestry,
hp.full_path || ' โ ' || p.name AS full_path,
hp.level + 1 AS level
FROM projects p
JOIN hierarchy_path hp ON
CASE
WHEN p.ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(p.ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
WHEN p.ancestry ~ '^\d+$' THEN
CAST(p.ancestry AS BIGINT)
END = hp.id
)
SELECT * FROM hierarchy_path
ORDER BY level, id;
โ Best Practices
๐ฏ Data Validation
โ Validate Ancestry Format on Insert/Update
-- Add constraint to ensure valid ancestry format
ALTER TABLE projects
ADD CONSTRAINT check_ancestry_format
CHECK (
ancestry IS NULL
OR ancestry ~ '^(\d+)(\/\d+)*$'
);
๐ Regular Data Integrity Checks
-- Find orphaned projects (ancestry points to non-existent parent)
SELECT p.id, p.name, p.ancestry
FROM projects p
WHERE p.ancestry IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM projects parent
WHERE parent.id = CASE
WHEN p.ancestry ~ '.*\/(\d+)$' THEN
CAST(regexp_replace(p.ancestry, '.*\/(\d+)$', '\1') AS BIGINT)
WHEN p.ancestry ~ '^\d+$' THEN
CAST(p.ancestry AS BIGINT)
END
);
๐ก๏ธ Error Handling
-- Function to safely extract parent ID
CREATE OR REPLACE FUNCTION extract_parent_id(ancestry_text TEXT)
RETURNS BIGINT AS $$
BEGIN
IF ancestry_text IS NULL OR ancestry_text = '' THEN
RETURN NULL;
END IF;
IF ancestry_text ~ '.*\/(\d+)$' THEN
RETURN CAST(regexp_replace(ancestry_text, '.*\/(\d+)$', '\1') AS BIGINT);
ELSIF ancestry_text ~ '^\d+$' THEN
RETURN CAST(ancestry_text AS BIGINT);
ELSE
RETURN NULL;
END IF;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Usage
SELECT p.*, parent.name AS parent_name
FROM projects p
LEFT JOIN projects parent ON extract_parent_id(p.ancestry) = parent.id;
๐ Monitoring and Maintenance
-- Query to analyze ancestry data quality
SELECT
'Total Projects' AS metric,
COUNT(*) AS count
FROM projects
UNION ALL
SELECT
'Projects with Ancestry' AS metric,
COUNT(*) AS count
FROM projects
WHERE ancestry IS NOT NULL
UNION ALL
SELECT
'Valid Ancestry Format' AS metric,
COUNT(*) AS count
FROM projects
WHERE ancestry ~ '^(\d+)(\/\d+)*$'
UNION ALL
SELECT
'Orphaned Projects' AS metric,
COUNT(*) AS count
FROM projects p
WHERE p.ancestry IS NOT NULL
AND extract_parent_id(p.ancestry) NOT IN (SELECT id FROM projects);
๐ Conclusion
Working with ancestry data in PostgreSQL requires careful handling of string manipulation, type conversion, and edge cases. By following the techniques outlined in this guide, you can:
๐ฏ Key Takeaways
๐ Use robust regex patterns to handle different ancestry formats
๐ก๏ธ Always handle edge cases like NULL values and malformed strings
โก Consider performance implications and use appropriate indexing
โ Implement data validation to maintain ancestry integrity
๐ง Create reusable functions for complex extraction logic
๐ก Final Recommendations
๐ฏ Test thoroughly with various ancestry formats
๐ Monitor query performance and optimize as needed
๐ Consider alternative approaches like ltree for complex hierarchies
๐ Document your ancestry format for team members
๐ ๏ธ Implement proper error handling in production code
The techniques demonstrated here provide a solid foundation for working with hierarchical data in PostgreSQL. Whether you’re building organizational charts, category trees, or project hierarchies, these patterns will help you extract and manipulate ancestry data effectively and reliably! ๐
PostgreSQL, also known as Postgres, is a powerful and feature-rich relational database management system. One of its notable features is the array aggregation function, array_agg, which allows you to aggregate values from multiple rows into a single array. In this blog post, we’ll explore how array_agg works, its applications, and considerations for performance.
How Does array_agg Work?
The array_agg function takes an expression as an argument and returns an array containing the values of that expression for all the rows that match the query. Let’s illustrate this with an example.
Consider a table called employees with columns id, name, and department. Suppose we want to aggregate all the names of employees belonging to the “Sales” department into an array. We can achieve this using the following query:
SELECT array_agg(name) AS sales_employees
FROM employees
WHERE department = 'Sales';
The result of this query will be a single row with a column named sales_employees, which contains an array of all the names of employees in the “Sales” department.
Usage of array_agg with Subqueries
The ability to get an array as the output opens up various possibilities, especially when used in subqueries. You can leverage this feature to aggregate data from related tables or filter results based on complex conditions.
For instance, imagine you have two tables, orders and order_items, where each order can have multiple items. You want to retrieve a list of orders along with an array of item names for each order. The following query achieves this:
SELECT o.order_id, (
SELECT array_agg(oi.item_name)
FROM order_items oi
WHERE oi.order_id = o.order_id
) AS item_names
FROM orders o;
In this example, the subquery within the main query’s select list utilizes array_agg to aggregate item names from the order_items table, specific to each order.
Complex Query Example Using array_agg
To demonstrate a more complex scenario, let’s consider a database that stores books and their authors. We have three tables: books, authors, and book_authors (a join table that associates books with their respective authors).
Suppose we want to retrieve a list of books along with an array of author names for each book by alphabetical order. We can achieve this using a query that involves joins and array_agg:
SELECT b.title, array_agg(a.author_name ORDER BY a.author_name ASC) AS authors
FROM books b
JOIN book_authors ba ON b.book_id = ba.book_id
JOIN authors a ON ba.author_id = a.author_id
GROUP BY b.book_id;
In this query, we join the tables based on their relationships and use array_agg to aggregate author names into an array for each book. The GROUP BY clause ensures that each book’s array of author names is grouped correctly.
Performance Considerations
While array_agg is a powerful function, it’s essential to consider its performance implications, especially when working with large datasets. Aggregating values into arrays can be computationally intensive, and the resulting array can consume significant memory.
If you anticipate working with large result sets or complex queries involving array_agg, it’s worth optimizing your database schema, indexing relevant columns, and analyzing query performance using PostgreSQL’s built-in tools.
Additionally, consider whether array_agg is the most efficient solution for your specific use case. Sometimes, alternative approaches, such as using temporary tables or custom aggregate functions, might offer better performance.
Conclusion
The array_agg function in PostgreSQL provides a powerful mechanism for aggregating values into arrays. It offers flexibility and opens up opportunities for various applications, including subqueries and complex data manipulations. However, when working with large datasets, it’s crucial to be mindful of potential performance implications and explore optimization strategies accordingly.
List of commands to remember using postgres DB managment system.
Login, Create user and password
# login to psql client
psql postgres # OR
psql -U postgres
create database mydb; # create db
create user abhilash with SUPERUSER CREATEDB CREATEROLE encrypted password 'abhilashPass!';
grant all privileges on database mydb to myuser; # add privileges
Connect to DB, List tables and users, functions, views, schema
\l # lists all the databases
\c dbname # connect to db
\dt # show tables
\d table_name # Describe a table
\dn # List available schema
\df # List available functions
\dS [your_table_name] # List triggers
\dv # List available views
\du # lists all user accounts and roles
\du+ # is the extended version which shows even more information.
Show history, save to file, edit using editor, execution time, help
SELECT version(); # version of psql
\g # Execute the previous command
\s # Command history
\s filename # save Command history to a file
\i filename # Execute psql commands from a file
\? # help on psql commands
\h ALTER TABLE # To get help on specific PostgreSQL statement
\timing # Turn on/off query execution time
\e # Edit command in your own editor
\e [function_name] # It is more useful when you edit a function in the editor. Do \df for functions
\o [file_name] # send all next query results to file
\o out.txt
\dt
\o # switch
\dt
Change output, Quit psql
# Switch output options
\a command switches from aligned to non-aligned column output.
\H command formats the output to HTML format.
\q # quit psql
Postgres uses role based access for the unix users. After the installation a default role called ‘postgres’ will be created. You can login to postgres account and start using or creating new roles with Postgres.
Sign in as postgres user
$ sudo -i -u postgres
Access the postgres console by
$ psql
But i cannot enter into the console and I got the following error:
postgres@8930a29k5d05:/home/rails/my_project$ psql
psql: could not connect to server: No such file or directory
Is the server running locally and accepting
connections on Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?
Original Reason: PostgreSQL Server was not running after the installation.
I tried rebooting the system and via init script the server should run automatically. But the server is not running again. I understood that something prevents postgres from running the server. What is it?
root@8930a29k5d05:/home/rails/my_project# /etc/init.d/postgresql start
* Starting PostgreSQL 9.3 database server
[ OK ]
root@8930a29k5d05:/home/rails/my_project# ps aux | grep postgres
postgres 158 0.1 2.0 244928 20752 ? S 06:28 0:00 /usr/lib/postgresql/9.3/bin/postgres -D /var/lib/postgresql/9.3/main -c config_file=/etc/postgresql/9.3/main/postgresql.conf
postgres 160 0.0 0.3 244928 3272 ? Ss 06:28 0:00 postgres: checkpointer process
postgres 161 0.0 0.4 244928 4176 ? Ss 06:28 0:00 postgres: writer process
postgres 162 0.0 0.3 244928 3272 ? Ss 06:28 0:00 postgres: wal writer process
postgres 163 0.0 0.5 245652 6000 ? Ss 06:28 0:00 postgres: autovacuum launcher process
postgres 164 0.0 0.3 100604 3336 ? Ss 06:28 0:00 postgres: stats collector process
root 178 0.0 0.0 8868 884 ? S+ 06:28 0:00 grep --color=auto post
root@8930a29k5d05:/home/rails/my_project#
Now the server starts running. If still not works, then try to reconfigure your locales as mentioned here
$ dpkg-reconfigure locales
It is strange that, after installing such a popular database software, it doesn’t provide any information regarding the failure of its own server. It should give the developers some clue so that they can save their precious time.
The reason of this failure, what I concluded is
1. After installation we have to run the server manually
OR
2. I tried resetting the locales (So if no locales set in the machine may prevented the postgres from starting automatically?)