SQLite的 – UPSERT *不*插入或replace

http://en.wikipedia.org/wiki/Upsert

在SQL Server中插入更新存储过程

有没有一些聪明的方式来做到这一点,我没有想到的SQLite?

基本上我想更新四列中的三个,如果logging存在,如果它不存在,我想插入logging与第四列的默认(NUL)值。

ID是一个主键,所以只有一个logging到UPSERT。

(我试图避免SELECT的开销,以确定如果我需要更新或INSERT显然)

build议?

假设表中有3列.. ID,NAME,ROLE


坏的:这将插入或replaceID = 1的新值的所有列:

INSERT OR REPLACE INTO Employee (id, name, role) VALUES (1, 'John Foo', 'CEO'); 

坏的:这将插入或replace2列… NAME列将被设置为NULL或默认值:

 INSERT OR REPLACE INTO Employee (id, role) VALUES (1, 'code monkey'); 

好的:这将更新2列。 当ID = 1时,NAME将不受影响。 当ID = 1不存在时,名称将为默认(NULL)。

 INSERT OR REPLACE INTO Employee (id, role, name) VALUES ( 1, 'code monkey', (SELECT name FROM Employee WHERE id = 1) ); 

这将更新2列。 当ID = 1时,ROLE将不受影响。 当ID = 1不存在时,angular色将被设置为“Benchwarmer”而不是默认值。

 INSERT OR REPLACE INTO Employee (id, name, role) VALUES ( 1, 'Susan Bar', COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer') ); 

INSERT或REPLACE 等于“UPSERT”。

假设我的表Employee具有字段id,名称和angular色:

 INSERT OR REPLACE INTO Employee ("id", "name", "role") VALUES (1, "John Foo", "CEO") INSERT OR REPLACE INTO Employee ("id", "role") VALUES (1, "code monkey") 

繁荣,你已经失去了第一号员工的名字.SQLite已经用默认值取代了它。

UPSERT的预期输出将是改变angular色并保持名称。

埃里克B的答案是好的,如果你想保留现有的行只有一个或两列。 如果你想保留很多列,那就太麻烦了。

这个方法可以很好地适应任何一边的任意数量的列。 为了说明这一点,我将假定以下模式:

  CREATE TABLE page ( id INTEGER PRIMARY KEY, name TEXT UNIQUE, title TEXT, content TEXT, author INTEGER NOT NULL REFERENCES user (id), ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); 

特别注意, name是行的自然键 – id仅用于外键,因此当插入新行时,SQLite自己selectID值。 但是,当基于name更新现有的行时,我希望它继续具有旧的ID值(显然!)。

我用下面的构造实现了一个真正的UPSERT

  WITH new (name, title, author) AS ( VALUES('about', 'About this site', 42) ) INSERT OR REPLACE INTO page (id, name, title, content, author) SELECT old.id, new.name, new.title, old.content, new.author FROM new LEFT JOIN page AS old ON new.name = old.name; 

这个查询的确切forms可以有所不同。 关键是使用带有左外连接的INSERT SELECT来将现有行连接到新值。

在这里,如果一行以前不存在, old.id将为NULL ,SQLite将自动分配一个ID,但是如果已经存在这样的行, old.id将会有一个实际的值,这将被重用。 这正是我想要的。

其实这是非常灵活的。 注意ts列在所有方面是完全缺失的 – 因为它有一个DEFAULT值,所以SQLite在任何情况下都会做正确的事情,所以我不必亲自处理。

你也可以在newold方面都包含一个列,然后在外部SELECT使用例如COALESCE(new.content, old.content)来说“插入新的内容,否则保留旧的内容” -例如,如果您正在使用固定查询,并使用占位符绑定新值。

如果你正在做更新,我会..

  1. 开始交易
  2. 做更新
  3. 检查行数
  4. 如果是0,则执行插入操作
  5. 承诺

如果你通常在做插入,我会的

  1. 开始交易
  2. 尝试插入
  3. 检查主键违规错误
  4. 如果我们得到一个错误做更新
  5. 承诺

这样你避免了select,你在Sqlite上的事务声音。

我意识到这是一个古老的线程,但我一直在最近在sqlite3工作,并提出了这种方法更适合我的dynamic生成参数化查询的需求:

 insert or ignore into <table>(<primaryKey>, <column1>, <column2>, ...) values(<primaryKeyValue>, <value1>, <value2>, ...); update <table> set <column1>=<value1>, <column2>=<value2>, ... where changes()=0 and <primaryKey>=<primaryKeyValue>; 

它仍然是更新中的where子句的2个查询,但似乎是伎俩。 在我的脑海里,我也有这样的想法:如果对变化()的调用大于零,sqlite可以完全优化掉更新语句。 不pipe它是否真的这么做,超出了我的知识范围,但是一个人能够做到梦想吗? ;)

对于奖励积分,您可以附加这一行,返回行的id,无论是新插入的行还是现有的行。

 select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end; 

我知道我迟到了,但是……

 UPDATE employee SET role = 'code_monkey', name='fred' WHERE id = 1; INSERT INTO employee(id, role, name) values (1, 'code monkey, 'fred') WHERE changes() = 0; 

所以它试图更新,如果logging是那里然后改变()== 1,所以插入不是行动。

或者:

另一种完全不同的方式是:在我的应用程序中,我将内存rowID设置为long.MaxValue,当我在内存中创build行时。 (MaxValue永远不会被用作一个ID,你将不会活得够长….那么,如果rowID不是那个值,那么它必须已经在数据库中,所以需要一个UPDATE,如果它是MaxValue,那么它需要一个插入。这只有在您可以跟踪您的应用中的行ID时才有用。

这是一个真正是UPSERT(更新或插入)而不是INSERT或REPLACE(在许多情况下工作方式不同)的解决scheme。

它是这样工作的:
1.如果存在具有相同ID的logging,则尝试更新。
2.如果更新没有改变任何行( NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0) ),则插入logging。

因此,现有的logging被更新或插入将被执行。

重要的细节是使用changes()SQL函数来检查update语句是否碰到任何现有logging,如果没有碰到任何logging,只执行insert语句。

有一点要提到的是,changes()函数不会返回由较低级别触发器执行的更改(请参阅http://sqlite.org/lang_corefunc.html#changes ),因此请务必考虑这一点。

这是SQL …

testing更新:

 --Create sample table and records (and drop the table if it already exists) DROP TABLE IF EXISTS Contact; CREATE TABLE [Contact] ( [Id] INTEGER PRIMARY KEY, [Name] TEXT ); INSERT INTO Contact (Id, Name) VALUES (1, 'Mike'); INSERT INTO Contact (Id, Name) VALUES (2, 'John'); -- Try to update an existing record UPDATE Contact SET Name = 'Bob' WHERE Id = 2; -- If no record was changed by the update (meaning no record with the same Id existed), insert the record INSERT INTO Contact (Id, Name) SELECT 2, 'Bob' WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0); --See the result SELECT * FROM Contact; 

testing插入:

 --Create sample table and records (and drop the table if it already exists) DROP TABLE IF EXISTS Contact; CREATE TABLE [Contact] ( [Id] INTEGER PRIMARY KEY, [Name] TEXT ); INSERT INTO Contact (Id, Name) VALUES (1, 'Mike'); INSERT INTO Contact (Id, Name) VALUES (2, 'John'); -- Try to update an existing record UPDATE Contact SET Name = 'Bob' WHERE Id = 3; -- If no record was changed by the update (meaning no record with the same Id existed), insert the record INSERT INTO Contact (Id, Name) SELECT 3, 'Bob' WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0); --See the result SELECT * FROM Contact; 

扩展亚里士多德的答案,你可以select一个虚拟的“单身”表(一个自己创build一个单一的表)。 这避免了一些重复。

我还将示例保存在MySQL和SQLite中,并使用'date_added'列作为您如何仅在第一次设置列的示例。

  REPLACE INTO page ( id, name, title, content, author, date_added) SELECT old.id, "about", "About this site", old.content, 42, IFNULL(old.date_added,"21/05/2013") FROM singleton LEFT JOIN page AS old ON old.name = "about"; 

我知道的最好的方法是做一个更新,然后是插入。 “select的开销”是必要的,但是它不是一个可怕的负担,因为你正在search快速的主键。

你应该可以用你的表格和字段名称修改下面的语句来做你想做的事情。

 --first, update any matches UPDATE DESTINATION_TABLE DT SET MY_FIELD1 = ( SELECT MY_FIELD1 FROM SOURCE_TABLE ST WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY ) ,MY_FIELD2 = ( SELECT MY_FIELD2 FROM SOURCE_TABLE ST WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY ) WHERE EXISTS( SELECT ST2.PRIMARY_KEY FROM SOURCE_TABLE ST2 ,DESTINATION_TABLE DT2 WHERE ST2.PRIMARY_KEY = DT2.PRIMARY_KEY ); --second, insert any non-matches INSERT INTO DESTINATION_TABLE( MY_FIELD1 ,MY_FIELD2 ) SELECT ST.MY_FIELD1 ,NULL AS MY_FIELD2 --insert NULL into this field FROM SOURCE_TABLE ST WHERE NOT EXISTS( SELECT DT2.PRIMARY_KEY FROM DESTINATION_TABLE DT2 WHERE DT2.PRIMARY_KEY = ST.PRIMARY_KEY ); 

Mosor,

我无法确认SQLite网站上的CREATE语法。 我还没有build立一个演示来testing它,但似乎并不支持..

如果是这样,我有三列,所以它实际上看起来像:

 CREATE TABLE table1( id INTEGER PRIMARY KEY ON CONFLICT REPLACE, Blob1 BLOB ON CONFLICT REPLACE, Blob2 BLOB ON CONFLICT REPLACE, Blob3 BLOB ); 

但前两个斑点不会导致冲突,只有身份证将所以我的asusme Blob1和Blob2不会被取代(根据需要)

三宝,绑定数据是一个完整的事务SQLite中的更新,这意味着每个发送的行更新需要:准备/绑定/步骤/结束语句不像插入,允许使用复位function

陈述对象的生命是这样的:

  1. 使用sqlite3_prepare_v2()创build对象
  2. 使用sqlite3_bind_接口将值绑定到主机参数。
  3. 通过调用sqlite3_step()来运行SQL
  4. 使用sqlite3_reset()重置语句,然后返回到步骤2并重复。
  5. 使用sqlite3_finalize()销毁语句对象。

更新我猜是比较慢与INSERT,但它是如何比较SELECT使用主键?

也许我应该使用select来读取第四列(Blob3),然后使用REPLACE写一个新的logging,将原始的第四列与前三列的新数据混合?

如果有人想阅读我在cordovaSQLite解决scheme,我得到了这个通用的js方法感谢上面的@david答案。

 function addOrUpdateRecords(tableName, values, callback) { get_columnNames(tableName, function (data) { var columnNames = data; myDb.transaction(function (transaction) { var query_update = ""; var query_insert = ""; var update_string = "UPDATE " + tableName + " SET "; var insert_string = "INSERT INTO " + tableName + " SELECT "; myDb.transaction(function (transaction) { // Data from the array [[data1, ... datan],[()],[()]...]: $.each(values, function (index1, value1) { var sel_str = ""; var upd_str = ""; var remoteid = ""; $.each(value1, function (index2, value2) { if (index2 == 0) remoteid = value2; upd_str = upd_str + columnNames[index2] + "='" + value2 + "', "; sel_str = sel_str + "'" + value2 + "', "; }); sel_str = sel_str.substr(0, sel_str.length - 2); sel_str = sel_str + " WHERE NOT EXISTS(SELECT changes() AS change FROM "+tableName+" WHERE change <> 0);"; upd_str = upd_str.substr(0, upd_str.length - 2); upd_str = upd_str + " WHERE remoteid = '" + remoteid + "';"; query_update = update_string + upd_str; query_insert = insert_string + sel_str; // Start transaction: transaction.executeSql(query_update); transaction.executeSql(query_insert); }); }, function (error) { callback("Error: " + error); }, function () { callback("Success"); }); }); }); } 

所以,首先用这个函数拿起列名:

 function get_columnNames(tableName, callback) { myDb.transaction(function (transaction) { var query_exec = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name ='" + tableName + "'"; transaction.executeSql(query_exec, [], function (tx, results) { var columnParts = results.rows.item(0).sql.replace(/^[^\(]+\(([^\)]+)\)/g, '$1').split(','); ///// RegEx var columnNames = []; for (i in columnParts) { if (typeof columnParts[i] === 'string') columnNames.push(columnParts[i].split(" ")[0]); }; callback(columnNames); }); }); } 

然后以编程方式构build交易。

“值”是您之前应该构build的数组,它表示要插入或更新到表中的行。

“remoteid”是我用作参考的id,因为我正在与远程服务器同步。

对于使用SQLite Cordova插件,请参考官方链接

我认为这可能是你正在寻找的: ON CONFLICT条款 。

如果你这样定义你的表格:

 CREATE TABLE table1( id INTEGER PRIMARY KEY ON CONFLICT REPLACE, field1 TEXT ); 

现在,如果你使用已经存在的ID进行INSERT,SQLite会自动执行UPDATE而不是INSERT。

心连心…

这种方法混合了一些其他方法从这个问题的答案,并结合使用CTE(公用表expression式)。 我将介绍查询,然后解释为什么我做了我所做的。

如果有员工300,我想将员工300的姓氏更改为DAVIS。否则,我将添加一个新员工。

表名称:员工列:id,first_name,last_name

查询是:

 INSERT OR REPLACE INTO employees (employee_id, first_name, last_name) WITH registered_employees AS ( --CTE for checking if the row exists or not SELECT --this is needed to ensure that the null row comes second * FROM ( SELECT --an existing row * FROM employees WHERE employee_id = '300' UNION SELECT --a dummy row if the original cannot be found NULL AS employee_id, NULL AS first_name, NULL AS last_name ) ORDER BY employee_id IS NULL --we want nulls to be last LIMIT 1 --we only want one row from this statement ) SELECT --this is where you provide defaults for what you would like to insert registered_employees.employee_id, --if this is null the SQLite default will be used COALESCE(registered_employees.first_name, 'SALLY'), 'DAVIS' FROM registered_employees ; 

基本上,我使用CTE来减lessselect语句必须用来确定默认值的次数。 由于这是一个CTE,我们只需从表中select我们想要的列,INSERT语句就使用它。

现在你可以通过在COALESCE函数中replace空值来决定要使用的默认值。

刚读过这个post,对这个“UPSERT”不太容易,我进一步调查了…

实际上,你可以在SQLITE中直接轻松地做到这一点。

而不是使用: INSERT INTO

使用: INSERT OR REPLACE INTO

这正是你想要它做的!

 SELECT COUNT(*) FROM table1 WHERE id = 1; 

如果COUNT(*) = 0

 INSERT INTO table1(col1, col2, cole) VALUES(var1,var2,var3); 

否则如果COUNT(*) > 0

 UPDATE table1 SET col1 = var4, col2 = var5, col3 = var6 WHERE id = 1;