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 +4 -4
- data/.rspec +2 -1
- data/.rubocop_todo.yml +5 -5
- data/CHANGELOG.md +18 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile.lock +2 -2
- data/README.md +60 -33
- data/{diagrams → docs}/how-it-works.excalidraw +0 -0
- data/{diagrams → docs}/how-it-works.png +0 -0
- data/docs/load-test-1.png +0 -0
- data/docs/load-test.md +138 -0
- data/lib/pg_online_schema_change/cli.rb +13 -3
- data/lib/pg_online_schema_change/client.rb +3 -1
- data/lib/pg_online_schema_change/functions.rb +0 -12
- data/lib/pg_online_schema_change/orchestrate.rb +8 -4
- data/lib/pg_online_schema_change/query.rb +23 -3
- data/lib/pg_online_schema_change/replay.rb +6 -9
- data/lib/pg_online_schema_change/version.rb +1 -1
- data/lib/pg_online_schema_change.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 273557a58c9492e628277e6eed7b655233c47bb51b12a61ca3eaf3377557205a
|
4
|
+
data.tar.gz: 6ec45511249401c420cc3c61652a4ad18225917b644574fb58329819fb422ab1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 29f07b0af4c6a78de6945b6f43e271518f9c5953388e99dcc7582c4db865881ac5e5b8d0f1dff9a1215216ea534ad1e393fb268816e3799832911688b7abc14e
|
7
|
+
data.tar.gz: 0362f9c666c58acdb2fb63f1cd0208c719eb788555b218a57e2aee7bb9d98bbd9a7ff4d1e0ee0a56deec8e9679def1ae116fef919366e86ae30b2444d3379e6a
|
data/.rspec
CHANGED
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-
|
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:
|
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:
|
29
|
+
# Offense count: 67
|
30
30
|
# Configuration parameters: CountAsOne.
|
31
31
|
RSpec/ExampleLength:
|
32
32
|
Max: 55
|
33
33
|
|
34
|
-
# Offense count:
|
34
|
+
# Offense count: 24
|
35
35
|
RSpec/MultipleExpectations:
|
36
|
-
Max:
|
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
|
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.
|
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.
|
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
|
-
[![
|
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
|
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
|
-
--
|
157
|
-
--
|
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", "
|
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:
|
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
|
-
#{
|
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
|
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
|
161
|
+
def self_foreign_keys_to_refresh(client, table)
|
162
162
|
references = get_all_constraints_for(client).select do |row|
|
163
|
-
row["
|
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 <=
|
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 #{
|
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[
|
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[
|
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[
|
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 #{
|
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
|
@@ -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
|
+
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-
|
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
|