<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html" version="2.0">
    <channel>
        <title>Stories by James Coleman on Medium</title>
        <description>Stories by James Coleman on Medium</description>
        <link>https://medium.com/@james.coleman_25666?source=rss-4c9c573aa350------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*6OGkUPKV3JziKb70vcSpMg.jpeg</url>
            <title>Stories by James Coleman on Medium</title>
            <link>https://medium.com/@james.coleman_25666?source=rss-4c9c573aa350------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 05 Jun 2026 19:15:28 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@james.coleman_25666/feed" rel="self" type="application/rss+xml"/>
        <webMaster>yourfriends@medium.com</webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title>PostgreSQL at Scale: Saving Space (Basically) for Free</title>
            <link>https://medium.com/paypal-tech/postgresql-at-scale-saving-space-basically-for-free-d94483d9ed9a?source=rss-4c9c573aa350------2</link>
            <guid isPermaLink="false">https://medium.com/p/d94483d9ed9a</guid>
            <category>database</category>
            <category>scaling</category>
            <category>postgresql</category>
            <category>programming</category>
            <category>sql</category>
            <dc:creator>James Coleman</dc:creator>
            <pubDate>Mon, 28 Sep 2020 20:57:58 GMT</pubDate>
            <atom:updated>2021-08-06T14:00:13.779Z</atom:updated>
            <content:encoded>&lt;p&gt;Braintree Payments operates dozens of PostgreSQL clusters with over 100 terabytes of data. At this scale, even a few percentage points change in disk space growth rate can meaningfully impact the writable lifespan of a database cluster. Unfortunately, many ideas to save disk space require application changes and therefore need to be slotted into product timelines.&lt;/p&gt;&lt;p&gt;But today I want to focus on a technique that saved us approximately 10% of our disk space with very little effort beyond existing processes. In short, carefully choosing column order when creating a table can eliminate padding that would otherwise be&#160;needed.&lt;/p&gt;&lt;p&gt;This technique isn&#8217;t revolutionary: it&#8217;s been well-documented by 2ndQuadrant in &lt;a href="https://www.2ndquadrant.com/en/blog/on-rocks-and-sand/"&gt;On Rocks and Sand&lt;/a&gt;, EDB in &lt;a href="https://postgresrocks.enterprisedb.com/t5/Postgres-Gems/Data-Alignment-in-PostgreSQL/ba-p/2113"&gt;Data Alignment in PostgreSQL&lt;/a&gt;, GitLab in &lt;a href="https://docs.gitlab.com/ee/development/ordering_table_columns.html"&gt;Ordering Table Columns in PostgreSQL&lt;/a&gt;, the classic &lt;a href="https://stackoverflow.com/a/7431468"&gt;&#8220;Column Tetris&#8221;&lt;/a&gt; answer on a StackOverflow question, and I&#8217;m sure many more. What I hope we&#8217;re bringing to the table is tooling encoding these ideas so that you don&#8217;t have to re-invent the wheel (or apply the technique manually).&lt;/p&gt;&lt;p&gt;Below I&#8217;ll describe the rules and heuristics we apply to determine an ideal column ordering. But a list of rules sounds a lot like the definition for an algorithm. And that implies a problem space we can tackle at the systems, not people, level. Instead of sending a mass email to every engineer writing database DDL changes and expecting them to remember these rules, we authored a Ruby gem called &lt;a href="https://github.com/braintree/pg_column_byte_packer"&gt;pg_column_byte_packer&lt;/a&gt; to automate the solution in our development cycle. We&amp;#39;ll talk more about that soon, but first let&amp;#39;s take a more in-depth look a the problem&#160;space.&lt;/p&gt;&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*poRsrDHIA6In_GdAzVtCBQ.jpeg" /&gt;&lt;figcaption&gt;Photo by &lt;a href="https://unsplash.com/@pickawood?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Pickawood&lt;/a&gt; on&#160;&lt;a href="https://unsplash.com/?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;h3&gt;Data Alignment, Padding, and&#160;Waste&lt;/h3&gt;&lt;p&gt;PostgreSQL&#8217;s heap storage, much like fields in C-language structs, writes columns guaranteeing alignment boundaries. For example, a column having 8-byte alignment is guaranteed to start at a byte index evenly divisible by 8 (zero-indexed). The heap storage engine automatically introduces any padding necessary to maintain this alignment.&lt;/p&gt;&lt;p&gt;We can introspect all kinds of system behavior and objects using PostgreSQL&#8217;s catalog tables, and alignment is no exception. Each datatype is listed in pg_catalog.pg_type, and you can determine the alignment required for any data type in the typalign column of that catalog table. PostgreSQL&amp;#39;s documentation provides an excellent &lt;a href="https://www.postgresql.org/docs/current/catalog-pg-type.html"&gt;summary of how to interpret this&#160;column&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;What Can We Do About&#160;It?&lt;/h3&gt;&lt;p&gt;From a high-level perspective we can minimize the amount of space that will be lost to alignment padding by ordering each table&#8217;s columns in descending order of their data type&#8217;s alignment.&lt;/p&gt;&lt;p&gt;For example, suppose on our 64-bit system we have a table with two columns: a bigint column (which requires 8-byte alignment) and an integer column (which requires 4-byte alignment). If we put the integer column first we&amp;#39;ll have the following data layout that takes up 16&#160;bytes:&lt;/p&gt;&lt;iframe src="" width="0" height="0" frameborder="0" scrolling="no"&gt;&lt;a href="https://medium.com/media/fa91648efcd6be172826714faa976f42/href"&gt;https://medium.com/media/fa91648efcd6be172826714faa976f42/href&lt;/a&gt;&lt;/iframe&gt;&lt;p&gt;However if we put the bigint column first our data layout will only take up 12&#160;bytes:&lt;/p&gt;&lt;iframe src="" width="0" height="0" frameborder="0" scrolling="no"&gt;&lt;a href="https://medium.com/media/efb1858af43a1e152cc5ca9dab9bf793/href"&gt;https://medium.com/media/efb1858af43a1e152cc5ca9dab9bf793/href&lt;/a&gt;&lt;/iframe&gt;&lt;p&gt;But there are a few other cases we want to handle at the same&#160;time:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Variable length data types like TEXT can have variable alignment requirements depending on their size. While we obviously can&amp;#39;t look at the data when creating the table, we do want to take hints based on length constraints on the column, if&#160;any.&lt;/li&gt;&lt;li&gt;Binary (BYTEA data type) columns are similarly variable in length (and therefore variable alignment), we assume as a heuristic that binary data is usually &amp;quot;long&amp;quot;&#160;length.&lt;/li&gt;&lt;li&gt;NOT NULL columns are definitionally more likely to contain data than a random nullable column, so it makes sense to order them earlier (for faster unpacking during&#160;reads).&lt;/li&gt;&lt;li&gt;Columns with a DEFAULT are more likely to contain data (though slightly less so than NOT NULL), so it makes sense to order them earlier (also for faster unpacking during&#160;reads).&lt;/li&gt;&lt;li&gt;PRIMARY KEY columns not only always have data, but are also often the most frequently accessed columns (because they tend to be JOIN conditions), so we order then at the beginning of their alignment group.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Bundled Up in a Ruby&#160;Gem&lt;/h3&gt;&lt;p&gt;I mentioned earlier that we&#8217;ve incorporated this set of column ordering rules into our recently open-sourced Ruby Gem &lt;a href="https://github.com/braintree/pg_column_byte_packer"&gt;pg_column_byte_packer&lt;/a&gt;. We implemented two complementary approaches to try to solve the problem holistically.&lt;/p&gt;&lt;p&gt;First, we automatically patch ActiveRecord&#8217;s migration code to re-order columns on-the-fly when all of those columns are included in a single create_table (or, safe_create_table if you&amp;#39;re using our &lt;a href="https://github.com/braintree/pg_ha_migrations"&gt;pg_ha_migrations&lt;/a&gt; gem to maintain uptime guarantees when running migrations!) call.&lt;/p&gt;&lt;p&gt;Second, we provide an API to re-order the columns found in CREATE TABLE statements in a SQL file generated by PostgreSQL&amp;#39;s pg_dump&#160;utility.&lt;/p&gt;&lt;p&gt;Beginning to use the tool in your applications now will immediately benefit new tables. But we didn&#8217;t want to stop there, because we have many existing large tables! Of course re-ordering existing columns meant we needed to re-write tables. So we created entirely new databases and applied schemas files updated using the pg_dump SQL file modification feature described above. Finally we logically replicated all data to these new databases and transparently cutover from the old databases. This is also the foundation of how we achieve zero-downtime major version upgrades in PostgreSQL, but that&amp;#39;s a topic for a future&#160;post!&lt;/p&gt;&lt;p&gt;We hope many of you will benefit from our work here. And we&#8217;d also love to see any ideas you might have for improving it.&lt;/p&gt;&lt;img src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=d94483d9ed9a" width="1" height="1" alt=""&gt;&lt;hr&gt;&lt;p&gt;&lt;a href="https://medium.com/paypal-tech/postgresql-at-scale-saving-space-basically-for-free-d94483d9ed9a"&gt;PostgreSQL at Scale: Saving Space (Basically) for Free&lt;/a&gt; was originally published in &lt;a href="https://medium.com/paypal-tech"&gt;The PayPal Technology Blog&lt;/a&gt; on Medium, where people are continuing the conversation by highlighting and responding to this story.&lt;/p&gt;</content:encoded>
        </item>
        <item>
            <title>PostgreSQL at Scale: Database Schema Changes Without Downtime</title>
            <link>https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680?source=rss-4c9c573aa350------2</link>
            <guid isPermaLink="false">https://medium.com/p/20d3749ed680</guid>
            <category>programming</category>
            <category>postgresql</category>
            <category>database</category>
            <category>high-availability</category>
            <category>sql</category>
            <dc:creator>James Coleman</dc:creator>
            <pubDate>Fri, 01 Feb 2019 23:01:28 GMT</pubDate>
            <atom:updated>2026-04-10T17:56:09.925Z</atom:updated>
            <content:encoded>&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ifBarYCL0UMEBfss" /&gt;&lt;/figure&gt;&lt;p&gt;Braintree Payments uses PostgreSQL as its primary datastore. We rely heavily on the data safety and consistency guarantees a traditional relational database offers us, but these guarantees come with certain operational difficulties. To make things even more interesting, we allow zero scheduled functional downtime for our main payments processing services.&lt;/p&gt;&lt;p&gt;Several years ago we &lt;a href="https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/"&gt;published a blog post&lt;/a&gt; detailing some of the things we had learned about how to safely run &lt;a href="https://www.postgresql.org/docs/current/ddl.html"&gt;DDL&lt;/a&gt; (&lt;a href="https://en.wikipedia.org/wiki/Data_definition_language"&gt;data definition language&lt;/a&gt;) operations without interrupting our production API&#160;traffic.&lt;/p&gt;&lt;p&gt;Since that time PostgreSQL has gone through quite a few major upgrade cycles&#8202;&#8212;&#8202;several of which have added improved support for concurrent DDL. We&#8217;ve also further refined our processes. Given how much has changed, we figured it was time for a blog post&#160;redux.&lt;/p&gt;&lt;p&gt;In this post we&#8217;ll address the following topics:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="#cc22"&gt;Transactional DDL&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="#2456"&gt;Rollback Strategy&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="#f0d8"&gt;Locking&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="#bddc"&gt;Table Operations&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="#324f"&gt;Column Operations&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="#04b4"&gt;Index Operations&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="#00dc"&gt;Constraints&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="#6cfa"&gt;Enum Types&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="#57c1"&gt;Bonus: Library for Ruby on&#160;Rails&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;First, some&#160;basics&lt;/h3&gt;&lt;p&gt;For all code and database changes, we require&#160;that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Live code and schemas be forward-compatible with updated code and schemas: this allows us to roll out deploys gradually across a fleet of application servers and database clusters.&lt;/li&gt;&lt;li&gt;New code and schemas be backward-compatible with live code and schemas: this allows us to roll back any change to the previous version in the event of unexpected errors.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For all DDL operations we require&#160;that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Any exclusive locks acquired on tables or indexes be held for at most ~2&#160;seconds.&lt;/li&gt;&lt;li&gt;Rollback strategies &lt;a href="#2456"&gt;do not involve reverting the database schema to its previous&#160;version.&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Transactionality&lt;/h3&gt;&lt;p&gt;PostgreSQL supports transactional DDL. In most cases, you can execute multiple DDL statements inside an explicit database transaction and take an &#8220;all or nothing&#8221; approach to a set of changes. However, running multiple DDL statements inside a transaction has one serious downside: if you alter multiple objects, you&#8217;ll need to acquire exclusive locks on all of those objects in a single transactions. Because locks on multiple tables creates the possibility of deadlock and increases exposure to long waits, we do not combine multiple DDL statements into a single transaction. PostgreSQL will still execute each separate DDL statement transactionally; each statement will be either cleanly applied or fail and the transaction rolled&#160;back.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Concurrent index creation is a special case. Postgres disallows executing CREATE INDEX CONCURRENTLY inside an explicit transaction; instead Postgres itself manages the transactions. If for some reason the index build fails before completion, you may need to drop the index before retrying, though the index will still never be used for regular queries if it did not finish building successfully.&lt;/p&gt;&lt;h3&gt;Rollback Strategy&lt;/h3&gt;&lt;p&gt;Many database schema management tools assume that each schema change should include both a &#8220;forward&#8221; and &#8220;reverse&#8221; change definition. For example, a schema change that adds a table should be accompanied by the ability to undo that change (in this case by dropping the new&#160;table).&lt;/p&gt;&lt;p&gt;This strategy is appealing in development (e.g., &#8220;run one schema change, see if tests pass, iterate by reversing the change and trying again&#8221;), but we do not believe it&#8217;s appropriate for production deployments.&lt;/p&gt;&lt;p&gt;Although some schema changes are safe to reverse (e.g., dropping a just-created non-unique index), most possible schema changes are not safe to reverse. A few examples relative to database integrity:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Dropping a newly added column may result in data&#160;loss.&lt;/li&gt;&lt;li&gt;Re-adding a dropped unique index (or any other constraint) may fail because data may now exist that violates the constraint.&lt;/li&gt;&lt;li&gt;Dropping a enum value simply isn&#8217;t supported by Postgres (and wouldn&#8217;t be safe since it might be referenced by rows in the database).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These concerns are magnified if we also consider currently running application code (especially across multiple revisions, i.e., during a deploy). For example, the application may&#160;expect:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Indexes to be present for performant queries.&lt;/li&gt;&lt;li&gt;Constraints to&#160;hold.&lt;/li&gt;&lt;li&gt;Columns to be&#160;present.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For these reasons our deployment tooling and risk mitigation strategies do &lt;em&gt;not&lt;/em&gt; involve reverting the database schema to its previous version. Consequently, we do not define &#8220;undo&#8221; operations in our schema management tooling. Instead we concentrate on ensuring that operations are safe to apply while both old and new revisions of an application are running. In the rare case where we need to &#8220;undo&#8221; a schema change, we roll forward, rather than rolling back, by having an engineer write a new schema change and deploying that&#160;change.&lt;/p&gt;&lt;h3&gt;Locking&lt;/h3&gt;&lt;p&gt;PostgreSQL has &lt;a href="https://www.postgresql.org/docs/current/explicit-locking.html"&gt;many different levels of locking&lt;/a&gt;. We&#8217;re concerned primarily with the following table-level locks since DDL generally operates at these&#160;levels:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;ACCESS EXCLUSIVE: blocks all usage of the locked&#160;table.&lt;/li&gt;&lt;li&gt;SHARE ROW EXCLUSIVE: blocks concurrent DDL against and row modification (allowing reads) in the locked&#160;table.&lt;/li&gt;&lt;li&gt;SHARE UPDATE EXCLUSIVE: blocks concurrent DDL against the locked&#160;table.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: &#8220;Concurrent DDL&#8221; for these purposes includes VACUUM and ANALYZE operations.&lt;/p&gt;&lt;p&gt;All DDL operations generally necessitate acquiring one of these locks on the object being manipulated. For example, when you&#160;run:&lt;/p&gt;&lt;iframe src="" width="0" height="0" frameborder="0" scrolling="no"&gt;&lt;a href="https://medium.com/media/b1ba327e98b89a84461c915a075c77aa/href"&gt;https://medium.com/media/b1ba327e98b89a84461c915a075c77aa/href&lt;/a&gt;&lt;/iframe&gt;&lt;p&gt;PostgreSQL attempts to acquire an ACCESS EXCLUSIVE lock on the table foos. Attempting to acquire this lock &lt;strong&gt;causes all subsequent queries on this table to queue until the lock is released&lt;/strong&gt;. In practice your DDL operations can cause other queries to back up for as long as your longest running query takes to execute. Because arbitrarily long queueing of incoming queries is indistinguishable from an outage, we try to avoid any long-running queries in databases supporting our payments processing applications.&lt;/p&gt;&lt;p&gt;But sometimes a query takes longer than you expect. Or maybe you have a few special case queries that you already know will take a long time. PostgreSQL offers some additional runtime configuration options that allow us to guarantee query queueing backpressure doesn&#8217;t result in downtime.&lt;/p&gt;&lt;p&gt;Instead of relying on Postgres to lock an object when executing a DDL statement, we acquire the lock explicitly ourselves. This allows us to carefully control the time the queries may be queued. Additionally when we fail to acquire a lock within several seconds, we pause before trying again so that any queued queries can be executed without significantly increasing load. Finally, before we attempt lock acquisition, we query pg_locks&lt;a href="#a579"&gt;&#185;&lt;/a&gt; for any currently long running queries to avoid unnecessarily queueing queries for several seconds when it is unlikely that lock acquisition is going to&#160;succeed.&lt;/p&gt;&lt;p&gt;Starting with Postgres 9.3, you adjust the &lt;a href="https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT"&gt;lock_timeout&lt;/a&gt; parameter to control how long Postgres will allow for lock acquisition before returning without acquiring the lock. If you happen to be using 9.2 or earlier (and those are unsupported; you should upgrade!), then you can simulate this behavior by using the &lt;a href="https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT"&gt;statement_timeout&lt;/a&gt; parameter around an explicit LOCK &amp;lt;table&amp;gt; statement.&lt;/p&gt;&lt;p&gt;In many cases an ACCESS EXCLUSIVE lock need only be held for a very short period of time, i.e., the amount of time it takes Postgres to update its &amp;quot;catalog&amp;quot; (think metadata) tables. Below we&amp;#39;ll discuss the cases where a lower lock level is sufficient or alternative approaches for avoiding long-held locks that block SELECT/INSERT/UPDATE/DELETE.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Sometimes holding even an ACCESS EXCLUSIVE lock for something more than a catalog update (e.g., a full table scan or even rewrite) can be functionally acceptable when the table size is relatively small. We recommend testing your specific use case against realistic data sizes and hardware to see if a particular operation will be &amp;quot;fast enough&amp;quot;. On good hardware with a table easily loaded into memory, a full table scan or rewrite for thousands (possibly even 100s of thousands) of rows may be &amp;quot;fast&#160;enough&amp;quot;.&lt;/p&gt;&lt;h3&gt;Table operations&lt;/h3&gt;&lt;h3&gt;Create table&lt;/h3&gt;&lt;p&gt;In general, adding a table is one of the few operations we don&#8217;t have to think too hard about since, by definition, the object we&#8217;re &#8220;modifying&#8221; can&#8217;t possibly be in use yet.&#160;:D&lt;/p&gt;&lt;p&gt;While most of the attributes involved in creating a table do not involve other database objects, including a foreign key in your initial table definition will cause Postgres to acquire a SHARE ROW EXCLUSIVE lock against the referenced table blocking any concurrent DDL or row modifications. While this lock should be short-lived, it nonetheless requires the same caution as any other operation acquiring such a lock. We prefer to split these into two separate operations: create the table and then &lt;a href="#01cd"&gt;add the foreign&#160;key&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;Drop table&lt;/h3&gt;&lt;p&gt;Dropping a table requires an exclusive lock on that table. As long as the table isn&#8217;t in current use you can safely drop the table. Before allowing a DROP TABLE&#160;... to make its way into our production environments we require documentation showing when all references to the table were removed from the codebase. To double check that this is the case you can query PostgreSQL&amp;#39;s table statistics view pg_stat_user_tables&lt;a href="#2217"&gt;&#178;&lt;/a&gt; confirming that the returned statistics don&amp;#39;t change over the course of a reasonable length of&#160;time.&lt;/p&gt;&lt;h3&gt;Rename table&lt;/h3&gt;&lt;p&gt;While it&#8217;s unsurprising that a table rename requires acquiring an ACCESS EXCLUSIVE lock on the table, that&amp;#39;s far from our biggest concern. Unless the table is not being read from or written to, it&amp;#39;s very unlikely that your application code could safely handle a table being renamed underneath it.&lt;/p&gt;&lt;p&gt;We avoid table renames almost entirely. But if a rename is an absolute must, then a safe approach might look something like the following:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Create a new table with the same schema as the old&#160;one.&lt;/li&gt;&lt;li&gt;Backfill the new table with a copy of the data in the old&#160;table.&lt;/li&gt;&lt;li&gt;Use INSERT and UPDATE triggers on the old table to maintain parity in the new&#160;table.&lt;/li&gt;&lt;li&gt;Begin using the new&#160;table.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Other approaches involving views and/or RULEs may also be viable depending on the performance characteristics required.&lt;/p&gt;&lt;h3&gt;Column operations&lt;/h3&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: For column constraints (e.g., NOT NULL) or other constraints (e.g., EXCLUDES), see &lt;a href="#00dc"&gt;Constraints&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;Add column&lt;/h3&gt;&lt;p&gt;Adding a column to an existing table generally requires holding a short ACCESS EXCLUSIVE lock on the table while catalog tables are updated. But there are several potential gotchas:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Default values&lt;/strong&gt;: Introducing a default value at the same time of adding the column will cause the table to be locked while the default value in propagated for all rows in the table. Instead, you&#160;should:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Add the new column (without the default&#160;value).&lt;/li&gt;&lt;li&gt;Set the default value on the&#160;column.&lt;/li&gt;&lt;li&gt;Backfill all existing rows separately.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: In the recently release PostgreSQL 11, this is no longer the case for non-volatile default values. Instead adding a new column with a default value only requires updating catalog tables, and any reads of rows without a value for the new column will &lt;a href="https://www.depesz.com/2018/04/04/waiting-for-postgresql-11-fast-alter-table-add-column-with-a-non-null-default/"&gt;magically have it &#8220;filled in&#8221; on the&#160;fly&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Not-null constraints&lt;/strong&gt;: Adding a column with a NOT NULL constraint is only possible if there are no existing rows or a DEFAULT is also provided. If there are no existing rows, then the change is effectively equivalent to a catalog only change. If there are existing rows and you are also specifying a default value, then the same caveats apply as above with respect to default&#160;values.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Adding a column will cause all SELECT * FROM&#160;... style queries referencing the table to begin returning the new column. It is important to ensure that all currently running code safely handles new columns. To avoid this gotcha in our applications we require queries to avoid * expansion in favor of explicit column references.&lt;/p&gt;&lt;h3&gt;Change column&#160;type&lt;/h3&gt;&lt;p&gt;In the general case changing a column&#8217;s type requires holding an exclusive lock on a table while the entire table is rewritten with the new&#160;type.&lt;/p&gt;&lt;p&gt;There are a few exceptions:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Changing VARCHARto TEXT [9.1+] (more specifically: &lt;a href="https://www.postgresql.org/message-id/flat/E1PoFlG-0005Et-J1%40gemulon.postgresql.org"&gt;&amp;quot;when the old type is binary coercible to the new type and the using clause does not change the column contents&amp;quot;&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.postgresql.org/message-id/flat/E1PpCi5-0005St-N1%40gemulon.postgresql.org"&gt;&#8220;When the new type is an unconstrained domain over the old type&#8221;&lt;/a&gt;&#160;[9.1+].&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.depesz.com/2012/02/14/waiting-for-9-2-more-rewrite-less-alter-table-alter-types/"&gt;When increasing or removing a length or precision limit&lt;/a&gt;, e.g., VARCHAR(5)to VARCHAR(10)and VARCHAR(5)to VARCHAR&#160;[9.2+].&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Even though one of the exceptions above was added in 9.1, changing the type of an indexed column would always rewrite the index even if a table rewrite was avoided. In 9.2 any column data type that avoids a table rewrite also &lt;a href="https://www.postgresql.org/message-id/E1QipNf-0002ob-0f@gemulon.postgresql.org"&gt;avoids rewriting the associated indexes&lt;/a&gt; as long as Postgres can verify that the logical sort order remains the same (for example, a collation change on a text column will still require rebuilding indexes). If you&#8217;d like to confirm that your change won&#8217;t rewrite the table or any indexes, you can query pg_class&lt;a href="#e6e2"&gt;&#179;&lt;/a&gt; and verify the relfilenode column doesn&amp;#39;t&#160;change.&lt;/p&gt;&lt;p&gt;If you need to change the type of a column and one of the above exceptions doesn&#8217;t apply, then the safe alternative is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Add a new column new_&amp;lt;column&amp;gt;.&lt;/li&gt;&lt;li&gt;Dual write to both columns (e.g., with a BEFORE INSERT/UPDATE trigger).&lt;/li&gt;&lt;li&gt;Backfill the new column with a copy of the old column&#8217;s&#160;values.&lt;/li&gt;&lt;li&gt;Rename &amp;lt;column&amp;gt; to old_&amp;lt;column&amp;gt; and new_&amp;lt;column&amp;gt; inside a single transaction and explicit LOCK &amp;lt;table&amp;gt; statement.&lt;/li&gt;&lt;li&gt;Drop the old&#160;column.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Drop column&lt;/h3&gt;&lt;p&gt;It goes without saying that dropping a column is something that should be done with great care. Dropping a column requires an exclusive lock on the table to update the catalog but &lt;strong&gt;does not rewrite the table&lt;/strong&gt;. As long as the column isn&#8217;t in current use you can safely drop the column. It&#8217;s also important to confirm that the column is not referenced by any dependent objects that could be unsafe to drop. In particular, any indexes using the column should be dropped separately and safely with &lt;a href="#6bb7"&gt;DROP INDEX CONCURRENTLY&lt;/a&gt; since otherwise they will be automatically dropped along with the column under an ACCESS EXCLUSIVE lock. You can query pg_depend&lt;a href="#fcad"&gt;&#8308;&lt;/a&gt; for any dependent objects.&lt;/p&gt;&lt;p&gt;Before allowing a ALTER TABLE&#160;... DROP COLUMN&#160;... to make its way into our production environments we require documentation showing when all references to the column were removed from the codebase. This process allows us to safely roll back to the release prior to the one that dropped the&#160;column.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Dropping a column will require that you update all views, triggers, function, etc. that rely on that&#160;column.&lt;/p&gt;&lt;h3&gt;Index operations&lt;/h3&gt;&lt;h3&gt;Create index&lt;/h3&gt;&lt;p&gt;The standard form of CREATE INDEX&#160;... acquires an ACCESS EXCLUSIVE lock against the table being indexed while building the index using a single table scan. In contrast, the form CREATE INDEX CONCURRENTLY&#160;... acquires an SHARE UPDATE EXCLUSIVE lock but must complete two table scans (and hence is somewhat slower). This lower lock level allows reads and writes to continue against the table while the index is&#160;built.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Caveats&lt;/strong&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Multiple concurrent index creations on a single table will not return from either CREATE INDEX CONCURRENTLY&#160;... statement until the slowest one completes. In addition &lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=c98763bf51bf"&gt;until Postgres 14+&lt;/a&gt; the various phases building the index concurrently each wait on transactions held open by other such concurrent operations.&lt;/li&gt;&lt;li&gt;CREATE INDEX CONCURRENTLY&#160;... may not be executed inside of a transaction but does maintain transactions internally. Prior to Postgres 14 this always involved holding open a transaction preventing auto-vacuums (against any table in the system) from cleaning up dead tuples introduced after the index build began until the index operation returned. If you have a table with a large volume of updates (particularly bad if to a very small table) this could result in extremely sub-optimal query execution. In Postgres 14+ VACUUM is &lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=d9d076222"&gt;able to ignore concurrent index operations&lt;/a&gt; on other tables so long as they contain only bare columns (no expressions) and are not partial&#160;indexes.&lt;/li&gt;&lt;li&gt;CREATE INDEX CONCURRENTLY&#160;... must wait for all transactions using the table to complete before returning.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Drop index&lt;/h3&gt;&lt;p&gt;The standard form of DROP INDEX&#160;... acquires an ACCESS EXCLUSIVE lock against the table with the index while removing the index. For small indexes this may be a short operation. For large indexes, however, file system unlinking and disk flushing can take a significant amount of time. In contrast, the form DROP INDEX CONCURRENTLY&#160;... acquires a SHARE UPDATE EXCLUSIVE lock to perform these operations allowing reads and writes to continue against the table while the index is&#160;dropped.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Caveats&lt;/strong&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;DROP INDEX CONCURRENTLY&#160;... cannot be used to drop any index that supports a constraint (e.g., PRIMARY KEY or&#160;UNIQUE).&lt;/li&gt;&lt;li&gt;DROP INDEX CONCURRENTLY&#160;... may not be executed inside of a transaction but does maintain transactions internally. Prior to Postgres 14 this always involved holding open a transaction preventing auto-vacuums (against any table in the system) from cleaning up dead tuples introduced after the index build began until the index operation returned. If you have a table with a large volume of updates (particularly bad if to a very small table) this could result in extremely sub-optimal query execution. In Postgres 14+ VACUUM is &lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=d9d076222"&gt;able to ignore concurrent index operations&lt;/a&gt; on other tables so long as they contain only bare columns (no expressions) and are not partial&#160;indexes.&lt;/li&gt;&lt;li&gt;DROP INDEX CONCURRENTLY&#160;... must wait for all transactions using the table to complete before returning.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: DROP INDEX CONCURRENTLY&#160;... was &lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=8cb53654dbdb4c386369eb988062d0bbb6de725e"&gt;added in Postgres 9.2&lt;/a&gt;. If you&amp;#39;re still running 9.1 or prior, you can achieve somewhat similar results by marking the index as invalid and not ready for writes, flushing buffers with the pgfincore extension, and the dropping the&#160;index.&lt;/p&gt;&lt;h3&gt;Rename index&lt;/h3&gt;&lt;p&gt;ALTER INDEX&#160;... RENAME TO&#160;... requires an ACCESS EXCLUSIVE lock on the index blocking reads from and writes to the underlying table. However a &lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=1b5d797cd4f7133ff0d18e123fcf41c67a5a7b0b"&gt;recent commit&lt;/a&gt; expected to be a part of Postgres 12 lowers that requirement to SHARE UPDATE EXCLUSIVE.&lt;/p&gt;&lt;h3&gt;Reindex&lt;/h3&gt;&lt;p&gt;REINDEX INDEX&#160;... requires an ACCESS EXCLUSIVE lock on the index blocking reads from and writes to the underlying table. Instead we use the following procedure:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Create a new index &lt;a href="#b2f4"&gt;concurrently&lt;/a&gt; that duplicates the existing index definition.&lt;/li&gt;&lt;li&gt;Drop the old index &lt;a href="#6bb7"&gt;concurrently&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href="#9b1f"&gt;Rename&lt;/a&gt; the new index to match the original index&#8217;s&#160;name.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;However in Postgres 12+ &lt;a href="https://paquier.xyz/postgresql-2/postgres-12-reindex-concurrently/"&gt;CONCURRENTLY support has been added to &lt;/a&gt;&lt;a href="https://paquier.xyz/postgresql-2/postgres-12-reindex-concurrently/"&gt;REINDEX&lt;/a&gt; which essentially implemented the procedure outline above into a single command. The same caveats apply as the CONCURRENTLY variants of CREATE INDEX and DROP&#160;INDEX.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: If the index you need to rebuild backs a constraint, remember to re-add the constraint as well (subject to all of the &lt;a href="#00dc"&gt;caveats we&#8217;ve documented&lt;/a&gt;.)&lt;/p&gt;&lt;h3&gt;Constraints&lt;/h3&gt;&lt;h3&gt;NOT NULL Constraints&lt;/h3&gt;&lt;p&gt;Removing an existing not-null constraint from a column requires an exclusive lock on the table while a simple catalog update is performed.&lt;/p&gt;&lt;p&gt;In contrast, adding a not-null constraint to an existing column requires an exclusive lock on the table while a full table scan verifies that no null values exist. Instead you&#160;should:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Add a &lt;a href="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS"&gt;CHECK constraint&lt;/a&gt; requiring the column be not-null with ALTER TABLE &amp;lt;table&amp;gt; ADD CONSTRAINT &amp;lt;name&amp;gt; CHECK (&amp;lt;column&amp;gt; IS NOT NULL) NOT VALID;. The NOT VALID tells Postgres that it doesn&amp;#39;t need to scan the entire table to verify that all rows satisfy the condition.&lt;/li&gt;&lt;li&gt;Manually verify that all rows have non-null values in your&#160;column.&lt;/li&gt;&lt;li&gt;Validate the constraint with ALTER TABLE &amp;lt;table&amp;gt; VALIDATE CONSTRAINT &amp;lt;name&amp;gt;;. With this statement PostgreSQL will block acquisition of other EXCLUSIVE locks for the table, but will not block reads or&#160;writes.&lt;/li&gt;&lt;li&gt;On Postgres 12 and following, additionally proceed to ALTER TABLE &amp;lt;table&amp;gt; ALTER COLUMN &amp;lt;column&amp;gt; SET NOT NULL. According to the &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html"&gt;docs&lt;/a&gt; &#8220;if a valid CHECK constraint is found which proves no NULL can exist, then the table scan is skipped&#8221;.&lt;/li&gt;&lt;/ol&gt;&lt;h3&gt;Foreign keys&lt;/h3&gt;&lt;p&gt;ALTER TABLE&#160;... ADD FOREIGN KEY requires a SHARE ROW EXCLUSIVE lock (&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=0ef0396ae1687bf738d4703773d55467c36b2bcd"&gt;as of 9.5&lt;/a&gt;) &lt;em&gt;on both the altered and referenced tables&lt;/em&gt;. While this won&amp;#39;t block SELECT queries, blocking row modification operations for a long period of time is equally unacceptable for our transaction processing applications.&lt;/p&gt;&lt;p&gt;To avoid that long-held lock you can use the following process:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;ALTER TABLE&#160;... ADD FOREIGN KEY&#160;... NOT VALID: Adds the foreign key and begins enforcing the constraint for all new INSERT/UPDATE statements but does not validate that all existing rows conform to the new constraint. This operation still requires SHARE ROW EXCLUSIVE locks, but the locks are only briefly&#160;held.&lt;/li&gt;&lt;li&gt;ALTER TABLE&#160;... VALIDATE CONSTRAINT &amp;lt;constraint&amp;gt;: This operation checks all existing rows to verify they conform to the specified constraint. Validation requires a SHARE UPDATE EXCLUSIVE so may run concurrently with row reading and modification queries.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Check constraints&lt;/h3&gt;&lt;p&gt;ALTER TABLE&#160;... ADD CONSTRAINT&#160;... CHECK (...) requires an ACCESS EXCLUSIVE lock. However, as with foreign keys, Postgres supports breaking the operation into two&#160;steps:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;ALTER TABLE&#160;... ADD CONSTRAINT&#160;... CHECK (...) NOT VALID: Adds the check constraint and begins enforcing it for all new INSERT/UPDATE statements but does not validate that all existing rows conform to the new constraint. This operation still requires an ACCESS EXCLUSIVE lock.&lt;/li&gt;&lt;li&gt;ALTER TABLE&#160;... VALIDATE CONSTRAINT &amp;lt;constraint&amp;gt;: This operation checks all existing rows to verify they conform to the specified constraint. Validation requires a SHARE UPDATE EXCLUSIVE on the altered table so may run concurrently with row reading and modification queries. A ROW SHARE lock is held on the reference table which will block any operations requiring exclusive locks while validating the constraint.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Uniqueness constraints&lt;/h3&gt;&lt;p&gt;ALTER TABLE&#160;... ADD CONSTRAINT&#160;... UNIQUE (...) requires an ACCESS EXCLUSIVE lock. However, Postgres supports breaking the operation into two&#160;steps:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Create a unique index &lt;a href="#b2f4"&gt;concurrently&lt;/a&gt;. This step will immediately enforce uniqueness, but if you need a declared constraint (or a primary key), then continue to add the constraint separately.&lt;/li&gt;&lt;li&gt;Add the constraint using the already existing index with ALTER TABLE&#160;... ADD CONSTRAINT&#160;... UNIQUE USING INDEX &amp;lt;index&amp;gt;. Adding the constraint still requires an ACCESS EXCLUSIVE lock, but the lock will only be held for fast catalog operations.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: If you specify PRIMARY KEY instead of UNIQUE then any nullable columns in the index will be made NOT NULL. This requires a full table scan which currently can&amp;#39;t be avoided. See &lt;a href="#09eb"&gt;NOT NULL Constraints&lt;/a&gt; for more&#160;details.&lt;/p&gt;&lt;h3&gt;Exclusion constraints&lt;/h3&gt;&lt;p&gt;ALTER TABLE&#160;... ADD CONSTRAINT&#160;... EXCLUDE USING&#160;... requires an ACCESS EXCLUSIVE lock. Adding an exclusion constraint builds the supporting index, and, unfortunately, there is currently no support for using an existing index (as you can do with a &lt;a href="#62d6"&gt;unique constraint&lt;/a&gt;).&lt;/p&gt;&lt;h3&gt;Enum Types&lt;/h3&gt;&lt;p&gt;CREATE TYPE &amp;lt;name&amp;gt; AS (...) and DROP TYPE &amp;lt;name&amp;gt; (after verifying there are no existing usages in the database) can both be done safely without unexpected locking.&lt;/p&gt;&lt;h3&gt;Modifying enum&#160;values&lt;/h3&gt;&lt;p&gt;ALTER TYPE &amp;lt;enum&amp;gt; RENAME VALUE &amp;lt;old&amp;gt; TO &amp;lt;new&amp;gt; was &lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=0ab9c56d0fe3acc9d4717a9cbac6ef3369275b90"&gt;added in Postgres 10&lt;/a&gt;. This statement does not require locking tables which use the enum&#160;type.&lt;/p&gt;&lt;h3&gt;Deleting enum&#160;values&lt;/h3&gt;&lt;p&gt;Enums are stored internally as integers and there is no support for gaps in the valid range, removing a value would currently shifting values and rewriting all rows using those values. PostgreSQL does not currently support removing values from an existing enum&#160;type.&lt;/p&gt;&lt;h3&gt;Announcing Pg_ha_migrations for Ruby on&#160;Rails&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;We&#8217;re also excited to announce&lt;/strong&gt; that we have open-sourced our internal library &lt;a href="https://github.com/braintree/pg_ha_migrations"&gt;pg_ha_migrations&lt;/a&gt;. This Ruby gem enforces DDL safety in projects using Ruby on Rails and/or ActiveRecord with an emphasis on explicitly choosing trade-offs and avoiding unnecessary magic (and the corresponding surprises). You can read more in the project&#8217;s &lt;a href="https://github.com/braintreeps/pg_ha_migrations/blob/master/README.md"&gt;README&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;Footnotes&lt;/h3&gt;&lt;p&gt;[1] You can find active long-running queries and the tables they lock with the following query:&lt;/p&gt;&lt;iframe src="" width="0" height="0" frameborder="0" scrolling="no"&gt;&lt;a href="https://medium.com/media/90ee0e73f1d666273b12ad4df0ce2dfd/href"&gt;https://medium.com/media/90ee0e73f1d666273b12ad4df0ce2dfd/href&lt;/a&gt;&lt;/iframe&gt;&lt;p&gt;[2] You can see PostgreSQL&#8217;s internal statistics about table accesses with the following query:&lt;/p&gt;&lt;iframe src="" width="0" height="0" frameborder="0" scrolling="no"&gt;&lt;a href="https://medium.com/media/571d5801ea7602f91f4c6cf98361de39/href"&gt;https://medium.com/media/571d5801ea7602f91f4c6cf98361de39/href&lt;/a&gt;&lt;/iframe&gt;&lt;p&gt;[3] You can see if DDL causes a relation to be rewritten by seeing if the relfilenode value changes after running the statement:&lt;/p&gt;&lt;iframe src="" width="0" height="0" frameborder="0" scrolling="no"&gt;&lt;a href="https://medium.com/media/4526240441c8b45733d22b7b04326d48/href"&gt;https://medium.com/media/4526240441c8b45733d22b7b04326d48/href&lt;/a&gt;&lt;/iframe&gt;&lt;p&gt;[4] You can find objects (e.g., indexes) that depend on a specific column by running the statement:&lt;/p&gt;&lt;iframe src="" width="0" height="0" frameborder="0" scrolling="no"&gt;&lt;a href="https://medium.com/media/5dfc04bc00958bcd9a44f3568c73a49c/href"&gt;https://medium.com/media/5dfc04bc00958bcd9a44f3568c73a49c/href&lt;/a&gt;&lt;/iframe&gt;&lt;img src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=20d3749ed680" width="1" height="1" alt=""&gt;&lt;hr&gt;&lt;p&gt;&lt;a href="https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680"&gt;PostgreSQL at Scale: Database Schema Changes Without Downtime&lt;/a&gt; was originally published in &lt;a href="https://medium.com/paypal-tech"&gt;The PayPal Technology Blog&lt;/a&gt; on Medium, where people are continuing the conversation by highlighting and responding to this story.&lt;/p&gt;</content:encoded>
        </item>
    </channel>
</rss>