SQL - Part 2: Advanced Features =============================== - In this lecture, we will learn more advanced features of SQL. - Examples database to be used in this lecture is given in SQL here: See :download:`example database to be used `. Overview -------- - Remember that while SQL is a standard, there are still differences in implementations of it. - Writing queries that do not rely on specific features results in portable applications. - However, you cannot deny that some constructs may simplify your queries and performance. So, it is important to decide when to use a specific method to write a query. - Remember: a query is not an algorithm. It is for the most part a logical statement of what you are interested in. - Often, there are multiple algorithms to implement it. - Most DBMSs feature state of the art query optimizers (QOPT) that choose the lowest cost algorithm for a given query and database. - QOPT engines are very sophisticated, often operate better than even expert human judgment. - So, instead of trying to optimize your queries, you can try to make your queries easy to optimize: simple queries are better. - Once you become sophisticated in a specific DBMS, you may learn specific weaknesses and you can develop strategies to adopt for that. We will discuss some. - Finally, you should still follow some very simple guidelines: - Do not join with a relation if it is not needed for your query. - Do not sort (order by) or remove duplicates (distinct) unless it is necessary. Outer Join --------------- - A INNER JOIN B: inner join selects tuples that satisfy a join condition, eliminates all tuples that do not satisfy the join condition. A is called the left operand and B is the right operand of the join operation. - A LEFT OUTER JOIN B returns all tuples in the inner join as well as the tuples in A that do not join with any tuples in in B. - A RIGHT OUTER JOIN B returns all tuples in the inner join as well as the tuples in B that do not join with any tuples in in A. - A FULL OUTER JOIN B returns all tuples in the inner join as well as the tuples from A and B that do not participate in the inner join. - You can also use terms: JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN Inner vs. outer join. ------------------------- - Given R(A,B) and S(B,C) with the following contents: ==== ==== A B ==== ==== a1 b1 a2 b2 ==== ==== ==== ==== B C ==== ==== b1 c1 b3 c3 ==== ==== - We get the following results: :: SELECT R.A, R.B, S.B, S.C FROM R JOIN S ON R.B=S.B; ==== ==== ==== ==== A B B C ==== ==== ==== ==== a1 b1 b1 c1 ==== ==== ==== ==== :: SELECT R.A, R.B, S.B, S.C FROM R LEFT JOIN S ON R.B=S.B; ==== ==== ==== ==== A B B C ==== ==== ==== ==== a1 b1 b1 c1 a2 b2 null null ==== ==== ==== ==== :: SELECT R.A, R.B, S.B, S.C FROM R RIGHT JOIN S ON R.B=S.B; ==== ==== ==== ==== A B B C ==== ==== ==== ==== a1 b1 b1 c1 null null b3 c3 ==== ==== ==== ==== :: SELECT R.A, R.B, S.B, S.C FROM R FULL JOIN S ON R.B=S.B; ==== ==== ==== ==== A B B C ==== ==== ==== ==== a1 b1 b1 c1 a2 b2 null null null null b3 c3 ==== ==== ==== ==== - We can use the fact that tuples that do not match have null values for the join. Outer join examples: ~~~~~~~~~~~~~~~~~~~~~~ - For each baker, find the total number of times they were a favorite. :: SELECT b.baker , count(f.baker) as numfavorites FROM bakers b left join favorites f on b.baker = f.baker GROUP BY b.baker ; For bakers with no favorite tuples, f.baker will be null. So, when we count f.baker values, the count will be zero for these bakers. If we used inner join, we would have elimited all bakers with zero favorite tuples as they would not join with favorites. - Find bakers who were never a favorite. :: SELECT b.baker FROM bakers b left join favorites f on b.baker = f.baker WHERE f.baker IS NULL; This works because if a baker has no matching tuple in favorites, then the f.baker attribute would be null. - For each baker, find how many times they won the technical challenge. Note that we would like to use left join as in the previous case, but not with the whole technicals table but only the tuples where rank is 1. :: SELECT b.baker , count(t.rank) as numwins FROM bakers b left join technicals t on b.baker = t.baker and t.rank = 1 GROUP BY b.baker; Anonymous relations --------------------- - A query can be treated like a relation in the from clause It is treated like a virtual relation: :: SELECT t.baker , count(t.rank) as numtophalf FROM ( SELECT episodeid , count(*) as numbakers FROM technicals GROUP BY episodeid ) as epnum , technicals t WHERE t.episodeid = epnum.episodeid and t.rank < epnum.numbakers/2 GROUP BY t.baker ; The inner query allows us to find how many bakers competed in each episode. We can then use this information in the main query as if it was a real relation, and find how many times a baker performed in the top half of the technical challenges. This query would not be possible to write without an anonymous relation as we cannot count for different types of things (bakers for episodes and episodes for bakers) with a single group by. - Find the maximum number of people eliminated in an episode: :: SELECT max(numeliminated) FROM (SELECT -- number of people eliminated in each episode count(*) as numeliminated FROM results WHERE result='eliminated' GROUP BY episodeid ) as elim; - Be careful: Do not use any anonymous relations to make it simpler to write/read the query. :: SELECT S.d FROM (SELECT a.* FROM R WHERE b>5) as newR , S WHERE S.c = newR.c; Anonymous relation is not really necessary here. The same query can be written with a simple join: :: SELECT S.d FROM R,S WHERE R.b>5 and S.c=R.c; When using an anonymous view, query optimizer may miss certain optimizations, especially in older DBMS. Scalar Queries ----------------- - Any query that returns a single number with an aggregate function is called a scalar query. You can use a scalar query as if it was a number. We first find the biggest drop in ratings between two episodes: :: SELECT max(e2.viewers7day-e1.viewers7day) FROM episodes e1 , episodes e2 WHERE e2.id = e1.id+1; max ------- 0.84 (1 row) Now we find who was eliminated in this episode (or episodes if there is more than one with the same drop): :: SELECT r.baker FROM episodes e1 , episodes e2 , results r WHERE e2.id = e1.id+1 and e2.viewers7day-e1.viewers7day = 0.84 and r.episodeid = e1.id and r.result = 'eliminated'; baker -------- Briony (1 row) We can write the same query by simply substituting the first query for the constant 0.84: :: SELECT r.baker FROM episodes e1 , episodes e2 , results r WHERE e2.id = e1.id+1 and e2.viewers7day-e1.viewers7day = (SELECT max(e2.viewers7day-e1.viewers7day) FROM episodes e1, episodes e2 WHERE e2.id = e1.id+1) and r.episodeid = e1.id and r.result = 'eliminated'; Comparisons involving sets/bags -------------------------------- - Many expressions in the WHERE clause (or HAVING) can compare a value against a SET :: WHERE hometown IN ('London','Bristol') WHERE baker NOT IN ('Imelda','Luke') - Substitute a query for the set: Find bakers who were never eliminated. :: SELECT baker , fullname FROM bakers WHERE baker NOT IN (SELECT baker FROM results WHERE result = 'eliminated'); - You can write equivalent queries using EXCEPT and LEFT JOIN. Set Comparison Operators -------------------------- - There are many set comparison operators that can be used in queries. The inner query must return a single column for this to work. Some useful operations: :: value IN (QUERY) value NOT IN (QUERY) value > ANY (QUERY) value >= ALL (QUERY) value > ALL (QUERY) value = ANY (QUERY) --> same as IN value <> ALL (QUERY) --> same as NOT IN - You can also write expressions that check whether a query returns any tuples at all: :: EXISTS (QUERY) => True if Query returns at least one tuple NOT EXISTS (QUERY) => True if Query returns no tuples - Examples: :: 5 IN (1,2,3,4) FALSE 5 NOT IN (1,2,3,4) TRUE 2 IN (1,2,3,4) TRUE EXISTS (1,2,3,4) TRUE NOT EXISTS (1,2,3,4) FALSE NOT EXISTS () TRUE 5 ALL (1,2,3,4) TRUE - Example: :: SELECT * FROM bakers WHERE EXISTS (SELECT 1 FROM signatures WHERE lower(make) LIKE '%cardamom%'); This is a kind of stupid query: if there is any make with cardamom, we will return all bakers. Otherwise, we return no students. - Since it does not matter what we return in EXISTS/NOT EXISTS conditions (we only care whether a tuple is returned or not), we can return something simple like an integer, instead of a relation column. Correlated Subqueries ----------------------- - More interesting queries involve correlated subqueries. - Find bakers who never won a technical challenge: :: SELECT b.baker , b.fullname FROM bakers b WHERE NOT EXISTS ( SELECT 1 FROM technicals t WHERE t.baker = b.baker and t.rank=1 ) ; For each baker tuple b, execute the inner subquery to find all technical tuples for this baker with rank 1. If there is no such tuple, then return tuple b. - Outer query: bakers b - Inner query: technicals t - Scope of variables: - The inner query can reference any tuple value in the outer query (from the FROM clause), treating these values as constants for the inner query. - The outer query cannot access the variables of the inner query. - If the iner query rewrites an alias from the outer query, then the closest definition is used. Common mistake when using a nested subquery ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Rewrite your own alias in the inner subquery :: SELECT b.baker , b.fullname FROM bakers b WHERE NOT EXISTS ( SELECT 1 FROM technicals t, bakers b WHERE t.baker = b.baker and t.rank=1 ) ; This query will return a very different result than the correct query as inner baker b overwrites the outer baker b. For each baker tuple, execute the inner query independently. If it returns no tuples, return the baker tuple. Hence: this will return no baker tuples as there is at least one tuple in the inner query. Examples ~~~~~~~~~~ - Find bakers who won at least 3 technical challenges: :: SELECT b.baker , b.fullname FROM bakers b WHERE 1 >= (SELECT count(*) FROM technicals t WHERE t.baker=b.baker and t.rank=1); Note that while this is a correct query, it is likely more efficient to use a group by statement for the same purpose: :: SELECT b.baker , b.fullname FROM bakers b LEFT JOIN technicals t ON b.baker = t.baker and t.rank=1 GROUP BY b.baker , b.fullname HAVING count(t.baker) <= 1; Be careful: the query would not be correct without a left join. Why? - Scalar queries can also be correlated (though use this as a last resort as well): :: SELECT e.id , count(*) as numeliminated , (SELECT count(*) FROM favorites f WHERE f.episodeid = e.id) as numfavorites FROM episodes e , results r WHERE e.id = r.episodeid and r.result = 'eliminated' GROUP BY e.id; Remember: this is not likely an efficient way to write this query. Can you write the same query without a correlated subquery in SELECT? Examples ----------------- - We will finish section with a few complex queries. Suppose we wanted to find if a baker did not compete in a specific episode. We would need find when they were eliminated and then see if there was an episode before their elimination in which there was no tuple for them competing in one of the challenges. :: SELECT DISTINCT b.baker , b.fullname , e.id FROM results r , bakers b , episodes e WHERE r.result = 'eliminated' and r.baker = b.baker and e.id < r.episodeid -- an episode before they were eliminated AND NOT EXISTS (SELECT 1 FROM signatures s WHERE s.episodeid = e.id and s.baker = b.baker); - Since we can find the absence of a tuple with left join too, how about we look for an alternate way to write this query with left join. But we need to be careful to set the relation carefully that will left join. Here is one: :: SELECT DISTINCT b.baker , b.fullname , e.id FROM bakers b join results r on r.baker = b.baker and r.result='eliminated' join episodes e on e.id < r.episodeid left join signatures s on s.episodeid = e.id and s.baker = b.baker WHERE s.baker is null; FOR ALL Queries --------------- - What is we wanted to find bakers who competed in all the episodes of the show. - This is a complex query: we want to check that the set of all episodes that the baker competed in is equal to the set of all episodes that exist. In relational algebra, this query would need two set subtractions. We can represent this query logically as follows: :: Find bakers who competed in all episodes: Find bakers b such that there does not exist an episode e such that b did not take compete in episode e (or there does not exists a tuple in signatures (or showstoppers or technicals) for b and e) - SQL query will also require two subqueries: :: SELECT b.baker , b.fullname FROM bakers b WHERE NOT EXISTS (SELECT 1 FROM episodes e WHERE NOT EXISTS (SELECT 1 FROM signatures s WHERE s.episodeid = e.id AND s.baker = b.baker)); - Do we really need this level of complexity? Can we do this using a count? :: Return each baker if the number of different episodes they competed in is equal to the number of different episodes in the database. Let's write this expression: :: SELECT b.baker , b.fullname FROM bakers b , signatures s WHERE b.baker = s.baker GROUP BY b.baker , b.fullname HAVING count(*) = (SELECT count(*) FROM episodes) ; - Not only this query is simpler to write, it is likely much more efficient given it has no correlated subqueries. WITH Statement (newer form of anonymous relations) ---------------------------------------------------- - Postgresql implements the WITH statement, part of SQL standard. In its simplest form, WITH acts like anonymous relations. But in reality it can do a lot more. - The following is the identical query from above written using WITH clause: :: WITH maxdrop AS ( SELECT max(e2.viewers7day-e1.viewers7day) as drop FROM episodes e1, episodes e2 WHERE e2.id = e1.id+1) SELECT r.baker FROM episodes e1 , episodes e2 , results r , maxdrop m WHERE e2.id = e1.id+1 and e2.viewers7day-e1.viewers7day = m.drop and r.episodeid = e1.id and r.result = 'eliminated'; - However, anonymous relations can only be used in FROM while relations generated using WITH can be used in any SQL statement, including in subsequent WITH statements. :: WITH dropval AS ( SELECT e1.id , max(e2.viewers7day-e1.viewers7day) as drop FROM episodes e1 , episodes e2 WHERE e2.id = e1.id+1 GROUP BY e1.id ), maxdropval AS ( SELECT max(drop) as maxdrop FROM dropval) SELECT r.baker FROM results r , dropval d , maxdropval m WHERE r.episodeid = d.id and r.result = 'eliminated' and d.drop = m.maxdrop ; - In this case, `maxdropval` is referring to a query above it in the WITH statemnt. You cannot do this in anonymous queries. Even though `maxdropval` builds on `dropval`, you can use both in the FROM statement below. - While WITH statement is quite powerful as a construct, be very careful to use it only if is helps you write a query that is cumbersome or very ineffecient to write using regular SQL. You can do this by checking the cost of different queries. Find the cost of queries by using cost estimators or by load testing and check if it results in cost savings. Do not allow the WITH statements to make SQL more procedural, this may result in the optimizer missing some crucial query optimizations. - We will reexamine WITH when we look at advanced SQL features. Summary ------------ - Most queries that use IN or EXISTS can be rewritten using simple joins. Joins are much easier to optimize. - Set subtraction usually can be expressed using NOT IN or NOT EXISTS. - Using anonymous relations in the from clause may cause the optimizer to miss some optimizations. Simpler the query, the better it is. - There is a subtle difference on the syntax of the two statements: :: Attribute NOT IN (select statement) NOT EXISTS (select statement) - For all queries usually require two NOT EXISTS. - SQL aggregates and outer joins are powerful constructs for formulating complex queries, even those involving some sort of negation.