Changing your huge table's data types in production
CC Attribution 2.0 Belgium:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor.
Angelakos, Jimmy
You have a huge table, and it is necessary to change a column's data type, but your database has to keep running with no downtime. What do you do? Here's one way to perform this change, in as unobtrusive a manner as possible while your table keeps serving users, by avoiding long DDL table locks and leveraging procedural transaction control.
 Hello, virtual FOSDEM. My name is Jimmy Angelacos and I work as a senior Postgres architect at EDB. And today we're going to talk about changing your huge tables, data types and production. What is the motivation for this talk? First of all, we appear to be still in the era of big data, whatever that means, which means that along with everything else, Postgres has been seeing more heavy use with bigger tables and bigger data sets. Also Postgres performance has allowed this because it keeps getting better by the day. So you may find yourself in a situation where your database is facing rapid growth and you need to find a way to deal with that. So perhaps your growth is too rapid and you need to mitigate some decisions that you've taken early on. So why would you want to change data types on your database tables, especially if they're in production? First of all, you may have an incorrect data type. So something which was not suitable for what you wanted, like you have set a limit for VARCAR, which is not enough for your purposes. So you want to change it to variable length text. Secondly, you may want to change a non-optimal data type that you entered in your table. So for example, an ID column that is text that takes up nine bytes for an integer column that stores this ID for only four bytes, which will help your table scale better. Also, you may come across the situation of running out of IDs. If you have an integer ID column, the maximum limit is 2.1 billion, a little bit over that. So you may come across the situation of ID exhaustion. And in that case, what do you do? How do you keep your application running? Of course, you know that you can change types. You can change types only if they are compatible, though. So what you need is for data types that you, for the old data type to be binary-corrosable to the new type. So for example, XML can be converted to text very easily without any conversion function needed to be invoked. However, text to XML conversion requires a function to be run. So they are not binary-corrosable in that direction. And you can also have binary compatible columns and data types that have exactly the same internal representation in Postgres, such as text and VARCAR. So the way you do it is you have the alter table, alter type statement. It's a dvl command that lets you alter table, alter the column name to type the data type you want. You may also need to use the using expression if there is no implicit cast between the first and the second type, which needs that you may have to drop the default value for that column, run the alter table statement, and then add another default for the new data type. And this requires indexes to be rebuilt, even if the table doesn't have to get rewritten. So what is the problem here exactly? Alter table, alter column requires an access exclusive lock in order to change types. So that means that nothing can take place on that table. No reads and no writes are allowed to any other transaction. And this effectively stops us from using the table in production, which is a bad thing. Moreover, if the old data type and the new data type are not binary courseable between them, then you will have to rewrite the entire table, which is of course slow and requires double the disk space. So let us look at one possible scenario that you may encounter. Let's say that you have a huge table in production that is 1.7 billion rows. And you have a primary key, which is integer. So because of rapid growth, your table that is now 1.7 billion rows can become 2.1 billion rows very soon. So you find that big integer, which doesn't have the 2.1 billion limit, but is much larger, can be the solution for your problem. However, integer and big int are not binary compatible, because big int is eight bytes and integer is four bytes. So you have to rewrite the table. So how do you avoid having to lock the table in production and take everything down? So if you cannot take a maintenance window in order to perform this lengthy operation, then you need a concurrent solution. And let's look at one possible concurrent solution that can work. So first off, we add a new big int column to the table. Then we write a procedure that copies values from the old primary key column to the new big int column. And we do it in batches so that we don't affect the performance of the system too much. At the same time, we need to keep using the table and keep being able to write to the table. So we need to write a trigger that replicates the changes from the old column, which your application is aware of, to the new column that is practically invisible to your application. After we have treated the entire table and we have copied everything from the old column into the new big int column, we can drop the old column and rename the new one to the name of the old column. And then we make it the primary key. So let's look at a few details on what exactly needs to take place here. One detail is that we need to create a sequence for the new primary key. We also need to create the new primary key index. And after the conversion, we had best perform all of the DDL that does the column dropping and renaming in one transaction in order to be safe. And also keeping it in one transaction, Postgres tries to execute it as fast as possible. So we will encounter the minimum possible locking and or blocking of other operations by other users of our database. So the test system that was used to test this procedure was a laptop basically. And we created a table with 1.7 billion rows of 170 bytes each. So let's see how we create this example data. We create a table called large table with an integer column and a text content column. We create the table and we insert into the table our Lorem Ipsum to populate the content column. And then we generate series from 1 to 1.7 billion as we want 1.7 billion rows in the table. The insert takes quite a bit of time. It takes 32 minutes to generate this. It's not too bad because we haven't created any indexes yet. So we create a sequence for our large table ID column starting at the very next number after 1.7 billion. We set the default for the ID column to be this sequence. And then we create the index for the primary key. This takes 26 minutes on the test rig. So make of that what you will. And then we need to add this unique index as the primary key constraints to the table. So alter table, large table. We add the primary key using the index we have just created. And that takes a very short time. So let's say that now our data is in production and our table looks like this. It has a primary key and a sequence and an integer ID column. And we find that the size of our table is 265 gigs. So not small by any means. And the content of our table now looks like this. There is an ID from 1 to 1.7 billion and some content. If we select the number of live tuples we can see from pgstatusertables. For a large table we have 1.7 billion rows. First step is to add the new column. And if we make sure that the new column has a default of 0 then that can be an instantaneous operation because it is non-volatile. So alter table, large table, add column ID underscore new. Big integer is what we want this time around. We want the column to be not null and we set a default of 0. And that takes a very short time like 13 milliseconds. Next we need to build the trigger function to replicate the changes that are coming into our table while our conversion script is running. That copies everything from the old ID column to the new ID column. So we create the function large table trigger function returns trigger. And the only thing it does is when the value changes in the table we make sure that ID new takes the value of ID. So we create the function and then we need to add the trigger to the table that runs this function. So create trigger large table trigger. Before insert or update on large table because we want to replicate the changes the new column for each row execute the function large table trigger function. So now anything that changes in our table while we're running the conversion procedure is going to affect the new column as well. The conversion procedure, I'm sorry if the text is a bit cramped but we'll look at it in detail. We define a cursor for select ID from large table because we want to examine every single ID of that table. We define a batch size so we're going to do it in batches of 100,000 rows at a time. So we begin a loop of updating our large table and setting ID new equals ID for each one of the rows that we've selected. We increment our counter to determine where we are in the batch and every 100,000 rows so count modulo batch size equals 0 then we commit. So we don't commit with every row we commit with every 100,000 rows for performance reasons. That ends the loop and after we're done we commit and the procedure is over. Now what is important here is that we weren't able to do this with functions so we are taking advantage of the fact that procedures in Postgres offer us transactional control and we can do things such as begin transactions, commit them at will, rollback and so on. So what if we want this huge task to give us some progress indicator so we can add in the loop if count modulo batch size multiplied by 10 then raise notice rows done. So it will tell us every million rows 10 times our batch of 100,000 that the number of rows that have already been processed by the script. So now it's time to do it. We call the large table sync procedure and we see that it's processing 1 million rows, 2 million rows, 3 million rows and so on. Now to check that our procedure isn't actually blocking anything and users are not prevented from using the table let's select something from our large table, a random selection of one row and we use the backslash watch command in psql to run it every second. So every second we get an id from the table and another id the next second and so on. So we keep it running and we see that because it runs it means we're not blocking our users from reading from the table and also they're not prevented from writing to the table. So now we wait and seven hours later we see that our procedure has completed. It is done processing 1.7 billion rows and it took seven hours and six seconds on the test rig which is not too bad for something which is running concurrently and remember the purpose of this procedure is not to run fast, it is not to overpower your system and affect your performance so negatively while you're doing it that nobody else is able to use your table. So what does our table look like now? It has an id column, the content that it had and an id new column which is identical but is big integer instead of integer. Now we need to create the index for our primary key. So create unique index concurrently, we name it large table id new index on our large table and we created on the new id column that is big int. So index creation takes an hour and 17 minutes and now we haven't really made any changes to the table so it's the time to run the ddl all at once as we said so we're going to use an anonymous code block to execute all of it. So we need to find the new start for our sequence so we select the max id from large table into the variable new start then we create a sequence and we concatenate new start to the end of that statement that we then execute so create sequence large table with that number then we alter table to set the sequence to be the default for our column id new and then we drop the id column rename id new to id and then we add constraints the primary key using the index we just created in the last step we drop the trigger and we commit at the moment we commit all of the ddl is going to execute at the same time and it shouldn't block the table for very long at all and it shouldn't have any other negative consequences so we run it and we see alter table add constraint using index is a warning that postgres throws and says it will rename the index from large table id new index to large table id primary key which is fine and it does it and it only takes 451 milliseconds so less than half a second on our test rig and now we're all done we have changed our huge table uh primary key from integer to big integer without affecting the operation of our system so thank you very much and this is the background photo that was used uh you can find me on twitter and i am open to your questions now thanks very much for listening all right i assume that we are going to have the broadcast in any moment i just told you now to avoid this awkward silence and so we have a few questions thank you so much jimmy for this specific i work around so you came with this idea probably from a life uh existing issue right can you tell us whether there was something you just wanted to study or or this is a real problem that you faced yeah yeah it's a real life problem um people start using um primary keys with hint and it actually happens that sometimes they run out of integers because of the number of transactions or number of customers that they add in their database so it is a real world problem nice not not just a case study so good so a question coming from the audience is it advisable to change a value in a column while changing the data type so for instance changing text to a boolean and set all empty fields to false sorry i don't understand the idea is that you change the data type yeah but you also change the values when you're doing that all the same time oh you can do that well if you if you set your conversion function to change everything and also the trigger that deals with the incoming values then sure you can do that whether it's something that you want to do if you want a consistency in your application is a different story because as as we said this example is on running live database so maybe your application is not expecting the values to be changing right yeah and also because you're mentioning is a live database so there are new integers that are coming in the primary key there is a question or a comment about the the sequence of the big integer that you're creating when you're changing whether it is advisable or not just set it to a higher value so that you avoid any race conditions while doing the switch right yeah so in this case i just selected the max integer and because nobody was writing into my table in my laptop then it didn't change but yeah certainly it's a good idea to skip a few numbers ahead when you're creating the sequence good okay thanks for clarifying that there is another question here saying how big the table became after you updated all the rules um it grew but not to twice its size so it's a bit better than rewriting the entire table and also if you have auto vacuum running because you have multiple transactions then it's able to clean up a bit better than doing it in one huge transaction so any maintenance you have to do like after you do all this change because probably you would like to do a vacuum full but it's not possible of course and it's not kind of a advice you just run vacuum full in such a large table but yeah it's probably a good idea to analyze after you've changed your data type analyze okay yeah that's good for the statistics i also see a question about um uh declaring the cursor with hold you can't do that that's an sql thing if you declare the cursor um in plpg sql you don't need to save with hold right okay yes thanks for clarifying that one as well okay so i don't i don't see much questions uh at the moment but we already have a several ones so that's just being good anything else that you would like to add from the presentation that i say like okay i forgot to mention this that would have been valuable for the presentation um probably it doesn't uh what i would like to have done if i had the time is uh show a few more different data conversion so in this case we saw that it's just a one-for-one copy of an integer into a big end and it fits precisely um but possibly there are other use cases and if you're interested then perhaps i can have a follow-up or add a few slides in my presentation with different use cases okay excellent and uh more like a practical question because this was not your database when you said that this was inspired by a real-life case you needed to work together with let's say the customer that was running how how how what do you have to do as a dba to convince the developer of the project management in order to have this kind of maintenance uh window that you need to have for all this change well this didn't require maintenance window that's the whole point uh so it was the only acceptable solution in some cases uh so um when your management says that you cannot take the database offline because we'll be using money for example uh then it's the only thing you can do you can only go for some concurred solution yeah okay thanks for clarifying that that's the the value of this specific workaround so that's nice good um iila do you have anything else to add no thank you thank you jimmy for your talk it was really interesting um we have three more minutes for questions and then this live will interrupt so yeah if there are any questions ask them now or else we can continue this conversation in the room up to talk which will be available in three minutes thanks everyone thanks very much you me
 Hello, virtual FOSDEM. My name is Jimmy Angelacos and I work as a senior Postgres architect  at EDB. And today we're going to talk about changing your huge tables, data types and  production. What is the motivation for this talk? First of all, we appear to be still in the era of  big data, whatever that means, which means that along with everything else, Postgres has been  seeing more heavy use with bigger tables and bigger data sets. Also Postgres performance has allowed  this because it keeps getting better by the day. So you may find yourself in a situation where your  database is facing rapid growth and you need to find a way to deal with that. So perhaps your growth  is too rapid and you need to mitigate some decisions that you've taken early on. So why would you want  to change data types on your database tables, especially if they're in production? First of all,  you may have an incorrect data type. So something which was not suitable for what you wanted, like  you have set a limit for VARCAR, which is not enough for your purposes. So you want to change it to  variable length text. Secondly, you may want to change a non-optimal data type that you entered  in your table. So for example, an ID column that is text that takes up nine bytes for an integer  column that stores this ID for only four bytes, which will help your table scale better. Also,  you may come across the situation of running out of IDs. If you have an integer ID column,  the maximum limit is 2.1 billion, a little bit over that. So you may come across the situation of  ID exhaustion. And in that case, what do you do? How do you keep your application running?  Of course, you know that you can change types. You can change types only if they are compatible,  though. So what you need is for data types that you, for the old data type to be  binary-corrosable to the new type. So for example, XML can be converted to text very easily without  any conversion function needed to be invoked. However, text to XML conversion requires a function  to be run. So they are not binary-corrosable in that direction. And you can also have binary  compatible columns and data types that have exactly the same internal representation in  Postgres, such as text and VARCAR. So the way you do it is you have the alter table,  alter type statement. It's a dvl command that lets you alter table, alter the column name  to type the data type you want. You may also need to use the using expression if there is no  implicit cast between the first and the second type, which needs that you may have to drop the  default value for that column, run the alter table statement, and then add another default  for the new data type. And this requires indexes to be rebuilt, even if the table doesn't have to  get rewritten. So what is the problem here exactly? Alter table, alter column requires an access  exclusive lock in order to change types. So that means that nothing can take place on that table.  No reads and no writes are allowed to any other transaction. And this effectively stops us from  using the table in production, which is a bad thing. Moreover, if the old data type and the  new data type are not binary courseable between them, then you will have to rewrite the entire table,  which is of course slow and requires double the disk space.  So let us look at one possible scenario that you may encounter.  Let's say that you have a huge table in production that is 1.7 billion rows.  And you have a primary key, which is integer. So because of rapid growth, your table that is now  1.7 billion rows can become 2.1 billion rows very soon. So you find that big integer, which doesn't  have the 2.1 billion limit, but is much larger, can be the solution for your problem.  However, integer and big int are not binary compatible, because big int is eight bytes and  integer is four bytes. So you have to rewrite the table. So how do you avoid having to lock  the table in production and take everything down? So if you cannot take a maintenance window in order  to perform this lengthy operation, then you need a concurrent solution. And let's look at one  possible concurrent solution that can work. So first off, we add a new big int column  to the table. Then we write a procedure that copies values from the old primary key column  to the new big int column. And we do it in batches so that we don't affect the performance of the  system too much. At the same time, we need to keep using the table and keep being able to write to  the table. So we need to write a trigger that replicates the changes from the old column,  which your application is aware of, to the new column that is practically invisible to your  application. After we have treated the entire table and we have copied everything from the  old column into the new big int column, we can drop the old column and rename the new one  to the name of the old column. And then we make it the primary key. So let's look at a few details  on what exactly needs to take place here. One detail is that we need to create a sequence for the new  primary key. We also need to create the new primary key index. And after the conversion, we had best  perform all of the DDL that does the column dropping and renaming in one transaction in order to be  safe. And also keeping it in one transaction, Postgres tries to execute it as fast as possible.  So we will encounter the minimum possible locking and or blocking of other operations by other users  of our database. So the test system that was used to test this procedure was a laptop basically.  And we created a table with 1.7 billion rows of 170 bytes each. So let's see how we create this  example data. We create a table called large table with an integer column and a text content  column. We create the table and we insert into the table our Lorem Ipsum to populate the  content column. And then we generate series from 1 to 1.7 billion as we want 1.7 billion rows in  the table. The insert takes quite a bit of time. It takes 32 minutes to generate this. It's not too  bad because we haven't created any indexes yet. So we create a sequence for our large table ID column  starting at the very next number after 1.7 billion. We set the default for the ID column to be this  sequence. And then we create the index for the primary key. This takes 26 minutes on the test rig.  So make of that what you will. And then we need to add this unique index as the primary key  constraints to the table. So alter table, large table. We add the primary key using the index we  have just created. And that takes a very short time. So let's say that now our data is in production  and our table looks like this. It has a primary key and a sequence and an integer ID column.  And we find that the size of our table is 265 gigs. So not small by any means.  And the content of our table now looks like this. There is an ID from 1 to 1.7 billion and some content.  If we select the number of live tuples we can see from pgstatusertables.  For a large table we have 1.7 billion rows. First step is to add the new column. And if we make sure  that the new column has a default of 0 then that can be an instantaneous operation because it is non-volatile.  So alter table, large table, add column ID underscore new. Big integer is what we want this time around.  We want the column to be not null and we set a default of 0. And that takes a very short time  like 13 milliseconds. Next we need to build the trigger function to replicate the changes that  are coming into our table while our conversion script is running. That copies everything from  the old ID column to the new ID column. So we create the function large table trigger function  returns trigger. And the only thing it does is when the value changes in the table we make sure that  ID new takes the value of ID. So we create the function and then we need to add the trigger to  the table that runs this function. So create trigger large table trigger. Before insert or update  on large table because we want to replicate the changes the new column  for each row execute the function large table trigger function.  So now anything that changes in our table while we're running the conversion procedure  is going to affect the new column as well. The conversion procedure, I'm sorry if the text is  a bit cramped but we'll look at it in detail. We define a cursor for select ID from large table  because we want to examine every single ID of that table. We define a batch size so we're going to  do it in batches of 100,000 rows at a time. So we begin a loop of updating our large table and setting  ID new equals ID for each one of the rows that we've selected. We increment our counter to  determine where we are in the batch and every 100,000 rows so count modulo batch size equals 0  then we commit. So we don't commit with every row we commit with every 100,000 rows for performance  reasons. That ends the loop and after we're done we commit and the procedure is over. Now what is  important here is that we weren't able to do this with functions so we are taking advantage of  the fact that procedures in Postgres offer us transactional control and we can do things such  as begin transactions, commit them at will, rollback and so on.  So what if we want this huge task to give us some progress indicator so we can add in the loop if  count modulo batch size multiplied by 10 then raise notice rows done. So it will tell us every  million rows 10 times our batch of 100,000 that the number of rows that have already been processed  by the script. So now it's time to do it. We call the large table sync procedure and we see  that it's processing 1 million rows, 2 million rows, 3 million rows and so on. Now to check  that our procedure isn't actually blocking anything and users are not prevented from  using the table let's select something from our large table, a random selection of one row  and we use the backslash watch command in psql to run it every second. So every second we get an  id from the table and another id the next second and so on. So we keep it running and we see that  because it runs it means we're not blocking our users from reading from the table and also  they're not prevented from writing to the table. So now we wait and seven hours later we see that  our procedure has completed. It is done processing 1.7 billion rows and it took seven hours and six  seconds on the test rig which is not too bad for something which is running concurrently and  remember the purpose of this procedure is not to run fast, it is not to overpower your system and  affect your performance so negatively while you're doing it that nobody else is able to  use your table. So what does our table look like now? It has an id column, the content that it had  and an id new column which is identical but is big integer instead of integer.  Now we need to create the index for our primary key.  So create unique index concurrently, we name it large table id new index on our large table  and we created on the new id column that is big int. So index creation takes an hour and 17 minutes  and now we haven't really made any changes to the table so it's the time to run the  ddl all at once as we said so we're going to use an anonymous code block to execute all of it.  So we need to find the new start for our sequence so we select the max id from large table  into the variable new start then we create a sequence and we concatenate new start to the  end of that statement that we then execute so create sequence large table with that number  then we alter table to set the sequence to be the default for our column id new  and then we drop the id column rename id new to id and then we add constraints  the primary key using the index we just created in the last step  we drop the trigger and we commit at the moment we commit all of the ddl is going to  execute at the same time and it shouldn't block the table for very long at all and it  shouldn't have any other negative consequences so we run it and we see alter table add constraint  using index is a warning that postgres throws and says it will rename the index from large table id  new index to large table id primary key which is fine and it does it and it only takes 451 milliseconds  so less than half a second on our test rig and now we're all done we have changed our huge table  uh primary key from integer to big integer without affecting the operation of our system  so thank you very much and this is the background photo that was used  uh you can find me on twitter and i am open to your questions now thanks very much for listening  all right i assume that we are going to have the broadcast in any moment i just told you now to  avoid this awkward silence and so we have a few questions thank you so much jimmy for this specific  i work around so you came with this idea probably from a life uh existing issue right can you tell  us whether there was something you just wanted to study or or this is a real problem that you faced  yeah yeah it's a real life problem um people start using um primary keys with hint and it actually  happens that sometimes they run out of integers because of the number of transactions or number  of customers that they add in their database so it is a real world problem nice not not just a case  study so good so a question coming from the audience is it advisable to change a value  in a column while changing the data type so for instance changing text to a boolean and set all  empty fields to false sorry i don't understand the idea is that you change the data type yeah  but you also change the values when you're doing that all the same time oh you can do that well  if you if you set your conversion function to change everything and also the trigger  that deals with the incoming values then sure you can do that whether it's something that you want  to do if you want a consistency in your application is a different story because as as we said this  example is on running live database so maybe your application is not expecting the values to be changing  right yeah and also because you're mentioning is a live database so there are new integers that are  coming in the primary key there is a question or a comment about the the sequence of the big integer  that you're creating when you're changing whether it is advisable or not just set it to a higher value  so that you avoid any race conditions while doing the switch right yeah so in this case  i just selected the max integer and because nobody was writing into my table in my laptop  then it didn't change but yeah certainly it's a good idea to skip a few numbers  ahead when you're creating the sequence good okay thanks for clarifying that  there is another question here saying how big the table became after you updated all the rules  um it grew but not to twice its size so it's a bit better than rewriting the entire table  and also if you have auto vacuum running because you have multiple transactions then it's able  to clean up a bit better than doing it in one huge transaction so any maintenance you have to do  like after you do all this change because probably you would like to do a vacuum full but it's not  possible of course and it's not kind of a advice you just run vacuum full in such a large table  but yeah it's probably a good idea to analyze after you've changed your data type analyze okay yeah  that's good for the statistics i also see a question about um uh declaring the cursor with hold you  can't do that that's an sql thing if you declare the cursor um in plpg sql you don't need to save  with hold right okay yes thanks for clarifying that one as well  okay so i don't i don't see much questions uh at the moment but we already have a  several ones so that's just being good anything else that you would like to add from the presentation  that i say like okay i forgot to mention this that would have been valuable for the presentation  um probably it doesn't uh what i would like to have done if i had the time is uh show a few more  different data conversion so in this case we saw that it's just a one-for-one copy of  an integer into a big end and it fits precisely um but possibly there are other use cases and  if you're interested then perhaps i can have a follow-up or add a few slides in my  presentation with different use cases okay excellent and uh more like a practical question  because this was not your database when you said that this was inspired by a real-life  case you needed to work together with let's say the customer that was running how how  how what do you have to do as a dba to convince the developer of the project management in order  to have this kind of maintenance uh window that you need to have for all this change well this  didn't require maintenance window that's the whole point uh so it was the only acceptable solution  in some cases uh so um when your management says that you cannot take the database offline  because we'll be using money for example uh then it's the only thing you can do you can only go  for some concurred solution yeah okay thanks for clarifying that that's the the value of this  specific workaround so that's nice good um iila do you have anything else to add no thank you thank  you jimmy for your talk it was really interesting um we have three more minutes for questions and  then this live will interrupt so yeah if there are any questions ask them now or else we can  continue this conversation in the room up to talk which will be available in three minutes thanks  everyone thanks very much you me
postgresql_changing_your_huge_tables_data_types_in_production_0_179.png postgresql_changing_your_huge_tables_data_types_in_production_180_10403.png postgresql_changing_your_huge_tables_data_types_in_production_10404_25763.png postgresql_changing_your_huge_tables_data_types_in_production_25764_25859.png postgresql_changing_your_huge_tables_data_types_in_production_25860_29447.png postgresql_changing_your_huge_tables_data_types_in_production_29448_30251.png postgresql_changing_your_huge_tables_data_types_in_production_30252_32339.png postgresql_changing_your_huge_tables_data_types_in_production_32340_32939.png postgresql_changing_your_huge_tables_data_types_in_production_32940_33035.png postgresql_changing_your_huge_tables_data_types_in_production_33036_33491.png postgresql_changing_your_huge_tables_data_types_in_production_33492_33551.png postgresql_changing_your_huge_tables_data_types_in_production_33552_33827.png postgresql_changing_your_huge_tables_data_types_in_production_33828_33983.png postgresql_changing_your_huge_tables_data_types_in_production_33984_34307.png postgresql_changing_your_huge_tables_data_types_in_production_34308_35051.png postgresql_changing_your_huge_tables_data_types_in_production_35052_35579.png postgresql_changing_your_huge_tables_data_types_in_production_35580_35999.png postgresql_changing_your_huge_tables_data_types_in_production_36000_36851.png postgresql_changing_your_huge_tables_data_types_in_production_36852_37343.png postgresql_changing_your_huge_tables_data_types_in_production_37344_37799.png postgresql_changing_your_huge_tables_data_types_in_production_37800_37847.png postgresql_changing_your_huge_tables_data_types_in_production_37848_38351.png postgresql_changing_your_huge_tables_data_types_in_production_38352_38939.png postgresql_changing_your_huge_tables_data_types_in_production_38940_39227.png postgresql_changing_your_huge_tables_data_types_in_production_39228_39647.png postgresql_changing_your_huge_tables_data_types_in_production_39648_40391.png postgresql_changing_your_huge_tables_data_types_in_production_40392_40463.png postgresql_changing_your_huge_tables_data_types_in_production_40464_40691.png postgresql_changing_your_huge_tables_data_types_in_production_40692_40967.png postgresql_changing_your_huge_tables_data_types_in_production_40968_41087.png postgresql_changing_your_huge_tables_data_types_in_production_41088_41267.png postgresql_changing_your_huge_tables_data_types_in_production_41268_41315.png postgresql_changing_your_huge_tables_data_types_in_production_41316_41399.png postgresql_changing_your_huge_tables_data_types_in_production_41400_41523.png