Say I have 2 tables:
Person
- Id
- Name
PersonAttribute
- Id
- PersonId
- Name
- Value
Further, let's say that each person had 2 attributes (say, gender and age). A sample record would be like this:
Person->Id = 1
Person->Name = 'John Doe'
PersonAttribute->Id = 1
PersonAttribute->PersonId = 1
PersonAttribute->Name = 'Gender'
PersonAttribute->Value = 'Male'
PersonAttribute->Id = 2
PersonAttribute->PersonId = 1
PersonAttribute->Name = 'Age'
PersonAttribute->Value = '30'
Question: how do I query this such that I get a result like this:
'John Doe', 'Male', '30'
SELECT p.name, p1.Value, p2.Value
FROM Person p, PersonAttribute p1, PersonAttribute p2
WHERE p.Id = p1.PersonId AND p.Id = p2.PersonId
AND p1.Name = 'Gender' AND p2.Name = 'Age'
I think you need redesign your schema.
Why not?
Person
- Id
- Name
- Gender
- Birthday
...
SELECT p.Name, g.Value, a.Value
FROM Person p INNER JOIN PersonAttribute g ON p.Id = g.Id AND g.Name = "Gender" INNER JOIN PersonAttribute a ON p.Id = a.Id AND a.Name = "Age"
Storing Name Value pairs does give flexibility but is very cumbersome to query.
Take a look at http://www.simple-talk.com/community/blogs/philfactor/archive/2008/05/29/56525.aspx
Leaving the design aside, you can always PIVOT the result but you need to know how many attributes you are selecting out in advance.
There's no easy way to do this.
The concept of a pivot table (already mentioned by another answer) is basically what you are looking for, except that pivot tables require you to know the names of the columns you wish to use. Clearly this is a problem when you want to exploit the power of such a table design!
In my previous life, I just settled on X number of columns, like 20-30, and if they didn't exist, then the row set included a bunch of null values. No big deal.
select piv.name,
max(case piv.a_name when 'Gender' then piv.a_value else null end) as Gender,
max(case piv.a_name when 'Age' then piv.a_value else null end) as Age,
max(case piv.a_name when 'Hobby' then piv.a_value else null end) as Hobby
from
(select p.name as name, pa.name as a_name, pa.value as a_value
from person p, personattribute pa
where p.id = pa.personid) piv
group by piv.name
This would generate output like so:
name | gender | age | hobby
-----------+--------+-----+---------
Bob Swift | Male | | Reading
John Doe | Male | 30 |
(2 rows)
Which is pretty damned close to what you are looking for. I would leave the rest of it up to your application-layer.
I also highly recommend that you include the attribute NAME as part of the return value, to provide context to the VALUEs.
These types of so-called Entity-Attribute designs often end up having to rely on a combination of server-specific functions, stored procedures, and hard-coded queries.
You need to JOIN the two tables. Wikipedia provides a pretty good explanation of JOIN: http://en.wikipedia.org/wiki/Join_%28SQL%29
SELECT Name, g.Value, a.Value
FROM Person,
PersonAttribute g INNER JOIN ON g.Name = "Gender",
PersonAttribute a INNER JOIN ON a.Name = "Age"
Related
In the simplified example:
The idea is to get all (player, coach, and ref) names into the final query, but the only way I can think to do that is to join 3 times on the respective id. What is a better way?
Team
...|Coachid | Playerid | Refid|
--------------------------
...| 98 | 23 | 77 |
Name
Id | Name |
--------------------
98 | Andy |
23 | Charlie |
SELECT [t].[Id],
[t].[TeamName],
[c].[Name] AS CoachName,
[p].[Name] AS PlayeName,
[r].[Name] as RefName
FROM Team [t]
JOIN Name [c]
ON c.id = t.Coachid
JOIN Name [p]
ON p.id = t.PlayerId
JOIN Name [r]
ON r.id = t.RefId
As it has been commented, your approach is the best way to adress your case and should have good performance.
Alternatives would include:
1) A series of correlated subqueries - this is OK because there is just one value to return per relation:
SELECT
t.Id,
t.TeamName,
(SELECT n.Name FROM AS Name n WHERE n.id = t.CoachId) CoachName,
(SELECT n.Name FROM AS Name n WHERE n.id = t.PlayerId) PlayerName,
(SELECT n.Name FROM AS Name n WHERE n.id = t.RefId) RefName
FROM Team t
2) Conditional aggregation - makes the query more cumbersome:
SELECT
t.Id,
t.TeamName,
MAX(CASE WHEN n.id = t.CoachId THEN n.Name END) CoachName,
MAX(CASE WHEN n.id = t.PlayerId THEN n.Name END) PlayerName,
MAX(CASE WHEN n.id = t.RefId THEN n.Name END) RefName
FROM Team t
INNER JOIN Name n ON n.id IN (t.CoachId, t.PlayerId, t.RefId)
GROUP BY t.Id, t.TeamName
With the way your table is structured, the way you provided is the best way to do it.
It is odd though, the way you have structured the Team table.
You can simplify the whole query just to one join, if you had made your Team table just have the id's of each person, and in your Name table, you would have the id and the role / position of that id.
I guess this is a good example of how important it is to structure your tables correctly.
I have problems displaying columns from two common table expression. I created the first table by querying the student names and their mid-term grades and the other table the student names and their final-term grades.
CREATE TABLE MidTerm AS (SELECT Name, Score
FROM GRADE
WHERE TYPE = ''MidTerm
)
CREATE TABLE FinalTerm AS (SELECT Name, Score
FROM GRADE
WHERE TYPE = 'Final'
)
Both of the created have the same number of columns and the same variables. Now I want to display the Name, Score "MidTerm" and Score "FinalTerm", how can I achieve this? I manage to use UNION at the expense of SELECT * only. If I specify
Midterm table:
Name : Score
A : 50
B : 60
Finalterm table:
Name : Score
A : 70
B : 80
I want to join the CTE tables by displaying
Final Intended Result:
Name : Score "MidTerm" : Score "FinalTerm"
A : 50 : 70
B : 60 : 80
it would say invalid column identifier. How do I solve this?
A simple join will handle this:
SELECT m.NAME AS "Name",
m.SCORE AS "Score MidTerm",
f.SCORE AS "Score FinalTerm"
FROM MIDTERM m
LEFT OUTER JOIN FINALTERM f
ON f.NAME = m.NAME
db<>fiddle here
If you have two tables for midterm and final score as per comment in gordon's answer then just do join and you will get your result like this:
Select m.name,
M.score as midterm_score,
F.score as final_score
From midterm_table m
Join final_table f
on (m.name = f.name);
Cheers!!
I think that you are just looking for conditional aggregation:
select
name,
max(case when score = 'MidTerm' then score end) MidTerm,
max(case when score = 'Final' then score end) Final
from grade
where score in ('MidTerm', 'Final')
group by name
I am baffled. Use conditional aggregation:
SELECT Name,
MAX(CASE WHEN Type = 'MidTerm' THEN Score END) as midterm_score,
MAX(CASE WHEN Type = 'Final' THEN Score END) as final_score,
FROM GRADE
GROUP BY Name;
CTEs do not help with this query at all.
You could also do this using a JOIN:
select m.name, m.score as midterm_score, f.score as final_score
from grade m join
grade f
on m.name = f.name and
m.type = 'midterm' and
f.type = 'final';
Note that this only shows names with both scores.
Add student's id in those tables and use it to join them and gather the columns that you need.
I dont believe that create this two tables is realy a good idea,
but, ok, I don't know the complexity of your calculations to get the score.
anyway, I would suggest to you consider the creation of an view for that instead of create those table.
I have two tables tblPatient, tblDropDowns
tblpatient:
firstname gender patienttype
anil 1 3
Satheesh 1 4
Vinod 1 4
Shashikanth 1 3
Srimani 2 3
Thanuja 2 4
Nandini 2 4
Vishu 2 3
and
tblDropdowns:
id Name
1 Male
2 Female
3 Inpatient
4 Outpatient
Now i want to display the patient table with gender and patient type as their significant names are connected to dropdown table.
result table:
firstname gender patienttype
anil male inpatient
satheesh male outpatient
vinod male outpatient
please help me out..
thanks
anil
In general it would be better avoid storing different things in the same table. However, you could join with subqueries that only contain the relevant records.
SELECT firstname, gender.Name AS gender, patienttype.Name As patienttype
FROM tblPatient p
INNER JOIN (SELECT id, Name
FROM tblDropdowns
WHERE id IN (1, 2)) gender
ON p.gender = gender.id
INNER JOIN (SELECT id, Name
FROM tblDropdowns
WHERE id > 2) patienttype
ON p.patienttype = patienttype.id
Please try out this.
select [column1], [column2] from tblpatient a, tbldropdowns b
where a.gender = b.id order by a.gender;
You could also use joins: Please refer this W3Schools SQL Link.
Hope this helps. Thanks.
Edit
Maybe this query could be your solution:
select a.firstname, b.name as 'gender', b.name as 'PatientType'
from tblpatient a, tbldropdowns b
where a.gender = b.id and a.patienttype = b.id
order by a.gender;
Thanks again. :-)
Try as follows:
SELECT firstname, name, CASE WHEN (patienttype=3) THEN 'inpatient' ELSE 'outpatient' END as patienttype_text from tblpatient INNER JOIN tbldropdowns ON gender = id
join your "tblDropdowns" table twice in your select query.
Please refer this link to understand join,
http://www.w3schools.com/sql/sql_join_left.asp
select tP.firstname,tG.Name gender,tPT.Name patienttype
from tblPatient tP
left join tblDropDowns tG
on tG.id = tP.gender
left join tblDropDowns tPT
on tPT.id = tP.patienttype
names:
id, first, last
879 Scotty Anderson
549 Melvin Anderson
554 Freddy Appleton
321 Grace Appleton
112 Milton Appleton
189 Jackson Black
99 Elizabeth Black
298 Jordan Frey
parents:
id, student_id
549 879
321 554
112 554
99 189
298 189
Expected Output
(without the 'Student:' / 'Parent:')
Student: Anderson, Scotty
Parent: Anderson, Melvin
Student: Appleton, Freddy
Parent: Appleton, Grace
Parent: Appleton, Milton
Student: Black, Jackson
Parent: Black, Elizabeth
Parent: Frey, Jordan
Using the data above, how can I achieve the expected output?
I currently use SQL similar to this to get a list of current students and names.
select b.last, b.first
from term a, names b
where a.id = b.id(+)
order by b.last
Which returns:
Anderson, Scotty
Appleton, Freddy
Black, Jackson
My question is how to take the parents table and add to this query so it has this output:
Anderson, Scotty
Anderson, Melvin
Appleton, Freddy
Appleton, Grace
Appleton, Milton
Black, Jackson
Black, Elizabeth
Frey, Jordan
The idea in a query like this is to break the data down into something that helps you solve the problem, and then put it back together as needed. In this case I'm going to make use of common table expressions, which allows me to treat queries as tables and then recombine them handily.
Looking at the desired results it looks like we want to have the students appear first, followed by their mothers (ladies first :-), and then their fathers. So, OK, let's figure out how to extract the needed data. We can get the students and their associated data pretty simply:
select distinct p.student_id as student_id,
n.first,
n.last,
0 as type
from parents p
inner join names n
on n.id = p.student_id
The type column, with its constant value of zero, is just used to identify that this is a student. (You'll see why in a minute).
Now, getting the mother's is a bit more difficult because we don't have any gender information to use. However, we'll use what we have, which is names. We know that names like Melvin, Milton, and Jordan are "guy" names. (Yes, I know Jordan can be a girls name too. My daughter has a male coach named Jordan, and a female teammate named Jordan. Just go with it - for purposes of argument in this case Jordan is a guys name, 'K? 'K :-). So we'll use that information to help us identify the mom's:
select p.student_id, n.first, n.last, 1 as type
from parents p
inner join names n
on n.id = p.id
where first not in ('Melvin', 'Milton', 'Jordan')
Notice here that we assign the value of 1 to the type column for mothers.
Similarly, we'll find the dads:
select p.student_id, n.first, n.last, 2 as type
from parents p
inner join names n
on n.id = p.id
where first in ('Melvin', 'Milton', 'Jordan')
And here we assign a value of 2 for the type.
OK - given the above we just need to combine the data properly. We don't want to use a JOIN, however, because we want the names to get spit out one after the other from the query - and the way we do THAT in SQL is with the UNION or UNION ALL operator. (Generally, you're going to want to use UNION ALL, because UNION will check the result set to ensure there are no duplicates - which in the case of a large result set takes, oh, more or less FOREVER!). And so, the final query looks like:
with all_students as (select distinct p.student_id as student_id,
n.first,
n.last,
0 as type
from parents p
inner join names n
on n.id = p.student_id),
all_mothers as (select p.student_id, n.first, n.last, 1 as type
from parents p
inner join names n
on n.id = p.id
where first not in ('Melvin', 'Milton', 'Jordan')),
all_fathers as (select p.student_id, n.first, n.last, 2 as type
from parents p
inner join names n
on n.id = p.id
where first in ('Melvin', 'Milton', 'Jordan'))
select last || ', ' || first as name from
(select * from all_students
union all
select * from all_mothers
union all
select * from all_fathers)
order by student_id desc, type;
We just take the student data, followed by the mom data, followed by the dad data, then sort it by the student ID from highest to lowest (I just looked at the desired results to figure out that this should be a descending sort), and then by the type (which results in the student (type=0) being first, following by their mother (type=1) and then their father (type=2)).
SQLFiddle here
Share and enjoy.
generic SQL, mmmmm I'd like there to be A generic SQL :)
First off you want to stop using the antique (+) join syntax that is exclusive to Oracle
select b.last, b.first
from term a
LEFT OUTER JOIN names b ON a.id = b.id
order by b.last
That is way more generic! (nb: You can abbreviate to just LEFT JOIN)
Now to concatenate (Last Name comma space First Name) there are options some not generic
SQL Server/MySQL and others supporting CONCAT()
select CONCAT(b.last , ', ', b.first)
from term a
LEFT OUTER JOIN names b ON a.id = b.id
order by b.last
not all versions of Oracle or SQL Server support CONCAT()
Oracle's concat() only takes 2 parameters; grrrrr
ORACLE
select b.last || ', ' || b.first
from term a
LEFT OUTER JOIN names b ON a.id = b.id
order by b.last
In this form Oracle generally handles data type conversions automatically (I think, please check on date/timestamps maybe others)
TSQL (Sybase, MS SQL Server)
select b.last + ', ' + b.first
from term a
LEFT OUTER JOIN names b ON a.id = b.id
order by b.last
In this form you must explicitly cast/convert data types to n|var|char for concatenation if not already a string type
For your list of concatenated names:
You need in addition to the last name a method to retain the family group together, plus distinguish between student and parent. As you want just one column of names this indicates you need a column of id's that point to the last and first names. So making some assumptions about the table TERM my guess is you list the students from that, then append the parents that relate to that group of students, and finally to output the required list in the required order.
select
case when type = 1 then 'Student' else 'Parent' end as who
, names.last || ', ' || names.first as Name
from (
select
STUDENT_ID as name_id
, STUDENT_ID as family_id
, 1 as TYPE
from term
union all
select
PARENTS.ID as name_id
, PARENTS.STUDENT_ID as family_id
, 2 as TYPE
from PARENTS
inner join term on PARENTS.STUDENT_ID = term.STUDENT_ID
) sq
inner join NAMES ON sq.name_id = NAMES.ID
order by
names.last
, sq.family_id
, sq.type
see: http://sqlfiddle.com/#!4/01804/6
This is too long for a comment.
Your question doesn't make sense. The easy answer to the question is:
select last, first
from names;
But it seems unlikely that is what you want.
Your sample query mentions a table term. That is not mentioned elsewhere in the question. Please clarify the question or delete this one and ask another.
I think I see what you're trying to do. I think you could set up a derived table and then query it. Set up something like: case when student id= id then 1 else 0 as match or whatever. Then query your derived table and group by match.
I would do it like that in SQL:
Select last +', '+ first as fullname from names;
I was asked this trick question:
Table: Student
ID NAME
1 JOHN
2 MARY
3 ROBERT
4 DENNIS
Table: Grade
ID GRADE
1 A
1 A
1 F
2 B
3 A
How do you write SQL query to return DISTINCT name of all students who has never received grade 'F' OR who has never taken a course (meaning, their ID not present in Grade table)?
Trick part is, you're not allowed to use OUTER JOIN, UNION or DISTINCT. Also, why this is a big deal?
Expected result is MARY, ROBERT, DENNIS (3 rows).
SELECT name FROM Student
WHERE
NOT EXISTS (SELECT * FROM Grade WHERE Grade.id = Student.id AND grade = 'F')
OR
NOT EXISTS (SELECT * FROM Grade WHERE Grade.id = Student.id);
You may use GROUP BY in order to fake a distinct.
SELECT name FROM student
WHERE (SELECT COUNT(*) FROM grade WHERE grade = 'F'
AND id = student.id) = 0
at least this is the shortest answer so far ...
Something like this could work, if you're allowed to use subqueries.
SELECT `NAME`
FROM Student
WHERE 'F' NOT IN
(SELECT GRADE FROM Grade WHERE ID = Student.ID)
Hmm, my homework sense is tingling... Not that my questions have never related to homework though...
You could use the GROUP BY and aggregate functions in order to fake a distinct.
You want to exclude everyone who has both taken a course and received a grade of 'F'. Something like this might work:
SELECT NAME
FROM Student
WHERE 0 = (SELECT COUNT(*)
FROM Student
LEFT JOIN Grade
USING (ID)
WHERE GRADE='F')
GROUP BY NAME
SELECT name
FROM grade G, student S
WHERE (S.id = G.id AND 'F' NOT IN (SELECT G1.grade
FROM grade G1
WHERE G1.id = G.id))
OR
S.id NOT IN (SELECT id
FROM grade)
GROUP BY name
A reason why they might not want you to use UNION, JOIN or DISTINCT is that some of those queries might be slow if you try to force an "optimized" solution.
I'm not too familiar with query optimization techniques but usually if you use some of those aggregators and JOINs, you might slow down your query rather than just letting the query optimizations run through and organize your SQL based on your table structure and contents.