Tricky: Left join relation with row value - mysql

Suppose that I have the following two tables:
PRICE
price_id price room_id nr_of_people
1 80 1 1
2 75 1 2
3 90 2 2
4 120 3 3
ROOM
room_id room_no max_people
1 101 2
2 102 3
3 103 4
And, I need the following result:
QUERY_RESULT
price room_no nr_of_people
80 101 1
75 101 2
0 102 1
90 102 2
0 102 3
0 103 1
0 103 2
120 103 3
0 103 4
The tricky part in here is, I need to retrieve price for each people (for 2 people, for 3 people, for 4 people; that is incrementing upto the max_people defined in the room table), if there is no actual data available in the price table, it should fill in 0 by default. Above illustration should support my explanation.
I am not sure whether the table structure have any logical error.
Any thought/input/help regarding how to resolve the issue is much appreciated.

As abresas' answer and xQbert's comments suggest, you somehow need to create data in order to join it with your tables.
Like abresas' answer, I use an auxiliary table, but in my solution, this table needs to be filled with numbers 1 to N only, where N = biggest value that can ever appear on column max_people.
I created an auxiliary table called aux with a single column num. This query works for me:
SELECT IF(price.price IS NULL, 0, price.price) AS price, room.room_no, aux.num AS nr_of_people
FROM room
JOIN aux ON aux.num <= room.max_people
LEFT JOIN price ON ( price.room_id = room.room_id
AND aux.num = price.nr_of_people )
ORDER BY room.room_id, num
Unfortunately, mysql doesn't provide a native mechanism to generate a sequence of integers (see these questions), so physically creating the auxiliary table seems to be the most practical way to achieve what you need, though workarounds certainly exist if you really can't or don't want to create such table.
Just for the fun of it, the following would work without creating a new table (all inspired in the questions I linked to):
SELECT [...]
FROM room
JOIN (
SELECT 1 num
UNION SELECT 2
UNION SELECT 3
UNION SELECT 4
-- ...add as many entries as needed...
) aux ON aux.num <= room.max_people
LEFT JOIN [...]
As well as this:
SELECT [...]
FROM room
JOIN (
SELECT #row := #row +1 AS num
FROM any_table_that_is_big_enough, (SELECT #row :=0) r
) aux ON aux.num <= room.max_people
LEFT JOIN [...]

You should create a table where you have the nr_of_people from 1 up to max_people for each room, and the room_id. Then you can do an OUTER JOIN do get the information as you asked.
You can also create it as a temporary table constructing a query with the data you need in your code.
mysql> CREATE TEMPORARY TABLE nr ( nr_of_people int, room_id int );
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO nr VALUES ( 1, 1 ), ( 2, 1 ), ( 1, 2 ), ( 2, 2 ),
( 3, 2 ), ( 1, 3 ), ( 2, 3 ), ( 3, 3 ), ( 4, 3 );
Query OK, 9 rows affected (0.00 sec)
Records: 9 Duplicates: 0 Warnings: 0
mysql> SELECT price.price, room.room_no, nr.nr_of_people
FROM price
RIGHT OUTER JOIN nr ON price.room_id = nr.room_id AND price.nr_of_people = nr.nr_of_people
INNER JOIN room ON nr.room_id = room.room_id;
+-------+---------+--------------+
| price | room_no | nr_of_people |
+-------+---------+--------------+
| 80 | 101 | 1 |
| 75 | 101 | 2 |
| NULL | 102 | 1 |
| 90 | 102 | 2 |
| NULL | 102 | 3 |
| NULL | 103 | 1 |
| NULL | 103 | 2 |
| 120 | 103 | 3 |
| NULL | 103 | 4 |
+-------+---------+--------------+
9 rows in set (0.00 sec)

Related

Can I count multiple columns with Group By?

I have a table with these columns:
s, s2, s3
1, 2, 3
4
1, 3
4, 2,
2, 1
3, 4
4
I want to know how many times the unique values in column s appears in the columns s, s2 and s3.
So far I have:
$query = "SELECT s, COUNT(*) as count FROM table GROUP BY s";
This will give me:
1 - count 2
2 - count 1
3 - count 1
4 - count 3
But I want to count the column s2 and s3 also so the outcome will be:
1 - count 3
2 - count 3
3 - count 3
4 - count 4
Any idea how I must edit the query so I can count the columns s, s2 and s3 group by the values of column s?
Kind regards,
Arie
You need a UNION ALL for all the columns and then count them:
select
t.s, count(*) counter
from (
select s from tablename union all
select s2 from tablename union all
select s3 from tablename
) t
where t.s is not null
group by t.s
See the demo.
Results:
| s | counter |
| --- | ------- |
| 1 | 3 |
| 2 | 3 |
| 3 | 3 |
| 4 | 4 |
If in the columns s2 and s3 there are values that do not exist in the column s and you want them excluded, then instead of:
where t.s is not null
use
where t.s in (select s from tablename)
#forpas answer is a good one. However, two things you should consider.
Due to the use of union the query will become slower as the data size increases.
If the input is as following:
s, s2, s3
1, 2, 3
4
1, 3
4, 2,
2, 1
3, 4
4 5
The result of the provided query will be:
| s | counter |
| --- | ------- |
| 1 | 3 |
| 2 | 3 |
| 3 | 3 |
| 4 | 4 |
| 5 | 1 |
whereas it should remain the same as 5 is not present into the s column.
In order to resolve both of the above issues, I propose the approach to use JOIN instead of UNION:
SELECT t3.s, IF(t3.s = t4.s3, cnt1 + 2, cnt1 + 1) as counter FROM
(SELECT *, count(*) AS cnt1 FROM
(SELECT s from table) AS t1
LEFT JOIN
(SELECT s2 FROM table) AS t2
ON t1.s = t2.s2 GROUP BY t1.s
) AS t3
LEFT JOIN
(SELECT s3 FROM table) AS t4
ON t3.s = t4.s3
ORDER BY t3.s
The query might look a bit lengthy and complicated but it is really simple when you look into the logic.
Step 1
What I have done here is to make a left join from s column to s2 and counted results for that so it will give you 1 lesser number than how many numbers are present in total as it will make relation left to right.
Step 2
Then I have made a left join from s to s3, and only increase the count of step 1 by 1 if the relation is found.
Step 3
Ultimately I have increased the count by 1 so that we can convert the number of relations to the number of the enities.
I hope it makes sense

Sum, Group Concat and Join with 3 tables.

I am working on a tool to administer customer and payment data.
I use MySQL and have the following tables: customers and payments:
customers:
ID | invoiceID | supreme_invoiceID
1 123 a123
2 124 a123
3 103 a103
4 110 a110
payments:
ID | supreme_invoiceID | amount | date
1 a123 10 10.10.2010
2 a103 105 10.11.2017
3 a123 5 11.10.2010
And my result should look like this:
view_complete:
ID | supreme_invoideID | number_invoices | GROUP_CONCAT(invoiceID) | SUM(payments.amount) | GROUP_CONCAT(payments.amount)
1 a123 2 123;124 15 10;15
Unfortunately, I cannot get it directly into one table. Instead I create 2 views and query the payments table separately for aggregate data on payments.
First, I create an auxiliary view:
CREATE VIEW precomplete as
SELECT *, COUNT(supreme_invoiceID) as number_invoices FROM customers
GROUP BY supreme_invoiceID;
Then, a second one:
Then I take a second VIEW
CREATE VIEW complete AS
SELECT precomplete.*, SUM(payments.amount)
LEFT JOIN payments p ON precomplete.supreme_invoiceID = p.supreme_invoiceID
GROUP BY precomplete.supreme_invoiceID;
And the concatenated Values I receive in an additional query. But I would like to receive my data all in one query and hopefully, without such view hierarchy. PhpMyAdmin is already pretty slow in loading my views even with few entries.
Any help is greatly appreciated.
Thanks!
The db design forces an approach which builds the aggregates separately to avoid duplicates before joining on a common field for example
drop table if exists c,p;
create table c(ID int, invoiceID int, supreme_invoiceID varchar(4));
insert into c values
(1 , 123 , 'a123'),
(2 , 124 , 'a123'),
(3, 103 , 'a103'),
(4 , 110 , 'a110');
create table p(ID int, supreme_invoiceID varchar(4), amount int, date varchar(10));
insert into p values
(1 , 'a123' , 10 , '10.10.2010'),
(2 , 'a103' , 105 , '10.11.2017'),
(3 , 'a123' , 5 , '11.10.2010');
select c.*,p.*
from
(select min(c.id) minid,count(*) nofinvoices,group_concat(c.invoiceid) gciid, max(supreme_invoiceid) maxsid
from c
group by supreme_invoiceid
) c
join
(select group_concat(supreme_invoiceid) gcsid, sum(amount),group_concat(amount),max(supreme_invoiceid) maxsid
from p
group by supreme_invoiceid
) p
on p.maxsid = c.maxsid
order by minid
;
+-------+-------------+---------+--------+-----------+-------------+----------------------+--------+
| minid | nofinvoices | gciid | maxsid | gcsid | sum(amount) | group_concat(amount) | maxsid |
+-------+-------------+---------+--------+-----------+-------------+----------------------+--------+
| 1 | 2 | 123,124 | a123 | a123,a123 | 15 | 10,5 | a123 |
| 3 | 1 | 103 | a103 | a103 | 105 | 105 | a103 |
+-------+-------------+---------+--------+-----------+-------------+----------------------+--------+
2 rows in set (0.15 sec)
Much like your view approach. Note there doesn't appear to be a customer in the customer table

PHPMyAdmin - Update 30k rows with looped number sequence

I hope i'm explaining this properly... but i'm trying to update a column in a table with 30k rows with a repeated sequence.
I've populated entire columns before with random numbers using:
UPDATE locations SET template = CAST((RAND() * 4)+1 AS UNSIGNED);
Which gave:
2
4
5
1
3
etc. in a random fashion throughout the 30k rows...
I would like to enter a query that can produce a repeated sequence like:
1
2
3
4
5
1
2
3
4
5
across all 30k rows.
I've been looking into loops and auto increments but can't get it to work.
Any help much appreciated :)
Perhaps using a variable will do for example
DROP TABLE IF EXISTS T;
CREATE TABLE T(ID INT, SEQNO INT);
INSERT INTO T VALUES (1,NULL),(2,NULL),(3,NULL),(4,NULL),(5,NULL),(6,NULL),(7,NULL);
UPDATE T
SET SEQNO = (SELECT IF(#RN = 2 ,#RN:=1,#RN:=#RN + 1) FROM (SELECT #RN:=0) R)
WHERE 1 = 1
+------+-------+
| ID | SEQNO |
+------+-------+
| 1 | 1 |
| 2 | 2 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 2 |
| 7 | 1 |
+------+-------+
Thanks for the suggestions... I had a hard time finding an answer but eventually found something that would do exactly what I was after. I must admit it is far beyond my capabilities, but here it is:
SET #row_number = 0;
SET #max_num = 75;
update locations loc1
join (
select
if ((num % #max_num) = 0, #max_num, (num % #max_num)) as num2,
a.*
from (
select
(#row_number:=#row_number + 1) AS num,
loc.*
from locations loc
ORDER BY num
) a
order by num, num2
) loc2 on (loc2.id = loc1.id)
set loc1.colname = loc2.num2;

Finding cooccuring values in MYSQL weak relation table

I have a weak relation table, called header, it is basically just three ID's: id is an autoincrement primary key, did points to the id of table D and hid points to the id of table H. D and H are irrelevant here.
I want to find for any value of hid, the other values of hid that shares did with the original hid. An example:
id | did | hid
===============
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3
4 | 2 | 1
5 | 2 | 4
6 | 2 | 5
7 | 3 | 2
8 | 3 | 6
For hid = 1 I would thus like to find id = {2,3,5,6} as those are the rows that have did in common with hid = 1.
I can do this by creating some arrays in PHP and running through all possible values of hid and respective did, but this is a quite slow process for large tables. I was wondering if there is a clever kind of JOIN or similar statement that could be used to find the cooccuring values of hid.
If I have understood you correctly:-
SELECT a.hid, GROUP_CONCAT(b.id)
FROM header a
INNER JOIN header b
ON a.did = b.did
AND b.hid != 1
WHERE a.hid = 1
GROUP BY a.hid
SQL fiddle:-
http://www.sqlfiddle.com/#!2/9aa26/1
Maybe this:
SELECT d.id
FROM (
SELECT *
FROM header
WHERE header.hid =1
) AS h
JOIN header AS d ON d.did = h.did
WHERE d.hid !=1

Mysql join with counting results in another table

I have two tables, one with ranges of numbers, second with numbers. I need to select all ranges, which have at least one number with status in (2,0). I have tried number of different joins, some of them took forever to execute, one which I ended with is fast, but it select really small number of ranges.
SELECT SQL_CALC_FOUND_ROWS md_number_ranges.*
FROM md_number_list
JOIN md_number_ranges
ON md_number_list.range_id = md_number_ranges.id
WHERE md_number_list.phone_num_status NOT IN (2, 0)
AND md_number_ranges.reseller_id=1
GROUP BY range_id
LIMIT 10
OFFSET 0
What i need is something like "select all ranges, join numbers where number.range_id = range.id and where there is at least one number with phone_number_status not in (2, 0).
Any help would be really appreciated.
Example data structure:
md_number_ranges:
id | range_start | range_end | reseller_id
1 | 000001 | 000999 | 1
2 | 100001 | 100999 | 2
md_number_list:
id | range_id | number | phone_num_status
1 | 1 | 0000001 | 1
2 | 1 | 0000002 | 2
3 | 2 | 1000012 | 0
4 | 2 | 1000015 | 2
I want to be able select range 1, because it has one number with status 1, but not range 2, because it has two numbers, but with status which i do not want to select.
It's a bit hard to tell what you want, but perhaps this will do:
SELECT *
from md_number_ranges m
join (
SELECT md_number_ranges.id
, count(*) as FOUND_ROWS
FROM md_number_list
JOIN md_number_ranges
ON md_number_list.range_id = md_number_ranges.id
WHERE md_number_list.phone_num_status NOT IN (2, 0)
AND md_number_ranges.reseller_id=1
GROUP BY range_id
) x
on x.id=m.id
LIMIT 10
OFFSET 0
Is this what you're looking for?
SELECT DISTINCT r.*
FROM md_number_ranges r
JOIN md_number_list l ON r.id = l.range_id
WHERE l.phone_num_status NOT IN (0,2)
SQL Fiddle Demo