How do I write a function in plpgsql that compares a date with a timestamp without time zone?

こ雲淡風輕ζ 提交于 2021-02-17 03:08:24

问题


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 and lastDate

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 type timestamp 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:

  1. 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 like where current_timestamp <= ' || quote_literal(d).

    Now produced query is correct:

    select 1 from generate_series(1,100) where current_timestamp <= '2020-05-25'
    
  2. 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 use USING 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$
    
  3. 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 just RETURN 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!