Multiple columns in one subquery - mysql

I have a bit of a problem. I have a database layout like so:
customer
customer_id, name, age, etc...
customer_survey_question
id, category, caption, type
customer_survey_answer
id, customer_id, customer_survey_question_id, answer
and I need to pull in their answers like so:
name, age, etc..., question 1, question 2, question 3, etc...
Now I could do a sub-query:
SELECT
`customer`.*,
(
SELECT `answer`
FROM `customer_survey_answer`
WHERE `customer_survey_answer`.`customer_id`=`customer`.`customer_id`
AND `id`=1
) AS `question_1`,
(
SELECT `answer`
FROM `customer_survey_answer`
WHERE `customer_survey_answer`.`customer_id`=`customer`.`customer_id`
AND `id`=2
) AS `question_2`,
....
But there is 14 questions and I need to be able to do this pretty quickly and expand up to 80+ questions. What is the best way to approach?

You cannot do this through a query by and of itself.
Look here for some example code: How to merge data from 2 tables with MySQL
What you can do is use an INNER JOIN or GROUP_CONCAT and then reformat the data in your script (whether that be php or another language)
Using JOIN
Unser the circumstances this would likely lead to a large excess/irrelevant data in each row
SELECT c.*, csa.answer
FROM customers c
INNER JOIN
customer_survey_answer csa ON csa.`customer_id`=c.`customer_id`
ORDER BY c.customer_id, csa.customer_survey_question_id
Using GROUP_CONCAT
This will output one cell (csv style) as the answers
SELECT c.*, GROUP_CONCAT(csa.answer) as answers
FROM customers c
INNER JOIN
customer_survey_answer csa ON csa.`customer_id`=c.`customer_id`
GROUP BY c.customer_id
Using a loop
You might also consider querying the database for a list of customers with answers and then running a second query (for each customer returned) to get their answer. This could lead to a large number of queries.
First query:
SELECT * FROM customers
Second query:
SELECT answer
FROM customer_survey_answer
WHERE customer_id = INSERT_CUSTOMER_ID_HERE}

you want to pivot the data
SELECT class,GROUP_CONCAT(member)
FROM tbl
GROUP BY class;
is the basic case
http://www.artfulsoftware.com/infotree/queries.php#78

It is not wise to force DB output into result's columns.
In case you would like to operate over one only customer, make simple question query and then answer query to get everything and put it together outside of DB - in PHP for example.
In case you would like the list of customers with their answers, make first the question query and then make the "answer x customer" query with respective ids and put them together using hashing outside the DB. It works well and fast unless you don't use paging for your output list ;).
Hash can work like this
// suppose we have from database
// $answer_list(id,customer_id,customer_name,question_id,answer)
// $question_list(id,question)
// hash
$customer_list = array();
$customer_hash = array();
foreach ($answer_list as $answer)
{
$customer_id = $answer['customer_id'];
if (!isset($customer_hash[$customer_id]))
{
$customer_list[] = $customer_id;
$customer_hash[$customer_id]['name'] = $answer['customer_name'];
}
$customer_hash[$customer_id]['answer_hash'][$answer['question_id']] = $answer;
}
// output
foreach ($customer_list as $customer_id)
{
echo $customer_hash[$customer_id]['id'];
echo $customer_hash[$customer_id]['name'];
foreach ($question_list as $question)
{
echo $question['question'];
echo $customer_hash[$customer_id]['answer_hash'][$question['id]]['answer'];
}
}

If you want to generate a table in a single query you could use the group by on a query with a single INNER JOIN and then collect the values in several MAX( CASE ... END ) expressions. Sounds complicated at first but it does work reliably (just add another line for each desired column):
SELECT name, age,
MAX(CASE WHEN customer_survey_question_id=1 THEN answer END) a1,
MAX(CASE WHEN customer_survey_question_id=2 THEN answer END) a2,
MAX(CASE WHEN customer_survey_question_id=3 THEN answer END) a3,
MAX(CASE WHEN customer_survey_question_id=4 THEN answer END) a4,
MAX(CASE WHEN customer_survey_question_id=5 THEN answer END) a5,
MAX(CASE WHEN customer_survey_question_id=6 THEN answer END) a6,
MAX(CASE WHEN customer_survey_question_id=7 THEN answer END) a7,
MAX(CASE WHEN customer_survey_question_id=8 THEN answer END) a8,
...
FROM customer c INNER JOIN customer_survey_answer s
ON s.customer_id=c.customer_id

Related

Data base One To Many Relationship Query

I have 3 tables in my DB; Transactions, transaction_details, and accounts - basically as below.
transactions :
id
details
by_user
created_at
trans_details :
id
trans_id (foreign key)
account_id
account_type (Enum -[c,d])
amount
Accounts :
id
sub_name
In each transaction each account may be creditor or debtor. What I'm trying to get is an account statement (ex : bank account movements) so I need to query each movement when the account is type = c (creditor) or the account type is = d (debtor)
trans_id, amount, created_at, creditor_account, debtor_account
Update : I tried the following query but i get the debtor column values all Null!
SELECT transactions.created_at,trans_details.amount,(case WHEN trans_details.type = 'c' THEN sub_account.sub_name END) as creditor,
(case WHEN trans_details.type = 'd' THEN sub_account.sub_name END) as debtor from transactions
JOIN trans_details on transactions.id = trans_details.trans_id
JOIN sub_account on trans_details.account_id = sub_account.id
GROUP by transactions.id
After the help of #Jalos I had to convert the query to Laravel which also toke me 2 more hours to convert and get the correct result :) below is the Laravel code in case some one needs to perform such query
I also added between 2 dates functionality
public function accountStatement($from_date,$to_date)
{
$statemnt = DB::table('transactions')
->Join('trans_details as credit_d',function($join) {
$join->on('credit_d.trans_id','=','transactions.id');
$join->where('credit_d.type','c');
})
->Join('sub_account as credit_a','credit_a.id','=','credit_d.account_id')
->Join('trans_details as debt_d',function($join) {
$join->on('debt_d.trans_id','=','transactions.id');
$join->where('debt_d.type','d');
})
->Join('sub_account as debt_a','debt_a.id','=','debt_d.account_id')
->whereBetween('transactions.created_at',[$from_date,$to_date])
->select('transactions.id','credit_d.amount','transactions.created_at','credit_a.sub_name as creditor','debt_a.sub_name as debtor')
->get();
return response()->json(['status_code'=>2000,'data'=>$statemnt , 'message'=>''],200);
}
Your transactions table denotes transaction records, while your accounts table denotes account records. Your trans_details table denotes links between transactions and accounts. So, since in a transaction there is a creditor and a debtor, I assume that trans_details has exactly two records for each transaction:
select transactions.id, creditor_details.amount, transactions.created_at, creditor.sub_name, debtor.sub_name
from transactions
join trans_details creditor_details
on transactions.id = creditor_details.trans_id and creditor_details.account_type = 'c'
join accounts creditor
on creditor_details.account_id = creditor.id
join trans_details debtor_details
on transactions.id = debtor_details.trans_id and debtor_details.account_type = 'd'
join accounts debtor
on debtor_details.account_id = debtor.id;
EDIT
As promised, I am looking into the query you have written. It looks like this:
SELECT transactions.id,trans_details.amount,(case WHEN trans_details.type = 'c' THEN account.name END) as creditor,
(case WHEN trans_details.type = 'd' THEN account.name END) as debtor from transactions
JOIN trans_details on transactions.id = trans_details.trans_id
JOIN account on trans_details.account_id = account.id
GROUP by transactions.id
and it is almost correct. The problem is that due to the group-by MySQL can only show a single value for each record for creditor and debtor. However, we know that there are exactly two values for both: there is a null value for creditor when you match with debtor and a proper creditor value when you match with creditor. The case for debtor is similar. My expectation for this query would have been that MySQL would throw an error because you did not group by these computed case-when fields, yet, there are several values, but it seems MySQL can surprise me after so many years :)
From the result we see that MySQL probably found the first value and used that both for creditor and debtor. Since it met with a creditor match as a first match, it had a proper creditor value and a null debtor value. However, if you write bullet-proof code, you will never meet these strange behavior. In our case, doing some minimalistic improvements on your code transforms it into a bullet-proof version of it and provides correct results:
SELECT transactions.id,trans_details.amount,max((case WHEN trans_details.type = 'c' THEN account.name END)) as creditor,
max((case WHEN trans_details.type = 'd' THEN account.name END)) as debtor from transactions
JOIN trans_details on transactions.id = trans_details.trans_id
JOIN account on trans_details.account_id = account.id
group by transactions.id
Note, that the only change I did with your code is to wrap a max() function call around the case-when definitions, so we avoid the null values, so your approach was VERY close to a bullet-proof solution.
Fiddle: http://sqlfiddle.com/#!9/d468dc/10/0
However, even though your thought process was theoretically correct (theoretically there is no difference between theory and practice, but in practice they are usually different) and some slight changes are transforming it into a well-working code, I still prefer my query, because it avoids group by clauses, which can be useful, if necessary, but here it's unnecessary to do group by, which is probably better in terms of performance, memory usage, it's easier to read and keeps more options open for you for your future customisations. Yet, your try was very close to a solution.
As about my query, the trick I used was to do several joins with the same tables, aliasing them and from that point differentiating them as if they were different tables. This is a very useful trick that you will need a lot in the future.

MySQL DISTINCT returning not so distinct results

Good day,
I have a small issue with MySQL Distinct.
Trying the following query in my system :
SELECT DISTINCT `booking_id`, `booking_ticket`, `booking_price`, `bookingcomment_id`, `bookingcomment_message` FROM `mysystem_booking`
LEFT JOIN `mysystem_bookingcomment` ON `mysystem_booking`.`booking_id` = `mysystem_bookingcomment`.`bookingcomment_link`
WHERE `booking_id` = 29791
The point is that there are bookings like 29791 that have many comments added.
Let's say 10. Then when running the above query I see 10 results instead of one.
And that's not the way DISTINCT supposes to work.
I simply want to know if there are any comments. If the comment ID is not 0 then there is a comment. Of course I can add COUNT(blabla) as comment_number but that's a whole different story. For me now I'd like just to have this syntax right.
You may try aggregating here, to find which bookings have at least a single comment associated with them:
SELECT
b.booking_id,
b.booking_ticket,
b.booking_price
FROM mysystem_booking b
LEFT JOIN mysystem_bookingcomment bc
ON b.booking_id = bc.bookingcomment_link
WHERE
b.booking_id = 29791
GROUP BY
b.booking_id
HAVING
COUNT(bc.bookingcomment_link) > 0;
Note that depending on your MySQL server mode, you might have to also add the booking_ticket and booking_price columns to the GROUP BY clause to get the above query to run.
You can try below - using a case when expression
SELECT DISTINCT `booking_id`, `booking_ticket`, `booking_price`, `bookingcomment_id`,
case when `bookingcomment_message`<>'0' then 'No' else 'Yes' end as comments
FROM `mysystem_booking`
LEFT JOIN `mysystem_bookingcomment` ON `mysystem_booking`.`booking_id` = `mysystem_bookingcomment`.`bookingcomment_link`
WHERE `booking_id` = 29791

SQL SELECT IN Array NodeJS

I have an array with results = ['5', '2', '11', '12', '4'];
Now I want to make get count SQL SELECTION based on such array, that is make the SELECT IN Array SQL command. I want to return number of of rows that are there in this array from each element, I tried
The SQL Command I tried so far is as in the image provided and my table.
The result you've got is exactly what I would expect from a COUNT statement. I think you forgot to add a GROUP BY company_two.
EDIT
To handle the case of companies not having any entry, you can do:
SELECT s.company_two_id, COUNT(s.id)
FROM supplies_table s
RIGHT OUTER JOIN companies_table c ON s.company_two_id = c.company_id
WHERE company_two_id IN (...)
GROUP BY s.company_two_id;
Try below :
select COALESCE(count(a), 0) from table1 where b in (something) group by b

Mysql: Create a view with multiple self joins without duplicates in result

Just to clarify i can't change the tables structure, so please leave out the "you should change your tables to this and that" answers, thank you.
So i have a table entities_attributes_values where an entity has a lot of attributes and the value of that attribute, basically imagine 3 fields:
entity_id
entity_attributes_id
value
Because every entities attribute and its value is on row getting more values is not so easy i was thinking of multiple self joins, and because this query will be very common i created a view, which is built with this query:
SELECT `L1`.`entity_id`,
`L1`.`value` as 'company_id',
`L2`.`value` as 'entity_name',
`P`.`value` as 'person_name',
`L4`.`value` as 'establishment_id',
`L5`.`value` as 'department_id'
FROM `entities_attributes_values` `L1`
LEFT JOIN `entities_attributes_values` `L2` ON `L1`.`entity_id` = `L2`.`entity_id` AND `L2`.`entity_attributes_id` = 1
LEFT JOIN `entities_attributes_values` `L3` ON `L1`.`entity_id` = `L3`.`entity_id` AND `L3`.`entity_attributes_id` = 3
LEFT JOIN `persons_attributes_values` `P` ON `L3`.`value` = `P`.`core_persons_id` AND `P`.`core_persons_attributes_id` = 4
LEFT JOIN `entities_attributes_values` `L4` ON `L1`.`entity_id` = `L4`.`entity_id` AND `L4`.`entity_attributes_id` = 12
LEFT JOIN `entities_attributes_values` `L5` ON `L1`.`entity_id` = `L5`.`entity_id` AND `L5`.`entity_attributes_id` = 13
WHERE `L1`.`entity_attributes_id` = 2
So this works but i have one problem i get "duplicate" values and its not really duplicate but the point is that in my view i want every entity to be only one row with all its attributes values but instead i get this:
So as you can see the first three result are not good for me, i only need the fourth one, where i have all my data about one entity.
Thank you in advance for any help!
Try using conditional aggregation instead:
select eav.entity_id,
max(case when entity_attributes_id = 2 then eav.value end) as company_id,
max(case when entity_attributes_id = 1 then eav.value end) as entity_name,
max(case when entity_attributes_id = 3 then eav.value end) as company_name,
. . .
from entities_attributes_values eav
group by eav.entity_id;
This will make it easy to add new attributes to the view. Also, don't use single quotes to delimit column names. Single quotes should only be used for date and time constants.

Multiple SQL count requests, one query

I'm brand new to SQL and I know this should be easy, but I can't seem to find any reference on how to do specifically what I'm looking for. I've check the archives and I can't find a basic example.
Anyway, all I want is -
SELECT
(COUNT (i.productNumber WHERE i.type = 'type1') AS 'Type 1'),
(COUNT (i.productNumber WHERE i.type = 'type2') AS 'Type 2'),
FROM items AS i
WHERE i.dateadded BETWEEN '2015-03-02' and '2015-03-04'
The two count conditions are different, but both of those queries share that date condition. I've done two distinct select statements and put a UNION between them. That works. The only issue is all of the data appears in one column under the first alias in the statement. I would need each alias to be a new column. I also have to write the date condition in twice.
You could group them by type so you would get a different row for each of them :
SELECT i.type, COUNT(i.productNumber)
FROM items i
WHERE i.dateadded BETWEEN '2015-03-02' AND '2015-03-04'
GROUP BY i.type;
If you really want to have one row, then you could do
SELECT COUNT(b.productNumber) AS 'type1', COUNT(c.productNumber) AS 'type2'
FROM items i
LEFT JOIN items b on b.productNumber = i.productNumber
LEFT JOIN items c on c.productNumber = i.productNumber
WHERE i.dateadded BETWEEN '2015-03-02' AND '2015-03-04'
AND b.type = 'type1'
AND c.type = 'type2';