问题
I want to write a function that returns a table with all the rows between firstDate
and lastDate
. The rows have datatype timestamp without time zone They also have to be of a specific node id.
This is my function:
CREATE OR REPLACE FUNCTION get_measurements_by_node_and_date(nodeID INTEGER, firstDate date, lastDate date)
RETURNS TABLE (measurement_id INTEGER, node_id INTEGER, carbon_dioxide DOUBLE PRECISION,
hydrocarbons DOUBLE PRECISION, temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
air_pressure DOUBLE PRECISION,
measurement_timestamp timestamp without time zone ) AS
$$
DECLARE
sql_to_execute TEXT;
BEGIN
SELECT 'SELECT measurements_lora.id,
measurements_lora.node_id,
measurements_lora.carbon_dioxide,
measurements_lora.hydrocarbons,
measurements_lora.temperature,
measurements_lora.humidity,
measurements_lora.air_pressure,
measurements_lora.measurement_timestamp AS measure
FROM public.measurements_lora
WHERE measurements_lora.measurement_timestamp <= '||lastDate||'
AND measurements_lora.measurement_timestamp >= '||firstDate||'
AND measurements_lora.node_id = '||nodeID||' '
INTO sql_to_execute;
RETURN QUERY EXECUTE sql_to_execute;
END
$$ LANGUAGE plpgsql;
The column measurement_timestamp is of type timestamp without time zone and is formatted like yy-mm-dd hh-mm-ss
When I run SELECT * FROM get_measurements_by_node_and_date(1, '2020-5-1', '2020-5-24')
I get the following error:
ERROR: operator does not exist: timestamp without time zone <= integer LINE 10: ... WHERE measurements_lora.measurement_timestamp <= 2020-05...
I don't get why it says "integer", because I clearly defined firstDate
and lastDate
as type date
.
回答1:
What Pavel said.
What's more, nothing indicates a need for PL/pgSQL to begin with. A plain (prepared) SELECT
statement can do it. Or an SQL function, if you want to persist it in the database. See:
- Difference between language sql and language plpgsql in PostgreSQL functions
And concerning:
with all the rows between
firstDate
andlastDate
Define inclusive / exclusive upper / lower bound precisely to avoid surprising corner case results. When comparing a timestamp
column to a date
, the latter is coerced to the timestamp signifying the first instance of the day: YYYY.MM.DD 00:00:00
.
Your query says:
measurement_timestamp <= lastDate AND measurement_timestamp >= firstDate
... which would include all of firstDate
, but exclude all of lastDate
except for the first (common) instance at 00:00. Typically not what you want. Given your formulation, I suppose this is what you really want:
CREATE OR REPLACE FUNCTION get_measurements_by_node_and_date(node_id integer
, firstDate date
, lastDate date)
RETURNS TABLE (measurement_id integer
, node_id integer
, carbon_dioxide float8
, hydrocarbons float8
, temperature float8
, humidity float8
, air_pressure float8
, measurement_timestamp timestamp)
LANGUAGE sql STABLE AS
$func$
SELECT m.id
, m.node_id
, m.carbon_dioxide
, m.hydrocarbons
, m.temperature
, m.humidity
, m.air_pressure
, m.measurement_timestamp -- AS measure -- just documentation
FROM public.measurements_lora m
WHERE m.node_id = _node_id
AND m.measurement_timestamp >= firstDate::timestamp
AND m.measurement_timestamp < (lastDate + 1)::timestamp -- ①!
$func$;
① This includes all of lastDate
, and efficiently. You can just add / subtract an integer value to / from a date to add / subtract days. The explicit cast to ::timestamp
is optional as date would be coerced in the expression automatically. But since we are trying to clear up confusion here ...
Related:
- How do I determine the last day of the previous month using PostgreSQL?
Aside 1:
The column
measurement_timestamp
is of typetimestamp without time zone
and is formatted like yy-mm-dd hh-mm-ss
No. timestamp
values are not formatted, period. They are just timestamp values (internally stored as the number of microseconds since the epoch). Display is completely separate from the value and can be adjusted in a hundred and one ways without changing the value. Get rid of this misconception to better understand what's going on. See:
- Ignoring time zones altogether in Rails and PostgreSQL
Aside 2:
About the devious nature of SQL BETWEEN
:
- How to add a day/night indicator to a timestamp column?
Aside 3:
Consider legal, lower-case identifiers in Postgres. first_date
instead of firstDate
. See:
- Are PostgreSQL column names case-sensitive?
Related:
- PostgreSQL function returning a data cube
- Postgresql trying to use execute format in a function but getting column not found error when giving string format in coalesce
回答2:
In this case is good to write executed query first. I try to reduce your example:
CREATE OR REPLACE FUNCTION public.foo(d date)
RETURNS TABLE(o integer)
LANGUAGE plpgsql
AS $function$
declare q text;
begin
q := 'select 1 from generate_series(1,100) where current_timestamp <= ' || d ;
raise notice '%', q;
return query execute q;
end;
$function$
postgres=# select * from foo('2020-05-25');
NOTICE: 00000: select 1 from generate_series(1,100) where current_timestamp <= 2020-05-25
LOCATION: exec_stmt_raise, pl_exec.c:3826
ERROR: 42883: operator does not exist: timestamp with time zone <= integer
LINE 1: ...om generate_series(1,100) where current_timestamp <= 2020-05...
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
QUERY: select 1 from generate_series(1,100) where current_timestamp <= 2020-05-25
CONTEXT: PL/pgSQL function foo(date) line 6 at RETURN QUERY
I got same error message. So there are more than one error:
The dynamic query is not valid - the where clause looks like
where current_timestamp <= 2020-05-25
and you can see, it is not valid - there are not quotes. You can fix it, when you use quotes manually (but it is strong error and don't do it, or you can use function
quote_literal
likewhere current_timestamp <= ' || quote_literal(d)
.Now produced query is correct:
select 1 from generate_series(1,100) where current_timestamp <= '2020-05-25'
But in this case, is much better to use
EXECUTE USING
. When a variable is used as query parameter (not as table or column name), then you can useUSING
clause. Then you don't need to use quoting:CREATE OR REPLACE FUNCTION public.foo(d date) RETURNS TABLE(o integer) LANGUAGE plpgsql AS $function$ declare q text; begin q := 'select 1 from generate_series(1,100) where current_timestamp <= $1'; return query execute q using d; end; $function$
but the big mistake is using dynamic SQL when it is not necessary (like your example). There is not any visible reason why you use
RETURN QUERY EXECUTE
statement. You can use justRETURN QUERY
:CREATE OR REPLACE FUNCTION get_measurements_by_node_and_date(nodeID INTEGER, firstDate date, lastDate date) RETURNS TABLE (measurement_id INTEGER, node_id INTEGER, carbon_dioxide DOUBLE PRECISION, hydrocarbons DOUBLE PRECISION, temperature DOUBLE PRECISION, humidity DOUBLE PRECISION, air_pressure DOUBLE PRECISION, measurement_timestamp timestamp without time zone ) AS $$ BEGIN RETURN QUERY SELECT measurements_lora.id, measurements_lora.node_id, measurements_lora.carbon_dioxide, measurements_lora.hydrocarbons, measurements_lora.temperature, measurements_lora.humidity, measurements_lora.air_pressure, measurements_lora.measurement_timestamp AS measure FROM public.measurements_lora WHERE measurements_lora.measurement_timestamp <= lastDate AND measurements_lora.measurement_timestamp >= firstDate AND measurements_lora.node_id = nodeID; END $$ LANGUAGE plpgsql;
来源:https://stackoverflow.com/questions/62004811/how-do-i-write-a-function-in-plpgsql-that-compares-a-date-with-a-timestamp-witho