如何克服 Netezza 缺乏唯一约束/参照完整性执行的问题?

Posted

技术标签:

【中文标题】如何克服 Netezza 缺乏唯一约束/参照完整性执行的问题?【英文标题】:How to overcome Netezza's lack of unique constraint/referential integrity enforcement? 【发布时间】:2011-04-13 12:35:49 【问题描述】:

似乎缺乏对强制执行 2 个基本约束(唯一和外键)的支持,是导致调试和解决难题的许多工时损失的原因。什么开始是一个微不足道的、易于修复的问题(重复行/不一致的主详细信息表)在我们的应用程序中,甚至在我们的硬件中(例如,自连接与 dup会导致通货膨胀和存储耗尽)。

Netezza 在我们的环境中有多种用途:生产、研究、质量保证和分期。当然,我们的 ETL 流程还不够成熟,也无法验证所有这些场景中的所有约束。 即使在我们最成熟的生产应用程序中,在 ETL 加载数据时验证数据,我们也会创建一系列表格,每个表格都是对其前身的计算结果。有时,数据完整性在整个过程中被破坏,而不是在一开始就被破坏(由于声明错误)

谁能推荐一种方法/工具来避免这些麻烦?

【问题讨论】:

【参考方案1】:

我知道有一个公认的答案,但我想提供一种替代方法。作为我目前职位的新手,我并不了解我们仓库中主键声明背后的所有业务决策。我开发了日志记录类型的方法来跟踪重复行删除工作随着时间的推移。以下是该设计的主要特点:

始终保持最新状态,解决 DDL / DML 的流动性问题 新建/删除表 新的/更新的主键 新的/更新的/删除的行 自填历史 随着时间的推移跟踪改进 为各级趋势分析提供基础 轻松查询目标表以进行研究 不需要使用 HAVING 子句进行自联接或查找键列 此时仅处理主键 可以轻松扩展以解决唯一约束(CONTYPE = 'u' in _V_RELATION_KEYDATA)

以下是从 Netezza 角度来看所需的一切。如果有注明,您将需要填补空白以创建动态 SQL。

首先,我创建了一个表,用于跟踪所有重复记录的数据库、表和内部 rowid。

CREATE TABLE
    NZ_DUPLICATE_PKS
    (
        DATABASE_NAME CHARACTER VARYING(128) NOT NULL
        ,TABLE_OWNER CHARACTER VARYING(128) NOT NULL
        ,TABLE_NAME CHARACTER VARYING(128) NOT NULL
        ,ROW_ID BIGINT NOT NULL
        ,CURRENT_RECORD_INDICATOR CHARACTER(1) NOT NULL
        ,CREATE_TIMESTAMP TIMESTAMP NOT NULL
        ,LAST_UPDATE_TIMESTAMP TIMESTAMP NOT NULL
    )
DISTRIBUTE ON
    (
        ROW_ID
    );

注意:分配键和进入表的行数的 YMMV。我们的 Netezza 应用程序中的行 ID 具有足够自然的分布,它在基于 Mustang 的 NPS 10050 上为我提供了很好的服务。

接下来,创建了此表的暂存版本:

CREATE TABLE
    STG_NZ_DUPLICATE_PKS
    (
        DATABASE_NAME CHARACTER VARYING(128)
        ,TABLE_OWNER CHARACTER VARYING(128)
        ,TABLE_NAME CHARACTER VARYING(128)
        ,ROW_ID BIGINT
        ,CURRENT_RECORD_INDICATOR CHARACTER(1)
        ,CREATE_TIMESTAMP TIMESTAMP
        ,LAST_UPDATE_TIMESTAMP TIMESTAMP
    )
DISTRIBUTE ON
    (
        ROW_ID
    );

然后,我从系统视图创建了动态查询来为临时表提供种子。这是我开始的基本查询:

SELECT
    DATABASE
    ,OWNER
    ,RELATION
    ,CONSTRAINTNAME
    ,ATTNAME
FROM
    YOUR_DATABASE_NAME._V_RELATION_KEYDATA
WHERE
    CONTYPE = 'p'
    -- Exclude the duplicate tracking table
    AND RELATION != 'NZ_DUPLICATE_PKS'
ORDER BY
    DATABASE
    ,OWNER
    ,RELATION
    ,CONSTRAINTNAME
    ,CONSEQ
;

现在我循环遍历基本查询以动态创建插入查询。我的店用的是DataStage,它的方法很深奥,不值得在这里详述。

注意:这里需要做一些工作来循环和构造动态 SQL。可以使用多种风格的 shell、Perl、Python 等。使用具有两列键的示例表,以下是要构造的插入临时表的内容:

INSERT
INTO
    STG_NZ_DUPLICATE_PKS
    (
        DATABASE_NAME
        ,TABLE_OWNER
        ,TABLE_NAME
        ,ROW_ID
        ,CURRENT_RECORD_INDICATOR
        ,CREATE_TIMESTAMP
        ,LAST_UPDATE_TIMESTAMP
    )
SELECT
    'YOUR_DATABASE_NAME' DATABASE_NAME
    ,'YOUR_TABLE_OWNER' TABLE_OWNER
    ,'YOUR_TABLE_NAME' TABLE_NAME
    ,DUPS.ROWID ROW_ID
    ,'Y' CURRENT_RECORD_INDICATOR
    ,CURRENT_TIMESTAMP CREATE_TIMESTAMP
    ,CURRENT_TIMESTAMP LAST_UPDATE_TIMESTAMP
FROM
    YOUR_TABLE_NAME DUPS
    INNER JOIN
        (
            SELECT
                KEY_COLUMN_1
                ,KEY_COLUMN_2
            FROM
                YOUR_TABLE_NAME
            GROUP BY
                KEY_COLUMN_1
                ,KEY_COLUMN_2
            HAVING
                COUNT(*) > 1
        )
        KEYS
        ON
            DUPS.KEY_COLUMN_1 = KEYS.KEY_COLUMN_1
            AND DUPS.KEY_COLUMN_2 = KEYS.KEY_COLUMN_2;

在遍历所有表以播种临时表之后,我运行一系列查询,将数据库、所有者、表名和行 ID 视为一个缓慢变化的维度。此查询结束日期目标表中暂存表中不存在的记录:

UPDATE
    NZ_DUPLICATE_PKS
SET
    CURRENT_RECORD_INDICATOR = 'N'
    ,LAST_UPDATE_TIMESTAMP = CURRENT_TIMESTAMP
WHERE
    CURRENT_RECORD_INDICATOR = 'Y'
    AND
    (
        DATABASE_NAME
        ,TABLE_OWNER
        ,TABLE_NAME
        ,ROW_ID
    )
    NOT IN
    (
        SELECT
            DATABASE_NAME
            ,TABLE_OWNER
            ,TABLE_NAME
            ,ROW_ID
        FROM
            STG_NZ_DUPLICATE_PKS
    )
;

最后,将最新的记录插入到目标表中:

INSERT
INTO
    NZ_DUPLICATE_PKS
    (
        DATABASE_NAME
        ,TABLE_OWNER
        ,TABLE_NAME
        ,ROW_ID
        ,CURRENT_RECORD_INDICATOR
        ,CREATE_TIMESTAMP
        ,LAST_UPDATE_TIMESTAMP
    )
SELECT
    DATABASE_NAME
    ,TABLE_OWNER
    ,TABLE_NAME
    ,ROW_ID
    ,CURRENT_RECORD_INDICATOR
    ,CREATE_TIMESTAMP
    ,LAST_UPDATE_TIMESTAMP
FROM
    STG_NZ_DUPLICATE_PKS
WHERE
    (
        DATABASE_NAME
        ,TABLE_OWNER
        ,TABLE_NAME
        ,ROW_ID
    )
    NOT IN
    (
        SELECT
            DATABASE_NAME
            ,TABLE_OWNER
            ,TABLE_NAME
            ,ROW_ID
        FROM
            NZ_DUPLICATE_PKS
        WHERE
            CURRENT_RECORD_INDICATOR = 'Y'
    )
;

注意:我们的环境并不需要仅插入模型。 Netezza 的老手会熟悉这种思路。如果您的环境是仅插入的,请相应地调整策略。

一切就绪后,可以快速定位重复行以进行调查:

SELECT
    *
FROM
    YOUR_TABLE_NAME
WHERE
    ROWID IN
    (
        SELECT
            ROW_ID
        FROM
            NZ_DUPLICATE_PKS
        WHERE
            CURRENT_RECORD_INDICATOR = 'Y'
            AND DATABASE_NAME = 'YOUR_DATABASE_NAME'
            AND TABLE_OWNER = 'YOUR_OWNER_NAME'
            AND TABLE_NAME = 'YOUR_TABLE_NAME'
    );

我喜欢这个,因为它对所有表都简单且相同,无论主键声明的差异如何。

我也经常使用这个查询来按表查看当前的主键违规情况:

SELECT
    DATABASE_NAME
    ,TABLE_OWNER
    ,TABLE_NAME
    ,COUNT(*) QUANTITY
FROM
    NZ_DUPLICATE_PKS
WHERE
    CURRENT_RECORD_INDICATOR = 'Y'
GROUP BY
    1
    ,2
    ,3
ORDER BY
    1
    ,2
    ,3;

这总结了一切。我希望有些人觉得它有用。我们已经在这种方法上取得了很大进展。在这一点上,你可能想知道我为什么要这么麻烦。我讨厌违反 PK 的行为被允许进入我们的仓库,我希望有一个全面的方法来根除它们。上述过程已经在我们的生产环境中每天运行了几个月。我们有约 350 个声明了主键的表,大小从 5 行维度到约 2 亿行事实 @ 10Gb。对于 Netezza 来说,这是一笔不小的开支。在我们的 Mustang NPS 10050 上,整个过程不到 10 分钟。

【讨论】:

【参考方案2】:

我们最初在数据仓库中编写了一个存储过程来处理这个问题。 虽然它在理论上有效,但对于新西兰常见的大型表来说有点慢(500M 记录表大约需要 10 分钟)。

我将在下面解释存储过程,但现在我想说的是,我们现在甚至没有利用过程,因为我们的增量/更新加载明确地只插入目标中不存在的记录数据库。 (对于 upsert,我们基本上只是在插入之前删除要插入的记录集中存在的任何记录)。

这有它自己的问题,特别是因为 NZ 不喜欢删除并且需要不断整理表以实际回收空间,更不用说我们的 upsert 可能会通过删除旧记录而丢失历史列数据自从被改变(我们有其他进程来加载我们想要跟踪的缓慢变化的维度。)

无论如何,约束存储过程看起来像:

check_constraints('table','constraint_type') returns boolean

并且基本上循环通过:

  select constraintname, contype, attname
    from _v_relation_keydata
   where relation = upper(p_table) order by constraintname, conseq;

获取应比较的列。对于每个约束,它会运行如下内容:

  select count(*) 
    from ( 
    select distinct col1,col2,...
      from p_table 
     where col1 is not null 
       and col2 is not null... );

并将该数字与

进行比较
  select count(*) 
    from p_table 
   where col1 is not null 
     and col2 is not null...;

如果它们不同,我们会引发异常。

【讨论】:

有趣,你们是否采取了相同的方法来强制执行“唯一”约束? 是的,我们调用 check_constrants('table', ),其中 是 'unique'、'primary' 或 'all' 之一。基于此,我们将 _v_relation_keydata 的查询限制为 contype='u' 或 contype='p'。【参考方案3】:

为什么不添加一个列,该列是您希望应用约束的列的哈希值? NZ SQL 扩展包具有散列函数。如果您使用 hash8('key') ,其中 'key' 是受约束的列值的连接(强制转换为 varchar),那么您可能会得到一个唯一的 BIGINT 值。我说“可能”是因为哈希冲突的概率是有限的——尽管它很低。如果您需要更强的唯一性保证,您可以使用加密级别的“hash()”函数。

但是,除非您拥有大量数据,否则 hash8 应该是更好的选择。因为它给出整数值,所以行的比较是一个简单的算术问题,因此速度很快。因此,例如您可以根据哈希值更新或插入行,这只是一个整数比较。

【讨论】:

【参考方案4】:

Netezza 是一个数据仓库。它没有 PK、FK、约束、UK、SK 等。它是一个数据仓库——它不应该有。所有表格都应该是平面表格,因为它是 MPP - 分布式的。查找分发键。仅分区。它是并行处理——有点像 Map Reduce。将您的联接放入关系数据库(具有键、索引),然后将您的扁平表移动到 Netezza。 https://docs.microsoft.com/en-us/sql/analytics-platform-system/staging-database?view=aps-pdw-2016-au7#:~:text=A%20staging%20database%20is%20a,is%20loaded%20into%20the%20appliance.&text=For%20example%2C%20an%20ELT%20process,into%20the%20target%20fact%20table。 不要在 Netezza 中进行任何数据建模。使用甲骨文。 Netezza 确实有主键和外键约束,甚至可能有唯一键——但它们是不活动的——这也是故意的——它被调整为在整个表中并行运行所有内容。 Snowflake 更宽容,因为它在水平和垂直方向上都进行了分区,并且具有类似集群的并行处理,因此它并行运行非常快(可能不如 Netezza 好 - 或便宜),但 Snowflake 可能要好得多在过滤时。

我在“我们仓库中的主键声明背后的业务决策”上方看到一条评论 - 所以做出这些决定的人是上面的评论。当然,除非你在 Exadata 中构建你的仓库,它确实有索引,但我知道一些 Exadata 数据库删除了所有索引后速度会大大加快。

【讨论】:

以上是关于如何克服 Netezza 缺乏唯一约束/参照完整性执行的问题?的主要内容,如果未能解决你的问题,请参考以下文章

完整性约束语法定义

SQL中的外部键约束有啥用?

oracle入门数据完整性约束

数据库完整性和约束

如何通过关系代数表达参照完整性约束?

Redshift 中如何保持参照完整性?