PostgreSQL: find number of consecutive days up until now - sql

Given a bunch of records (which represent checkins in my app) with a timestamp field, what would be a good way to determine the current streak of consecutive checkins?
In other words, with the checkins sorted by checkin time descending, how many records are there until a user missed a day?
Currently I'm using this technique:
SELECT distinct(uca.created_at::date) as created_at
FROM user_challenge_activities as uca INNER JOIN user_challenges as uc
ON user_challenge_id = uc.ID WHERE uc.user_id = #{user.id}
order by (uca.created_at::date) DESC;
...where I cast the checkin timestamps to a date (to end up with e.g. 2012-03-20), then in code, go through the records and increment a counter until the date between the record and the next record is greater than 1 day.
However, this approach seems clumsy to me, and it seems like the sort of thing that Postgres would excel at.
So is there in fact a better way to accomplish this?

with t as (
SELECT distinct(uca.created_at::date) as created_at
FROM user_challenge_activities as uca
INNER JOIN user_challenges as uc ON user_challenge_id = uc.ID
WHERE uc.user_id = #{user.id}
)
select count(*)
from t
where t.create_at > (
select d.d
from generate_series('2010-01-01'::date, CURRENT_DATE, '1 day') d(d)
left outer join t on t.created_at = d.d::date
where t.created_at is null
order by d.d desc
limit 1
)

let's try this again. will only generate series for the necessary range:
SELECT count(distinct(uca.created_at::date)) FROM user_challenge_activities as uca
JOIN
(SELECT generate_series(max(series_date),
(select max(user_challenge_activities.created_at)
FROM user_challenge_activities), '1 day') as datez
FROM
(SELECT generate_series(min(user_challenge_activities.created_at::date),
max(user_challenge_activities.created_at), '1 day')::date
as series_date
FROM user_challenge_activities) x
LEFT JOIN user_challenge_activities
ON (user_challenge_activities.created_at::date = x.series_date)
WHERE created_at IS NULL) d ON d.datez = uca.created_at::date
INNER JOIN user_challenges as uc ON user_challenge_id = uc.ID
WHERE uc.user_id = #{user.id};

Related

Compare dates from 2 separate sql queries

For an application we have, it is incorrectly listing the last access date and I'm trying tp get it updated.
To see what the date it is currently showing I can do;
select LoginID, ActionDate
From LoginHistory
Now to see what the date should actually be I'm doing:
select lg.UserName, lg.LoginID, lg.Activated, id.Title, max(ih.ActionedDate) as LastAccessed
from Login lg
left join (
ItemLoginAssociation la
join ItemView id
on id.ItemId = la.ItemID
) on la.LoginID = lg.LoginID
left join ItemHistory ih
on id.ItemId = ih.ActionedBy and id.VersionRank =1
where action= 'View'
group by lg.UserName, lg.LoginID, lg.Activated, id.Title
order by LastAccessed
What I would like to do is compare the ActionDate from the first query and the LastAccessed date from the second query by the loginID and if the date in the second query is more recent than the date in the first one, replace the first query date with the second query date if that makes sense?
How can I go about doing that?
You can left join your second query:
select lh.LoginID, lh.ActionDate, x.LastAccessed
From LoginHistory lh
left join (
select lg.UserName, lg.LoginID, lg.Activated, id.Title, max(ih.ActionedDate) as LastAccessed
from Login lg
left join (
ItemLoginAssociation la
join ItemView id
on id.ItemId = la.ItemID
) on la.LoginID = lg.LoginID
left join ItemHistory ih
on id.ItemId = ih.ActionedBy and id.VersionRank =1
where action= 'View'
group by lg.UserName, lg.LoginID, lg.Activated, id.Title
-- order by LastAccessed
) x on x.loginID = lh.LoginID
The order by is commented because ordering a sub-query is pretty useless.
P.S. If both queries work on your DBMS, this query should work too, because it's pretty standard SQL.

Oracle/SQL: How can I show dates and counts even when count is zero?

So, I need to query a table and show counts per day even if that count is zero. I tried something like the below, but it doesn't show the days that have a count of zero. Anyone have any other ideas? Using Oracle, BTW. Many thanks!!
with the_dates as (
select to_date('080114','MMDDYY') + level - 1 as the_date
from dual
connect by level <= to_date('011716', 'MMDDYY')
- to_date('080114', 'MMDDYY') + 1
)
select distinct trunc(a.the_date), count(*)
from the_dates a
left outer join TableFoo f on a.the_date = to_date(admit_date, 'MMDDYYYY')
where f.customer_num = 10
group by trunc(a.the_date)
order by trunc(a.the_date);
The problem is that your where clause turns the left join into an inner join. So:
with . . .
select trunc(a.the_date), count(f.customer_num)
from the_dates a left outer join
TableFoo f
on a.the_date = to_date(admit_date, 'MMDDYYYY') and
f.customer_num = 10
group by trunc(a.the_date)
order by trunc(a.the_date);
Also, select distinct is almost never needed when using group by (and certainly not in this case).

SQL Query For Customers Not Used For Last 3 Years

I THINK I'm having some trouble.
I'm trying to query 2 tables for customers that haven't been used on the table for the last 3 Years. The data consists of data ranging for 7+ years, so customers are used multiple times.
I think the issue with my current query: It's finding data of customers not used in the last 3 years... but it's not accounting for if there is also data of the customer within the last 3 years as well.
Can someone possibly help me? I'm guessing the answer is to use only the data of the customer with the latest date and ignore previous data.
SELECT DISTINCT
tbl_Customer.CustomerID
, tbl_Customer.CustomerName
, Table1.ImportDate
, Table2.ImportDate
FROM
tbl_Customer
LEFT JOIN
Table1 ON tbl_Customer.CustomerName = Table1.CustomerName
LEFT JOIN
Table2 ON tbl_Customer.CustomerName = Table2.CustomerName
WHERE
(((DateAdd("yyyy", 3, [Table2].[ImportDate])) < Now())
AND
((DateAdd("yyyy", 3, [Table1].[ImportDate])) < Now()))
ORDER BY
Table1.ImportDate DESC,
Table2.ImportDate DESC;
The core problem with the initial query is that, for no imports (which will happen for "no order" customers) the condition
DateAdd("yyyy", 3, ImportDate) < Now()
--> DateAdd("yyyy", 3, NULL) < Now()
--> NULL < Now()
--> NULL (or not true)
is not true. A simple fix is to add a guard
([Table1].[ImportDate] IS NULL
OR DateAdd("yyyy", 3, [Table1].[ImportDate]) < Now())
around such expressions or to coalesce the NULL value before using it.
The ordering will also be wrong, as that means order by one value and then the other, not "by the greater of both" values. Compare with
ORDER BY
IIF(Table1.ImportDate > Table2.ImportDate, Table1.ImportDate, Table2.ImportDate)
However, I would use a LEFT JOIN on customers/orders, GROUP BY with a MAX on the order dates. Then you can use that result (as a derived subquery) to complete the query asked fairly trivially.
SELECT
c.CustomerID
, MAX(o.ImportDate) as lastImport
FROM tbl_Customer as c
-- The UNION is to simply "normalize" to a single table.
-- (Also, shouldn't the join be on a customer "ID"?)
LEFT JOIN (
SELECT CustomerName, ImportDate from Table1
UNION
SELECT CustomerName, ImportDate from Table2) as o
ON c.CustomerName = o.CustomerName
GROUP BY c.CustomerID
Then,
SELECT s.CustomerID
FROM (thatSubQuery) as s
WHERE
-- no orders
s.lastImport IS NULL
-- only old orders
OR DateAdd("yyyy", 3, s.lastImport) < Now()
ORDER BY s.lastImport
(YMMV with MS Access, this will work in a "real" database ;-)
SELECT DISTINCT
tbl_Customer.CustomerID,
tbl_Customer.CustomerName,
Table1.ImportDate,
Table2.ImportDate
FROM (tbl_Customer
LEFT JOIN Table1
ON tbl_Customer.CustomerName = Table1.CustomerName)
LEFT JOIN Table2
ON tbl_Customer.CustomerName = Table2.CustomerName
WHERE DateAdd("yyyy",3,[Table2].[ImportDate]) < Now()
AND DateAdd("yyyy",3,[Table1].[ImportDate]) < Now()
AND tbl_Customer.CustomerID NOT IN (
SELECT DISTINCT
tbl_Customer.CustomerID,
FROM (tbl_Customer
LEFT JOIN Table1
ON tbl_Customer.CustomerName = Table1.CustomerName)
LEFT JOIN Table2
ON tbl_Customer.CustomerName = Table2.CustomerName
WHERE DateAdd("yyyy",3,[Table2].[ImportDate]) >= Now()
AND DateAdd("yyyy",3,[Table1].[ImportDate]) >= Now()
)
ORDER BY Table1.ImportDate DESC , Table2.ImportDate DESC;
Based on what I can infer from your query about your data structure, I think you want something like this:
DECLARE #CutOff DateTime
SET #CutOff = DATEADD(y, -3 GETDATE())
SELECT tbl_Customer.CustomerID, tbl_Customer.CustomerName
WHERE (CustomerName IN
(SELECT CustomerName FROM Table1 WHERE ImportDate < #CutOff))
OR
(CustomerName IN
(SELECT CustomerName FROM Table2 WHERE ImportDate < #CutOff)))
AND CustomerName NOT IN
(SELECT CustomerName FROM Table1 WHERE ImportDate > #CutOff)
AND CustomerName NOT IN
(SELECT CustomerName FROM Table2 WHERE ImportDate > #CutOff)

Limit join to one row

I have the following query:
SELECT sum((select count(*) as itemCount) * "SalesOrderItems"."price") as amount, 'rma' as
"creditType", "Clients"."company" as "client", "Clients".id as "ClientId", "Rmas".*
FROM "Rmas" JOIN "EsnsRmas" on("EsnsRmas"."RmaId" = "Rmas"."id")
JOIN "Esns" on ("Esns".id = "EsnsRmas"."EsnId")
JOIN "EsnsSalesOrderItems" on("EsnsSalesOrderItems"."EsnId" = "Esns"."id" )
JOIN "SalesOrderItems" on("SalesOrderItems"."id" = "EsnsSalesOrderItems"."SalesOrderItemId")
JOIN "Clients" on("Clients"."id" = "Rmas"."ClientId" )
WHERE "Rmas"."credited"=false AND "Rmas"."verifyStatus" IS NOT null
GROUP BY "Clients".id, "Rmas".id;
The problem is that the table "EsnsSalesOrderItems" can have the same EsnId in different entries. I want to restrict the query to only pull the last entry in "EsnsSalesOrderItems" that has the same "EsnId".
By "last" entry I mean the following:
The one that appears last in the table "EsnsSalesOrderItems". So for example if "EsnsSalesOrderItems" has two entries with "EsnId" = 6 and "createdAt" = '2012-06-19' and '2012-07-19' respectively it should only give me the entry from '2012-07-19'.
SELECT (count(*) * sum(s."price")) AS amount
, 'rma' AS "creditType"
, c."company" AS "client"
, c.id AS "ClientId"
, r.*
FROM "Rmas" r
JOIN "EsnsRmas" er ON er."RmaId" = r."id"
JOIN "Esns" e ON e.id = er."EsnId"
JOIN (
SELECT DISTINCT ON ("EsnId") *
FROM "EsnsSalesOrderItems"
ORDER BY "EsnId", "createdAt" DESC
) es ON es."EsnId" = e."id"
JOIN "SalesOrderItems" s ON s."id" = es."SalesOrderItemId"
JOIN "Clients" c ON c."id" = r."ClientId"
WHERE r."credited" = FALSE
AND r."verifyStatus" IS NOT NULL
GROUP BY c.id, r.id;
Your query in the question has an illegal aggregate over another aggregate:
sum((select count(*) as itemCount) * "SalesOrderItems"."price") as amount
Simplified and converted to legal syntax:
(count(*) * sum(s."price")) AS amount
But do you really want to multiply with the count per group?
I retrieve the the single row per group in "EsnsSalesOrderItems" with DISTINCT ON. Detailed explanation:
Select first row in each GROUP BY group?
I also added table aliases and formatting to make the query easier to parse for human eyes. If you could avoid camel case you could get rid of all the double quotes clouding the view.
Something like:
join (
select "EsnId",
row_number() over (partition by "EsnId" order by "createdAt" desc) as rn
from "EsnsSalesOrderItems"
) t ON t."EsnId" = "Esns"."id" and rn = 1
this will select the latest "EsnId" from "EsnsSalesOrderItems" based on the column creation_date. As you didn't post the structure of your tables, I had to "invent" a column name. You can use any column that allows you to define an order on the rows that suits you.
But remember the concept of the "last row" is only valid if you specifiy an order or the rows. A table as such is not ordered, nor is the result of a query unless you specify an order by
Necromancing because the answers are outdated.
Take advantage of the LATERAL keyword introduced in PG 9.3
left | right | inner JOIN LATERAL
I'll explain with an example:
Assuming you have a table "Contacts".
Now contacts have organisational units.
They can have one OU at a point in time, but N OUs at N points in time.
Now, if you have to query contacts and OU in a time period (not a reporting date, but a date range), you could N-fold increase the record count if you just did a left join.
So, to display the OU, you need to just join the first OU for each contact (where what shall be first is an arbitrary criterion - when taking the last value, for example, that is just another way of saying the first value when sorted by descending date order).
In SQL-server, you would use cross-apply (or rather OUTER APPLY since we need a left join), which will invoke a table-valued function on each row it has to join.
SELECT * FROM T_Contacts
--LEFT JOIN T_MAP_Contacts_Ref_OrganisationalUnit ON MAP_CTCOU_CT_UID = T_Contacts.CT_UID AND MAP_CTCOU_SoftDeleteStatus = 1
--WHERE T_MAP_Contacts_Ref_OrganisationalUnit.MAP_CTCOU_UID IS NULL -- 989
-- CROSS APPLY -- = INNER JOIN
OUTER APPLY -- = LEFT JOIN
(
SELECT TOP 1
--MAP_CTCOU_UID
MAP_CTCOU_CT_UID
,MAP_CTCOU_COU_UID
,MAP_CTCOU_DateFrom
,MAP_CTCOU_DateTo
FROM T_MAP_Contacts_Ref_OrganisationalUnit
WHERE MAP_CTCOU_SoftDeleteStatus = 1
AND MAP_CTCOU_CT_UID = T_Contacts.CT_UID
/*
AND
(
(#in_DateFrom <= T_MAP_Contacts_Ref_OrganisationalUnit.MAP_KTKOE_DateTo)
AND
(#in_DateTo >= T_MAP_Contacts_Ref_OrganisationalUnit.MAP_KTKOE_DateFrom)
)
*/
ORDER BY MAP_CTCOU_DateFrom
) AS FirstOE
In PostgreSQL, starting from version 9.3, you can do that, too - just use the LATERAL keyword to achieve the same:
SELECT * FROM T_Contacts
--LEFT JOIN T_MAP_Contacts_Ref_OrganisationalUnit ON MAP_CTCOU_CT_UID = T_Contacts.CT_UID AND MAP_CTCOU_SoftDeleteStatus = 1
--WHERE T_MAP_Contacts_Ref_OrganisationalUnit.MAP_CTCOU_UID IS NULL -- 989
LEFT JOIN LATERAL
(
SELECT
--MAP_CTCOU_UID
MAP_CTCOU_CT_UID
,MAP_CTCOU_COU_UID
,MAP_CTCOU_DateFrom
,MAP_CTCOU_DateTo
FROM T_MAP_Contacts_Ref_OrganisationalUnit
WHERE MAP_CTCOU_SoftDeleteStatus = 1
AND MAP_CTCOU_CT_UID = T_Contacts.CT_UID
/*
AND
(
(__in_DateFrom <= T_MAP_Contacts_Ref_OrganisationalUnit.MAP_KTKOE_DateTo)
AND
(__in_DateTo >= T_MAP_Contacts_Ref_OrganisationalUnit.MAP_KTKOE_DateFrom)
)
*/
ORDER BY MAP_CTCOU_DateFrom
LIMIT 1
) AS FirstOE
Try using a subquery in your ON clause. An abstract example:
SELECT
*
FROM table1
JOIN table2 ON table2.id = (
SELECT id FROM table2 WHERE table2.table1_id = table1.id LIMIT 1
)
WHERE
...

select multiple tables in single sql query

The other threads about this didn't seem to help me. I want to select all the information from one table, but order them by a value in another table.
SELECT message,
DATE,
ip,
name,
website,
id
FROM guestbook_message
WHERE deleted = 0
AND DATE > Date_sub(Now(), interval 1 day)
ORDER BY DATE DESC
Except I need to ORDER BY 'votes' DESC; which is in another table called m_votes.
Is it possible to do this? I have read on another website that this query is impossible.
$query="SELECT g.message,
g.DATE,
g.ip,
g.name,
g.website,
g.id
FROM guestbook_message AS g
join m_votes AS v
ON g.id = v.vid
WHERE g.deleted = 0
AND v.messageid = $mid
AND g.DATE > Date_sub(Now(), interval 1 day)
ORDER BY SUM(v.votes) DESC;"
^^This doesn't work
You need a join:
SELECT
g.message,
g.date,
g.ip,
g.name,
g.website,
g.id
FROM guestbook_message AS g
LEFT JOIN m_votes AS v
ON g.id = v.message_id
WHERE g.deleted = 0
AND g.date > NOW() - INTERVAL 1 DAY
GROUP BY g.id
ORDER BY COUNT(v.message_id) DESC
I'm a bit of a beginner and to me this is a very hard query as it needs to find the SUM of 'votes' in m_votes and order by that. As well as get the information from the other query.
This is the query that gets the information about the message:
"SELECT message,
`date`,
ip,
name,
website,
id
FROM `guestbook_message`
WHERE deleted = 0
AND date > DATE_SUB(NOW(), INTERVAL 1 DAY)
ORDER BY `date` DESC";
And this is the query that gets the information about the votes:
"SELECT SUM(votes) as votes FROM m_votes WHERE messageid = $mid"
But I have no idea who I would put them into one query that will gather all the information from the first query, then ORDER them by votes.
You have to join the data, that is, you need to have the votes for each guestbook message.
Let's suppose for simplicity you have the following tables:
Message
----
id INT
messsageText VARCHAR(5000)
and
MessageVotes
------------
messageId INT (references the `id` column in table Message)
voteValue INT (suppose it can be +1 or -1, whatever)
votingIp VARCHAR(100)
Then you could do something like
SELECT
m.id,
m.messageText,
SUM(mv.voteValue) AS votes
FROM
Message AS m,
MessageVotes AS mv
WHERE
mv.messageId = m.id
GROUP BY
m.id, m.messageText /* here you need to place every field you `select` from Message */
ORDER BY
SUM(mv.voteValue) DESC
or even better:
SELECT
m.id,
m.messageText,
SUM(mv.voteValue) AS votes
FROM
Message AS m
LEFT JOIN MessageVotes AS mv ON mv.messageId = m.id
GROUP BY
m.id, m.messageText
ORDER BY
SUM(mv.voteValue) DESC
See:
mysql joins,
mysql left join
mysql group by - aggregate functions
Join Syntax
Please try below query, you missed group by clause.
SELECT g.message,
g.date,
g.ip,
g.name,
g.website,
g.id
FROM guestbook_message AS g
JOIN m_votes AS v ON g.id = v.vid
WHERE g.deleted = 0
AND v.messageid = $mid
AND g.date > DATE_SUB(NOW(), INTERVAL 1 DAY)
GROUP BY g.message,
g.date,
g.ip,
g.name,
g.website,
g.id
ORDER BY SUM(v.votes) DESC;