Keep the last 'x' results in a database - sql

I would like to ask if there is a quick way to keep the last 'x' inserted rows in a database.
For example, i have an application through which users can search for items and I want to keep the last 10 searches that each user has made. I do not want to keep all his searches into the database as this will increase db_size. Is there a quick way of keeping only the latest 10 searches on my db or shall i check every time:
A) how many searches has been stored on database so far
B) if (searches = 10) delete last row
C) insert new row
I think that this way will have an impact on performance as it will need 3 different accesses on the database: check, delete and insert

I don't think an easy/quick way to do this. Based on your conditions i created the below stored procedure.
I considered SearchContent which is going to store the data.
CREATE TABLE SearchContent (
Id INT IDENTITY (1, 1),
UserId VARCHAR (8),
SearchedOn DATETIME,
Keyword VARCHAR (40)
)
In the stored procedure passing the UserId, Keyword and do the calculation. The procedure will be,
CREATE PROCEDURE [dbo].[pub_SaveSearchDetails]
(
#UserId VARCHAR (8),
#Keyword VARCHAR (40)
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Count AS INT = 0;
-- A) how many searches has been stored on database so far
SELECT #Count = COUNT(Keyword) FROM SearchContent WHERE UserId = #UserId
IF #Count >= 10
BEGIN
-- B) if (searches = 10) delete last row
DELETE FROM SearchContent WHERE Id IN ( SELECT TOP 1 Id FROM SearchContent WHERE UserId = #UserId ORDER BY SearchedOn ASC)
END
-- C) insert new row
INSERT INTO SearchContent (UserId, SearchedOn, Keyword)
VALUES (#UserId, GETDATE(), #Keyword)
END
Sample execution: EXEC pub_SaveSearchDetails 'A001', 'angularjs'

Related

SQL while loop with select query

I'm trying to accomplish the following thing:
I receive a DocumentID. Find all the records in a table that match the specific DocumentID, for example I have 10 records matching and every record is with different DocumentAttachmentID.
I update all the records with the new data.
The problem comes that I need to insert some of the information from these ten records + other information received to a new table, which is History table, i.e. I need to insert ten new records there.
I've succeeded to this with Cursor, but it looks like that the Cursor isn't really good, because of the performance.
Is there a way to loop throught the 10 records that I selected from this table and for every record to take some information, add some additional info and then insert this in the other table ?
EDIT:
I tried to do this withoud looping(thanks you all for the answers)
I will try it tomorrow, do you think this is gonna work ?:
With the first Update, I update all documentAttachments,
The second block is INSERT TO, which should insert all document attachments in the other table with some extra columns.
UPDATE [sDocumentManagement].[tDocumentAttachments]
SET DeletedBy = #ChangedBy,
DeletedOn = #CurrentDateTime,
IsDeleted = 1,
WHERE DocumentID = #DocumentID;
INSERT INTO [sDocumentManagement].[tDocumentHistory] ( DocumentAttachmentID, DocumentID, ActivityCodeID, ChangedOn, ChangedBy, AdditionalInformation )
SELECT DocumentAttachmentID,
#DocumentID, [sCore].[GetActivityCodeIDByName] ( 'DeletedDocument' ),
#CurrentDateTime,
#ChangedBy,
#AdditionalInformation
FROM [sDocumentManagement].[tDocumentAttachments]
WHERE DocumentID = #DocumentID;
for looping without a cursor I quite often use the following technique:
DECLARE #items TABLE(id INT, val INT);
DECLARE #id INT;
DECLARE #val INT;
WHILE EXISTS(SELECT * FROM #items) BEGIN
SELECT TOP(1) #id = id, #val = val FROM #items;
DELETE FROM #items WHERE (id = #id);
--do what is needed with the values here.
SELECT #id, #val;
END
this treats the #items table as a queue pulling the rows off one at a time till it is empty.

SQL Stored Procedure Simultaneously Call Issue

I have stored procedure in the sql server 2008, my stored procedure calculate and get the last number "not primary key" from column from table B and add one ( +1 ) to this number to use it on the next statement on the same stored procedure.
My issue that i have a duplicate number some times, i think this happened when multiple users call the stored procedure on the same time. is this the issue and how can i solve it
my code is like the below:-
DECLARE #ID AS NVARCHAR(10)
SET #ID = (
SELECT TOP 1 MyNo
FROM Employee
WHERE (
(TypeID = #TypeID) AND
(Year = #Year)
)
ORDER BY ID DESC
)
SET #ID = ISNULL(#ID,0) + 1
INSERT INTO Employee (name,lname,MyNo) VALUES (#name,#lname,#MyNo)
You can lock a table for the duration of a transaction with the WITH (TABLOCKX, HOLDLOCK) syntax:
BEGIN TRANSACTION
DECLARE #ID AS NVARCHAR(10)
SET #ID = (
SELECT TOP 1 MyNo
FROM Employee WITH (TABLOCKX, HOLDLOCK)
WHERE (
(TypeID = #TypeID) AND
(Year = #Year)
)
ORDER BY ID DESC
)
SET #ID = ISNULL(#ID,0) + 1
INSERT INTO Employee (name,lname,MyNo) VALUES (#name,#lname,#MyNo)
COMMIT TRANSACTION
You can find more information about TABLOCK and TABLOCKX here:
https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table
Per discussion, the best lock to use in this case would be:
(UPDLOCK,HOLDLOCK)
If you cannot use Identity column or the Table lock, another alternative is to use sp_getapplock
The advantage with this mechanism is that this kind of lock can be used across multiple stored procedures that should not run concurrently or for operations that span multiple tables. It also allows for handling timeout and other kinds of behavior if the lock is not available.
You have to be careful when using this feature and ensure you acquire and release locks properly or you will create more problems than you solve.

SQL stored procedure inserting duplicate OrderNumber

I searched the Internet for days, no effort, maybe I cant ask in a right way.
I have a sql table like this:
create table Items
(
Id int identity(1,1),
OrderNumber varchar(7),
ItemName varchar(255),
Count int
)
Then I have a stored procedure inserting items, on demand creating new OrderNumber:
create procedure spx_insertItems
#insertNewOrderNr bit,
#orderNumber varchar(7),
#itemName varchar(255),
#count int
as
begin
set nocount on;
if (#insertNewOrderNr = 1)
begin
declare #newNr = (select dbo.fun_getNewOrderNr())
INSERT INTO Items (OrderNumber, ItemName, Count) values (#newNr, #itemName, #count)
select #newNr
end
else
begin
INSERT INTO Items (OrderNumber, ItemName, Count) values (#orderNumber, #itemName, #count)
select scope_identity()
end
end
Finally there is a user defined function returning new OrderNumber:
create function dbo.fun_getNewOrderNr
()
return varchar(7)
as
begin
/* this func works well */
declare #output varchar(7)
declare #currentMaxNr varchar(7)
set #currentMaxNr = (isnull((select max(OrderNumber) from Items), 'some_default_value_here')
/* lets assume the #currentMaxNr is '01/2014', here comes logic that increments to #newValue='02/2014' and sets to #output, so: */
set #output = #newValue
return #output
end
Into Items can be inserted items that do as well that do not belong to any OrderNumber.
Whether an Item should become new OrderNumber, the procedure is called with #insertNewOrderNr=1, returns the new order number, that can be used to insert next items with that OrderNumber while #insertNewOrderNr=0.
Occasionally there happens that there come simultaneously 2 requests to #insertNewOrderNr and THERE IS THE PROBLEM - Items, that should correspond with different OrderNumbers get the same OrderNumber.
I tried to use transaction with no success.
The table structure cant be modified by me.
What would be the right way to ensure, that there won't be used the same newOrderNumber when simultaneous requests to the procedure come?
I am stuck here for a long time till now. Please, help.
You will have that problem as long as you use MAX(OrderNumber).
You might consider using sequences:
Create sequence
CREATE SEQUENCE dbo.OrderNumbers
AS int
START WITH 1
INCREMENT BY 1
NO CACHE;
GO
CREATE SEQUENCE dbo.OrderNumberYear
AS int
START WITH 2014
INCREMENT BY 1
NO CACHE;
SELECT NEXT VALUE FOR dbo.OrderNumberYear; --Important, run this ONE time after creation, this years value must be returned one initial time to work correctly.
Insert code
DECLARE #orderNumberYear INT = (SELECT CONVERT(INT, current_value) FROM sys.sequences WHERE name = 'OrderNumberYear');
IF(#orderNumberYear < YEAR(GETDATE()))
BEGIN
SELECT #orderNumberYear = NEXT VALUE FOR dbo.OrderNumberYear;
ALTER SEQUENCE dbo.OrderNumbers RESTART WITH 1 ;
END
IF(#orderNumberYear != YEAR(GETDATE()))
RAISERROR(N'Order year sequence is out of sync.', 16, 1);
DECLARE #newNr VARCHAR(15) = CONCAT(FORMAT(NEXT VALUE FOR dbo.OrderNumbers, '00/', 'en-US'), #orderNumberYear);
INSERT INTO Items (OrderNumber, ItemName, Count) values (#newNr, #itemName, #count)
SELECT #newNr
The duplicity still occured, not so often, but did.
The trick I finally used to get around didn't find itself inside SQL. Since the DB is always used by ONLY one web app that is used by several users, this is the solution:
in all (about 5) places in my VB.NET code I surrounded the myCommand.ExecuteScalar() with SyncLock (read more) statement that DID the trick :)

Generating the Next Id when Id is non-AutoNumber

I have a table called Employee. The EmpId column serves as the primary key. In my scenario, I cannot make it AutoNumber.
What would be the best way of generating the the next EmpId for the new row that I want to insert in the table?
I am using SQL Server 2008 with C#.
Here is the code that i am currently getting, but to enter Id's in key value pair tables or link tables (m*n relations)
Create PROCEDURE [dbo].[mSP_GetNEXTID]
#NEXTID int out,
#TABLENAME varchar(100),
#UPDATE CHAR(1) = NULL
AS
BEGIN
DECLARE #QUERY VARCHAR(500)
BEGIN
IF EXISTS (SELECT LASTID FROM LASTIDS WHERE TABLENAME = #TABLENAME and active=1)
BEGIN
SELECT #NEXTID = LASTID FROM LASTIDS WHERE TABLENAME = #TABLENAME and active=1
IF(#UPDATE IS NULL OR #UPDATE = '')
BEGIN
UPDATE LASTIDS
SET LASTID = LASTID + 1
WHERE TABLENAME = #TABLENAME
and active=1
END
END
ELSE
BEGIN
SET #NEXTID = 1
INSERT INTO LASTIDS(LASTID,TABLENAME, ACTIVE)
VALUES(#NEXTID+1,#TABLENAME, 1)
END
END
END
Using MAX(id) + 1 is a bad idea both performance and concurrency wise.
Instead you should resort to sequences which were design specifically for this kind of problem.
CREATE SEQUENCE EmpIdSeq AS bigint
START WITH 1
INCREMENT BY 1;
And to generate the next id use:
SELECT NEXT VALUE FOR EmpIdSeq;
You can use the generated value in a insert statement:
INSERT Emp (EmpId, X, Y)
VALUES (NEXT VALUE FOR EmpIdSeq, 'x', 'y');
And even use it as default for your column:
CREATE TABLE Emp
(
EmpId bigint PRIMARY KEY CLUSTERED
DEFAULT (NEXT VALUE FOR EmpIdSeq),
X nvarchar(255) NULL,
Y nvarchar(255) NULL
);
Update: The above solution is only applicable to SQL Server 2012+. For older versions you can simulate the sequence behavior using dummy tables with identity fields:
CREATE TABLE EmpIdSeq (
SeqID bigint IDENTITY PRIMARY KEY CLUSTERED
);
And procedures that emulates NEXT VALUE:
CREATE PROCEDURE GetNewSeqVal_Emp
#NewSeqVal bigint OUTPUT
AS
BEGIN
SET NOCOUNT ON
INSERT EmpIdSeq DEFAULT VALUES
SET #NewSeqVal = scope_identity()
DELETE FROM EmpIdSeq WITH (READPAST)
END;
Usage exemple:
DECLARE #NewSeqVal bigint
EXEC GetNewSeqVal_Emp #NewSeqVal OUTPUT
The performance overhead of deleting the last inserted element will be minimal; still, as pointed out by the original author, you can optionally remove the delete statement and schedule a maintenance job to delete the table contents off-hour (trading space for performance).
Adapted from SQL Server Customer Advisory Team Blog.
Working SQL Fiddle
The above
select max(empid) + 1 from employee
is the way to get the next number, but if there are multiple user inserting into the database, then context switching might cause two users to get the same value for empid and then add 1 to each and then end up with repeat ids. If you do have multiple users, you may have to lock the table while inserting. This is not the best practice and that is why the auto increment exists for database tables.
I hope this works for you. Considering that your ID field is an integer
INSERT INTO Table WITH (TABLOCK)
(SELECT CASE WHEN MAX(ID) IS NULL
THEN 1 ELSE MAX(ID)+1 END FROM Table), VALUE_1, VALUE_2....
Try following query
INSERT INTO Table VALUES
((SELECT isnull(MAX(ID),0)+1 FROM Table), VALUE_1, VALUE_2....)
you have to check isnull in on max values otherwise it will return null in final result when table contain no rows .

Does anyone know a neat trick for reusing identity values?

Typically when you specify an identity column you get a convenient interface in SQL Server for asking for particular row.
SELECT * FROM $IDENTITY = #pID
You don't really need to concern yourself with the name if the identity column because there can only be one.
But what if I have a table which mostly consists of temporary data. Lots of inserts and lots of deletes. Is there a simple way for me to reuse the identity values.
Preferably I would want to be able to write a function that would return say NEXT_SMALLEST($IDENTITY) as next identity value and do so in a fail-safe manner.
Basically find the smallest value that's not in use. That's not entirely trivial to do, but what I want is to be able to tell SQL Server that this is my function that will generate the identity values. But what I know is that no such function exists...
I want to...
Implement global data base IDs, I need to provide a default value that I'm in control of.
My idea was based around that I should be able to have a table with all known IDs and then every row ID from some other table that needed a global ID would reference that table. The default value would be provided by something like
INSERT INTO GlobalID
RETURN SCOPE_IDENTITY()
No; it's not unique if it can be reused.
Why do you want to re-use them? Why do you concern yourself with this field? If you want to be in control of it, don't make it an identity; create your own scheme and use that.
Don't reuse identities, you'll just shoot your self in the foot. Use a large enough value so that it never rolls over (64 bit big int).
To find missing gaps in a sequence of numbers join the table against itself with a +/- 1 difference:
SELECT a.id
FROM table AS a
LEFT OUTER JOIN table AS b ON a.id = b.id+1
WHERE b.id IS NULL;
This query will find the numbers in the id sequence for which id-1 is not in the table, ie. contiguous sequence start numbers. You can then use SET IDENTITY INSERT OFF to insert a specific id and reuse a number. The cost of doing so is overwhelming (both runtime and code complexity) compared with the an ordinary identity based insert.
If you really want to reset Identity value to the lowest,
here is the trick you can use through DBCC CHECKIDENT
Basically following sql statements resets identity value so that identity value restarts from the lowest possible number
create table TT (id int identity(1, 1))
GO
insert TT default values
GO 10
select * from TT
GO
delete TT where id between 5 and 10
GO
--; At this point, next ID will be 11, not 5
select * from TT
GO
insert TT default values
GO
--; as you can see here, next ID is indeed 11
select * from TT
GO
--; Now delete ID = 11
--; so that we can reseed next highest ID to 5
delete TT where id = 11
GO
--; Now, let''s reseed identity value to the lowest possible identity number
declare #seedID int
select #seedID = max(id) from TT
print #seedID --; 4
--; We reseed identity column with "DBCC CheckIdent" and pass a new seed value
--; But we can't pass a seed number as argument, so let's use dynamic sql.
declare #sql nvarchar(200)
set #sql = 'dbcc checkident(TT, reseed, ' + cast(#seedID as varchar) + ')'
exec sp_sqlexec #sql
GO
--; Now the next
insert TT default values
GO
--; as you can see here, next ID is indeed 5
select * from TT
GO
I guess we would really need to know why you want to reuse your identity column. The only reason I can think of is because of the temporary nature of your data you might exhaust the possible values for the identity. That is not really likely, but if that is your concern, you can use uniqueidentifiers (guids) as the primary key in your table instead.
The function newid() will create a new guid and can be used in insert statements (or other statements). Then when you delete the row, you don't have any "holes" in your key because guids are not created in that order anyway.
[Syntax assumes SQL2008....]
Yes, it's possible. You need to two management tables, and two triggers on each participating table.
First, the management tables:
-- this table should only ever have one row
CREATE TABLE NextId (Id INT)
INSERT NextId VALUES (1)
GO
CREATE TABLE RecoveredIds (Id INT NOT NULL PRIMARY KEY)
GO
Then, the triggers, two on each table:
CREATE TRIGGER tr_TableName_RecoverId ON TableName
FOR DELETE AS BEGIN
IF ##ROWCOUNT = 0 RETURN
INSERT RecoveredIds (Id) SELECT Id FROM deleted
END
GO
CREATE TRIGGER tr_TableName_AssignId ON TableName
INSTEAD OF INSERT AS BEGIN
DECLARE #rowcount INT = ##ROWCOUNT
IF #rowcount = 0 RETURN
DECLARE #required INT = #rowcount
DECLARE #new_ids TABLE (Id INT PRIMARY KEY)
DELETE TOP (#required) OUTPUT DELETED.Id INTO #new_ids (Id) FROM RecoveredIds
SET #rowcount = ##ROWCOUNT
IF #rowcount < #required BEGIN
DECLARE #output TABLE (Id INT)
UPDATE NextId SET Id = Id + (#required-#rowcount)
OUTPUT DELETED.Id INTO #output
-- this assumes you have a numbers table around somewhere
INSERT #new_ids (Id)
SELECT n.Number+o.Id-1 FROM Numbers n, #output o
WHERE n.Number BETWEEN 1 AND #required-#rowcount
END
SET IDENTITY_INSERT TableName ON
;WITH inserted_CTE AS (SELECT _no = ROW_NUMBER() OVER (ORDER BY Id), * FROM inserted)
, new_ids_CTE AS (SELECT _no = ROW_NUMBER() OVER (ORDER BY Id), * FROM #new_ids)
INSERT TableName (Id, Attr1, Attr2)
SELECT n.Id, i.Attr1, i.Attr2
FROM inserted_CTE i JOIN new_ids_CTE n ON i._no = n._no
SET IDENTITY_INSERT TableName OFF
END
You could script the triggers out easily enough from system tables.
You would want to test this for concurrency. It should work as is, syntax errors notwithstanding: The OUTPUT clause guarantees atomicity of id lookup->increment as one step, and the entire operation occurs within a transaction, thanks to the trigger.
TableName.Id is still an identity column. All the common idioms like $IDENTITY and SCOPE_IDENTITY() will still work.
There is no central table of ids by table, but you could create one easily enough.
I don't have any help for finding the values not in use but if you really want to find them and set them yourself, you can use
set identity_insert on ....
in your code to do so.
I'm with everyone else though. Why bother? Don't you have a business problem to solve?