I have a StockCard table with below schema:
create table StockCard(
id int(9) zerofill primary key auto_increment,
ref_page smallint(3) not null,
ref_number int(9) not null,
sur_key int unsigned null,
description varchar(255),
warehouse_product_id int(9) zerofill not null,
sc_date datetime not null,
qty int not null,
price double(15,2) not null,
reserved_qty int not null,
left_qty int not null,
status boolean not null, /* true = buy, false = sell*/
CONSTRAINT FOREIGN KEY (warehouse_product_id) REFERENCES Warehouse_Product(id),
CONSTRAINT FOREIGN KEY (ref_page) REFERENCES Page(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
For now, when I want to sell a product I use LIMIT to know whether I got enough left_qty/stock for selling that product. Below is my query, which runs in a for loop as long as avail_stock still below ordered_qty:
SELECT SUM(StockCard.left_qty) AS avail_qty FROM StockCard
LEFT JOIN Warehouse_Product ON Warehouse_Product.id = StockCard.warehouse_product_id
WHERE Warehouse_Product.product_id = {product_id}
AND StockCard.status = 1 AND left_qty > 0
ORDER BY sc_date ASC, id ASC
LIMIT {row_limit}
//this row_limit will increase in every loop as long as the SUM smaller than the ordered qty
My question here, is it true that by using my approach above it will improve my query performance or should I just delete the LIMIT? Feel free to give other approach..
Here is the simplified version of my issue: http://sqlfiddle.com/#!9/36ef5
Don't use LIMIT, it would be a lot of queries generated. See the fiddle here, first you need to calculate the sum of previous (see x2 or minqty) and sum of previous and current (see x1 or maxqty) then use them to filter what you need.
SELECT x3.*
FROM
( SELECT sc1.*, IFNULL(SUM(sc2.left_qty),0) maxqty
FROM StockCard sc1
LEFT JOIN StockCard sc2 ON sc2.id <= sc1.id
WHERE sc1.status = 1 AND sc2.status = 1 AND sc1.warehouse_product_id = 1 AND sc2.warehouse_product_id = 1
GROUP BY sc1.id
ORDER BY sc2.sc_date ASC, sc2.id ASC ) x1
LEFT JOIN
( SELECT sc1.*, IFNULL(SUM(sc2.left_qty),0)+1 minqty
FROM StockCard sc1
LEFT JOIN StockCard sc2 ON sc2.id < sc1.id
WHERE sc1.status = 1 AND sc2.status = 1 AND sc1.warehouse_product_id = 1 AND sc2.warehouse_product_id = 1
GROUP BY sc1.id
ORDER BY sc2.sc_date ASC, sc2.id ASC ) x2
ON x1.id = x2.id
LEFT JOIN StockCard x3 ON x3.id <= x1.id
AND x3.warehouse_product_id = sc1.warehouse_product_id
WHERE IFNULL(x2.minqty,0) <= 15 -- > change these
AND x1.maxqty >= 15 --> change these
For example when the table contains: 15, 2 and 3, minqty and maxqty would be:
qty min max
15 1 15
2 16 17
3 18 21
Related
I have the following tables and data:
CREATE TABLE `jobs` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`product_id` INT(10) UNSIGNED NOT NULL,
`status_id` INT(10) UNSIGNED NOT NULL,
`start_dt` TIMESTAMP NOT NULL,
`end_dt` TIMESTAMP NOT NULL,
`refreshed_dt` TIMESTAMP AS ((`start_dt` + interval ((to_days(`end_dt`) - to_days(`start_dt`)) / 2) day)) STORED,
`job_title` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`)
)
COLLATE='utf8_unicode_ci'
ENGINE=InnoDB
CREATE TABLE `job_industry` (
`job_id` INT(10) UNSIGNED NOT NULL,
`industry_id` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`job_id`, `industry_id`),
INDEX `job_industry_industry_id_foreign` (`industry_id`),
CONSTRAINT `job_industry_industry_id_foreign` FOREIGN KEY (`industry_id`) REFERENCES `industries` (`id`),
CONSTRAINT `job_industry_job_id_foreign` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE
)
COLLATE='utf8_unicode_ci'
ENGINE=InnoDB
INSERT INTO jobs (product_id, status_id, start_dt, end_dt, job_title)
VALUES (1, 4, "2019-07-28", "2019-08-28", "Financial Accountant"),
(1, 4, "2019-07-28", "2019-08-28", "Payroll Clerk"),
(3, 4, "2019-07-28", "2019-08-28", "Management Accountant"),
(1, 4, "2019-07-28", "2019-08-28", "Accounts Assistant"),
(1, 4, "2019-07-28", "2019-08-28", "Auditor");
INSERT INTO job_industry (job_id, industry_id)
VALUES (1, 1), (2, 1), (3, 1), (4, 1), (5, 1);
I have the following query to return a paginated results set to return all jobs which are currently live and within the accountancy industry sector:
select jobs.id,
jobs.job_title
from jobs
inner join job_industry on job_industry.job_id = jobs.id
where job_industry.industry_id in (1)
and jobs.start_dt <= now()
and jobs.end_dt >= now()
and jobs.status_id = 4
group by jobs.id
order by CASE WHEN jobs.product_id = 3 AND jobs.refreshed_dt <= now() THEN
jobs.refreshed_dt
ELSE jobs.start_dt
END desc, jobs.id desc limit 10 offset 0
The order by clause in the above query uses the product_id and refreshed_dt to give a higher ranking to records - if the product_id is 3 then it's considered a premium listing and if its reached half way through it's listing period then we use the refreshed_dt to bump it up in the list. The refreshed_dt basically is the mid point between the start_dt and end_dt. We want to list the newest listing first.
The above query give me the following result set:
id | job_title
----------------------
3 | Management Accountant <--- premium listing
5 | Auditor <--- previous
4 | Accounts Assistant <--- selected record
2 | Payroll Clerk <--- next
1 | Financial Accountant
Now if I select record id 4, how do I get the previous and next records?
I've checked the following post How to get next/previous record in MySQL? but that only works if you're ordering by id.
This is my attempt to get previous record which returns record id 5 which is correct however if there were other premium records then i feel this query would fail:
select MAX(jobs.id)
from jobs
inner join job_industry on job_industry.job_id = jobs.id
where job_industry.industry_id in (1)
and jobs.start_dt <= now()
and jobs.end_dt >= now()
and jobs.status_id = 4
and jobs.id > 4
order by CASE WHEN jobs.product_id = 3 AND jobs.refreshed_dt <= now() THEN
jobs.refreshed_dt
ELSE jobs.start_dt
END desc, jobs.id desc
And to get next record I have the following which return record id 1 which is incorrect:
select MIN(jobs.id)
from jobs
inner join job_industry on job_industry.job_id = jobs.id
where job_industry.industry_id in (1)
and jobs.start_dt <= now()
and jobs.end_dt >= now()
and jobs.status_id = 4
and jobs.id < 4
order by CASE WHEN jobs.product_id = 3 AND jobs.refreshed_dt <= now() THEN
jobs.refreshed_dt
ELSE jobs.start_dt END desc, jobs.id desc
Some help to tackle this would be appreciated. Please note I've provided a minimal reproducible example above. Also i'm using and limited to mysql version 5.7.17
CREATE TABLE `user_activity` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`type` enum('request','response') DEFAULT NULL,
`data` longtext NOT NULL,
`created_at` datetime DEFAULT NULL,
`source` varchar(255) DEFAULT NULL,
`task_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
);
I have this data:-
Now I need to select all rows for user_id=527 where created_at value is the maximum. So I need the last 3 rows in this image.
I wrote this query:-
SELECT *
FROM user_activity
WHERE user_id = 527
AND source = 'E1'
AND task_name IN ( 'GetReportTask', 'StopMonitoringUserTask' )
AND created_at = (SELECT Max(created_at)
FROM user_activity
WHERE user_id = 527
AND source = 'E1'
AND task_name IN ( 'GetReportTask',
'StopMonitoringUserTask' ));
This is very inefficient because I am running the exact same query again as an inner query except that it disregards created_at. What's the right way to do this?
I would use a correlated subquery:
SELECT ua.*
FROM user_activity ua
WHERE ua.user_id = 527 AND source = 'E1' AND
ua.task_name IN ('GetReportTask', 'StopMonitoringUserTask' ) AND
ua.created_at = (SELECT MAX(ua2.created_at)
FROM user_activity ua2
WHERE ua2.user_id = ua.user_id AND
ua2.source = ua.source AND
ua2.task_name IN ( 'GetReportTask', 'StopMonitoringUserTask' )
);
Although this might seem inefficient, you can create an index on user_activity(user_id, source, task_name, created_at). With this index, the query should have decent performance.
Order by created_at desc and limit your query to return 1 row.
SELECT *
FROM user_activity
WHERE user_id = 527
AND source = 'E1'
AND task_name IN ( 'GetReportTask', 'StopMonitoringUserTask' )
ORDER BY created_at DESC
LIMIT 1;
I used EverSQL and applied my own changes to come up with this single-select query that uses self-join:-
SELECT *
FROM user_activity AS ua1
LEFT JOIN user_activity AS ua2
ON ua2.user_id = ua1.user_id
AND ua2.source = ua1.source
AND ua2.task_name IN ( 'GetReportTask', 'StopMonitoringUserTask' )
AND ua1.created_at < ua2.created_at
WHERE ua1.user_id = 527
AND ua1.source = 'E1'
AND ua1.task_name IN ( 'GetReportTask', 'StopMonitoringUserTask' )
AND ua2.created_at IS NULL;
However, I noticed that the response times of both queries were similar. I tried to use Explain to identify any performance differences; and from what I understood from its output, there are no noticeable differences because proper indexing is in place. So for readability and maintainability, I'll just use the nested query.
I am using mysql2 module in nodejs v8.9.4.
This is my function to get a message from message queue which meets this conditions :
status==0
if count of botId with status==1 is less than 10
if retry_after in wait table for botId+chatId and just botId is less than NOW(timestamp)
if there is no same chatId with status==1
static async Find(activeMessageIds, maxActiveMsgPerBot) {
let params = [maxActiveMsgPerBot];
let filterActiveMessageIds = ' ';
let time = Util.GetTimeStamp();
if (activeMessageIds && activeMessageIds.length) {
filterActiveMessageIds = 'q.id NOT IN (?) AND ';
params.push(activeMessageIds);
}
let q =
`select q.*
from bot_message_queue q
left join bot_message_queue_wait w on q.botId=w.botId AND q.chatId=w.chatId
left join bot_message_queue_wait w2 on q.botId=w2.botId AND w2.chatId=0
where
q.status=0 AND
q.botId NOT IN (select q2.botId from bot_message_queue q2 where q2.status=1 group by q2.botId HAVING COUNT(q2.botId)>?) AND
${filterActiveMessageIds}
q.chatId NOT IN (select q3.chatId from bot_message_queue q3 where q3.status=1 group by q3.chatId) AND
(w.retry_after IS NULL OR w.retry_after <= ?) AND
(w2.retry_after IS NULL OR w2.retry_after <= ?)
order by q.priority DESC,q.id ASC
limit 1;`;
params.push(time);
params.push(time);
let con = await DB.connection();
let result = await DB.query(q, params, con);
if (result && result.length) {
result = result[0];
let updateQ = `update bot_message_queue set status=1 where id=?;`;
await DB.query(updateQ, [result.id], con);
} else
result = null;
con.release();
return result;
}
This query runs fine on my local dev system. It also runs fine in servers phpmyadmin in couple of milliseconds.
BUT when it runs throw nodejs+mysql2 The cpu usage goes up to 100%
There is only 2K rows in this table.
CREATE TABLE IF NOT EXISTS `bot_message_queue` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`botId` int(10) UNSIGNED NOT NULL,
`chatId` varchar(50) CHARACTER SET utf8 NOT NULL,
`type` varchar(50) DEFAULT NULL,
`message` longtext NOT NULL,
`add_date` int(10) UNSIGNED NOT NULL,
`status` tinyint(2) UNSIGNED NOT NULL DEFAULT '0' COMMENT '0=waiting,1=sendig,2=sent,3=error',
`priority` tinyint(1) UNSIGNED NOT NULL DEFAULT '5' COMMENT '5=normal messages,<5 = bulk messages',
`delay_after` int(10) UNSIGNED NOT NULL DEFAULT '1000',
`send_date` int(10) UNSIGNED DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `botId` (`botId`,`status`),
KEY `botId_2` (`botId`,`chatId`,`status`,`priority`),
KEY `chatId` (`chatId`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `bot_message_queue_wait` (
`botId` int(10) UNSIGNED NOT NULL,
`chatId` varchar(50) CHARACTER SET utf8 NOT NULL,
`retry_after` int(10) UNSIGNED NOT NULL,
PRIMARY KEY (`botId`,`chatId`),
KEY `retry_after` (`retry_after`),
KEY `botId` (`botId`,`chatId`,`retry_after`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
UPDATE: Real table data here
UPDATE 2:
FetchMessageTime :
- Min : 1788 ms
- Max : 44285 ms
- Average : 20185.4 ms
The max was like 20ms until yesterday :( now its 40 seconds!!!
UPDATE 3: I merged these 2 joins and wheres:
left join bot_message_queue_wait w on q.botId=w.botId AND q.chatId=w.chatId
left join bot_message_queue_wait w2 on q.botId=w2.botId AND w2.chatId=0
(w.retry_after IS NULL OR w.retry_after <= ?) AND
(w2.retry_after IS NULL OR w2.retry_after <= ?)
into a single one, I hope this will work as intended!
left join bot_message_queue_wait w on q.botId=w.botId AND ( q.chatId=w.chatId OR w.chatId=0 )
and for the time being I removed the 2 wheres and the query time went back to normal.
q.botId NOT IN (select ...)
q.chatId NOT IN (select ...)
So these 2 where queries are the chock points and needs to be fixed.
NOT IN ( SELECT ... ) is difficult to optimize.
OR cannot be optimized.
In ORDER BY, mixing DESC and ASC eliminates use of an index (until 8.0). Consider changing ASC to DESC. After that, INDEX(priority, id) might help.
What is ${filterActiveMessageIds}?
The GROUP BY is not needed in
NOT IN ( SELECT q3.chatId
from bot_message_queue q3
where q3.status=1
group by q3.chatId )
INDEX(status, chatid) in this order would benefit that subquery.
INDEX(status, botid) in this order
More on index creation: http://mysql.rjweb.org/doc.php/index_cookbook_mysql
I would replace the NOT IN subquery with a NOT EXISTS in this case, as it can perform better.
Switch the ORDER BY to either all DESC or all ASC
So to optimize the query, first, add these indexes:
ALTER TABLE `bot_message_queue` ADD INDEX `bot_message_queue_idx_status_botid_chatid_priori_id` (`status`,`botId`,`chatId`,`priority`,`id`);
ALTER TABLE `bot_message_queue` ADD INDEX `bot_message_queue_idx_priority_id` (`priority`,`id`);
ALTER TABLE `bot_message_queue` ADD INDEX `bot_message_queue_idx_botid_status` (`botId`,`status`);
ALTER TABLE `bot_message_queue` ADD INDEX `bot_message_queue_idx_chatid_status` (`chatId`,`status`);
ALTER TABLE `bot_message_queue_wait` ADD INDEX `bot_message_queue_wa_idx_chatid_botid` (`chatId`,`botId`);
Now, you can try to run this query (please note I changed the order by to all DESC, so you can change it to ASC if that's a requirement):
SELECT
bot_message_queue.*
FROM
bot_message_queue q
LEFT JOIN
bot_message_queue_wait w
ON q.botId = w.botId
AND q.chatId = w.chatId
LEFT JOIN
bot_message_queue_wait w2
ON q.botId = w2.botId
AND w2.chatId = 0
WHERE
q.status = 0
AND NOT EXISTS (
SELECT
1
FROM
bot_message_queue AS q21
WHERE
q21.status = 1
AND q.botId = q21.botId
GROUP BY
q21.botId
HAVING
COUNT(q21.botId) > ?
ORDER BY
NULL
)
AND NOT EXISTS (
SELECT
1
FROM
bot_message_queue AS q32
WHERE
q32.status = 1
AND q.chatId = q32.chatId
GROUP BY
q32.chatId
ORDER BY
NULL
)
AND (
w.retry_after IS NULL
OR w.retry_after <= ?
)
AND (
w2.retry_after IS NULL
OR w2.retry_after <= ?
)
ORDER BY
q.priority DESC,
q.id DESC LIMIT 1
I have came up with the following schema:
CREATE TABLE products
(
id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
name VARCHAR(255) NOT NULL,
quantity INT(10) UNSIGNED NOT NULL,
purchase_price DECIMAL(8,2) NOT NULL,
sell_price DECIMAL(8,2) NOT NULL,
provider VARCHAR(255) NULL,
created_at TIMESTAMP NULL,
PRIMARY KEY (id)
);
# payment methods = {
# "0": "CASH",
# "1": "CREDIT CARD",
# ...
# }
CREATE TABLE orders
(
id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
product_id INT(10) UNSIGNED NOT NULL,
quantity INT(10) UNSIGNED NOT NULL,
payment_method INT(11) NOT NULL DEFAULT 0,
created_at TIMESTAMP NULL,
PRIMARY KEY (id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
# status = {
# "0": "PENDING"
# "1": "PAID"
# }
CREATE TABLE invoices
(
id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
price INT(10) UNSIGNED NOT NULL,
status INT(10) UNSIGNED NOT NULL DEFAULT 0,
created_at TIMESTAMP NULL,
PRIMARY KEY (id)
);
# payment methods = {
# "0": 'CASH',
# "1": 'CREDIT CARD',
# ...
# }
CREATE TABLE bills
(
id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
name VARCHAR(255) NOT NULL,
payment_method INT(10) UNSIGNED NOT NULL DEFAULT 0,
price DECIMAL(8,2) NOT NULL,
created_at TIMESTAMP NULL,
PRIMARY KEY (id)
);
And the following query to select a balance:
SELECT ((orders + invoices) - bills) as balance
FROM
(
SELECT SUM(p.sell_price * o.quantity) as orders
FROM orders o
JOIN products p
ON o.product_id = p.id
) orders,
(
SELECT SUM(price) as invoices
FROM invoices
WHERE status = 1
) invoices,
(
SELECT SUM(price) as bills
FROM bills
) bills;
Its working and returning the right balance, but I want to create a chart using Morris.js and I need to change it to return a daily or monthly balance at a given period of time and in this format:
Daily (2017-02-27 to 2017-03-01)
balance | created_at
--------------------------
600.00 | 2017-03-01
50.00 | 2017-02-28
450.00 | 2017-02-27
And monthly (2017-01 to 2017-03)
balance | created_at
--------------------------
200.00 | 2017-03
250.00 | 2017-02
350.00 | 2017-01
What I need to change in my schema or query to return results in this way?
http://sqlfiddle.com/#!9/2289a9/2
Any hints are welcomed. Thanks in advance
Include the created_at date in the SELECT list and a GROUP BY clause in each query.
Ditch the old school comma operator for the join operation, and replace it with a LEFT JOIN.
To return dates for which there are no orders (or no payments, or no invoices) we need a separate row source that is guaranteed to return the date values. As an example, we could use an inline view:
SELECT d.created_dt
FROM ( SELECT '2017-02-27' + INTERVAL 0 DAY AS created_dt
UNION ALL SELECT '2017-02-28'
UNION ALL SELECT '2017-03-01'
) d
ORDER BY d.created_dt
The inline view is just an option. If we had a calendar table that contains rows for the three dates we're interested in, we could make use of that instead. What's important is that we have a query that is guaranteed to return to us exactly three rows with the distinct created_at date values we want to return.
Once we have that, we can add a LEFT JOIN to get the value of "bills" for that date.
SELECT d.created_dt
, b.bills
FROM ( SELECT '2017-02-27' + INTERVAL 0 DAY AS created_dt
UNION ALL SELECT '2017-02-28'
UNION ALL SELECT '2017-03-01'
) d
LEFT
JOIN ( SELECT DATE(bills.created_at) AS created_dt
, SUM(bills.price) AS bills
FROM bills
WHERE bills.created_at >= '2017-02-27'
AND bills.created_at < '2017-03-01' + INTERVAL 1 DAY
GROUP BY DATE(bills.created_at)
) b
ON b.created_dt = d.created_dt
ORDER BY d.created_dt
Extending that to add another LEFT JOIN, to get invoices
SELECT d.created_dt
, i.invoices
, b.bills
FROM ( SELECT '2017-02-27' + INTERVAL 0 DAY AS created_dt
UNION ALL SELECT '2017-02-28'
UNION ALL SELECT '2017-03-01'
) d
LEFT
JOIN ( SELECT DATE(bills.created_at) AS created_dt
, SUM(bills.price) AS bills
FROM bills
WHERE bills.created_at >= '2017-02-27'
AND bills.created_at < '2017-03-01' + INTERVAL 1 DAY
GROUP BY DATE(bills.created_at)
) b
ON b.created_dt = d.created_dt
LEFT
JOIN ( SELECT DATE(invoices.created_at) AS created_dt
, SUM(invoices.price) AS invoices
FROM invoices
WHERE invoices.status = 1
AND invoices.created_at >= '2017-02-27'
AND invoices.created_at < '2017-03-01' + INTERVAL 1 DAY
GROUP BY DATE(invoices.created_at)
) i
ON i.created_dt = d.created_dt
ORDER BY d.created_dt
Similarly, we can a LEFT JOIN to another inline view that returns total orders grouped by DATE(created_at).
It's important that the inline views return distinct value of created_dt, a single row for each date value.
Note that for dev, test and debugging, we can independently execute just the inline view queries.
When a matching row is not returned from a LEFT JOIN, for example no matching row returned from i because there were no invoices on that date, the query is going to return a NULL for the expression i.invoices. To replace the NULL with a zero, we can use the IFNULL function, or the more ANSI standard COALESCE function. For example:
SELECT d.created_dt
, IFNULL(i.invoices,0) AS invoices
, COALESCE(b.bills,0) AS bills
FROM ...
To get the results monthly, we'd need a calendar query that returns one row per month. Let's assume we're going to return a DATE value which as the first day of the month. For example:
SELECT d.created_month
FROM ( SELECT '2017-02-01' + INTERVAL 0 DAY AS created_month
UNION ALL SELECT '2017-03-01'
) d
ORDER BY d.created_month
The inline view queries will need to GROUP BY created_month, so they return a single value for each month value. My preference would be to use a DATE_FORMAT function to return the first day of the month, derived from created_at. But there are other ways to do it. The goal is return a single row for '2017-02-01' and a single row for '2017-03-01'. Note that the date ranges on created_at extend from '2017-02-01' up to (but not including) '2017-04-01', so we get the total for the whole month.
( SELECT DATE_FORMAT(bills.created_at,'%Y-%m-01') AS created_month
, SUM(bills.price) AS bills
FROM bills
WHERE bills.created_at >= '2017-02-01'
AND bills.created_at < '2017-03-01' + INTERVAL 1 MONTH
GROUP BY DATE_FORMAT(bills.created_at,'%Y-%m-01')
) b
I have a event/calendar MySQL table where each user have multiple appointments/events throughout the day. If one user can't make that appointment/event "because he/she are running behind on other appointment" I need to be able to re-assign this appointment to a different available user. So I need to display a suggestion of the top 5 users that are available for the scheduled time frame and can take this appointment, a manager will be able to re-assign this appointment to one of the suggested users.
My events table looks something like this
CREATE TABLE `calendar_events` (
`event_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`start_on` datetime NOT NULL,
`end_on` datetime NOT NULL,
`subject` varchar(255) NOT NULL,
`event_type` enum('Phone Call','Meeting','Event','Appointment','Other') CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL DEFAULT 'Phone Call',
`all_day_event` tinyint(1) DEFAULT '0' COMMENT '1 = all day event, 0 = no',
`phone_call_id` int(11) unsigned DEFAULT NULL,
`account_id` int(11) unsigned DEFAULT NULL,
`client_id` int(11) unsigned DEFAULT NULL,
`owner_id` int(11) unsigned NOT NULL,
`created_by` int(11) unsigned NOT NULL,
`created_on` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modified_by` int(11) unsigned DEFAULT NULL,
`modified_on` datetime DEFAULT NULL,
`event_location` varchar(255) DEFAULT NULL,
`event_notes` varchar(10000) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0 = purged, 1 = active, 2=pass, 3 = cancled, 5 = waiting for auditor to be enabled',
PRIMARY KEY (`event_id`),
UNIQUE KEY `phone_call_id` (`phone_call_id`,`account_id`,`client_id`),
KEY `client_id` (`client_id`),
KEY `account_id` (`account_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
so lets for event_id = 100 is assigned to user_id = 2 and scheduled to start_on = '2014-09-21 10:00:00' and end_on '2014-09-21 10:00:00'
and user_id = 5 has appointment start_on '2014-09-21 11:45:00' and end_on '2014-09-21 12:30:00'
and user_id = 2 can not make his appointment that is scheduled for '2014-09-21 10:00:00' so they system will suggest user_id = 5 as he will be for the following 105 minutes.
The the final data set will need to be
event_id org_owner suggested_owner available_for
100 2 5 105
The following query will give me a list of all available users to from the users table along with a start_on end_on value if the user have an event scheduled (one user can have multiple records.) If the start_on is null in this query that means this user does not have any event otherwise it will return the start of each event.
So if user ID appears in the query above and have a NULL value in the start_on column, this means that this user is available all day so this user should be 1 of the 5 users to recommend because it has one of the highest availability. But if a user has one/multiple rows in the data set with a non-null value in the start on then, we need to look at the start_on that is the closest to the event and then recommend the top 5 that have the greatest availability value.
SELECT user_id, start_on, end_on, subject
FROM view_users AS su
LEFT JOIN calendar_events AS c ON c.owner_id = su.user_id AND c.start_on NOT BETWEEN '2014-09-30 00:00:00' AND '2014-09-30 23:59:59' AND c.status = 1
WHERE su.is_available_today = 1
How can I extract this data set?
First proposal edited thanks to your help, just need to take care of users that don't have any events (could be achieved with a left join in 't' subquery). This could be improved a lot, but right now I'm a bit tired :)
SELECT
c.event_id, -- Event id
c.owner_id AS org_owner, -- Original owner of event
t.owner_id AS suggested_owner, -- Suggested new user
c.start_on, -- Event start
t.free_from, -- Owner free slot start
t.free_to, -- Owner free slot end
TIME_TO_SEC( TIMEDIFF( t.free_to, c.start_on ) ) /60 AS available_for -- Availibility of minutes (diff between event start and free slot end)
FROM calendar_events AS c
-- Join with free slots
LEFT JOIN (
-- Add a slot for beginning, 1999-01-01 to first event start
SELECT * FROM (
SELECT owner_id, '1900-01-01' AS free_from, MIN( start_on ) AS free_to
FROM calendar_events c3
GROUP BY owner_id
) AS deb
UNION
-- select free slots by taking the event end and the following event start
SELECT owner_id, `end_on` AS free_from, (
SELECT start_on
FROM calendar_events c2
WHERE c2.owner_id = c1.owner_id
AND c2.start_on > c1.end_on
ORDER BY c2.start_on
LIMIT 0 , 1
) AS free_to
FROM calendar_events c1
UNION
-- Add a slot for end, last event end to 2100-01-01
SELECT * FROM (
SELECT owner_id, MAX( end_on ) AS free_from, '2100-01-01' AS free_to
FROM calendar_events c3
GROUP BY owner_id
) AS end
) AS t ON t.owner_id <> c.owner_id
-- Join avoid using same user and ensure free slot matches event dates
AND t.free_from <= c.start_on AND t.free_to >= c.end_on
WHERE c.status = 1
AND c.event_id =52
GROUP BY t.owner_id -- To avoid multiple free slots by user
ORDER BY available_for DESC -- Sort to list biggest slots first
LIMIT 0, 5 -- Only five first matching users
Good luck :)
How about this:
SELECT event_id, owner_id, start_on INTO #eventid, #user, #start_on
FROM calender_events WHERE event_id = 100;
SELECT #event_id event_id,
#user org_owner,
c.owner_id suggested_owner,
TIMESTAMPDIFF(MINUTE, $start_on, COALESCE(c.min_start, DATE(#start_on) + INTERVAL 18 HOUR)) available_for
FROM
users u
LEFT JOIN
(SELECT
owner_id,
MIN(start_on)
FROM
calender_events
WHERE
(start_on BETWEEN #start_on AND DATE(#start_on) + INTERVAL 18 HOUR)
OR
(start_on BETWEEN DATE(#start_on) AND DATE(#start_on) + INTERVAL 18 HOUR AND all_day_event = 1)
GROUP BY owner_id
) c
ON u.user_id = c.owner_id
WHERE u.user_id <> #user
ORDER BY available_for DESC
LIMIT 5
Maybe you have to adjust the INTERVAL, I just made an assumption the daay ending 6 P.M.
Try this:
SELECT
co.event_id,
co.owner_id org_owner,
su.user_id suggested_owner,
ifnull(min((to_seconds(c.start_on) - to_seconds(co)) / 60), 999) available
FROM calendar_events co
CROSS JOIN view_users su
LEFT JOIN calendar_events c ON c.owner_id = su.user_id
AND c.start_on BETWEEN co.start_on AND date(adddate(co.start_on, 1))
AND c.status = 1
WHERE co.event_id = 100
AND su.is_available_today = 1
GROUP BY 1, 2, 3
ORDER BY 4 DESC
LIMIT 5
Users that have no appointments for day day after the target event get assigned the available value of "999", putting them at the top of the list.
The next event for each user is found using min() over the time gap, and all users are sorted largest time gap first, them limit gives you the top 5.