【问题标题】:SQLite: mass updating a field without cursorSQLite:批量更新没有游标的字段
【发布时间】:2011-05-10 12:39:06
【问题描述】:

我有下表:

CREATE TABLE Records (
RecordIndex INTEGER NOT NULL,
...
Some other fields
...
Status1 INTEGER NOT NULL,
Status2 INTEGER NOT NULL,
UpdateDate DATETIME NOT NULL,
CONSTRAINT PK_Records PRIMARY KEY (RecordIndex ASC))

还有一个索引:

CREATE INDEX IDX_Records_Status ON ClientRecords
  (Status1 ASC, Status2 ASC, RecordIndex ASC)

我需要一个一个的去获取某个状态的记录,所以我用了这个语句:

SELECT *
FROM RECORDS
WHERE RecordIndex > @PreviousIndex
AND Status1 = @Status1
AND Status2 = @Status2
LIMIT 1

但是现在我需要获取按另一个字段排序的记录,但是这个字段对于每条记录都不是唯一的,所以我不能以相同的方式使用它。所以我决定在我的表中添加一个新的 SortIndex 字段。

由于 SQLite 中没有游标,我正在执行以下操作来初始化 SortIndex 的值。
首先我创建一个临时表:

CREATE TEMP TABLE Sort (
SortIdx INTEGER PRIMARY KEY AUTOINCREMENT,
RecordIdx INTEGER )

然后我按正确的排序顺序填写此表:

INSERT INTO Sort
  SELECT NULL, RecordIndex
  FROM Records
  ORDER BY SomeField ASC, RecordIndex ASC

然后我在临时表上创建一个索引:

CREATE INDEX IDX_Sort_RecordIdx ON Sort (RecordIdx ASC)

然后我更新 Records 表中的 SortIndex 字段:

UPDATE Records
SET SortIndex =
  (SELECT SortIdx
   FROM Sort
   WHERE RecordIdx = RecordIndex)

然后我删除临时表:

DROP TABLE Sort

最后我在我的记录表上创建了一个新索引

CREATE INDEX IDX_Records_Sort ON Records
  (Status1 ASC, Status2 ASC, SortIndex ASC)

这允许我做以下选择

SELECT *
FROM Records
WHERE SortIndex > @PreviousSortIndex
AND Status1 = @Status1
AND Status2 = @Status2
LIMIT 1

问题是,由于该表包含大约 500K 条记录,因此整个过程大约需要 2 分钟。使用游标初始化 SortIndex 可能会快很多,但是 SQLite 缺少此功能:(

有更快的方法吗?

提前致谢!

【问题讨论】:

    标签: sql performance sqlite


    【解决方案1】:

    您应该考虑 SQLite 的 INSERT OR REPLACE 功能,而不是使用相关子查询执行 UPDATE,当主键重复时,它将执行整行的 UPDATE

    UPDATE Records
       SET SortIndex =
           (SELECT SortIdx
              FROM Sort
             WHERE RecordIdx = RecordIndex) 
    

    变成

    INSERT OR REPLACE INTO Records (RecordIndex, SortIndex, ...)
    SELECT RecordIndex, SortIdx, ... FROM another_temporary_table_containing_all_columns.
    

    除了使用包含所有列的临时表,您当然可以使用连接旧表和新表的 SELECT:在 SQLite shell 中尝试此操作

    CREATE TABLE original (id INTEGER PRIMARY KEY, content TEXT);
    
    BEGIN TRANSACTION;
    INSERT INTO original(id, content) VALUES(1, 'foo');
    INSERT INTO original(id, content) VALUES(2, 'bar');
    INSERT INTO original(id, content) VALUES(3, 'baz');
    COMMIT TRANSACTION;
    
    CREATE TABLE id_remap(old_id INTEGER, new_id INTEGER);
    
    BEGIN TRANSACTION;
    INSERT INTO id_remap(old_id, new_id) VALUES(2,3);
    INSERT INTO id_remap(old_id, new_id) VALUES(3,2);
    COMMIT TRANSACTION;
    
    INSERT OR REPLACE INTO original (id, content)
    SELECT b.new_id, a.content
      FROM original a
     INNER JOIN id_remap b
        ON b.old_id = a.id;
    
    SELECT * FROM original;
    

    结果:

    1|foo
    2|baz
    3|bar
    

    如果您需要进行大量更新但不希望关联子查询,另一种选择是在视图中执行连接,并在该视图上创建触发器 INSTEAD OF UPDATE。一个问题是你不能有在这个过程中失败的约束。我想每行都会检查约束,这样可能会很慢。

    在 SQLite 外壳中:

    CREATE TABLE original (id INTEGER PRIMARY KEY, content TEXT);
    
    BEGIN TRANSACTION;
    INSERT INTO original(id, content) VALUES(1, 'foo');
    INSERT INTO original(id, content) VALUES(2, 'bar');
    INSERT INTO original(id, content) VALUES(3, 'baz');
    COMMIT TRANSACTION;
    
    CREATE TABLE id_remap(old_id INTEGER, new_id INTEGER);
    
    BEGIN TRANSACTION;
    INSERT INTO id_remap(old_id, new_id) VALUES(3,6);
    COMMIT TRANSACTION;
    
    CREATE TEMPORARY VIEW tmp_id_mapping
        AS
    SELECT a.content, b.old_id, b.new_id
      FROM original a
     INNER JOIN id_remap b
        ON b.old_id = a.id;
    
     CREATE TEMPORARY TRIGGER IF NOT EXISTS tmp_trig_id_remap
    INSTEAD OF UPDATE OF content ON tmp_id_mapping
        FOR EACH ROW
      BEGIN
        UPDATE original
           SET id = new.new_id
         WHERE id = new.old_id;
       END;
    
    UPDATE tmp_id_mapping
       SET content = 'hello';
    
    SELECT * FROM original;
    

    结果:

    1|foo
    2|bar
    6|baz
    

    【讨论】:

    • 谢谢,我不知道。但这并不可行,因为表非常大,它包含 500k+ 条记录和很多列,因此创建一个包含所有列的临时表需要大量内存。但无论如何我都会尝试一下,看看它是否真的快得多。谢谢。
    • 感谢您添加更多想法和示例。我尝试了您的第一个想法,但是当我尝试将整个表复制到临时表中时,SQLiteSpy 似乎挂起(您最喜欢的 SQLite 查询工具是什么?)。然后我尝试了加入的想法。由于我不想更改主键而只想添加一个新的 SortIndex 字段,所以我保留了我的 Sort 临时表并以与以前相同的方式填充它。然后我启动了更新INSERT OR REPLACE INTO Records(RecordIndex, ..., UpdateDate, SortIndex) SELECT RecordIndex, ..., UpdateDate, SortIdx) FROM Records INNER JOIN Sort ON RecordIdx = RecordIndex ...
    • ... 它的时间是我最初仅更新 SortIndex 字段的时间的两倍(请参阅初始问题)。除非我做错了什么,否则这似乎也不是解决方案。但是感谢您的想法!
    【解决方案2】:

    主要答案

    我认为,这不可能在 SQLlite 中快速插入约 50 万条带索引的记录(以及未来的许多索引)。

    希望有人在这里发明新轮子。


    马克,我认为你应该避免这种动态添加的索引,而只是添加其他经典索引,无论你多么需要它们。

    此外,游标在任何 DMBS 中并不总是好主意——仅当我们需要复杂的逻辑时,但在这里以简单的顺序排列,我认为它已经过时了。

    只需添加经典索引——即使它们不是唯一的。

    或者在这里发布更多关于你为什么要填写的详细信息,你应该选择一些动态的方式。

    另外sqlite,正如我see,支持偏移量。


    用于测试的 SQL

    -- init
    CREATE TABLE IF NOT EXISTS `records` (
      `RecordID` int(10) default NULL,
      `Status` int(10) default NULL,
      `SomeField` char(50) default NULL,
      `RecordIndex` int(11) default NULL
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
    
    truncate `records`;
    INSERT INTO `records` (`RecordID`, `Status`, `SomeField`, `RecordIndex`) VALUES
        (1, 1, 'a', 35),
        (2, 1, 'b', 20),
        (3, 1, 'c', 42);
    
    -- 1st select
    SELECT * FROM records WHERE Status = 1 ORDER BY SomeField ASC, RecordIndex ASC LIMIT 1 OFFSET 0;
    
    -- update
    update records set `Status` = 2 where RecordID = 1;
    
    -- select next
    SELECT * FROM records WHERE Status = 1 ORDER BY SomeField ASC, RecordIndex ASC LIMIT 1 OFFSET 1;
    

    【讨论】:

    • 首先,索引 IDX_Records_Sort 不是动态的。填表一次,以后不添加记录,所以SortIndex字段只需要“计算”一次,填表后立即创建索引。其次,选择“SELECT * FROM Records WHERE Status1 = @Status1 AND St​​atus2 = @Status2 ORDER BY SomeField ASC , ClientIndex ASC LIMIT 1 OFFSET @PreviousIndex”将不起作用,因为两种状态都可以更改,在这种情况下偏移量不会不再正确了。
    • @Marc,这种初始化多久发生一次? “一次”是什么意思?你也能描述一下你奇怪的逻辑:为什么在阅读过程中会发生一些变化?
    • @Alexander,表填充一次,批量插入,除了2个状态字段外,大部分字段永远不会改变(包括用于排序的字段)。用户希望按照排序字段的顺序在屏幕上查看具有特定状态的记录。他将依次查看每条记录并最终更新状态。然后他将显示“下一个”记录。因此,尽管他通过 2 个状态值选择了一条记录,但他可以在选择下一条记录之前更新状态值。这就是偏移方法不起作用的原因。谢谢。
    • @Marc,我仍然认为,这里有些过分了。即使用户更改了某些内容,索引也会变得不可用,对吗?您能否提供有关您的默认工作流程的更多详细信息 - 以及您对用户的每个操作使用哪些查询?
    • 我也没有t think, that SELECT * FROM Records WHERE Status1 = @Status1 AND St​​atus2 = @Status2 ORDER BY SomeField ASC , ClientIndex ASC LIMIT 1 OFFSET @PreviousIndex` 将不起作用 - 因为它直接一条线。那你对我们隐瞒另一件重要的事? :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-18
    • 1970-01-01
    • 1970-01-01
    • 2020-11-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多