After I slept, I had a completely new, simpler and faster idea:
CREATE OR REPLACE FUNCTION f_combos(_arr anyarray) RETURNS TABLE (combo anyarray) LANGUAGE plpgsql AS $BODY$ BEGIN IF array_upper(_arr, 1) IS NULL THEN combo := _arr; RETURN NEXT; RETURN; END IF; CASE array_upper(_arr, 1)
Call:
SELECT * FROM f_combos('{1,2,3,4,5,6,7,8,9}'::int[]) ORDER BY 1;
512 lines, total run time: 2.899 ms
To explain
- Note the special cases with
NULL and an empty array. - Assembly layout for a primitive array of two.
- Any longer array is divided into:
- combinations for one array of length n-1
- plus all those combined with element n .. recursively.
Really simple once you got it.
- Works for 1-dimensional integer arrays, starting at index 1 (see below).
- 2-3 times faster than the old solution, it scales better.
- Works for any type of element again (using polymorphic types).
- Includes an empty array in the result that appears in the question (and as @Craig pointed me out in the comments).
- Shorter, more elegant.
This assumes array indices starting at 1 (the default). If you are unsure of your values, call a function like this to normalize:
SELECT * FROM f_combos(_arr[array_lower(_arr, 1):array_upper(_arr, 1)]);
Not sure if there is a more elegant way to normalize array indices. I asked a question about this:
Normalize array indices for a 1-dimensional array so that they start at 1
Old solution (slower)
CREATE OR REPLACE FUNCTION f_combos2(_arr int[], _a int[] = '{}', _z int[] = '{}') RETURNS SETOF int[] LANGUAGE plpgsql AS $BODY$ DECLARE i int; j int; _up int; BEGIN IF array_length(_arr,1) > 0 THEN _up := array_upper(_arr, 1); FOR i IN array_lower(_arr, 1) .. _up LOOP FOR j IN i .. _up LOOP CASE ji WHEN 0,1 THEN RETURN NEXT _a || _arr[i:j] || _z; WHEN 2 THEN RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z; RETURN NEXT _a || _arr[i:j] || _z; ELSE RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z; RETURN QUERY SELECT * FROM f_combos2(_arr[i+1:j-1], _a || _arr[i], _arr[j] || _z); END CASE; END LOOP; END LOOP; ELSE RETURN NEXT _arr; END IF; END; $BODY$;
Call:
SELECT * FROM f_combos2('{7,15,48}'::int[]) ORDER BY 1;
Works for one-dimensional integer arrays. This can be further optimized, but it is certainly not required for the scope of this issue.
ORDER BY to override the order displayed in the question.
Provide for NULL or an empty array, since NULL is mentioned in the comments.
Tested with PostgreSQL 9.1, but should work with any intermediate version. array_lower() and array_upper() existed since at least PostgreSQL 7.4. Only the default parameter values ββare new in version 8.4. It can be easily replaced.
The performance is decent.
SELECT DISTINCT * FROM f_combos('{1,2,3,4,5,6,7,8,9}'::int[]) ORDER BY 1;
511 lines, total execution time: 7.729 ms
Explanation
It is based on this simple form, which creates only all combinations of neighboring elements:
CREATE FUNCTION f_combos(_arr int[]) RETURNS SETOF int[] LANGUAGE plpgsql AS $BODY$ DECLARE i int; j int; _up int; BEGIN _up := array_upper(_arr, 1); FOR i in array_lower(_arr, 1) .. _up LOOP FOR j in i .. _up LOOP RETURN NEXT _arr[i:j]; END LOOP; END LOOP; END; $BODY$;
But this fails for sub-arrays with more than two elements. So:
For any submatrix with 3 elements, one array is added with only two external elements. it is a shortcut for this special occasion that improves performance and is not strictly necessary.
For any submatrix with more than three elements, I take the outer two elements and fill in all the combinations of internal elements constructed using the same function recursively .