to_char() issue in PostgreSQL - postgresql

I have a PostgreSQL table with a field named effective_date and data type is integer(epoch date). What I want to do is to select only the entries that have an effective_date of my choice (I only want to query by the month). My query is below and the problem is, it is not returning anything although the table do have many entries that match the selection criteria.
$query = "select *
from ". $this->getTable() ."
where pay_stub_entry_name_id = 43
AND to_char(effective_date, 'Mon') = 'Jul'
AND deleted = 0";

Use extract(month from the_date) instead of to_char. See datetime functions in the Pg docs.
With to_char you'll suffer from all sorts of issues with case, localisation, and more.
Assuming you meant that the data type of effective_date was timestamp or date, you'd write:
$query = "select *
from ". $this->getTable() ."
where pay_stub_entry_name_id = 43
AND extract(month from effective_date) = 7
AND deleted = 0";
If it's integer then - assuming it's an epoch date - you have to convert it to a timestamp with to_timestamp, then use extract on it. See the epoch section in the documentation linked to above, eg:
$query = "select *
from ". $this->getTable() ."
where pay_stub_entry_name_id = 43
AND extract(month from to_timestamp(effective_date)) = 7
AND deleted = 0";
The immediate cause of your problem was that you were calling to_char(integer,text) with an integer epoch date. Only the timestamp versions of to_char do date formatting; Mon isn't special for the others, so it was simply output as a literal string Mon. Compare:
regress=# SELECT to_char(current_timestamp, 'Mon');
to_char
---------
Aug
(1 row)
regress=# select to_char( extract(epoch from current_timestamp), 'Mon');
to_char
---------
Mon
(1 row)
Remember to parameterise your real-world versions of these queries to help avoid SQL injection.

Related

DB2 How to convert the following "with temp as" into a normal select statement

I'm trying to do an insert with this "with" statement but it seems like it supports select statement only, therefore I want to convert it into select statement. I'm just amazed on how this is working. Got a similar example on stack and changed it to fit my needs.
with temp (startdate, enddate, maxdate) as (
select min(salesdate) startdate, min(salesdate)+3 months enddate, max(salesdate) maxdate
from SALES
union all
select startdate + 3 months + 1 days, enddate + 3 months + 1 days, maxdate from temp
where enddate <= maxdate
)
select startdate, min(enddate, maxdate) from temp;
Thanks in advance.
Edit: It seems my query is misunderstood. Here is the pseudo code of what the query is supposed to be doing. The query is returning the expected result which is pretty amazing to me. I don't know how the recursive doesn't overlap after I added 1 day. After writing the pseudo code, I see that the select startdate + 3 months + 1 days should have been written as select enddate + 1 days which logically says what it's supposed to do instead of magically work:
rows = []
startdate = min(salesdate)
enddate = startdate + 3 months
maxdate = max(salesdate)
i = 0;
do {
rows[i++] = [startdate, min(enddate, maxdate)] // min for final iteration where enddate > maxdate.
startdate = enddate + 1 days
enddate = enddate + 1 days + 3 months // aka: startdate + 3 months
} while (enddate <= maxdate)
return rows
Hence, I've broken a huge date range into smaller chunks of 3 months ranges. Whether it is exactly 90 days or 91 days is not important, as long as I get every single date without gap and without overlap.
I'm curious about your decision, that a query with a recursive common table expression (RCTE) is "not normal". IBM calls it as 'select-statement' and considers it as normal. If it's some educational question, and you don't want to use RCTE due to some reason, then consider the the following example.
select s + (3*(x.i-1)) month start, s + (3*x.i) month - 1 day end
from table(values (date('2011-01-01'), date('2012-01-01'))) d(s, e)
, xmltable('for $id in (1 to $e) return <i>{number($id)}</i>'
passing ((year(e)-year(s))*12 + (month(e)-month(s)))/3 as "e"
columns i int path '.'
) x;
START END
---------- ----------
2011-01-01 2011-03-31
2011-04-01 2011-06-30
2011-07-01 2011-09-30
2011-10-01 2011-12-31
;
It's a little bit complicated, since you must pass desired number of rows to return to the xmltable table function, which returns a single column with values 1 to N. In other words you must compute desired number of 3-months intervals and pass it to the function.
(R)CTE can't be used in the UPDATE/DELETE statements, where you are able to use so called fullselect statements only (they don't allow CTEs). If you really need to use CTE for UPDATE/DELETE as in this case, you can do one of the following:
If you ARE ABLE to compute a temporary result set for whole delete/update statement, you can do something like this (I don't use here RCTE for simplicity, but a simple CTE only):
with a (id) as (values 1)
select count(1)
from old table(
delete from test t
where exists (select 1 from a where a.id=t.id)
);
If you ARE NOT ABLE to compute a temporary result set for whole delete/update statement, you can create a table/scalar function with the corresponding parameters, where you are able to use your RCTE. This function can be used in the outer statement afterwards.
I was able to insert it by moving the insert statement in front of the "with" statement. Copying answer here so I know next time. Although, I'm still interested in seeing how to convert it to a pure select statement. Will select that answer as the correct answer.
insert into my_temp_table
with temp (startdate, enddate, maxdate) as (
select min(salesdate) startdate, min(salesdate)+3 months enddate, max(salesdate) maxdate
from SALES
union all
select startdate + 3 months + 1 days, enddate + 3 months + 1 days, maxdate from temp
where enddate <= maxdate
)
select startdate, min(enddate, maxdate) from temp;

Postgres: difference between two timestamps (hours:minutes:seconds)

i'm creating a select that calculate the difference between two timestamps
here the code: (isn't necessary you understand tables below. Just follow the thread)
(select value from demo.data where id=q.id and key='timestampend')::timestamp
- (select value from demo.data where id=q.id and key='timestampstart')::timestamp) as durata
Look at this example, if you want easier:
select timestamp_end::timestamp - timestamp_start as duration
here the result:
// "durata" is duration
The problem is that the first timestamp is 2017-06-21 and the second is 2017-06-22 so we have 1 day and some hours of difference.
How can i do for show the result not like "1 day 02:06:41.993657" but "26:06:41.993657" without milliseconds (26:06:41)?
Update
I'm testing this query:
select id as ticketid,
(select value from demo.data where id=q.id and key = 'timestampstart')::timestamp as TEnd,
(select value from demo.data where id=q.id and key = 'timestampend')::timestamp as TStart,
(select
make_interval
(
0,0,0,0, -- years, months, weeks, days
extract(days from duration1)::int * 24 + extract(hours from duration1)::int, -- calculated hours (days * 24 + hours)
extract(mins from duration1)::int, -- minutes
floor(extract(secs from duration1))::int -- seconds, without miliseconds, thus FLOOR()
) as duration1
from
(
(select value from demo.data where id=q.id and key='timestampstart')::timestamp - (select value from demo.data where id=q.id and key='timestampend')::timestamp
) t(duration) as dur
from (select distinct id from demo.data) q
error is the same: [Err] ERROR: syntax error at or near "::"
there is an error on id = q.id
data table is like this:
You could use EXTRACT function and wrap it up with MAKE_INTERVAL and some math. It's pretty straight forward, since you pass each part of timestamp to it:
select
make_interval(
0,0,0,0, -- years, months, weeks, days
extract(days from durdata)::int * 24 + extract(hours from durdata)::int, -- calculated hours (days * 24 + hours)
extract(mins from durdata)::int, -- minutes
floor(extract(secs from durdata))::int -- seconds, without miliseconds, thus FLOOR()
) as durdata
from (
select '2017-06-22 02:06:41.993657'::timestamp - '2017-06-21'::timestamp
) t(durdata);
Output:
durdata
----------
26:06:41
You could wrap it up within a function to make it easy to work with.
There is no worry about timestamp - timestamp returning an output with precision to more than days, and thus losing you some information, because even calculation for different years would still return days and additional time part.
Example:
postgres=# select ('2019-06-22 01:03:05.993657'::timestamp - '2017-06-21'::timestamp) as durdata;
durdata
------------------------
731 days 01:03:05.993657
In Postgres, although interval data type allows having hours value greater than 23 (see https://www.postgresql.org/docs/9.6/static/functions-formatting.html), to_char() function will cut out days and will take only "hours within a day" if you put delta value to it and try to get 'HH24' value.
So, I ended up with such trick, combining to_char(...) with extract('epoch' from...) and then putting the concatinated value to another to_char():
with timestamps(ts1, ts2) as (
select
'2017-06-21'::timestamptz,
'2017-06-22 01:03:05.1212'::timestamptz
), res as (
select
round(extract('epoch' from ts2 - ts1) / 3600) as hours,
to_char(ts2 - ts1, 'MI:SS') as min_sec
from timestamps
)
select hours, min_sec, to_char(format('%s:%s', hours, min_sec)::interval, 'HH24:MI:SS')
from res;
The result is:
hours | min_sec | to_char
-------+---------+----------
25 | 03:05 | 25:03:05
(1 row)
You can define an SQL function to make using it easier:
create or replace function extract_hhmmss(timestamptz, timestamptz) returns interval as $$
with delta(i) as (
select
case when $2 > $1 then $2 - $1
else $1 - $2
end
), res as (
select
round(extract('epoch' from i) / 3600) as hours,
to_char(i, 'MI:SS') as min_sec
from delta
)
select
(
case when $2 < $1 then '-' else '' end
|| to_char(format('%s:%s', hours, min_sec)::interval, 'HH24:MI:SS')
)::interval
from res;
$$ language sql stable;
Example of usage:
[local]:5432 nikolay#test=# select extract_hhmmss('2017-06-21'::timestamptz, '2017-06-22 01:03:05.1212'::timestamptz);
extract_hhmmss
----------------
25:03:05
(1 row)
Time: 0.882 ms
[local]:5432 nikolay#test=# select extract_hhmmss('2017-06-22 01:03:05.1212'::timestamptz, '2017-06-21'::timestamptz);
extract_hhmmss
----------------
-25:03:05
(1 row)
Notice, that it will give an error if timestamps are provided in reverse order, but it's not really hard to fix. // Update: already fixed.

Postgres Concate: Date + Timestamp + Integer of 7

How can I combine these two columns and add integer of 7 to it? In postgres.
Both columns are in the table a_event.
start_time is a timestamp without time zone data type.
start_date is a date data type.
so far I have:
SELECT start_time || start_date AS start_date_time
FROM a_event;
This works but it shows:
2015-03-06 21:17:162015-02-06
How can I get it to simply show:
2015-03-06 13:17:16 with an added 7 days so: 2015-03-13 in the desired result.
The || operator concatenates two strings, so the 'start_time' and 'start_date' columns are converted to text and then concatenated to give the result you see.
You want to add 7 days to the starting date. That is simply:
SELECT start_date + 7 AS start_date_time
FROM a_event;
On a date data type the + operator adds a number of days.
If you want to keep the starting time information, the solution is slightly more complex:
SELECT start_time + interval '7 days' AS start_date_time
FROM a_event;
The timestamp data type records microseconds since 01-01-1970, so you have to explicitly indicate that you want to add an interval of 7 days (and not 7 microseconds).

PostgreSql - Adding YEar and Month to Table

I am creating a Customer table and i want one of the attributes to be Expiry Date of credit card.I want the format to be 'Month Year'. What data type should i use? i want to use date but the format is year/month/day. Is there any other way to restrict format to only Month and year?
You can constrain the date to the first day of the month:
create table customer (
cc_expire date check (cc_expire = date_trunc('month', cc_expire))
);
Now this fails:
insert into customer (cc_expire) values ('2014-12-02');
ERROR: new row for relation "customer" violates check constraint "customer_cc_expire_check"
DETAIL: Failing row contains (2014-12-02).
And this works:
insert into customer (cc_expire) values ('2014-12-01');
INSERT 0 1
But it does not matter what day is entered. You will only check the month:
select
date_trunc('month', cc_expire) > current_date as valid
from customer;
valid
-------
t
Extract year and month separately:
select extract(year from cc_expire) "year", extract(month from cc_expire) "month"
from customer
;
year | month
------+-------
2014 | 12
Or concatenated:
select to_char(cc_expire, 'YYYYMM') "month"
from customer
;
month
--------
201412
Use either
char(5) for two-digit years, or
char(7) for four-digit years.
Code below assumes two-digit years, which is the form that matches all my credit cards. First, let's create a table of valid expiration dates.
create table valid_expiration_dates (
exp_date char(5) primary key
);
Now let's populate it. This code is just for 2013. You can easily adjust the range by changing the starting date (currently '2013-01-01'), and the "number" of months (currently 11, which lets you get all of 2013 by adding from 0 to 11 months to the starting date).
with all_months as (
select '2013-01-01'::date + (n || ' months')::interval months
from generate_series(0, 11) n
)
insert into valid_expiration_dates
select to_char(months, 'MM') || '/' || to_char(months, 'YY') exp_date
from all_months;
Now, in your data table, create a char(5) column, and set a foreign key reference from it to valid_expiration_dates.exp_date.
While you're busy with this, think hard about whether "exp_month" might be a better name for that column than "exp_date". (I think it would.)
As another idea you could essentially create some brief utilities to do this for you using int[]:
CREATE OR REPLACE FUNCTION exp_valid(int[]) returns bool LANGUAGE SQL IMMUTABLE as
$$
SELECT $1[1] <= 12 AND (select count(*) = 2 FROM unnest($1));
$$;
CREATE OR REPLACE FUNCTION first_invalid_day(int[]) RETURNS date LANGUAGE SQL IMMUTABLE AS
$$
SELECT (to_date($1[2]::text || $1[1]::text, CASE WHEN $1[2] < 100 THEN 'YYMM' ELSE 'YYYYMM' END) + '1 month'::interval)::date;
$$;
These work:
postgres=# select exp_valid('{04,13}');
exp_valid
-----------
t
(1 row)
postgres=# select exp_valid('{13,04}');
exp_valid
-----------
f
(1 row)
postgres=# select exp_valid('{04,13,12}');
exp_valid
-----------
f
(1 row)
Then we can convert these into a date:
postgres=# select first_invalid_day('{04,13}');
first_invalid_day
-------------------
2013-05-01
(1 row)
This use of arrays does not violate any normalization rules because the array as a whole represents a single value in its domain. We are storing two integers representing a single date. '{12,2}' is December of 2002, while '{2,12}' is Feb of 2012. Each represents a single value of the domain and is therefore perfectly atomic.

Create date efficiently

On Pavel's page is the following function:
CREATE OR REPLACE FUNCTION makedate(year int, dayofyear int)
RETURNS date AS $$
SELECT (date '0001-01-01' + ($1 - 1) * interval '1 year' + ($2 - 1) * interval '1 day'):: date
$$ LANGUAGE sql;
I have the following code:
makedate(y.year,1)
What is the fastest way in PostgreSQL to create a date for January 1st of a given year?
Pavel's function would lead me to believe it is:
date '0001-01-01' + y.year * interval '1 year' + interval '1 day';
My thought would be more like:
to_date( y.year||'-1-1', 'YYYY-MM-DD');
Am looking for the fastest way using PostgreSQL 8.4. (The query that uses the date function can select between 100,000 and 1 million records, so it needs speed.)
Thank you!
I would just use the following, given that year is a variable holding the year, instead of using a function:
(year || '-01-01')::date
Btw. I can't believe that this conversion is your bottleneck. But maybe you should have a look at generate_series here (I don't know your usecase).
select current_date + s.a as dates from generate_series(0,14,7) as s(a);
dates
------------
2004-02-05
2004-02-12
2004-02-19
(3 rows)
Using to_date() is even simpler than you expect:
> select to_date('2008','YYYY');
to_date
------------
2008-01-01
(1 row)
> select to_date(2008::text,'YYYY');
to_date
------------
2008-01-01
(1 row)
Note that you still have to pass the year as a string, but no concatenation is needed.
As suggested by Daniel, in the unlikely case that this conversion is a bottleneck, you might prefer to precompute the function and store in a table. Eg:
select ynum, to_date( ynum ||'-01-01', 'YYYY-MM-DD') ydate
from generate_series(2000,2009) as ynum;
If there are a few years (and hence no need of indexes), you might even create the table dinamically for the scope of each query, with the new WITH.