Get MAX inputted date within sub query - mysql

I have a script which is working but not as desired. My aim is to select the most recently inputted record on the plans database for each seller in the account_manager_sellers list.
The current issue with the script below is: It is returning the oldest record rather than the newest, for example: it is selecting a record in 2016 rather than one which has a timestamp in 2018. (eventually I need to change the WHERE clause to get all lastsale records before 2017-01-01.
Simple Database Samples.
plans AKA (sales list)
+----+------------------+-----------+
| id | plan_written | seller_id |
+----+------------------+-----------+
| 1 | 20/09/2016 09:12 | 123 |
| 2 | 22/12/2016 09:45 | 444 |
| 3 | 19/10/2016 09:07 | 555 |
| 4 | 02/10/2015 14:26 | 123 |
| 5 | 15/08/2016 11:06 | 444 |
| 6 | 16/08/2016 11:03 | 123 |
| 7 | 03/10/2016 10:15 | 555 |
| 8 | 28/09/2016 10:12 | 123 |
| 9 | 27/09/2016 15:12 | 444 |
+----+------------------+-----------+
account_manager_sellers (seller list)
+-----+----------+
| id | name |
+-----+----------+
| 123 | person 1 |
| 444 | person 2 |
| 555 | person 3 |
+-----+----------+
Current Code Used
SELECT p.plan_written, p.seller_id
FROM plans AS p NATURAL JOIN (
SELECT id, MAX(plan_written) AS lastsale
FROM plans
GROUP BY seller_id
) AS t
JOIN account_manager_sellers AS a ON a.id = p.seller_id
WHERE lastsale < "2018-05-08 00:00:00"
Summary
Using the code and example tables above, this code would return these 3 results, whilst we do expect 3 results, the MAX(plan_written) does not seem to have followed, my guess is that it is something to do with the GROUP clause, I am not sure if we can utilise an ORDER BY and LIMIT clause?
+--------------+------------------+
| seller_id | plan_written |
+--------------+------------------+
| 123 | 16/08/2016 11:03 |
| 444 | 15/08/2016 11:06 |
| 555 | 03/10/2016 10:15 |
+--------------+------------------+

The join condition in your query is off, and you should be restricting to the max date for each seller. Also, you don't need to join to the account_manager_sellers table to get your expected output:
SELECT p1.*
FROM plans p1
INNER JOIN
(
SELECT
seller_id, MAX(plan_written) AS max_plan_written
FROM plans
WHERE plan_written < '2018-05-08 00:00:00'
GROUP BY seller_id
) p2
ON p1.seller_id = p2.seller_id AND
p1.plan_written = p2.max_plan_written;

Related

SQL Query not having a value

I have a table as follows:
------+-------------+------------+----------------+------------------------------------------------+
| id | customer_id | date | action_type_id | details |
+------+-------------+------------+----------------+------------------------------------------------+
| 4225 | 324 | 2015-09-07 | 1 | Sent mail Malcolm Murrey |
| 6320 | 324 | 2017-05-08 | 3 | quotes for price. |
| 156 | 326 | 2013-07-25 | 3 | Site visit to price job |
| 943 | 326 | 2013-10-23 | 1 | Arranged visit for snags on panel |
| 1135 | 326 | 2013-11-28 | 1 | Arranged visit for site mod |
| 1930 | 326 | 2014-04-15 | 2 | Quoted for new HMI |
| 2644 | 326 | 2014-10-20 | 2 | Sent email about pending quote for HMI |
| 2821 | 326 | 2014-11-25 | 1 | Screen problem |
| 2184 | 328 | 2014-07-21 | 1 | Sent email detailing services |
+------+-------------+------------+----------------+------------------------------------------------+
I'm trying to find the customer_id of customers that don't have an action_type_id of 2. In this case 324 and 328 but just can't seem to figure it out.
I was thinking I could do something like group by customer_id not having action_type_id = 2.... any help much appreciated.
I would use aggregation:
select customerid
from t
group by customerid
having sum(action_type_id = 2) = 0;
I find the use of group by and having for this type of query to be very flexible on the conditions you might have (say you want 2 and 3, or not 2 but either 3 or 4).
You can do this by a left join to itself on records with an id of 2, then filtering on those without a match.
Something along the lines of
SELECT DISTINCT customer_id
FROM customer a
LEFT JOIN customer b
ON a.customer_id = b.customer_id
AND b.action_type_id = 2
WHERE b.customer_id IS NULL
Why not just use not exist:
select *
from table t
where not exists (select 1 from table
where customer_id = t.customer_id and
action_type_id = 2);
Why not this,
select distinct customer_id from customer where action_type_id <> 2
This should work:
SELECT customer_id FROM customers WHERE action_type_id <> 2

Query with dynamic date intervals

Given a statuses table that holds information about products availability, how do I select the date that corresponds to the 1st day in the latest 20 days that the product has been active?
Yes I know the question is hard to follow. I think another way to put it would be: I want to know how many times each product has been sold in the last 20 days that it was active, meaning the product could have been active for years, but I'd only want the sales count from the latest 20 days that it had a status of "active".
It's something easily doable in the server-side (i.e. getting any collection of products from the DB, iterating them, performing n+1 queries on the statuses table, etc), but I have hundreds of thousands of items so it's imperative to do it in SQL for performance reasons.
table : products
+-------+-----------+
| id | name |
+-------+-----------+
| 1 | Apple |
| 2 | Banana |
| 3 | Grape |
+-------+-----------+
table : statuses
+-------+-------------+---------------+---------------+
| id | name | product_id | created_at |
+-------+-------------+---------------+---------------+
| 1 | active | 1 | 2018-01-01 |
| 2 | inactive | 1 | 2018-02-01 |
| 3 | active | 1 | 2018-03-01 |
| 4 | inactive | 1 | 2018-03-15 |
| 6 | active | 1 | 2018-04-25 |
| 7 | active | 2 | 2018-03-01 |
| 8 | active | 3 | 2018-03-10 |
| 9 | inactive | 3 | 2018-03-15 |
+-------+-------------+---------------+---------------+
table : items (ordered products)
+-------+---------------+-------------+
| id | product_id | order_id |
+-------+---------------+-------------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 1 | 4 |
| 5 | 1 | 5 |
| 6 | 2 | 3 |
| 7 | 2 | 4 |
| 8 | 2 | 5 |
| 9 | 3 | 5 |
+-------+---------------+-------------+
table : orders
+-------+---------------+
| id | created_at |
+-------+---------------+
| 1 | 2018-01-02 |
| 2 | 2018-01-15 |
| 3 | 2018-03-02 |
| 4 | 2018-03-10 |
| 5 | 2018-03-13 |
+-------+---------------+
I want my final results to look like this:
+-------+-----------+----------------------+--------------------------------+
| id | name | recent_sales_count | date_to_start_counting_sales |
+-------+-----------+----------------------+--------------------------------+
| 1 | Apple | 3 | 2018-01-30 |
| 2 | Banana | 0 | 2018-04-09 |
| 3 | Grape | 1 | 2018-03-10 |
+-------+-----------+----------------------+--------------------------------+
So this is what I mean by latest 20 active days for e.g. Apple:
It was last activated at '2018-04-25'. That's 4 days ago.
Before that, it was inactive since '2018-03-15', so all these days until '2018-04-25' don't count.
Before that, it was active since '2018-03-01'. That's more 14 days until '2018-03-15'.
Before that, inactive since '2018-02-01'.
Finally, it was active since '2018-01-01', so it should only count the missing 2 days (4 + 14 + 2 = 20) backwards from '2018-02-01', resulting in date_to_start_counting_sales = '2018-01-30'.
With the '2018-01-30' date in hand, I'm then able to count Apple orders in the last 20 active days: 3.
Hope that makes sense.
Here is a fiddle with the data provided above.
I've got a standard SQL solution, that does not use any window function as you are on MySQL 5
My solution requires 3 stacked views.
It would have been better with a CTE but your version doesn't support it. Same goes for the stacked Views... I don't like to stack views and always try to avoid it, but sometimes you have no other choice, because MySQL doesn't accept subqueries in FROM clause for Views.
CREATE VIEW VIEW_product_dates AS
(
SELECT product_id, created_at AS active_date,
(
SELECT created_at
FROM statuses ti
WHERE name = 'inactive' AND ta.created_at < ti.created_at AND ti.product_id=ta.product_id
GROUP BY product_id
) AS inactive_date
FROM statuses ta
WHERE name = 'active'
);
CREATE VIEW VIEW_product_dates_days AS
(
SELECT product_id, active_date, inactive_date, datediff(IFNULL(inactive_date, SYSDATE()),active_date) AS nb_days
FROM VIEW_product_dates
);
CREATE VIEW VIEW_product_dates_days_cumul AS
(
SELECT product_id, active_date, ifnull(inactive_date,sysdate()) AS inactive_date, nb_days,
IFNULL((SELECT SUM(V2.nb_days) + V1.nb_days
FROM VIEW_product_dates_days V2
WHERE V2.active_date >= IFNULL(V1.inactive_date, SYSDATE()) AND V1.product_id=V2.product_id
),V1.nb_days) AS cumul_days
FROM VIEW_product_dates_days V1
);
The final view produce this :
| product_id | active_date | inactive_date | nb_days | cumul_days |
|------------|----------------------|----------------------|---------|------------|
| 1 | 2018-01-01T00:00:00Z | 2018-02-01T00:00:00Z | 31 | 49 |
| 1 | 2018-03-01T00:00:00Z | 2018-03-15T00:00:00Z | 14 | 18 |
| 1 | 2018-04-25T00:00:00Z | 2018-04-29T11:28:39Z | 4 | 4 |
| 2 | 2018-03-01T00:00:00Z | 2018-04-29T11:28:39Z | 59 | 59 |
| 3 | 2018-03-10T00:00:00Z | 2018-03-15T00:00:00Z | 5 | 5 |
So it aggregates all active periods of all products, it counts the number of days for each period, and the cumulative days of all past active periods since current date.
Then we can query this final view to get the desired date for each product. I set a variable for your 20 days, so you can change that number easily if you want.
SET #cap_days = 20 ;
SELECT PD.id, Pd.name,
SUM(CASE WHEN o.created_at > PD.date_to_start_counting_sales THEN 1 ELSE 0 END) AS recent_sales_count ,
PD.date_to_start_counting_sales
FROM
(
SELECT p.*,
(CASE WHEN LowerCap.max_cumul_days IS NULL
THEN ADDDATE(ifnull(HigherCap.min_inactive_date,sysdate()),(-#cap_days))
ELSE
CASE WHEN LowerCap.max_cumul_days < #cap_days AND HigherCap.min_inactive_date IS NULL
THEN ADDDATE(ifnull(LowerCap.max_inactive_date,sysdate()),(-LowerCap.max_cumul_days))
ELSE ADDDATE(ifnull(HigherCap.min_inactive_date,sysdate()),(LowerCap.max_cumul_days-#cap_days))
END
END) as date_to_start_counting_sales
FROM products P
LEFT JOIN
(
SELECT product_id, MAX(cumul_days) AS max_cumul_days, MAX(inactive_date) AS max_inactive_date
FROM VIEW_product_dates_days_cumul
WHERE cumul_days <= #cap_days
GROUP BY product_id
) LowerCap ON P.id=LowerCap.product_id
LEFT JOIN
(
SELECT product_id, MIN(cumul_days) AS min_cumul_days, MIN(inactive_date) AS min_inactive_date
FROM VIEW_product_dates_days_cumul
WHERE cumul_days > #cap_days
GROUP BY product_id
) HigherCap ON P.id=HigherCap.product_id
) PD
LEFT JOIN items i ON PD.id = i.product_id
LEFT JOIN orders o ON o.id = i.order_id
GROUP BY PD.id, Pd.name, PD.date_to_start_counting_sales
Returns
| id | name | recent_sales_count | date_to_start_counting_sales |
|----|--------|--------------------|------------------------------|
| 1 | Apple | 3 | 2018-01-30T00:00:00Z |
| 2 | Banana | 0 | 2018-04-09T20:43:23Z |
| 3 | Grape | 1 | 2018-03-10T00:00:00Z |
FIDDLE : http://sqlfiddle.com/#!9/804f52/24
Not sure which version of MySql you're working with, but if you can use 8.0, that version came out with a lot of functionality that makes things slightly more doable (CTE's, row_number(), partition, etc.).
My recommendation would be to create a view like in this DB-Fiddle Example, call the view on server side and iterate programatically. There are ways of doing it in SQL, but it'd be a bear to write, test and likely would be less efficient.
Assumptions:
Products cannot be sold during inactive date ranges
Statuses table will always alternate status active/inactive/active for each product. I.e. no date ranges where a certain product is both active and inactive.
View Results:
+------------+-------------+------------+-------------+
| product_id | active_date | end_date | days_active |
+------------+-------------+------------+-------------+
| 1 | 2018-01-01 | 2018-02-01 | 31 |
+------------+-------------+------------+-------------+
| 1 | 2018-03-01 | 2018-03-15 | 14 |
+------------+-------------+------------+-------------+
| 1 | 2018-04-25 | 2018-04-29 | 4 |
+------------+-------------+------------+-------------+
| 2 | 2018-03-01 | 2018-04-29 | 59 |
+------------+-------------+------------+-------------+
| 3 | 2018-03-10 | 2018-03-15 | 5 |
+------------+-------------+------------+-------------+
View:
CREATE OR REPLACE VIEW days_active AS (
WITH active_rn
AS (SELECT *, Row_number()
OVER ( partition BY NAME, product_id
ORDER BY created_at) AS rownum
FROM statuses
WHERE name = 'active'),
inactive_rn
AS (SELECT *, Row_number()
OVER ( partition BY NAME, product_id
ORDER BY created_at) AS rownum
FROM statuses
WHERE name = 'inactive')
SELECT x1.product_id,
x1.created_at AS active_date,
CASE WHEN x2.created_at IS NULL
THEN Curdate()
ELSE x2.created_at
END AS end_date,
CASE WHEN x2.created_at IS NULL
THEN Datediff(Curdate(), x1.created_at)
ELSE Datediff(x2.created_at,x1.created_at)
END AS days_active
FROM active_rn x1
LEFT OUTER JOIN inactive_rn x2
ON x1.rownum = x2.rownum
AND x1.product_id = x2.product_id ORDER BY
x1.product_id);

Grouping by date and combining data from 3 tables

The following are the three tables I have where session.id = signup.session_id AND session.loc_id = location.id. The max override is as the name suggest override the default max capacity for the location hence IFNULL(session.max_override, location.max_cap).
mysql> SELECT * FROM session;
+----+---------------------+---------------+--------+
| id | date_time | max_override | loc_id |
+----+---------------------+---------------+--------+
| 1 | 2014-02-04 10:30:00 | 35 | 2 |
| 2 | 2014-02-04 17:00:00 | | 2 |
| 3 | 2014-02-06 11:30:00 | 50 | 2 |
| 4 | 2014-02-09 13:30:00 | | 1 |
+----+---------------------+---------------+--------+
mysql> SELECT * FROM location;
+-----------------+---------+
| id | location | max_cap |
+-----------------+---------+
| 1 | up | 20 |
| 2 | down | 103 |
| 3 | right | 50 |
| 4 | left | 50 |
+-----------------+---------+
mysql> SELECT * FROM signups;
+-----------------+------------+
| id | name | session_id |
+-----------------+------------+
| 1 | test | 3 |
| 2 | admin | 1 |
| 3 | meme | 2 |
| 4 | anna | 4 |
+-----------------+------------+
The report I am trying to create looks simple but I am not sure how to approach the problem. The following is how I would like the report/output to look like..
mysql> query ouput;
+------------+----------+-----------+----------+----------+-----------+----------+
| date | am_time | am_ses_id | am_spots | pm_time | pm_ses_id | pm_spots |
+------------+----------+-----------+----------+----------+-----------+----------+
| 2014-02-04 | 10:30 AM | 1 | 34 | 05:00 PM | 2 | 102 |
| 2014-02-06 | 11:30 AM | 3 | 49 | | | |
| 2014-02-09 | | | | 01:30 PM | 4 | 49 |
+------------+----------+-----------+----------+----------+-----------+----------+
I can group the date and time correctly and also managed to get the session_id to match since it is all within one table but to calculate the am/pm spots which is nothing but counting the records in signups table for a particular session and deducting the value from either the max_cap or max_override depending on the situation.
THIS is what I tried
Using the following query
SELECT
DATE_FORMAT(a.date_time,'%m/%d/%Y') AS ses_date,
DATE_FORMAT(a.date_time,'%r') AS ses_time,
a.id,
COUNT(b.id) as signed_up,
IFNULL(a.max_override,c.max_cap) AS cap
FROM
test.session a
LEFT JOIN
test.signups b
ON (b.session_id = a.id)
LEFT JOIN
test.location c
ON (c.id = a.loc_id)
GROUP BY b.session_id
I get the following output
+------------+----------+--------+-----------+------+
| date | ses_time | ses_id | signed_up | cap |
+------------+----------+--------+-----------+------+
| 2014-02-04 | 10:30 AM | 1 | 1 | 35 |
| 2014-02-04 | 05:00 PM | 2 | 1 | 103 |
| 2014-02-06 | 10:30 AM | 3 | 1 | 50 |
| 2014-02-09 | 10:30 AM | 4 | 1 | 50 |
+------------+----------+--------+-----------+------+
But I cannot seem to find a way to group it only by the date so the output would appear like desired! I don't know if I should union two queries either.
Here is a very convoluted way of doing it...
sqlfiddle: http://sqlfiddle.com/#!2/d85ca/11
select c.ses_date `date`, a.ses_time am_time, a.id am_ses_id, a.cap-a.signed_up am_spots,
b.ses_time pm_time, b.id pm_ses_id, b.cap-b.signed_up pm_spots
from (
select distinct DATE_FORMAT(a.date_time,'%m/%d/%Y') ses_date
from session a) c
left join (
SELECT
DATE_FORMAT(a.date_time,'%m/%d/%Y') AS ses_date,
DATE_FORMAT(a.date_time,'%r') AS ses_time,
a.id,
COUNT(b.id) as signed_up,
IFNULL(a.max_override,c.max_cap) AS cap
FROM
session a
LEFT JOIN
signups b
ON (b.session_id = a.id)
LEFT JOIN
location c
ON (c.id = a.loc_id)
where date_format(a.date_time, '%p') = 'AM'
GROUP BY b.session_id) a on c.ses_date = a.ses_date
left join (
SELECT
DATE_FORMAT(a.date_time,'%m/%d/%Y') AS ses_date,
DATE_FORMAT(a.date_time,'%r') AS ses_time,
a.id,
COUNT(b.id) as signed_up,
IFNULL(a.max_override,c.max_cap) AS cap
FROM
session a
LEFT JOIN
signups b
ON (b.session_id = a.id)
LEFT JOIN
location c
ON (c.id = a.loc_id)
where date_format(a.date_time, '%p') = 'PM'
GROUP BY b.session_id) b on c.ses_date = b.ses_date;
You need to use the JOIN operator to let the SQL DB know the relationship between the tables.
In this case also, it may be easier to do a subquery to get the count (to avoid GROUP BY). I've not separated out AM and PM by day but you could do that.
SELECT session.date_time,
IFNULL(session.max_override,location.max_cap)-(
SELECT COUNT(signups.id)
FROM signups
WHERE signups.session_id = session.id) as avail_spots
FROM session LEFT JOIN location ON session.loc_id = location.id;
Note the LEFT JOIN will include 2014-02-04 17:00:00 with a NULL avail_spots since neither max_override nor max_cap have a value, whereas INNER JOIN would not report that session at all.
fiddle
EDIT: once you have the information by day you can use it on output. Trying to pivot out the times but grouping on the date adds a lot of complexity to the query that could be solved a lot more simply by whatever program you are using for your UI.

Join multiple tables and result multiple records

I have 3 tables with information.
Table1: Orders
+---------+----------------+------------+---------------+
| OrderID | OrderDate | Community | Status |
+-------------------------------------------------------+
1 | 1 march 2013 | S1 | Approved
2 | 5 march 2013 | S2 | Aporoved
3 | 7 march 2013 | Z1 | Approved
+-------------------------------------------------------+
Table2: OrderArtickles
+------------------------------------------------------------------+
|Ordertitem | OrderID | ArtikelID | UnitPrice | Delivered |
+------------------------------------------------------------------+
| 1 | 1 | 20 | 5 | yes
| 2 | 1 | 20 | 5 | yes
| 3 | 2 | 21 | 10 | yes
| 4 | 3 | 30 | 50 | yes
+-------------------------------------------------------------------+
Table3: users
+-----------------------------------------------------+
| Userid | Username | Community | Department |
+-----------------------------------------------------+
| 1 | User1 | S1 | S
| 2 | User2 | S2 | S
| 3 | User3 | Z1 | Z
+-----------------------------------------------------+
I need a MySQL query that give the following output:
+--------------------------------------+
| Department | TotalPriceOfArtikels
+--------------------------------------+
| S | 20
| Z | 50
+--------------------------------------+
I tried with JOIN, SUM, GROUP BY but without result. The problem that I have is that the one order gives multiple articles. Who can help me?
try this
select Department , sum(UnitPrice) as TotalPriceOfArtikels
from users u
inner join Orders o
on o.Community = u.Community
inner join OrderArtickles oa
on oa.OrderId = o.OrderId
group by Department
DEMO HERE
OUTPUT:
Department TotalPriceOfArtikels
S 20
Z 50
Something like this:
select us.Department,
sum ( art.UnitPrice ) as TotalPriceOfArtikels
from user us
left join orders ors
on ( us.Community = ors.Community)
left join OrderArtickles art
on ( ors.OrderID = art.OrderID)
group by us.Department
------------------EDITED------------------------
I copy the same values and structures of your tables in my mysql, and the result is fine, it give me:
Department TotalPriceOfArtikels
S 20
Z 50
Maybe you want to check a condition, like if delivered = yes or status = aprobed??....with this query the result it's the same that you have posted ;)
Saludos ;)

Sum up values in SQL once all values are available

I have events flowing into a MySQL database and I need to group and sum the events to transactions and store away into another table. The data looks like:
+----+---------+------+-------+
| id | transid | code | value |
+----+---------+------+-------+
| 1 | 1 | b | 12 |
| 2 | 1 | i | 23 |
| 3 | 2 | b | 34 |
| 4 | 1 | e | 45 |
| 5 | 3 | b | 56 |
| 6 | 2 | i | 67 |
| 7 | 2 | e | 78 |
| 8 | 3 | i | 89 |
| 9 | 3 | i | 90 |
+----+---------+------+-------+
The events arrive in batches and I would like to create the transaction by summing up the values for each transid, like:
select transid, sum(value) from eventtable group by transid;
but only after all the events for that transid have arrived. That is determined by the event with the code e (b for the beginning, e for the end and i for varying amount of intermediates). Being a novice in SQL, how could I implement the requirement for the existance of the end code before the summing?
Perhaps with having:
select transid, sum(value)
from eventtable
group by transid
having max(case code when 'e' then 1 end)=1;
select transid, sum(value) from eventtable
group by transid
HAVING COUNT(*) = 3
you should count the records in the group. So when there is (b)egin, (i)?? don't know what it is and (e)nd this group is not filtered out.