PostgreSQL: selecting a function
Comments enabled. I *really* need your comment
From Stack Overflow:
Hello,
I want to write a
SELECTstatement as follows:SELECT field_a FROM my_table WHERE field_b IN (my_function(field_c))Is that possible?
Would
my_functionhave to return an array?
It is of course possible in PostgreSQL using a set returning function.
Return type of such a function should be declared as SETOF, so the function will return a rowset of given type.
There are at least two ways to call this function, and in this article I will consider the benefits and drawbacks of each method.
Let's create sample tables:
CREATE TABLE t_address ( id INT NOT NULL PRIMARY KEY, grouper INT NOT NULL, address CIDR NOT NULL ); CREATE TABLE t_ban ( id INT NOT NULL PRIMARY KEY, grouper INT NOT NULL, address CIDR NOT NULL ); CREATE INDEX ix_address_grouper ON t_address (grouper); CREATE INDEX ix_ban_grouper ON t_ban (grouper); CREATE OR REPLACE FUNCTION fn_ban(INT) RETURNS SETOF t_ban AS $$ SELECT * FROM t_ban WHERE grouper = $1 $$ LANGUAGE 'sql'; SELECT setseed(20090514); BEGIN TRANSACTION; INSERT INTO t_address (id, grouper, address) SELECT s, s % 10, '10.0.0.0/32'::cidr + (RANDOM() * x'10000'::integer)::integer FROM generate_series(1, 1000) s; INSERT INTO t_ban (id, grouper, address) SELECT s, s % 10, '10.0.0.0/32'::cidr + (RANDOM() * x'10000'::integer)::integer FROM generate_series(1, 10000) s; COMMIT; ANALYZE t_address; ANALYZE t_ban;
There are two tables: t_address and t_ban.
t_address contains a lits of 1,000 random IP addresses grouped into 10 groups.
t_ban contains a list of 10,000 random banned addresses, also grouped into 10 groups.
fn_ban is a set returning function. Given a group, it returns all banned addresses for this group.
Out goal is to write a query that check all addresses from t_address against the results of the function to see whether they are banned or not.
We can use a traditional IN expession with a subquery:
SELECT * FROM t_address WHERE address IN ( SELECT (fn_ban(grouper)).address )
| id | grouper | address | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 238 | 8 | 10.0.252.94/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 239 | 9 | 10.0.75.83/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 277 | 7 | 10.0.159.64/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 330 | 0 | 10.0.125.79/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 451 | 1 | 10.0.132.173/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 459 | 9 | 10.0.105.176/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 491 | 1 | 10.0.136.40/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 645 | 5 | 10.0.71.243/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 702 | 2 | 10.0.212.83/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 728 | 8 | 10.0.46.171/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 767 | 7 | 10.0.218.97/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 778 | 8 | 10.0.65.71/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 808 | 8 | 10.0.8.88/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 844 | 4 | 10.0.201.183/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 873 | 3 | 10.0.2.48/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 923 | 3 | 10.0.87.233/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 16 rows fetched in 0.0006s (1.3593s) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Seq Scan on t_address (cost=0.00..149.75 rows=500 width=15)
Filter: (subplan)
SubPlan
-> Result (cost=0.00..0.26 rows=1 width=0)
PostgreSQL also offers the ability to use a set returning function in a SELECT clause along with other columns.
In this case, each row of the resultset will be repeated as many times as many rows are returned by the function.
In other words, calling a function inside a SELECT clause acts similar to SQL Server's APPLY operator.
We can call this function inside a SELECT clause and then use the outer subquery to filter the results:
SELECT id, grouper, address FROM ( SELECT a.*, fn_ban(grouper) AS ban FROM t_address a ) q WHERE q.address = (ban).address
| id | grouper | address | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 238 | 8 | 10.0.252.94/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 239 | 9 | 10.0.75.83/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 277 | 7 | 10.0.159.64/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 330 | 0 | 10.0.125.79/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 451 | 1 | 10.0.132.173/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 459 | 9 | 10.0.105.176/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 491 | 1 | 10.0.136.40/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 645 | 5 | 10.0.71.243/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 702 | 2 | 10.0.212.83/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 728 | 8 | 10.0.46.171/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 767 | 7 | 10.0.218.97/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 778 | 8 | 10.0.65.71/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 808 | 8 | 10.0.8.88/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 844 | 4 | 10.0.201.183/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 873 | 3 | 10.0.2.48/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 923 | 3 | 10.0.87.233/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 16 rows fetched in 0.0006s (1.4374s) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Subquery Scan q (cost=0.00..278.50 rows=5 width=72) Filter: ((q.address)::inet = ((q.ban).address)::inet) -> Seq Scan on t_address a (cost=0.00..266.00 rows=1000 width=15)
Now, which one is better?
If we look into the plan of the former query (using IN clause), we will see that PostgreSQL uses a Filter on the results of SubPlan.
This Filter returns TRUE as soon as it finds the first matching result, so it just stops scanning the function as soon as it happens.
And we actually can see that the query performance is slightly better in the first case.
For any given value, the function always returns the same rowset:
SELECT * FROM fn_ban(0)
| id | grouper | address | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10 | 0 | 10.0.227.232/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 20 | 0 | 10.0.212.255/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 30 | 0 | 10.0.33.91/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 9980 | 0 | 10.0.34.80/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 9990 | 0 | 10.0.232.149/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 10000 | 0 | 10.0.158.92/32 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1000 rows fetched in 0.0530s (0.0074s) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Function Scan on fn_ban (cost=0.00..260.00 rows=1000 width=40)
Let's check what will happen if the subquery matches the first or the last value returned by the function.
This query matches the first value (10.0.227.232/32) of those returned by fn_ban for group 0:
SELECT COUNT(*) FROM t_address WHERE grouper = 0 AND '10.0.227.232/32'::cidr IN ( SELECT (fn_ban(grouper)).address )
| count | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 100 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1 row fetched in 0.0001s (0.0223s) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Aggregate (cost=25.77..25.78 rows=1 width=0)
-> Bitmap Heap Scan on t_address (cost=5.01..25.64 rows=50 width=0)
Recheck Cond: (grouper = 0)
Filter: (subplan)
-> Bitmap Index Scan on ix_address_grouper (cost=0.00..5.00 rows=100 width=0)
Index Cond: (grouper = 0)
SubPlan
-> Result (cost=0.00..0.26 rows=1 width=0)
In the second case, when the subquery matches the last record returned by the function, the execution time is much higher:
SELECT COUNT(*) FROM t_address WHERE grouper = 0 AND '10.0.158.92/32'::cidr IN ( SELECT (fn_ban(grouper)).address )
| count | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 100 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1 row fetched in 0.0001s (0.1430s) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Aggregate (cost=25.77..25.78 rows=1 width=0)
-> Bitmap Heap Scan on t_address (cost=5.01..25.64 rows=50 width=0)
Recheck Cond: (grouper = 0)
Filter: (subplan)
-> Bitmap Index Scan on ix_address_grouper (cost=0.00..5.00 rows=100 width=0)
Index Cond: (grouper = 0)
SubPlan
-> Result (cost=0.00..0.26 rows=1 width=0)
The latter query takes 140 ms to complete, while the former one takes only 20 ms (7 times as performant as the latter one).
We can easily see that PostgreSQL stops fetching the records from the function as soon as it find the first matching record.
Both queries, being rewritten using dervied tables, would run for the same time.
Derived tables, however, have two major advantages.
First, they allow seeing and selecting columns that cannot be evaluated with an IN expression syntax.
Say, we need the id of the banned address.
We cannot select it with a subquery (since it can return only one column and it's already used in the IN clause).
But with a dervied query this is easy:
SELECT id, grouper, address, (ban).id AS banid FROM ( SELECT a.*, fn_ban(grouper) AS ban FROM t_address a ) q WHERE q.address = (ban).address
| id | grouper | address | banid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 238 | 8 | 10.0.252.94/32 | 1638 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 239 | 9 | 10.0.75.83/32 | 7529 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 277 | 7 | 10.0.159.64/32 | 9747 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 330 | 0 | 10.0.125.79/32 | 2260 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 451 | 1 | 10.0.132.173/32 | 4751 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 459 | 9 | 10.0.105.176/32 | 7279 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 491 | 1 | 10.0.136.40/32 | 2381 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 645 | 5 | 10.0.71.243/32 | 7105 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 702 | 2 | 10.0.212.83/32 | 5492 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 728 | 8 | 10.0.46.171/32 | 2248 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 767 | 7 | 10.0.218.97/32 | 1757 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 778 | 8 | 10.0.65.71/32 | 9028 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 808 | 8 | 10.0.8.88/32 | 2428 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 844 | 4 | 10.0.201.183/32 | 8124 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 873 | 3 | 10.0.2.48/32 | 233 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 923 | 3 | 10.0.87.233/32 | 9733 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 16 rows fetched in 0.0007s (1.4843s) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Subquery Scan q (cost=0.00..278.50 rows=5 width=72) Filter: ((q.address)::inet = ((q.ban).address)::inet) -> Seq Scan on t_address a (cost=0.00..266.00 rows=1000 width=15)
Now we can see the id or the record in t_ban along with the address.
The second advantage of a derived table is ability to return duplicate records.
If an address has a duplicate entry in t_ban, the derived table will return both occurencies along with their id's, while in IN clause will return only the first.
Summary:
There are two methods for checking records of one table against the results of a set-returning function in PostgreSQL: using IN subquery and using SELECT clause function calling with subsequent filtering.
Using IN subquery is faster, since it stops executing the function as soon as it find the first matching value.
Using the function in SELECT clause is slower by design, since it can return duplicate values.
This method, however, allows selecting other columns along with those we are checking against.
Duplicate values may also be of use, in which case this method will be the only acceptable one.
Subscribe in a reader