I've try implement expression indexing on virtual columns as Bruce Momjian suggest in blog.
It work as expected (but require additional data in table to became index useful). Then I've try use it in view and it break it!
Step by step:
CREATE TABLE customer (id SERIAL, firstname TEXT, lastname TEXT);
-- create virtual column
CREATE FUNCTION fullname(customer) RETURNS text AS $$
SELECT $1.firstname || ' ' || $1.lastname
$$ LANGUAGE SQL;
INSERT INTO customer VALUES (DEFAULT, 'Mark', 'Pennypincher');
-- Insert some data for index became useful
INSERT INTO customer(firstname, lastname)
SELECT 'first_' || n, 'last_' || n FROM generate_series(1, 1000) as n;
CREATE INDEX i_customer_firstname ON customer (firstname);
CREATE INDEX i_customer_bothnames ON customer ((firstname || ' ' || lastname));
CREATE INDEX i_customer_fullname ON customer (fullname(customer));
ANALYZE customer;
EXPLAIN SELECT * FROM customer WHERE customer.fullname = 'Mark Pennypincher';
For now it work as expected, query use index:
Index Scan using i_customer_bothnames on customer (cost=0.28..8.29 rows=1 width=21)
Index Cond: (((firstname || ' '::text) || lastname) = 'Mark Pennypincher'::text)
Now create view:
CREATE OR REPLACE VIEW v_customer AS
SELECT
c.*
,c.fullname
FROM customer c;
We have no aggregations and limits, so according to the documentation thanks to rewrite mechanism and optimizer it should be pulled up in the query. But it is not!
Look:
EXPLAIN SELECT * FROM v_customer WHERE v_customer.fullname = 'Mark Pennypincher';
immediately leads to next query plan:
Subquery Scan on v_customer (cost=0.00..34.53 rows=5 width=53)
Filter: (v_customer.fullname = 'Mark Pennypincher'::text)
-> Seq Scan on customer c (cost=0.00..22.01 rows=1001 width=53)
Why Subquery Scan??
What also interesting next two statements:
EXPLAIN SELECT * FROM v_customer WHERE v_customer.firstname = 'Mark';
EXPLAIN SELECT * FROM v_customer WHERE v_customer.firstname || ' ' || v_customer.lastname = 'Mark Pennypincher';
both use their indexes also.
Off course I have more complicated view in reality with several joins. There just example of problem. And it is the reason why I want replace it by view for simplicity.
You need to make the function immutable, otherwise the planner won't use it:
CREATE FUNCTION fullname(customer) RETURNS text
AS
$$
SELECT $1.firstname || ' ' || $1.lastname
$$
LANGUAGE SQL
immutable; --<< here
This then generates the following plan with your sample data:
Index Scan using i_customer_fullname on stuff.customer (cost=0.28..8.29 rows=1 width=21) (actual time=0.054..0.054 rows=1 loops=1)
Output: id, firstname, lastname
Index Cond: (((customer.firstname || ' '::text) || customer.lastname) = 'Mark Pennypincher'::text)
Buffers: shared hit=1 read=2
I/O Timings: read=0.035
Planning time: 0.571 ms
Execution time: 0.079 ms
If the function is not immutable, Postgres can't rely on the values that are stored in the index to be always the same. stable would have achieved the same goal. But as the function only relies on values passed to it, immutable is the correct choice.