SQL view query not working - mysql

These are my tables:
portal_users(id, first_name, last_name, email, ...,)
courses(id, name, location, capacity, ...,)
feedback_questions(id, question, ...,)
feedback_answers(id, answers, IDquestion, IDuser, IDcourse)
I want to do a view with this:
course | first_name | last_name | IDuser | IDcourse | question_1 | answer_1 | question_2 | answer_2
So far
CREATE VIEW feedback_answers_vw
as
SELECT
fa.id,
fa.answer,
fq.question,
pu.first_name,
pu.last_name,
fa.IDuser,
fa.IDcourse
FROM feedback_answers fa
INNER JOIN feedback_questions fq
ON fa.IDquestion = fq.id
INNER JOIN portal_users pu
ON fa.IDuser = pu.id
INNER JOIN courses cu
on fa.IDcourse = cu.id
GROUP BY
fa.IDcourse, fa.IDuser
This just display one question and its answers, but not all the questions that belong to the same course and user.
I could hardcode this with something like this in the SELECT statement
SELECT
fa.id,
(select question
from feedback_questions
where id = 1) as question_1,
(select question
from feedback_questions
where id = 2) as question_2,
(select question
from feedback_questions
where id = 3) as question_3,
pu.first_name,
pu.last_name,
fa.IDuser,
fa.IDcourse
But I want to do it in the right way, so I won't be changing the code everytime that a question is added.
Edit:
This is data example of my tables:
**Portal users:**
1, tom, hanks, tom_hanks#example.com, ...,
2, steven, spielberg, steven#example.com, ...,
**Courses:**
1, quality, california, 30
2, information technologies, texas, 24
**Questions:**
1, How did you find the course?, ...,
2, Do you want purchase order?, ...,
**Answers:**
1, Internet, 1, 1, 1
2, yes, 2, 1, 1
3, TV, 1, 2, 1,
4, no, 2, 2, 1,
5, Internet, 1, 1, 2
6, yes, 1, 1, 2
This are data example I want to display in the view:
course|first_name|last_name|IDuser|IDcourse|Question_1|Answer_1|Question_2|Answer_2
----------------------------------------------------------------------------------
quality | tom | hanks | 1 | 1 | How did you find the course? | Internet | Do you want purchase order? | yes
quality | steven | spielberg | 2 | 1 | How did you find the course? | TV | Do you want purchase order? | no
Information technologies | tom | hanks | 1 | 2 How did you find the course? | Internet | Do you want purchase order? | yes

I dont know if you would actually need it as a view, and here is a sample query for it.
If you look at the first part of the where clause, it is based on a minimum of question ID = 1. I then join that to the course table and portal_user table to get those fields. Since the feedback answers is the first table, we can immediately get that answer, but also join to the questions table to get the question.
So now, how to simply expand this query for future questions? Notice the LEFT-JOINs right after that for the feedback answers AGAIN, but this time as an alias "fa2" (feedback_answers2), and the JOIN clause is based on the same user, same course, but only for question ID = 2. This then joins to the questions table on the fa2.questionID to the questions table (alias fq2). And then another exact same setup but for question 3. So here, I am doing nothing but using the same tables, but just different aliases. They all start with question 1, and if there is a question 2, get it... if a question 3, get it too, expand as needed, no group by, etc.
So now if you only cared about a single course you were interested, just add that to the outermost WHERE clause, nothing else needs to change. Get 10 questions, copy/paste the LEFT-JOIN components for the corresponding question IDs.
SELECT
c.name as course,
pu.first_name,
pu.last_name,
fa1.IDUser,
fa1.IDCourse,
fq1.question as question1,
fa1.answers as answer1,
fq2.question as question2,
fa2.answers as answer2,
fq3.question as question3,
fa3.answers as answer3
from
feedback_answers fa1
JOIN courses c
ON fa1.IDCourse = c.id
JOIN portal_users pu
ON fa1.IDUser = pu.id
JOIN feedback_questions fq1
ON fa1.IDquestion = fq1.id
LEFT JOIN feedback_answers fa2
ON fa1.IDUser = fa2.IDUser
AND fa1.IDCourse = fa2.IDCourse
AND fa2.id = 2
LEFT JOIN feedback_questions fq2
ON fa2.IDquestion = fq2.id
LEFT JOIN feedback_answers fa3
ON fa1.IDUser = fa3.IDUser
AND fa1.IDCourse = fa3.IDCourse
AND fa2.id = 3
LEFT JOIN feedback_questions fq3
ON fa3.IDquestion = fq3.id
where
fa1.id = 1

After some sample data was posted, it is clear a pivot table (in access/excel known as a 'crosstab') is needed here. We could hard code a select with some case statements to cover a fixed number of questions, but that solution would have to be revisited each time a question was added or removed. #drapp has such a solution below.
However, what we really want is the number of columns returned by the view to grow and shrink with the number of questions. Unfortunately mysql doesn't have any built in functions to help us build one. The best solution I found online is http://www.artfulsoftware.com/infotree/qrytip.php?id=523. The author proposes building the sql select statement dynamically then executing it. It is actually a pretty elegant solution.
Faced with this requirement, I wouldn't do this work in SQL. The problem I foresee is that as the numbers of questions rises, more and more columns will be returned; your view would quickly slow down to a crawl, and eventually stop working altogether. I would try to transform the data outside of sql, perhaps in an etl tool, application server, or BI tool.
Or, if I had the ability to (and I never do), I would switch database engines. Here are three solutions from other engines that provide tools for creating pivot tables:
Oracle: http://www.oracle.com/technetwork/articles/sql/11g-pivot-097235.html
Postgres: http://www.postgresql.org/docs/9.1/static/tablefunc.html
SQL Server: http://blogs.msdn.com/b/spike/archive/2009/03/03/pivot-tables-in-sql-server-a-simple-sample.aspx

We just talked about views in my DB Design class and my Professor said that you can't use a join in a VIEW statement. I can't recall why not but your issues looks familiar.

Related

Joining and selecting multiple tables and creating new column names

I have very limited experience with MySQL past standard queries, but when it comes to joins and relations between multiple tables I have a bit of an issue.
I've been tasked with creating a job that will pull a few values from a mysql database every 15 minutes but the info it needs to display is pulled from multiple tables.
I have worked with it for a while to figure out the relationships between everything for the phone system and I have discovered how I need to pull everything out but I'm trying to find the right way to create the job to do the joins.
I'm thinking of creating a new table for the info I need, with columns named as:
Extension | Total Talk Time | Total Calls | Outbound Calls | Inbound Calls | Missed Calls
I know that I need to start with the extension ID from my 'user' table and match it with 'extensionID' in my 'callSession'. There may be multiple instances of each extensionID but each instance creates a new 'UniqueCallID'.
The 'UniqueCallID' field then matches to 'UniqueCallID' in my 'CallSum' table. At that point, I just need to be able to say "For each 'uniqueCallID' that is associated with the same 'extensionID', get the sum of all instances in each column or a count of those instances".
Here is an example of what I need it to do:
callSession Table
UniqueCallID | extensionID |
----------------------------
A 123
B 123
C 123
callSum table
UniqueCallID | Duration | Answered |
------------------------------------
A 10 1
B 5 1
C 15 0
newReport table
Extension | Total Talk Time | Total Calls | Missed Calls
--------------------------------------------------------
123 30 3 1
Hopefully that conveys my idea properly.
If I create a table to hold these values, I need to know how I would select, join and insert those things based on that diagram but I'm unable to construct the right query/statement.
You simply JOIN the two tables, and do a group by on the extensionID. Also, add formulas to summarize and gather the info.
SELECT
`extensionID` AS `Extension`,
SUM(`Duration`) AS `Total Talk Time`,
COUNT(DISTINCT `UniqueCallID`) as `Total Calls`,
SUM(IF(`Answered` = 1,0,1)) AS `Missed Calls`
FROM `callSession` a
JOIN `callSum` b
ON a.`UniqueCallID` = b.`UniqueCallID`
GROUP BY a.`extensionID`
ORDER BY a.`extensionID`
You can use a join and group by
select
a.extensionID
, sum(b.Duration) as Total_Talk_Time
, count(b.Answered) as Total_Calls
, count(b.Answered) -sum(b.Answered) as Missed_calls
from callSession as a
inner join callSum as b on a.UniqueCallID = b.UniqueCallID
group by a.extensionID
This should do the trick. What you are being asked to do is to aggregate the number of and duration of calls. Unless explicitly requested, you do not need to create a new table to do this. The right combination of JOINs and AGGREGATEs will get the information you need. This should be pretty straightforward... the only semi-interesting part is calculating the number of missed calls, which is accomplished here using a "CASE" statement as a conditional check on whether each call was answered or not.
Pardon my syntax... My experience is with SQL Server.
SELECT CS.Extension, SUM(CA.Duration) [Total Talk Time], COUNT(CS.UniqueCallID) [Total Calls], SUM(CASE CS.Answered WHEN '0' THEN SELECT 1 ELSE SELECT 0 END CASE) [Missed Calls]
FROM callSession CS
INNER JOIN callSum CA ON CA.UniqueCallID = CS.UniqueCallID
GROUP BY CS.Extension

MySQL Query which require multiple joins

I have a system that is used to log kids' their behavior. If a child is naughty it is logged as negative and if it has a well behaviour it is logged as positive.
For instance - if a child is rude it gets a 'Rude' negative and this is logged in the system with minus x points.
My structure can be seen in this sqlfiddle - http://sqlfiddle.com/#!9/46904
In the users_rewards_logged table, the reward_id column is a foreign key linked to either the deductions OR achievements table depending on the type of column.
If type is 1 is a deduction reward, if the type value is 2 is a achievement reward.
I basically want a query to list out something like this:
+------------------------------+
| reward | points | count |
+------------------------------+
| Good Work | 100 | 1 |
| Rude | -50 | 2 |
+------------------------------+
So it tallys up the figures and matches the reward depending on type (1 is a deduction, 2 is a achievement)
What is a good way to do this, based on the sqlfiddle?
Here's a query that gets the above desired results:
SELECT COALESCE(ua.name, ud.name) AS reward,
SUM(url.points) AS points, COUNT(url.logged_id) AS count
FROM users_rewards_logged url
LEFT JOIN users_deductions ud
ON ud.deduction_id = url.reward_id
AND url.type = 1
LEFT JOIN users_achievements ua
ON ua.achievement_id = url.reward_id
AND url.type = 2
GROUP BY url.reward_id, url.type
Your SQLFiddle had the order of points and type in the wrong order for the table users_rewards_logged.
Here's the fixed SQLFiddle with the result:
reward points count
Good Work 100 1
Rude -50 2
Although eggyal is correct--this is rather bad design for your data--what you ask can be done, but requires a UNION clause:
SELECT users_achievements.name, users_rewards_logged.points, COUNT(*)
FROM users_rewards_logged
INNER JOIN users_achievements ON users_achievements.achievement_id = users_rewards_logged.reward_id
WHERE users_rewards_logged.type = 2
UNION
SELECT users_deductions.name, users_rewards_logged.points, COUNT(*)
FROM users_rewards_logged
INNER JOIN users_deductions ON users_deductions.deduction_id = users_rewards_logged.reward_id
WHERE users_rewards_logged.type = 1
GROUP BY 1, 2
There's no reason NOT to combine the achievements and deductions tables and just use non-conflicting codes. If you combined the tables, then you would no longer need the UNION clause--your query would be MUCH simpler.
I noticed that you have two tables (users_deductions and users_achievements) that defines the type of reward. As #eggyal stated, you are violating the principle of orthogonal design, which causes the lack of normalization of your schema.
So, I have combined the tables users_deductions and users_achievements in one table called reward_type.
The result is in this fiddle: http://sqlfiddle.com/#!9/813d5/6

visual basic/mysql displaying data

We're making a program with visual basic and we have a grid, it shows data from a database.
But now there is a problem, we're displaying numbers instead of actual words, this is easy to fix with a bit of google BUT we got 2 columns with references to the same table.
So in more details, we got one table with the references, it's name is v_Transport and has an id, relationid, carrierid (those are more then enough for this example)
the data looks like this
1 | 803 | 503
2 | 653 | 321
then we have a second table called Relations with an id and name(again those are enough)
653 | spike
321 | google
803 | stackoverflow
503 | humbletest
normaly I would do something like
select t.id, r.name
from transport t, relations r
where r.id = t.relationid
This could work if there was just one reference but there are 2 to the same table. just a small thing, I can't test my query so im pretty sure it's not working or even near working.
But my question is: what would be a good way to get the data to show the right way like
1 | stackoverflow | humbletest
2 | spike | google
And if possible with an explanation of the code and possible link to a source to get more info about the topic.
How about
select t.id, r.name relation_name, rc.name carried_name
from transport t INNER JOIN
relations r ON r.id = t.relationid INNER JOIN
relations rc ON rc.id = t.carrierid
This is way yuo join to the same reference table twice, from 2 different fields in the source table.
If you are unsure of if all the reference ids are populated, you would rathet use a LEFT JOIN
select t.id, IFNULL(r.name,'NA') relation_name, IFNULL(rc.name,'NA') carried_name
from transport t LEFT JOIN
relations r ON r.id = t.relationid LEFT JOIN
relations rc ON rc.id = t.carrierid
Then you could also use IFNULL to display what you want, if you wish.

MySQL LEFT JOIN issue: Retrieve workshops names in column A and a row id in column B

Apologies for the rubbish question title, but it's a bit tricky to summarise my requirement into a single line. I usually don't have an issue with MySQL JOINs but this one is throwing me.
I'm building a training feedback system and for one feature would like to display a list of all available workshops in the database, which workshops a given delegate has been assigned to and whether any feedback has been submitted by that delegate for those assigned workshops.
I could do this in a couple of queries, but I'm trying to do something a bit more elegant with a single query.
The pertinent details of my database structure:
WORKSHOPS table
id: INT
name: TINYTEXT
DELEGATES table
id: INT
name: TINYTEXT
FEEDBACK table
delegate_id: INT
workshop_id: INT
feedback: TEXT
DELEGATES_X_WORKSHOPS table
delegate_id: INT
workshop_id: INT
delegate_id and workshop_id in the tables are Foreign Keys to the DELEGATES and WORKSHOPS tables.
As any given delegate can be assigned to multiple workshops, I'm using the DELEGATES_X_WORKSHOPS table as a cross-referencing table so I can quickly search for who is assigned to any given workshop or which workshops any given delegate is assigned to.
However, I've tried LEFT JOINing a couple of different ways and I can't get a full list of workshops on the left and matches (if they exist) on the right for a given delegate_id.
Example data
Delegate Ross has delegate_id = 1
Registered workshops are
C++
PHP
ASP.NET
HTML5
JavaScript
Ross is assigned to PHP, HTML5 and JavaScript
Question 1 is this: how do I return the following for delegate_id=1:
[workshop] | [assigned]
C++ | null
PHP | TRUE
ASP.NET | null
HTML5 | TRUE
JavaScript | TRUE
(it doesn't matter right now what goes into column B, I just want a null if a particular delegate_id hasn't been assigned to a workshop).
I've used this:
SELECT
workshops.name,
delegates_x_workshops.delegate_id
FROM
workshops
LEFT JOIN
delegates_x_workshops
ON
workshops.id=delegates_x_workshops.workshop_id
WHERE
delegates_x_workshops.delegate_id=1
However I'm only returning the 3 rows where delegate_id=1, not 5 rows for all workshops.
Question 2 is a bit more involved:
Taking question 1 as a base, how would I work column C to display if feedback has been left for a workshop that Ross has been assigned to?
[workshop] | [assigned] | [givenfeedback]
C++ | null | null
PHP | TRUE | TRUE
ASP.NET | null | null
HTML5 | TRUE | null
JavaScript | TRUE | TRUE
Thanks in advance to anybody who makes it this far and has a clue what I'm blithering about. As I said, I could rattle through this with a few different queries, but I'm trying to keep things elegant.
No doubt half of this will need clarification, so ask any questions.
Thanks
For question 1, you need to move the where condition into the on clause. It is turning the left outer join into an inner join because non-matching rows have NULL values:
SELECT w.name, dxw.delegate_id
FROM workshops w LEFT JOIN
delegates_x_workshops dxw
ON w.id = dxw.workshop_id and
dxw.delegate_id = 1;
For the second question, I think this is what you want:
SELECT w.name,
(case when max(w.name = 'Ross') > 0 then 'True' end) as Assigned,
(case when count(f.workshop_id) > 0 then 'True' end) as Feedback
FROM workshops w LEFT JOIN
delegates_x_workshops dxw
ON w.id = dxw.workshop_id and
dxw.delegate_id = 1 LEFT JOIN
delegates d
on d.id = dxw.delegate_id LEFT JOIN
feedback f
on f.workshop_id = w.id
GROUP BY w.name;
For reference, here's my final query:
SELECT DISTINCT
workshops.id AS wid,
workshops.name AS workshop,
(delegates_x_workshops.delegate_id IS NOT NULL) AS assigned,
(initial_feedback.delegate_id IS NOT NULL
OR
ongoing_feedback.delegate_id IS NOT NULL) AS hasfeedback
FROM
workshops
LEFT JOIN
delegates_x_workshops
ON
workshops.id = delegates_x_workshops.workshop_id
AND
delegates_x_workshops.delegate_id = 1
LEFT JOIN
initial_feedback
ON
workshops.id = initial_feedback.workshop_id
AND
initial_feedback.delegate_id = 1
LEFT JOIN
ongoing_feedback
ON
workshops.id = ongoing_feedback.workshop_id
AND
ongoing_feedback.delegate_id = 1
ORDER BY
workshop ASC
For every workshop in the WORKSHOPS table, I'll get the id and name of the workshop, 1 or 0 if a given delegate_id is assigned and 1 or 0 if feedback of either type (I have 2 kinds) has been left for that workshop.
Scary to think that all I was missing was an AND condition on my LEFT JOIN.
Thanks again Gordon!

MySQL multiple intersection with self performance

For simplicity, let's say we have a table with two columns: uid (user id) and fruit, describing what kinds of fruit a user likes.
E.g.:
uid | fruit
----|------------
1 | Strawberry
1 | Orange
2 | Strawberry
2 | Banana
3 | Watermelon
and so forth.
If I want to find what kinds of fruit are common in N particular users (i.e. the intersection N times of the table with itself), the first option is to use an INNER JOIN.
SELECT DISTINCT fruit FROM Fruits f1
INNER JOIN Fruits f2 USING (fruit)
INNER JOIN Fruits f3 USING (fruit)
...
INNER JOIN Fruits fN USING (fruit)
WHERE f1.uid = 1 AND f2.uid = 2 ... AND fN.uid = M
But this kinds of looks silly to me. What if N = 10? or even 20? Is it sensible to do 20 joins? Is there some other join operation I'm missing?
Before learning the "magic" of joins, I used another method, which would apply in my current case as follows:
SELECT DISTINCT fruit FROM Fruits
WHERE uid IN (1, 2, ..., M)
GROUP BY fruit
HAVING COUNT (*) = N
It seems much more compact, but I remember somebody telling me to avoid using GROUP BY because it is slower than an INNER JOIN.
So, I guess my question really is, is there maybe a third method for doing the above? If yes/no, which one is the most efficient?
-- EDIT --
So, it seems a question has been asked before, bearing a resemblance to mine. The two answers provided, are actually the two methods I'm using.
But the question remains. Which one is really more efficient? Is there, maybe, a third one?