327 lines
8.6 KiB
PL/PgSQL
327 lines
8.6 KiB
PL/PgSQL
---------------------------------------------------------------------------
|
|
-- Practical SQL: A Beginner's Guide to Storytelling with Data, 2nd Edition
|
|
-- by Anthony DeBarros
|
|
|
|
-- Chapter 17 Code Examples
|
|
----------------------------------------------------------------------------
|
|
|
|
-- VIEWS
|
|
|
|
-- Listing 17-1: Creating a view that displays Nevada 2019 counties
|
|
|
|
CREATE OR REPLACE VIEW nevada_counties_pop_2019 AS
|
|
SELECT county_name,
|
|
state_fips,
|
|
county_fips,
|
|
pop_est_2019
|
|
FROM us_counties_pop_est_2019
|
|
WHERE state_name = 'Nevada'
|
|
ORDER BY county_fips;
|
|
|
|
-- Listing 17-2: Querying the nevada_counties_pop_2019 view
|
|
|
|
SELECT *
|
|
FROM nevada_counties_pop_2019
|
|
LIMIT 5;
|
|
|
|
-- Listing 17-3: Creating a view showing population change for US counties
|
|
|
|
CREATE OR REPLACE VIEW county_pop_change_2019_2010 AS
|
|
SELECT c2019.county_name,
|
|
c2019.state_name,
|
|
c2019.state_fips,
|
|
c2019.county_fips,
|
|
c2019.pop_est_2019 AS pop_2019,
|
|
c2010.estimates_base_2010 AS pop_2010,
|
|
c2019.pop_est_2019 - c2010.estimates_base_2010 AS raw_change,
|
|
round( (c2019.pop_est_2019::numeric - c2010.estimates_base_2010)
|
|
/ c2010.estimates_base_2010 * 100, 1 ) AS pct_change_2019_2010
|
|
FROM us_counties_pop_est_2019 AS c2019
|
|
JOIN us_counties_pop_est_2010 AS c2010
|
|
ON c2019.state_fips = c2010.state_fips
|
|
AND c2019.county_fips = c2010.county_fips
|
|
ORDER BY c2019.state_fips, c2019.county_fips;
|
|
|
|
-- Listing 17-4: Selecting columns from the county_pop_change_2010_2000 view
|
|
|
|
SELECT county_name,
|
|
state_name,
|
|
pop_2019,
|
|
pct_change_2019_2010
|
|
FROM county_pop_change_2019_2010
|
|
WHERE state_name = 'Nevada'
|
|
LIMIT 5;
|
|
|
|
-- Listing 17-5: Creating a view on the employees table
|
|
|
|
CREATE OR REPLACE VIEW employees_tax_dept AS
|
|
SELECT emp_id,
|
|
first_name,
|
|
last_name,
|
|
dept_id
|
|
FROM employees
|
|
WHERE dept_id = 1
|
|
ORDER BY emp_id
|
|
WITH LOCAL CHECK OPTION;
|
|
|
|
SELECT * FROM employees_tax_dept;
|
|
-- SELECT * FROM employees;
|
|
-- Listing 17-6: Successful and rejected inserts via the employees_tax_dept view
|
|
|
|
INSERT INTO employees_tax_dept (emp_id, first_name, last_name, dept_id)
|
|
VALUES (5, 'Suzanne', 'Legere', 1);
|
|
|
|
INSERT INTO employees_tax_dept (emp_id, first_name, last_name, dept_id)
|
|
VALUES (6, 'Jamil', 'White', 2);
|
|
|
|
-- optional:
|
|
SELECT * FROM employees_tax_dept;
|
|
|
|
SELECT * FROM employees;
|
|
|
|
-- Listing 17-7: Updating a row via the employees_tax_dept view
|
|
|
|
UPDATE employees_tax_dept
|
|
SET last_name = 'Le Gere'
|
|
WHERE emp_id = 5;
|
|
|
|
SELECT * FROM employees_tax_dept;
|
|
|
|
-- Bonus: This will fail because the salary column is not in the view
|
|
UPDATE employees_tax_dept
|
|
SET salary = 100000
|
|
WHERE emp_id = 5;
|
|
|
|
-- Listing 17-8: Deleting a row via the employees_tax_dept view
|
|
|
|
DELETE FROM employees_tax_dept
|
|
WHERE emp_id = 5;
|
|
|
|
|
|
-- FUNCTIONS
|
|
-- https://www.postgresql.org/docs/current/static/plpgsql.html
|
|
|
|
-- Listing 17-9: Creating a percent_change function
|
|
-- To delete this function: DROP FUNCTION percent_change(numeric,numeric,integer);
|
|
|
|
CREATE OR REPLACE FUNCTION
|
|
percent_change(new_value numeric,
|
|
old_value numeric,
|
|
decimal_places integer DEFAULT 1)
|
|
RETURNS numeric AS
|
|
'SELECT round(
|
|
((new_value - old_value) / old_value) * 100, decimal_places
|
|
);'
|
|
LANGUAGE SQL
|
|
IMMUTABLE
|
|
RETURNS NULL ON NULL INPUT;
|
|
|
|
-- Listing 17-10: Testing the percent_change() function
|
|
|
|
SELECT percent_change(110, 108, 2);
|
|
|
|
-- Listing 17-11: Testing percent_change() on Census data
|
|
|
|
SELECT c2019.county_name,
|
|
c2019.state_name,
|
|
c2019.pop_est_2019 AS pop_2019,
|
|
percent_change(c2019.pop_est_2019, c2010.estimates_base_2010) AS pct_chg_func,
|
|
round( (c2019.pop_est_2019::numeric - c2010.estimates_base_2010)
|
|
/ c2010.estimates_base_2010 * 100, 1 ) AS pct_chg_formula
|
|
FROM us_counties_pop_est_2019 AS c2019
|
|
JOIN us_counties_pop_est_2010 AS c2010
|
|
ON c2019.state_fips = c2010.state_fips
|
|
AND c2019.county_fips = c2010.county_fips
|
|
ORDER BY pct_chg_func DESC
|
|
LIMIT 5;
|
|
|
|
-- Listing 17-12: Adding a column to the teachers table and seeing the data
|
|
|
|
ALTER TABLE teachers ADD COLUMN personal_days integer;
|
|
|
|
SELECT first_name,
|
|
last_name,
|
|
hire_date,
|
|
personal_days
|
|
FROM teachers;
|
|
SELECT * FROM teachers;
|
|
-- Listing 17-13: Creating an update_personal_days() function
|
|
|
|
CREATE OR REPLACE FUNCTION update_personal_days()
|
|
RETURNS void AS $$
|
|
BEGIN
|
|
UPDATE teachers
|
|
SET personal_days =
|
|
CASE WHEN (now() - hire_date) BETWEEN '5 years'::interval
|
|
AND '10 years'::interval THEN 4
|
|
WHEN (now() - hire_date) > '10 years'::interval THEN 5
|
|
ELSE 3
|
|
END;
|
|
RAISE NOTICE 'personal_days updated!';
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- To run the function:
|
|
SELECT update_personal_days();
|
|
|
|
-- Listing 17-14: Enabling the PL/Python procedural language
|
|
|
|
CREATE EXTENSION plpythonu; -- doesn't work on macOS with PostgresApp
|
|
CREATE EXTENSION plpython3u; -- doesn't work on macOS with PostgresApp
|
|
|
|
-- Listing 17-15: Using PL/Python to create the trim_county() function
|
|
|
|
CREATE OR REPLACE FUNCTION trim_county(input_string text)
|
|
RETURNS text AS $$
|
|
import re
|
|
cleaned = re.sub(r' County', '', input_string)
|
|
return cleaned
|
|
$$ LANGUAGE plpythonu;
|
|
|
|
-- Listing 17-16: Testing the trim_county() function
|
|
|
|
SELECT geo_name,
|
|
trim_county(geo_name)
|
|
FROM us_counties_2010
|
|
ORDER BY state_fips, county_fips
|
|
LIMIT 5;
|
|
|
|
|
|
-- TRIGGERS
|
|
|
|
-- Listing 17-17: Creating the grades and grades_history tables
|
|
|
|
CREATE TABLE grades (
|
|
student_id bigint,
|
|
course_id bigint,
|
|
course text NOT NULL,
|
|
grade text NOT NULL,
|
|
PRIMARY KEY (student_id, course_id)
|
|
);
|
|
|
|
INSERT INTO grades
|
|
VALUES
|
|
(1, 1, 'Biology 2', 'F'),
|
|
(1, 2, 'English 11B', 'D'),
|
|
(1, 3, 'World History 11B', 'C'),
|
|
(1, 4, 'Trig 2', 'B');
|
|
|
|
CREATE TABLE grades_history (
|
|
student_id bigint NOT NULL,
|
|
course_id bigint NOT NULL,
|
|
change_time timestamp with time zone NOT NULL,
|
|
course text NOT NULL,
|
|
old_grade text NOT NULL,
|
|
new_grade text NOT NULL,
|
|
PRIMARY KEY (student_id, course_id, change_time)
|
|
);
|
|
|
|
-- Listing 17-18: Creating the record_if_grade_changed() function
|
|
|
|
CREATE OR REPLACE FUNCTION record_if_grade_changed()
|
|
RETURNS trigger AS
|
|
$$
|
|
BEGIN
|
|
IF NEW.grade <> OLD.grade THEN
|
|
INSERT INTO grades_history (
|
|
student_id,
|
|
course_id,
|
|
change_time,
|
|
course,
|
|
old_grade,
|
|
new_grade)
|
|
VALUES
|
|
(OLD.student_id,
|
|
OLD.course_id,
|
|
now(),
|
|
OLD.course,
|
|
OLD.grade,
|
|
NEW.grade);
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Listing 17-19: Creating the grades_update trigger
|
|
|
|
CREATE TRIGGER grades_update
|
|
AFTER UPDATE
|
|
ON grades
|
|
FOR EACH ROW
|
|
EXECUTE PROCEDURE record_if_grade_changed();
|
|
|
|
-- Listing 17-20: Testing the grades_update trigger
|
|
|
|
-- Initially, there should be 0 records in the history
|
|
SELECT * FROM grades_history;
|
|
|
|
-- Check the grades
|
|
SELECT * FROM grades;
|
|
|
|
-- Update a grade
|
|
UPDATE grades
|
|
SET grade = 'C'
|
|
WHERE student_id = 1 AND course_id = 1;
|
|
|
|
-- Now check the history
|
|
SELECT student_id,
|
|
change_time,
|
|
course,
|
|
old_grade,
|
|
new_grade
|
|
FROM grades_history;
|
|
|
|
-- Listing 17-21: Creating a temperature_test table
|
|
|
|
CREATE TABLE temperature_test (
|
|
station_name text,
|
|
observation_date date,
|
|
max_temp integer,
|
|
min_temp integer,
|
|
max_temp_group text,
|
|
PRIMARY KEY (station_name, observation_date)
|
|
);
|
|
|
|
-- Listing 17-22: Creating the classify_max_temp() function
|
|
-- CHECK AGAINST CATEGORIES USED PREVIOUSLY
|
|
|
|
CREATE OR REPLACE FUNCTION classify_max_temp()
|
|
RETURNS trigger AS
|
|
$$
|
|
BEGIN
|
|
CASE
|
|
WHEN NEW.max_temp >= 90 THEN
|
|
NEW.max_temp_group := 'Hot';
|
|
WHEN NEW.max_temp BETWEEN 70 AND 89 THEN
|
|
NEW.max_temp_group := 'Warm';
|
|
WHEN NEW.max_temp BETWEEN 50 AND 69 THEN
|
|
NEW.max_temp_group := 'Pleasant';
|
|
WHEN NEW.max_temp BETWEEN 33 AND 49 THEN
|
|
NEW.max_temp_group := 'Cold';
|
|
WHEN NEW.max_temp BETWEEN 20 AND 32 THEN
|
|
NEW.max_temp_group := 'Freezing';
|
|
ELSE NEW.max_temp_group := 'Inhumane';
|
|
END CASE;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Listing 17-23: Creating the temperature_insert trigger
|
|
|
|
CREATE TRIGGER temperature_insert
|
|
BEFORE INSERT
|
|
ON temperature_test
|
|
FOR EACH ROW
|
|
EXECUTE PROCEDURE classify_max_temp();
|
|
|
|
-- Listing 17-24: Inserting rows to test the temperature_update trigger
|
|
|
|
INSERT INTO temperature_test (station_name, observation_date, max_temp, min_temp)
|
|
VALUES
|
|
('North Station', '1/19/2019', 10, -3),
|
|
('North Station', '3/20/2019', 28, 19),
|
|
('North Station', '5/2/2019', 65, 42),
|
|
('North Station', '8/9/2019', 93, 74);
|
|
|
|
SELECT * FROM temperature_test;
|