One of the changes coming in PostgreSQL 10 is the ability for the CASE .. WHEN
statement to return multiple rows if the expression contains a set returning function. To demonstrate the feature,
we'll use the classic generate_series function:
Since this article was written, this feature has been removed, because it caused old logic that had sets from doing something different without warning. Refer to this discussion
generate_series with CASE WHEN
SELECT CASE generate_series(1,10) WHEN 1 THEN 'First' ELSE 'Not First' END;
In PostgreSQL 10, this returns:
In PostgreSQL 9.6 and below, you are slapped with an error message:
ERROR: set-valued function called in context that cannot accept a set
Interesting, right, but how is this useful?
Why would I ever do that instead of something like this which will work in all versions?
SELECT CASE i WHEN 1 THEN 'First' ELSE 'Not First' END
FROM generate_series(1,10) AS i;
The answer seems reading from the commit notes,
that it's a side effect of other optimizations and introduction of new ProjectSet executor node.
As a side effect, the previously prohibited case of multiple set returning
arguments to a function, is now allowed. Not because it's particularly
desirable, but because it ends up working and there seems to be no argument
for adding code to prohibit it.
Currently the behavior for COALESCE and CASE containing SRFs has changed,
returning multiple rows from the expression, even when the SRF containing
"arm" of the expression is not evaluated. That's because the SRFs are
evaluated in a separate ProjectSet node. As that's quite confusing, we're
likely to instead prohibit SRFs in those places. But that's still being
discussed, and the code would reside in places not touched here, so that's
a task for later.
It should be noted though that, the new approach if you wanted to do something kinda crazy like that does seem to be faster than the standard approach.
Here is a quick test I did to compare the results and timing:
New feature way:
FROM (SELECT CASE generate_series(1,10000000) WHEN 1 THEN 'First' ELSE 'Not First' END) AS f;
Uses this new thing called a ProjectSet executor and takes ~1 second
Aggregate (cost=32.52..32.53 rows=1 width=8) (actual time=1036.048..1036.048 rows=1 loops=1)
-> Result (cost=0.00..20.02 rows=1000 width=32) (actual time=0.004..722.375 rows=10000000 loops=1)
Output: CASE (generate_series(1, 10000000)) WHEN 1 THEN 'First'::text ELSE 'Not First'::text END
-> ProjectSet (cost=0.00..5.02 rows=1000 width=4) (actual time=0.002..329.822 rows=10000000 loops=1)
Output: generate_series(1, 10000000)
-> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.000..0.001 rows=1 loops=1)
Planning time: 0.034 ms
Execution time: 1036.082 ms
Good old backwards compatible way:
FROM (SELECT CASE i WHEN 1 THEN 'First' ELSE 'Not First' END
FROM generate_series(1,10000000) AS i) AS f;
Aggregate (cost=12.50..12.51 rows=1 width=8)
(actual time=1800.258..1800.259 rows=1 loops=1)
-> Function Scan on pg_catalog.generate_series i (cost=0.00..10.00 rows=1000 width=0)
(actual time=863.841..1469.087 rows=10000000 loops=1)
Function Call: generate_series(1, 10000000)
Planning time: 0.032 ms
Execution time: 1818.415 ms
Wow that's much slower.
One thing I love about PostgreSQL that you miss with closed-source and even with other open source projects, is that when you are curious about a new feature
or why it's there there is plenty of dialogue on the hacker list and commit logs to keep you entertained.