UNION result-set part is different than individual SELECT - mysql

The query
(SELECT timest, `char`, exp_cnt AS XP FROM `charac` WHERE DATE(timest)='2017-01-30')
UNION
(SELECT timest, `char`, -exp_cnt AS XP FROM `charac` WHERE DATE(timest)=ADDDATE('2017-01-30',-1))
ORDER BY `char` ASC
LIMIT 50
gives me this result:
Where the date is 01-29, the values should not be 0, usually.
I try to debug it by copying-pasting and running the 2ndSELECT:
SELECT timest, `char`, -exp_cnt AS XP FROM `charac` WHERE DATE(timest)=ADDDATE('2017-01-30',-1)
ORDER BY `char` ASC
And I get correct results!
Why? I am confused. UNION ALL does not help.

First, you should use union all rather than union unless you want to incur the overhead of removing duplicates.
Second, you have a limit on the query. So, your results are probably being generated, they are just further down the list and cut-off.
If you care about what happens on 2017-01-29, then sort the list by date and do the comparison that way:
(SELECT timest, `char`, exp_cnt AS XP
FROM `charac`
WHERE DATE(timest) = '2017-01-30'
) UNION ALL
(SELECT timest, `char`, -exp_cnt AS XP
FROM `charac`
WHERE DATE(timest) = ADDDATE('2017-01-30', -1)
)
ORDER BY timest ASC
LIMIT 50;
This may not be the final result you want, but you will at least see that all rows are accounted for.
If you don't want 0 values on the 29th, then explicitly put that into the query:
(SELECT timest, `char`, exp_cnt AS XP
FROM `charac`
WHERE DATE(timest) = '2017-01-30'
) UNION ALL
(SELECT timest, `char`, -exp_cnt AS XP
FROM `charac`
WHERE DATE(timest) = ADDDATE('2017-01-30', -1) AND
exp_cnt <> 0
)
ORDER BY timest ASC
LIMIT 50;

How many rows for each day? I'll bet there are fewer than 50 for the 30th. I'll bet the UNION got all the rows from the 30th, then some from the 29th. That would explain it.
To further test my theory, run each SELECT in the UNION separately.
Another test: Swap the two SELECTs in the UNION. Now it may lose some of the rows from the 30th.
If you want the first 50 of both, use this pattern:
( SELECT ... ORDER BY timest LIMIT 50 )
UNION ALL
( SELECT ... ORDER BY timest LIMIT 50 )
ORDER BY timest LIMIT 50
If you need an OFFSET, too, it gets messier, but is possible:
For, say, the 3rd page:
Inside, do LIMIT 150 -- to be sure to get at least 150 from each (if available)
Outside, do LIMIT 100, 50 -- to get just the 3rd page after the ORDER BY.
But... There is still another issue. Since timest is not unique, the 50 picked is not really deterministic. So, you might want to change to ORDER BY timest, id or ORDER BY timest, char.

Related

MYSQL LIMIT only the values where "viewed"=1 , but not viewed="0"

This is a tough one for me. I have a table that holds user notifications. There is a column "VIEWED" where 0 means it has NOT been viewed, 1 means it has.
I want to generate my list of notifications, SELECT * that is unviewed (0), however in the case that I have no un viewed notification, I don't want to just display "no notififications" I want to display a few old ones.
I'm already showing my most recent unviewed first. Is there a way to do something that shows ALL unviewed, but only a few viewed? Like below?
$QUERY= "SELECT * FROM point_member_notifications WHERE account_id='$accountid' ORDER BY viewed ASC, created_date DESC (LIMIT 10 WHERE VIEWED='1') "
Maybe you want UNION ALL, to limit the 'VIEWED' old ones, just place the limit clause inside the parentheses that enclose the SELECT
(SELECT * FROM point_member_notifications WHERE account_id='$accountid' ORDER BY viewed ASC)
UNION ALL
(SELECT * FROM point_member_notifications WHERE account_id='$accountid' WHERE VIEWED='1' created_date DESC LIMIT 10);
You could use a combination of LIMIT and UNION (or UNION ALL if you don't want to remove duplicate records). That could look like:
(SELECT *
FROM point_member_notifications WHERE account_id='$accountid' AND VIEWED='1'
ORDER BY created_date DESC LIMIT 10)
UNION ALL
SELECT *
FROM point_member_notifications WHERE account_id='$accountid' AND VIEWED='0'
ORDER BY created_date DESC
LIMIT 10
If you want to have 10 records unviewed + the 10 old records you need to put the 2nd SELECT in brackets too, otherwise the LIMIT 10 at the end will limit the whole thing. An ORDER BY viewed should not be necessary since UNION already selects the unviewed first and then the others.

Mysql: Get records from last date

I want to get all records which are not "older" than 20 days. If there are no records within 20 days, I want all records from the most recent day. I'm doing this:
SELECT COUNT(DISTINCT t.id) FROM t
WHERE
(DATEDIFF(NOW(), t.created) <= 20
OR
(date(t.created) >= (SELECT max(date(created)) FROM t)));
This works so far, but it is awful slow. created is a datetime, might be due tue the conversion to a date... Any ideas how to speed this up?
SELECT COUNT(*) FROM (
SELECT * FROM t WHERE datediff(now(),created) between 0 and 20
UNION
SELECT * FROM (SELECT * FROM t WHERE created<now() LIMIT 1) last1
) last20d
I used the between clause just in case there might be dates in the future in the table. These will be excluded. Also you can simplify the select, if you just need the count() to
SELECT COUNT(*) FROM (
SELECT id FROM t WHERE datediff(now(),created) between 0 and 20
UNION
SELECT id FROM (SELECT id FROM t WHERE created<now() LIMIT 1) last1
) last20d
otherwise, in the first select version you can leave out the outer select if you want all the data of the chosen records. The UNION will make sure that duplicates will be excluded (in other cases I always use UNION ALL since it is faster).

How can I limit query's results without using LIMIT

I need to show ordered 20 records on my grid but I can't use LIMIT because of my generator(Scriptcase) using LIMIT to show lines per page. It's generator's bug but I need to solve it for my project. So is it possible to show 20 ordered record from my table with a query?
As from comments,if you can't use limit then you can rank your results on basis of some order and in parent select filter limit the results by rank number
select * from (
select *
,#r:=#r + 1 as row_num
from your_table_name
cross join (select #r:=0)t
order by some_column asc /* or desc*/
) t1
where row_num <= 20
Demo with rank no.
Another hackish way would be using group_concat() with order by to get the list of ids ordered on asc/desc and substring_index to pick the desired ids like you need 20 records then join with same table using find_in_set ,But this solution will be very expensive in terms of performance and group_concat limitations if you need more than 20 records
select t.*
from your_table_name t
join (
select
substring_index(group_concat(id order by some_column asc),',',20) ids_list
from your_table_name
) t1 on (find_in_set(t.id , t1.ids_list) > 0)
Demo without rank
What about SELECT in SELECT:
SELECT *
FROM (
-- there put your query
-- with LIMIT 20
) q
So outer SELECT is without LIMIT and your generator can add own.
In a Scriptcase Grid, you CAN use Limit. This is a valid SQL query that selects only the first 20 records from a table. The grid is set to show only 10 records per page, so it will show 20 results split in a total of 2 pages:
SELECT
ProductID,
ProductName
FROM
Products
LIMIT 20
Also the embraced query works out well:
SELECT
ProductID,
ProductName
FROM
(SELECT
ProductID,
ProductName
FROM Products LIMIT 20) tmp

SQL Distinct - Get all values

Thanks for looking, I'm trying to get 20 entries from the database randomly and unique, so the same one doesn't appear twice. But I also have a questionGroup field, which should also not appear twice. I want to make that field distinct, but then get the ID of the field selected.
Below is my NOT WORKING script, because it does the ID as distinct too which
SELECT DISTINCT `questionGroup`,`id`
FROM `questions`
WHERE `area`='1'
ORDER BY rand() LIMIT 20
Any advise is greatly appreciated!
Thanks
Try doing the group by/distinct first in a subquery:
select *
from (select distinct `questionGroup`,`id`
from `questions`
where `area`='1'
) qc
order by rand()
limit 20
I see . . . What you want is to select a random row from each group, and then limit it to 20 groups. This is a harder problem. I'm not sure if you can do this accurately with a single query in mysql, not using variables or outside tables.
Here is an approximation:
select *
from (select `questionGroup`
coalesce(max(case when rand()*num < 1 then id end), min(id)) as id
from `questions` q join
(select questionGroup, count(*) as num
from questions
group by questionGroup
) qg
on qg.questionGroup = q.questionGroup
where `area`='1'
group by questionGroup
) qc
order by rand()
limit 20
This uses rand() to select an id, taking, on average two per grouping (but it is random, so sometimes 0, 1, 2, etc.). It chooses the max() of these. If none appear, then it takes the minimum.
This will be slightly biased away from the maximum id (or minimum, if you switch the min's and max's in the equation). For most applications, I'm not sure that this bias would make a big difference. In other databases that support ranking functions, you can solve the problem directly.
Something like this
SELECT DISTINCT *
FROM (
SELECT `questionGroup`,`id`
FROM `questions`
WHERE `area`='1'
ORDER BY rand()
) As q
LIMIT 20

mysql limit inside group?

I want to limit the size of records inside a group, and here is my trial, how to do it right?
mysql> select * from accounts limit 5 group by type;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the
right syntax to use near 'group by type' at line 1
The point of an aggregate function (and the GROUP BY it requires) is to turn many rows into one row. So if you really just want the top 5 savings accounts and the top 5 chequing accounts and the top 5 USD accounts etc., what you need is more like this:
criteria: top 5 of particular account type by account_balance
SELECT account_type, account_balance FROM accounts WHERE account_type='savings'
ORDER BY account_balance DESC LIMIT 5
UNION
SELECT account_type, account_balance FROM accounts WHERE account_type='chequing'
ORDER BY account_balance DESC LIMIT 5
UNION
SELECT account_type, account_balance FROM accounts WHERE account_type='USD'
ORDER BY account_balance DESC LIMIT 5;
It's not pretty, but if you construct the SQL with a script then subbing in the account_types and concatenating together a query is straightforward.
I've had some luck with using numbered rows:
set #type = '';
set #num = 0;
select
items.*,
#num := if(#type = item_type, #num + 1, 1) as dummy_1,
#type := item_type as dummy_2,
#num as row_number
from items
group by
item_type,
row_number
having row_number < 3;
This will give you 2 results per item_type. (One gotcha: make sure you re-run the first two set statements otherwise your row numbers will steadily get higher and higher and the row_number < 3 restriction won't work.
I pieced this together from a couple of posts which have been linked in other answers on SO.
It appears you want to limit the number of rows returned within each group of your overall result set... this is difficult to do in a way that scales well. One technique is to perform N joins on the same table with the conditions such that the only rows that match are the top/bottom N that you want.
this page may offer some additional insight into your solution... although returning the top 5 in each group is going to get ugly fast.
Try placing the LIMIT clause after the GROUP BY clause.
EDIT: Try this:
SELECT *
FROM accounts a1
WHERE 5 >
(
SELECT COUNT(*)
FROM accounts a2
WHERE a2.type = a1.type
AND a2.balance > a1.balance
)
This returns at most 5 accounts of each type with the biggest balances.
Group by is used for aggregate functions (sums, averages...)
Is allows you to split the aggregate result into groups. You have not used one of these functions.
I am not sure you can use a limit in the group by. You can probably use it if your group by is a sub select that returns one row/value. For example:
select * from foo order by (select foo2.id from foo2 limit 1)
I am just guessing this would work.
This will probably do the trick, although if type isn't indexed, it'll be sloooowwww. And even with one, it's not especially fast:
SELECT a.*
FROM accounts a
LEFT JOIN accounts a2 ON (a2.type = a.type AND a2.id < a.id)
WHERE count(a2.id) < 5
GROUP BY a.id;
A better bet would be to just order the list by type and then use a loop at the business layer to remove the rows you don't want.
#dnagirl's answer almost has it, but for some reason, my version of MySQL only returns the first LIMIT'd set. To get around that, I put each statement into a subquery
SELECT * FROM (
SELECT account_type, account_balance FROM accounts WHERE account_type='savings'
ORDER BY account_balance DESC LIMIT 5
) as a
UNION
SELECT* FROM (
SELECT account_type, account_balance FROM accounts WHERE account_type='chequing'
ORDER BY account_balance DESC LIMIT 5
) as b
UNION
SELECT * FROM (
SELECT account_type, account_balance FROM accounts WHERE account_type='USD'
ORDER BY account_balance DESC LIMIT 5
) as c
This gave me back each set's results in the final result set. Otherwise, I would have only gotten the first 5 from the first query and nothing else - not sure if it's just some MySQL funk with my version