|
| 1 | +--- |
| 2 | +title: How to Alter Column Type in Postgres |
| 3 | +updated_at: 2025/02/28 09:00:00 |
| 4 | +--- |
| 5 | + |
| 6 | +_Official documentation: [ALTER TABLE](https://www.postgresql.org/docs/current/sql-altertable.html)_ |
| 7 | + |
| 8 | +<HintBlock type="info"> |
| 9 | + |
| 10 | +Changing column type should be conducted with caution. Some organizations have strict approval process and even disallow altering column type at all. You can enforce [approval process](/docs/administration/custom-approval/) or [disallowing altering column type](/docs/sql-review/review-rules/#column.disallow-change-type) via Bytebase. |
| 11 | + |
| 12 | +</HintBlock> |
| 13 | + |
| 14 | +## Simple Type Conversions |
| 15 | + |
| 16 | +For straightforward conversions that don't require data transformation: |
| 17 | + |
| 18 | +```sql |
| 19 | +-- Change an integer column to bigint |
| 20 | +ALTER TABLE orders |
| 21 | +ALTER COLUMN order_id |
| 22 | +TYPE bigint; |
| 23 | + |
| 24 | +-- Change a varchar column to text |
| 25 | +ALTER TABLE customers |
| 26 | +ALTER COLUMN notes |
| 27 | +TYPE text; |
| 28 | + |
| 29 | +-- Change a float column to numeric with precision |
| 30 | +ALTER TABLE products |
| 31 | +ALTER COLUMN price |
| 32 | +TYPE numeric(10,2); |
| 33 | +``` |
| 34 | + |
| 35 | +## Using USING Clause for Data Transformation |
| 36 | + |
| 37 | +When the conversion requires transformation, use the `USING` clause: |
| 38 | + |
| 39 | +```sql |
| 40 | +-- Convert text to integer |
| 41 | +ALTER TABLE employees |
| 42 | +ALTER COLUMN age |
| 43 | +TYPE integer USING (age::integer); |
| 44 | + |
| 45 | +-- Convert string to date |
| 46 | +ALTER TABLE events |
| 47 | +ALTER COLUMN event_date |
| 48 | +TYPE date USING (event_date::date); |
| 49 | + |
| 50 | +-- Convert string to timestamp |
| 51 | +ALTER TABLE logs |
| 52 | +ALTER COLUMN created_at |
| 53 | +TYPE timestamp USING (created_at::timestamp); |
| 54 | + |
| 55 | +-- Complex transformation with conditional logic |
| 56 | +ALTER TABLE users |
| 57 | +ALTER COLUMN status |
| 58 | +TYPE boolean USING (CASE WHEN status = 'active' THEN TRUE ELSE FALSE END); |
| 59 | +``` |
| 60 | + |
| 61 | +## Converting Between Text Types |
| 62 | + |
| 63 | +```sql |
| 64 | +-- varchar to text (no data loss) |
| 65 | +ALTER TABLE messages |
| 66 | +ALTER COLUMN content |
| 67 | +TYPE text; |
| 68 | + |
| 69 | +-- text to varchar with potential truncation |
| 70 | +ALTER TABLE products |
| 71 | +ALTER COLUMN description |
| 72 | +TYPE varchar(255) USING substring(description, 1, 255); |
| 73 | +``` |
| 74 | + |
| 75 | +## Converting Numeric Types |
| 76 | + |
| 77 | +```sql |
| 78 | +-- integer to bigint (safe, no data loss) |
| 79 | +ALTER TABLE measurements |
| 80 | +ALTER COLUMN value |
| 81 | +TYPE bigint; |
| 82 | + |
| 83 | +-- decimal to integer (truncation of fractional part) |
| 84 | +ALTER TABLE products |
| 85 | +ALTER COLUMN price |
| 86 | +TYPE integer USING (price::integer); |
| 87 | + |
| 88 | +-- float to numeric (fixed precision) |
| 89 | +ALTER TABLE financial |
| 90 | +ALTER COLUMN amount |
| 91 | +TYPE numeric(15,2) USING (amount::numeric(15,2)); |
| 92 | +``` |
| 93 | + |
| 94 | +## Date and Time Conversions |
| 95 | + |
| 96 | +```sql |
| 97 | +-- timestamp to date (drops time portion) |
| 98 | +ALTER TABLE events |
| 99 | +ALTER COLUMN event_timestamp |
| 100 | +TYPE date USING (event_timestamp::date); |
| 101 | + |
| 102 | +-- date to timestamp (adds 00:00:00 time) |
| 103 | +ALTER TABLE appointments |
| 104 | +ALTER COLUMN appointment_date |
| 105 | +TYPE timestamp USING (appointment_date::timestamp); |
| 106 | + |
| 107 | +-- timestamp to timestamptz (applies server timezone) |
| 108 | +ALTER TABLE logs |
| 109 | +ALTER COLUMN log_time |
| 110 | +TYPE timestamptz USING (log_time::timestamptz); |
| 111 | +``` |
| 112 | + |
| 113 | +## UUID and Identifier Conversions |
| 114 | + |
| 115 | +```sql |
| 116 | +-- Convert text to UUID |
| 117 | +ALTER TABLE sessions |
| 118 | +ALTER COLUMN session_id |
| 119 | +TYPE uuid USING (session_id::uuid); |
| 120 | + |
| 121 | +-- Convert integer to UUID (requires custom function) |
| 122 | +CREATE OR REPLACE FUNCTION int_to_uuid(i integer) RETURNS uuid AS $$ |
| 123 | +BEGIN |
| 124 | + RETURN ('00000000-0000-0000-0000-' || lpad(i::text, 12, '0'))::uuid; |
| 125 | +END; |
| 126 | +$$ LANGUAGE plpgsql; |
| 127 | + |
| 128 | +ALTER TABLE legacy_users |
| 129 | +ALTER COLUMN user_id |
| 130 | +TYPE uuid USING int_to_uuid(user_id); |
| 131 | +``` |
| 132 | + |
| 133 | +## Array Type Conversions |
| 134 | + |
| 135 | +```sql |
| 136 | +-- Convert text to string array using delimiters |
| 137 | +ALTER TABLE products |
| 138 | +ALTER COLUMN tags |
| 139 | +TYPE text[] USING string_to_array(tags, ','); |
| 140 | + |
| 141 | +-- Convert array element types |
| 142 | +ALTER TABLE measurements |
| 143 | +ALTER COLUMN values |
| 144 | +TYPE numeric[] USING (SELECT array_agg(v::numeric) FROM unnest(values) AS v); |
| 145 | +``` |
| 146 | + |
| 147 | +## JSON/JSONB Conversions |
| 148 | + |
| 149 | +```sql |
| 150 | +-- Convert text to JSON |
| 151 | +ALTER TABLE api_responses |
| 152 | +ALTER COLUMN response |
| 153 | +TYPE json USING (response::json); |
| 154 | + |
| 155 | +-- Convert JSON to JSONB |
| 156 | +ALTER TABLE configurations |
| 157 | +ALTER COLUMN config |
| 158 | +TYPE jsonb USING (config::jsonb); |
| 159 | + |
| 160 | +-- Convert JSONB to text |
| 161 | +ALTER TABLE archived_data |
| 162 | +ALTER COLUMN data |
| 163 | +TYPE text USING (data::text); |
| 164 | +``` |
| 165 | + |
| 166 | +## Handling Special Cases |
| 167 | + |
| 168 | +### Converting NULL Values |
| 169 | + |
| 170 | +```sql |
| 171 | +-- Set default value for NULLs during conversion |
| 172 | +ALTER TABLE users |
| 173 | +ALTER COLUMN last_login |
| 174 | +TYPE timestamp USING (COALESCE(last_login::timestamp, '1970-01-01'::timestamp)); |
| 175 | +``` |
| 176 | + |
| 177 | +### Converting with Length Constraints |
| 178 | + |
| 179 | +```sql |
| 180 | +-- Handle possible truncation with warning |
| 181 | +DO $$ |
| 182 | +DECLARE |
| 183 | + over_length INTEGER; |
| 184 | +BEGIN |
| 185 | + SELECT COUNT(*) INTO over_length |
| 186 | + FROM products |
| 187 | + WHERE LENGTH(description) > 100; |
| 188 | + |
| 189 | + IF over_length > 0 THEN |
| 190 | + RAISE WARNING 'Warning: % rows will have data truncated', over_length; |
| 191 | + END IF; |
| 192 | +END $$; |
| 193 | + |
| 194 | +ALTER TABLE products |
| 195 | +ALTER COLUMN description |
| 196 | +TYPE varchar(100) USING substring(description, 1, 100); |
| 197 | +``` |
| 198 | + |
| 199 | +## Performance Considerations |
| 200 | + |
| 201 | +### Using Transactions |
| 202 | + |
| 203 | +For large tables, wrap the alteration in a transaction: |
| 204 | + |
| 205 | +```sql |
| 206 | +BEGIN; |
| 207 | +-- Check if the conversion is safe |
| 208 | +-- ... |
| 209 | +ALTER TABLE large_table |
| 210 | +ALTER COLUMN data |
| 211 | +TYPE new_type USING (data::new_type); |
| 212 | +COMMIT; |
| 213 | +``` |
| 214 | + |
| 215 | +### Low-Impact Approaches for Production |
| 216 | + |
| 217 | +For large tables in production, you might prefer multi-step approach: |
| 218 | + |
| 219 | +```sql |
| 220 | +-- 1. Add a new column |
| 221 | +ALTER TABLE large_table ADD COLUMN new_column new_type; |
| 222 | + |
| 223 | +-- 2. Update data in batches |
| 224 | +DO $$ |
| 225 | +DECLARE |
| 226 | + batch_size INTEGER := 10000; |
| 227 | + max_id INTEGER; |
| 228 | + current_id INTEGER := 0; |
| 229 | +BEGIN |
| 230 | + SELECT MAX(id) INTO max_id FROM large_table; |
| 231 | + WHILE current_id < max_id LOOP |
| 232 | + EXECUTE 'UPDATE large_table |
| 233 | + SET new_column = old_column::new_type |
| 234 | + WHERE id > $1 AND id <= $2' |
| 235 | + USING current_id, current_id + batch_size; |
| 236 | + |
| 237 | + current_id := current_id + batch_size; |
| 238 | + COMMIT; |
| 239 | + END LOOP; |
| 240 | +END $$; |
| 241 | + |
| 242 | +-- 3. Add constraints if needed |
| 243 | +ALTER TABLE large_table ALTER COLUMN new_column SET NOT NULL; |
| 244 | + |
| 245 | +-- 4. Drop the old column when ready |
| 246 | +ALTER TABLE large_table DROP COLUMN old_column; |
| 247 | + |
| 248 | +-- 5. Rename the new column to the old name |
| 249 | +ALTER TABLE large_table RENAME COLUMN new_column TO old_column; |
| 250 | +``` |
| 251 | + |
| 252 | +## Common Errors and Solutions |
| 253 | + |
| 254 | +### "cannot cast type X to Y" |
| 255 | + |
| 256 | +```sql |
| 257 | +-- Use explicit conversion function instead |
| 258 | +ALTER TABLE products |
| 259 | +ALTER COLUMN code |
| 260 | +TYPE uuid USING uuid_generate_v5(uuid_ns_url(), code); |
| 261 | +``` |
| 262 | + |
| 263 | +### "value too long for type character varying(N)" |
| 264 | + |
| 265 | +```sql |
| 266 | +-- Check and handle long values first |
| 267 | +UPDATE table_name |
| 268 | +SET column_name = substring(column_name, 1, 50) |
| 269 | +WHERE LENGTH(column_name) > 50; |
| 270 | + |
| 271 | +-- Then alter the type |
| 272 | +ALTER TABLE table_name |
| 273 | +ALTER COLUMN column_name |
| 274 | +TYPE varchar(50); |
| 275 | +``` |
| 276 | + |
| 277 | +### "operator does not exist: X = Y" |
| 278 | + |
| 279 | +```sql |
| 280 | +-- Create a custom cast or use an explicit function |
| 281 | +CREATE FUNCTION custom_text_to_uuid(text) RETURNS uuid AS $$ |
| 282 | + SELECT CASE |
| 283 | + WHEN $1 ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN $1::uuid |
| 284 | + ELSE uuid_nil() |
| 285 | + END; |
| 286 | +$$ LANGUAGE SQL; |
| 287 | + |
| 288 | +ALTER TABLE items |
| 289 | +ALTER COLUMN item_id |
| 290 | +TYPE uuid USING custom_text_to_uuid(item_id); |
| 291 | +``` |
0 commit comments