SQLite: mass updating a field without cursor - sql

I have the following table:
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))
And an Index:
CREATE INDEX IDX_Records_Status ON ClientRecords
(Status1 ASC, Status2 ASC, RecordIndex ASC)
I need to fetch the records of a certain status one by one, so i used this statement:
SELECT *
FROM RECORDS
WHERE RecordIndex > #PreviousIndex
AND Status1 = #Status1
AND Status2 = #Status2
LIMIT 1
But now I need to fetch the records sorted by another field, but this field is not unique for each record, so I can not use it in the same way. So I decided to add a new SortIndex field to my table.
As there are no cursors in SQLite, I am doing the following to initialize the values for SortIndex.
First I create a temporary table:
CREATE TEMP TABLE Sort (
SortIdx INTEGER PRIMARY KEY AUTOINCREMENT,
RecordIdx INTEGER )
Then I fill this table in the correct sort order:
INSERT INTO Sort
SELECT NULL, RecordIndex
FROM Records
ORDER BY SomeField ASC, RecordIndex ASC
Then I create an index on the temporary table:
CREATE INDEX IDX_Sort_RecordIdx ON Sort (RecordIdx ASC)
Then I update the SortIndex field in my Records table:
UPDATE Records
SET SortIndex =
(SELECT SortIdx
FROM Sort
WHERE RecordIdx = RecordIndex)
Then I drop the temporary table:
DROP TABLE Sort
And finaly I create a new index on my Records table
CREATE INDEX IDX_Records_Sort ON Records
(Status1 ASC, Status2 ASC, SortIndex ASC)
This allows me to do the following select
SELECT *
FROM Records
WHERE SortIndex > #PreviousSortIndex
AND Status1 = #Status1
AND Status2 = #Status2
LIMIT 1
The problem is, as the table contains around 500K records the whole thing takes around 2 minutes. It would probably have been a lot faster to initialize SortIndex with a cursor, but SQLite lacks this feature :(
Is there a faster way to do this ?
Thanks in advance !

Instead of doing an UPDATE with a correlated subquery, you should consider the INSERT OR REPLACE feature of SQLite, which will perform an UPDATE of a whole row when the primary key is a duplicate:
UPDATE Records
SET SortIndex =
(SELECT SortIdx
FROM Sort
WHERE RecordIdx = RecordIndex)
becomes
INSERT OR REPLACE INTO Records (RecordIndex, SortIndex, ...)
SELECT RecordIndex, SortIdx, ... FROM another_temporary_table_containing_all_columns.
Instead of using a temporary table containing all columns you can of course use a SELECT which joins the old table and the new one: try this inside the 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;
Result:
1|foo
2|baz
3|bar
Another option if you need to do mass updates but do not want a correlated subquery is to perform the join in a view, and to create a trigger INSTEAD OF UPDATE on that view. A problem is that you cannot have constraints that fail during the process. I suppose that the constraints are checked for each row so that might be very slow.
In the 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(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;
Result:
1|foo
2|bar
6|baz

Main answer
I think, that this impossible to have quick insert in SQLlite ~500k records with indexes (and many indexes in future).
I hope someone will invent new wheel here.
Mark, I think you should avoid this type of dynamically added indexes and just add other classic indexes, no matter how much do you need them.
Also cursors not always good idea in any DMBS -- only if we need complex logic, but here in simple order by, I think it`s over-ing.
Just add classic indexes -- even if they non unique.
Or post here more details about why exactly you fill, that you should choose some dynamic way.
Also sqlite, as I see, supports offset.
SQL for tests
-- 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;

Related

How to insert a query into SQLite with an autoincrementing value for each row

Suppose I am inserting the following queryset into a new table in SQLite:
CREATE TABLE queryset_cache AS
SELECT ROW_NUMBER() over () AS rowid, * FROM mytable ORDER BY product;
Is it possible to either:
Set the rowid as auto-incrementing PK in sqlite from the insert, or;
Exclude the rowid and have SQLite auto-add in an autoincrementing primary key for each inserted record.
How would this be done?
Currently, without that when I do the insert, the rowid is not indexed.
rowid is already there. You can just do:
CREATE TABLE queryset_cache AS
SELECT t.*
FROM mytable t
ORDER BY product;
You will see it if you do:
SELECT rowid, t.*
FROM queryset_cache;
Here is a db<>fiddle
Auo increment should solve this. Documentation here:
https://www.sqlite.org/autoinc.html
Create source table:
create table sourceTable (oldID integer, data TEXT);
Add source data:
insert into sourceTable values(7, "x");
insert into sourceTable values(8, "y");
insert into sourceTable values(9, "z");
Create target table with auto-increment:
create table target(newID INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT);
Move data from source to target:
insert into target select null, data from sourceTable
If we have a table like:
create table employee (empID integer, name text , address text);
insert data into this table.
create a table in which you want to insert employee table data:
create table newEmployee (newempID integer PRIMARY KEY, name text , address text);
copy data to newEmployee table:
insert into newEmployee select * from employee
(select * from employee) to copy all the columns

Generate ID for duplicate values in sql server

I found following link to assign identical ID to duplicates in SQL server,
my understanding there is no sql server function to automatically generate it rather than using insert and update queries in link attached, is that statement True, if yes, then what would be the trigger if for example someone insert data to MyTable then run insert and update query from link:
Assign identical ID to duplicates in SQL server
INSERT INTO secondTable (word) SELECT distinct word FROM MyTable;
UPDATE MyTable SET ID = (SELECT id from secondTable where MyTable.word = secondTable.word)
thanks,
S
Is this what you want? I can't think of an "automatic" solution that would just increase the Id for new words.
CREATE TABLE MyTable (
Id INT NOT NULL,
Word NVARCHAR(255) NOT NULL
PRIMARY KEY (Id, Word)); -- primary key will make it impossible to have more than one combination of word and id
DECLARE #word NVARCHAR(255) = 'Hello!';
-- Get existing id or calculate a new id
DECLARE #Id INT = (SELECT Id FROM MyTable WHERE Word = #word);
IF(#id IS NULL) SET #Id = (SELECT MAX(Id) + 1 FROM MyTable);
INSERT INTO MyTable (Id, Word)
VALUES (#id, #word)
SELECT * FROM MyTable
If you can't for some reason have id and word as a combined primary key, you may use an unique index to make sure that there is only one combination

SQL Server: return joined data from insert select

I perform steps:
Create temporal table and fill it with data and unique order column [_oid]
Insert everything from temporal table into real table except fictional [_oid], outputting generated [id]'s
Return those generated [id]'s along with corresponding [_oid]
SQL:
CREATE TABLE #temp
(
[Hash] INT NOT NULL,
[Size] INT NOT NULL,
[Data] NVARCHAR(MAX),
[_oid] INT NOT NULL
)
--here insert data into #temp--
INSERT [dbo].[TestObjects]
OUTPUT INSERTED.[Id]
SELECT [Hash], [Size], [Data]
FROM #temp
DROP TABLE #temp
How I can return ([Id], [_oid]) rows ? ....Or at least return [Id] ordered by [_oid] ?
I know insert does not preserve order of inserted items in it's output, but still...
I think you what you are asking for is INSERT INTO, as so:
INSERT INTO [dbo].[TestObjects]
SELECT Hash, Size, Data FROM #temp
ORDER BY _oid
But as you say, there's no guarantee about order when you select from TestObjects, so if it's important can you not have a field in TestObjects you can ORDER BY when you SELECT from it?
IF your insert into #temp is such that both o_id and (hash,size,data) are unique for each row (ie keys), then you could retrieve the inserted o_id from #temp:
select t.[_oid],to.[Id]
from #temp t
inner join [dbo].[TestObjects] to
on t.Hash=to.Hash and t.Size=to.Size and t.data=to.data
As noted by George Menoutis, I did merge:
MERGE [dbo].[TestObjects] AS T_Base
USING #temp AS T_Source
ON (0<>0)
WHEN NOT MATCHED THEN INSERT ([Hash],[Size],[Data]) VALUES (T_Source.[Hash],T_Source.[Size],T_Source.[Data])
OUTPUT INSERTED.[Id], T_Source.[_oid];
If anyone have better approach - feel free to contribute to this answer.

Use autogenerated column to populate another column

How can I use an auto-generated column to populate another column during an INSERT statement?
Long story short: we are reusing a database table and an related ASP page to display completely different data than was originally intended.
I have a table similar in structure to the following. It's structure is out of my control.
ID int NON-NULL, IDENTITY(1,1)
OrderNo varchar(50) NON-NULL, UNIQUE
More ...
The table has been repurposed and we are not using the OrderNo column. However, it's NON-NULL and UNIQUE. As dummy data, I want to populate it with the row's ID column.
I have the following SQL so far, but can't work out how to use the row's generated ID.
INSERT INTO MyTable (OrderNo, More)
OUTPUT INSERTED.ID
VALUES (CAST(ID AS varchar(50)))
This just gives:
Msg 207, Level 16, State 1, Line 3
Invalid column name 'ID'.
Here's a solution using the OUTPUT clause. Unfortunately, you won't be able to do it in a single statement.
CREATE TABLE Orders (
ID int not null identity(1,1),
OrderNo varchar(50) not null unique
)
CREATE TABLE #NewIDs ( ID int )
INSERT Orders (OrderNo)
OUTPUT INSERTED.ID INTO #NewIDs
SELECT 12345
UPDATE o
SET o.OrderNo = i.ID
FROM Orders o
JOIN #NewIDs i
ON i.ID = o.ID
SELECT * FROM Orders
One option would be:
create trigger YourTable_Trigger
on YourTable
INSTEAD OF INSERT
as begin
INSERT INTO YourTable (OrderNo, AnotherField)
SELECT 0, AnotherField FROM Inserted
UPDATE YourTable SET OrderNo = SCOPE_IDENTITY() WHERE ID = SCOPE_IDENTITY()
end;
And here is the Fiddle.
Good luck.

inserting into A errors because of a foreign key contraint issue

Can someone help explain this to me and resolve it?
http://sqlfiddle.com/#!6/2adc7/9
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_tblMobileForms_tblForms". The conflict occurred in database "db_6_2adc7", table "dbo.tblForms", column 'fm_id'.: insert into tblMobileForms(fm_name) values ('lol')
My schema has the ID from tblMobileForms be a foreign key to tblForms.fm_id
To do what you are trying to do you cannot set up the FK on tblMobileForms as an identity. See my fiddle below for more information.
http://sqlfiddle.com/#!6/be6f7/2
Alternatively what you could do is to have tblMobileForms have it's own separate surrogate key and have a different FK column to the tblForms table.
The PK on the tblMobileForms table has the same name as the FK on the same table. Seeing the PK is an IDENTITY column, you can end up with non-matching values.
In my fiddle, the tblForms table contained IDs in the upper 60s. Running the INSERT in the child table would add a record with id 1, which does not exist in the parent table.
I'd create a new row in the tblMobileForms table, and reference that to the parent table.
You could use an INSTEAD OF trigger to apply a random ID to each mobile form as it is inserted:
CREATE TRIGGER dbo.tblMobileForms_Insert
ON dbo.tblMobileForms
INSTEAD OF INSERT
AS
BEGIN
DECLARE #Inserted TABLE (fm_ID INT, fm_html_file VARBINARY(MAX), fm_name NVARCHAR(50));
INSERT #Inserted (fm_ID, fm_html_File, fm_Name)
SELECT fm_ID, fm_html_File, fm_Name
FROM inserted;
IF EXISTS (SELECT 1 FROM #Inserted WHERE fm_ID IS NULL)
BEGIN
WITH NewRows AS
( SELECT fm_ID, fm_html_File, fm_Name, RowNumber = ROW_NUMBER() OVER (ORDER BY fm_name)
FROM #Inserted
WHERE fm_ID IS NULL
), AvailableIDs AS
( SELECT fm_ID, RowNumber = ROW_NUMBER() OVER (ORDER BY fm_ID)
FROM tblForms f
WHERE NOT EXISTS
( SELECT 1
FROM tblMobileForms m
WHERE f.Fm_ID = m.fm_ID
)
AND NOT EXISTS
( SELECT 1
FROM inserted i
WHERE f.fm_ID = i.fm_ID
)
)
UPDATE NewRows
SET fm_ID = a.fm_ID
FROM NewRows n
INNER JOIN AvailableIDs a
ON a.RowNumber = n.RowNumber
IF EXISTS (SELECT 1 FROM #Inserted WHERE fm_ID IS NULL)
BEGIN
RAISERROR ('Not enough free Form IDs to allocate an ID to the inserted rows', 16, 1);
RETURN;
END
END
INSERT dbo.tblMobileForms (fm_ID, fm_html_File, fm_Name)
SELECT fm_ID, fm_html_file, fm_name
FROM #Inserted
END
When each row is inserted the trigger will check for the next available ID in tblForms and apply it sequentially to the inserted rows where fm_id is not specified. If there are no free ID's in tblForms then the trigger will throw an error so a 1 to 1 relationship is maintained (The error would be thrown anyway since tblMobileForms.fm_id is also a PK).
N.b. this requires tblForms.fm_ID to just be an int column, and not identity.