David Schovanec
Github, Twitter: @schovi
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';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');CREATE INDEX reminders_not_sent_at 
          ON reminders(sent_at) WHERE NOT sent_at;
SELECT * FROM reminders WHERE send_at IS NULL;-- 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
       )-- 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++"-- 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);-- 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);-- 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
  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 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} | 
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% | 
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 | 
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]])
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
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
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
?
# Gemfile
gem "postgres_ext"
# App
Meetup.with(popular_cities: City.popular.limit(10)).
       where("city IN popular_cities").
       where(language: 'ruby')# 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# 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)Any questions?