Difficult where clause, may require the use of two queries? - php

I've got a query to produce which I can't seem to get right. I've got three tables:
student: student details
link: links that exist for a student, links have a status which can be active or completed
email: shows what links have been sent out by email.
I need to get a list of student IDs (from the student table) based on the following criteria:
link.status = active and related email doesn't exist (i.e. a link has been created but it hasn't been sent in an email)
link.status is null and email is null (i.e. there are no existing links for that student)
link.status = completed, and there are no other links for this student which have an active status
So if I have the following data in my tables:
student
+----+
| id |
+----+
| 1 |
| 2 |
| 3 |
| 4 |
+----+
link
+----+-----------+------------+
| id | status | student_id |
+----+-----------+------------+
| 1 | completed | 1 |
| 2 | active | 1 |
| 3 | completed | 2 |
| 4 | active | 3 |
+----+-----------+------------+
email
+----+---------+
| id | link_id |
+----+---------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----+---------+
Then my query should return the following student IDs: 2,3,4
2 - because there is only a completed link for this student
3 - because there is an active link with no associated email
4 - because there are no links for this student
I currently have this query which gets part of what I need:
SELECT DISTINCT student.id
FROM student
LEFT JOIN link ON link.student_id = student.id
LEFT JOIN email ON email.link_id = link.id
WHERE student.course = 'phd'
AND student.institution_id = '2'
AND (
(link.status != "active" AND email.id IS NULL)
OR
(link.status IS NULL AND email.id IS NULL)
OR
(link.status = "active" AND email.id IS NULL)
)
This of course doesn't get any student IDs where link.status = completed and no other links exist for the student. I can of course do this by adding in:
(link.status = "completed" and email.id IS NOT NULL)
into the WHERE, but this will return the student ID if they have another active link or they don't have an active link. This being the bit I'm struggling with.
I get the feeling this may not be able to be accomplished by a single query, so would I need to do two queries then subtract them from one another? I.e. the query above and a separate query selecting the links with a 'completed' status then subtracting them from the first query?
My application using these queries is built in PHP so I'm happy to do some logic in PHP with two queries if needed.
(Didn't have a clue what to put for the title so if anyone can think of anything better please edit it!)

SELECT s.*
FROM student s
LEFT
JOIN link l
ON l.student_id = s.id
AND l.status <> 'completed'
LEFT
JOIN email e
ON e.link_id = l.id
WHERE e.id IS NULL;
?

Because your request is based on links and one student can have more than one link, you should start with query to links table, after that, you add joins and conditions.
Preparation SQL:
CREATE TABLE IF NOT EXISTS student
(
id int auto_increment primary key,
course tinytext,
institution_id int
);
INSERT INTO student (id, course, institution_id) VALUES
(1, 'phd', 2),
(2, 'phd', 2),
(3, 'phd', 2),
(4, 'phd', 2);
CREATE TABLE IF NOT EXISTS link
(
id int auto_increment primary key,
status tinytext,
student_id int
);
INSERT INTO link (id, status, student_id) VALUES
(1, 'completed', 1),
(2, 'active', 1),
(3, 'completed', 2),
(4, 'active', 3);
CREATE TABLE IF NOT EXISTS email
(
id int auto_increment primary key,
link_id int
);
INSERT INTO email (id, link_id) VALUES
(1, 1),
(2, 2),
(3, 3);
Query:
SELECT DISTINCT s.id
FROM link l
LEFT JOIN student s ON l.student_id = s.id
LEFT JOIN email e ON l.id = e.link_id
WHERE s.course = 'phd'
AND s.institution_id = '2'
AND (
(l.status != "active" AND e.id IS NULL)
OR
(l.status IS NULL AND e.id IS NULL)
OR
(l.status = "active" AND e.id IS NULL)
)
Play with it: http://sqlfiddle.com/#!2/dae16b/2
I don't really understand your question, because you have many mistakes in it. I will try dig deeper to find if your logic is ok.
EDIT: "Strawberry" approach by filtering JOIN's could be the thing you need
SELECT s.id
FROM student s
LEFT JOIN link l
ON l.student_id = s.id AND l.status = 'active' OR l.status IS NULL
LEFT JOIN email e
ON e.link_id = l.id
WHERE
e.id IS NULL
AND s.course = 'phd'
AND s.institution_id = '2';
Play with it: http://sqlfiddle.com/#!2/dae16b/26
We select "student" table and add only those links, that have "active" or "null" status (LEFT JOIN link l ON l.student_id = s.id AND l.status != 'completed'), which solves for rule #2 (there are no existing links for that student) and first part of rule #1 (a link has been created but it hasn't been sent in an email) and second part of rule #3 (link.status = completed, and there are no other links for this student which have an active status). After that, to solve for second par of rule #2 (a link has been created but it hasn't been sent in an email), we remove rows which does not have an e-mail (JOIN email e ON e.link_id = l.id and e.id IS NULL part).
Only thing left is to think, if you need to solve for first part of rule #3 (link.status = completed, and there are no other links for this student which have an active status), because I do not know, if situation where "student has no links" = "student has link.completed status".
For now, this query returns what you requested.

Related

Need help in joining two tables from mysql database for a photography project

I am working on a photography project and I am facing a bit issue with joining tables and retrieving data from mysql database.
I have created two tables for this project. One table named cm_team is for team members and another table named cm_events for photography events..Assume to shoot a event, we require 6 persons and the id of the person is stored in cm_events table.
As you can see from the above images.. I am storing the id's of members of cm_team in cm_events table.. I wish to obtain the name of the team member in the respective highlighted fields in the cm_events table..Any help is highly appreciated.
for example my desired output should be: instead of 5 under team_lead heading, I should get the name corresponding to 5 i.e Arjun
Something like this? (cleaner and faster than subqueries)
SELECT
`event`.client_name,
`event`.client_number,
# some more event cols ..
`team_lead`.`cm_name` AS `team_lead`,
`candid_photo`.`cm_name` AS `candid_photo`,
`candid_video`.`cm_name` AS `candid_video`,
`traditional_photo`.`cm_name` AS `traditional_photo`,
`traditional_video`.`cm_name` AS `traditional_video`,
`helper`.`cm_name` AS `helper`
FROM cm_events `event`
JOIN cm_team `team_lead` ON `team_lead`.`cm_code` = `event`.`team_lead`
JOIN cm_team `candid_photo` ON `candid_photo`.`cm_code` = `event`.`candid_photo`
JOIN cm_team `candid_video` ON `candid_video`.`cm_code` = `event`.`candid_video`
JOIN cm_team `traditional_photo` ON `traditional_photo`.`cm_code` = `event`.`traditional_photo`
JOIN cm_team `traditional_video` ON `traditional_video`.`cm_code` = `event`.`traditional_video`
JOIN cm_team `helper` ON `helper`.`cm_code` = `event`.`helper`
With Sub queries
DROP TABLE IF EXISTS T,t1;
CREATE TABLE T (
id int, name varchar(10));
insert into t values
(1 , 'aaa'),
(2 , 'bbb');
create table t1 (
id int, team_lead int,team_a int);
insert into t1 values
(1,1,2),
(2,2,2);
select t1.id, (select t.name from t where t.id = t1.team_lead) team_lead,
(select t.name from t where t.id = t1.team_a) team_a
from t1;
+------+-----------+--------+
| id | team_lead | team_a |
+------+-----------+--------+
| 1 | aaa | bbb |
| 2 | bbb | bbb |
+------+-----------+--------+
2 rows in set (0.001 sec)

Mysql: Query which orders by default and rank (pin/hold up entries?)

I have a products table which contains all my products. Those products table gets filled permanently with new products. However, I want to have the possibility to "hold up"/"pin" certain products to a place in the returned query collection.
Means, I want to set something like rank_index which contains the number the product should have in the returned query collection.
Example:
id title rank_index
1 An awesome product
2 Another product 5
3 Baby car
4 Green carpet 2
5 Toy
Lets assume the default order would be the id. But because the rank_index is set for the product with the id 4 I would like to get the collection with the following order of ids returned: 1, 4, 3, 5, 2.
Is this somehow possible to do? The rank_index column was just an idea of mine. I mean.. I also could do this on the php side and do a normal query which does only include the products without an rank_index and one which only contains products with an index_rank and order them manually on the php side.
However, because this takes a lot of time and processing power I am looking for a solution which is done by the database... Any ideas?
Btw: I am using Laravel 8 if this makes any difference.
Kind regards
This is a very tricky problem. If you try the other approach setting consecutive values -- like 2 and 3 -- you will see that they do not work.
There may be simpler ways to solve this. But, here is a brute force approach.
It constructs a derived table by enumerating the rows in the original table.
It adds into this table (using a left join) all the force-ranked values.
It joins in the rest of the values by enumerating the empty slots both in table1 and in the derived table.
So:
with recursive n as (
select row_number() over (order by id) as n
from table1 t1
),
nid as (
select n.n, t1.id
from n left join
table1 t1
on t1.rank_index = n.n
),
nids as (
select n.n, coalesce(n.id, t1.id) as id
from (select nid.*, sum(nid.id is null) over (order by nid.n) as seqnum
from nid
) n left join
(select t1.*, row_number() over (order by id) as seqnum
from table1 t1
where rank_index is null
) t1
on n.seqnum = t1.seqnum
)
select t1.*
from nids join
table1 t1
on t1.id = nids.id
order by nids.n;
Use the rank_index if it's not null as the ordering, id otherwise:
Since you want the rank_index to be ahead of an id, a -0.5 adjustment is made:
SELECT *
FROM table
ORDER BY IF(rank_index IS NULL, id, rank_index - 0.5)
You can use IF clause and to have the correct the number to get te right order, so
CREATE TABLE table1 (
`id` INTEGER,
`title` VARCHAR(18),
`rank_index` INT
);
INSERT INTO table1
(`id`, `title`, `rank_index`)
VALUES
('1', 'An awesome product', NULL),
('2', 'Another product', '5'),
('3', 'Baby car', NULL),
('4', 'Green carpet', '2'),
('5', 'Toy', NULL);
SELECT *
FROM table1
ORDER BY IF(rank_index IS NULL, id, rank_index + .01)
+----+--------------------+------------+
| id | title | rank_index |
+----+--------------------+------------+
| 1 | An awesome product | NULL |
| 4 | Green carpet | 2 |
| 3 | Baby car | NULL |
| 5 | Toy | NULL |
| 2 | Another product | 5 |
+----+--------------------+------------+
db<>fiddle here

Sum 3 Tables and Subtract the User Request

Hello Developers/Programmers
I am working on withdrawal function on my website.
So it goes like this
I need to total the 3 tables i have by User ID with status of '1' ,and Subtract the Inputed amount by the User Requested the Withdrawal
These are my 3 tables
tbl_bonus_1
id | amount | user_id | status
1 20 1 1
2 20 1 1
3 20 3 1
tbl_bonus_2
id | amount | user_id | status
1 30 1 1
2 30 1 1
3 30 3 1
tbl_bonus_3
id | amount | user_id | status
1 40 1 1
2 40 1 1
3 40 3 1
Now I need to get all that 3 tables by USER ID and get the total of it.
After getting the total i need to subtract the Inputted quantity of the USER
and update the status to 0 so that the user cant withdraw again.
Im using Codeigniter 3.1.5
select user_id, sum(amount)
from
(select * from tbl_bonus_1
union
select * from tbl_bonus_2
union
select * from tbl_bonus_3) tt
where status = 1
group by user_id
DEMO:
http://sqlfiddle.com/#!9/7f1807e/1
And UPDATE (single user):
UPDATE tbl_bonus_1 t1 INNER JOIN tbl_bonus_2 t2
ON t1.user_id = t2.user_id
INNER JOIN tbl_bonus_3 t3
ON t1.user_id = t3.user_id
SET t1.amount = 0, t2.amount = 0, t3.amount = 0
WHERE t1.user_id = 1;
Realistically, you don't want to subtract from these tables if you want to manage a balance. You need to add a 4th table that is withdrawal amount, so you can capture the transactions. If you have a total of 160 across 3 tables, and the user withdrawals 150, how would you determine which to decrement.
I would suggest actually consolidating all of these into 1 trasaction table, and capture the amounts there.
So to get what you need you will need to leverage variables which will need to be passed to the query. Below will give you what you are asking for. That being said, this is not the correct way to do this. Also, there is no way to determine which of the 3 balances you want to subtract the withdraw from. This is just not how transaction ledgers work and for a lot of reasons I am not going to get in to right now. At the bottom of this answer is how I suggest you should build your table. You will be able to get information with more easy while capturing more/better data.
How to get data from current structure:
set #withdraw = 150.00, #user = 1;
select user_id, sum(amount) as prevBalance
, #remainingBalance := if(#user = user_id,sum(amount)-#withdraw,sum(amount)) as remainingBalance
from
(select * from tbl_bonus_1
union
select * from tbl_bonus_2
union
select * from tbl_bonus_3) balance
group by user_id;
How you should build your schema:
CREATE TABLE ledger (id int NOT NULL AUTO_INCREMENT
,user_id int
, amount decimal(5,2)
, transaction_type varchar(20)
,PRIMARY KEY (ID));
INSERT INTO ledger VALUES
(null,1,20,'Bonus1'),
(null,1,20,'Bonus1'),
(null,3,20,'Bonus1'),
(null,1,30,'Bonus2'),
(null,1,30,'Bonus2'),
(null,3,30,'Bonus2'),
(null,1,40,'Bonus3'),
(null,1,40,'Bonus3'),
(null,3,40,'Bonus3'),
(null,1,-150,'Withdraw')
;
Then all you would need to do is run the following query.
select user_id, sum(amount) balance from ledger
group by user_id;

PHP MySQL Group BY Having issue

Suppose I have a table like:
ID|Word |Reference
1 |Dog |1
1 |Fish |2
1 |Sheep|3
2 |Dog |4
2 |Fish |5
3 |Sheep|6
4 |Dog |7
I want to select all ID's that have the word Dog AND Sheep. So the result should be ID's: 1 and 2. I tried using this query:
SELECT ID FROM `Table` WHERE Word='Dog' OR Word='Fish' GROUP BY ID Having Word='Dog AND Word='Fish'
However, this AND in the Having clause makes me get 0 results. So, am I doing something wrong or is there another way to achieve wat I want based on MySQL query only (to optimize speed, since it has to search through many rows with the same setup as in the example above)
Basically the problem is the AND statement over multiple rows with the same ID.
UPDATE:
I need to get the reference for the ID's that where found. E.g. when the ID 1 and 2 are returned I need to know that ID 1 has reference 1 and 2. ID 2 has reference 3 and 4. Currently, I'm using this query:
SELECT ID FROM `Test` WHERE Word in ('Dog', 'Fish') GROUP BY ID HAVING count(DISTINCT Word) = 2;
Thanks
Here are two solutions that return the correct records, the first as individual records by ID and Reference, and the second with one record per ID and the Words and References as comma separated in columns.
Setup table and populate rows:
DROP TABLE IF EXISTS `list1`;
CREATE table `list1` (
id int(10),
Word varchar(10),
Reference int(10)
);
INSERT INTO `list1` (`ID`, `Word`, `Reference`)
VALUES
(1, 'Dog',1),
(1 ,'Fish',2),
(1 ,'Sheep',3),
(2 ,'Dog',4),
(2 ,'Sheep',5),
(3 ,'Sheep',6),
(4 ,'Dog',7);
Returns one row for each combination of ID and Word
SELECT
t.`ID`,
t.`Word`,
t.`Reference`
FROM `list1` as t
JOIN (
SELECT
t1.`ID` as `ref_id`
FROM `list1` AS t1
WHERE `Word` in ('Sheep','Dog')
GROUP BY t1.`ID`
HAVING count(DISTINCT t1.`Word`) = 2
) AS ts
ON t.`ID` = ts.`ref_id`
WHERE t.`Word` in ('Sheep','Dog')
ORDER BY t.`ID`,t.`Word`;
Results
ID | Word | Reference
1 | Dog | 1
1 | Sheep | 3
2 | Dog | 4
2 | Sheep | 5
Returns one row per ID, with a comma separated list of Words in one column, and a comma separated list of Reference in another.
SELECT
t.`ID`,
GROUP_CONCAT(t.`Word`) AS `Words`,
GROUP_CONCAT(t.`Reference`) AS `References`
FROM `list1` as t
JOIN (
SELECT
t1.`ID` as `ref_id`
FROM `list1` AS t1
WHERE `Word` in ('Sheep','Dog')
GROUP BY t1.`ID`
HAVING count(DISTINCT t1.`Word`) = 2
) AS ts
ON t.`ID` = ts.`ref_id`
WHERE t.`Word` in ('Sheep','Dog')
GROUP BY t.`ID`
ORDER BY t.`ID`,t.`Word`;
Results:
ID | Words | References
1 | Dog,Sheep | 1,3
2 | Dog,Sheep | 4,5
You need to join the table on itself. This way you can pick up where the id's are the same for instances where dog and sheep overlap.
Try this:
declare #t table (id int , Word varchar(10) )
insert into #t (ID, Word) values (1, 'Dog'),
(1 ,'Fish'),
(1 ,'Sheep'),
(2 ,'Dog'),
(2 ,'Sheep'),
(3 ,'Sheep'),
(4 ,'Dog')
select t.ID
from #t as t
join #t as t1 on t1.id = t.id
where t.word = 'Dog' and t1.word = 'Sheep'
Here's one way to do it by joining your table to itself.
SELECT t1.id FROM `Table` t1
INNER JOIN `Table` t2 ON t1.id = t2.id
WHERE t1.word='Dog' AND t2.word='Sheep';
The answer for my problem is solved using underneath query:
SELECT ID, GROUP_CONCAT(Reference) as ReferencesGrouped FROM `Test` WHERE Word in ('Dog', 'Fish') GROUP BY ID HAVING count(DISTINCT Word) = 2;
This will return me:
ID|ReferencesGrouped
1 |1,4
2 |4,5

How to combine results from multiple tables with different columns?

I have several tables with different numbers and types of columns, and a single column in common.
+--------+---------+------------+-------------+
| person | beardID | beardStyle | beardLength |
+--------+---------+------------+-------------+
+--------+-------------+----------------+
| person | moustacheID | moustacheStyle |
+--------+-------------+----------------+
I want to fetch all the results that match a given value of the shared column. I can do it using multiple select statements like this:
SELECT * FROM beards WHERE person = "bob"
and
SELECT * FROM moustaches WHERE person = "bob"
But this requires multiple mysql API calls, which seems inefficient. I was hoping I could use UNION ALL to get all the results in a single API call, but UNION requires that the tables have the same number and similar type of columns. I could write a SELECT statement that would manually pad the results from each table by adding columns with NULL values, but that would quickly get unmanageable for a few more tables with a few more columns.
I'm looking for a result set roughly like this:
+--------+---------+------------+-------------+-------------+----------------+
| person | beardID | beardStyle | beardLength | moustacheID | moustacheStyle |
+--------+---------+------------+-------------+-------------+----------------+
| bob | 1 | rasputin | 1 | | |
+--------+---------+------------+-------------+-------------+----------------+
| bob | 2 | samson | 12 | | |
+--------+---------+------------+-------------+-------------+----------------+
| bob | | | | 1 | fu manchu |
+--------+---------+------------+-------------+-------------+----------------+
Is there a way to achieve this that's fast and maintainable? Or am I better off running a separate query for each table?
Clarification:
I'm not looking for a cartesian product. I don't want a row for every combination of beard-and-moustache, I want a row for every beard and a row for every moustache.
So if there are 3 matching beards and 2 matching moustaches I should get 5 rows, not 6.
this should be working fine:
SELECT * FROM `beards` b LEFT OUTER JOIN `mustaches` ON (0) WHERE person = "bob"
UNION ALL
SELECT * FROM `beards` b RIGHT OUTER JOIN `mustaches` ON (0) WHERE person = "bob"
you don't have to handle the columns by yourself. the left and right outer join do this job.
unfortunately mysql doesn't have a full join. that's why you have to do it this way with a union
SELECT * FROM `customer` b LEFT OUTER JOIN `charges` ON (0) LEFT OUTER JOIN `day` ON (0)
UNION
SELECT * FROM `customer` b RIGHT OUTER JOIN `charges` ON (0) LEFT OUTER JOIN `day` ON (0)
UNION
SELECT * FROM `customer` b LEFT OUTER JOIN `charges` ON (0) RIGHT OUTER JOIN `day` ON (0)
this is a local test i made
Join on person....
I.e.
Select
t1.(asterix), t2.(asterix)
FROM
beards t1
INNER JOIN
moustaches t2 On t2.person = t1.person
SELECT *
FROM beards
JOIN moustaches
ON moustaches.person = beards.person
WHERE person = "bob"
I had fun with this, not sure it's entirely manageable with what more you have to add, but it accomplished the goal.
create table beard (
person varchar(20)
,beardID int
,beardStyle varchar(20)
,beardLength int )
create table moustache(
person varchar(20)
,moustacheID int
,moustacheStyle varchar(20))
insert into beard
select 'bob', 1, 'rasputin', 1
union select 'bob', 2, 'samson', 12
insert into moustache
select 'bob', 1, 'fu manchu'
declare #facialhair table (
person varchar(20)
,beardID int
,beardStyle varchar(20)
,beardLength int
,moustacheID int
,moustacheStyle varchar(20))
declare #i int
declare #name varchar(20)
set #name = 'bob'
set #i = (select COUNT(*) from beard where person = #name)
+ (select COUNT(*) from moustache where person = #name)
print #i
while #i > 0
begin
insert into #facialhair (person, beardID, beardStyle, beardLength)
select person, beardID, beardStyle, beardLength
from beard
where person = #name
set #i = #i-##ROWCOUNT
insert into #facialhair (person, moustacheID, moustacheStyle)
select person, moustacheID, moustacheStyle
from moustache
where person = #name
set #i = #i-##ROWCOUNT
end
select *
from #facialhair
I think you would be better by making queries for data in each table.
One of other possibilities is to concatenate data from all columns into one big string (you could choose some sign to separete column's values), then you should be able to use union all clause to combine results from each query - but then you will have to parse each row.. And data types will be lost.

Categories