Using Clause (Part 2)
Introduction
Besides the clauses mentioned in the previous article, PostgreSQL, as an exceptionally powerful database management system, supports many other specialized clauses as follows.
Navigation and Pagination Group
This is a basic yet crucial group that is very widely used.
ORDER BY: Sorts the returned results in ascending (ASC) or descending (DESC) order.- There is also
ORDER BY col DESC NULLS LASTto pushNULLvalues to the very bottom of the result table instead of the default top when sorting in descending order.
- There is also
LIMIT/OFFSET: Used for pagination.LIMIT Nretrieves a maximum ofNrows.OFFSET Mskips the firstMrows before retrieving.- When you use
OFFSET M, PostgreSQL must still scan and count all the firstMrows to know where to begin, then it retrieves the nextNrows forLIMIT.- The consequence is that if
OFFSETis small (for the first pages), the query still runs very fast. - If
OFFSETis large, such as usingOFFSET 100000, the database must expend resources scanning through those100,000preceding rows without using them, causing the query to become progressively slower as users view the final pages.
- The consequence is that if
FETCH FIRST N ROWS ONLY: This is an ANSI SQL standard clause, which has completely similar functionality toLIMIT Nbut is more standardized, helping code easily convert to other databases like Oracle or SQL Server.- The standard syntax of this clause is fully equivalent to
LIMIT, although it is longer, it reads very much like natural English and you can write this clause flexibly using synonyms:FIRSTorNEXT: You can writeFETCH NEXT 5 ROWS ONLY(often used when accompanied byOFFSET).ROWorROWS: If retrieving 1 row, you can writeFETCH FIRST 1 ROW ONLY.- Omitting the quantity: If you write
FETCH FIRST ROWS ONLY(without a number), Postgres automatically understands that you only want to retrieve exactly the first row.
- Combining pagination with
OFFSET- Just like
LIMIT,FETCH FIRSTcan also be combined withOFFSETto implement the pagination feature. - According to the ANSI SQL standard,
OFFSETwill be written more explicitly asOFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY.
- Just like
FETCH WITH TIES- This is an advanced feature of the ANSI standard clause that standard
LIMITcannot achieve (unless writing additional complex subqueries). - Suppose you want to retrieve the top 3 products with the highest prices, if you use
LIMIT 3as usual, you still get the correct result. - However, if the data has a special form where the first 4 rows all have the same price (or the 3rd and 4th products have equal prices), using the conventional method will only retrieve exactly 3 products, missing the products with the same price.
- In this case, using the
WITH TIESclause will resolve this issue, as it will retrieve the number of rows you requested, plus all rows that have values equal to the last row (tied). - The
WITH TIESclause must be accompanied byORDER BY, otherwise the data in the database will be in an unordered chaotic state and Postgres will not be able to determine the tied rows.
- This is an advanced feature of the ANSI standard clause that standard
- The standard syntax of this clause is fully equivalent to
Concurrency Control
When working with systems that have multiple users accessing simultaneously, you need row-locking clauses to avoid race conditions:
FOR UPDATE: Locks the selected rows. Until the transaction ends, no one else is allowed to modify or delete these rows.FOR SHARE: Locks the rows in "shared" mode. Other transactions can still read the data, but no one is allowed to change it until you finish your work.SKIP LOCKED: AccompaniesFOR UPDATE. If any row of data is currently locked by another process, Postgres will completely skip that row and proceed to read the next free row. This clause is the backbone for building high-performance message queue systems using a database.
Data Manipulation Extensions
ON CONFLICT (...) DO UPDATE/DO NOTHING: The clause that handles the upsert (insert + update) strategy. When you insert a new row into a table, if a conflict occurs duplicating the primary key or a unique constraint, you can command Postgres to automatically convert it into anUPDATEstatement to update the new data, orDO NOTHINGto skip it without reporting an error.RETURNING: Normally when you runINSERT,UPDATEorDELETE, Postgres only returns a message indicating the number of affected rows. If you add theRETURNING col1, col2clause, Postgres will additionally return the data of the rows that were just modified, just like aSELECTstatement. You will save one query back to the table.
Optimization and Data Sampling Group
TABLESAMPLE SYSTEM/BERNOULLI: Used to take a random sample of data from a large table.- For example, using
TABLESAMPLE BERNOULLI (1)will randomly sample1%of the rows in the data table. This method runs much faster than usingORDER BY random() LIMITon a large data set.
How It Works
- When using
ORDER BY random() LIMIT, the Postgres optimizer must perform as follows:- Uses a sequential scan to read block by block, loading all data rows in the table into RAM.
- For each row, the CPU must run a mathematical function to assign that row a random number from 0 to 1.
- Postgres must resort those rows based on the random number just calculated and if it cannot fit within RAM, it will have to write temporary data to the disk to run the external merge sort algorithm.
- Finally, it retrieves the top rows and discards the remaining rows.
- Thus, the issue with this solution is having to sequentially scan all data in the table (incurring I/O costs) and sorting the data (incurring CPU costs).
TABLESAMPLE BERNOULLIwill also use a sequential scan combined with independent probability for each row to distribute the random probability.- Because it does not know the total number of rows, it must read the entire data to calculate the corresponding percentage.
- Since every row has a certain selection rate based on the percentage, when using this clause multiple times you will get different row count results and the gathered result will be a dataset evenly distributed from beginning to end.
- Although it utilizes a sequential scan, it skips the sorting step and loading data into RAM to process the
LIMIT, so it is still much faster thanORDER BY random() LIMIT.
TABLESAMPLE SYSTEMdoes not count the number of rows to calculate the percentage, but calculates it based on the total number of blocks or pages of that table on the disk.- Each data table is stored as a sequence of consecutive data blocks on the disk. Postgres always knows exactly the total number of blocks a table is occupying (this information is continuously updated and stored in the file management system of the operating system and the database).
- Consequently, there will be no need to sequentially scan the entire table or sort the data anymore, making this the method with the fastest response speed.
- When using
TABLESAMPLE SYSTEM (1)to sample1%, the specific operational mechanism is as follows:- Postgres will use a random number generation algorithm to select exactly
1%of the pages. - After that, it only needs to directly access the chosen blocks to retrieve all the internal rows.
- After reading these blocks, it will stop and return the result, completely skipping the remaining blocks.
- Postgres will use a random number generation algorithm to select exactly
- Therefore, using
SYSTEMwill sample based on the percentage of the total blocks, unlikeBERNOULLIwhich samples based on the percentage of the total rows and one page contains varying numbers of rows.- So each time you use this method, the row count result may also differ.
- Because the retrieved rows are consecutive within the same page, the obtained results will usually be more clustered and less randomly distributed than the two methods above.
Detail
Please create the tables as follows
CREATE TABLE categories (
category_id SERIAL PRIMARY KEY,
category_name VARCHAR(100) NOT NULL
);
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
category_id INT REFERENCES categories(category_id),
product_name VARCHAR(255) NOT NULL,
price NUMERIC(12, 2) NOT NULL,
rating NUMERIC(3, 2),
stock_quantity INT NOT NULL DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INT NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(50) DEFAULT 'Pending',
total_amount NUMERIC(12, 2) DEFAULT 0.00
);
CREATE TABLE payment_requests (
idempotency_key UUID PRIMARY KEY,
order_id INT NOT NULL,
amount NUMERIC(12,2) NOT NULL,
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE product_sales_stats (
product_id INT PRIMARY KEY REFERENCES products(product_id),
total_sold INT DEFAULT 0,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- The
payment_requeststable is used to store the list of requests waiting for payment. - The
product_sales_statstable is used to store statistical data on the quantity sold.
ORDER BY & FETCH
-- Query 1
SELECT product_id, product_name, rating, price
FROM products
ORDER BY rating DESC;
-- Query 2
SELECT product_id, product_name, rating, price
FROM products
ORDER BY rating DESC NULLS LAST;
-- Query 3
SELECT product_id, product_name, price
FROM products
ORDER BY price DESC
FETCH FIRST 3 ROWS ONLY;
-- Query 4
SELECT product_id, product_name, price
FROM products
ORDER BY price DESC
OFFSET 3 ROWS
FETCH NEXT 5 ROWS ONLY;
-- Query 5
SELECT product_id, product_name, rating
FROM products
WHERE category_id = 1
ORDER BY rating DESC NULLS LAST
FETCH FIRST 1 ROWS WITH TIES;
- Query 1: Displays a list of products with ratings from high to low, used with a conventional
ORDER BY DESCwhere rows withNULLvalues will be sorted at the very top. - Query 2: Similar to Query 1, but used with
DESC NULLS LASTwhere rows withNULLvalues will be sorted at the very end. - Query 3: Used in pagination, only retrieving the first 3 products, utilizing the
FETCH FIRSTsyntax. - Query 4: Skips the first 3 products, retrieving the next 5 products (OFFSET + FETCH NEXT).
- Query 5: Retrieves the top products with the highest rating, if there are multiple products with equal scores at the final position, it retrieves them all without omission, so even when using
FIRST 1 ROWS, it can retrieve 2 rows with equal scores.
SKIP LOCKED
BEGIN;
SELECT idempotency_key, order_id
FROM payment_requests
WHERE status = 'PROCESSING'
ORDER BY created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED;
- The
payment_requeststable stores requests waiting for payment, normally workers will be used to process each request to avoid hanging the system. - When a worker wants to process a request, it will start a transaction and use
FOR UPDATE SKIP LOCKEDto fetch a row with the statusPROCESSINGto handle. - When the system has multiple workers, they will do the same and automatically skip rows that have already been blocked, even though all have the status
PROCESSING.
ON CONFLICT & RETURNING
-- Statement 1
INSERT INTO product_sales_stats (product_id, total_sold, last_updated)
VALUES (1, 5, CURRENT_TIMESTAMP)
ON CONFLICT (product_id)
DO UPDATE SET
total_sold = product_sales_stats.total_sold + EXCLUDED.total_sold,
last_updated = EXCLUDED.last_updated
RETURNING product_id, total_sold AS new_total_sold;
-- Statement 2
INSERT INTO payment_requests (idempotency_key, order_id, amount, status)
VALUES ('43d35134-7379-4404-a42c-7c31e7b6c15f', 2, 200000, 'PROCESSING')
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING status;
- Statement 1: Used for an
UPSERT(INSERT+UPDATE) to update the sold quantity of a product.- If the product is not yet in the
product_sales_statstable, it performs anINSERT, if it already exists, it aggregates the quantity. - If you execute this statement twice, you will see the quantity increase instead of inserting a new row.
- Combined with
RETURNINGto immediately see the information just inserted without needing an additionalSELECT.
- If the product is not yet in the
- Statement 2: In a distributed system, an
Idempotency Keyis usually utilized uniformly across the system to distinguish between different requests. - Therefore, use
ON CONFLICT DO NOTHINGto avoid inserting a new row if theidempotency_keyis duplicated. - Avoids having to process a single request multiple times.
TABLESAMPLE
-- Query 1
SELECT product_id, product_name, price
FROM products
WHERE is_active = TRUE
ORDER BY random()
LIMIT 3;
-- Query 2
SELECT product_id, product_name, price
FROM products TABLESAMPLE BERNOULLI(10);
-- Query 3
SELECT product_id, product_name, price
FROM products TABLESAMPLE SYSTEM(10);
- Query 1: This is the conventional way to get random data in a table. It can fetch the exact desired number of rows but is only suitable for small tables because it must sequentially scan the entire table to perform
SORTandLIMIT. - Query 2: Randomly samples at a rate of
10%of the rows across the entire table, still using a sequential scan but without needingSORTandLIMITso the performance is better, the number of rows fetched each execution will not be fixed. - Query 3: Randomly samples at a rate of
10%of the blocks or pages of that table, offering the best performance because it does not require a sequential scan or sorting, but the number of rows fetched is not fixed and will usually be higher than the two cases above.
Comments
Post a Comment