简体   繁体   English

Postgresql 将列类型从 int 更改为 UUID

[英]Postgresql change column type from int to UUID

I'd like to change the column type from an int to a uuid .我想将列类型从int更改为uuid I am using the following statement我正在使用以下语句

ALTER TABLE tableA ALTER COLUMN colA SET DATA TYPE UUID;

But I get the error message但我收到错误信息

ERROR:  column "colA" cannot be cast automatically to type uuid
HINT:  Specify a USING expression to perform the conversion.

I am confused how to use USING to do the cast.我对如何使用USING进行转换感到困惑。

You can't just cast an int4 to uuid;您不能只是将 int4 转换为 uuid; it'd be an invalid uuid, with only 32 bits set, the high 96 bits being zero.它会是一个无效的 uuid,只设置了 32 位,高 96 位为零。

If you want to generate new UUIDs to replace the integers entirely, and if there are no existing foreign key references to those integers, you can use a fake cast that actually generates new values.如果您想生成新的 UUID 以完全替换整数,并且如果没有对这些整数的现有外键引用,您可以使用实际生成新值的假转换。

Do not run this without a backup of your data.不要在没有备份数据的情况下运行它 It permanently throws away the old values in colA .它永久丢弃colA的旧值。

ALTER TABLE tableA ALTER COLUMN colA SET DATA TYPE UUID USING (uuid_generate_v4());

A better approach is usually to add a uuid column, then fix up any foreign key references to point to it, and finally drop the original column.更好的方法通常是添加一个 uuid 列,然后修复任何指向它的外键引用,最后删除原始列。

You need the UUID module installed:您需要安装 UUID 模块:

CREATE EXTENSION "uuid-ossp";

The quotes are important.引号很重要。

Just if someone comes across this old topic.只要有人遇到这个老话题。 I solved the problem by first altering the field into a CHAR type and then into UUID type.我首先将字段更改为 CHAR 类型,然后更改为 UUID 类型,从而解决了该问题。

I had to convert from text to uuid type, and from a Django migration, so after solving this I wrote it up at http://baltaks.com/2015/08/how-to-change-text-fields-to-a-real-uuid-type-for-django-and-postgresql in case that helps anyone.我不得不从文本转换为 uuid 类型,并从 Django 迁移,所以在解决这个问题后,我在http://baltaks.com/2015/08/how-to-change-text-fields-to-a 上写了它-real-uuid-type-for-django-and-postgresql以防万一。 The same techniques would work for an integer to uuid conversion.相同的技术适用于整数到 uuid 的转换。

Based on a comment, I've added the full solution here:根据评论,我在这里添加了完整的解决方案:

Django will most likely create a migration for you that looks something like: Django 很可能会为您创建一个类似于以下内容的迁移:

class Migration(migrations.Migration):

    dependencies = [
        ('app', '0001_auto'),
    ]

    operations = [
        migrations.AlterField(
            model_name='modelname',
            name='uuid',
            field=models.UUIDField(db_index=True, unique=True),
        ),
    ]

First, put the auto created migration operations into a RunSQL operation as the state_operations parameter.首先,将自动创建的迁移操作作为state_operations参数放入 RunSQL 操作中。 This allows you to provide a custom migration, but keep Django informed about what's happened to the database schema.这允许您提供自定义迁移,但让 Django 了解数据库模式发生了什么。

class Migration(migrations.Migration):

    dependencies = [
        ('app', '0001_auto'),
    ]

    operations = [
    migrations.RunSQL(sql_commands, None, [
            migrations.AlterField(
                model_name='modelname',
                name='uuid',
                field=models.UUIDField(db_index=True, unique=True),
            ),
        ]),
    ]

Now you'll need to provide some SQL commands for that sql_commands variable.现在您需要为该sql_commands变量提供一些 SQL 命令。 I opted to put the sql into a separate file and then load in with the following python code:我选择将 sql 放入一个单独的文件中,然后使用以下 python 代码加载:

sql_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '0001.sql')
with open(sql_path, "r") as sqlfile:
    sql_commands = sqlfile.read()

Now for the real tricky part, where we actually perform the migration.现在是真正棘手的部分,我们实际执行迁移的地方。 The basic command you want looks like:您想要的基本命令如下所示:

alter table tablename alter column uuid type uuid using uuid::uuid;

But the reason we are here is because of indexes.但我们在这里的原因是因为索引。 And as I discovered, Django likes to use your migrations to created randomly named indexes on your fields while running tests, so your tests will fail if you just delete and then recreate a fixed name index or two.正如我发现的那样,Django 喜欢在运行测试时使用您的迁移在您的字段上创建随机命名的索引,因此如果您只是删除然后重新创建一个或两个固定名称索引,您的测试将失败。 So the following is sql that will delete one constraint and all indexes on the text field before converting to a uuid field.所以下面的 sql 将在转换为 uuid 字段之前删除文本字段上的一个约束和所有索引。 It also works for multiple tables in one go.它还可以一次性处理多个表。

DO $$
DECLARE
    table_names text[];
    this_table_name text;
    the_constraint_name text;
    index_names record;

BEGIN

SELECT array['table1',
             'table2'
             ]
    INTO table_names;


FOREACH this_table_name IN array table_names
LOOP
    RAISE notice 'migrating table %', this_table_name;

    SELECT CONSTRAINT_NAME INTO the_constraint_name
    FROM information_schema.constraint_column_usage
    WHERE CONSTRAINT_SCHEMA = current_schema()
        AND COLUMN_NAME IN ('uuid')
        AND TABLE_NAME = this_table_name
    GROUP BY CONSTRAINT_NAME
    HAVING count(*) = 1;
    if the_constraint_name is not NULL then
        RAISE notice 'alter table % drop constraint %',
            this_table_name,
            the_constraint_name;
        execute 'alter table ' || this_table_name
            || ' drop constraint ' || the_constraint_name;
    end if;

    FOR index_names IN
    (SELECT i.relname AS index_name
     FROM pg_class t,
          pg_class i,
          pg_index ix,
          pg_attribute a
     WHERE t.oid = ix.indrelid
         AND i.oid = ix.indexrelid
         AND a.attrelid = t.oid
         AND a.attnum = any(ix.indkey)
         AND t.relkind = 'r'
         AND a.attname = 'uuid'
         AND t.relname = this_table_name
     ORDER BY t.relname,
              i.relname)
    LOOP
        RAISE notice 'drop index %', quote_ident(index_names.index_name);
        EXECUTE 'drop index ' || quote_ident(index_names.index_name);
    END LOOP; -- index_names

    RAISE notice 'alter table % alter column uuid type uuid using uuid::uuid;',
        this_table_name;
    execute 'alter table ' || quote_ident(this_table_name)
        || ' alter column uuid type uuid using uuid::uuid;';
    RAISE notice 'CREATE UNIQUE INDEX %_uuid ON % (uuid);',
        this_table_name, this_table_name;
    execute 'create unique index ' || this_table_name || '_uuid on '
        || this_table_name || '(uuid);';

END LOOP; -- table_names

END;
$$

I was able to convert a column with an INT type, configured as an incrementing primary key using the SERIAL shorthand, using the following process:我能够使用以下过程转换具有INT类型的列,使用SERIAL速记将其配置为递增主键:

--  Ensure the UUID extension is installed.
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

--  Dropping and recreating the default column value is required because
--  the default INT value is not compatible with the new column type.
ALTER TABLE table_to_alter ALTER COLUMN table_id DROP DEFAULT, 
ALTER COLUMN table_id SET DATA TYPE UUID USING (uuid_generate_v4()), 
ALTER COLUMN table_id SET DEFAULT uuid_generate_v4();

I'm bumping to this after a long time, but there is a way to convert your integer column to a UUID with some kind of backwards-compatibility, namely keeping a way to have a reference to your old values, rather than dropping your values.很长一段时间后我遇到了这个问题,但是有一种方法可以将您的整数列转换为具有某种向后兼容性的 UUID,即保留一种引用旧值的方法,而不是删除您的值. It comprises of converting your integer value to a hex string and then padding that with necesary zeroes to make up an artificial UUID.它包括将您的整数值转换为十六进制字符串,然后用必要的零填充它以组成一个人工 UUID。

So, assuming your current integer column is named ColA, the following statement would do it (mind the using part):因此,假设您当前的整数列名为 ColA,以下语句将执行此操作(注意using部分):

ALTER TABLE tableA ALTER COLUMN ColA SET DATA TYPE UUID USING LPAD(TO_HEX(ColA), 32, '0')::UUID;

In PostgreSQL 9.3 you can do this:在 PostgreSQL 9.3 中你可以这样做:

ALTER TABLE "tableA" ALTER COLUMN "ColA" SET DATA TYPE UUID USING "ColA"::UUID;

And cast the type of data to UUID and this will avoid the error message.并将数据类型转换为UUID ,这将避免错误消息。

WARNING: I've noticed some comments and answers that try to cast integers to a UUID4.警告:我注意到一些评论和答案试图将整数转换为 UUID4。

You must not cast or force-set uuid values .不得强制转换或强制设置 uuid 值 They must be generated using functions relating to RFC4122 .它们必须使用与RFC4122相关的函数生成。

UUIDs must be randomly distributed or they will not work. UUID 必须随机分布,否则它们将不起作用。 You cannot cast or enter your own UUIDs as they will not be properly distributed.您不能投射或输入您自己的 UUID,因为它们不会被正确分发。 This can lead to bad actors guessing your sequencing or finding other artifacts or patterns in your UUIDs that will lead them to discover others.这可能会导致不良行为者猜测您的序列或在您的 UUID 中找到其他工件或模式,从而导致他们发现其他工件或模式。

Any answer that converts to char types and then to uuid may lead to these kinds problems.任何转换为​​ char 类型然后转换为 uuid 的答案都可能导致这些类型的问题。

Follow any answer here that refers to ' uuid_generate_v4 '.遵循此处涉及“ uuid_generate_v4 ”的任何答案。 Ignore ones that are casting or setting without using the formal functions.在不使用正式函数的情况下忽略正在转换或设置的那些。

For changing from int column type to uuid I want to keep following properties:为了从int列类型更改为uuid我想保留以下属性:

  • uuid must be globally unique uuid必须是全局唯一的
  • id in migrated column must be deterministically generated, so in case that there are any hard-coded data in migrations, I'm able to reference them even after migration identifier.迁移列中的 id 必须确定地生成,因此如果迁移中有任何硬编码数据,即使在迁移标识符之后我也可以引用它们。

Migration path迁移路径

1) Drop foreign key 1)删除外键

...drop all references to Table1 ...删除对Table1的所有引用

ALTER TABLE "Table2" DROP CONSTRAINT "Foreign_Table2_IdTable1";

2) migrate int-ids to uuid deterministically 2) 确定性地将 int-ids 迁移到 uuid

ALTER TABLE "Table1" ALTER COLUMN "Id" SET DATA TYPE UUID USING DETERMINISTIC_TO_UUID("Id");
ALTER TABLE "Table2" ALTER COLUMN "IdTable1" SET DATA TYPE UUID USING DETERMINISTIC_TO_UUID("IdTable1");

Note: DETERMINISTIC_TO_UUID() needs to be defined, see below!注意:需要定义DETERMINISTIC_TO_UUID() ,见下文!

3) add foreign keys back 3)添加外键回来

ALTER TABLE "Table2" ADD CONSTRAINT "Foreign_Table2_IdTable1" FOREIGN KEY ("IdTable1") REFERENCES "Table1" ("Id") ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE;

How to deterministically convert UUIDs?如何确定性地转换 UUID?

Simplest way最简单的方法

Function that generates invalid UUID, which are however good-enough for most cases: Function 生成无效的 UUID,但对于大多数情况来说已经足够了:

CREATE OR REPLACE FUNCTION DETERMINISTIC_TO_UUID(inputData bigint) RETURNS uuid AS $$
BEGIN
    RETURN LPAD(TO_HEX(inputData), 32, '0')::uuid;
END;
$$ LANGUAGE plpgsql;

uuid4 variant1 compatible version uuid4 variant1 兼容版本

I have implemented following two functions:我实现了以下两个功能:

  • bitstringIdFrom(tableName varchar, id bigint): bit(128) – generate 128-bitstring for given table name & numeric ID bitstringIdFrom(tableName varchar, id bigint): bit(128) – 为给定的表名和数字 ID 生成 128 位字符串
  • makeUuid4variant1From(bit(128)): uuid – Generate valid uuid4 from 128-bitstring makeUuid4variant1From(bit(128)): uuid – 从 128 位字符串生成有效的 uuid4
CREATE OR REPLACE FUNCTION makeUuid4variant1From(inputData bit(128)) RETURNS uuid AS $$
DECLARE uuid4variant1_mask CONSTANT bit(128) := ~((B'1111'::bit(128) >> 48) | (B'11'::bit(128) >> 64));
    DECLARE uuid4variant1_versionData CONSTANT bit(128) := (4::bit(4)::bit(128) >> 48) | (2::bit(2)::bit(128) >> 64);
    DECLARE uuidInBitString bit(128);
    DECLARE low_bits bit(64);
    DECLARE hi_bits bit(64);
BEGIN
    -- This actually makes valid uuid4 variant1
    -- mask removes bits necesarry for version & variant data
    -- OR operation adds required version & variant data
    uuidInBitString := ("inputdata" & uuid4variant1_mask) | uuid4variant1_versionData;

    -- As PostgreSQL does NOT support working with 128-bit itegers, we need to split it into half
    -- working with bit-strings: https://www.postgresql.org/docs/13/functions-bitstring.html, https://www.postgresql.org/docs/9.5/functions-bitstring.html
    low_bits := (uuidInBitString << 64)::bit(64);
    hi_bits  := (uuidInBitString << 0) ::bit(64);

    RETURN (
            LPAD(TO_HEX(hi_bits::bigint), 16, '0') || LPAD(TO_HEX(low_bits::bigint), 16, '0')
        )::uuid;
END;
$$ LANGUAGE plpgsql;


-- creates bit-string from given table name & int-ID
-- This uses deterministic hash function
CREATE OR REPLACE FUNCTION bitstringIdFrom(tableName varchar, id bigint) RETURNS bit(128) AS $$
    DECLARE uuidAsBitString bit(128) := 0::bit(128);
    DECLARE salt text := 'some-secret-text-to-improve-unguessability';
    DECLARE part1 bit(32);
    DECLARE part2 bit(32);
    DECLARE part3 bit(32);
    DECLARE part4 bit(32);
BEGIN
    -- in case someone want to guess migrated uuids
    -- they would need to know this secret & hashing algorithm
    id := id + hashtext(salt);

    -- source: https://hakibenita.com/postgresql-hash-index#hash-function
    part1 := (hashtext(tablename) * id)::bit(32);
    part2 := (hashtext(part1::text) * id)::bit(32);
    part3 := (hashtext(part2::text) * id)::bit(32);
    part4 := (hashtext(part3::text) * id)::bit(32);

    uuidAsBitString := part1::bit(128) | uuidAsBitString;
    uuidAsBitString := uuidAsBitString >> 32;
    uuidAsBitString := part2::bit(128) | uuidAsBitString;
    uuidAsBitString := uuidAsBitString >> 32;

    uuidAsBitString := part3::bit(128) | uuidAsBitString;
    uuidAsBitString := uuidAsBitString >> 32;

    uuidAsBitString := part4::bit(128) | uuidAsBitString;

    RETURN uuidAsBitString;
END;
$$ LANGUAGE plpgsql;

How to achieve real uuid4 randomness?如何实现真正的uuid4随机性?

As when we have been creating new forign key, we have created it with ON UPDATE CASCADE , you can simply achieve that by:当我们创建新的外键时,我们已经使用ON UPDATE CASCADE创建了它,您可以通过以下方式简单地实现:

UPDATE "Table1" SET "Id" = uuid_generate_v4();

... which will automatically update also all foreign keys referenced. ...这将自动更新所有引用的外键。

Note: This can take a really long time as machine randomness can be depleted quite quickly.注意:这可能需要很长时间,因为机器随机性会很快耗尽。 Therefore I recommend to use good salt/secret and use deterministic migration path.因此,我建议使用好的 salt/secret 并使用确定性迁移路径。

My answer is a derivative from @pritstift's answer above我的答案是上面@pritstift 的答案的派生词

here's how I did It, first to convert it to CHAR这是我的做法,首先将其转换为CHAR

ALTER TABLE my_tbl ALTER my_ col TYPE CHARACTER VARYING(10);

then as @SeinopSys in the accepted answer's comment said然后正如@SeinopSys 在接受的答案评论中所说

ALTER TABLE tableA ALTER COLUMN colA DROP DEFAULT, ALTER COLUMN colA TYPE uuid USING (uuid_generate_v4()), ALTER COLUMN colA SET DEFAULT uuid_generate_v4()

This will first convert the integer column into CHAR column, and then replace the whole column with newly created uuids.这将首先将 integer 列转换为 CHAR 列,然后用新创建的 uuid 替换整个列。 Make sure you have no foreign key relations associated with ID column and you have a backup of your table before implementing it确保您没有与 ID 列关联的外键关系,并且在实施之前备份了您的表

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM