using max and limit in a compund MySQL statement - mysql

I have a simple process I'm trying to do in a single SQL statement.
I've got a table of players (called tplayers) with columns indicating what their userid and tourneyid are, as well as a "playerpoints" column. I've also got a table called "tscores" which contains scores, a userid and column called "rankpoints" - I want to take the top 3 rows per player with the highest rankpoints and put that value in the corresponding user record in tplayers -- all for a specific tourneyid.
Here's the query:
update tplayers p set playerpoints=
(
select sum(b.mypoints) y from
(
select scorerankpoints as mypoints from tscores t where t.tourneyid=p.tourneyid and p.userid=t.userid and t.scorerankpoints>0 order by scorerankpoints desc limit 3
) as b
) where p.tourneyid='12'
This generates this error: Unknown column 'p.tourneyid' in 'where clause'
I'm basically looking to take the top 3 values of "scorerankpoints" from table tscores and put the summed value into a column in table tplayers called playerpoints,
and I want to do this for all players and scores who have the same tourneyid in their tables.
It appears that the inner reference to p.tourneyid is undefined... Is there a way to do this in a single statement or do I have to break it up?

MySQL has a problem resolving correlated references that are more than one layer deep. This is a hard one to fix.
The following uses variables to enumerate the rows and then choosing the right rows for aggregation in an update/join:
update tplayers p join
(select ts.userid, sum(ts.scorerankpoints) as mypoints
from (select ts.*,
#rn := if(#userid = userid, 1, #rn + 1) as rn,
#userid := #userid
from tscores ts cross join
(select #rn := 0, #userid := '') const
where ts.tourneyid = '12'
order by ts.userid, ts.scorerankpoints desc
) ts
where rn <= 3
) ts
on p.userid = ts.userid
set playerpoints = ts.mypoints
where p.tourneyid = '12' ;

Related

Optimizing Sub Query Getting Last Entry Based On Group

We had a bug in our code that cached the wrong values to the last_order_id (expected_previous_order) column. The query I wrote properly finds the correct last order id but is too slow for our data set.
I want to select this data into another table but I cannot because the query will take too long
I have setup a simple example here with a smaller data set. Original table has about 170k rows.
SQL Fiddle of my Example
In the example:
original_artwork_id is how these rows are grouped.
order_id is the current rows order id
actual_previous_order is the corrected last order id
expected_previous_order is the currently stored last order id. This is the wrong value as it does not actually reference the last order id
EXPLAIN Results
EDIT
Every time a reorder is placed a new entry is placed into the order_artwork table with a reference to the original_artwork_id and last_order_id.
The reference in the current data set to the last_order_id is wrong.
I need to update all records to properly indicate the last order id.
I am doing this by trying to find each artwork and joining it with the previous entry of the same original_artwork_id. Then I can pull the order_id from the last entry to update the current entries last_order_id
Join the current row with the previous row created before the current row with the same original_artwork_id or the current row original_artwork_id = the previous rows id
Not sure if this will be faster than your current query. But anyway.
SQL DEMO
First you need add a new field
`pos` int DEFAULT 0,
And update your base case so can do the JOIN.
update `order_artwork` o
SET `original_artwork_id` = `id`
WHERE `original_artwork_id` IS NULL;
You could use COALESCE(original_artwork_id, id) but cant use index on that case.
Then assign a row_number to each order based in original_artwork_id and date
update `order_artwork` o
left join (
SELECT o.id,
#rn := if(#order_id = `original_artwork_id`,
#rn + 1,
if(#order_id := `original_artwork_id`, 1, 1)
) as rn
FROM `order_artwork` o
CROSS JOIN (SELECT #id := 0, #order_id := 0, #rn := 0) as var
ORDER BY `original_artwork_id`,
`created`
) b on
o.id = b.id
set
o.pos = b.rn;
Finally update the last order.
UPDATE `order_artwork` o
JOIN (
SELECT o1.original_artwork_id,
o2.order_id,
o1.order_id as last_order_id
FROM `order_artwork` o1
LEFT JOIN `order_artwork` o2
ON o1.pos = o2.pos - 1
AND o1.original_artwork_id = o2.`original_artwork_id`
WHERE o2.pos IS NOT NULL
) as b
ON o.original_artwork_id = b.original_artwork_id
AND o.order_id = b.order_id
SET o.last_order_id = b.last_order_id;
I found that the created time column was not reliable. So I decided to just find the last highest order id with the same original_artwork_id.
Create a table that has the corrected values
CREATE TABLE order_artwork_two AS
select
d1.id,
d1.order_id,
max(d2.order_id) last_order_id,
d1.original_artwork_id
from order_artwork d1
left join order_artwork d2
ON d1.original_artwork_id = d2.original_artwork_id
and d1.order_id > d2.order_id
group by d1.original_artwork_id, d1.order_id;
Add an index to the new table. Otherwise the update would be way too slow
alter table order_artwork_two add primary KEY(id);
Update our original table.
update order_artwork d1
left join order_artwork_two d2 on d2.id = d1.id
set d1.last_order_id = d2.last_order_id;

insert / update records from one table to another table, no clear join

I have a list of sku's in one table that I need to assign to product id's in another table the same way that one would in excel, by copying records from a column of sku's and pasting it next to the a column of product id's starting at the first row. I'd like to do this with an update query or other.
table1: tmp_pid
fields: pid, sku
This is where I have a random number of pid records. The sku field is empty. I'm trying to fill it with date from the next table.
table2: tmp_sku
fields: sku, used
This is where I keep a very long list of unique sku's and whether they have been used.
I tried this query but it does not work ([Err] 1054 - Unknown column 'tmp_sku.sku' in 'IN/ALL/ANY subquery')
UPDATE tmp_pid
SET tmp_pid.sku = tmp_sku.sku
WHERE tmp_sku.sku IN (SELECT sku FROM tmp_sku WHERE used = NO )
Table1 can have 20 or 1000 pid records, Table2 has 10000 unused sku's. I only need to copy the needed sku's next to the 20-1000 pid records in Table1. I know there is no connecting key between the two, but I am limited to this structure.
If I understand correctly, you want to get this result:
select p.*, s.sku
from (select p.*, (#rnp := #rnp + 1) as n
from tmp_pid p cross join (select #rnp := 0) params
order by pid
) p join
(select s.*, (#rns := #rns + 1) as n
from tmp_sku s cross join (select #rns := 0) params
where used = 'NO'
order by sku
) s
on p.n = s.n;
If so, you can adapt this to an update:
update tmp_pid p join
(select p.*, (#rnp := #rnp + 1) as n
from tmp_pid p cross join (select #rnp := 0) params
order by pid
) pp
on p.pid = pp.pid join
(select s.*, (#rns := #rns + 1) as n
from tmp_sku s cross join (select #rns := 0) params
order by sku
) s
on pp.n = s.n
set p.sku = s.sku;

MYSQL Deleting all records over 15 for Each GROUP

At the end of this process I need to have a maximum of 15 records for each type in a table
My (hypothetical) table "stickorder" has 3 columns: StickColor, OrderNumber, PrimeryKey. (OrderNumber, PrimeryKey are unique)
I can only handle 15 orders for each stick color So I need to delete all the extra orders (They will be processed another day and are in a master table so I don't need them in this table.)
I have tried some similar solutions on this site but nothing seem to work, this is the closest
INSERT INTO stickorder2
(select posts_ordered.*
from (
select
stickorder.*,
#row:=if(#last_order=stickorder.OrderNumber, #row+1, 1) as row,
#last_orders:=stickorder.OrderNumber
from
stickorder inner join
(select OrderNumber from
(select distinct OrderNumber
from stickorder
order by OrderNumber) limit_orders
) limit_orders
on stickorder.OrderNumber = limit_orders.OrderNumber,
(select #last_order:=0, #row:=0) r
) posts_ordered
where row<=15);
When using insert, you should always list the columns. Alternatively, you might really want create table as.
Then, there are lots of other issues with your query. For instance, you say you want a limit on the number for each color, and yet you have no reference to StickColor in your query. I think you want something more along these lines:
INSERT INTO stickorder2(col1, . . . col2)
select so.*
from (select so.*,
#row:=if(#lastcolor = so.StickColor, #row+1,
if(#lastcolor := so.lastcolor, 1, 1)
) as row
from stickorders so cross join
(select #lastcolor := 0, #row := 0) vars
order by so.StickColor
) so
where row <= 15;

How to optimize query if table contain 10000 entries using MySQL?

When I execute this query like this they take so much execution time because user_fans table contain 10000 users entries. How can I optimize it?
Query
SELECT uf.`user_name`,uf.`user_id`,
#post := (SELECT COUNT(*) FROM post WHERE user_id = uf.`user_id`) AS post,
#post_comment_likes := (SELECT COUNT(*) FROM post_comment_likes WHERE user_id = uf.`user_id`) AS post_comment_likes,
#post_comments := (SELECT COUNT(*) FROM post_comments WHERE user_id = uf.`user_id`) AS post_comments,
#post_likes := (SELECT COUNT(*) FROM post_likes WHERE user_id = uf.`user_id`) AS post_likes,
(#post+#post_comments) AS `sum_post`,
(#post_likes+#post_comment_likes) AS `sum_like`,
((#post+#post_comments)*10) AS `post_cal`,
((#post_likes+#post_comment_likes)*5) AS `like_cal`,
((#post*10)+(#post_comments*10)+(#post_likes*5)+(#post_comment_likes*5)) AS `total`
FROM `user_fans` uf ORDER BY `total` DESC lIMIT 20
I would try to simplify this COMPLETELY by putting triggers on your other tables, and just adding a few columns to your User_Fans table... One for each respective count() you are trying to get... from Posts, PostLikes, PostComments, PostCommentLikes.
When a record is added to whichever table, just update your user_fans table to add 1 to the count... it will be virtually instantaneous based on the user's key ID anyhow. As for the "LIKES"... Similar, only under the condition that something is triggered as a "Like", add 1.. Then your query will be a direct math on the single record and not rely on ANY joins to compute a "weighted" total value. As your table gets even larger, the queries too will get longer as they have more data to pour through and aggregate. You are going through EVERY user_fan record which in essence is querying every record from all the other tables.
All that being said, keeping the tables as you have them, I would restructure as follows...
SELECT
uf.user_name,
uf.user_id,
#pc := coalesce( PostSummary.PostCount, 000000 ) as PostCount,
#pl := coalesce( PostLikes.LikesCount, 000000 ) as PostLikes,
#cc := coalesce( CommentSummary.CommentsCount, 000000 ) as PostComments,
#cl := coalesce( CommentLikes.LikesCount, 000000 ) as CommentLikes,
#pc + #cc AS sum_post,
#pl + #cl AS sum_like,
#pCalc := (#pc + #cc) * 10 AS post_cal,
#lCalc := (#pl + #cl) * 5 AS like_cal,
#pCalc + #lCalc AS `total`
FROM
( select #pc := 0,
#pl := 0,
#cc := 0,
#cl := 0,
#pCalc := 0
#lCalc := 0 ) sqlvars,
user_fans uf
LEFT JOIN ( select user_id, COUNT(*) as PostCount
from post
group by user_id ) as PostSummary
ON uf.user_id = PostSummary.User_ID
LEFT JOIN ( select user_id, COUNT(*) as LikesCount
from post_likes
group by user_id ) as PostLikes
ON uf.user_id = PostLikes.User_ID
LEFT JOIN ( select user_id, COUNT(*) as CommentsCount
from post_comment
group by user_id ) as CommentSummary
ON uf.user_id = CommentSummary.User_ID
LEFT JOIN ( select user_id, COUNT(*) as LikesCount
from post_comment_likes
group by user_id ) as CommentLikes
ON uf.user_id = CommentLikes.User_ID
ORDER BY
`total` DESC
LIMIT 20
My variables are abbreviated as
"#pc" = PostCount
"#pl" = PostLikes
"#cc" = CommentCount
"#cl" = CommentLike
"#pCalc" = weighted calc of post and comment count * 10 weighted value
"#lCalc" = weighted calc of post and comment likes * 5 weighted value
The LEFT JOIN to prequeries runs those queries ONCE through, then the entire thing is joined instead of being hit as a sub-query for every record. By using the COALESCE(), if there are no such entries in the LEFT JOINed table results, you won't get hit with NULL values messing up the calcs, so I've defaulted them to 000000.
CLARIFICATION OF YOUR QUESTIONS
You can have any QUERY as an "AS AliasResult". The "As" can also be used to simplify any long table names for simpler readability. Aliases can also be using the same table but as a different alias to get similar content, but for different purpose.
select
MyAlias.SomeField
from
MySuperLongTableNameInDatabase MyAlias ...
select
c.LastName,
o.OrderAmount
from
customers c
join orders o
on c.customerID = o.customerID ...
select
PQ.SomeKey
from
( select ST.SomeKey
from SomeTable ST
where ST.SomeDate between X and Y ) as PQ
JOIN SomeOtherTable SOT
on PQ.SomeKey = SOT.SomeKey ...
Now, the third query above is not practical requiring the ( full query resulting in alias "PQ" representing "PreQuery" ). This could be done if you wanted to pre-limit a certain set of other complex conditions and wanted a smaller set BEFORE doing extra joins to many other tables for all final results.
Since a "FROM" does not HAVE to be an actual table, but can be a query in itself, any place else used in the query, it has to know how to reference this prequery resultset.
Also, when querying fields, they too can be "As FinalColumnName" to simplify results to where ever they will be used too.
select
CONCAT( User.Salutation, User.LastName ) as CourtesyName
from ...
select
Order.NonTaxable
+ Order.Taxable
+ ( Order.Taxable * Order.SalesTaxRate ) as OrderTotalWithTax
from ...
The "As" columnName is NOT required being an aggregate, but is most commonly seen that way.
Now, with respect to the MySQL variables... If you were doing a stored procedure, many people will pre-declare them setting their default values before the rest of the procedure. You can do them in-line in a query by just setting and giving that result an "Alias" reference. When doing these variables, the select will simulate always returning a SINGLE RECORD worth of the values. Its almost like an update-able single record used within the query. You don't need to apply any specific "Join" conditions as it may not have any bearing on the rest of the tables in a query... In essence, creates a Cartesian result, but one record against any other table will never create duplicates anyhow, so no damage downstream.
select
...
from
( select #SomeVar := 0,
#SomeDate := curdate(),
#SomeString := "hello" ) as SQLVars
Now, how the sqlvars work. Think of a linear program... One command is executed in the exact sequence as the query runs. That value is then re-stored back in the "SQLVars" record ready for the next time through. However, you don't reference it as SQLVars.SomeVar or SQLVars.SomeDate... just the #SomeVar := someNewValue. Now, when the #var is used in a query, it is also stored as an "As ColumnName" in the result set. Some times, this can be just a place-holder computed value in preparation of the next record. Each value is then directly available for the next row. So, given the following sample...
select
#SomeVar := SomeVar * 2 as FirstVal,
#SomeVar := SomeVar * 2 as SecondVal,
#SomeVar := SomeVar * 2 as ThirdVal
from
( select #SomeVar := 1 ) sqlvars,
AnotherTable
limit 3
Will result in 3 records with the values of
FirstVal SecondVal ThirdVal
2 4 8
16 32 64
128 256 512
Notice how the value of #SomeVar is used as each column uses it... So even on the same record, the updated value is immediately available for the next column... That said, now look at trying to build a simulated record count / ranking per each customer...
select
o.CustomerID,
o.OrderID
#SeqNo := if( #LastID = o.CustomerID, #SeqNo +1, 1 ) as CustomerSequence,
#LastID := o.CustomerID as PlaceHolderToSaveForNextRecordCompare
from
orders o,
( select #SeqNo := 0, #LastID := 0 ) sqlvars
order by
o.CustomerID
The "Order By" clause forces the results to be returned in sequence first. So, here, the records per customer are returned. First time through, LastID is 0 and customer ID is say...5. Since different, it returns 1 as the #SeqNo, THEN it preserves that customer ID into the #LastID field for the next record. Now, next record for customer... Last ID is the the same, so it takes the #SeqNo (now = 1), and adds 1 to 1 and becomes #2 for the same customer... Continue on the path...
As for getting better at writing queries, take a look at the MySQL tag and look at some of the heavy contributors. Look into the questions and some of the complex answers and how problems solving works. Not to say there are not others with lower reputation scores just starting out and completely competent, but you'll find who gives good answers and why's. Look at their history of answers posted too. The more you read and follow, the more you'll get a better handle on writing more complex queries.
You can convert this query to Group By clause, instead of using Subquery for each column.
You can create indexes on the relationship parameters ( it will be the most helpful way of optimizing your query response ).
1000 user records isn't much data at all.
There may be work you can do on the database itself:
1) Have you got the relevant indexes set on the foreign keys (indexes set on user_id in each of the tables)? Try running EXPLAIN before the query http://www.slideshare.net/phpcodemonkey/mysql-explain-explained
2) Are your data types correct?
See the difference between #me(see image 1) and #DRapp(see image 2) Query execution time and explain. When i read #Drapp answer i realized that what am i doing wrong in this query and why my query take so much time basically answer is so simple my query dependent on subquery or #Drapp used derived (temporary/file sort) with the help of session variables , Alias and joins...
image 1 exe time (00:02:56:321)
image 2 exe time (00:00:32:860)

Auto-increment with Group BY

I have two tables as follows:
Contract
|
Contractuser
My job was to fetch latest invoice date for each contract number from Contractuser table and display results. The resultant table was as follows:
Result Table
Now I wanted to get a auto-increment column to display as the first column in my result set.
I used the following query for it:
SELECT #i:=#i+1 AS Sno,a.ContractNo,a.SoftwareName,a.CompanyName,b.InvoiceNo,b.InvoiceDate,
b.InvAmount,b.InvoicePF,max(b.InvoicePT) AS InvoicePeriodTo,b.InvoiceRD,b.ISD
FROM contract as a,contractuser as b,(SELECT #i:=0) AS i
WHERE a.ContractNo=b.ContractNo
GROUP BY b.ContractNo
ORDER BY a.SoftwareName ASC;
But it seems that the auto-increment is getting performed before the group by procedure because of which serial numbers are getting displayed in a non contiguous manner.
GROUP BY and variables don't necessarily work as expected. Just use a subquery:
SELECT (#i := #i + 1) AS Sno, c.*
FROM (SELECT c.ContractNo, c.SoftwareName, c.CompanyName, cu.InvoiceNo, cu.InvoiceDate,
cu.InvAmount, cu.InvoicePF, max(cu.InvoicePT) AS InvoicePeriodTo, cu.InvoiceRD, cu.ISD
FROM contract c JOIN
contractuser as b
ON c.ContractNo = cu.ContractNo
GROUP BY cu.ContractNo
ORDER BY c.SoftwareName ASC
) c CROSS JOIN
(SELECT #i := 0) params;
Notes:
I also fixed the JOIN syntax. Never use commas in the FROM clause.
I also added reasonable table aliases -- abbreviations for the tables. a and b don't mean anything, so they make the query harder to follow.
I left the GROUP BY with only one key. It should really have all the unaggregated keys but this is allowed under some circumstances.
SELECT #row_no := IF(#prev_val = lpsm.lit_no, #row_no + 1, 1) AS row_num,#prev_val := get_pad_value(lpsm.lit_no,26) LAWSUIT_NO,lpsm.cust_rm_no
FROM lit_person_sue_map lpsm,(SELECT #row_no := 0) x,(SELECT #prev_val := '') y
ORDER BY lpsm.lit_no ASC;
This will return sequence number group by lit_no;