mysql query performance improve - php

I have performance issue in below query:
SELECT t.local_branch_revenue, t.total_payment,
(SELECT SUM(IF(cpo.real_account_type = 'HQ', 0, cpo.payment_amount)) AS cpo_payment_amount
FROM customer_payment_options cpo
WHERE tran_id=t.id
AND cpo.payment_type != 'WALLET' AND cpo.payment_type != 'REWARD_CREDIT'
GROUP BY cpo.tran_id)
as cpo_payment_amount,
b.ben_firstname, b.ben_lastname
FROM transaction t
LEFT JOIN beneficiary b
ON b.id=t.ben_id
WHERE t.local_branch_id='31'
AND DATE(t.date_added) < '2016-04-07'
AND source_country_id='40'
AND t.transaction_status != 'CANCELLED'
EXPLAIN
+----+--------------------+-------+--------+----------------------------------------+----------------------------------------+---------+-----------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+-------+--------+----------------------------------------+----------------------------------------+---------+-----------------+------+-------------+
| 1 | PRIMARY | t | ref | local_branch_id,source_country_id | local_branch_id | 5 | const | 2 | Using where |
+----+--------------------+-------+--------+----------------------------------------+----------------------------------------+---------+-----------------+------+-------------+
| 1 | PRIMARY | b | eq_ref | PRIMARY | PRIMARY | 8 | mtesdb.t.ben_id | 1 | |
+----+--------------------+-------+--------+----------------------------------------+----------------------------------------+---------+-----------------+------+-------------+
| 2 | DEPENDENT SUBQUERY | cpo | ref | tran_id_payment_type_real_account_type | tran_id_payment_type_real_account_type | 9 | mtesdb.t.id | 1 | Using where |
+----+--------------------+-------+--------+----------------------------------------+----------------------------------------+---------+-----------------+------+-------------+
As you can see, it's using indexes from possible key. But still query takes about 13 sec.
I also have index over transaction table: (ben_id, company_id, source_country_id, date_added, tran_owner). But, it's not even coming in possible keys section.
Let me know if you need table schema.
What am I missing here?

Dependent subqueries don't perform very well in MySQL ... the query planner doesn't transform them to JOINed subqueries efficiently. (They're OK in Oracle and SQL Server, but who has the money for those?) So, a good bet for you is to refactor your query to eliminate the dependent subquery.
Here's your subquery. Let's refactor it as an independent subquery. We'll get rid of the WHERE tran_id=t.id and move it, later, to an ON clause.
SELECT tran_id,
SUM(IF(real_account_type = 'HQ',
0,
payment_amount)) AS cpo_payment_amount
FROM customer_payment_options
WHERE payment_type != 'WALLET'
AND payment_type != 'REWARD_CREDIT'
GROUP BY tran_id
Notice you can simplify this as follows -- your IF() clause excludes rows with real_account_type = 'HQ'. You can do that in the WHERE clause instead.
SELECT tran_id,
SUM(payment_amount) AS cpo_payment_amount
FROM customer_payment_options
WHERE payment_type != 'WALLET'
AND payment_type != 'REWARD_CREDIT'
AND real_account_type != 'HQ'
GROUP BY tran_id
A compound index on (tran_id, payment_type, real_account_type, payment_amount) may help this subquery run faster. But the presence of those three != clauses guarantees a full index scan; there's no way to random access any index for those.
This generates a virtual table containing one row per tran_id with the sum you need.
Next we need to join that into your main query.
SELECT t.local_branch_revenue,
t.total_payment,
IFNULL(cposum.cpo_payment_amount,0) cpo_payment_amount,
b.ben_firstname, b.ben_lastname
FROM transaction t
LEFT JOIN beneficiary b ON b.id=t.ben_id
LEFT JOIN (
SELECT tran_id,
SUM(payment_amount) AS cpo_payment_amount
FROM customer_payment_options
WHERE payment_type != 'WALLET'
AND payment_type != 'REWARD_CREDIT'
AND real_account_type != 'HQ'
GROUP BY tran_id
) cposum ON t.id = cposum.tran_id
WHERE t.local_branch_id='31'
AND DATE(t.date_added) < '2016-04-07'
AND source_country_id='40'
AND t.transaction_status != 'CANCELLED'
Do you see how we've changed the dependent summary subquery into its own virtual table? That lets the query planner run that query just once, rather than once for each row in the main query. That helps a lot.
The IFNULL() gets you a numeric value for cpo_payment_amount, rather than NULL, for transaction rows lacking any corresponding customer_payment_options rows.
A compound index on the transaction table on (local_branch_id, source_country_id, date_added) will help this query; the query engine can random access the local_branch_id and source_country_id values, then range scan the date_added value.
How do you learn to do this yourself? http://use-the-index-luke.com/ is a good start.

WHERE t.local_branch_id='31'
AND DATE(t.date_added) < '2016-04-07'
AND source_country_id='40'
Change that date test to simply t.date_added < '2016-04-07' ! Otherwise the following index suggestions won't work.
What table is source_country_id in?? If it is in t, then you need INDEX(local_branch_id, source_country_id, date_added). If it is not in t, then INDEX(local_branch_id, date_added).
Please provide SHOW CREATE TABLE if you need further discussion.

Related

Joining table mysql -- without double iteration

Let me start by saying this should be a relatively simple problem which is / was made unnecessary complicated by bad Database design (not by me) that said im also no expert in mysql.
Consider the following
Table Schedule
Note how the columns homeID and visitorID contains the names of the teams and not the actual teamID's
In a bid to fix this I created a new table with columns containing teamID AND teamName as can be seen by below image.
Table Teams
My Problem(s)
I must get the teamID from table Teams for BOTH home team AND away team
So I created the Teams table and this simple script:
SELECT schedule.*, teams.*
FROM schedule
JOIN teams ON schedule.homeID = teams.teamName OR schedule.visitorID = teams.teamName
WHERE schedule.gameID = 411
LIMIT 1 #added Limit1 else the code generates to rows
Output of mysql Script
Limit 1
Notice above how teamID is only generated for 1 team with Limit 1
No Limit Statement (Double Iteration)
Notice above how teamID can get retrieved for BOTH teams. Problem is its doing a double iteration.
TLDR; The above presents the following problems
Firstly the script will generate two outputs one for home team and once for away team. As to be expected however I cant have that.
As a workaround to Problem number 1 -- I added Limit 1 the problem I get with Limit though is that it only gives back a single teamID (as to be expected, I guess)
Question
How can I get BOTH teamID's from table teams with a single iteration? Hope this make sense....
Extra
A demo of application with hard coded team names looks like this (just to give an idea of what they are trying to achieve)
Sounds like you want to join teams twice to schedule.
SELECT s.*,
th.*,
ta.*
FROM schedule s
INNER JOIN teams th
ON s.homeid = th.teamname
INNER JOIN teams ta
ON s.visitorid = ta.teamname
WHERE s.gameid = 411;
I guess that you want to show both team in one row instead of two rows.
If yes, then you need to join the table teams twice.
Consider this demo: http://www.sqlfiddle.com/#!9/bb5e61/1
This join will collect both teams into one row:
SELECT s.*,
t1.teamId as homeId_teamId,
t1.teamCode as homeId_teamCode,
t1.teamName as homeId_teamName,
t2.teamId as visitorId_teamId,
t2.teamCode as visitorId_teamCode,
t2.teamName as visitorId_teamName
FROM Schedule s
JOIN Teams t1 ON s.homeId = t1.teamName
JOIN Teams t2 ON s.visitorId = t2.teamName;
| id | homeId | visitorId | homeId_teamId | homeId_teamCode | homeId_teamName | visitorId_teamId | visitorId_teamCode | visitorId_teamName |
|----|--------|-----------|---------------|-----------------|-----------------|------------------|--------------------|--------------------|
| 1 | Poland | Colombia | 1 | PL | Poland | 2 | CO | Colombia |
However you can also consider LEFT joins instead on INNER joins, which will work in a case where there is no relevant data in the TEAMS table:
SELECT s.*,
t1.teamId as homeId_teamId,
t1.teamCode as homeId_teamCode,
t1.teamName as homeId_teamName,
t2.teamId as visitorId_teamId,
t2.teamCode as visitorId_teamCode,
t2.teamName as visitorId_teamName
FROM Schedule s
LEFT JOIN Teams t1 ON s.homeId = t1.teamName
LEFT JOIN Teams t2 ON s.visitorId = t2.teamName;
| id | homeId | visitorId | homeId_teamId | homeId_teamCode | homeId_teamName | visitorId_teamId | visitorId_teamCode | visitorId_teamName |
|----|----------|-----------|---------------|-----------------|-----------------|------------------|--------------------|--------------------|
| 1 | Poland | Colombia | 1 | PL | Poland | 2 | CO | Colombia |
| 3 | Ya Majka | Poland | (null) | (null) | (null) | 1 | PL | Poland |
| 2 | Ya Majka | Rossija | (null) | (null) | (null) | (null) | (null) | (null) |
Here are the scripts that make up the tables from the examples
CREATE TABLE Schedule(
id int, homeId varchar(20),visitorId varchar(20)
);
INSERT INTO Schedule VALUES
(1, 'Poland', 'Colombia' ),(2,'Ya Majka','Rossija'),
(3,'Ya Majka','Poland');
CREATE TABLE Teams(
teamId int, teamCode varchar(10), teamName varchar(20)
);
INSERT INTO Teams VALUES
(1, 'PL', 'Poland' ),(2,'CO','Colombia'),(3,'US','United States');
You can use a subquery (two of them in the same query) to solve this:
select
gameID,
weekNum,
gameTimeEastern,
(select teamName from teams where teamID = schedule.homeID) as homeName,
homeScore,
(select teamName from teams where teamID = schedule.visitorID) as visitorName,
visitorScore from schedule;
This doesn't get all the columns from schedule, just an example to show how it works. If you need various queries (including select *, though this isn't a good practice except for testing), you could create a view based on a query like the above (with ALL columns from schedule, except homeID and visitorID that get replaced with sub-queries from the teams table). Then you can place queries against that view - and they will work like the original table where you had team names directly in it.

Single Query to fetch records from multiple tables with different columns

I have 2 different tables as want to get records in a single query. Currently, I am using 2 queries then merging the array result and then displaying the record. Following is my current code:
$db = JFactory::getDbo();
$query1 = "SELECT a.id as cId, a.title, a.parent_id,a.level FROM `categories` AS a WHERE ( a.title LIKE '%keyword%' )";
$result1 = $db->setQuery($query1)->loadObjectlist(); //gives selected records
$query2 = "SELECT b.id as indId, b.indicator , b.cat_id, b.subcat_id, b.section_id FROM `indicator` as b WHERE ( b.indicator LIKE '%keyword%' )";
$result2 = $db->setQuery($query2)->loadObjectlist(); //gives selected records
$_items = array_merge($result1,$result2); //then using $_items in php code to display the data
It is in Joomla however I just want to know how we can merge these 2 queries into one. I tried the following but it gives the result of first query from categories table.
(SELECT id as cId, title, parent_id,level, NULL FROM `categories` WHERE ( title LIKE '%birth%' ))
UNION ALL
(SELECT id as indId, indicator , cat_id, subcat_id, section_id FROM `indicator` WHERE ( indicator LIKE '%birth%' ))
Desired output:
+------+-------------+------------+--------+--------+----------------+--------+-----------+----------+
| cId | title | parent_id | level | indId | indicator | cat_id | subcat_id | section_id
+------+-------------+------------+--------+--------+----------------+--------+-----------+----------+
| 2874 | births | 2703 | 2 | null | null | null | null | null |
+------+-------------+------------+--------+--------+----------------+--------+-----------+----------+
| 13 | birth weight| 12 | 3 | null | null | null | null | null |
+------+-------------+------------+--------+--------+----------------+--------+-----------+----------+
| null | null | null | null | 135 | resident births| 23 | 25 | 1 |
+------+-------------+------------+--------+--------+----------------+--------+-----------+----------+
| null | null | null | null | 189 | births summary | 23 | 25 | 1 |
+------+-------------+------------+--------+--------+----------------+--------+-----------+----------+
This above output will help to get proper pagination records. I tried to use join but JOIN needs a common column in ON clause. Here, I want all the columns and their values. Basically I want to combine the 2 table records in one query. Any help would be appreciated
Here is an example,
There are a number of ways to do this, depending on what you really want. With no common columns, you need to decide whether you want to introduce a common column or get the product.
Let's say you have the two tables:
parts: custs:
+----+----------+ +-----+------+
| id | desc | | id | name |
+----+----------+ +-----+------+
| 1 | Sprocket | | 100 | Bob |
| 2 | Flange | | 101 | Paul |
+----+----------+ +-----+------+
Forget the actual columns since you'd most likely have a customer/order/part relationship in this case; I've just used those columns to illustrate the ways to do it.
A cartesian product will match every row in the first table with every row in the second:
> select * from parts, custs;
id desc id name
-- ---- --- ----
1 Sprocket 101 Bob
1 Sprocket 102 Paul
2 Flange 101 Bob
2 Flange 102 Paul
That's probably not what you want since 1000 parts and 100 customers would result in 100,000 rows with lots of duplicated information.
Alternatively, you can use a union to just output the data, though not side-by-side (you'll need to make sure column types are compatible between the two selects, either by making the table columns compatible or coercing them in the select):
> select id as pid, desc, '' as cid, '' as name from parts
union
select '' as pid, '' as desc, id as cid, name from custs;
pid desc cid name
--- ---- --- ----
101 Bob
102 Paul
1 Sprocket
2 Flange
In some databases, you can use a rowid/rownum column or pseudo-column to match records side-by-side, such as:
id desc id name
-- ---- --- ----
1 Sprocket 101 Bob
2 Flange 101 Bob
The code would be something like:
select a.id, a.desc, b.id, b.name
from parts a, custs b
where a.rownum = b.rownum;
It's still like a cartesian product but the where clause limits how the rows are combined to form the results (so not a cartesian product at all, really).
I haven't tested that SQL for this since it's one of the limitations of my DBMS of choice, and rightly so, I don't believe it's ever needed in a properly thought-out schema. Since SQL doesn't guarantee the order in which it produces data, the matching can change every time you do the query unless you have a specific relationship or order by clause.
I think the ideal thing to do would be to add a column to both tables specifying what the relationship is. If there's no real relationship, then you probably have no business in trying to put them side-by-side with SQL.
As #Sinto suggested the answer for union and dummy column names following is the whole correct query:
(SELECT id as cId, title, parent_id,level, NULL as indId, NULL as indicator , NULL as cat_id, NULL as subcat_id, NULL as section_id FROM `jm_categories` WHERE ( title LIKE '%births%' )) UNION ALL (SELECT NULL as cId, NULL as title, NULL as parent_id,NULL as level, id as indId, indicator , cat_id, subcat_id, section_id FROM `jm_indicator_setup` WHERE ( indicator LIKE '%births%' ))
We have to match the column names from both tables so that we get records as a combination.

how do i get this kind of result ? i'm not sure how its called [duplicate]

I have 2 tables - reservation:
id | some_other_column
----+------------------
1 | value
2 | value
3 | value
And second table - reservation_log:
id | reservation_id | change_type
----+----------------+-------------
1 | 1 | create
2 | 2 | create
3 | 3 | create
4 | 1 | cancel
5 | 2 | cancel
I need to select only reservations NOT cancelled (it is only ID 3 in this example).
I can easily select cancelled with a simple WHERE change_type = cancel condition, but I'm struggling with NOT cancelled, since the simple WHERE doesn't work here.
SELECT *
FROM reservation
WHERE id NOT IN (select reservation_id
FROM reservation_log
WHERE change_type = 'cancel')
OR:
SELECT r.*
FROM reservation r
LEFT JOIN reservation_log l ON r.id = l.reservation_id AND l.change_type = 'cancel'
WHERE l.id IS NULL
The first version is more intuitive, but I think the second version usually gets better performance (assuming you have indexes on the columns used in the join).
The second version works because LEFT JOIN returns a row for all rows in the first table. When the ON condition succeeds, those rows will include the columns from the second table, just like INNER JOIN. When the condition fails, the returned row will contain NULL for all the columns in the second table. The WHERE l.id IS NULL test then matches those rows, so it finds all the rows that don't have a match between the tables.
Just for completeness (and I honestly believe it fits better), I encourage you to use a simple NOT EXISTS.
SELECT * FROM reservation R
WHERE NOT EXISTS (
SELECT 1 FROM reservation_log
WHERE reservation_id = R.id
AND change_type = 'cancel'
);

Query based on three database tables always returns zero results

I have a database with three tables in it:
places:
id | name | latitude | longitude |
------|--------|------------|------------|
1 | place1 | 11.123456 | 76.123456 |
------|--------|------------|------------|
2 | place2 | 23.123456 | 65.123456 |
etc ...
categorized_places:
id | place_id | cat_id |
------|----------|--------|
1 | 1 | 2 |
------|----------|--------|
2 | 2 | 1 |
etc ...
places_visited:
id | user_name | user_email | place_id |
------|-----------|------------|----------|
1 | user_1 | x#mail.com | 2 |
------|-----------|------------|----------|
2 | user_2 | y#mail.com | 2 |
There's also a fourth named categories, but it's not important in this.
I'm trying to filter the places from the places-table to show the user the nearest place, that he/she has not yet visited.
$cur_cat is set on the previous page, where the user selects which kind of place he/she would like to visit.
$cur_user and $cur_user_email are based on $_SESSION variables
$max_lat, $max_lon, $min_lat and $min_lon are based on the users current position
I'm using this code in php (with PDO), but it always returns zero results:
$get_places = $db->prepare("
SELECT
places.id,
places.name,
places.latitude,
places.longitude
FROM
places,
categorized_places,
places_visited
WHERE
places.id = categorized_places.place_id
AND categorized_places.cat_id = '$cur_cat'
AND places.latitude <= '$max_lat'
AND places.latitude >= '$min_lat'
AND places.longitude <= '$max_lon'
AND places.longitude >= '$min_lon'
AND places_visited.user_name = '$cur_user'
AND places_visited.user_email = '$cur_user_email'
AND places.id != places_visited.place_id
");
$get_places->execute();
The code always shows 0 results and throws no error. I've also made sure, that the places are not already in the places_visited table.
I've stared at this for so very long now, and I just can't figure out the error.
Any help would be very appreciated!
Your query is doing inner joins. So, it can only return places that the user has visited. No way that it can return places that a user hasn't visited. Before proceeding further, here is a simple rule: Never use commas in the from clause. ANSI standard explicit JOIN syntax has been around for over two decades, and you should use it. In fact, in this case, you need it, because you need an outer join:
SELECT p.id, p.name, p.latitude, p.longitude
FROM places p INNER JOIN
categorized_places cp
ON p.id = cp.place_id LEFT JOIN
places_visited pv
ON pv.place_id = p.id AND
pv.user_name = '$cur_user' AND
pv.user_email = '$cur_user_email'
WHERE cp.cat_id = '$cur_cat' AND
p.latitude <= '$max_lat' AND
p.latitude >= '$min_lat' AND
p.longitude <= '$max_lon' AND
p.longitude >= '$min_lon' AND
pv.place_id IS NULL;
What this does is it matches the conditions to all the places visited, using an outer join. Then the condition pv.place_id IS NULL chooses the ones that have not been visited. Note that the conditions on the places_visited table go in the ON clause. The conditions on the other two tables remain in the WHERE clause. In general, when using LEFT OUTER JOIN, the filters on the first table stay in the WHERE clause. The filters on the second table go in the ON clause.
I also introduced table aliases. These help make queries easier to write and to read.

How best to get someone's 'rank' from a scores table with php and mysql without looping

My table will hold scores and initials.
But the table wont be ordered.
I can get the total row count easy enough and I know I can get all of them and Order By and then loop through them and get the rank THAT way...
But is there a better way? Could this maybe be done with the SQL statement?
I'm not TOO concerned about performance so if the SQL statement is some crazy thing, then Ill just loop.
Sorry - Table has id as primary key, a string to verify unique app install, a column for initials and a column for score.
When someone clicks GET RANK... I want to be able to tell them that their score is 100 out of 1000 players.
SELECT s1.initials, (
SELECT COUNT(*)
FROM scores AS s2
WHERE s2.score > s1.score
)+1 AS rank
FROM scores AS s1
Do you have a primary key for the table or are you fetching data by the initials or are you just fetching all the data and looping through it to get the single record? How many results are you trying to fetch?
If you only want to fetch one record, as the title suggests, you would query the database using a WHERE conditional:
SELECT score FROM table WHERE user_id = '1';
See this answer: https://stackoverflow.com/a/8684441/125816
In short, you can do something like this:
SELECT id, (#next_rank := IF(#score <> score, 1, 0)) nr,
(#score := score) score, (#r := IF(#next_rank = 1, #r + 1, #r)) rank
FROM rank, (SELECT #r := 0) dummy1
ORDER BY score DESC;
And it will produce a result like this:
+------+----+-------+------+
| id | nr | score | rank |
+------+----+-------+------+
| 2 | 1 | 23 | 1 |
| 4 | 1 | 17 | 2 |
| 1 | 0 | 17 | 2 |
| 5 | 1 | 10 | 3 |
| 3 | 1 | 2 | 4 |
+------+----+-------+------+
Note that users with equal scores have equal ranks. :-)

Categories