Why won't this simple SQL statement work? - sql

dateposted is a MySQL TIMESTAMP column:
SELECT *
FROM posts
WHERE dateposted > NOW() - 604800
...SHOULD, if I am not mistaken, return rows where dateposted was in the last week. But it returns only posts less than roughly one day old. I was under the impression that TIMESTAMP used seconds?
IE: 7 * 3600 * 24 = 604800

Use:
WHERE dateposted BETWEEN DATE_ADD(NOW(), INTERVAL -7 DAY) AND NOW()

That is because now() is implicitly converted into a number from timestamp and mysql conversion rules create a number like YYYYMMDDHHMMSS.uuuuuu
from mysql docs:
mysql> SELECT NOW();
-> '2007-12-15 23:50:26'
mysql> SELECT NOW() + 0;
-> 20071215235026.000000

Internally perhaps. The way to do this is the date math functions. So it would be:
SELECT * FROM posts WHERE dateposted > DATE_ADD(NOW(), INTERVAL -7 DAY)
I think there is a DATE_SUB, I'm just used to using ADD everywhere.

No, you can't implicitly use integer arithmetic with TIMESTAMP, DATETIME, and other date-related data types. You're thinking of the UNIX timestamp format, which is an integer number of seconds since 1/1/1970.
You can convert SQL data types to a UNIX timestamp in MySQL and then use arithmetic:
SELECT * FROM posts WHERE UNIX_TIMESTAMP(dateposted)+604800 > NOW()+0;
NB: adding zero to NOW() makes it return a numeric value instead of a string value.
update: Okay, I'm totally wrong with the above query. Converting NOW() to a numeric output doesn't produce a number that can be compared to UNIX timestamps. It produces a number, but the number doesn't count seconds or anything else. The digits are just YYYYMMDDHHMMSS strung together.
Example:
CREATE TABLE foo (
id SERIAL PRIMARY KEY,
dateposted TIMESTAMP
);
INSERT INTO foo (dateposted) VALUES ('2009-12-4'), ('2009-12-11'), ('2009-12-18');
SELECT * FROM foo;
+----+---------------------+
| id | dateposted |
+----+---------------------+
| 1 | 2009-12-04 00:00:00 |
| 2 | 2009-12-11 00:00:00 |
| 3 | 2009-12-18 00:00:00 |
+----+---------------------+
SELECT *, UNIX_TIMESTAMP(dateposted) AS ut, NOW()-604800 AS wk FROM foo
+----+---------------------+------------+-----------------------+
| id | dateposted | ut | wk |
+----+---------------------+------------+-----------------------+
| 1 | 2009-12-04 00:00:00 | 1259913600 | 20091223539359.000000 |
| 2 | 2009-12-11 00:00:00 | 1260518400 | 20091223539359.000000 |
| 3 | 2009-12-18 00:00:00 | 1261123200 | 20091223539359.000000 |
+----+---------------------+------------+-----------------------+
It's clear that the numeric values are not comparable. However, UNIX_TIMSTAMP() can also convert numeric values in that format as it can convert a string representation of a timestamp:
SELECT *, UNIX_TIMESTAMP(dateposted) AS ut, UNIX_TIMESTAMP(NOW())-604800 AS wk FROM foo
+----+---------------------+------------+------------+
| id | dateposted | ut | wk |
+----+---------------------+------------+------------+
| 1 | 2009-12-04 00:00:00 | 1259913600 | 1261089774 |
| 2 | 2009-12-11 00:00:00 | 1260518400 | 1261089774 |
| 3 | 2009-12-18 00:00:00 | 1261123200 | 1261089774 |
+----+---------------------+------------+------------+
Now one can run a query with an expression comparing them:
SELECT * FROM foo WHERE UNIX_TIMESTAMP(dateposted) > UNIX_TIMESTAMP(NOW())-604800
+----+---------------------+
| id | dateposted |
+----+---------------------+
| 3 | 2009-12-18 00:00:00 |
+----+---------------------+
But the answer given by #OMGPonies is still better, because this expression in my query probably can't make use of an index. I'm just offering this as an explanation of how the TIMESTAMP and NOW() features work.

Try this query:
SELECT * FROM posts WHERE DATE_SUB(CURDATE(),INTERVAL 7 DAY) < dateposted;
I am assuming that you are using mySQL.

Related

Date arithmetic on values of type DATE without resulting in TIMESTAMP

Let's say I am doing similar query (where due_date is of type date) :
SELECT
(due_date + (7 * INTERVAL '1 DAY')) AS due_date_mod
FROM test_table
The resulting due_date_mod is type timestamp.
This makes sense as the result of the operation should be one type regardless of specific values and interval can have hours/minutes/seconds.
But is there a way to add days/months/years to a date without the result being time stamp and also obviously without casting? Or is casting the only way?
I know I can add days by using:
SELECT
(due_date + INTEGER '7') AS due_date_mod
And the result is type date.
But can I do something similar for months or years (without converting them to days)?
EDIT:
There seems to be no solution satisfying the requirements of the question. Proper way to get the required results is in the marked answer.
You already found that you can add integer to date to add days, returning date:
SELECT due_date + 7 AS due_date_mod -- type date
FROM test_table;
(Since date is an integer quantity inside, it's dead simple and cheap to add integer to it.)
As you can see in the table Date/Time Operators in the manual, there is no other operator that would return date (and no function, either).
And the unit "months" must be interpreted relative to the left operand (as months differ in their number of days). So you cannot simply extract the number of days from your months. You must use date + interval to get it right. Just add a cast, it's simple and very cheap:
SELECT (due_date + interval '3 months')::date AS due_date_mod
FROM test_table;
You are aware that (7 * interval '1 mon') = interval '7 mon', right?
Originally I misinterpreted the question so what is below is examples (including output) of what #mcc had already discovered. I'll leave this answer here as they may be useful to others looking for examples of date, timestamp, and interval arithmetic.
Adding a date to an interval:
select '2021-08-11 12:23:00'::date + '2 months'::interval;
?column?
---------------------
2021-09-11 00:00:00
Or, if the field is a timestamp you can to cast it to date, but you still get a datetime answer:
select ('2021-08-11 12:23:00'::timestamp)::date + '2 years'::interval;
?column?
---------------------
2023-08-11 00:00:00
As the above examples show, when adding an interval to a date or timestamp the results is always a timestamp. If you require the results to be a date you can cast after adding the interval:
select ('2021-08-11 12:23:00'::timestamp + '60 days'::interval)::date;
date
------------
2021-10-10
I've used intervals of days, months, and years to demonstrate how each time period can be added.
reference: https://www.postgresql.org/docs/current/catalog-pg-operator.html
SELECT
oprname,
p1.typname AS left_type,
p2.typname AS right_type,
p3.typname AS result_type,
oprcode
FROM
pg_operator po
JOIN pg_type p1 ON p1.oid = po.oprleft
JOIN pg_type p2 ON p2.oid = po.oprright
JOIN pg_type p3 ON p3.oid = po.oprresult
WHERE
oprname = '+'
AND p1.typname = 'date'::name;
So there is a row like :
oprname | left_type | right_type | result_type | oprcode
---------+-----------+------------+-------------+------------------
+ | date | int4 | date | date_pli
That's why select now()::date + 1; will work.
But is there a way to add days/months/years to a date without the
result being time stamp and also obviously without casting? Or is
casting the only way?
"add days/months/years" Means:
Is there a operator that right side (or left side) is interval data type then the return is date data type.
Does it exist? we can query the catalog schema and find out.
SELECT
oprname,
p1.typname AS left_type, --left side of a operator data type
p2.typname AS right_type, --right side of a operator data type
p3.typname AS result_type, --return data type
oprcode
FROM
pg_operator po
JOIN pg_type p1 ON p1.oid = po.oprleft
JOIN pg_type p2 ON p2.oid = po.oprright
JOIN pg_type p3 ON p3.oid = po.oprresult
WHERE
p1.typname = 'interval'::name
OR p2.typname = 'interval'::name;
oprname | left_type | right_type | result_type | oprcode
---------+-------------+-------------+-------------+-------------------------
+ | date | interval | timestamp | date_pl_interval
- | date | interval | timestamp | date_mi_interval
+ | timestamptz | interval | timestamptz | timestamptz_pl_interval
- | timestamptz | interval | timestamptz | timestamptz_mi_interval
= | interval | interval | bool | interval_eq
<> | interval | interval | bool | interval_ne
< | interval | interval | bool | interval_lt
<= | interval | interval | bool | interval_le
> | interval | interval | bool | interval_gt
>= | interval | interval | bool | interval_ge
+ | interval | interval | interval | interval_pl
- | interval | interval | interval | interval_mi
* | interval | float8 | interval | interval_mul
* | float8 | interval | interval | mul_d_interval
/ | interval | float8 | interval | interval_div
+ | time | interval | time | time_pl_interval
- | time | interval | time | time_mi_interval
+ | timetz | interval | timetz | timetz_pl_interval
- | timetz | interval | timetz | timetz_mi_interval
+ | interval | time | time | interval_pl_time
+ | timestamp | interval | timestamp | timestamp_pl_interval
- | timestamp | interval | timestamp | timestamp_mi_interval
+ | interval | date | timestamp | interval_pl_date
+ | interval | timetz | timetz | interval_pl_timetz
+ | interval | timestamp | timestamp | interval_pl_timestamp
+ | interval | timestamptz | timestamptz | interval_pl_timestamptz
<-> | interval | interval | interval | interval_dist
(27 rows)
As you can see the query column return type is {timestamp,timestamptz, bool, interval,time,timetz}. So it cannot return date.

Aggregate results split by day

I'm trying to write a query that returns summarised data, per day, over many day's of data.
For example
| id | user_id | start
|----|---------|------------------------------
| 1 | 1 | 2020-02-01T17:35:37.242+00:00
| 2 | 1 | 2020-02-01T13:25:21.344+00:00
| 3 | 1 | 2020-01-31T16:42:51.344+00:00
| 4 | 1 | 2020-01-30T06:44:55.344+00:00
The outcome I'm hoping for is a function that I can pass in a the userid and timezone, or UTC offset, and get out:
| day | count |
|---------|-------|
| 1/2/20 | 2 |
| 31/1/20 | 1 |
| 30/1/20 | 7 |
Where the count is all the rows that have a start time falling between 00:00:00.0000 and 23:59:59.9999 on each day - taking into consideration the supplied UTC offset.
I don't really know where to start writing a query like this, and I the fact I can't even picture where to start feels like a big gap in my SQL thinking. How should I approach something like this?
You can use:
select date_trunc('day', start) as dte, count(*)
from t
where userid = ?
group by date_trunc('day', start)
order by dte;
If you want to handle an additional offset, build that into the query:
select dte, count(*)
from t cross join lateral
(values (date_trunc('day', start + ? * interval '1 hour'))) v(dte)
where userid = ?
group by v.dte
order by v.dte;

Extract date from datetime and calculate total minutes from duration

I have a table in Postgres as follows:
| id | start_time | end_time | duration |
|----|--------------------------|--------------------------|----------|
| 1 | 2018-05-11T00:00:20.631Z | 2018-05-11T01:03:14.496Z | 1:02:54 |
| 2 | 2018-05-11T00:00:04.877Z | 2018-05-11T00:00:14.641Z | 0:00:10 |
| 3 | 2018-05-11T01:03:28.063Z | 2018-05-11T01:04:36.410Z | 0:01:08 |
| 4 | 2018-05-11T00:00:20.631Z | 2018-05-11T02:03:14.496Z | 2:02:54 |
start_time and end_time are stored as varchar. Format is 'yyyy-mm-dd hh24:mi:ss.ms' (ISO format).
duration has been calculated as end_time - start_time. Format is hh:mi:ss.
I need result table output as follows:
| id | start_time | end_time | duration | start | end | duration_minutes |
|----|--------------------------|--------------------------|----------|-----------|-----------|------------------|
| 1 | 2018-05-11T00:00:20.631Z | 2018-05-11T01:03:14.496Z | 1:02:54 | 5/11/2018 | 5/11/2018 | 62 | -- (60+2)
| 2 | 2018-05-11T00:00:04.877Z | 2018-05-11T00:00:14.641Z | 0:00:10 | 5/11/2018 | 5/11/2018 | 0 |
| 3 | 2018-05-11T01:03:28.063Z | 2018-05-11T01:04:36.410Z | 0:01:08 | 5/11/2018 | 5/11/2018 | 1 |
| 4 | 2018-05-11T00:00:20.631Z | 2018-05-11T02:03:14.496Z | 2:02:54 | 5/11/2018 | 5/11/2018 | 122 | -- (2X60 +2)
start and end need to contain only the mm/dd/yyyy portion of start_time and end_time respectively.
duration_minutes should calculate total duration in minutes (eg, if duration is 1:02:54, duration in minutes should be 62 which is 60+2)
How can I do this using SQL?
Based in varchar input, this query produces your desired result, exactly:
SELECT *
, to_char(start_time::timestamp, 'FMMM/DD/YYYY') AS start
, to_char(end_time::timestamp, 'FMMM/DD/YYYY') AS end
, extract(epoch FROM duration::interval)::int / 60 AS duration_minutes
FROM tbl;
Major points:
Use timestamp and interval instead of varchar to begin with.
Or do not store the functionally dependent column duration at all. It can cheaply be computed on the fly.
For display / a particular text representation use to_char().
Be explicit and do not rely on locale settings that may change from session to session.
The FM pattern modifier is for (quoting the manual):
fill mode (suppress leading zeroes and padding blanks)
extract (epoch FROM interval_tpe) produces the number of contained seconds. You want to truncate fractional minutes? Integer division does just that, so cast to int like demonstrated. Related:
Get difference in minutes between times with timezone
The following appears to do what you want:
select v.starttime::timestamp::date, v.endtime::date,
extract(epoch from v.endtime::timestamp - v.starttime::timestamp)/60
from (values ('2018-05-11T00:00:20.631Z', '2018-05-11T01:03:14.496Z')) v(starttime, endtime)
If you want the dates in a particular format, then use to_char().

How can I calculate time difference(HH:MM:SS) from date_time (ISO format) in Postgres

I have a table T1 in Postgres which is as follows:
| event | Date_Time |
|-------|--------------------------|
| start | 2018-04-30T06:09:30.986Z |
| run | 2018-04-30T10:37:38.044Z |
| end | 2018-04-30T11:39:38.044Z |
The Date_Time is in ISO format (stored as varchar) and I need to calculate the difference in Date_Time so that my output is as follows:
| event | Date_Time | Time_Difference |
|-------|--------------------------|-----------------|
| start | 2018-04-30T06:09:30.986Z | 4:28:08 |
| run | 2018-04-30T10:37:38.044Z | 1:02:00 |
| end | 2018-04-30T11:39:38.044Z | |
(10: 37: 38 - 06: 09: 30 = 4:28:08)
How can I do this using SQL?
Unrelated to the question, but: you should never store timestamp (or date or number) values in a varchar.
You first have to convert the varchar value to a timestamp. If the values are indeed formatted correctly, you can simply cast them: Date_Time::timestamp - or maybe to a timestamptz.
As far as I can tell, you want the different to the next row in your result. This can be achieved with the window function lead()
select event,
Date_Time,
date_time::timestamp - lead(date_time::timestamp) over (order by date_time::timestamp) as time_difference
from the_table
order by date_time;
The result of subtracting one timestamp from another is an interval you can format if you want.

Select row with timestamp nearest to, but not later than, now

Using Postgres 9.4, I am trying to select a single row from from a table that contains data nearest to, but not before, the current system time. The datetime colum is a timestamp without time zone data type, and the data is in the same timezone as the server. The table structure is:
uid | datetime | date | day | time | predictionft | predictioncm | highlow
-----+---------------------+------------+-----+----------+--------------+--------------+---------
1 | 2015-12-31 03:21:00 | 2015/12/31 | Thu | 03:21 AM | 5.3 | 162 | H
2 | 2015-12-31 09:24:00 | 2015/12/31 | Thu | 09:24 AM | 2.4 | 73 | L
3 | 2015-12-31 14:33:00 | 2015/12/31 | Thu | 02:33 PM | 4.4 | 134 | H
4 | 2015-12-31 21:04:00 | 2015/12/31 | Thu | 09:04 PM | 1.1 | 34 | L
Query speed is not a worry since the table contains ~1500 rows.
For clarity, if the current server time was 2015-12-31 14:00:00, the row returned should be 3 rather than 2.
EDIT:
The solution, based on the accepted answer below, was:
select *
from myTable
where datetime =
(select min(datetime)
from myTable
where datetime > now());
EDIT 2: Clarified question.
You can also use this. This will be faster. But it wont make much difference if you have few rows.
select * from table1
where datetime >= current_timestamp
order by datetime
limit 1
SQLFiddle Demo
The general idea follows. You can adjust it for postgresql.
select fields
from yourTable
where datetimeField =
(select min(datetimeField)
from yourTable
where datetimeField > current_timestamp)
Another approach other than the answers given is to use a window function first_value
select id, first_value(dt) over (order by dt)
from test
where dt >= current_timestamp
limit 1
See it working here: http://sqlfiddle.com/#!15/0031c/12