3-level Hierarchical SQL Query - sql

I'm very well aware of basic SQL queries, however I've never worked with a query that deals with hierarchical data.
I'm using the Telerik TreeView control to display data related to a school board with the following structure:
--School "ABC"
----Class "Grade 1"
----Class "Grade 2"
------Student #1
------Student #2
--School "DEF"
----Class "Grade 1"
------Student #3
----Class "Grade 3"
The TreeView is in a tri-state checkbox mode, so each element can be checked off. So if a single child element is checked then the parent element will be checked as well, and when checking a parent element all the child elements will be checked.
After checking off some schools, classes, and students, the following screen displays information about the students in a graph which currently uses the school IDs (if multiple are checked) to select all students of those schools.
Here's where it gets tricky. Using the above structure as an example, I select the class "Grade 1" from the school "ABC" and class "Grade 3" from the school "DEF" which in turn will select the students #1 & #2 AND the schools "ABC" & "DEF".
Selected schools: ABC, DEF
Selected classes: Grade 1, Grade 3
Selected Students: #1, #2
As mentioned before, my current SQL query is based solely on the school ID and I know that I can't simply add two other conditions in the where clause that look like this:
AND ClassID IN ('Grade 1', 'Grade 3') --Note there is no primary key for classes, and I can't change that in my position..
AND StudentID IN (1,2)
as this will also select student #3 from the other class title "Grade 1"
So my question is, how to I combine the School ID(s), Class name(s), and student ID(s) into one condition that will solve this issue.
Edit:
Here's a structure of the tables.

If i understand you correctly you have the selected school, class and student. So would this work for you:
where (school = "ABC" and
class = "Grade1")
OR
(school = "DEF" and
class = "Grade3")
I hope i understood your requirements correctly.

Related

SQL join both ways to one result

I have two tables "TestItem" and "Connector" where Connector is used for relating two items in "TestItem".
I have two questions in prioritized order. But first, feel free to suggest alternative approaches. I'm open for suggestion to completely rethink my approach to what I want to achieve here.
Question 1) How to get relations both ways returned in the same result
Question 2) How to filter the most efficient way for specific items
Q1)
Two tables
Table: "TestItem"
ID, ITEM
1, "John Doe"
2, "Peggy Sue"
3, "Papa Sue"
Table: "Connector"
MOTHER, CHILD
1,2
The connector table will be used for several purposes (see below), but this is a destilled scenario for the equal type connection, like for instance marriage. If "John Doe" is married to "Peggy Sue" that information should also be sufficient to return "Peggy Sue" as married to "John Doe".
I can do this in two queries, but for efficiency (especially regarding my question 2) I'd appreciate this done in one query, so an implementation is not dependent on which way the connection is defined.
What is the most efficient way to do this?
Two queries approach to illustrate how the data can be fetched, but how one connection is missed one way or the other.
//Connector through "mother"-part SELECT ITEM, SUBITEM FROM TestItem
INNER JOIN (
SELECT MOTHER, ITEM AS SUBITEM
FROM Connector
INNER JOIN TestItem ON Connector.CHILD = TestItem.ID
) AS SUB ON TestItem.ID = SUB.MOTHER
/* WHERE ITEM = "John Doe" return "Peggy Sue" => Correct
WHERE ITEM = "Peggy Sue" return nothing => Wrong
*/
//Connector through "child"-part SELECT ITEM, SUBITEM FROM TestItem
INNER JOIN (
SELECT CHILD, ITEM AS SUBITEM
FROM Connector
INNER JOIN TestItem ON Connector.MOTHER= TestItem.ID
) AS SUB ON TestItem.ID = SUB.CHILD
/* WHERE ITEM = "John Doe" return nothing => Wrong
WHERE ITEM = "Peggy Sue" return "John Doe" => Correct
*/
Q2) Having the two approaches returned in one result may increase the amount of data involved, and hence bring down performance. If my focus is Peggy Sue, I assume sorting out only the relevant data as early as possible will improve performance. Is there a neat way of doing this from top level, or will every sub-query require an added WHERE?
PS: Some more information of the bigger perspective.
I'm planning to use the connector table for several purposes, both of the mentioned equal type, like colleagues, family, friends, etc, but also for hierarchical connection types like mother/child, leader/employee, country/city.
Thus solutions eliminating the mother/child-type connection may not suit my bigger purpose.
Basically I'm requesting how to handle the equal type of connections without losing the opportunity to use the same architecture and data for hierarchical connections.
Peggy Sue may through the same dataset be defined as daughter of Papa Sue through the relation
Mother, Child, Mother_type, Child_type
3, 2, Father, Daughter
1, 2, Married to, Married to
(But this is as mentioned on the side of what I'm requesting here. )
UNION ALL might be what you are looking for:
select mother.id as connectedToId,
mother.item as connectedToItem,
'Mother' as role
from TestItem ti
join Connector c on c.child = ti.id
join TestItem mother on c.mother = mother.id
where ti.item = 'John Doe'
union all
select child.id as connectedToId,
child.item as connectedToItem,
'Child' as role
from TestItem ti
join Connector c on c.mother = ti.id
join TestItem child on c.child = child.id
where ti.item = 'John Doe'

OrientDB SQL query to emulate a many to many join

Given the following schema created using the OrientDB Document API:
OClass team = getoDocDatabase().getMetadata().getSchema().createClass(TEAM);
team.createProperty(NAME, OType.STRING);
OClass driver = getoDocDatabase().getMetadata().getSchema().createClass(DRIVER);
driver.createProperty(NAME, OType.STRING);
OClass car = getoDocDatabase().getMetadata().getSchema().createClass(CAR);
car.createProperty(NAME, OType.STRING);
// Relationships
team.createProperty(CARS_HERITAGE, OType.LINKSET, car);
car.createProperty(BUILT_BY, OType.LINK, team);
car.createProperty(DRIVEN_BY, OType.LINKSET, driver);
driver.createProperty(DRIVER_OF, OType.LINKSET, car);
What's the sql query to fetch all the teams that Fernando Alonso has driven for?
In relational SQL would be as easy as
SELECT team.name FROM {the join} where driver.name = 'Fernando Alonso'
I have try with this db
create class Team
CREATE PROPERTY Team.name String
create class DRIVER
CREATE PROPERTY DRIVER.name String
create class Car
CREATE PROPERTY Car.name String
CREATE PROPERTY Team.CARS_HERITAGE LINKSET Car
CREATE PROPERTY Car.BUILT_BY LINK Team
CREATE PROPERTY Car.DRIVEN_BY LINKSET DRIVER
CREATE PROPERTY DRIVER.DRIVER_OF LINKSET Car
INSERT
INSERT INTO TEAM(name) values ("Ferrari"),("Renault") // Ferrari 12:0 Renault 12:1
insert into Driver(name) values ("Fernando Alonso"),("Giancarlo Fisichella") // Alonso 13:0 Fisichella 13:1
insert into car(name,BUILT_BY,DRIVEN_BY) values ("car ferrari",#12:0,[#13:0,#13:1])
insert into car(name,BUILT_BY,DRIVEN_BY) values ("car renault",#12:1,[#13:0])
Query
select BUILT_BY.name as TeamName from car where DRIVEN_BY.name contains "Fernando Alonso"
Hope it helps.
UPDATE 1
select distinct(BUILT_BY.name) as team from car where DRIVEN_BY.name contains "Fernando Alonso"
FROM JAVA API
UPDATE 2
FROM JAVA API
The answer from #Alessandro is correct, however I've found some weirdnesses in the way the sql is interpreted. Let me explain myself.
I've simplified the goal, let's try to find the query to fetch the teams which have had at least a car.
This first query works, is the one suggested by Alessandro. It returns a list of documents containing one property, that is the name of the team.
select distinct(team.name) as name from Car
This second query works as well, returning the list of teams (as documents).
select expand(distinct(team)) from Car
This third query works and returns exactly the same result that the previous, so it ignores the ".name" part of the select.
select expand(distinct(team)).name from Car
This last query fails. Well it doesn't fail, but it doesn't return what I expected, it returns a list of links to the teams.
select distinct(team).name from Car
Tests running the queries: Tests.

Subqueries for each row

Using these tables:
*student {'s_id','s_name,'...} , class {'c_id','c_name',...} student2class {'s_id','c_id'}, grades {'s_id','c_id','grade'}*
Is it possible to perform a query (nested query?) put class name as subtitle and then all students (of that class) and grades, next class name as subtitle ...
The result I need is:
Maths
John .... C
Anna .... B
[...]
Biology
Anna .... C
Jack .... A
[...]
For each row from class I'll have a subquery fetching all data related with this class
No need of any sub-query. You can get your data this way:
SELECT c_name, s_name_, grade
FROM student, class, grades
WHERE student.s_id = grades.s_id and class.c_id = grades.c_id
ORDER BY c_name
The presentation of results depends on your system/tools, as others already said. This is a link to the solution for Microsoft Access:
http://office.microsoft.com/en-001/access-help/create-a-grouped-or-summary-report-HA001159160.aspx
The solution should be implemented in your client side code and not in the database. From database you should just get a simple table formatted data (subject, student, grade)
Then convert the above recordset to the format you want:
For an example in C# you could convert the recordset into lookup
var Lookup = DataSet.Tables[0].Rows.ToLookup(x => x["subjectColumn"]);
now you can loop through the lookup and format your result
foreach (var grade in Lookup)
{
subject = grade.Key;
...
}

Problems with Rails 3 Active Record Query Interface with join on ID

I have been having a problem with the Rails 3 Active Record Query Interface. I have a lookup table (lookups), a Main table (through_references), and a through/join table called through_tables. Thus this is a HABTM configuration that I have set up using has_many :through.
Update: Of special note here is that when I am doing these joins, I have been joining on IDs, to provide filtering of records. It seems that this does not work with Active Record Query Interface. If you do not want to see the gory details of my travails, you can skip down to see my workaround below.
We are also going to have a number of Main Items (through_references table) should be able to have any combination of lookup items, and to conveniently be able to click the relevant lookup items say through check boxes.
I have posted the code on github. There is quite a lot more explanations on the github source code. to see the results, go to the lookups index page. Note that you will need to create the records using the scaffold code.
I also have the code up and running on heroku, with more explanations and examples.
class Lookup < ActiveRecord::Base
has_many :fk_references
has_many :through_tables
has_many :through_references, :through => :through_tables
attr_accessible :name, :value
end
class ThroughTable < ActiveRecord::Base
belongs_to :through_reference
belongs_to :lookup
attr_accessible :description, :through_reference_id, :lookup_id
end
class ThroughReference < ActiveRecord::Base
has_many :through_tables
has_many :lookups, :through => :through_tables
attr_accessible :description
end
If we want to have a listing if all the lookup items, and the Main Items that correspond with them, we can LEFT JOIN the ‘lookups’ table with the Main Items (through_references) table.
Corresponding SQL:
SELECT * FROM lookups
LEFT OUTER JOIN through_tables ON (lookups.id = through_tables.lookup_id AND through_tables.through_reference_id = 1)
LEFT OUTER JOIN through_references ON through_references.id = through_tables.through_reference_id
ORDER BY lookups.id
Returned records:
1;“Lookup Item 1”;“1”;“2012-06-06 17:14:40.819791”;“2012-06-06 17:14:40.819791”;1;1;1;“Main Item 1 has Lookup item 1”;“2012-06-06 17:17:31.355425”;“2012-06-06 17:17:31.355425”;1;“Main Item 1”;“2012-06-06 17:16:30.004375”;“2012-06-06 17:16:30.004375”
2;“Lookup Item 2”;“2”;“2012-06-06 17:14:59.584756”;“2012-06-06 17:14:59.584756”;;;;“”;“”;“”;;“”;“”;“”
3;“Lookup Item 3”;“3”;“2012-06-06 17:15:14.700239”;“2012-06-06 17:15:14.700239”;2;1;3;“Main Item 1 has Lookup item 3”;“2012-06-06 17:17:53.169715”;“2012-06-06 17:17:53.169715”;1;“Main Item 1”;“2012-06-06 17:16:30.004375”;“2012-06-06 17:16:30.004375”
This is what I expected.
=== Active Record Query Interface using custom left join
Lookup.joins(“LEFT OUTER JOIN through_tables ON (lookups.id = through_tables.lookup_id AND through_tables.through_reference_id = 1)” ).includes(:through_references).order(‘lookups.id’)
What is returned from Active Record Query Interface (note I navigate down through the Active Record hierarchy):
Lookup ID Lookup Name Lookup Value Through Table ID Through Table Description Main Item ID Main Item Description
1 Lookup Item 1 1 1 Main Item 1 has Lookup item 1 1 Main Item 1
1 Lookup Item 1 1 3 Main Item 2 has Lookup item 1 2 Main Item 2
2 Lookup Item 2 2 4 Main Item 2 has Lookup item 2 2 Main Item 2
3 Lookup Item 3 3 2 Main Item 1 has Lookup item 3 1 Main Item 1
This is NOT what I expected.
What we have here is identical to the simple left join (without the AND clause). This tells me that the AND clause is being ignored in the Active Record Query Interface.
=== Active Record Query Interface using find_by_sql approach
Lookup.find_by_sql("SELECT * FROM lookups LEFT OUTER JOIN through_tables ON (through_tables.lookup_id = lookups.id AND through_tables.through_reference_id = 1) LEFT OUTER JOIN through_references ON through_references.id = through_tables.through_reference_id ORDER BY lookups.value, through_references.id" )
What is returned from Active Record Query Interface (note I navigate down through the Active Record hierarchy)::
Lookup ID Lookup Name Lookup Value Through Table ID Through Table Description Main Item ID Main Item Description
1 Lookup Item 1 1 3 Main Item 2 has Lookup item 1 2 Main Item 2
1 Lookup Item 1 1 1 Main Item 1 has Lookup item 1 1 Main Item 1
Lookup Item 2 2 No through_tables entry
1 Lookup Item 3 3 3 Main Item 2 has Lookup item 1 2 Main Item 2
1 Lookup Item 3 3 1 Main Item 1 has Lookup item 1 1 Main Item 1
The results here are crazy!
Is this a BUG, is this the intended effects, or am I missing something ?
I hope there is a clean way of doing this, without having to generate two result sets, and merge them by code.
I have found a work-around. The issue seems to be that Active Record will not recognize joins that filter on an ID (LEFT OUTER JOIN xyz ON xyz.id = ID).
My work-around involves creating a stored procedure or function that takes the ID in as a parameter, does the join in the Database, and returns a nice flat recordset.
see: Heroku demo page (skip to bottom)
Note, I am not marking this as a solution, because this is a work-around, and nothing to do with active record.
Well, reading the github project, I see this:
What I really want to do is have a list of all of the lookup items,
and if there are matching Main Items, have them appended on to the
returned record, and if not, I want nulls. This is a technique that I
have used for over 10 years.
I'm thinking that problem is exactly that you want to do it that way, when it would be more natural to let rails eager loading handle it, and so you've gotten fixated on fetching everything in a single massive join.
What I would do is something like:
Lookup.where( .. insert any needed conditions here ...).includes(:through_tables)
Then ActiveQuery will then fetch all the Lookup in one query, and then use eager loading to fetch any associations named in the includes statement, one query per association.
Note I'm not saying that joins are bad, just saying that this is a more natural way to do it in rails. I like to use the Preloader http://apidock.com/rails/ActiveRecord/Associations/Preloader to separate out the decision about what to eager load from the decision about which data to fetch. I find that helpful in controllers - let the model decide what the conditions are, but let the controller decide which objects it'll need to eager load.
HTH

nHibernate collections and alias criteria

I have a simple test object model in which there are schools, and a school has a collection of students.
I would like to retrieve a school and all its students who are above a certain age.
I carry out the following query, which obtains a given school and the children which are above a certain age:
public School GetSchoolAndStudentsWithDOBAbove(int schoolid, DateTime dob)
{
var school = this.Session.CreateCriteria(typeof(School))
.CreateAlias("Students", "students")
.Add(Expression.And(Expression.Eq("SchoolId", schoolid), Expression.Gt("students.DOB", dob)))
.UniqueResult<School>();
return school;
}
This all works fine and I can see the query going to the database and returning the expected number of rows.
However, when I carry out either of the following, it gives me the total number of students in the given school (regardless of the preceding request) by running another query:
foreach (Student st in s.Students)
{
Console.WriteLine(st.FirstName);
}
Assert.AreEqual(s.Students.Count, 3);
Can anyone explain why?
You made your query on the School class and you restricted your results on it, not on the mapped related objects.
Now there are many ways to do this.
You can make a static filter as IanL said, however its not really flexible.
You can just iterate the collection like mxmissile but that is ugly and slow (especially considering lazy loading considerations)
I would provide 2 different solutions:
In the first you maintain the query you have and you fire a dynamic filter on the collection (maintaining a lazy-loaded collection) and doing a round-trip to the database:
var school = GetSchoolAndStudentsWithDOBAbove(5, dob);
IQuery qDob = nhSession.CreateFilter(school.Students, "where DOB > :dob").SetDateTime("dob", dob);
IList<Student> dobedSchoolStudents = qDob.List<Student>();
In the second solution just fetch both the school and the students in one shot:
object result = nhSession.CreateQuery(
"select ss, st from School ss, Student st
where ss.Id = st.School.Id and ss.Id = :schId and st.DOB > :dob")
.SetInt32("schId", 5).SetDateTime("dob", dob).List();
ss is a School object and st is a Student collection.
And this can definitely be done using the criteria query you use now (using Projections)
Unfortunately s.Students will not contain your "queried" results. You will have to create a separate query for Students to reach your goal.
foreach(var st in s.Students.Where(x => x.DOB > dob))
Console.WriteLine(st.FirstName);
Warning: That will still make second trip to the db depending on your mapping, and it will still retrieve all students.
I'm not sure but you could possibly use Projections to do all this in one query, but I am by no means an expert on that.
You do have the option of filtering data. If it there is a single instance of the query mxmissle option would be the better choice.
Nhibernate Filter Documentation
Filters do have there uses, but depending on the version you are using there can be issues where filtered collections are not cached correctly.