【问题标题】:How to normalize data efficently while INSERTing into SQL table (Postgres)如何在插入 SQL 表(Postgres)时有效地规范化数据
【发布时间】:2014-07-05 16:10:26
【问题描述】:

我想将一个大的日志文件导入 (Postgres-)SQL

某些字符串列非常重复,例如列 'event_type' 有 10 个不同的字符串值中的 1 个。

我对规范化数据有一个粗略的了解。

首先,以下假设是否正确:将 event_type 存储在单独的表中(可能具有外键关系)是有益的(对于存储大小、索引和查询速度)?

为了规范化,我必须在原始日志中检查 event_type 的不同值并将它们插入 event_types 表中。

有很多字段类型,例如 event_types。

那么其次:有没有办法告诉数据库在插入数据时创建和维护这种表?

还有其他策略可以实现这一点吗?我正在和熊猫一起工作。

【问题讨论】:

  • 这是 PostgreSQL 拥有 upsert ... returning 的众多案例之一。遗憾的是,我们没有。
  • 有可能使用重写规则来实现这一点,但这是一种非常脆弱(并且容易出错......)的方法。最好先将 1to1 导入“暂存”表,然后从那里进行插入/更新。

标签: python sql postgresql pandas


【解决方案1】:

这是开始从以前以其他方式存储的数据(例如在日志文件中)构建数据库时的典型情况。有一个解决方案 - 像往常一样 - 但它不是一个非常快的解决方案。也许您可以编写一个日志消息处理程序来处理传入的消息;如果通量(消息/秒)不太大,您不会注意到开销,特别是如果您忘记将消息写入平面文本文件时。

首先,关于规范化的问题。是的,您应该始终规范化并使用所谓的第三范式 (3NF)。这基本上意味着任何类型的真实世界数据(例如您的 event_type)都只存储一次。 (在某些情况下,您可以稍微放松一下并转到 2NF - 通常仅当实际数据需要很少的存储空间时,例如 ISO 国家代码、M/F(男性/女性)选择等 - 但是在大多数其他情况下,3NF 会更好。)

在您的具体情况下,假设您的 event_type 是 char(20) 类型。十个这样的事件及其对应的int 代码很容易放在单个数据库页面上,通常是 4kB 的磁盘空间。如果您有 1,000 条 event_type 为 char(20) 的日志消息,那么您需要 20kB 来存储该信息,或者五个数据库页面。如果您的日志消息中有其他此类项目,则存储减少会相应变大。其他项目(例如 datetimestamp)可以以其原始格式(分别为 4 和 8 字节)存储,以获得更小的存储空间、更好的性能和更多的功能(例如比较日期或查看范围)。

其次,你不能告诉数据库创建这样的表,你必须自己做。但是一旦创建,存储过程就可以解析您的日志消息并将数据放入正确的表中。

对于日志消息,您可以执行以下操作(假设您想在数据库中进行解析,而不是在 python 中):

CREATE FUNCTION ingest_log_message(mess text) RETURNS int AS $$
DECLARE
  parts  text[];
  et_id  int;
  log_id int;
BEGIN
  parts := regexp_split_to_array(mess, ','); -- Whatever your delimiter is

  -- Assuming:
  --   parts[1] is a timestamp
  --   parts[2] is your event_type
  --   parts[3] is the actual message

  -- Get the event_type identifier. If event_type is new, INSERT it, else just get the id.
  -- Do likewise with other log message parts whose unique text is located in a separate table.
  SELECT id INTO et_id
  FROM event_type
  WHERE type_text = quote_literal(parts[2]);
  IF NOT FOUND THEN
    INSERT INTO event_type (type_text)
    VALUES (quote_literal(parts[2]))
    RETURNING id INTO et_id;
  END IF;

  -- Now insert the log message
  INSERT INTO log_message (dt, et, msg)
  VALUES (parts[1]::timestamp, et_id, quote_literal(parts[3]))
  RETURNING id INTO log_id;

  RETURN log_id;
END; $$ LANGUAGE plpgsql STRICT;

您需要的表格是:

CREATE TABLE event_type (
  id        serial PRIMARY KEY,
  type_text char(20)
);

CREATE TABLE log_message (
  id        serial PRIMARY KEY,
  dt        timestamp,
  et        integer REFERENCES event_type
  msg       text
);

然后您可以将这个函数作为一个简单的SELECT 语句调用,它将返回新插入日志消息的id

SELECT * FROM ingest_log_message(the_message);

注意函数体中quote_literal()函数的使用。这有两个重要的功能:(1)字符串中的引号被正确转义(这样“不是”之类的词就不会弄乱命令); (2) 防止日志消息的恶意生成器注入SQL。

显然,以上所有内容都需要根据您的具体情况量身定制。

【讨论】:

  • a) 规范化与一个值可能需要多少存储空间没有任何关系。 b) 规范化没有为不同的数据类型提供不同的指导;通过 BCNF 进行规范化是基于函数依赖,而不是数据类型。 c) 规范化并不意味着“用 ID 号替换文本”。 d) 您的“event_type”表允许重复数据。
  • @MikeSherrill'CatRecall':非常正确,但首先,我不认为我写了你在这里评论的任何东西 (a)、(b) 和 (c) ,其次,这不应该是规范化的教程。
  • 阅读您的第三段和“event_type”的 CREATE TABLE 语句。您的答案不应该是规范化教程这一事实并不能成为事实错误的借口。
  • 函数逻辑避免重复插入。如果有其他途径导致将记录插入该表 - 在 OP 中不是问题 - 那么可以添加 UNIQUE 约束。
  • 专业(和狂热)DBA 不依赖过程代码来做not null unique 可以做的事情。
猜你喜欢
  • 1970-01-01
  • 2021-09-05
  • 2021-10-09
  • 1970-01-01
  • 2021-08-31
  • 2015-11-13
  • 2014-12-31
  • 2019-12-19
  • 1970-01-01
相关资源
最近更新 更多