I am trying to do something like this in MYSQL, but without making query multiple times (50 times, in my case) through a PHP foreach.
foreach($this->map_ids as $key => $val) {
$this->db->query("SELECT scores.profile_number, scores.score FROM scores
LEFT JOIN players ON scores.profile_number = players.profile_number
WHERE scores.map_id = {'$val'}
AND scores.profile_number IN (SELECT profile_number FROM players WHERE banned = 0) LIMIT 10");
}
This is how it looks approximately when I retrieve all scores without LIMIT.
profile score map_id
76561198026851335 2478 47455
76561198043770492 2480 47455
... ... ...
76561198043899549 1340 47452
76561198048179892 1345 47452
... ... ...
I want only 10 entries (scores) from each unique map_id.
This is surprisingly difficult to do but I've ended up using user variables to do the job, check out the following demo. Obviously my data structure is much simplified but it should be enough to get you going:
SQL Fiddle example
Here is the SQL for anyone who may be interested in skipping the demo (hideous, I know)
SELECT *
FROM (
SELECT profile_number, score, map_id
FROM (
SELECT
profile_number, score, map_id,
IF( #prev <> map_id, #rownum := 1, #rownum := #rownum+1 ) AS rank,
#prev := map_id
FROM scores
JOIN (SELECT #rownum := NULL, #prev := 0) AS r
ORDER BY map_id
) AS tmp
WHERE tmp.rank <= 10
) s
JOIN players p
ON s.profile_number = p.profile_number
Basically, what is happening is this:
ORDER BY map_id
Orders your table by map_id so that all the same ones are together.
Next we assign a rownumber to each row by using the following logic:
IF( #prev <> map_id, #rownum := 1, #rownum := #rownum+1 )
If the previous row's map_id is not equal to the current row's ID, set the row number = 1, otherwise increase the rownumber by 1.
Finally, only return the rows who have a rownumber less than or equal to 10
WHERE tmp.rank <= 10
Hope that makes it a little clearer for you.
You can use the limit directive.
SELECT * FROM `your_table` LIMIT 0, 10
This will display the first 10 results from the database.
SELECT * FROM `your_table` LIMIT 5, 5
This will show records 6, 7, 8, 9, and 10
Related
there are 2 tables:
crash and traffic_flow.
crash table has attributes as crash_date, time, and corresponding detector ID.
traffic_flow table recorded by detectors has attributes date,time,detector_ID, auto-incrementing id and traffic flow parameters.
Now I'm willing to random select 10 rows in traffic_flow for each row in crash respectively and insert them in a new table.
The following is a trial:
select traffic_flow.id
from traffic_flow,crash
where traffic_flow.date=crash.date and traffic_flow.ID=crash.ID
order by rand()
limit 10;
but this sql statement select 10 rows in total for all crash records, not for each row in crash, so it can't meet my requirement. could you please modify the statement for me?
In MySQL, the simplest method is to use variables to enumerate the rows for each crash:
select *
from (select ct.*,
(#rn := if(#ct = id, #rn + 1,
if(#ct := id, 1, 1)
)
) as rn
from (select c.*, tf.id as tf_id
from traffic_flow tf join
crash c
on tf.date = c.date and tf.ID = c.ID
order by c.id, rand()
) ct cross join
(select #cid := -1, #rn := 0) params
)
where rn <= 10;
I have a table in which I need to query 9 returns, each of which need to be of a random order each time, they cannot duplicate except for one value of TYPE X. The problem is that if using GROUP BY, it only returns 8 results, but I require 9, with 2 results having the same TYPE value.
The draft query I have been using so far has been:
"SELECT * FROM (SELECT * FROM questions ORDER BY RAND() ) R GROUP BY type;
// TABLE REMOVED, FORMATTING NOT WORKING, IMAGE INSTEAD
If you want to fetch random rows from groups in MySQL, I think the safest way is to use variables. This is the ordering:
select q.*
from (select q.*,
(#rn := if(#q = questionid, #rn + 1,
if(#q := questionid, 1, 1)
)
) as rn
from questions q cross join
(select #q := 0, #rn := 0) params
order by questionid, rand()
) q
Then, if you know you have 8 questions but you want 2 from the second:
where rn = 1 or (questionid = 2 and rn = 2)
Or, if you want 9 random questions as non-overlapping as possible:
order by rn
limit 9
I am using a modified version of a query similiar to another question here:Convert SQL Server query to MySQL
Select *
from
(
SELECT tbl.*, #counter := #counter +1 counter
FROM (select #counter:=0) initvar, tbl
Where client_id = 55
ORDER BY ordcolumn
) X
where counter >= (80/100 * #counter);
ORDER BY ordcolumn
tbl.* contains the field 'client_id' and I am attempting to get the top 20% of the records for each client_id in a single statement. Right now if I feed it a single client_id in the where statement it gives me the correct results, however if I feed it multiple client_id's it simply takes the top 20% of the combined recordset instead of doing each client_id individually.
I'm aware of how to do this in most databases, but the logic in MySQL is eluding me. I get the feeling it involves some ranking and partitioning.
Sample data is pretty straight forward.
Client_id rate
1 1
1 2
1 3
(etc to rate = 100)
2 1
2 2
2 3
(etc to rate = 100)
Actual values aren't that clean, but it works.
As an added bonus...there is also a date field associated to these records and 1 to 100 exists for this client for multiple dates. I need to grab the top 20% of records for each client_id, year(date),month(date)
You need to do the enumeration for each client:
SELECT *
FROM (SELECT tbl.*, #counter := #counter +1 counter
(#rn := if(#c = client_id, #rn + 1,
if(#c := client_id, 1, 1)
)
)
FROM (select #c := -1, #rn := 0) initvar CROSS JOIN tbl
ORDER BY client_id, ordcolumn
) t cross join
(SELECT client_id, COUNT(*) as cnt
FROM tbl
GROUP BY client_id
) tt
where rn >= (80/100 * tt.cnt);
ORDER BY ordcolumn;
Using Gordon's answer as a starting point, I think this might be closer to what you need.
SELECT t.*
, (#counter := #counter+1) AS overallRow
, (#clientRow := if(#prevClient = t.client_id, #clientRow + 1,
if(#prevClient := t.client_id, 1, 1) -- This just updates #prevClient without creating an extra field, though it makes it a little harder to read
)
) AS clientRow
-- Alteratively (for everything done in clientRow)
, #clientRow := if(#prevClient = t.client_id, #clientRow + 1, 1) AS clientRow
, #prevClient := t.client_id AS extraField
-- This may be more reliable as well; I not sure if the order
-- of evaluation of IF(,,) is reliable enough to guarantee
-- no side effects in the non-"alternatively" clientRow calculation.
FROM tbl AS t
INNER JOIN (
SELECT client_id, COUNT(*) AS c
FROM tbl
GROUP BY client_id
) AS cc ON tbl.client_id = cc.client_id
INNER JOIN (select #prevClient := -1, #clientRow := 0) AS initvar ON 1 = 1
WHERE t.client_id = 55
HAVING clientRow * 5 < cc.c -- You can use a HAVING without a GROUP BY in MySQL
-- (note that clientRow is derived, so you cannot use it in the `WHERE`)
ORDER BY t.client_id, t.ordcolumn
;
Couldn't really explain my problem with words, but with an example I can show it clearly:
I have a table like this:
id num val
0 3 10
1 5 12
2 7 12
3 11 15
And I want to go through all the rows, and calculate the increase of the "num", and multiply that difference with the "val" value. And when I calculated all of these, I want to add these results together.
This is the mathematical equation, that I want to run on the table:
Result = (3-0)*10 + (5-3)*12 + (7-5)*12 + (11-7)*15
138 = Result
Thank you.
You can do with mysql variables, but you will still get one record for each entry.
select
#lastTotal := #lastTotal + ( (yt.num - #lastNum) * yt.val ) thisLineTotal,
#lastNum := yt.num as saveForNextRow,
yt.id
from
yourTable yt,
( select #lastTotal := 0,
#lastNum := 0 ) sqlvars
order by
id
This SHOULD give you what you want to confirm the calculations to each record basis.
Now, to get the one record and one column result, you can wrap it such as
select
pq.thisLineTotal
from
(above entire query ) as pq
order by
pq.id DESC
limit 1
Assuming the IDs are consecutive as your sample data suggests, just join the table to itself:
select sum((t1.num-ifnull(t2.num,0))*t1.val) YourValue
from YourTable t1
left join YourTable t2
on t2.id = t1.id - 1;
http://www.sqlfiddle.com/#!2/40b9f/12
This will give you the total. Make sure to order in the order you wish - I have ordered by id
SET #runtot:=0;
SET #prevval:=0;
select max(rt) as total FROM (
SELECT
q.val,
q.num,
(#runtot := #runtot + (q.num- #prevval) * q.val) AS rt,
(#prevval := q.num) AS pv
FROM thetable q
ORDER by ID) tot
If you want to see the details of the calculation, leave out the outer select as so:
SET #runtot:=0;
SET #prevval:=0;
SELECT
q.val,
q.num,
(#runtot := #runtot + (q.num- #prevval) * q.val) AS rt,
(#prevval := q.num) AS pv
FROM thetable q
ORDER by ID
If it is possible to have negative numbers for your column values, using max(rt) won't work for the total. You should then use:
SET #runtot:=0;
SET #prevval:=0;
select #runtot as total FROM (
SELECT
q.val,
q.num,
(#runtot := #runtot + (q.num- #prevval) * q.val) AS rt,
(#prevval := q.num) AS pv
FROM thetable q
ORDER by ID) tot LIMIT 1
I have a table with numerous columns including category and rating. In the entire table, there are two category values: category 1 and category 2. Rating values range from 1 to 5. The following anayltic query retrieves 4 products of each rating irrespective of category, so, 4 products with rating 1, four products with rating 2 and so on... The (id > 10) is of no particular significance)
SELECT
DISTINCT x.*
FROM
(SELECT t.*,
CASE WHEN #rating != t.rating THEN #rownum := 1
ELSE #rownum := #rownum + 1 END
AS rank,
#rating := t.rating
AS var_rating
FROM products t
JOIN (
SELECT #rownum := NULL, #rating := ''
) r
WHERE ( id > 10 )
ORDER BY t.rating, price
) x
WHERE
x.rank <= 4
I wish to modify it to retrieve 4 products of each rating of each category. Please can someone help.
Many thanks
This page is fantastic at the "select-n-per-group" problem.
I'm basically following it in my solution.
These queries selects the 4 cheapest products per (category,rating). In the case of a tie in price it picks the one with the smaller id.
If you just wanted 4 products per (category,rating) regardless of price then skip out the p.price<=products.price line.
Method 1 (easy to understand):
SELECT *
FROM products
where (
select count(*) from products as p
where p.category = products.category
and p.rating = products.rating
and p.price <= products.price
and p.id < products.id -- in case of tie with price
) < 4
ORDER BY category,rating,price;
The inner query says to count up how many items are cheaper than or the same price as you, and the outer query says to pick only things that have less than 4 items cheaper than/same price as you.
If you want to use user variables you can try:
SET #num := 0, #cat := '', #rating := '';
SELECT category,rating,id,price
FROM (
SELECT category,rating,id,price,
#num := if(#cat = category AND #rating=rating, #num + 1, 1) as row_number,
#cat := category as dummy,
#rating := rating as dummy2
FROM products
ORDER BY category,rating,price,id
) AS p WHERE p.row_number <= 4;
Where the inner query orders your table by category, rating, price and id, and assigns row numbers within each (category,rating) pair. The outer query selects the first 4 from each.
The last method on that page would have this query, but it doesn't work unless you have some sort of index on your columns (and my MySQL foo is not enough to work it out):
ALTER TABLE products ADD KEY(???);
SET #num := 0, #cat := '', #rating := '';
SELECT category,rating,id,price,
#num := if(#cat = category AND #rating=rating, #num + 1, 1) as row_number,
#cat := category as dummy,
#rating := rating as dummy2
FROM products FORCE INDEX (???)
GROUP BY category,rating, ???
HAVING row_number <= 2;
which supposedly relies on MYSQL doing an ORDER BY in the GROUP BY and hence making sure rows are ordered as requested.