CodeIgniter query to exclude a subset from results - php

I'm having some trouble figuring out how to write the proper query after doing a JOIN. I need to get all users in Group 1 while excluding a subset of these results.
Table users:
id name
1 John Smith
2 Joe Blow
3 Mary Jane
Table users_groups:
user_id group_id
1 1
1 3
1 4
2 1
2 4
2 5
3 1
3 6
Everyone in Group 6 will also be in Group 1, however, not everyone in Group 1 will be in Group 6. In other words, Group 6 is a sub-set of Group 1.
I need a query that will give me a list of all users who are in Group 1 (while excluding the users in Group 6). For the example above, I should get two results, John Smith and Joe Blow.
I'm using CodeIgniter v3
Here is my attempt (I removed the cache code for clarity)...
$this->db->from('users');
$this->db->select('
users.id AS `id`,
users.name AS `name`,
users_groups.group_id AS `group_id`
', FALSE);
$this->db->join('users_groups', 'users_groups.user_id = users.id', 'LEFT');
$this->db->group_by('users.email'); // remove duplication caused by JOIN
$this->db->where('users_groups.group_id = 1'); // get all users in Group 1
$this->db->where('users_groups.group_id <> 6'); // ignore all users in Group 6
return $this->db->get()->result_array();
The problem I'm having here is that I always get the full list of users in Group 1. Because the JOIN produces a list of all users and all groups, where the same user is listed multiple times, one entry for every Group that person belongs. My query is removing the Group 6 entries, but this is no good since the same users are also in Group 1.
I just explained why my query is failing, but I still cannot figure out how to achieve success. How do I get the Group 1 users and then remove the subset of users that are in Groups 1 & 6? These users can also be in other Groups, but these should be ignored... I just want to exclude users who are in Groups 1 & 6 from the list of users in Group 1.
Each user in the result:
must be in Group 1
must not be in Group 6
may or may not be in any other Group
Any suggestions appreciated.

You need a "not exists" clause in there as a filter.
And not exists (select 1 from users_groups x where
x.user_id = users_groups.user_id and group_id = 6
Im not familiar with code ignite but im sure this is doable

Thanks to Philip's answer, it's working. This is how to do it within CodeIgniter...
$this->db->where('users_groups.group_id = 1'); // get all users in Group 1
$this->db->where('
NOT EXISTS (
SELECT 1 FROM users_groups x
WHERE x.user_id = users_groups.user_id AND group_id = 6
)
'); // exclude users in Group 6

Related

MySql Statement across different rows

Im trying to query a wordpress mySQL database where the values pertaining to each user is spread across different rows (only god knows why they did it that way, why didnt they just put all the user information on one row)
For example
ID USER_ID FIELD_ID VALUE
1 1 2 my name is paul smith
2 1 3 books
3 1 4 loggedin
4 1 5 state=busy
5 2 2 my name is big boy
5 2 3 pens
6 2 4 offline
7 2 5 state=idle
here you can see each row has a different meaning based on the FIELD_ID for the same user
FIELD_ID=2 contains the user name
FIELD_ID=3 contains mean what they bought
FIELD_ID=4 means logged in
FIELD_ID=5 is busy or idle
I need to make ONE query that will return all users that are LOGGED IN and STATE=IDLE and bought BOOKS (or bought some other item like pens whatever they want to search for)
so i started off with this php statement
$find="pens";
$q = "SELECT * FROM table
WHERE FIELD_ID='4' AND VALUE='loggedin'
AND FIELD_ID='5' AND VALUE='idle'
AND USER_ID IN
(SELECT USER_ID FROM table WHERE FIELD_ID ='3' AND VALUE LIKE '%$find%')
But i dont think this will work because how can
FIELD_ID = '4' AND VALUE='loggedin' AND FIELD_ID='5' AND VALUE='idle' all at the same time because they are on different rows.
Does anybody know how to make ONE mySQL statement to do it?
you could use a self join
SELECT distinct a.user_id
FROM table a
inner join table b on a.user_id = b.user_id and b.field_id = 5 and b.value='idle'
inner join table c on a.user_id = c.user_id and c.field_id = 4 and c.value = 'loggedin'
where a.field_id=3 and a.value='BOOK'

Get 5 random rows from MySQL DB [duplicate]

This question already has answers here:
MySQL select 10 random rows from 600K rows fast
(28 answers)
Closed 8 years ago.
I have searched all over for an answer and although people say not to use the ORDER BY RAND() clause, I think for my purposes it is ok as this is for a competition which barely has more than a few hundred records at a time PER competition.
So basically i need to retrieve 5 random records from a competition entries table. However any loyalty customers will received an additional EXTRA entry so example:
compEntryid | firstName | lastName | compID |
1 | bob | smith | 100
2 | bob | smith | 100
3 | jane | doe | 100
4 | sam | citizen | 100
etc
So we are giving the loyalty members a better chance at winning a prize. However im a little worried that the returned result from a usual ORDER BY RAND() can include 2 entries of the SAME person ? What is an optimised method to ensure that we truly have 5 random records but at the same time giving those extra entrants a better or (weighted) chance ? Happy to use multiple queries, sub-queries or even a mix of MySQL and PHP ? Any advice is deeply appreciated thank you !
Bass
EDIT:
These 2 queries both work!
query1
SELECT concat(firstName, " ", lastName) name,id, email
FROM t WHERE
RAND()<(SELECT ((5/COUNT(id))*10) FROM t)
group by email ORDER BY RAND() limit 5;
query2
select distinct
email, id, firstName, lastName from
(
select id ,
email, firstName , lastName , compID, rand()/(select count(*) from t where
email=t1.email
) as rank
from t t1
where compID = 100
order by rank) t2 limit 5;
http://sqlfiddle.com/#!2/73470c/2
If you have a few hundred record, I think that order by rand() solution should be fine:
subquery will order weighting number of entries, but duplicates remains. Parent SELECT will take the first 5 distinct rows.
SELECT DISTINCT firstName ,
lastName ,
compID
FROM
( SELECT compEntryid ,firstName , lastName , compID, rand()/(select count(*)
FROM t
WHERE firstName=t1.firstName AND
lastName = t1.lastName) AS rank
FROM t t1
WHERE compID = 100
ORDER BY rank) t2
LIMIT 5
Fiddle
I think you will need to use a sub query if you want to return a compEntryid.
SELECT t.firstName, t.lastName, t.compID, MIN(compEntryid)
FROM t
INNER JOIN
(
SELECT DISTINCT firstName, lastName, compID
FROM t
ORDER by rand()
LIMIT 5
) t2
ON t.firstName = t2.firstName
AND t.lastName = t2.lastName
AND t.compID = t2.compID
GROUP BY t.firstName, t.lastName, t.compID;
This uses a sub query to get 5 random firstName / lastName / compID. Then joins against the table to get the MIN compEntryId.
However not certain about this. Think it will eliminate the duplicates in the sub query before performing the order / limit, which would prevent someone with more entries having more chances.
EDIT
More of a play and I think I have found a solution. Although efficiency is not one of its strong points.
SELECT MIN(compEntryid), firstName, lastName, compID
FROM
(
SELECT firstName, lastName, compID, compEntryid, #seq:=#seq+1 AS seq
FROM
(
SELECT firstName, lastName, compID, compEntryid
FROM t
ORDER by rand()
) sub0
CROSS JOIN (SELECT #seq:=0) sub1
) sub2
GROUP BY sub2.firstName, sub2.lastName, sub2.compID
ORDER BY MIN(seq)
LIMIT 5
This has an inner sub query that gets all the records in a random order. Around that another sub query adds a sequence number to the records. The outer query groups by the name, etc, and orders by the min sequence number for that name. The compEntryId is just grabbed as the MIN for the name / competition (I am assuming you don't care too much about this).
This way if someone had 5 entries the inner sub query would mix them up in the list. the next sub query would add a sequence number. At this stage those 5 entries could be sequence numbers 1 to 5. The outer one would order by the lowest sequence number for the name and ignore the others, so of those 5 only sequence number 1 would be used and 2 to 5 ignored, with the next selected person being the one with sequence number 6.
This way the more entries they have the more likely they are to be a winner, but can't be 2 of the 5 winners.
With thanks to kiks73 for setting up some sqlfiddle data:-
http://sqlfiddle.com/#!2/cd777/1
EDIT
A solution based on that above by #kiks73. Tweaked to use a non correlated sub query for the counts, and eliminates a few uncertainties. For example with his solution I am not quite sure whether MySQL will chose to do the DISTINCT by implicitly doing a GROUP BY, which would also implicitly do an orderering of the results prior to doing the limit (it doesn't seem to, but I am not sure this behaviour is defined).
SELECT t.firstName ,
t.lastName ,
t.compID,
MIN(rand() / t1.entry_count) AS rank
FROM
(
SELECT firstName, lastName, compID, COUNT(*) AS entry_count
FROM t
GROUP BY firstName, lastName, compID
) t1
INNER JOIN t
ON t.firstName=t1.firstName
AND t.lastName = t1.lastName
AND t.compID = t1.compID
GROUP BY t.firstName, t.lastName, t.compID
ORDER BY rank
LIMIT 5

PDO and prepared statements on dynamic sized queries

I am developing a small gaming website for college fest where users attend few contests and based on their ranks in result table, points are updated in their user table. Then the result table is truncated for the next event. The schemas are as follows:
user
-------------------------------------------------------------
user_id | name | college | points |
-------------------------------------------------------------
result
---------------------------
user_id | score
---------------------------
Now, the first 3 students are given 100 points, next 15 given 50 points and others are given 10 points each.
Now, I am having problem in developing queries because I don't know how many users will attempt the contest, so I have to append that many ? in the query. Secondly, I also need to put ) at the end.
My queries are like
$query_top3=update user set points =points+100 where id in(?,?,?);
$query_next5=update user set points = points +50 where id in(?,?,?,?,?);
$query_others=update user set points=points+50 where id in (?,?...........,?);
How can I prepare those queries dynamically? Or, is there any better approach?
EDIT
Though its similar to this question,but in my scenario I have 3 different dynamic queries.
If I understand correctly your requirements you can rank results and update users table (adding points) all in one query
UPDATE users u JOIN
(
SELECT user_id,
(
SELECT 1 + COUNT(*)
FROM result
WHERE score >= r.score
AND user_id <> r.user_id
) rank
FROM result r
) q
ON u.user_id = q.user_id
SET points = points +
CASE
WHEN q.rank BETWEEN 1 AND 3 THEN 100
WHEN q.rank BETWEEN 4 AND 18 THEN 50
ELSE 10
END;
It totally dynamic based on the contents in of result table. You no longer need to deal with each user_id individually.
Here is SQLFiddle demo

Selecting N rows from each group in MYSQL

I need to search for the updates sent by the friends of a giving user.
There is a table called friendship. It has a column called profile1 and another one called profile2. It represents the friendship between two users in this websystem, and a friendship is the presence of two giving ids, no matter in what position. So the profile with id 1 may have 2 friends, profile with id 2 and with id 3 as following:
friendship
profile1 profile2
1 2 <--
3 1 <--
2 5
...
Now I want to search for the updates sent by some user's friends. There is this table update
update
id content time profile
1 A text ... 2
2 A text ... 2
3 A text ... 3
4 A text ... 2
5 A text ... 3
6 A text ... 2
7 A text ... 10
8 A text ... 11
If my profile/user is identified by the id 1, and it has only 2 friends (the profiles identified by id 2 and 3) and also I need my search to return only 2 results by each user, my SELECT has to return updates 1,2,3 and 5.
Preferably updates should be grouped by its author and it would be great if I could set the number of different profiles to be considered in this search (for example, if profile 1 had 10 friends and I wanted only updates from 3 profiles, the most recent must appear first).
Do you know how can I achieve this??
thank you very much!
#EDIT
This returns all updates sent by friends of profile 1. But i'm not sure whether or not i'm in the right direction
SELECT u.*
FROM `update` u
INNER JOIN friendship f1 ON f1.profile1 = u.author
WHERE f1.profile2 =1
UNION
SELECT u.*
FROM `update` u
INNER JOIN friendship f2 ON f2.profile2 = u.author
WHERE f2.profile1 =1
If you are willing to do it in two queries, you can do it like this. First, get three profiles who have most recently posted based on your constraints:
-- Get the three latest updated profiles from here.
-- (we can't use a CTE because MySQL doesn't support
-- them yet).
SELECT DISTINCT p.profile FROM
(
SELECT ui.profile, ui.time FROM
(
SELECT u.profile, u.time
FROM `update` u
INNER JOIN `friendship` f ON f.profile2 = u.profile
WHERE f.profile1 = 1
UNION ALL
SELECT u.profile, u.time
FROM `update` u
INNER JOIN `friendship` f ON f.profile1 = u.profile
WHERE f.profile2 = 1
) ui ORDER BY ui.time DESC
) p LIMIT 0, 3;
From that query, get the three profile IDs out and put them in place of <id1>, <id2> and <id3> in the following query
-- Use a union to get the result set back
(SELECT a.content, a.time, a.profile FROM `update` a
WHERE a.profile = <id1>
ORDER BY a.time DESC
LIMIT 0, 2)
UNION ALL
(SELECT a.content, a.time, a.profile FROM `update` a
WHERE a.profile = <id2>
ORDER BY a.time DESC
LIMIT 0, 2)
UNION ALL
(SELECT a.content, a.time, a.profile FROM `update` a
WHERE a.profile = <id3>
ORDER BY a.time DESC
LIMIT 0, 2);
If you get less than three profiles back, either remove parts of the query in your PHP code, or set the WHERE clause to something like 0 so it always evaluates to fault (assuming you don't have a profile ID of zero)
The 2 in the limit clauses above can be changed if you want more or fewer results per profile.
Sample SQL fiddle: http://sqlfiddle.com/#!2/22e57/1 (updated fiddle to make the content more meaningful and to use times)
I would suggest doing a series of queries for each author within one transaction, that way there would not be a need for grouping - you could simply append results together outside of your SQL.
SELECT * FROM `update` WHERE
profile IN (SELECT profile2 FROM `friendship` WHERE profile1=1) OR
profile IN (SELECT profile1 FROM `friendship` WHERE profile2=1);
try this sqlFiddle
SELECT T1.profile,T1.content,T1.time
FROM
(SELECT UPD.profile,UPD.content,UPD.time,
IF (#prevProfile != UPD.profile,#timeRank:=1,#timeRank:=#timeRank+1) as timeRank,
#prevProfile := UPD.profile
FROM
(SELECT UP.profile,UP.content,UP.time
FROM
(SELECT profile,max(time) as latestUpdateTime
FROM friendship F INNER JOIN updates U
ON (F.profile1 = 1 AND U.profile = profile2) /* <-- specify profile on this line */
OR(F.profile2 = 1 AND U.profile = profile1) /* <-- specify profile on this line */
GROUP BY profile
ORDER BY latestUpdateTime DESC
LIMIT 3 /* limit to 3 friends profiles that have the most recent updates */
)as LU
INNER JOIN updates UP
ON (UP.profile = LU.profile)
ORDER BY profile,time DESC
)as UPD,(SELECT #prevProfile:=0,#timeRank:=0)variables
)T1
WHERE T1.timeRank BETWEEN 1 AND 2 /* grab 2 lastest updates for each profile */
ORDER BY T1.time DESC
in my example, profile id 1 has more than 3 friends, but i am only grabbing 3 friends that made the most recent updates.
explanation of above query.
LU grabs 3 profiles that are friends with profile id 1 that made the latest updates.
UPD grabs all contents that belong to these 3 friends.
T1 returns the contents along with a timeRank number for each content in order from 1 counting upward order by time DESCENDING for each profile
and finally the WHERE we only grab 2 content updates for each profile
then we finally ORDER these updates based on TIME starting from most recent.

Matching interests(Nearest neighbour) search in SQL

I'm trying to find users with similar set of interests, with the following schema..
USERS - ID name etc
Interests - ID UID PID
where ID is unique ID for Interests, UIS is user ID and PID is a product ID. I have looked at other similar questions at SO, but none of them had an exact answer.
Example- Let's say I'm interested in getting users with similar interest to John, and this is how to two tables look like ...
ID Name
11 John
12 Mary
13 Scott
14 Tim
ID UID PID
3 12 123
4 12 231
5 12 612
6 13 123
7 13 612
8 14 931
9 14 214
10 11 123
11 11 231
12 11 781
13 11 612
I would like a result with in that order.
I was thinking of doing a set intersection of the user I'm interested in with all other users. It doesn't sound like a very good solution, because it will have to be done everytime a user adds interest or another user is added. Its a small project, and as of now I'll be limiting users to 100. I still think that the above approach will not be efficient at all as it will take 1002 time.
Can someone guide me in the right direction? What are the possible solutions, and which one will be the best with above given constraints. I'm looking at ANN to see if I can use that.
This starts by counting the number of interests that each user has in common with John. The approach is to take all of John's interests, join back to the interests table and aggregate to the the count of common interests. Here is the SQL for that:
select i.uid, COUNT(*) as cnt
from (select i.*
from interests i join
users u
on i.uid = i.id
where u.name = 'John'
) ilist join
interests i
on ilist.pid = i.pid and
ilist.uid <> i.uid -- forget about John
group by i.uid
But, you actually want the list of products, rather than just the count. So, you have to join back to the interests table:
select i.*
from (select i.uid, COUNT(*) as cnt
from (select i.*
from interests i join
users u
on i.uid = i.id
where u.name = 'John'
) ilist join
interests i
on ilist.pid = i.pid and
ilist.uid <> i.uid -- forget about John
group by i.uid
) t join
interests i
on t.uid = i.uid
group by t.cnt, i.uid
The following query finds others users with atleast 2 or more similar interests according to the interests of user 11.
SELECT in2.UID FROM users u
INNER JOIN interest in1 ON (in1.UID = u.ID)
INNER JOIN interest in2 ON (in2.PID = in1.PID AND in2.UID <> u.ID)
WHERE u.ID = 11
GROUP BY in2.UID
HAVING COUNT(in2.UID) >= 2
ORDER BY COUNT(in2.UID) DESC
The ORDER BY ensures that users with the most similar interests ends up first. The HAVING COUNT(in2.UID) >= 2) makes sure the users which are found have atleast 2 or more similar interest.

Categories