Download as pdf or txt
Download as pdf or txt
You are on page 1of 14

Topics

{ Basic SQL queries


SQL: Queries, Constraints, Triggers { Set operations
{ Aggregate operations
{ Null values
Linda Wu
{ General constraints
{ Triggers
(CMPT 354 • 2004-2)

Chapter 5 CMPT 354 • 2004-2 2

Basic SQL Queries Basic SQL Queries (Cont.)


{ Syntax of a basic SQL query z from-list: a list of tables
z SELECT clause: specify columns to be { Possibly with a range variable after each table

retained in the result (projection in RA) name


z FROM clause: specify a cross-product of z select-list: a list of column names of tables in
tables from-list
z WHERE clause: optional; specify selection z qualification: comparisons combined using
conditions on the tables in FROM clause AND, OR and NOT
(selections in RA) { Comparison: attr op const, or, attr1 op attr2

{ op is one of <, >, =, ≤, ≥, ≠


SELECT [DISTINCT] select-list
FROM from-list z DISTINCT: an optional keyword indicating that
WHERE qualification the result should not contain duplicates
{ Default: duplicates are not eliminated!

Chapter 5 CMPT 354 • 2004-2 3 Chapter 5 CMPT 354 • 2004-2 4


Basic SQL Queries (Cont.) Basic SQL Queries (Cont.)
{ Semantics of an SQL query is defined in { Examples SELECT S.sname
terms of the following conceptual evaluation FROM Sailors S, Reserves R
strategy WHERE S.sid=R.sid AND R.bid=103
z Compute the cross-product of from-list
z Discard resulting tuples if they fail qualification SELECT DISTINCT S.sname, S.age
z Delete attributes that are not in select-list FROM Sailors AS S
z If DISTINCT is specified, eliminate duplicate rows
• Without DISTINCT:
{ This strategy is typically the least efficient The result might be a multiset (an unordered collection
way to compute a query! of elements, in which each element could appear for
several times)
{ An optimizer will find more efficient
strategies to compute the same answers • AS: optional keyword to introduce a range variable

Chapter 5 CMPT 354 • 2004-2 5 Chapter 5 CMPT 354 • 2004-2 6

Basic SQL Queries (Cont.) Basic SQL Queries (Cont.)


{ Range { Find the sid’s of sailors who have
variables SELECT S.sname reserved at least one boat
z Needed only FROM Sailors S, Reserves R
if the same z Would adding DISTINCT to this query make
WHERE S.sid=R.sid AND R.bid=103
relation a difference?
appears more z What is the effect of replacing S.sid by
than once in
the FROM SELECT sname S.sname in the SELECT clause? Would
clause FROM Sailors, Reserves adding DISTINCT to this variant of the query
z It is a good WHERE Sailors.sid=Reserves.sid make a difference?
style to AND bid=103
always use SELECT S.sid
range FROM Sailors S, Reserves R
variables! WHERE S.sid = R.sid

Chapter 5 CMPT 354 • 2004-2 7 Chapter 5 CMPT 354 • 2004-2 8


Basic SQL Queries (Cont.) Basic SQL Queries (Cont.)
{ More general version of select-list { More general version of qualification
z A list of columns z Arithmetic expressions
z expression AS new_column_name z String comparison (=, <, >, etc.)
z new_column_name = expression { The ordering of strings is determined
alphabetically
{ expression:
any arithmetic or string expression
{ String pattern matching using LIKE and wild
-
over column names and constants
card symbols
{ new_column_name: a new name for a field in
z ‘_’ stands for any one character
result
z ‘%’ stands for 0 or more arbitrary
z sum, count characters

Chapter 5 CMPT 354 • 2004-2 9 Chapter 5 CMPT 354 • 2004-2 10

Basic SQL Queries (Cont.) Set Operations


{ Examples { UNION: union
{ INTERSECT: intersection
SELECT S.age, age1 = S.age - 5, 2 * S.age AS age2 { EXCEPT: set difference
FROM Sailors S z UNION/INTERSECT/EXCEPT ALL
WHERE S.sname LIKE ‘B_%B’ { IN: to check if an element is in a set
{ EXISTS: to check if a set is empty
SELECT S1.sname AS name1, S2.sname AS name2 z NOT IN / NOT EXISTS
FROM Sailors S1, Sailors S2 { UNIQUE / NOT UNIQUE: to check duplicates
WHERE 2 * S1.rating = S2.rating - 1 { ANY / ALL: to compare a value with the
elements in a set

Chapter 5 CMPT 354 • 2004-2 11 Chapter 5 CMPT 354 • 2004-2 12


Set Operations (Cont.) Set Operations (Cont.)
{ UNION { INTERSECT
SELECT S.sid
z To compute the SELECT S.sid z To compute the FROM Sailors S, Boats B1, Reserves R1,
union of two FROM Sailors S, Boats B, Reserves R intersection of Boats B2, Reserves R2
union-compatible WHERE S.sid=R.sid AND R.bid=B.bid two union- WHERE S.sid=R1.sid AND R1.bid=B1.bid
sets of tuples AND (B.color=‘red’ OR B.color=‘green’) compatible sets AND S.sid=R2.sid AND R2.bid=B2.bid
z Example: find z In the SQL/92 AND (B1.color=‘red’ AND B2.color=‘green’)
sid’s of sailors SELECT S.sid standard; but
who have FROM Sailors S, Boats B, Reserves R some systems SELECT S.sid
reserved a red or WHERE S.sid=R.sid AND R.bid=B.bid don’t support it FROM Sailors S, Boats B, Reserves R
a green boat AND B.color=‘red’ z Example: find WHERE S.sid=R.sid AND R.bid=B.bid
z What if we UNION sid’s of sailors AND B.color=‘red’
replace UNION SELECT S.sid who have INTERSECT
by EXCEPT? FROM Sailors S, Boats B, Reserves R reserved a red SELECT S.sid
WHERE S.sid=R.sid AND R.bid=B.bid and a green FROM Sailors S, Boats B, Reserves R
AND B.color=‘green’ boat WHERE S.sid=R.sid AND R.bid=B.bid
Chapter 5 CMPT 354 • 2004-2 13 Chapter 5
AND B.color=‘green’
CMPT 354 • 2004-2 14

Set Operations (Cont.) Set Operations (Cont.)


{ Duplicate elimination in UNION, { Nested queries
INTERSECT, and EXCEPT z A very powerful feature of SQL
z Default: duplicates are eliminated z A nested query is a query that has another
z To retain duplicates query (subquery) embedded within it
{ UNION/INTERSECT/EXCEPT ALL z Subquery typically appears in WHERE clause
z Subquery sometimes appears in FROM or
* Compare with basic query form: HAVING clauses
The default is NO duplicate elimination z Set comparison operators
unless DISTINCT is specified { IN, EXISTS, UNIQUE, ANY, ALL (the first 3 may
be used together with NOT)

Chapter 5 CMPT 354 • 2004-2 15 Chapter 5 CMPT 354 • 2004-2 16


Set Operations (Cont.) Set Operations (Cont.)
{ Nested query example { Multiply nested query
Find the names of sailors who have reserved boat
z Find the names of sailors who have not
#103
SELECT S.sname reserved a red boat
FROM Sailors S
WHERE S.sid IN ( SELECT R.sid SELECT S.sname
FROM Reserves R FROM Sailors S
WHERE R.bid=103 ) WHERE S.sid NOT IN (
SELECT R.sid
z To understand semantics of nested queries, think
FROM Reserves R
of a nested loop evaluation: for each Sailors
WHERE R.bid IN ( SELECT B.bid
tuple, check the qualification by (re)computing
the subquery FROM Boat B
z What if IN is replaced by NOT IN? WHERE B.color = ‘red’ ) )

Chapter 5 CMPT 354 • 2004-2 17 Chapter 5 CMPT 354 • 2004-2 18

Set Operations (Cont.) Set Operations (Cont.)


{ Nested queries with correlation { UNIQUE / NOT UNIQUE
z Inner subquery depends on the row z Checks for duplicate tuples
currently being examined in the outer z When apply UNIQUE to a subquery, the
query, and must be re-evaluated for each resulting condition returns true if:
row in the outer query { there are no duplicate tuples in the answer to the
subquery, or,
SELECT S.sname { the answer to the subquery is empty
FROM Sailors S Correlation
WHERE EXISTS ( SELECT * SELECT S.sname
FROM Reserves R FROM Sailors S
WHERE R.bid=103 AND S.sid=R.sid ) WHERE UN IQUE ( SELECT R.bid
FROM Reserves R
Find names of sailors who have reserved boat #103 WHERE R.bid=103 AND S.sid=R.sid )
Chapter 5 CMPT 354 • 2004-2 19 Finds sailors with at CMPT
Chapter 5 most 354one reservation for boat #10320
• 2004-2
Set Operations (Cont.) Set Operations (Cont.)
{ op ANY (subquery) { Find sailors whose rating is greater than some
z op is one of >, <, =, ≥, ≤, ≠ sailor called Horatio
z Subquery must return a row that makes the
comparison “op” true in order for “op ANY” to
SELECT *
return true FROM Sailors S
z If subquery returns empty result, “op ANY” WHERE S.rating > ANY ( SELECT S2.rating
returns false FROM Sailors S2
WHERE S2.sname=‘Horatio’ )
{ op ALL (subquery)
z Every row returned by subquery must makes the { Find sailors with the highest rating
comparison “op” true in order for “op ALL” to
return true SELECT *
z If subquery returns empty result, “op ALL” FROM Sailors S
returns true WHERE S.rating >= ALL ( SELECT S2.rating
FROM Sailors S2 )
Chapter 5 CMPT 354 • 2004-2 21 Chapter 5 CMPT 354 • 2004-2 22

Set Operations (Cont.) Set Operations (Cont.)


{ INTERSECT query can be rewritten using IN { Division in SQL: find the names of sailors
{ EXCEPT query can be rewritten using NOT IN who have reserved all boats
Solution 1: for each sailor S, check the set of boats
SELECT S.sid reserved by S includes every boat
FROM Sailors S, Boats B, Reserves R SELECT S.sname
WHERE S.sid=R.sid AND R.bid=B.bid AND B.color=‘red’ FROM Sailors S Correlation
AND S.sid IN or, NOT IN WHERE NOT EXISTS
( SELECT S2.sid ( ( SELECT B.bid
FROM Sailors S2, Boats B2, Reserves R2 FROM Boats B )
WHERE S2.sid=R2.sid AND R2.bid=B2.bid EXCEPT
AND B2.color=‘green’) ( SELECT R.bid
FROM Reserves R
Chapter 5 CMPT 354 • 2004-2 23 Chapter 5
WHERE R.sid
CMPT 354 • 2004-2
= S.sid ) ) 24
Set Operations (Cont.) Aggregate Operations
Solution 2: for each sailor S, check there is no boat { Aggregate operations
that has not been reserved by S
z A significant extension of relational
algebra
SELECT S.sname
z Five aggregate operators
FROM Sailors S
1. COUNT (*)
WHERE NOT EXISTS ( SELECT B.bid
COUNT ( [ DISTINCT ] A)
FROM Boats B
2. SUM ( [ DISTINCT ] A)
WHERE NOT EXISTS
( SELECT R.bid 3. AVG ( [ DISTINCT ] A)
FROM Reserves R 4. MAX (A)
WHERE R.bid = B.bid 5. MIN (A)
AND R.sid = S.sid ) ) A: a single column of a relation

Chapter 5 CMPT 354 • 2004-2 25 Chapter 5 CMPT 354 • 2004-2 26

Aggregate Operations (Cont.) Aggregate Operations (Cont.)


SELECT COUNT (*) SELECT COUNT (DISTINCT S.rating) { Example: find the name and age of the
FROM Sailors S FROM Sailors S oldest sailor(s)
(1) WHERE S.sname = ‘Bob’ (2)
SELECT S.sname, MAX (S.age)
SELECT AVG (S.age) SELECT AVG (DISTINCT S.age) FROM Sailors S illegal!
FROM Sailors S FROM Sailors S
WHERE S.rating=10 WHERE S.rating = 10
SELECT S.sname, S.age SELECT S.sname, S.age
(3) (4)
FROM Sailors S FROM Sailors S
WHERE S.age = = WHERE ( SELECT MAX (S2.age)
SELECT S.sname
( SELECT MAX (S2.age) FROM Sailors S2 )
FROM Sailors S
FROM Sailors S2 ) = S.age
WHERE S.rating = ( SELECT MAX (S2.rating)
(5) FROM Sailors S2 ) may not be supported!
Chapter 5 CMPT 354 • 2004-2 27 Chapter 5 CMPT 354 • 2004-2 28
Aggregate Operations (Cont.) Aggregate Operations (Cont.)
{ Motivation for grouping { GROUP BY and HAVING clauses
Consider: find the age of the youngest sailor for z To apply aggregate operations to each of a
each rating level number of groups of rows in a relation
z In general, we don’t know how many rating z Each row in the result of the query
levels exist, and what the rating values for corresponds to one group
these levels are!
* Group: a collection of rows sharing some
z Suppose we know that rating values go from 1 common features (grouping-list)
to 10; we can write 10 queries that look like
this: SELECT [DISTINCT] select-list
FROM from-list
For i = 1, 2, ... , 10: SELECT MIN (S.age) WHERE qualification
FROM Sailors S GROUP BY grouping-list
WHERE S.rating = i HAVING group-qualification
Chapter 5 CMPT 354 • 2004-2 29 Chapter 5 CMPT 354 • 2004-2 30

Aggregate Operations (Cont.) Aggregate Operations (Cont.)


z All rows in a group have the same value for { Conceptual evaluation strategy for
all attributes in grouping-list GROUPING BY
z An answer row is to be generated for a given 1) Construct the cross-product of tables in
group if it satisfies group-qualification from-list
z select-list contains: 2) Discard the tuples that fail qualification in
1) A list of column names: must be a subset of
WHERE clause
grouping-list 3) Eliminate unnecessary fields (not in
2) A list of terms with aggregate operations, e.g., SELECT, GROUP BY or HAVING clause)
MIN (S.age) | SQL does not eliminate duplicates unless
DISTINCT is specified
4) Partition the remaining tuples into groups
by the values of attributes in grouping-list

Chapter 5 CMPT 354 • 2004-2 31 Chapter 5 CMPT 354 • 2004-2 32


Aggregate Operations (Cont.) Aggregate Operations (Cont.)
5) Apply the group-qualification to eliminate { Find the age of the youngest sailor with age
some groups. Expressions in group- ≥ 18 for each rating level with at least 2
qualification must have a single value per such sailors sid sname rating age
group! 22 Dustin 7 45.0
| In effect, an attribute in group-qualification
that is not an argument of an aggregate
29 Brutus 1 33.0
operation also appears in grouping-list (SQL SELECT S.rating, MIN (S.age) 31 Lubber 8 55.5
does not exploit primary key semantics AS minage 32 Andy 8 25.5
here!) FROM Sailors S 58 Rusty 10 35.0
6) Generate one answer tuple per qualifying WHERE S.age >= 18 64 Horatio 7 35.0
group GROUP BY S.rating 71 Zorba 10 16.0
7) Eliminate duplicates if DISTINCT is HAVING COUNT (*) > 1 74 Horatio 9 35.0
contained in SELECT clause 85 Art 3 25.5
95 Bob 3 63.5
Chapter 5 CMPT 354 • 2004-2 33 Chapter 5 96 Frodo
CMPT 354 • 2004-2 3 25.5
34

Aggregate Operations (Cont.) Aggregate Operations (Cont.)


Step 1,2,3 Step 4,5 { Find the age of the youngest sailor with age
rating age rating age ≥ 18 for each rating level with at least 2
7 45.0 1 33.0 such sailors and with every sailor under 60
1 33.0 3 25.5 Step 6
8 55.5 3 25.5 SELECT S.rating, MIN (S.age) AS minage
rating minage
8 25.5 3 63.5 FROM Sailors S
3 25.5
10 35.0 7 45.0 WHERE S.age >= 18
7 35.0 GROUP BY S.rating
7 35.0 7 35.0 8 25.5
9 35.0 8 55.5 HAVING COUNT (*) > 1 AND EVERY (S.age ≤ 60)
3 25.5 8 25.5
EVERY and ANY are supported by SQL
- 99
3 63.5 9 35.0
3 25.5 10 35.0 • What if EVERY is changed to ANY?

Chapter 5
Conceptual evaluation strategy
CMPT 354 • 2004-2 35 Chapter 5 CMPT 354 • 2004-2 36
Aggregate Operations (Cont.) Aggregate Operations (Cont.)
Step 1,2,3 Step 4,5 { Find the age of the youngest sailor with age
rating age rating age between 18 and 60 for each rating level
7 45.0 1 33.0 with at least 2 such sailors
1 33.0 3 25.5
8 55.5 3 25.5 Step 6 SELECT S.rating, MIN (S.age) AS minage
8 25.5 3 63.5 rating minage FROM Sailors S
7 35.0 WHERE S.age >= 18 AND S.age ≤ 60
10 35.0 7 45.0
GROUP BY S.rating
7 35.0 7 35.0 8 25.5
HAVING COUNT (*) > 1
9 35.0 8 55.5
3 25.5 8 25.5
rating minage
3 63.5 9 35.0 3 25.5 Answer
3 25.5 10 35.0 7 35.0 relation
Chapter 5 CMPT 354 • 2004-2 37 Chapter 5 CMPT8354 • 2004-2
25.5 38

Aggregate Operations (Cont.) Aggregate Operations (Cont.)


{ For each red boat, find the number of { Find the age of the youngest sailor with age >
reservations for this boat 18, for each rating level with at least 2 sailors
z Grouping over a join of two relations (of any age)
z HAVING clause can also contain a subquery
SELECT B.bid, COUNT (*) AS resvcount
FROM Boats B, Reserves R z What if HAVING clause is replaced by:
WHERE R.bid=B.bid AND B.color=‘red’ HAVING COUNT(*) >1 ?
GROUP BY B.bid SELECT S.rating, MIN (S.age) AS minage
z To move B.color=‘red’ from the WHERE clause FROM Sailors S
to HAVING clause: illegal! WHERE S.age > 18
{ Only columns in the GROUP BY clause can GROUP BY S.rating
appear in the HAVING clause, unless they are HAVING 1 < ( SELECT COUNT (*)
arguments to an aggregate operator in the FROM Sailors S2
HAVING clause
WHERE S.rating = S2.rating )
Chapter 5 CMPT 354 • 2004-2 39 Chapter 5 CMPT 354 • 2004-2 40
Aggregate Operations (Cont.) Aggregate Operations (Cont.)
Step 1,2,3 Step 4,5 { Find those ratings for which the average
rating age rating age # of age is the minimum over all ratings
7 45.0 sailors z Aggregate operations, for example AVG(S2.age),
1 33.0 (1) Step 6 cannot be nested!
1 33.0
8 55.5 3 25.5 rating minage
8 25.5 3 25.5 (3) 3 25.5
SELECT S.rating
10 35.0 3 63.5 7 35.0
FROM Sailors S
7 35.0 7 45.0 8 25.5 WHERE AVG(S.age) = ( SELECT MIN (AVG (S2.age) )
(2)
9 35.0 7 35.0 10 35.0 FROM Sailors S2
3 25.5 8 55.5 Wrong solution ! GROUP BY S2.rating )
(2)
3 63.5 8 25.5
3 25.5 9 35.0 (1)
10 35.0 (2)
Chapter 5 CMPT 354 • 2004-2 41 Chapter 5 CMPT 354 • 2004-2 42

Aggregate Operations (Cont.) Null Values


Correct solution: { Null
z A special value that denote unknown (e.g., a rating
SELECTTemp.rating, Temp.avgage has not been assigned) or inapplicable (e.g., no
FROM ( SELECT S.rating, AVG(S.age) AS avgage spouse’s name)
FROM Sailors S { The presence of null complicates many issues
GROUP BY S.rating ) AS Temp z Special operators are needed to check if value is/is
WHERE Temp.avgage = ( SELECT MIN(Temp.avgage) not null
FROM Temp ) z Is (rating>8) true or false when rating is null? What
about AND, OR and NOT connectives?
{ SQL allows nested subquery in FROM clause, but
z We need a 3-valued logic (true, false and unknown)
not all DBMSs support it
z Meaning of SQL constructs must be defined
{ HAVING clause is not necessary carefully (e.g., WHERE clause eliminates rows that
{ When a subquery appears in FROM, give it a don’t evaluate to true)
name using keyword AS z New operators (in particular, outer joins) are
possible/needed
Chapter 5 CMPT 354 • 2004-2 43 Chapter 5 CMPT 354 • 2004-2 44
Null Values (Cont.) General Constraints
{ Outer joins { Useful when more general ICs other than keys
are involved
z When S outer joins R, S rows without a
matching R rows according to the join condition { Can use queries to express constraints
appear exactly once in the result, with the { Constraints can be named
result columns inherited from R assigned null { Constraints may be specified over a single table,
values or, multiple tables
sid sname rate age sid bid day
22 Dustin 7 45.0 22 101 10/10/96 CREATE TABLE Sailors
31 Lubber 8 55.5 58 103 11/12/96 ( sid INTEGER,
58 Rusty 10 35.0 Reserves sname CHAR(10),
Sailors rating INTEGER,
sid bid age REAL,
SELECT S.sid, R.bid 22 101 PRIMARY KEY (sid),
FROM Sailors S NATURAL LEFT OUTER JOIN 31 null CHECK ( rating >= 1 AND rating <= 10 ) )
Chapter 5
Reserves R CMPT 354 • 2004-2 58 103 45 Chapter 5 CMPT 354 • 2004-2 46

General Constraints (Cont.) General Constraints (Cont.)


{ Table constraints: over a single table (# of boats) + (# of sailors) < 100
CHECK conditional-expression
z conditional-expression may refer to other tables CREATE TABLE Sailors (?)
z conditional-expression is required to hold only if ( sid INTEGER,
the associated table is not empty ……
CREATE TABLE Reserves CHECK ( (SELECT COUNT (S.sid) FROM Sailors S)
( sid INTEGER, + (SELECT COUNT (B.bid) FROM Boats B)
bid INTEGER, < 100 ) )
day DATE,
…… { If Sailors is empty, this constraint always holds
CONSTRAINT noInterlakeRes (the number of Boats tuples can be anything!)
CHECK ( `Interlake’ <> { ASSERTION is the right solution; not associated
( SELECT B.bname with either table
FROM Boats B
Chapter 5
WHERE B.bid = bid ) ) )
CMPT 354 • 2004-2 47 Chapter 5 CMPT 354 • 2004-2 48
General Constraints (Cont.) Triggers
{ Assertion { Trigger
z Constraint over multiple tables, but NOT z A procedure that is automatically invoked
associated with any table when specified changes occur to the DBMS
z Can be though of as a ‘daemon’ in DBMS
CREATE ASSERTION smallClub
CHECK ( (SELECT COUNT (S.sid) FROM Sailors S ) { Trigger description has 3 parts
+ (SELECT COUNT (B.bid) FROM Boats B) z Event (activates the trigger)
< 100 ) z Condition (tests whether the triggers should
run)
z Action (what happens if the trigger runs)

{ A database with a set of associated triggers is


called an active database

Chapter 5 CMPT 354 • 2004-2 49 Chapter 5 CMPT 354 • 2004-2 50

Triggers (Cont.) Triggers (Cont.)


{ Trigger event { Example (SQL
- 99 syntax)
z An insert, delete or update statement Trigger name
{ Trigger condition Give a table
name to the
z A true/false statement set of newly
CREATE TRIGGER youngSailorUpdate
z A query (true if its answer is nonempty) inserted AFTER INSERT ON SAILORS /* Event */
tuples REFERENCING NEW TABLE NewSailors
{ Trigger action
Statement FOR EACH STATEMENT
z May examine the answer to the query in trigger
level trigger
condition, execute new queries, make change to INSERT /* Action */
the database, …… INTO YoungSailors(sid, name, age, rating)
z May execute before / after / instead of activating WHEN SELECT sid, name, age, rating
statement Clause FROM NewSailors N
z May execute once per modified record, or, per
activating statement /* Condition */ WHERE N.age <= 18

Chapter 5 CMPT 354 • 2004-2 51 Chapter 5 CMPT 354 • 2004-2 52


Triggers (Cont.) Summary
{ If a statement activates multiple triggers, { SQL was an important factor in the early
the DBMS processes all of them, in some acceptance of the relational model
arbitrary order { SQL is relationally complete; in fact, it is more
expressive than relational algebra
{ Executing the action part of a trigger could
{ Even queries that can be expressed in RA can
in turn activates another trigger: recursive often be expressed more naturally in SQL
triggers { There are many alternative ways to write a
{ Usage query; optimizer should look for the most
z Maintain database consistency in more flexible efficient evaluation plan
way than constraints { NULL value brings many complications
z Gather statistics on table access and modification { SQL allows rich integrity constraints
z Alert users to unusual events { Triggers respond to changes in the database
z …..
Chapter 5 CMPT 354 • 2004-2 53 Chapter 5 CMPT 354 • 2004-2 54

You might also like