Assumptions and Clarification
- Postgres 9.4
- You want various counts from the same given table.
desc
is a reserved word in SQL, don't use it as identifier. Using descr
instead.
1. Basic query
The fastest and most elegant way to get multiple partial counts from the same table is a single SELECT
statement with multiple aggregate functions using the FILTER
clause:
A statement like:
SELECT count(*) FILTER (WHERE size > width) AS ct1
, count(*) FILTER (WHERE size > height) AS ct2
, count(*) FILTER (WHERE width <> height) AS ct3
FROM testtable;
Or, for Postgres 9.3:
SELECT count(size > width OR NULL) AS ct1
, ...
FROM testtable;
2. Advanced query
To get the result in the form you want it, I counter-pivot with a VALUES
expression in a LATERAL
join, adding the ID and description id, descr
in the process:
SELECT x.*
FROM (
SELECT count(*) FILTER (WHERE size > width) AS ct1
, count(*) FILTER (WHERE size > height) AS ct2
, count(*) FILTER (WHERE width <> height) AS ct3
FROM testtable
) t
, LATERAL (
VALUES (1, 'size should not greater than width' , ct1)
, (2, 'size should not greater than height', ct2)
, (3, 'with should be equal to height' , ct3)
) x(id, descr, ct);
Bold parts come from the errortable
when building the statement dynamically:
3. Dynamic query
To concatenate the above statement dynamically from values provided in errortable
:
CREATE OR REPLACE FUNCTION
SELECT 'SELECT x.* FROM (SELECT '
|| string_agg(
format('count(*) FILTER (WHERE %I %s %I) AS c%s'
, field1, operator, field2, id)
, ', ')
|| ' FROM testtable) t, LATERAL (VALUES ('
|| string_agg(format('%s, %L, c%s', id, descr, id), '), (')
|| ')) x(id, dscr, ct)'
FROM errortable;
format()
with %I
quotes column names where necessary and makes the statement safe against SQL injection - except for operator
, which is concatenated as is. You might be able to make that safe, too, with the OPERATOR()
construct ...
If untrusted users don't have write access to errortable
, you can control its content and need not worry.
4. Full automation
Since the return type is uniform and well known, we can encapsulate it in a function:
CREATE OR REPLACE FUNCTION f_error_count()
RETURNS TABLE (id int, descr text, ct bigint) AS
$func$
BEGIN
RETURN QUERY EXECUTE (
SELECT format('SELECT x.* FROM (SELECT %s FROM testtable) t
, LATERAL (VALUES (%s)) x(id, dscr, ct)'
, string_agg(format('count(*) FILTER (WHERE %I %s %I) AS c%s'
, e.field1, e.operator, e.field2, e.id), ', ')
, string_agg(format('%s, %L, c%s', e.id, e.descr, e.id), '), ('))
FROM errortable e
);
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM f_error_count();
Simplifying the concatenation by one more step with an outer format()
. Also note how all columns are table-qualified to avoid conflicts.
Again, since dynamic SQL is involved, make sure it cannot be abused for SQL injection.
SQL Fiddle for Postgres 9.3 since 9.4 is not available.
Best Answer
You can loop using an EXECUTE statement. I believe this post has the exact answer you're looking for, you just need to change the ALTER statement.