Drop Down MenusCSS Drop Down MenuPure CSS Dropdown Menu

Foreign Table Inheritance

Foreign tables can now either inherit local tables, or be inherited from.
For example, a local table can inherit a foreign table:
 -- Create a new table which inherits from the foreign table
 # CREATE TABLE local_customers () INHERITS (remote.customers);
 -- Insert some data into it
 # INSERT INTO local_customers VALUES (16, 'Bruce',$$Jo's Cupcakes$$, '2015-01-15', '2017-01-14', true, 'running', 'basic');
 -- And if we query the parent foreign table...
 # SELECT tableoid::regclass, * FROM remote.customers;
      tableoid     | id | name  |    company    | registered_date | expiry_date | active | status  | account_level 
 ------------------+----+-------+---------------+-----------------+-------------+--------+---------+---------------
  remote.customers |  1 | James | Hughson Corp  | 2014-05-03      | 2016-05-02  | t      | idle    | premium
  local_customers  | 16 | Bruce | Jo's Cupcakes | 2015-01-15      | 2017-01-14  | t      | running | basic
 (2 rows)
Or a foreign table can be made to inherit from a local table:

 -- Create a new table that the foreign table will be a child of
 # CREATE TABLE master_customers (LIKE remote.customers);
 -- Insert a new row into this table
 # INSERT INTO master_customers VALUES (99, 'Jolly',$$Cineplanet$$, '2014-10-30', '2016-10-29', true, 'running', 'premium');
 -- Have the foreign table inherit from the new table
# ALTER TABLE remote.customers INHERIT master_customers;
 -- Let's have a look at the contents of the new table now
 # SELECT tableoid::regclass, * FROM master_customers;
      tableoid     | id | name  |    company    | registered_date | expiry_date | active | status  | account_level 
 ------------------+----+-------+---------------+-----------------+-------------+--------+---------+---------------
  master_customers | 99 | Jolly | Cineplanet    | 2014-10-30      | 2016-10-29  | t      | running | premium
  remote.customers |  1 | James | Hughson Corp  | 2014-05-03      | 2016-05-02  | t      | idle    | premium
  local_customers  | 16 | Bruce | Jo's Cupcakes | 2015-01-15      | 2017-01-14  | t      | running | basic
 (3 rows)
 -- And the query plan...
 # EXPLAIN ANALYSE SELECT tableoid::regclass, * FROM master_customers;
                                                         QUERY PLAN                                                         
 ---------------------------------------------------------------------------------------------------------------------------
  Result  (cost=0.00..140.80 rows=1012 width=145) (actual time=0.014..0.595 rows=3 loops=1)
    ->  Append  (cost=0.00..140.80 rows=1012 width=145) (actual time=0.012..0.591 rows=3 loops=1)
          ->  Seq Scan on master_customers  (cost=0.00..1.48 rows=48 width=145) (actual time=0.012..0.013 rows=1 loops=1)
          ->  Foreign Scan on customers  (cost=100.00..124.52 rows=484 width=145) (actual time=0.567..0.567 rows=1 loops=1)
          ->  Seq Scan on local_customers  (cost=0.00..14.80 rows=480 width=145) (actual time=0.007..0.008 rows=1 loops=1)
  Planning time: 0.256 ms
  Execution time: 1.040 ms
 (7 rows)
ALLOW FOREIGN TABLES TO PARTICIPATE IN INHERITANCE:
Foreign tables can now be inheritance children, or parents.  Much of the
system was already ready for this, but we had to fix a few things of
course, mostly in the area of planner and executor handling of row locks.
 
As side effects of this, allow foreign tables to have NOT VALID CHECK
constraints (and hence to accept ALTER ... VALIDATE CONSTRAINT), and to
accept ALTER SET STORAGE and ALTER SET WITH/WITHOUT OIDS.  Continuing to
disallow these things would've required bizarre and inconsistent special
cases in inheritance behavior.  Since foreign tables don't enforce CHECK
constraints anyway, a NOT VALID one is a complete no-op, but that doesn't
mean we shouldn't allow it.  And it's possible that some FDWs might have
use for SET STORAGE or SET WITH OIDS, though doubtless they will be no-ops
for most.
 
An additional change in support of this is that when a ModifyTable node
has multiple target tables, they will all now be explicitly identified
in EXPLAIN output, for example:
 
 Update on pt1  (cost=0.00..321.05 rows=3541 width=46)
   Update on pt1
   Foreign Update on ft1
   Foreign Update on ft2
   Update on child3
   ->  Seq Scan on pt1  (cost=0.00..0.00 rows=1 width=46)
   ->  Foreign Scan on ft1  (cost=100.00..148.03 rows=1170 width=46)
   ->  Foreign Scan on ft2  (cost=100.00..148.03 rows=1170 width=46)
   ->  Seq Scan on child3  (cost=0.00..25.00 rows=1200 width=46)
 
This was done mainly to provide an unambiguous place to attach "Remote SQL"
fields, but it is useful for inherited updates even when no foreign tables
are involved.
Initially I almost dismissed it. Another cool feature for hard-code nerds. But then it hit me. This is really big. Why? Read on …
Let's see how to get it working, and what we can use it for 🙂
I'll make myself 5 databases, named master and shard_1, shard_2, shard_3, and shard_4.
$ echo master shard_{1,2,3,4} | xargs -n1 createdb
Of course in real life scenario the databases would be on separate machines/instances, but this is just test environment, so it has to be enough.
Now, in each of these databases, I'll make a poor-man sharding table for users:
for a in {1..4}
do
    echo "
        create table users (id serial primary key, username text not null);
        alter sequence users_id_seq increment by 4 restart with $a;
    " | psql -d shard_$a; done
done
It creates the users table, but in each shard it will give different ids. In shard 1 it will be 1, 5, 9, …, in shard 2 it will be 2, 6, 10, … and so on.
With the tables in place, let's make a master table and make all other inherit from it.
$ create table users (id serial primary key, username text not null);
$ create extension postgres_fdw;
$ create server shard_1 foreign data wrapper postgres_fdw options( dbname 'shard_1' );
$ create server shard_2 foreign data wrapper postgres_fdw options( dbname 'shard_2' );
$ create server shard_3 foreign data wrapper postgres_fdw options( dbname 'shard_3' );
$ create server shard_4 foreign data wrapper postgres_fdw options( dbname 'shard_4' );
$ create user mapping for depesz server shard_1 options ( user 'depesz' );
$ create user mapping for depesz server shard_2 options ( user 'depesz' );
$ create user mapping for depesz server shard_3 options ( user 'depesz' );
$ create user mapping for depesz server shard_4 options ( user 'depesz' );
$ create foreign table users_shard_1 () INHERITS (users) server shard_1 options ( table_name 'users' );
$ create foreign table users_shard_2 () INHERITS (users) server shard_2 options ( table_name 'users' );
$ create foreign table users_shard_3 () INHERITS (users) server shard_3 options ( table_name 'users' );
$ create foreign table users_shard_4 () INHERITS (users) server shard_4 options ( table_name 'users' );
Tedious. But it can be make more automatic.
Now, with this in place I should be able to select, insert, update, and delete rows directly to shards:
[master] $ insert into users_shard_1 (username) values ('Alice') returning *;
 id | username 
----+----------
  1 | Alice
(1 row)
 
INSERT 0 1
 
[master] $ insert into users_shard_1 (username) values ('Bob') returning *;
 id | username 
----+----------
  2 | Bob
(1 row)
 
INSERT 0 1
 
[master] $ ^C
 
[master] $ insert into users_shard_2 (username) values ('Carol') returning *;
 id | username 
----+----------
  3 | Carol
(1 row)
 
INSERT 0 1
 
[master] $ insert into users_shard_2 (username) values ('Dan') returning *;
 id | username 
----+----------
  4 | Dan
(1 row)
 
INSERT 0 1
 
[master] $ insert into users_shard_3 (username) values ('Eve') returning *;
 id | username 
----+----------
  5 | Eve
(1 row)
 
INSERT 0 1
 
[master] $ insert into users_shard_3 (username) values ('Frank') returning *;
 id | username 
----+----------
  6 | Frank
(1 row)
 
INSERT 0 1
 
[master] $ insert into users_shard_4 (username) values ('George') returning *;
 id | username 
----+----------
  7 | George
(1 row)
 
INSERT 0 1
 
[master] $ insert into users_shard_4 (username) values ('Harold') returning *;
 id | username 
----+----------
  8 | Harold
(1 row)
Immediately I can see a problem – my sequences on shards do not give the ids I thought they will. Reason is simple – ID is assigned on master. So, to make it work sensibly, I would have to make 4 sequences on master, and do the magic there. Given that values up to 8 are already taken, I'll have to change restart values too:
$ create sequence users_id_seq_shard_1 increment by 4 restart with 9;
$ create sequence users_id_seq_shard_2 increment by 4 restart with 10;
$ create sequence users_id_seq_shard_3 increment by 4 restart with 11;
$ create sequence users_id_seq_shard_4 increment by 4 restart with 12;
$ alter foreign table users_shard_1 alter COLUMN id set default nextval('users_id_seq_shard_1');
$ alter foreign table users_shard_2 alter COLUMN id set default nextval('users_id_seq_shard_2');
$ alter foreign table users_shard_3 alter COLUMN id set default nextval('users_id_seq_shard_3');
$ alter foreign table users_shard_4 alter COLUMN id set default nextval('users_id_seq_shard_4');
And now, new inserts, will have ids assigned correctly:
[master] $ insert into users_shard_1 (username) values ('ian'), ('james') returning *;
 id | username 
----+----------
  9 | ian
 13 | james
(2 rows)
 
INSERT 0 2
 
[master] $ insert into users_shard_2 (username) values ('kenneth'), ('larry') returning *;
 id | username 
----+----------
 10 | kenneth
 14 | larry
(2 rows)
 
INSERT 0 2
 
[master] $ insert into users_shard_3 (username) values ('michael'), ('nicholas') returning *;
 id | username 
----+----------
 11 | michael
 15 | nicholas
(2 rows)
 
INSERT 0 2
 
[master] $ insert into users_shard_4 (username) values ('oscar'), ('paul') returning *;
 id | username 
----+----------
 12 | oscar
 16 | paul
(2 rows)
 
INSERT 0 2
Better.
Now, thanks to this new patch, I can scan them all together:
[master] $ explain analyze select count(*) from users;
                                                          QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=883.69..883.70 rows=1 width=0) (actual time=3.392..3.392 rows=1 loops=1)
   ->  Append  (cost=0.00..849.56 rows=13653 width=0) (actual time=0.880..3.381 rows=16 loops=1)
         ->  Seq Scan on users  (cost=0.00..0.00 rows=1 width=0) (actual time=0.001..0.001 rows=0 loops=1)
         ->  Foreign Scan on users_shard_1  (cost=100.00..212.39 rows=3413 width=0) (actual time=0.878..0.879 rows=4 loops=1)
         ->  Foreign Scan on users_shard_2  (cost=100.00..212.39 rows=3413 width=0) (actual time=0.864..0.864 rows=4 loops=1)
         ->  Foreign Scan on users_shard_3  (cost=100.00..212.39 rows=3413 width=0) (actual time=0.768..0.769 rows=4 loops=1)
         ->  Foreign Scan on users_shard_4  (cost=100.00..212.39 rows=3413 width=0) (actual time=0.857..0.860 rows=4 loops=1)
 Planning time: 0.292 ms
 Execution time: 4.871 ms
(9 rows)
Delete rows from them:
[master] $ delete from users where random() < 0.3 returning *;
 id | username 
----+----------
 10 | kenneth
 15 | nicholas
  7 | George
 12 | oscar
(4 rows)
and of course update them:
[master] $ explain analyze update users set username = lower(username) where username <> lower(username) returning *;
                                                       QUERY PLAN                                                        
-------------------------------------------------------------------------------------------------------------------------
 Update on users  (cost=0.00..625.61 rows=4941 width=42) (actual time=0.775..2.344 rows=7 loops=1)
   Update on users
   Foreign Update on users_shard_1
   Foreign Update on users_shard_2
   Foreign Update on users_shard_3
   Foreign Update on users_shard_4
   ->  Seq Scan on users  (cost=0.00..0.00 rows=1 width=42) (actual time=0.001..0.001 rows=0 loops=1)
         Filter: (username <> lower(username))
   ->  Foreign Scan on users_shard_1  (cost=100.00..156.40 rows=1235 width=42) (actual time=0.507..0.510 rows=2 loops=1)
   ->  Foreign Scan on users_shard_2  (cost=100.00..156.40 rows=1235 width=42) (actual time=0.313..0.315 rows=2 loops=1)
   ->  Foreign Scan on users_shard_3  (cost=100.00..156.40 rows=1235 width=42) (actual time=0.294..0.296 rows=2 loops=1)
   ->  Foreign Scan on users_shard_4  (cost=100.00..156.40 rows=1235 width=42) (actual time=0.261..0.262 rows=1 loops=1)
 Planning time: 0.135 ms
 Execution time: 2.898 ms
(14 rows)
Now, you have to notice that sharding in this way doesn't really make the queries faster – all foreign scans are done sequentially.
But, assuming some a bit larger dataset:
[master] $ insert into users_shard_1 (username) select 'random user ' || i from generate_series(1,100000) i;
INSERT 0 100000
 
[master] $ insert into users_shard_2 (username) select 'random user ' || i from generate_series(1,100000) i;
INSERT 0 100000
 
[master] $ insert into users_shard_3 (username) select 'random user ' || i from generate_series(1,100000) i;
INSERT 0 100000
 
[master] $ insert into users_shard_4 (username) select 'random user ' || i from generate_series(1,100000) i;
INSERT 0 100000
We can see that with fast queries it still makes sense.
[master] $ explain analyze select * from users where id = 123124; QUERY PLAN ———————————————————————————————————————- Append (cost=0.00..508.81 rows=29 width=36) (actual time=4.270..4.271 rows=1 loops=1) -> Seq Scan on users (cost=0.00..0.00 rows=1 width=36) (actual time=0.001..0.001 rows=0 loops=1) Filter: (id = 123124) -> Foreign Scan on users_shard_1 (cost=100.00..127.20 rows=7 width=36) (actual time=1.020..1.020 rows=0 loops=1) -> Foreign Scan on users_shard_2 (cost=100.00..127.20 rows=7 width=36) (actual time=1.247..1.247 rows=0 loops=1) -> Foreign Scan on users_shard_3 (cost=100.00..127.20 rows=7 width=36) (actual time=1.004..1.004 rows=0 loops=1) -> Foreign Scan on users_shard_4 (cost=100.00..127.20 rows=7 width=36) (actual time=0.993..0.994 rows=1 loops=1) Planning time: 0.349 ms Execution time: 6.070 ms (9 rows)
This doesn't look so good. But, PostgreSQL has built in partitioning. Which we can now use for some great effects:
[master] $ alter foreign table  users_shard_1 add check ( id % 4 = 1 );
ALTER FOREIGN TABLE
 
[master] $ alter foreign table  users_shard_2 add check ( id % 4 = 2 );
ALTER FOREIGN TABLE
 
[master] $ alter foreign table  users_shard_3 add check ( id % 4 = 3 );
ALTER FOREIGN TABLE
 
[master] $ alter foreign table  users_shard_4 add check ( id % 4 = 0 );
ALTER FOREIGN TABLE
and with this in place:
[master] $ explain analyze select * from users where id = 123124 and (id % 4) = (123124 % 4);
                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..133.91 rows=2 width=36) (actual time=0.905..0.907 rows=1 loops=1)
   ->  Seq Scan on users  (cost=0.00..0.00 rows=1 width=36) (actual time=0.002..0.002 rows=0 loops=1)
         Filter: ((id = 123124) AND ((id % 4) = 0))
   ->  Foreign Scan on users_shard_4  (cost=100.00..133.91 rows=1 width=36) (actual time=0.902..0.903 rows=1 loops=1)
 Planning time: 0.448 ms
 Execution time: 1.451 ms
(6 rows)
This is much nicer.
Only two “tables" got scanned – users – empty parent of all shards, and the single shard that we needed.
If I used partition/sharding key differently (using ranges for example) PostgreSQL could have be even smart enough to pick the right shard without guiding with (id % 4) = (123124 % 4).
But this little thing doesn't really spoil the fun. Lo and behold it looks like we got sharding ( of course my choice of names for databases did spoil the fun a bit ) solution that will actually work.
With some simple triggers on users() table (to redirect inserts to partitions/shards) we'll be golden.
The only thing missing would be some way to run the foreign scans in parallel (when there is more than one). Maybe it will materialize one day, we'll see.

No comments:

Post a Comment