MySQL show used index in query - mysql

For example I have created 3 index:
click_date - transaction table, daily_metric table
order_date - transaction table
I want to check does my query use index, I use EXPLAIN function and get this result:
+----+--------------+--------------+-------+---------------+------------+---------+------+--------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+--------------+-------+---------------+------------+---------+------+--------+----------------------------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 668 | Using temporary; Using filesort |
| 2 | DERIVED | <derived3> | ALL | NULL | NULL | NULL | NULL | 645 | |
| 2 | DERIVED | <derived4> | ALL | NULL | NULL | NULL | NULL | 495 | |
| 4 | DERIVED | transaction | ALL | order_date | NULL | NULL | NULL | 291257 | Using where; Using temporary; Using filesort |
| 3 | DERIVED | daily_metric | range | click_date | click_date | 3 | NULL | 812188 | Using where; Using temporary; Using filesort |
| 5 | UNION | <derived7> | ALL | NULL | NULL | NULL | NULL | 495 | |
| 5 | UNION | <derived6> | ALL | NULL | NULL | NULL | NULL | 645 | Using where; Not exists |
| 7 | DERIVED | transaction | ALL | order_date | NULL | NULL | NULL | 291257 | Using where; Using temporary; Using filesort |
| 6 | DERIVED | daily_metric | range | click_date | click_date | 3 | NULL | 812188 | Using where; Using temporary; Using filesort |
| NULL | UNION RESULT | <union2,5> | ALL | NULL | NULL | NULL | NULL | NULL | |
+----+--------------+--------------+-------+---------------+------------+---------+------+--------+----------------------------------------------+
In EXPLAIN results I see, that index order_date of transaction table is not used, do I correct understand ?
Index click_date of daily_metric table was used correct ?
Please tell my how to understand from EXPLAIN result does my created index is used in query properly ?
My query:
SELECT
partner_id,
the_date,
SUM(clicks) as clicks,
SUM(total_count) as total_count,
SUM(count) as count,
SUM(total_sum) as total_sum,
SUM(received_sum) as received_sum,
SUM(partner_fee) as partner_fee
FROM (
SELECT
clicks.partner_id,
clicks.click_date as the_date,
clicks,
orders.total_count,
orders.count,
orders.total_sum,
orders.received_sum,
orders.partner_fee
FROM
(SELECT
partner_id, click_date, sum(clicks) as clicks
FROM
daily_metric WHERE DATE(click_date) BETWEEN '2013-04-01' AND '2013-04-30'
GROUP BY partner_id , click_date) as clicks
LEFT JOIN
(SELECT
partner_id,
DATE(order_date) as order_dates,
SUM(order_sum) as total_sum,
SUM(customer_paid_sum) as received_sum,
SUM(partner_fee) as partner_fee,
count(*) as total_count,
count(CASE
WHEN status = 1 THEN 1
ELSE NULL
END) as count
FROM
transaction WHERE DATE(order_date) BETWEEN '2013-04-01' AND '2013-04-30'
GROUP BY DATE(order_date) , partner_id) as orders ON orders.partner_id = clicks.partner_id AND clicks.click_date = orders.order_dates
UNION ALL SELECT
orders.partner_id,
orders.order_dates as the_date,
clicks,
orders.total_count,
orders.count,
orders.total_sum,
orders.received_sum,
orders.partner_fee
FROM
(SELECT
partner_id, click_date, sum(clicks) as clicks
FROM
daily_metric WHERE DATE(click_date) BETWEEN '2013-04-01' AND '2013-04-30'
GROUP BY partner_id , click_date) as clicks
RIGHT JOIN
(SELECT
partner_id,
DATE(order_date) as order_dates,
SUM(order_sum) as total_sum,
SUM(customer_paid_sum) as received_sum,
SUM(partner_fee) as partner_fee,
count(*) as total_count,
count(CASE
WHEN status = 1 THEN 1
ELSE NULL
END) as count
FROM
transaction WHERE DATE(order_date) BETWEEN '2013-04-01' AND '2013-04-30'
GROUP BY DATE(order_date) , partner_id) as orders ON orders.partner_id = clicks.partner_id AND clicks.click_date = orders.order_dates
WHERE
clicks.partner_id is NULL
ORDER BY the_date DESC
) as t
GROUP BY the_date ORDER BY the_date DESC LIMIT 50 OFFSET 0

Although I can't explain what the EXPLAIN has dumped, I thought there must be an easier solution to what you have and came up with the following. I would suggest the following indexes to optimize your existing query for the WHERE date range and grouping by partner.
Additionally, when you have a query that uses a FUNCTION on a field, it doesn't take advantage of the index. Such as your DATE(order_date) and DATE(click_date). To allow the index to better be used, qualify the full date/time such as 12:00am (morning) up to 11:59pm. I would typically to this via
x >= someDate #12:00 and x < firstDayAfterRange.
in your example would be (notice less than May 1st which gets up to April 30th at 11:59:59pm)
click_date >= '2013-04-01' AND click_date < '2013-05-01'
Table Index
transaction (order_date, partner_id)
daily_metric (click_date, partner_id)
Now, an adjustment. Since your clicks table may have entries the transactions dont, and vice-versa, I would adjust this query to do a pre-query of all possible date/partners, then left-join to respective aggregate queries such as:
SELECT
AllParnters.Partner_ID,
AllParnters.the_Date,
coalesce( clicks.clicks, 0 ) Clicks,
coalesce( orders.total_count, 0 ) TotalCount,
coalesce( orders.count, 0 ) OrderCount,
coalesce( orders.total_sum, 0 ) OrderSum,
coalesce( orders.received_sum, 0 ) ReceivedSum,
coalesce( orders.partner_fee 0 ) PartnerFee
from
( select distinct
dm.partner_id,
DATE( dm.click_date ) as the_Date
FROM
daily_metric dm
WHERE
dm.click_date >= '2013-04-01' AND dm.click_date < '2013-05-01'
UNION
select
t.partner_id,
DATE(t.order_date) as the_Date
FROM
transaction t
WHERE
t.order_date >= '2013-04-01' AND t.order_date < '2013-05-01' ) AllParnters
LEFT JOIN
( SELECT
dm.partner_id,
DATE( dm.click_date ) sumDate,
sum( dm.clicks) as clicks
FROM
daily_metric dm
WHERE
dm.click_date >= '2013-04-01' AND dm.click_date < '2013-05-01'
GROUP BY
dm.partner_id,
DATE( dm.click_date ) ) as clicks
ON AllPartners.partner_id = clicks.partner_id
AND AllPartners.the_date = clicks.sumDate
LEFT JOIN
( SELECT
t.partner_id,
DATE(t.order_date) as sumDate,
SUM(t.order_sum) as total_sum,
SUM(t.customer_paid_sum) as received_sum,
SUM(t.partner_fee) as partner_fee,
count(*) as total_count,
count(CASE WHEN t.status = 1 THEN 1 ELSE NULL END) as COUNT
FROM
transaction t
WHERE
t.order_date >= '2013-04-01' AND t.order_date < '2013-05-01'
GROUP BY
t.partner_id,
DATE(t.order_date) ) as orders
ON AllPartners.partner_id = orders.partner_id
AND AllPartners.the_date = orders.sumDate
order by
AllPartners.the_date DESC
limit 50 offset 0
This way, the first query will be quick on the index to get all possible combinations from EITHER table. Then the left-join will AT MOST join to one row per set. If found, get the number, if not, I am applying COALESCE() so if null, defaults to zero.
CLARIFICATION.
Like you when building your pre-aggregate queries of "clicks" and "orders", the "AllPartners" is the ALIAS result of the select distinct of partners and dates within the date range you were interested in. The resulting columns of that where were "partner_id" and "the_date" respective to your next queries. So this is the basis of joining to the aggregates of "clicks" and "orders". So, since I have these two columns in the alias "AllParnters", I just grabbed those for the field list since they are LEFT-JOINed to the other aliases and may not exist in either/or the respective others.

Related

Mysql Getting zero values when counting

I'm trying to count the number of sales orders has been canceled in a time period. But I run into the problem that it doesn't return results that are zero
My table
+---------------+------------+------------------+
| metrausername | signupdate | cancellationdate |
+---------------+------------+------------------+
| GLO00026 | 2017-06-22 | 2017-03-20 |
| GLO00055 | 2017-06-22 | 2017-04-18 |
| GLO00022 | 2017-06-27 | NULL |
| GLO00044 | 2017-06-24 | NULL |
| GLO00005 | 2017-06-26 | NULL |
+---------------+------------+------------------+
The statment i'm trying to count with
SELECT metrausername, COUNT(*) AS count FROM salesdata2
WHERE cancellationdate IS NOT NULL
AND signupDate >= '2017-6-21' AND signupDate <= '2017-7-20'
GROUP BY metrausername;
Let me know if any additional information would help
If the metrausername is filtered out by the where, it won't appear. Left join to the aggregation to get round this:
select distinct a1.metrausername, coalesce(a2.counted,0) as counted -- coalesce replaces null with a value
from salesdata2 a1
left join
(
SELECT metrausername, COUNT(*) AS counted
FROM salesdata2
WHERE cancellationdate IS NOT NULL
AND signupDate >= '2017-6-21' AND signupDate <= '2017-7-20'
GROUP BY metrausername
) a2
on a1.metrausername = a2.metrausername
I would just do this by moving the filtering clause to the select. Assuming you really do want the date range (as opposed to having users outside the range), then:
SELECT metrausername, COUNT(cancellationdate ) AS count
FROM salesdata2
WHERE signupDate >= '2017-06-21' AND signupDate <= '2017-07-20'
GROUP BY metrausername;
COUNT(<colname>) counts the non-NULL values, so this seems like the simplest approach.

Replace NULL by default value in LEFT JOIN, but not in ROLLUP

I have a query having LEFT JOIN, group by and ROLLUP, like this:
Select * from
(
Select user_agent,
value,
recoqty,
count(recoqty) as C
from august_2016_search_stats SS
LEFT JOIN august_2016_extra E
on (SS.id = E.stats_id and E.key = 'personalized')
where time >= '2016-08-22 00:00:00' and
time <= '2016-08-22 23:59:59' and
query_type = 'myfeed' and
recoqty = 'topics'
group by recoqty,
user_agent,
value
with ROLLUP
having recoqty is not null
) D
order by C desc;
which gives result like this:
+------------+-------+---------+------+
| user_agent | value | recoqty | C |
+------------+-------+---------+------+
| NULL | NULL | topics | 1330 |
| abscdef | NULL | topics | 1330 |
| abscdef | NULL | topics | 1285 |
| abscdef | 1 | topics | 25 |
| abscdef | 0 | topics | 20 |
+------------+-------+---------+------+
Here, the value (NULL 1285) is due to LEFT JOIN, and the value (NULL 1330) is due to rollup.
However, is there a way to replace NULL value ONLY for LEFT JOIN and not for ROLLUP ?
This is a little tricky, because it appears that the NULL values coming from your data are indistinguishable from the NULL values coming from the rollup. One possible workaround is to first do a non-aggregation query in which you replace the NULL values from the value column with 'NA', or some other placeholder, using COALESCE. Then aggregate this as a subquery using GROUP BY with rollup. Then the NULL values in the value column will with certainty be from the rollup and not your actual data.
SELECT t.user_agent,
t.value,
t.recoqty,
t.C
FROM
(
SELECT user_agent,
COALESCE(value, 'NA') AS value
recoqty,
COUNT(recoqty) AS C
FROM august_2016_search_stats SS
LEFT JOIN august_2016_extra E
ON SS.id = E.stats_id AND
E.key = 'personalized'
WHERE time >= '2016-08-22 00:00:00' AND
time <= '2016-08-22 23:59:59' AND
query_type = 'myfeed' AND
recoqty = 'topics'
) t
GROUP BY t.recoqty,
t.user_agent,
t.value
WITH ROLLUP
HAVING t.recoqty IS NOT NULL

SQL improvement in MySQL

I have these tables in MySQL.
CREATE TABLE `tableA` (
`id_a` int(11) NOT NULL,
`itemCode` varchar(50) NOT NULL,
`qtyOrdered` decimal(15,4) DEFAULT NULL,
:
PRIMARY KEY (`id_a`),
KEY `INDEX_A1` (`itemCode`)
) ENGINE=InnoDB
CREATE TABLE `tableB` (
`id_b` int(11) NOT NULL AUTO_INCREMENT,
`qtyDelivered` decimal(15,4) NOT NULL,
`id_a` int(11) DEFAULT NULL,
`opType` int(11) NOT NULL, -- '0' delivered to customer, '1' returned from customer
:
PRIMARY KEY (`id_b`),
KEY `INDEX_B1` (`id_a`)
KEY `INDEX_B2` (`opType`)
) ENGINE=InnoDB
tableA shows how many quantity we received order from customer, tableB shows how many quantity we delivered to customer for each order.
I want to make a SQL which counts how many quantity remaining for delivery on each itemCode.
The SQL is as below. This SQL works, but slow.
SELECT T1.itemCode,
SUM(IFNULL(T1.qtyOrdered,'0')-IFNULL(T2.qtyDelivered,'0')+IFNULL(T3.qtyReturned,'0')) as qty
FROM tableA AS T1
LEFT JOIN (SELECT id_a,SUM(qtyDelivered) as qtyDelivered FROM tableB WHERE opType = '0' GROUP BY id_a)
AS T2 on T1.id_a = T2.id_a
LEFT JOIN (SELECT id_a,SUM(qtyDelivered) as qtyReturned FROM tableB WHERE opType = '1' GROUP BY id_a)
AS T3 on T1.id_a = T3.id_a
WHERE T1.itemCode = '?'
GROUP BY T1.itemCode
I tried explain on this SQL, and the result is as below.
+----+-------------+------------+------+----------------+----------+---------+-------+-------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+----------------+----------+---------+-------+-------+----------------------------------------------+
| 1 | PRIMARY | T1 | ref | INDEX_A1 | INDEX_A1 | 152 | const | 1 | Using where |
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 21211 | |
| 1 | PRIMARY | <derived3> | ALL | NULL | NULL | NULL | NULL | 10 | |
| 3 | DERIVED | tableB | ref | INDEX_B2 | INDEX_B2 | 4 | | 96 | Using where; Using temporary; Using filesort |
| 2 | DERIVED | tableB | ref | INDEX_B2 | INDEX_B2 | 4 | | 55614 | Using where; Using temporary; Using filesort |
+----+-------------+-------------------+----------------+----------+---------+-------+-------+----------------------------------------------+
I want to improve my query. How can I do that?
First, your table B has int for opType, but you are comparing to string via '0' and '1'. Leave as numeric 0 and 1. To optimize your pre-aggregates, you should not have individual column indexes, but a composite, and in this case a covering index. INDEX table B ON (OpType, ID_A, QtyDelivered) as a single index. The OpType to optimize the WHERE, ID_A to optimize the group by, and QtyDelivered for the aggregate in the index without going to the raw data pages.
Since you are looking for the two types, you can roll them up into a single subquery testing for either in a single pass result. THEN, Join to your tableA results.
SELECT
T1.itemCode,
SUM( IFNULL(T1.qtyOrdered, 0 )
- IFNULL(T2.qtyDelivered, 0)
+ IFNULL(T2.qtyReturned, 0)) as qty
FROM
tableA AS T1
LEFT JOIN ( SELECT
id_a,
SUM( IF( opType=0,qtyDelivered, 0)) as qtyDelivered,
SUM( IF( opType=1,qtyDelivered, 0)) as qtyReturned
FROM
tableB
WHERE
opType IN ( 0, 1 )
GROUP BY
id_a) AS T2
on T1.id_a = T2.id_a
WHERE
T1.itemCode = '?'
GROUP BY
T1.itemCode
Now, depending on the size of your tables, you might be better doing a JOIN on your inner table to table A so you only get those of the item code you are expectin. If you have 50k items and you are only looking for items that qualify = 120 items, then your inner query is STILL qualifying based on the 50k. In that case would be overkill. In this case, I would suggest an index on table A by ( ItemCode, ID_A ) and adjust the inner query to
LEFT JOIN ( SELECT
b.id_a,
SUM( IF( b.opType = 0, b.qtyDelivered, 0)) as qtyDelivered,
SUM( IF( b.opType = 1, b.qtyDelivered, 0)) as qtyReturned
FROM
( select distinct id_a
from tableA
where itemCode = '?' ) pqA
JOIN tableB b
on PQA.id_A = b.id_a
AND b.opType IN ( 0, 1 )
GROUP BY
id_a) AS T2
My Query against your SQLFiddle

MySQL Inner Join On Clause Field Not Found

My current tables look like this:
Lessons:
+-----------------------------------+
| ID | Name | StartDate | Repeats |
|----|-------|------------|---------|
| 1 | Maths | 2014-05-05 | 5 |
| 2 | Lunch | 2014-05-05 | 1 |
| 3 | Comp | 2014-05-05 | 7 |
+-----------------------------------+
LessonTimes:
+-------------------------------------+
| ID | LessonID | StartTime | EndTime |
|----|----------|-----------|---------|
| 1 | 1 | 10:00:00 | 5 |
| 2 | 2 | 12:25:00 | 1 |
| 3 | 3 | 14:00:00 | 7 |
+-------------------------------------+
Tally:
+----+
| ID |
|----|
| 1 |
| 2 |
| . |
| . |
+----+
I have events that repeat on a certain number of days with a specific start date. The current query I have is:
SELECT E.ID
, E.Name
, E.StartDate
, E.Repeats
, A.ShowDate
, DATEDIFF(E.StartDate, A.ShowDate) diff
, T.StartTime
, DATE_ADD(A.ShowDate, INTERVAL T.StartTime HOUR_SECOND) ShowTime
FROM Planner_Lessons E
, ( SELECT DATE_ADD('2014-05-05 00:00:00',INTERVAL ID DAY ) ShowDate
FROM `Planner_Tally`
WHERE (DATE_ADD('2014-05-05 00:00:00',INTERVAL ID DAY )<= '2014-05-30 00:00:00')
ORDER
BY Id ASC
) A
LEFT
JOIN Planner_LessonTimes T
ON T.LessonID = E.ID
WHERE MOD(DATEDIFF(E.StartDate, A.ShowDate), E.Repeats) = 0
AND A.ShowDate >= E.StartDate
But the error I get is saying that the field E.ID cannot be found in the "ON" clause.
The original question I found the query on is here - PHP/MySQL: Model repeating events in a database but query for date ranges
Here is your query, formatting so one can read it:
SELECT E.ID, E.Name, E.StartDate, E.Repeats, A.ShowDate, DATEDIFF(E.StartDate, A.ShowDate) AS diff,
T.StartTime, DATE_ADD(A.ShowDate, INTERVAL T.StartTime HOUR_SECOND) AS ShowTime
FROM Planner_Lessons AS E,
(SELECT DATE_ADD('2014-05-05 00:00:00',INTERVAL ID DAY) as ShowDate
FROM `Planner_Tally`
WHERE (DATE_ADD('2014-05-05 00:00:00',INTERVAL ID DAY)<='2014-05-30 00:00:00')
ORDER BY Id ASC
) A LEFT JOIN
Planner_LessonTimes AS T
ON T.LessonID=E.ID
WHERE MOD(DATEDIFF(E.StartDate, A.ShowDate), E.Repeats)=0 AND A.ShowDate>=E.StartDate;
You are missing implicit and explicit join syntax. The columns in E are not recognized after the comma, due to MySQL scoping rules.
SELECT E.ID, E.Name, E.StartDate, E.Repeats, A.ShowDate, DATEDIFF(E.StartDate, A.ShowDate) AS diff,
T.StartTime, DATE_ADD(A.ShowDate, INTERVAL T.StartTime HOUR_SECOND) AS ShowTime
FROM Planner_Lessons E JOIN
Planner_LessonTimes T
ON T.LessonID = E.ID JOIN
(SELECT DATE_ADD('2014-05-05 00:00:00',INTERVAL ID DAY) as ShowDate
FROM `Planner_Tally`
WHERE (DATE_ADD('2014-05-05 00:00:00',INTERVAL ID DAY)<='2014-05-30 00:00:00')
ORDER BY Id ASC
) A
ON MOD(DATEDIFF(E.StartDate, A.ShowDate), E.Repeats)=0 AND A.ShowDate>=E.StartDate;
I switched the left join to inner join, because the where clause undoes the outer join.
You have missed JOIN condition for subquery aliased as A . Assuming Planner_Tally table contains column ID , you can add joining condition ON A.ID=E.ID as shown below
SELECT E.ID, E.Name, E.StartDate, E.Repeats, A.ShowDate,
DATEDIFF(E.StartDate, A.ShowDate) AS diff, T.StartTime,
DATE_ADD(A.ShowDate, INTERVAL T.StartTime HOUR_SECOND) AS ShowTime
FROM Planner_Lessons AS E
LEFT JOIN Planner_LessonTimes AS T ON T.LessonID=E.ID
LEFT JOIN (
SELECT DATE_ADD('2014-05-05 00:00:00',INTERVAL ID DAY) as ShowDate,ID
FROM `Planner_Tally`
WHERE (DATE_ADD('2014-05-05 00:00:00',INTERVAL ID DAY)<='2014-05-30 00:00:00')
) A ON A.ID=E.ID
WHERE MOD(DATEDIFF(E.StartDate, A.ShowDate), E.Repeats)=0
AND A.ShowDate>=E.StartDate
ORDER BY E.Id ASC
As an aside, consider the following... (ints is a table of integers from 0-9)
EXPLAIN
SELECT * FROM ints WHERE '2014-05-05 00:00:00' + INTERVAL i DAY < '2014-30-30 00:00:00'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: ints
type: index
possible_keys: NULL
key: PRIMARY
key_len: 4
ref: NULL
rows: 10
Extra: Using where; Using index
1 row in set (0.00 sec)
EXPLAIN
SELECT * FROM ints WHERE i < DATEDIFF('2014-05-30 00:00:00','2014-05-05 00:00:00')\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: ints
type: index
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: NULL
rows: 10
Extra: Using where; Using index
1 row in set (0.00 sec)
As you can see, although both queries are logically identical, the first registers NULL for possible_keys.
We have now reached the total extent of my knowledge of indices.

Join Nearest Date

I have read through a ton of responses on here but nothing is working quite as well as I would like. I currently have a working query that includes 2 sub queries, the problem is that it takes about 10 seconds to execute. I was wondering if there is any way to make this go quicker, maybe with a join. I just can't seem to get my head out of the box it is in. Please let me know your thoughts.
Here is the working query:
Select concat(a.emp_firstname, ' ', a.emp_lastname) as names
, if(if (a.emp_gender = 1, 'Male', a.emp_gender)=2, 'Female',
if (a.emp_gender = 1, 'Male', a.emp_gender)) as emp_gender
, c.name
, a.emp_work_telephone
, a.emp_hm_telephone, a.emp_work_email
, a.custom7, a.employee_id
, a.city_code, a.provin_code, d.name as status,
(SELECT cast(concat(DATE_FORMAT(e.app_datetime, '%H:%i'), ' ', e.app_facility) as char(100))
FROM li_appointments.li_appointments as e where e.terp_id = a.employee_id
and e.app_datetime <= str_to_date('06/26/13 at 3:20 PM', '%m/%d/%Y at %h:%i %p')
and date(e.app_datetime) = date(str_to_date('06/26/13 at 3:20 PM', '%m/%d/%Y at %h:%i %p'))
order by e.app_datetime desc limit 1) as prevapp,
(SELECT cast(concat(DATE_FORMAT(e.app_datetime, '%H:%i'), ' ', e.app_facility) as char(100))
FROM li_appointments.li_appointments as e
where e.terp_id = a.employee_id
and e.app_datetime > str_to_date('06/26/13 at 3:20 PM', '%m/%d/%Y at %h:%i %p')
and date(e.app_datetime) = date(str_to_date('06/26/13 at 3:20 PM', '%m/%d/%Y at %h:%i %p'))
order by e.app_datetime desc limit 1) as nextapp
from hs_hr_employee as a
Join hs_hr_emp_skill as b on a.emp_number = b.emp_number
Join ohrm_skill as c on b.skill_id = c.id
Join orangehrm_li.ohrm_employment_status as d on a.emp_status = d.id
where c.name like '%Arabic%'
and d.name = 'Active' order by rand();
EXPLAIN results:
+----+--------------------+-------+--------+---------------------+------------+---------+---------------------------+-------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+-------+--------+---------------------+------------+---------+---------------------------+-------+----------------------------------------------+
| 1 | PRIMARY | d | ALL | PRIMARY | | | | 10 | Using where; Using temporary; Using filesort |
| 1 | PRIMARY | a | ref | PRIMARY,emp_status | emp_status | 5 | orangehrm_li.d.id | 48 | Using where |
| 1 | PRIMARY | b | ref | emp_number,skill_id | emp_number | 4 | orangehrm_li.a.emp_number | 1 | |
| 1 | PRIMARY | c | eq_ref | PRIMARY | PRIMARY | 4 | orangehrm_li.b.skill_id | 1 | Using where |
| 3 | DEPENDENT SUBQUERY | e | ALL | | | | | 28165 | Using where; Using filesort |
| 2 | DEPENDENT SUBQUERY | e | ALL | | | | | 28165 | Using where; Using filesort |
+----+--------------------+-------+--------+---------------------+------------+---------+---------------------------+-------+----------------------------------------------+
Your tables appear small enough to do as I have done here. First, the inner-most query starts with all employees and did TWO left-joins immediately to the appointment table... By getting a MAX() of the appointments less than the date in question gets the "Previous Appointment", and getting the MIN() appointment AFTER the date in question gets the "Next Appointment". So now, for a single person, I have both their ID and possible previous and next appointments based on their specific times.
Now, I take that result and re-join first to the appointment tables (left joined again) but this time based on same person (Terp_ID) AND their respective Previous and Next appointments date/time. This would only be a problem if you had multiple entries with the exact same date/time for a single person, and that would just result in multiple records.
So now, I have each person with the specifics of previous and next appointments available.
The rest is simple joining to the other tables to get only employee status of "Active", and the skill set of "Arabic" criteria (which I have at their respective JOIN criteria), otherwise you could just move these to a WHERE clause.
As for the "Date/Time" basis of the query, I used #variable once so it could be used against both left-joins to appointments. Finally, I grabbed the respective fields you wanted. This SHOULD work, yet without your data, might need some tweaking.
SELECT
EmpPrevNext.Employee_ID,
EmpPrevNext.PrevApnt,
EmpPrevNext.NextApnt,
concat(Emp2.emp_firstname, ' ', Emp2.emp_lastname) as names,
if ( Emp2.emp_gender = 1, 'Male', 'Female' ) as emp_gender,
Emp2.emp_work_telephone,
Emp2.emp_hm_telephone,
Emp2.emp_work_email,
Emp2.custom7,
Emp2.city_code,
Emp2.provin_code,
cast( concat( DATE_FORMAT(PriorApp2.app_datetime, '%H:%i'), ' ', PriorApp2.app_facility) as char(100))
as PriorAppointment,
cast( concat( DATE_FORMAT(NextApp2.app_datetime, '%H:%i'), ' ', NextApp2.app_facility) as char(100))
as NextAppointment,
EStat.`name` as EmployeeStatus,
Skill.`name` as SkillName
FROM
( SELECT
Emp.Employee_ID,
MAX( PriorApp.app_DateTime ) as PrevApnt,
MIN( NextApp.app_DateTime ) as NextApnt
from
( select #DateBasis := '06/26/13 at 3:20 PM' ) sqlvars,
hs_hr_employee as Emp
LEFT JOIN li_appointments.li_appointments as PriorApp
ON Emp.Employee_ID = NextApp.Terp_ID
AND PriorApp.app_DateTime <= #DateBasis
LEFT JOIN li_appointments.li_appointments as NextApp
ON Emp.Employee_ID = NextApp.Terp_ID
AND NextApp.app_DateTime > #DateBasis
group by
Emp.Employee_ID ) EmpPrevNext
LEFT JOIN li_appointments.li_appointments as PriorApp2
ON EmpPrevNext.Employee_ID = PriorApp2.Terp_ID
AND EmpPrevNext.PrevApnt = PriorApp2.app_DateTime
LEFT JOIN li_appointments.li_appointments as NextApp2
ON EmpPrevNext.Employee_ID = NextApp2.Terp_ID
AND EmpPrevNext.NextApnt = NextApp2.app_DateTime
JOIN hs_hr_employee as Emp2
ON EmpPrevNext.Employee_ID = Emp2.Employee_ID
JOIN orangehrm_li.ohrm_employment_status as EStat
ON Emp2.Emp_Status = EStat.ID
AND EStat.`name` = 'Active'
JOIN hs_hr_emp_skill as EmpSkill
ON Emp2.emp_number = EmpSkill.emp_number
JOIN ohrm_skill as Skill
on EmpSkill.skill_id = Skill.id
AND Skill.`name` like '%Arabic%'
order by
rand();
Make sure your appointment table has an index on (Terp_ID, app_datetime )