Dynamic field names in a subquery?

I have a table similar to the following:

CREATE TABLE stats ( name character varying(15), q001001 numeric(9,0), q001002 numeric(9,0), q001003 numeric(9,0), q001004 numeric(9,0), q001005 numeric(9,0) ) 

I need to query this table for the sums of the various fields inside, for example:

 SELECT sum(q001001) as total001, sum(q001002) as total002, sum(q001005) as total005, FROM stats; 

This produces a result with ONE row of data and THREE columns.
However, for reporting purposes, I need the results to be listed in reverse order. I need three rows and one column (well, two actually, the first is the field that was summed):

 FieldName | SUM ----------+------- q001001 | 12345 q001002 | 5432 q001005 | 986 

I would like to use some SQL like this, where field_name (from the lookup table for field names in the statistics table) is used in a subquery:

 select l.field_name, (select sum(l.field_name) from stats) from stats_field_names_lookup as l where l.field_name in ('Q001001', 'Q001002', 'Q001005'); 

The idea here is that sum(l.field_name) will be replaced by the actual name of the field in question for each of the sentences in WHERE , and then evaluated to get the correct result value sum'd. This, however, fails with the following error:

function sum (character variation) does not exist

because text / character matters. How can I discard this character value for a string without quotes that will be correctly evaluated?

This SQL is working. But, of course, it gives the same sum values โ€‹โ€‹for each field_name , since here it is hardcoded as q001001 .

 select l.field_name, (select sum(q001001) from stats) from stats_field_names_lookup as l where l.field_name in ('Q001001', 'Q001002', 'Q001005'); 

So, I think the idea sounds theoretical. I just need help figuring out how to get this character / string called field_name. Does anyone have any ideas?

+6
source share
3 answers

Actually, I donโ€™t know how to dynamically indicate column names, but I suggest this way.

 SELECT 'q001001' as FieldName, sum(q001001) as SUM FROM stats UNION SELECT 'q001002' as FieldName, sum(q001002) as SUM FROM stats UNION SELECT 'q001003' as FieldName, sum(q001003) as SUM FROM stats; 

This is easy and will be the solution to your original problem.

-1
source

Main request

It is not possible to calculate each amount separately. Do it in one SELECT and cross-tabulate result.
To keep the answer โ€œshort,โ€ I reduced it to two columns as a result. Expand as needed.

Quick and dirty

Detach two arrays with an equal number of elements in parallel. Read more about this method here and here .

 SELECT unnest('{q001001,q001002}'::text[]) AS fieldname ,unnest(ARRAY[sum(q001001), sum(q001002)]) AS result FROM stats; 

"Dirty" because parallel parallel is Postgres' non-standard behavior, which some are unhappy with. However, it works like a charm. Follow the links for more details.

Detailed and clean

Use CTE and UNION ALL separate lines:

 WITH cte AS ( SELECT sum(q001001) AS s1 ,sum(q001002) AS s2 FROM stats ) SELECT 'q001001'::text AS fieldname, s1 AS result FROM cte UNION ALL SELECT 'q001002'::text, s2 FROM cte; 

"Clear" because it is purely standard SQL.

Minimalistic

The shortest form, but it's also harder to understand:

 SELECT unnest(ARRAY[ ('q001001', sum(q001001)) ,('q001002', sum(q001002))]) FROM stats; 

This works with an array of anonymous entries that are hard to ignore (but possible).

Short

To get individual columns with source types, declare the type on your system:

 CREATE TYPE fld_sum AS (fld text, fldsum numeric) 

You can do the same for the session temporarily by creating a temporary table:

 CREATE TEMP TABLE fld_sum (fld text, fldsum numeric); 

Then:

 SELECT (unnest(ARRAY[ ('q001001'::text, sum(q001001)::numeric) ,('q001002'::text, sum(q001002)::numeric)]::fld_sum[])).* FROM stats; 

The performance for all four options is basically the same, because the expensive part is aggregation.
SQL script demonstrating all the options (based on the script provided by @klin ).

Automation with the PL / pgSQL Function

Quick and dirty

Create and execute the code as described in the corresponding chapter above.

 CREATE OR REPLACE FUNCTION f_list_of_sums1(_tbl regclass, _flds text[]) RETURNS TABLE (fieldname text, result numeric) AS $func$ BEGIN RETURN QUERY EXECUTE ( SELECT ' SELECT unnest ($1) ,unnest (ARRAY[sum(' || array_to_string(_flds, '), sum(') || ')])::numeric FROM ' || _tbl) USING _flds; END $func$ LANGUAGE plpgsql; 
  • Being dirty, it is also not safe for SQL injection. Use it only with a confirmed entry.
    Below is the version.

Call:

 SELECT * FROM f_list_of_sums1('stats', '{q001001, q001002}'); 

Detailed and clean

Create and execute the code as described in the corresponding chapter above.

 CREATE OR REPLACE FUNCTION f_list_of_sums2(_tbl regclass, _flds text[]) RETURNS TABLE (fieldname text, result numeric) AS $func$ BEGIN -- RAISE NOTICE '%', ( -- to get debug output uncomment this line .. RETURN QUERY EXECUTE ( -- .. and comment this one SELECT 'WITH cte AS ( SELECT ' || string_agg( format('sum(%I)::numeric AS s%s', _flds[i], i) ,E'\n ,') || ' FROM ' || _tbl || ' ) ' || string_agg( format('SELECT %L, s%s FROM cte', _flds[i], i) , E'\nUNION ALL\n') FROM generate_subscripts(_flds, 1) i ); END $func$ LANGUAGE plpgsql; 

Call as above.

Basic moments

SQL Fiddle showing all the options.

Beyond this: table definition

The numeric(9,0) data type is a pretty inefficient choice for defining a table. Since you do not store fractional digits and no more than 9 decimal digits, use a simple integer instead. It does the same with only 4 bytes for storage (instead of 8-12 bytes for numeric(9,0) ). If you need numerical accuracy in your calculations, you can always drop the column at a negligible cost.
In addition, I do not use varchar(n) , unless necessary. Just use text .
Therefore, I suggest:

 CREATE TABLE stats ( name text ,q001001 int ,q001002 int , ... ); 
+1
source

Using execute in the plpgsql function. SqlFiddle .

 create or replace function show_stats(field_names text[]) returns table ("FieldName" text, "SUM" numeric) language plpgsql as $$ declare fname text; begin foreach fname in array field_names loop return query execute format(' select ''%s''::text, sum(%s) from stats', fname, fname); end loop; end $$; select * from show_stats(array['q001001', 'q001002', 'q001003', 'q001004']); select * from show_stats(array['q001001', 'q001004']); 
+1
source

Source: https://habr.com/ru/post/969447/


All Articles