【问题标题】:Optimize INSERT / UPDATE / DELETE operation优化 INSERT/UPDATE/DELETE 操作
【发布时间】:2013-10-25 03:33:23
【问题描述】:

我想知道是否可以以某种方式优化以下脚本。它确实会向磁盘写入很多内容,因为它可能会删除最新的行并重新插入它们。我正在考虑应用类似“在重复键更新上插入...”之类的东西,并发现了一些单行更新的可能性,但我不知道如何在 INSERT INTO ... SELECT query 的上下文中应用它。

CREATE OR REPLACE FUNCTION update_member_search_index() RETURNS VOID AS $$
DECLARE
   member_content_type_id INTEGER;
BEGIN
   member_content_type_id :=
      (SELECT id FROM django_content_type
       WHERE app_label='web' AND model='member');

   DELETE FROM watson_searchentry WHERE content_type_id = member_content_type_id;

   INSERT INTO watson_searchentry (engine_slug, content_type_id, object_id
                                 , object_id_int, title, description, content
                                 , url, meta_encoded)
   SELECT 'default',
         member_content_type_id,
         web_member.id,
         web_member.id,
         web_member.name,
         '',
         web_user.email||' '||web_member.normalized_name||' '||web_country.name,
         '',
         '{}'
   FROM web_member
   INNER JOIN web_user ON (web_member.user_id = web_user.id)
   INNER JOIN web_country ON (web_member.country_id = web_country.id)
   WHERE web_user.is_active=TRUE;
END;
$$ LANGUAGE plpgsql;

编辑:web_memberwatson_searchentryweb_userweb_country 的架构:http://pastebin.com/3tRVPPVi

主要是更新watson_searchentry中的titlecontent列。表上有一个触发器,根据这些列设置列 search_tsv 的值。

watson_searchentry 中的(content_type_id, object_id_int) 是表中的唯一对,但 atm 索引不存在(没有用处)。

该脚本每天最多运行一次,以完全重建搜索索引,偶尔在导入一些数据后运行。

【问题讨论】:

  • 用纯 SQL 重写它似乎是可行的。 (您不需要member_content_type_id 变量,它可以通过删除和插入查询中的额外术语获得) WRT upsert:只是一个更新,然后是一个插入(不存在的地方)就可以了。或者使用 RETURNING 构造。
  • 你的意思是'default',,还是DEFAULT? IOW:请添加表定义。
  • @wildplasser:我的意思是“默认”。我添加了更多信息。

标签: sql postgresql optimization plpgsql common-table-expression


【解决方案1】:

修改表定义

如果您确实需要将这些列设为 NOT NULL,并且您确实需要将字符串 'default' 作为 engine_slug 的默认值,我建议您引入列默认值:

COLUMN           |          TYPE           |      Modifiers
-----------------+-------------------------+---------------------
 id              | INTEGER                 | NOT NULL DEFAULT ... 
 engine_slug     | CHARACTER VARYING(200)  | NOT NULL DEFAULT 'default'
 content_type_id | INTEGER                 | NOT NULL
 object_id       | text                    | NOT NULL
 object_id_int   | INTEGER                 |
 title           | CHARACTER VARYING(1000) | NOT NULL
 description     | text                    | NOT NULL DEFAULT ''
 content         | text                    | NOT NULL
 url             | CHARACTER VARYING(1000) | NOT NULL DEFAULT ''
 meta_encoded    | text                    | NOT NULL DEFAULT '{}'
 search_tsv      | tsvector                | NOT NULL
 ...

DDL 语句将是:

ALTER TABLE watson_searchentry ALTER COLUMN  engine_slug DEFAULT 'default';

等等

那么您不必每次都手动插入这些值。

还有:object_id text NOT NULL, object_id_int INTEGER?这很奇怪。我猜你有你的理由......

我会满足你的更新要求:

重点是更新watson_searchentry中的titlecontent

当然,您必须添加 UNIQUE 约束来强制执行您的要求:

ALTER TABLE watson_searchentry
ADD CONSTRAINT ws_uni UNIQUE (content_type_id, object_id_int)

将使用随附的索引。通过这个查询开始。

顺便说一句,我几乎从不在 Postgres 中使用 varchar(n)。只需textHere's one reason.

查询data-modifying CTEs

这可以用数据修改公用表表达式重写为单个 SQL 查询,也称为“可写”CTE。需要 Postgres 9.1 或更高版本。
此外,此查询仅删除必须删除的内容,并更新可以更新的内容。

WITH  ctyp AS (
   SELECT id AS content_type_id
   FROM   django_content_type
   WHERE  app_label = 'web'
   AND    model = 'member'
   )
, sel AS (
   SELECT ctyp.content_type_id
         ,m.id       AS object_id_int
         ,m.id::text AS object_id       -- explicit cast!
         ,m.name     AS title
         ,concat_ws(' ', u.email,m.normalized_name,c.name) AS content
         -- other columns have column default now.
   FROM   web_user    u
   JOIN   web_member  m  ON m.user_id = u.id
   JOIN   web_country c  ON c.id = m.country_id
   CROSS  JOIN ctyp
   WHERE  u.is_active
   )
, del AS (     -- only if you want to del all other entries of same type
   DELETE FROM watson_searchentry w
   USING  ctyp
   WHERE  w.content_type_id = ctyp.content_type_id
   AND    NOT EXISTS (
      SELECT 1
      FROM   sel
      WHERE  sel.object_id_int = w.object_id_int
      )
   )
, up AS (      -- update existing rows
   UPDATE watson_searchentry 
   SET    object_id = s.object_id
         ,title     = s.title
         ,content   = s.content
   FROM   sel s
   WHERE  w.content_type_id = s.content_type_id
   AND    w.object_id_int   = s.object_id_int
   )
               -- insert new rows
INSERT  INTO watson_searchentry (
        content_type_id, object_id_int, object_id, title, content)
SELECT  sel.*  -- safe to use, because col list is defined accordingly above
FROM    sel
LEFT    JOIN watson_searchentry w1 USING (content_type_id, object_id_int)
WHERE   w1.content_type_id IS NULL;
  • django_content_type 上的子查询总是返回一个值?否则,CROSS JOIN 可能会引起麻烦。

  • 第一个 CTE sel 收集要插入的行。请注意我如何选择匹配的列名来简化事情。

  • 在 CTE del 中,我避免删除可以更新的行。

  • 在 CTE up 中,这些行改为更新。

  • 因此,我避免在最终的INSERT 中插入之前未删除的行。

可以很容易地封装到 SQL 或 PL/pgSQL 函数中以供重复使用。

对于大量并发使用不安全。比您拥有的功能要好得多,但对于并发写入仍然不是 100% 健壮。但根据您更新的信息,这不是问题。

用 DELETE 和 INSERT 替换 UPDATE 可能会或可能不会更昂贵。由于MVCC model,在内部每次更新都会产生一个新的行版本。

速度优先

如果您并不真正关心保留旧行,则更简单的方法可能会更快:删除所有内容并插入新行。此外,包装到 plpgsql 函数中可以节省一些规划开销。您的功能基本上是一些小的简化并遵守上面添加的默认值:

CREATE OR REPLACE FUNCTION update_member_search_index()
  RETURNS VOID AS
$func$
DECLARE
   _ctype_id int := (
      SELECT id
      FROM   django_content_type
      WHERE  app_label='web'
      AND    model = 'member'
      );  -- you can assign at declaration time. saves another statement
BEGIN
   DELETE FROM watson_searchentry
   WHERE content_type_id = _ctype_id;

   INSERT INTO watson_searchentry
         (content_type_id, object_id, object_id_int, title, content)
   SELECT _ctype_id, m.id, m.id::int,m.name
         ,u.email || ' ' || m.normalized_name || ' ' || c.name
   FROM   web_member  m
   JOIN   web_user    u USING (user_id)
   JOIN   web_country c ON c.id = m.country_id
   WHERE  u.is_active;
END
$func$ LANGUAGE plpgsql;

我什至不使用concat_ws():它对NULL 值是安全的,并且可以简化代码,但比简单的串联要慢一些。

还有:

表上有一个触发器设置列search_tsv的值 基于这些列。

将逻辑合并到此函数中会更快 - 如果这是唯一需要触发器的时间。否则,它可能不值得大惊小怪。

【讨论】:

  • 嘿,你避开了RETURNING 构造。干得好(还有 CTE!)
  • 顺便说一句,del CTE 从未被引用过。那里有错字,还是我误读了?
  • 从未见过这样的说法:)。但我收到错误 atm: ERROR: failed to find conversion function from unknown to character varying 在第 3 行附近。我在我的问题中添加了更多信息。
  • 好吧,你没有显示表定义,所以 Erwin 不得不做一些猜测。 (不是每个人都知道 django 数据模型)您可能需要在某处添加一个 to/from text/varchar。
  • 谢谢您,您对触发器的看法是正确的,移除它可以将时间缩短 5 秒(从 44 秒缩短到 39 秒)。否则没有真正的区别,这可能意味着功能无法显着优化。谢谢你的帮助。我学到了很多。
猜你喜欢
  • 2011-01-17
  • 1970-01-01
  • 2011-12-02
  • 1970-01-01
  • 1970-01-01
  • 2011-05-16
  • 1970-01-01
  • 2022-01-27
  • 2013-08-29
相关资源
最近更新 更多