I work with PostgreSQL and I have three variables in a function and one of them is of type date:
CREATE OR REPLACE FUNCTION public.fn_reporte_venta_mes(
IN p_fech = date,
IN p_client integer,
IN p_comprobante character
The parameter p_fech serves to enter a date from PHP in format '1111-11-11'. Instead, I want to only insert date a month and year format '1111-11'. But I do not know how to change the data type of the p_fech variable since when I put only the day and month in PHP I get compatibility error.
My complete function:
CREATE OR REPLACE FUNCTION public.fn_reporte_venta(
IN p_fech date,
IN p_client integer,
IN p_comprobante character)
RETURNS TABLE(nro integer, fecha date, tipo character varying, cliente text, porc_igv numeric, st numeric, igv numeric, total numeric) AS
$BODY$
begin
return query
select v.numero_venta,
v.fecha_venta,
tc.descripcion,
concat(apellido_paterno, ' ', apellido_materno, ' ', nombres) as client,
v.porcentaje_igv,
v.sub_total,
v.igv,
v.total
from venta v
inner join cliente cl on v.codigo_cliente = cl.codigo_cliente
inner join tipo_comprobante tc on v.codigo_tipo_comprobante = tc.codigo_tipo_comprobante
where v.estado = 'E' and
(
case when p_fech = '11-11-1111' then
1 = 1
else
v.fecha_venta = p_fech
end
)
and
(
case when p_client = 0 then
1 = 1
else
cl.codigo_cliente = p_client
end
) and
(
case when p_comprobante = '00' then
1 = 1
else
tc.codigo_tipo_comprobante = p_comprobante
end
)
order by 2;
end
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
ROWS 1000;
Use a character type parameter (text or varchar, not character!) and the function to_date() to convert the input to a date.
Your function, simplified and fixed:
CREATE OR REPLACE FUNCTION public.fn_reporte_venta(p_fech text
, p_client integer
, p_comprobante text)
RETURNS TABLE(nro integer, fecha date, tipo varchar, cliente text
, porc_igv numeric, st numeric, igv numeric, total numeric) AS
$func$
DECLARE
v_fech date := to_date(p_fech, 'YYYY-MM-DD'); -- transform to date
BEGIN
RETURN QUERY
SELECT v.numero_venta,
v.fecha_venta,
t.descripcion,
concat_ws(' ', c.apellido_paterno, c.apellido_materno, c.nombres) AS client,
v.porcentaje_igv,
v.sub_total,
v.igv,
v.total
FROM venta v
JOIN cliente c USING (codigo_cliente)
JOIN tipo_comprobante t USING (codigo_tipo_comprobante)
WHERE v.estado = 'E'
AND (v_fech = '1111-11-11' OR v.fecha_venta = v_fech) -- var goes here
AND (p_client = 0 OR c.codigo_cliente = p_client)
AND (p_comprobante = '00' OR t.codigo_tipo_comprobante = p_comprobante)
ORDER BY 2;
END
$func$ LANGUAGE plpgsql STABLE ROWS 1000;
Always use the ISO-8601 format for date literals (YYYY-MM-DD), which is unambiguous with any locale setting. Don't fall for local syntax dialects, they break if a session should run with a different locale.
I am using a date variable in the plpgsql function, which can be assigned at declaration right away.
This expression is very versatile:
to_date(p_fech, 'YYYY-MM-DD');
to_date() allows to omit elements to the right to 1 for missing data. All of these are valid and result in '2016-01-01':
SELECT to_date('2016-01-01', 'YYYY-MM-DD')
, to_date('2016-1-1' , 'YYYY-MM-DD')
, to_date('2016-01' , 'YYYY-MM-DD')
, to_date('2016-' , 'YYYY-MM-DD')
, to_date('2016' , 'YYYY-MM-DD');
So you can pass in full dates or truncated to month or even year. You always specify a single day this way.
Or to always cover a whole month:
...
v_fech date := to_date(p_fech, 'YYYY-MM'); -- no "-DD" truncates to month
...
AND (v_fech = '1111-11-01' -- also truncated to 1st of month!
OR v.fecha_venta >= v_fech
AND v.fecha_venta < v_fech + interval '1 month')
...
Why not data type character? Because that's an outdated, largely useless type, typically a misunderstanding:
Best way to check for "empty or null value"
Any downsides of using data type "text" for storing strings?
Also note how I replaced your verbose CASE statements with simple OR expressions.
And some other minor improvements ...
Related
I created the following function with plpgsql which takes 3 parameters.
CREATE OR REPLACE FUNCTION public.most_service_calls(
comp_id integer,
calls integer,
months integer)
RETURNS TABLE(state character varying, city character varying, cust_name character varying, num_calls bigint, cost numeric)
LANGUAGE 'plpgsql'
COST 100
VOLATILE
ROWS 1000
AS $BODY$
Begin
return query execute
'select * from
(select l.state, l.city, l.cust_name, count(distinct a.svc_ord_nbr) num_calls,
round(avg(a.std_labr_net_amt_dcrncy) + avg(a.travel_net_amt_dcrncy), 2)
from dmt_mas_svc_ord_fact a
inner join dmt_mas_cust_dim b on a.shipto_cust_id = b.cust_id
inner join store_location l on b.cust_name = l.cust_name
inner join company co on b.cust_lvl_2_nbr = co.company_nbr
where b.sap_sls_org_name like ''%Stanley US%''
and a.create_dtm >= now() - interval '' $3 months''
and co.company_id = $1
group by l.state, l.city, l.cust_name
order by l.state, l.city, l.cust_name ) q
where num_calls >= $2'
using comp_id, calls, months;
end;
$BODY$;
Since the query is quoted, all the single quoted strings are double quoted. Three variables are represented by $1, $2, $3. It is the variable inside a string that is causing the trouble. a.create_dtm >= now() - interval '' $3 months''
When I run the function, it seems to ignore whatever third parameter I provided. Therefore, all the following return the same result.
select * from most_service_calls(1,5,1)
select * from most_service_calls(1,5,12)
select * from most_service_calls(1,5,24)
And it turned out, $3 inside '' '' is taken as 3 since the result matches that of the query with 3 months hardcoded in the query.
What is the correct way to include the variable inside a string in a quoted query like this?
Your problem is not specific to dynamic SQL - you can't refer to a placeholder within a quoted string even in a normal SQL query.
Instead you could use:
$3 * interval '1 month'
or:
($3 || ' months')::interval
The first form multiplies your supplied numeric value by a one month interval. The second constructs a string specifying the number of months and then casts it to an interval.
I am trying to return array from stored function and use it's output in a where clause , for a query. But it is not working for unknown reason. I really do not have clue. Please help.
Using below function.
CREATE OR REPLACE FUNCTION terep_cstm.testdays(
v_in_date date,
v_n integer)
RETURNS text[] AS
$BODY$
declare
listndays text = '';
arrndays text[];
BEGIN
for i in 0..v_n
loop
listndays = listndays||''||(v_in_date - i*interval '1 day')::date::text||',';
end loop;
listndays = trim(trailing ',' from listndays);
arrndays = string_to_array(listndays,',');
return arrndays;
END
$BODY$
LANGUAGE plpgsql VOLATILE;
Executing this query , with stored function.
select retl_code,price_2, attr_1,start_date,price,printedprice
from tessttable abc
where abc.attr_1 = '011' and abc.price_2 = '034' and abc.retl_code = '00068247'
and abc.start_date = ANY (select testdays('2017-05-31'::date,7)) --'2017-05-31'
--and abc.start_date = ANY (ARRAY['2017-05-31','2017-05-30','2017-05-29','2017-05-28','2017-05-27','2017-05-26','2017-05-25','2017-05-24'])
--and abc.start_date in ('2017-05-31','2017-05-30','2017-05-29','2017-05-28','2017-05-27','2017-05-26','2017-05-25','2017-05-24');
This is not working for ANY operator , with error message,
ERROR: operator does not exist: text = text[];
Please let me know where i am making mistake. The abc. start_date datatype is text and it is platform configured. Unfortunately we have no liberty to convert this column datatype and have to carry this one only.
It seems to work with
and abc.start_date = ANY (testdays('2017-05-31'::date,7));
But query gets hanged for a long time.As i could see the different in explain plan as well.
Query:
select retl_code,price_2, attr_1,start_date,price,printedprice
from tessttable abc
where abc.attr_1 = '011' and abc.price_2 = '034' and abc.retl_code = '00068247'
and abc.start_date in ('2017-05-31','2017-05-30','2017-05-29','2017-05-28','2017-05-27','2017-05-26','2017-05-25','2017-05-24');
Explain Plan:
"Index Scan using tessttable_comp_date on tessttable abc (cost=0.70..41.70 rows=1 width=43)"
" Index Cond: ((start_date = ANY ('{2017-05-31,2017-05-30,2017-05-29,2017-05-28,2017-05-27,2017-05-26,2017-05-25,2017-05-24}'::text[])) AND (prd_price_attr_1 = '011'::text) AND (price_2 = '034'::text) AND (retl_code = '00068247': (...)"
Query:
select retl_code,price_2, attr_1,start_date,price,printedprice
from tessttable abc
where abc.attr_1 = '011' and abc.price_2 = '034' and abc.retl_code = '00068247'
and abc.start_date = ANY (testdays('2017-05-31'::date,7)) ;
Explain Plan:
"Index Scan using tessttable_comp_date on tessttable abc (cost=0.70..10285844.48 rows=4 width=43)"
" Index Cond: ((prd_price_attr_1 = '011'::text) AND (price_2 = '034'::text) AND (retl_code = '00068247'::text))"
" Filter: (start_date = ANY (just_n_days('2017-05-31'::date, 7)))"
Get rid of the select, simply use the function as an expression:
and abc.start_date = ANY ( testdays('2017-05-31'::date,7) )
If abc.start_date is defined as date then you shouldn't compare a date value with an array of strings. Make the function return a date[] instead.
The function can also be simplified to a simple SQL function, there is no need for PL/pgSQL:
CREATE OR REPLACE FUNCTION testdays(v_in_date date, v_n integer)
RETURNS date[] AS
$BODY$
select array_agg(x::date)
from generate_series(current_date, current_date + 6, interval '1' day) x;
$BODY$
LANGUAGE sql;
To stick with the wrong text values for dates, just change what you aggregate
CREATE OR REPLACE FUNCTION testdays(v_in_date date, v_n integer)
RETURNS text[] AS
$BODY$
select array_agg(to_char(x, 'yyyy-mm-dd'))
from generate_series(current_date, current_date + 6, interval '1' day) x;
$BODY$
LANGUAGE sql;
Note that I changed the return type to date[] so that you will compare dates with dates.
However, I don't see why you need a function in the first place:
and abc.start_date between date '2017-05-31' and date '2017-05-31' + 7
would achieve exactly the same thing and will be faster in most of the cases.
I have function to get employee in 'Create' status.
CREATE OR REPLACE FUNCTION get_probation_contract(AccountOrEmpcode TEXT, FromDate DATE,
ToDate DATE)
RETURNS TABLE("EmpId" INTEGER, "EmpCode" CHARACTER VARYING,
"DomainAccount" CHARACTER VARYING, "JoinDate" DATE,
"ContractTypeCode" CHARACTER VARYING, "ContractTypeName" CHARACTER VARYING,
"ContractFrom" DATE, "ContractTo" DATE, "ContractType" CHARACTER VARYING,
"Signal" CHARACTER VARYING) AS $$
BEGIN
RETURN QUERY
EXECUTE 'SELECT
he.id "EmpId",
rr.code "EmpCode",
he.login "DomainAccount",
he.join_date "JoinDate",
contract_type.code "ContractTypeCode",
contract_type.name "ContractTypeName",
contract.date_start "ContractFrom",
contract.date_end "ContractTo",
CASE WHEN contract_group.code = ''1'' THEN ''Probation''
WHEN contract_group.code IN (''3'', ''4'', ''5'') THEN ''Official''
WHEN contract_group.code = ''2'' THEN ''Collaborator'' END :: CHARACTER VARYING "ContractType",
''CREATE'' :: CHARACTER VARYING "Signal"
FROM
hr_employee he
INNER JOIN resource_resource rr
ON rr.id = he.resource_id
INNER JOIN hr_contract contract
ON contract.employee_id = he.id AND contract.date_start = (
SELECT max(date_start) "date_start"
FROM hr_contract cc
WHERE cc.employee_id = contract.employee_id
)
INNER JOIN hr_contract_type contract_type
ON contract_type.id = contract.type_id
INNER JOIN hr_contract_type_group contract_group
ON contract_group.id = contract_type.contract_type_group_id
WHERE
contract_group.code = ''1''
AND
($1 IS NULL OR $1 = '''' OR rr.code = $1 OR
he.login = $1)
AND (
(he.join_date BETWEEN $2 AND $3)
OR (he.join_date IS NOT NULL AND (contract.date_start BETWEEN $2 AND $3))
OR (he.create_date BETWEEN $2 AND $3 AND he.create_date > he.join_date)
)
AND rr.active = TRUE
'using AccountOrEmpcode, FromDate, ToDate ;
END;
$$ LANGUAGE plpgsql;
It took 37 second to execute
SELECT *
FROM get_probation_contract('', '2014-01-01', '2014-06-01');
When I use single query
SELECT
he.id "EmpId",
rr.code "EmpCode",
he.login "DomainAccount",
he.join_date "JoinDate",
contract_type.code "ContractTypeCode",
contract_type.name "ContractTypeName",
contract.date_start "ContractFrom",
contract.date_end "ContractTo",
CASE WHEN contract_group.code = '1' THEN 'Probation'
WHEN contract_group.code IN ('3', '4', '5') THEN 'Official'
WHEN contract_group.code = '2' THEN 'Collaborator' END :: CHARACTER VARYING "ContractType",
'CREATE' :: CHARACTER VARYING "Signal"
FROM
hr_employee he
INNER JOIN resource_resource rr
ON rr.id = he.resource_id
INNER JOIN hr_contract contract
ON contract.employee_id = he.id AND contract.date_start = (
SELECT max(date_start) "date_start"
FROM hr_contract
WHERE employee_id = he.id
)
INNER JOIN hr_contract_type contract_type
ON contract_type.id = contract.type_id
INNER JOIN hr_contract_type_group contract_group
ON contract_group.id = contract_type.contract_type_group_id
WHERE
contract_group.code = '1'
AND (
(he.join_date BETWEEN '2014-01-01' AND '2014-06-01')
OR (he.join_date IS NOT NULL AND (contract.date_start BETWEEN '2014-01-01' AND '2014-01-06'))
OR (he.create_date BETWEEN '2014-01-01' AND '2014-01-06' AND he.create_date > he.join_date)
)
AND rr.active = TRUE
It take 5 second to complete
How to optimize the function above.
and why function is slow than single query so much even I use execute 'select ...' in function.
Indexing in field id each table.
Possible reason is a blind optimization for prepared statements (embedded SQL). It is little bit better in new PostgreSQL releases, although it can be the issue there too. Execution plan in embedded SQL in PL/pgSQL is reused for more calls - and it is optimized for more often value (not for really used value). Sometimes this difference can make really big slowdowns.
Then you can use dynamic SQL - EXECUTE statement. Dynamic SQL uses only once executed plans and it uses real parameters. It should to fix this issue.
Example of embedded SQL with reused prepared plans.
CREATE OR REPLACE FUNCTION fx1(_surname text)
RETURNS int AS $$
BEGIN
RETURN (SELECT count(*) FROM people WHERE surname = _surname)
END;
Example with dynamic SQL:
CREATE OR REPLACE FUNCTION fx2(_surname text)
RETURNS int AS $$
DECLARE result int;
BEGIN
EXECUTE 'SELECT count(*) FROM people WHERE surname = $1' INTO result
USING _surname;
RETURN result;
END;
$$ LANGUAGE plpgsql;
Second function can be faster if your dataset contains some terrible often surname - then common plan will be seq scan, but lot of time you will ask some other surname, and you will want to use index scan. Dynamical query parametrization (like ($1 IS NULL OR $1 = '''' OR rr.code = $1 OR) has same effect.
Your queries are not the same.
The first one has
WHERE cc.employee_id = contract.employee_id
where the second one has:
WHERE employee_id = he.id
And also:
($1 IS NULL OR $1 = '''' OR rr.code = $1 OR
he.login = $1)
Please test again with identical queries and identical values.
I have created custom data type. In that I have given alias name of the one field. you will get that in body of the function below.
create type voucher as (
ori numeric, RECEIPT_NO numeric
, receipt_date timestamp with time zone, reg_no character varying
, patient_name character varying, tot_refund_bill_amount double precision
, username character varying );
Thea above statement completes successfully.
Then I want to create a function:
create or replace function billing.voucher_receipt (in_from_date timestamp with time zone, in_to_date timestamp with time zone)
returns setof voucher as $$
declare
out_put voucher%rowtype;
begin
return query(select C.receipt_no as ori ,A.RECEIPT_NO, receipt_date , A.reg_no, patient_name, tot_refund_bill_amount, username
from billing.tran_counter_receipt as a inner join mas_user as b on a.ent_by=b.uid AND cash_book='REFUND'
INNER JOIN billing.tran_BILL AS C ON C.REG_NO=A.REG_NO AND C.CASH_BOOK='GCASH' where receipt_date>=in_from_date and receipt_date<=in_to_date);
end;$$
LANGUAGE plpgsql
Executes without problem.
But when I call it with input like this:
select * from voucher_receipt ('2014-09-25 11:42:44.298346+05:30'
, '2014-09-29 11:03:47.573049+05:30')
it shows an error:
ERROR: function voucher_receipt(unknown, unknown) does not exist
LINE 1: select * from voucher_receipt ('2014-09-25 11:42:44.298346+0...
^
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
Can any one help me out from this?
Explain error
You created your function in the schema billing with:
create or replace function billing.voucher_receipt( ...
Then you call without schema-qualification:
select * from voucher_receipt ( ...
This only works while your current setting for search_path includes the schema billing.
Better function
You don't need to create a composite type. Unless you need the same type in multiple places just use RETURNS TABLE to define the return type in the function:
CREATE OR REPLACE FUNCTION billing.voucher_receipt (_from timestamptz
, _to timestamptz)
RETURNS TABLE (
ori numeric
, receipt_no numeric
, receipt_date timestamptz
, reg_no varchar
, patient_name varchar
, tot_refund_bill_amount float8
, username varchar) AS
$func$
BEGIN
RETURN QUERY
SELECT b.receipt_no -- AS ori
, cr.RECEIPT_NO
, ??.receipt_date
, cr.reg_no
, ??.patient_name
, ??.tot_refund_bill_amount
, ??.username
FROM billing.tran_counter_receipt cr
JOIN billing.tran_bill b USING (reg_no)
JOIN mas_user u ON u.uid = cr.ent_by
WHERE ??.receipt_date >= _from
AND ??.receipt_date <= _to
AND b.CASH_BOOK = 'GCASH'
AND ??.cash_book = 'REFUND'
END
$func$ LANGUAGE plpgsql;
Notes
Don't call your parameters "date" while they are actually timestamptz.
RETURN QUERY does not require parentheses.
No need for DECLARE out_put voucher%rowtype; at all.
Your format was inconsistent and messy. That ruins readability and that's also where bugs can hide.
This could just as well be a simple SQL function.
Column names in RETURNS TABLE are visible in the function body almost everywhere. table-qualify columns in your query to avoid ambiguities (and errors). Replace all ??. I left in the code, where information was missing.
Output column names are superseded by names in the RETURNS declaration. So AS ori in the SELECT list is just documentation in this case.
Why schema-qualify billing.tran_bill but not mas_user?
I am trying to write a dateadd() function using PL/PgSQL. I want to be able to add anything from seconds, right up to years to a date/timestamp. have cobbled together a function (from snippets etc obtained online), and have come up with this "implementation":
CREATE OR REPLACE FUNCTION dateadd(diffType VARCHAR(15), incrementValue int, inputDate timestamp) RETURNS timestamp AS $$
DECLARE
YEAR_CONST Char(15) := 'year';
MONTH_CONST Char(15) := 'month';
DAY_CONST Char(15) := 'day';
HOUR_CONST Char(15) := 'hour';
MIN_CONST Char(15) := 'min';
SEC_CONST Char(15) := 'sec';
dateTemp Date;
intervals interval;
BEGIN
IF lower($1) = lower(YEAR_CONST) THEN
select cast(cast(incrementvalue as character varying) || ' year' as interval) into intervals;
ELSEIF lower($1) = lower(MONTH_CONST) THEN
select cast(cast(incrementvalue as character varying) || ' months' as interval) into intervals;
ELSEIF lower($1) = lower(DAY_CONST) THEN
select cast(cast(incrementvalue as character varying) || ' day' as interval) into intervals;
ELSEIF lower($1) = lower(HOUR_CONST) THEN
select cast(cast(incrementvalue as character varying) || ' hour' as interval) into intervals;
ELSEIF lower($1) = lower(MIN_CONST) THEN
select cast(cast(incrementvalue as character varying) || ' minute' as interval) into intervals;
ELSEIF lower($1) = lower(SEC_CONST) THEN
select cast(cast(incrementvalue as character varying) || ' second' as interval) into intervals;
END IF;
dateTemp:= inputdate + intervals;
RETURN dateTemp;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
However, when I try to use the function, I get the following error:
template1=# select dateadd('hour', 1, getdate());
ERROR: function dateadd(unknown, integer, timestamp with time zone) does not exist
LINE 1: select dateadd('hour', 1, getdate());
^
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
template1=#
Why is the function not being found by PG?
I am running PG 9.3 on Ubuntu 12.0.4 LTS
You'll kick yourself: the difference between the line that's erroring (and the next few) and the one before it is that you've accidentally added an underscore in the wrong place:
You have
HOUR_CONST_Char(15) := 'hour';
It should be
HOUR_CONST Char(15) := 'hour';
EDIT
The updated question is suffering from Postgres's slightly fussy type system: your getdate() function is returning timestamp with time zone, but your dateadd accepts a timestamp (i.e. timestamp without time zone). Using the Postgres short-hand for cast of value::type, this should work (SQLFiddle demo, using now() in place of getdate())
select dateadd('hour', 1, getdate()::timestamp);
However, you have a few other odd type selections:
Your "constants" are Char(15), but aren't 15 characters long, so will be padded with spaces; you should probably use VarChar(15), or even just text (unlike MS SQL, in Postgres, all strings are stored out-of-page dynamically, and a VarChar is essentially just text with a maximum length constraint).
Your intermediate variable (which can probably be refactored out) is of type Date, not Timestamp, so will truncate the input to just the date part, no time.
Finally, I'll note one fundamental optimisation: you don't need to lower() your constants, because you already know they're lower case. :)
It's the underscore between HOUR_CONST and Char(15) You should enter "HOUR_CONST Char(15)" instead of "HOUR_CONST_Char(15)"