pg_online_schema_change 0.4.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ef1069f7d159544838ec27cf518f98eb31609a0a2e1878ebd40246b10a6b534
4
- data.tar.gz: 8e83cbac78164fb10870537bf4867020eae7cf06f4cd91930a2c8846fbc76c7c
3
+ metadata.gz: 273557a58c9492e628277e6eed7b655233c47bb51b12a61ca3eaf3377557205a
4
+ data.tar.gz: 6ec45511249401c420cc3c61652a4ad18225917b644574fb58329819fb422ab1
5
5
  SHA512:
6
- metadata.gz: 2657c94b3b07730aa3bb282f34cd1ee95f0549d3649b39a61be3ce1b6d2ba5832a1914b6a9788778c912464cbd9773b5e967ed4e6c896127d60e62a0daa76c99
7
- data.tar.gz: 4788523d691c25045b7f3c539f9743d880fabcbb01887ae34ec55a6c8a829804a9f4d43f619e79e0c44b1c9fff6ea88361fb5ac1111bc1f491aa17e6e43336d0
6
+ metadata.gz: 29f07b0af4c6a78de6945b6f43e271518f9c5953388e99dcc7582c4db865881ac5e5b8d0f1dff9a1215216ea534ad1e393fb268816e3799832911688b7abc14e
7
+ data.tar.gz: 0362f9c666c58acdb2fb63f1cd0208c719eb788555b218a57e2aee7bb9d98bbd9a7ff4d1e0ee0a56deec8e9679def1ae116fef919366e86ae30b2444d3379e6a
data/.rspec CHANGED
@@ -1,4 +1,5 @@
1
1
  --format documentation
2
2
  --color
3
3
  --require spec_helper
4
- --fail-fast
4
+ --fail-fast
5
+ --exclude-pattern spec/lib/smoke_spec.rb
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2022-02-21 22:46:44 UTC using RuboCop version 1.23.0.
3
+ # on 2022-03-13 19:35:49 UTC using RuboCop version 1.23.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -9,7 +9,7 @@
9
9
  # Offense count: 2
10
10
  # Configuration parameters: CountComments, CountAsOne.
11
11
  Metrics/ClassLength:
12
- Max: 233
12
+ Max: 250
13
13
 
14
14
  # Offense count: 2
15
15
  # Configuration parameters: IgnoredMethods.
@@ -26,14 +26,14 @@ Packaging/GemspecGit:
26
26
  Exclude:
27
27
  - 'pg_online_schema_change.gemspec'
28
28
 
29
- # Offense count: 62
29
+ # Offense count: 67
30
30
  # Configuration parameters: CountAsOne.
31
31
  RSpec/ExampleLength:
32
32
  Max: 55
33
33
 
34
- # Offense count: 38
34
+ # Offense count: 24
35
35
  RSpec/MultipleExpectations:
36
- Max: 14
36
+ Max: 13
37
37
 
38
38
  # Offense count: 6
39
39
  # Configuration parameters: AllowedMethods.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [0.6.0] - 2022-02-26
2
+ * Delete items by audit table PK when replaying by @shayonj @jfrost in https://github.com/shayonj/pg-osc/pull/60
3
+ - Fixes a race condition issue: https://github.com/shayonj/pg-osc/issues/58
4
+
5
+ ## [0.5.0] - 2022-02-26
6
+ * Share some preliminary load test figures in https://github.com/shayonj/pg-osc/pull/54
7
+ * Reuse existing transaction open for reading table columns in https://github.com/shayonj/pg-osc/pull/53
8
+ * Start to deprecate --password with PGPASSWORD in https://github.com/shayonj/pg-osc/pull/56
9
+ * Introduce configurable PULL_BATCH_COUNT and DELTA_COUNT in https://github.com/shayonj/pg-osc/pull/57
10
+
11
+ ## [0.4.0] - 2022-02-22
12
+ * Lint sourcecode, setup Rubocop proper and Lint in CI by @shayonj in https://github.com/shayonj/pg-osc/pull/46
13
+ * Uniquely identify operation_type column by @shayonj in https://github.com/shayonj/pg-osc/pull/50
14
+ * Introduce primary key on audit table for ordered reads by @shayonj in https://github.com/shayonj/pg-osc/pull/49
15
+ - This addresses an edge case with replay.
16
+ * Uniquely identify trigger_time column by @shayonj in https://github.com/shayonj/pg-osc/pull/51
17
+ * Abstract assertions into a helper function by @shayonj in https://github.com/shayonj/pg-osc/pull/52
18
+
1
19
  ## [0.3.0] - 2022-02-21
2
20
 
3
21
  - Explicitly call dependencies and bump dependencies by @shayonj https://github.com/shayonj/pg-osc/pull/44
data/CODE_OF_CONDUCT.md CHANGED
@@ -39,7 +39,7 @@ This Code of Conduct applies within all community spaces, and also applies when
39
39
 
40
40
  ## Enforcement
41
41
 
42
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at shayon@loom.com. All complaints will be reviewed and investigated promptly and fairly.
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at shayonj@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
43
 
44
44
  All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
45
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pg_online_schema_change (0.3.0)
4
+ pg_online_schema_change (0.6.0)
5
5
  ougai (~> 2.0.0)
6
6
  pg (~> 1.3.2)
7
7
  pg_query (~> 2.1.3)
@@ -22,7 +22,7 @@ GEM
22
22
  parallel (1.21.0)
23
23
  parser (3.0.3.2)
24
24
  ast (~> 2.4.1)
25
- pg (1.3.2)
25
+ pg (1.3.3)
26
26
  pg_query (2.1.3)
27
27
  google-protobuf (>= 3.19.2)
28
28
  pry (0.14.1)
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # pg-osc
2
2
 
3
- [![CircleCI](https://circleci.com/gh/shayonj/pg-osc/tree/main.svg?style=shield)](https://circleci.com/gh/shayonj/pg-osc/tree/main)
3
+ [![CI](https://github.com/shayonj/pg-osc/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/shayonj/pg-osc/actions/workflows/ci.yaml)
4
+ [![Smoke Test PG 9.6](https://github.com/shayonj/pg-osc/actions/workflows/smoke-tests-9-6.yaml/badge.svg?branch=main)](https://github.com/shayonj/pg-osc/actions/workflows/smoke-tests-9-6.yaml)
5
+ [![Smoke Test PG 13.6](https://github.com/shayonj/pg-osc/actions/workflows/smoke-tests-13-6.yaml/badge.svg?branch=main)](https://github.com/shayonj/pg-osc/actions/workflows/smoke-tests-13-6.yaml)
4
6
  [![Gem Version](https://badge.fury.io/rb/pg_online_schema_change.svg)](https://badge.fury.io/rb/pg_online_schema_change)
5
7
 
6
8
  pg-online-schema-change (`pg-osc`) is a tool for making schema changes (any `ALTER` statements) in Postgres tables with minimal locks, thus helping achieve zero downtime schema changes against production workloads.
@@ -16,8 +18,8 @@ pg-online-schema-change (`pg-osc`) is a tool for making schema changes (any `ALT
16
18
  - [Installation](#installation)
17
19
  - [Requirements](#requirements)
18
20
  - [Usage](#usage)
19
- - [How does it work](#how-does-it-work)
20
21
  - [Prominent features](#prominent-features)
22
+ - [Load test](#load-test)
21
23
  - [Examples](#examples)
22
24
  * [Renaming a column](#renaming-a-column)
23
25
  * [Multiple ALTER statements](#multiple-alter-statements)
@@ -25,6 +27,7 @@ pg-online-schema-change (`pg-osc`) is a tool for making schema changes (any `ALT
25
27
  * [Backfill data](#backfill-data)
26
28
  * [Running using Docker](#running-using-docker)
27
29
  - [Caveats](#caveats)
30
+ - [How does it work](#how-does-it-work)
28
31
  - [Development](#development)
29
32
  - [Releasing](#releasing)
30
33
  - [Contributing](#contributing)
@@ -75,13 +78,17 @@ Options:
75
78
  -u, --username=USERNAME # Username for the Database
76
79
  -p, --port=N # Port for the Database
77
80
  # Default: 5432
78
- -w, --password=PASSWORD # Password for the Database
81
+ -w, --password=PASSWORD # DEPRECATED: Password for the Database. Please pass PGPASSWORD environment variable instead.
79
82
  -v, [--verbose], [--no-verbose] # Emit logs in debug mode
80
83
  -f, [--drop], [--no-drop] # Drop the original table in the end after the swap
81
84
  -k, [--kill-backends], [--no-kill-backends] # Kill other competing queries/backends when trying to acquire lock for the shadow table creation and swap. It will wait for --wait-time-for-lock duration before killing backends and try upto 3 times.
82
85
  -w, [--wait-time-for-lock=N] # Time to wait before killing backends to acquire lock and/or retrying upto 3 times. It will kill backends if --kill-backends is true, otherwise try upto 3 times and exit if it cannot acquire a lock.
83
86
  # Default: 10
84
- -c, [--copy-statement=COPY_STATEMENT] # Takes a .sql file location where you can provide a custom query to be played (ex: backfills) when pg-osc copies data from the primary to the shadow table. More examples in README.
87
+ -c, [--copy-statement=COPY_STATEMENT] # Takes a .sql file location where you can provide a custom query to be played (ex: backfills) when pgosc copies data from the primary to the shadow table. More examples in README.
88
+ -b, [--pull-batch-count=N] # Number of rows to be replayed on each iteration after copy. This can be tuned for faster catch up and swap. Best used with delta-count.
89
+ # Default: 1000
90
+ -e, [--delta-count=N] # Indicates how many rows should be remaining before a swap should be performed. This can be tuned for faster catch up and swap, especially on highly volume tables. Best used with pull-batch-count.
91
+ # Default: 20
85
92
  ```
86
93
 
87
94
  ```
@@ -90,57 +97,39 @@ Usage:
90
97
 
91
98
  print the version
92
99
  ```
93
- ## How does it work
94
-
95
- - **Primary table**: A table against which a potential schema change is to be run
96
- - **Shadow table**: A copy of an existing primary table
97
- - **Audit table**: A table to store any updates/inserts/delete on a primary table
98
-
99
- ![how-it-works](diagrams/how-it-works.png)
100
-
101
-
102
- 1. Create an audit table to record changes made to the parent table.
103
- 2. Acquire a brief `ACCESS EXCLUSIVE` lock to add a trigger on the parent table (for inserts, updates, deletes) to the audit table.
104
- 3. Create a new shadow table and run ALTER/migration on the shadow table.
105
- 4. Copy all rows from the old table.
106
- 5. Build indexes on the new table.
107
- 6. Replay all changes accumulated in the audit table against the shadow table.
108
- - Delete rows in the audit table as they are replayed.
109
- 7. Once the delta (remaining rows) is ~20 rows, acquire an `ACCESS EXCLUSIVE` lock against the parent table within a transaction and:
110
- - swap table names (shadow table <> parent table).
111
- - update references in other tables (FKs) by dropping and re-creating the FKs with a `NOT VALID`.
112
- 8. Runs `ANALYZE` on the new table.
113
- 9. Validates all FKs that were added with `NOT VALID`.
114
- 10. Drop parent (now old) table (OPTIONAL).
115
-
116
100
  ## Prominent features
117
101
  - `pg-osc` supports when a column is being added, dropped or renamed with no data loss.
118
102
  - `pg-osc` acquires minimal locks throughout the process (read more below on the caveats).
119
103
  - Copies over indexes and Foreign keys.
120
104
  - Optionally drop or retain old tables in the end.
105
+ - Tune how slow or fast should replays be from the audit/log table ([Replaying larger workloads](#replaying-larger-workloads)).
121
106
  - Backfill old/new columns as data is copied from primary table to shadow table, and then perform the swap. [Example](#backfill-data)
122
107
  - **TBD**: Ability to reverse the change with no data loss. [tracking issue](https://github.com/shayonj/pg-osc/issues/14)
123
108
 
109
+ ## Load test
110
+
111
+ [More about the preliminary load test figures here](docs/load-test.md)
112
+
124
113
  ## Examples
125
114
 
126
115
  ### Renaming a column
127
116
  ```
117
+ export PGPASSWORD=""
128
118
  pg-online-schema-change perform \
129
119
  --alter-statement 'ALTER TABLE books RENAME COLUMN email TO new_email' \
130
120
  --dbname "postgres" \
131
121
  --host "localhost" \
132
122
  --username "jamesbond" \
133
- --password "" \
134
123
  ```
135
124
 
136
125
  ### Multiple ALTER statements
137
126
  ```
127
+ export PGPASSWORD=""
138
128
  pg-online-schema-change perform \
139
129
  --alter-statement 'ALTER TABLE books ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE; ALTER TABLE books RENAME COLUMN email TO new_email;' \
140
130
  --dbname "postgres" \
141
131
  --host "localhost" \
142
132
  --username "jamesbond" \
143
- --password "" \
144
133
  --drop
145
134
  ```
146
135
 
@@ -148,13 +137,30 @@ pg-online-schema-change perform \
148
137
  If the operation is being performed on a busy table, you can use `pg-osc`'s `kill-backend` functionality to kill other backends that may be competing with the `pg-osc` operation to acquire a lock for a brief while. The `ACCESS EXCLUSIVE` lock acquired by `pg-osc` is only held for a brief while and released after. You can tune how long `pg-osc` should wait before killing other backends (or if at all `pg-osc` should kill backends in the first place).
149
138
 
150
139
  ```
140
+ export PGPASSWORD=""
151
141
  pg-online-schema-change perform \
152
142
  --alter-statement 'ALTER TABLE books ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE;' \
153
143
  --dbname "postgres" \
154
144
  --host "localhost" \
155
145
  --username "jamesbond" \
156
- --password "" \
157
- --wait-time-for-lock=5 \
146
+ --wait-time-for-lock 5 \
147
+ --kill-backends \
148
+ --drop
149
+ ```
150
+
151
+ ### Replaying larger workloads
152
+ If you have a table with high write volume, the default replay iteration may not suffice. That is - you may see that `pg-osc` is replaying 1000 rows (`pull-batch-count`) in one go from the audit table. `pg-osc` also waits until the remaining row count (`delta-count`) in audit table is 20 before making the swap. You can tune these values to be higher for faster catch up on these kind of workloads.
153
+
154
+ ```
155
+ export PGPASSWORD=""
156
+ pg-online-schema-change perform \
157
+ --alter-statement 'ALTER TABLE books ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE;' \
158
+ --dbname "postgres" \
159
+ --host "localhost" \
160
+ --username "jamesbond" \
161
+ --pull-batch-count 2000
162
+ --delta-count 500
163
+ --wait-time-for-lock 5 \
158
164
  --kill-backends \
159
165
  --drop
160
166
  ```
@@ -183,7 +189,6 @@ pg-online-schema-change perform \
183
189
  --dbname "postgres" \
184
190
  --host "localhost" \
185
191
  --username "jamesbond" \
186
- --password "" \
187
192
  --copy-statement "/src/query.sql" \
188
193
  --drop
189
194
  ```
@@ -197,7 +202,6 @@ docker run --network host -it --rm shayonj/pg-osc:latest \
197
202
  --dbname "postgres" \
198
203
  --host "localhost" \
199
204
  --username "jamesbond" \
200
- --password "" \
201
205
  --drop
202
206
  ```
203
207
  ## Caveats
@@ -215,6 +219,29 @@ docker run --network host -it --rm shayonj/pg-osc:latest \
215
219
  - Can be fixed in future releases. Feel free to open a feature req.
216
220
  - Foreign keys are dropped & re-added to referencing tables with a `NOT VALID`. A follow on `VALIDATE CONSTRAINT` is run.
217
221
  - Ensures that integrity is maintained and re-introducing FKs doesn't acquire additional locks, hence the `NOT VALID`.
222
+ ## How does it work
223
+
224
+ - **Primary table**: A table against which a potential schema change is to be run
225
+ - **Shadow table**: A copy of an existing primary table
226
+ - **Audit table**: A table to store any updates/inserts/delete on a primary table
227
+
228
+ ![how-it-works](docs/how-it-works.png)
229
+
230
+
231
+ 1. Create an audit table to record changes made to the parent table.
232
+ 2. Acquire a brief `ACCESS EXCLUSIVE` lock to add a trigger on the parent table (for inserts, updates, deletes) to the audit table.
233
+ 3. Create a new shadow table and run ALTER/migration on the shadow table.
234
+ 4. Copy all rows from the old table.
235
+ 5. Build indexes on the new table.
236
+ 6. Replay all changes accumulated in the audit table against the shadow table.
237
+ - Delete rows in the audit table as they are replayed.
238
+ 7. Once the delta (remaining rows) is ~20 rows, acquire an `ACCESS EXCLUSIVE` lock against the parent table within a transaction and:
239
+ - swap table names (shadow table <> parent table).
240
+ - update references in other tables (FKs) by dropping and re-creating the FKs with a `NOT VALID`.
241
+ 8. Runs `ANALYZE` on the new table.
242
+ 9. Validates all FKs that were added with `NOT VALID`.
243
+ 10. Drop parent (now old) table (OPTIONAL).
244
+
218
245
  ## Development
219
246
 
220
247
  - Install ruby 3.0
File without changes
File without changes
Binary file
data/docs/load-test.md ADDED
@@ -0,0 +1,138 @@
1
+ # Preliminary Load Test
2
+
3
+ ## pg-osc: No downtime schema changes with 7K+ writes/s & 12k+ reads/s
4
+
5
+ This is a very basic load test performed with `pgbench` against a single instance PostgreSQL DB running on DigitialOcean with the following configuration:
6
+
7
+ - **128GB RAM**
8
+ - **32vCPU**
9
+ - **695GB Disk**
10
+ - Trasanction based connection pool with **500 pool limit**
11
+
12
+ Total time taken to run schema change: **<3mins**
13
+
14
+ ## Simulating load with pgbench
15
+
16
+ **Initialize**
17
+ ```
18
+ pgbench -p $PORT --initialize -s 20 -F 20 --foreign-keys --host $HOST -U $USERNAME -d $DB
19
+ ```
20
+
21
+ This creates bunch of pgbench tables. The table being used with `pg-osc` is `pgbench_accounts` which has FKs and also references by other tables with FKS, containing 2M rows.
22
+
23
+ **Begin**
24
+ ```
25
+ pgbench -p $PORT -j 72 -c 288 -T 500 -r --host $DB_HOST -U $USERNAME -d $DB
26
+ ```
27
+
28
+ ## Running pg-osc
29
+
30
+ Simple `ALTER` statement for experimentation purposes.
31
+
32
+ ```sql
33
+ ALTER TABLE pgbench_accounts ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE;
34
+ ```
35
+
36
+ **Execution**
37
+
38
+ ```bash
39
+ bundle exec bin/pg-online-schema-change perform \
40
+ -a 'ALTER TABLE pgbench_accounts ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE;' \
41
+ -d "pool" \
42
+ -p 25061
43
+ -h "..." \
44
+ -u "..." \
45
+ --pull-batch-count 2000 \
46
+ --delta-count 200
47
+ ```
48
+
49
+ ## Outcome
50
+
51
+ **pgbench results**
52
+
53
+ ```
54
+ number of transactions actually processed: 1060382
55
+ latency average = 144.874 ms
56
+ tps = 1767.057392 (including connections establishing)
57
+ tps = 1777.971823 (excluding connections establishing)
58
+ statement latencies in milliseconds:
59
+ 0.479 \set aid random(1, 100000 * :scale)
60
+ 0.409 \set bid random(1, 1 * :scale)
61
+ 0.247 \set tid random(1, 10 * :scale)
62
+ 0.208 \set delta random(-5000, 5000)
63
+ 3.136 BEGIN;
64
+ 4.243 UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
65
+ 4.488 SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
66
+ 71.017 UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
67
+ 46.689 UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;
68
+ 4.035 INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
69
+ 4.166 END;
70
+ ```
71
+
72
+ **Metrics**
73
+ ![load-test](load-test-1.png)
74
+
75
+ **New table structure**
76
+
77
+ Added `purchased` column.
78
+
79
+ ```
80
+ defaultdb=> \d+ pgbench_accounts;
81
+ Table "public.pgbench_accounts"
82
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
83
+ -----------+---------------+-----------+----------+---------+----------+--------------+-------------
84
+ aid | integer | | not null | | plain | |
85
+ bid | integer | | | | plain | |
86
+ abalance | integer | | | | plain | |
87
+ filler | character(84) | | | | extended | |
88
+ purchased | boolean | | | false | plain | |
89
+ Indexes:
90
+ "pgosc_st_pgbench_accounts_815029_pkey" PRIMARY KEY, btree (aid)
91
+ Foreign-key constraints:
92
+ "pgbench_accounts_bid_fkey" FOREIGN KEY (bid) REFERENCES pgbench_branches(bid)
93
+ Referenced by:
94
+ TABLE "pgbench_history" CONSTRAINT "pgbench_history_aid_fkey" FOREIGN KEY (aid) REFERENCES pgbench_accounts(aid)
95
+ Options: autovacuum_enabled=false, fillfactor=20
96
+ ```
97
+
98
+ **Logs**
99
+
100
+ <details>
101
+ <summary>Logs from pg-osc</summary>
102
+
103
+ ```json
104
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:01.147-05:00","v":0,"msg":"Setting up audit table","audit_table":"pgosc_at_pgbench_accounts_714a8b","version":"0.4.0"}
105
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:01.660-05:00","v":0,"msg":"Setting up triggers","version":"0.4.0"}
106
+ NOTICE: trigger "primary_to_audit_table_trigger" for relation "pgbench_accounts" does not exist, skipping
107
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:01.814-05:00","v":0,"msg":"Setting up shadow table","shadow_table":"pgosc_st_pgbench_accounts_714a8b","version":"0.4.0"}
108
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:02.169-05:00","v":0,"msg":"Running alter statement on shadow table","shadow_table":"pgosc_st_pgbench_accounts_714a8b","parent_table":"pgbench_accounts","version":"0.4.0"}
109
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:02.204-05:00","v":0,"msg":"Clearing contents of audit table before copy..","shadow_table":"pgosc_st_pgbench_accounts_714a8b","parent_table":"pgbench_accounts","version":"0.4.0"}
110
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:02.240-05:00","v":0,"msg":"Copying contents..","shadow_table":"pgosc_st_pgbench_accounts_714a8b","parent_table":"pgbench_accounts","version":"0.4.0"}
111
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:20.481-05:00","v":0,"msg":"Performing ANALYZE!","version":"0.4.0"}
112
+ INFO: analyzing "public.pgbench_accounts"
113
+ INFO: "pgbench_accounts": scanned 30000 of 166667 pages, containing 360000 live rows and 200 dead rows; 30000 rows in sample, 2000004 estimated total rows
114
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:21.078-05:00","v":0,"msg":"Replaying rows, count: 2000","version":"0.4.0"}
115
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:21.580-05:00","v":0,"msg":"Replaying rows, count: 2000","version":"0.4.0"}
116
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:22.022-05:00","v":0,"msg":"Replaying rows, count: 2000","version":"0.4.0"}
117
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:22.490-05:00","v":0,"msg":"Replaying rows, count: 2000","version":"0.4.0"}
118
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:22.866-05:00","v":0,"msg":"Replaying rows, count: 661","version":"0.4.0"}
119
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:23.212-05:00","v":0,"msg":"Replaying rows, count: 533","version":"0.4.0"}
120
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:23.512-05:00","v":0,"msg":"Replaying rows, count: 468","version":"0.4.0"}
121
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:23.809-05:00","v":0,"msg":"Remaining rows below delta count, proceeding towards swap","version":"0.4.0"}
122
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:23.809-05:00","v":0,"msg":"Performing swap!","version":"0.4.0"}
123
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:24.259-05:00","v":0,"msg":"Replaying rows, count: 449","version":"0.4.0"}
124
+ NOTICE: trigger "primary_to_audit_table_trigger" for relation "pgbench_accounts" does not exist, skipping
125
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:24.650-05:00","v":0,"msg":"Performing ANALYZE!","version":"0.4.0"}
126
+ INFO: analyzing "public.pgbench_accounts"
127
+ INFO: "pgbench_accounts": scanned 30000 of 32935 pages, containing 1821834 live rows and 6056 dead rows; 30000 rows in sample, 2000070 estimated total rows
128
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:24.941-05:00","v":0,"msg":"Validating constraints!","version":"0.4.0"}
129
+ NOTICE: table "pgosc_st_pgbench_accounts_714a8b" does not exist, skipping
130
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:26.159-05:00","v":0,"msg":"All tasks successfully completed","version":"0.4.0"}
131
+ ```
132
+
133
+ </details>
134
+
135
+
136
+ ## Conclusion
137
+
138
+ By tweaking `--pull-batch-count` to `2000` (replay 2k rows at once) and `--delta-count` to `200` (time to swap when remaining rows is <200), `pg-osc` was able to perform the schema change with no impact within very quick time. Depending on the database size and load on the table, you can further tune them to achieve desired impact. At some point this is going to plateau - I can imagine the replay factor not working quite well for say 100k commits/s workloads. So, YMMV.
@@ -3,8 +3,10 @@
3
3
  require "thor"
4
4
 
5
5
  module PgOnlineSchemaChange
6
+ PULL_BATCH_COUNT = 1000
7
+ DELTA_COUNT = 20
6
8
  class CLI < Thor
7
- desc "perform", "Perform the set of operations to safely apply the schema change with minimal locks"
9
+ desc "perform", "Safely apply schema changes with minimal locks"
8
10
  method_option :alter_statement, aliases: "-a", type: :string, required: true,
9
11
  desc: "The ALTER statement to perform the schema change"
10
12
  method_option :schema, aliases: "-s", type: :string, required: true, default: "public",
@@ -13,7 +15,7 @@ module PgOnlineSchemaChange
13
15
  method_option :host, aliases: "-h", type: :string, required: true, desc: "Server host where the Database is located"
14
16
  method_option :username, aliases: "-u", type: :string, required: true, desc: "Username for the Database"
15
17
  method_option :port, aliases: "-p", type: :numeric, required: true, default: 5432, desc: "Port for the Database"
16
- method_option :password, aliases: "-w", type: :string, required: true, desc: "Password for the Database"
18
+ method_option :password, aliases: "-w", type: :string, required: false, default: "", desc: "DEPRECATED: Password for the Database. Please pass PGPASSWORD environment variable instead."
17
19
  method_option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Emit logs in debug mode"
18
20
  method_option :drop, aliases: "-f", type: :boolean, default: false,
19
21
  desc: "Drop the original table in the end after the swap"
@@ -23,11 +25,19 @@ module PgOnlineSchemaChange
23
25
  desc: "Time to wait before killing backends to acquire lock and/or retrying upto 3 times. It will kill backends if --kill-backends is true, otherwise try upto 3 times and exit if it cannot acquire a lock."
24
26
  method_option :copy_statement, aliases: "-c", type: :string, required: false, default: "",
25
27
  desc: "Takes a .sql file location where you can provide a custom query to be played (ex: backfills) when pgosc copies data from the primary to the shadow table. More examples in README."
28
+ method_option :pull_batch_count, aliases: "-b", type: :numeric, required: false, default: PULL_BATCH_COUNT,
29
+ desc: "Number of rows to be replayed on each iteration after copy. This can be tuned for faster catch up and swap. Best used with delta-count."
30
+ method_option :delta_count, aliases: "-e", type: :numeric, required: false, default: DELTA_COUNT,
31
+ desc: "Indicates how many rows should be remaining before a swap should be performed. This can be tuned for faster catch up and swap, especially on highly volume tables. Best used with pull-batch-count."
26
32
 
27
33
  def perform
28
34
  client_options = Struct.new(*options.keys.map(&:to_sym)).new(*options.values)
29
-
30
35
  PgOnlineSchemaChange.logger(verbose: client_options.verbose)
36
+
37
+ PgOnlineSchemaChange.logger.warn("DEPRECATED: -w is deprecated. Please pass PGPASSWORD environment variable instead.") if client_options.password
38
+
39
+ client_options.password = ENV["PGPASSWORD"] || client_options.password
40
+
31
41
  PgOnlineSchemaChange::Orchestrate.run!(client_options)
32
42
  end
33
43
 
@@ -5,7 +5,7 @@ require "pg"
5
5
  module PgOnlineSchemaChange
6
6
  class Client
7
7
  attr_accessor :alter_statement, :schema, :dbname, :host, :username, :port, :password, :connection, :table, :drop,
8
- :kill_backends, :wait_time_for_lock, :copy_statement
8
+ :kill_backends, :wait_time_for_lock, :copy_statement, :pull_batch_count, :delta_count
9
9
 
10
10
  def initialize(options)
11
11
  @alter_statement = options.alter_statement
@@ -18,6 +18,8 @@ module PgOnlineSchemaChange
18
18
  @drop = options.drop
19
19
  @kill_backends = options.kill_backends
20
20
  @wait_time_for_lock = options.wait_time_for_lock
21
+ @pull_batch_count = options.pull_batch_count
22
+ @delta_count = options.delta_count
21
23
 
22
24
  handle_copy_statement(options.copy_statement)
23
25
  handle_validations
@@ -47,18 +47,6 @@ FUNC_CREATE_TABLE_ALL = <<~SQL
47
47
  EXECUTE format(
48
48
  'CREATE TABLE %s (LIKE %s including all)',
49
49
  newsource_table, source_table);
50
- for rec in
51
- SELECT oid, conname
52
- FROM pg_constraint
53
- WHERE contype = 'f'
54
- AND conrelid = source_table::regclass
55
- LOOP
56
- EXECUTE format(
57
- 'ALTER TABLE %s add constraint %s %s',
58
- newsource_table,
59
- rec.conname,
60
- pg_get_constraintdef(rec.oid));
61
- END LOOP;
62
50
  END
63
51
  $$;
64
52
  SQL
@@ -37,6 +37,9 @@ module PgOnlineSchemaChange
37
37
  Store.set(:audit_table_pk, "at_#{pgosc_identifier}_id")
38
38
  Store.set(:audit_table_pk_sequence, "#{audit_table}_#{audit_table_pk}_seq")
39
39
  Store.set(:shadow_table, "pgosc_st_#{client.table}_#{pgosc_identifier}")
40
+
41
+ Store.set(:referential_foreign_key_statements, Query.referential_foreign_keys_to_refresh(client, client.table))
42
+ Store.set(:self_foreign_key_statements, Query.self_foreign_keys_to_refresh(client, client.table))
40
43
  end
41
44
 
42
45
  def run!(options)
@@ -204,7 +207,7 @@ module PgOnlineSchemaChange
204
207
  return Query.run(client.connection, query, true)
205
208
  end
206
209
 
207
- sql = Query.copy_data_statement(client, shadow_table)
210
+ sql = Query.copy_data_statement(client, shadow_table, true)
208
211
  Query.run(client.connection, sql, true)
209
212
  ensure
210
213
  Query.run(client.connection, "COMMIT;") # commit the serializable transaction
@@ -221,7 +224,6 @@ module PgOnlineSchemaChange
221
224
  def swap!
222
225
  logger.info("Performing swap!")
223
226
 
224
- foreign_key_statements = Query.get_foreign_keys_to_refresh(client, client.table)
225
227
  storage_params_reset = primary_table_storage_parameters.empty? ? "" : "ALTER TABLE #{client.table} SET (#{primary_table_storage_parameters});"
226
228
 
227
229
  # From here on, all statements are carried out in a single
@@ -239,12 +241,13 @@ module PgOnlineSchemaChange
239
241
  sql = <<~SQL
240
242
  ALTER TABLE #{client.table} RENAME to #{old_primary_table};
241
243
  ALTER TABLE #{shadow_table} RENAME to #{client.table};
242
- #{foreign_key_statements}
244
+ #{referential_foreign_key_statements}
245
+ #{self_foreign_key_statements}
243
246
  #{storage_params_reset}
244
247
  DROP TRIGGER IF EXISTS primary_to_audit_table_trigger ON #{client.table};
245
248
  SQL
246
249
 
247
- Query.run(client.connection, sql)
250
+ Query.run(client.connection, sql, opened)
248
251
  ensure
249
252
  Query.run(client.connection, "COMMIT;")
250
253
  Query.run(client.connection, "SET statement_timeout = 0;")
@@ -270,6 +273,7 @@ module PgOnlineSchemaChange
270
273
  shadow_table_drop = shadow_table ? "DROP TABLE IF EXISTS #{shadow_table}" : ""
271
274
 
272
275
  sql = <<~SQL
276
+ DROP TRIGGER IF EXISTS primary_to_audit_table_trigger ON #{client.table};
273
277
  #{audit_table_drop};
274
278
  #{shadow_table_drop};
275
279
  #{primary_drop}
@@ -140,7 +140,7 @@ module PgOnlineSchemaChange
140
140
  end
141
141
  end
142
142
 
143
- def get_foreign_keys_to_refresh(client, table)
143
+ def referential_foreign_keys_to_refresh(client, table)
144
144
  references = get_all_constraints_for(client).select do |row|
145
145
  row["table_from"] == table && row["constraint_type"] == "f"
146
146
  end
@@ -158,12 +158,32 @@ module PgOnlineSchemaChange
158
158
  end.join
159
159
  end
160
160
 
161
- def get_foreign_keys_to_validate(client, table)
161
+ def self_foreign_keys_to_refresh(client, table)
162
162
  references = get_all_constraints_for(client).select do |row|
163
- row["table_from"] == table && row["constraint_type"] == "f"
163
+ row["table_on"] == table && row["constraint_type"] == "f"
164
164
  end
165
165
 
166
166
  references.map do |row|
167
+ add_statement = if row["definition"].end_with?("NOT VALID")
168
+ "ALTER TABLE #{row["table_on"]} ADD CONSTRAINT #{row["constraint_name"]} #{row["definition"]};"
169
+ else
170
+ "ALTER TABLE #{row["table_on"]} ADD CONSTRAINT #{row["constraint_name"]} #{row["definition"]} NOT VALID;"
171
+ end
172
+ add_statement
173
+ end.join
174
+ end
175
+
176
+ def get_foreign_keys_to_validate(client, table)
177
+ constraints = get_all_constraints_for(client)
178
+ referential_foreign_keys = constraints.select do |row|
179
+ row["table_from"] == table && row["constraint_type"] == "f"
180
+ end
181
+
182
+ self_foreign_keys = constraints.select do |row|
183
+ row["table_on"] == table && row["constraint_type"] == "f"
184
+ end
185
+
186
+ [referential_foreign_keys, self_foreign_keys].flatten.map do |row|
167
187
  "ALTER TABLE #{row["table_on"]} VALIDATE CONSTRAINT #{row["constraint_name"]};"
168
188
  end.join
169
189
  end
@@ -7,9 +7,6 @@ module PgOnlineSchemaChange
7
7
  extend Helper
8
8
 
9
9
  class << self
10
- PULL_BATCH_COUNT = 1000
11
- DELTA_COUNT = 20
12
-
13
10
  # This, picks PULL_BATCH_COUNT rows by primary key from audit_table,
14
11
  # replays it on the shadow_table. Once the batch is done,
15
12
  # it them deletes those PULL_BATCH_COUNT rows from audit_table. Then, pull another batch,
@@ -20,7 +17,7 @@ module PgOnlineSchemaChange
20
17
  loop do
21
18
  rows = rows_to_play
22
19
 
23
- raise CountBelowDelta if rows.count <= DELTA_COUNT
20
+ raise CountBelowDelta if rows.count <= client.delta_count
24
21
 
25
22
  play!(rows)
26
23
  end
@@ -28,7 +25,7 @@ module PgOnlineSchemaChange
28
25
 
29
26
  def rows_to_play(reuse_trasaction = false)
30
27
  select_query = <<~SQL
31
- SELECT * FROM #{audit_table} ORDER BY #{audit_table_pk} LIMIT #{PULL_BATCH_COUNT};
28
+ SELECT * FROM #{audit_table} ORDER BY #{audit_table_pk} LIMIT #{client.pull_batch_count};
32
29
  SQL
33
30
 
34
31
  rows = []
@@ -90,7 +87,7 @@ module PgOnlineSchemaChange
90
87
  SQL
91
88
  to_be_replayed << sql
92
89
 
93
- to_be_deleted_rows << "'#{row[primary_key]}'"
90
+ to_be_deleted_rows << "'#{row[audit_table_pk]}'"
94
91
  when "UPDATE"
95
92
  set_values = new_row.map do |column, value|
96
93
  "#{column} = '#{value}'"
@@ -103,14 +100,14 @@ module PgOnlineSchemaChange
103
100
  SQL
104
101
  to_be_replayed << sql
105
102
 
106
- to_be_deleted_rows << "'#{row[primary_key]}'"
103
+ to_be_deleted_rows << "'#{row[audit_table_pk]}'"
107
104
  when "DELETE"
108
105
  sql = <<~SQL
109
106
  DELETE FROM #{shadow_table} WHERE #{primary_key}=\'#{row[primary_key]}\';
110
107
  SQL
111
108
  to_be_replayed << sql
112
109
 
113
- to_be_deleted_rows << "'#{row[primary_key]}'"
110
+ to_be_deleted_rows << "'#{row[audit_table_pk]}'"
114
111
  end
115
112
  end
116
113
 
@@ -120,7 +117,7 @@ module PgOnlineSchemaChange
120
117
  return unless to_be_deleted_rows.count >= 1
121
118
 
122
119
  delete_query = <<~SQL
123
- DELETE FROM #{audit_table} WHERE #{primary_key} IN (#{to_be_deleted_rows.join(",")})
120
+ DELETE FROM #{audit_table} WHERE #{audit_table_pk} IN (#{to_be_deleted_rows.join(",")})
124
121
  SQL
125
122
  Query.run(client.connection, delete_query, reuse_trasaction)
126
123
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgOnlineSchemaChange
4
- VERSION = "0.4.0"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -6,12 +6,12 @@ require "ougai"
6
6
  require "pg_online_schema_change/version"
7
7
  require "pg_online_schema_change/helper"
8
8
  require "pg_online_schema_change/functions"
9
- require "pg_online_schema_change/cli"
10
9
  require "pg_online_schema_change/client"
11
10
  require "pg_online_schema_change/query"
12
11
  require "pg_online_schema_change/store"
13
12
  require "pg_online_schema_change/replay"
14
13
  require "pg_online_schema_change/orchestrate"
14
+ require "pg_online_schema_change/cli"
15
15
 
16
16
  module PgOnlineSchemaChange
17
17
  class Error < StandardError; end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_online_schema_change
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shayon Mukherjee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-22 00:00:00.000000000 Z
11
+ date: 2022-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ougai
@@ -204,9 +204,11 @@ files:
204
204
  - bin/console
205
205
  - bin/pg-online-schema-change
206
206
  - bin/setup
207
- - diagrams/how-it-works.excalidraw
208
- - diagrams/how-it-works.png
209
207
  - docker-compose.yml
208
+ - docs/how-it-works.excalidraw
209
+ - docs/how-it-works.png
210
+ - docs/load-test-1.png
211
+ - docs/load-test.md
210
212
  - lib/pg_online_schema_change.rb
211
213
  - lib/pg_online_schema_change/cli.rb
212
214
  - lib/pg_online_schema_change/client.rb