Let the database
do all hard work
David Schovanec
Github, Twitter: @schovi
Index
Multi column index
CREATE INDEX meetups_multi_index
ON meetups ("location", "language", "date");
SELECT * FROM meetups WHERE "location" = 'Prague'
AND "language" = 'ruby'
AND "date" = '2016-09-15';
SELECT * FROM meetups WHERE "location" = 'Prague'
AND "language" = 'ruby';
SELECT * FROM meetups WHERE "location" = 'Prague';
Expression index
CREATE INDEX lower_email ON meetups (LOWER("email"));
SELECT * FROM users WHERE LOWER("email") = LOWER('David@Schovi.cz');
Index implicitly supports pattern matching LIKE 'david%'
CREATE INDEX reverse_email ON users (REVERSE("email"));
SELECT * FROM users WHERE REVERSE("email") LIKE REVERSE('%schovi.cz');
Partial index
CREATE INDEX reminders_not_sent_at
ON reminders(sent_at) WHERE NOT sent_at;
SELECT * FROM reminders WHERE send_at IS NULL;
Data types
Date and time
-- rails begining_of_day(Time.now)
SELECT date_trunc('day', start_at)::date, count(*)
FROM meetups
GROUP BY date_trunc('day', start_at);
-- Get date before 3 months
SELECT now() - '3 month'::interval;
-- Series
SELECT generate_series(
date_trunc('day', now()),
date_trunc('day', now()::date + '1 month'::interval),
'1 day'::interval
)
Enum
-- Create
CREATE TYPE languages_enum AS
ENUM ('ruby', 'javascript', 'python', 'crystal');
-- Update
ALTER TYPE languages_enum ADD VALUE 'java' AFTER 'ruby';
-- Add column with enum type
CREATE TABLE meetups (
language languages_enum
);
-- Using
INSERT INTO meetups(language) VALUES ('ruby');
INSERT INTO meetups(language) VALUES ('c++');
-- ERROR: invalid input value for enum languages_enum: "c++"
Array
-- Creating
SELECT ARRAY[1, 2, 3];
-- Updating existing
SELECT ARRAY[1,2] || 3;
SELECT 3 || ARRAY[1,2];
SELECT ARRAY[1,2] || ARRAY[3, 4];
-- Aggregate to array
SELECT array_agg(distinct language) from meetups where location = 'Prague';
-- Create column
CREATE TABLE example (
my_text_array text[],
matrix number[][]
);
-- Get item
SELECT (ARRAY['a', 'b', 'c'])[1]; -- 'a'
-- Query
SELECT * FROM meetups WHERE tags @> array['party', 'food'];
-- Index
CREATE INDEX ON meetups(tags);
Jsonb
-- Create column
ALTER TABLE meetups ADD COLUMN meta jsonb;
-- Creating
SELECT '{"key": "val", "nested": {"key2": "val2"}}'::jsonb;
-- Upadting
SELECT meta || '{"new_key": "new_value"}'::jsonb FROM meetups;
-- Get item
SELECT meta->>'key'; -- "val"
SELECT meta->'nested'->>'key2'; -- "val2"
SELECT jsonb_extract_path_text(meta, 'nested', 'key2'); -- "val2"
-- Query
SELECT * FROM meetups WHERE meta ? 'nested';
SELECT * FROM meetups WHERE meta->'nested' @> '{"key2": "val2"}'
SELECT * FROM meetups WHERE meta->'nested'->>'key2' = 'val2'
-- Index
CREATE INDEX ON meetups(meta);
Extensions
-- https://github.com/petere/pgemailaddr
SELECT user(email) FROM users WHERE host(email) = 'schovi.cz'
-- https://github.com/petere/pguri
SELECT * FROM meetups WHERE uri_host(web) = 'schovi.cz'
SELECT host('https://schovi.cz'::uri)
WITH
WITH
WITH
habitable_cities AS (
SELECT id FROM cities
WHERE current_temp BETWEEN 15 AND 25
),
interesting_languages (
SELECT name FROM languages
WHERE popularity > 90
)
SELECT * FROM meetups
WHERE city_id IN habitable_cities
AND language IN interesting_languages;
WITH RECURSIVE
WITH RECURSIVE all_categories AS (
-- Static
SELECT c.id, c.name, c.category_id, ARRAY[c.id] AS path
FROM categories c
WHERE category_id IS NULL
UNION ALL
-- Recursive
SELECT c.id, c.name, c.category_id, ac.path || c.id
FROM categories c
JOIN all_categories ac ON c.category_id = ac.id
)
SELECT * FROM all_categories WHERE path @> ARRAY[1] ORDER BY path;
id | name | category_id | path |
---|---|---|---|
1 | Notebook | NULL | {1} |
2 | Apple | 1 | {1,2} |
3 | Asus | 1 | {1,3} |
4 | Macbook Pro | 2 | {1,2,4} |
Window Functions
PARTITION BY
SELECT id, location, language, attendees,
(
100 -
attendees /
AVG(attendees) OVER (PARTITION BY language)
* 100
) AS attendees_to_avg
FROM meetups;
location | language | attendees | attendees_to_avg |
---|---|---|---|
Prague | ruby | 200 | -20% |
Brno | ruby | 100 | -60% |
Berlin | ruby | 300 | 20% |
Vienna | ruby | 400 | 60% |
ORDER BY
SELECT name, date, attendees,
SUM(attendees) OVER (ORDER BY date ASC) as total
FROM meetups
WHERE name = 'Prague Ruby Meetup';
name | date | attendees | total |
---|---|---|---|
Prague Ruby Meetup | 2016-04-14 | 60 | 60 |
Prague Ruby Meetup | 2016-06-09 | 45 | 105 |
Prague Ruby Meetup | 2016-09-15 | 120 | 225 |
Functions
SELECT name, date, attendees,
rank() OVER (ORDER BY attendees DESC)
FROM meetups
WHERE name = 'Prague Ruby Meetup';
name | date | attendees | rank |
---|---|---|---|
Prague Ruby Meetup | 2016-04-14 | 60 | 2 |
Prague Ruby Meetup | 2016-06-09 | 45 | 3 |
Prague Ruby Meetup | 2016-09-15 | 120 | 1 |
first_value(column)
last_value(column)
lag(column, [offset, [default]])
lead(column, [offset, [default]])
ROWS
SELECT name, date, attendees,
(
attendees * 100.0 /
lag(attendees, 1) OVER (ORDER BY date ASC ROWS 1 PRECEDING)
) as diff
FROM meetups
WHERE name = 'Prague Ruby Meetup';
name | date | attendees | diff |
---|---|---|---|
Prague Ruby Meetup | 2016-04-14 | 60 | NULL |
Prague Ruby Meetup | 2016-04-09 | 45 | 75% |
Prague Ruby Meetup | 2016-09-15 | 120 | 266% |
ROWS UNBOUNDED PRECEDING AND CURRENT ROW
ROWS 10 PRECEDING AND CURRENT ROW
ROWS CURRENT ROW AND 20 FOLLOWING
ROWS CURRENT ROW AND UNBOUNDED FOLLOWING
Rails?
String
class Meetup < ActiveRecord::Base
def self.sql(raw_sql, *values)
sql = ActiveRecord::Base.send(:sanitize_sql_array, [raw_sql, *values])
find_by_sql(sql)
end
end
Meetup.sql(<<~SQL, 10, 'ruby')
WITH popular_cities AS (
SELECT name FROM cities
ORDER BY meetups_count DESC
LIMIT ?
)
SELECT * FROM meetups
WHERE city IN popular_cities
AND language = ?;
SQL
String and Scopes
class Meetup < ActiveRecord::Base
def self.find_in_popular_city(cities_count, language)
popular_cities = City.popular.limit(cities_count)
main_query = self.where("city IN popular_cities").
where(language: language)
find_by_sql(<<~SQL)
WITH popular_cities AS (#{popular_cities.to_sql})
#{main_query.to_sql}
SQL
end
end
Arel
?
Gem postgres_ext
# Gemfile
gem "postgres_ext"
# App
Meetup.with(popular_cities: City.popular.limit(10)).
where("city IN popular_cities").
where(language: 'ruby')
Gem schema_plus
# Gemfile
gem "schema_plus"
# Migration
add_index :users, index: { expression: "REVERSE(email)" }
create_enum :color, 'red', 'green', 'blue'
alter_enum :color, 'purple', after: 'red'
drop_enum :color
Dont be lazy to write code
# migration
def up
execute "CREATE TYPE roles AS ENUM ('worker', 'manager', 'boss');"
end
# model
class Employee < ActiveRecord::Base
include PostgresEnum
postgres_enum :role
end
# usage
# > Employee.new(role: 'ceo')
# SQL (1.9ms) SELECT unnest(enum_range(NULL::"roles"))::text AS enum_value;
# ArgumentError: 'ceo' is not a valid role (available values: worker, manager, boss)
Thanks!
Any questions?
Nechte těžkou práci na vaší databázi
By schovi
Nechte těžkou práci na vaší databázi
- 949