How to pass the number of days to a Postgres function? - sql

Argument days in function getAvgByDay() doesn't work, I guess because it is inside quotes:
CREATE OR REPLACE FUNCTION getAvgByDay(days int)
RETURNS TABLE ( average text,
date timestamp with time zone
) AS
$func$
BEGIN
RETURN QUERY
SELECT to_char( AVG(measure), '99999D22') AS average, ( now() - interval '$1 day') AS date
FROM (
SELECT mes.date, mes.measure
FROM measures mes
WHERE mes.date < ( now() - interval '$1 day')
) AS mydata;
END
$func$
LANGUAGE plpgsql;

Assuming the column measures.date is actually data type timestamptz and not a date:
CREATE OR REPLACE FUNCTION get_avg_by_day(_days int)
RETURNS TABLE (average text, ts timestamptz) AS -- not using "date" for a timestamp
$func$
SELECT to_char(avg(measure), '99999D22') -- AS average
, now() - interval '1 day' * $1 -- AS ts
FROM measures m
WHERE m.date < now() - interval '1 day' * $1
$func$ LANGUAGE sql;
No need for PLpgSQL, can be a simper SQL function.
Difference between language sql and language plpgsql in PostgreSQL functions
No need for a subquery. Only adds complexity and cost for no gain.
No need for column aliases in the outer query level. Those are not used, as visible column names are defined in the RETURNS clause.
No need for extra parentheses. Operator precedence works as desired anyway. (No harm in this case, either.)
Don't use CaMeL case identifier in Postgres if you can avoid it.
Are PostgreSQL column names case-sensitive?
Don't call a timestamptz column "date". That's misleading. Using "ts" instead.
Most importantly: You suspected as much, and "sticky bit" already explained: no interpolation inside strings. But just multiply the time unit with your integer input to subtract the given number of days:
interval '1 day' * $1
That's faster and cleaner than string concatenation.

There's no interpolation in strings. But you can concatenate strings and cast them to an interval. Try:
... concat(days, ' day')::interval ...
Or you could use format(), that's probably a little closer to what you originally had:
... format('%s day', days)::interval ...

Related

Add one day to Now() and return as default value in SQL query

I am attempting to add a day to NOW() and return as the values for a column.
This works
SELECT NOW() as date
But this gives an error
SELECT DATE_ADD( NOW(), INTERVAL 1 DAY) as date
Is there a way to achieve this in a postgres query?
Thanks
I don't think there's a date_add() function in PostgreSQL:
ERROR: function date_add(timestamp with time zone, interval) does not
exist
LINE 1: select date_add(now(), interval '1 day');
^
HINT: No function matches the given name and argument types. You
might need to add explicit type casts.
but you can use a regular + operator to add an interval to timestamptz that's returned by now(). Demo:
select now() + '1 day'::interval;
You can define that function for convenience:
create function date_add(arg1 timestamptz, arg2 interval)
returns timestamptz language sql as $$
select arg1+arg2
$$;
select date_add(now(), interval '1 day') as date;
-- date
---------------------------------
-- 2022-11-29 12:28:12.393508+00
But I don't think it's really more convenient than the operator. You'd also have to overload it to make sure how it deals with different types - you can see in the demo how by default PostgreSQL will try to guess and cast automatically.

How do I join a string and an int in PostgreSQL?

I have a procedure with an int parameter.
CREATE OR REPLACE PROCEDURE update_retention_policy(id int, days int)
language plpgsql
AS
$$
BEGIN
PERFORM add_retention_policy(('schema_' + id + '.my_hypertable'), days * INTERVAL '1 day', true);
END
$$;
The syntax for the add_retention_policy function is add_retention_policy('hypertable', INTERVAL 'x days', true). I want to prefix the hypertable with the schema which is always 'schema_' and then followed by the id parameter, how do I do that?
You just need to rewrite the INTERVAL part in your function call as days * INTERVAL '1 day'.
Instead of concatenating strings, you multiply the '1 day' interval by the days param.
EDIT: for the id part, you can just use the || operator, which is the string concatenation operator in Postgres, instead of +. You shouldn't even need to explicitly cast id to character varying

Read & evaluate cell as keyword in PostgreSQL

I have a table called providers where the column notice_period takes values such as interval '1 month' - interval '2 days'. If I do the following:
select to_date('11.11.2020', 'dd-mm-yyyyy') + notice_period from providers
I get the following error:
42883] ERROR: operator does not exist: ` text
How can I avoid this error and use the value of notice_period to directly calculate a valid date?
Cheers
Postgres readily converts intervals to strings. I would recommend removing the interval key word and just using ::interval:
select date_trunc('year', now()) + notice_period::interval
from (values ('1 day'), ( '3 month 1 day')) v(notice_period)
If you insist on the interval in the string, then remove it:
+ replace(notice_period, 'interval ', '')::interval
or:
+ substr(notice_period, 10)::interval
Another option is to use dynamic SQL in a function:
CREATE FUNCTION add_period(timestamp with time zone) RETURNS timestamp with time zone
LANGUAGE plpgsql STRICT AS
$$DECLARE
result timestamp with time zone;
BEGIN
EXECUTE format(
'SELECT TIMESTAMP WITH TIME ZONE %L + %s',
$1,
(SELECT notice_period FROM providers)
) INTO result;
RETURN result;
END;$$;
The problem with that is that the whole thing is vulnerable to SQL injection by bad values in the table providers, so use that only if you can trust the source.
This problem cannot be avoided with SQL strings supplied from outside.

Oracle. Select and function

I have function like this:
CREATE OR REPLACE FUNCTION IntervalToSec(Run_Duration interval day to second) RETURN NUMBER IS
vSeconds NUMBER ;
BEGIN
SELECT EXTRACT( DAY FROM Run_Duration ) * 86400
+ EXTRACT( HOUR FROM Run_Duration ) * 3600
+ EXTRACT( MINUTE FROM Run_Duration ) * 60
+ EXTRACT( SECOND FROM Run_Duration )
INTO
vSeconds
FROM DUAL ;
RETURN vSeconds ;
END;
That converts interval data to total seconds number
Then i have select query:
select RUN_DURATION from SYS.USER_SCHEDULER_JOB_RUN_DETAILS WHERE JOB_NAME = 'WASTE_MESSAGES_JOB' and LOG_DATE > (systimestamp - INTERVAL '0 00:10:00.0'DAY TO SECOND(1) )
order by LOG_DATE desc;
Output like:+00 00:00:01.000000
Question is how can i pipe query result into function?
Thank you!
A user defined function doesn't behave any differently to built-in functions, so you call it as you would any other function - to_char, to_date, trunc, round etc.
You can write user-defined functions in PL/SQL, Java, or C to provide functionality that is not available in SQL or SQL built-in functions. User-defined functions can appear in a SQL statement wherever an expression can occur.
For example, user-defined functions can be used in the following:
The select list of a SELECT statement
...
So just call the function as part of the query:
select IntervalToSec(RUN_DURATION)
from SYS.USER_SCHEDULER_JOB_RUN_DETAILS
WHERE JOB_NAME = 'WASTE_MESSAGES_JOB'
and LOG_DATE > (systimestamp - INTERVAL '0 00:10:00.0'DAY TO SECOND(1) )
order by LOG_DATE desc;
As long as the queried column is the same data type the function expects (or can be implicitly converted to that type) it can be passed as the function's argument.
https://docs.oracle.com/cd/B13789_01/appdev.101/b10807/13_elems045.htm
I believe a select Into statement will work for this? If you just want the query results to go into the function statement. Parameters for the into say it will take a function_name. Of course I could be way off as I've never used it for getting information into functions.

Dynamic Date Table

I am creating a Data Model in PowerPivot and am wondering if there is anyway I can create a dynamic date table in SQL. I was able to create one in PowerQuery however there are some bugs in PowerQuery(read only connection) when a table is modified in PowerPivot. What I am looking for is to have a start date of 1/1/2013 (interval is days) and as each new year rolls around rows are added to the date table. Is there anyway to do this?
I am running Postgres
So far I came up with this,
SELECT * FROM dbo.fof_GetDates('1/1/2013', GETDATE())
But I want it to display all dates till end of the year.
The completely dynamic approach would be a query based on generate_series():
SELECT the_date::date
FROM generate_series('2013-01-01 0:0'::timestamp
, date_trunc('year', now()::timestamp)
+ interval '1 year - 1 day'
, interval '1 day') the_date;
Always use ISO 8601 format for dates and timestamps, which works irregardless of locale settings.
A final cast to date (the_date::date), because the function returns timestamp (when fed timestamp arguments).
The expression
date_trunc('year', now()::timestamp) + interval '1 year - 1 day'
calculates the last day of the current year. Alternatively you could use EXTRACT (year FROM now())::text || '-12-31')::date, but that's slower.
You can wrap this into a custom "table-function" (a.k.a. set-returning function) that you can basically use as drop-in replacement for a table name in queries:
CREATE OR REPLACE FUNCTION f_dates_since_2013()
RETURNS SETOF date AS
$func$
SELECT the_date::date
FROM generate_series('2013-01-01 0:0'::timestamp
, date_trunc('year', now()::timestamp)
+ interval '1 year - 1 day'
, interval '1 day') the_date;
$func$ LANGUAGE sql STABLE;
Example:
SELECT * FROM f_dates_since_2013();
Going one step further, you could create a table or - more elegantly - a MATERIALIZED VIEW based on this function (or the underlying query directly):
CREATE MATERIALIZED VIEW my_dates(the_date) AS
SELECT * FROM f_dates_since_2013();
Call:
SELECT * FROM my_dates;
All you have to do now is to schedule a yearly cron job that runs REFRESH MATERIALIZED VIEW at the start of each new year:
REFRESH MATERIALIZED VIEW my_dates;