I have a SQL Server table with an identity column, set to autoincrement.
Coded in Perl, the insert in the code below works fine, in the while loop the fetchrow_array() call returns no data in the #row array.
How do I best retrieve the identity value for use in subsequent SQL statements?
my $term_sql = "INSERT INTO reminder_term(site, name, description, localization) OUTPUT \#\#IDENTITY VALUES(?,?,?,?)";
my $t_stmt = $dbh->prepare($term_sql);
...
$t_stmt->execute($site, $name, $description, $localizer);
while (#row = $t_stmt->fetchrow_array()) {
$referential_key = $row[0];
}
Avoid using the ##IDENTITY value since it's unreliable in the presence of triggers.
Given the example table schema...
create table [dbo].[reminder_term] (
[id] int not null identity(1,1),
[site] nvarchar(10),
[name] nvarchar(10),
[description] nvarchar(10),
[localization] nvarchar(10)
);
You can rework your OUTPUT clause slightly you can capture the new id value by way of the special inserted row source...
INSERT INTO reminder_term(site, name, description, localization)
OUTPUT inserted.id
VALUES(?,?,?,?)
Related
SQL Server lets you create in-memory tables. But how do you do insert into operation on that?
So for example, I used this code to create my type:
CREATE TYPE dbo.typeTableDelimetedFileSpec
AS TABLE
(
TemplateId INT NOT NULL,
FieldName VARCHAR(50) NOT NUL,
FieldPosition SMALLINT NOT NULL INDEX fpos
)
WITH (MEMORY_OPTIMIZED = ON);
Then, I tried to do this:
DECLARE #T [dbo].[typeTableDelimetedFileSpec]
SELECT *
INTO #T
FROM [dbo].[_DelimetedFileSpec]
WHERE TemplateId = 1
I know the structures match (_DelimetedFileSpec does not have index fpos, but other than that there are no differences).
I get:
Incorrect syntax near '#T'.
Also, just to check out that there are no other errors, I confirmed that the following works fine:
SELECT *
INTO #x
FROM [dbo].[_DelimetedFileSpec]
WHERE TemplateId = 1
Is it possible to somehow insert directly into the memory-table, like this?
I found a way to do it efficiently!
Declare #DeliSpecs [dbo].[typeTableDelimetedFileSpec]
Insert into #DeliSpecs (TemplateId, FieldName, FieldPosition) Select TemplateId, FieldName, FieldPosition from _DelimetedFileSpec where TemplateId = #Id
I have created a SQL Server table that uses uniqueidentifier as the primary key. I set the Default Value or Binding to newid(). (I would like to set the Identity Specification for this column, but that isn't supported for uniqueidentifier types.)
I'm then using ADO.NET to add a row to this table.
SqlComment command = new SqlCommand("INSERT INTO [User] (Name) VALUES (#name);
SELECT SCOPE_IDENTITY()", Connection);
command.Parameters.AddWithValue("#name", "Joe Smoe");
Guid userId = (Guid)command.ExecuteScalar();
However, the last line fails because ExecuteScaler() returns null. It appears that, since a uniqueidentifier cannot be the table's identity, SCOPE_IDENTITY() returns null (as does ##IDENTITY).
Okay, so is there another way to retrieve the newly added ID using ADO.NET?
SCOPE_IDENTITY() is only used for Identity value, for guid values you would need to use the OUTPUT clause with a table variable.
DECLARE #NewGuid TABLE(NewValue UNIQUEIDENTIFIER);
INSERT INTO [User] (Name)
OUTPUT inserted.pk_ColName INTO #NewGuid(NewValue)
VALUES (#name);
SELECT * FROM #NewGuid --<-- here you will have the new GUID Value
C# code would look something like....
string cmd = "DECLARE #NewGuid TABLE(NewValue UNIQUEIDENTIFIER);
INSERT INTO [User] (Name)
OUTPUT inserted.pk_ColName INTO #NewGuid(NewValue)
VALUES (#name);
SELECT #newID = NewValue FROM #NewGuid;"
SqlCommand command = new SqlCommand(cmd, Connection);
cmd.Parameters.AddWithValue("#name", "Joe Smoe");
cmd.Parameters.Add("#newID", SqlDbType.UniqueIdentifier).Direction = ParameterDirection.Output;
Guid userId = (Guid)cmd.ExecuteScalar();
Personally I would put the whole thing in a stored procedure.
Scope_Identity() focuses on an IDENTITY field, so it will never yield anything. You need to output from INSERTED instead. Even though this page is not focused on your particular problem, it should give you some clues:
Return ID on INSERT?
My normal direction is a stored procedure, but you can chain commands, as you have done. The stored procedure makes things a bit easier, as you can create an output parameter for the procedure, but outputting a value works fine.
EDITED to show specific example:
Assume the following table:
CREATE TABLE [dbo].[MyTable]
(
[Id] [uniqueidentifier] PRIMARY KEY NOT NULL DEFAULT NEWID(),
[Name] [varchar](50) NOT NULL,
)
The following program will output the new GUID created from NewID():
class Program
{
static void Main(string[] args)
{
var connString = ConfigurationManager.ConnectionStrings["testDB"].ToString();
var cmdString = "INSERT INTO MyTable (Name) OUTPUT Inserted.Id VALUES ('Name')";
var connection = new SqlConnection(connString);
var command = new SqlCommand(cmdString, connection);
Guid outputValue;
try
{
connection.Open();
//Convert to Guid here instead
Console.WriteLine(command.ExecuteScalar().ToString());
}
finally
{
connection.Dispose();
}
Console.Read();
}
}
I have the following stored procedure, I have two tables here, Movie and Director. Both need to be updated when a new movie has been created or added. How do you handle FKs in stored procedures? The FK in this case is director_id. It is a primary key in Director but a FK in Movie Do I need to specify it twice like so? I am getting conflict errors
CREATE PROCEDURE Book_Book_Creation
#Book_id_arg DECIMAL(12),
#author_id_arg DECIMAL(12),
#type_id_arg DECIMAL(12),
#title_arg VARCHAR(64), -
#copyright_arg DECIMAL(4),
#dauthor_id_2_arg DECIMAL(12),
#author_fname_arg VARCHAR (64),
#author_lname_arg VARCHAR (64)
AS
BEGIN
INSERT INTO Book(Book_id, author_id,genre_id, title, copyright)
VALUES (#author_arg, #author_id_arg, #type_id_arg, #title_arg, #copyright_arg);
INSERT INTO Author(author_id, author_fname, author_lname)
VALUES (#director_id_2_arg, #director_fname_arg, #director_lname_arg)
END;
EXECUTE Book_Book_Creation 32,32,1,'Tempting',2013,32,'Taylor','Mendez';
Basically, you just need to do this:
insert into the Director table first
get the newly inserted ID from that table (assuming that the Director_Id column is your primary key and is of type INT IDENTITY)
then insert into the Movie table with that new ID
Something like this:
DECLARE #NewDirectorID INT
INSERT INTO Director (Director_id, Director_fname, director_lname)
VALUES (#director_id_2_arg, #director_fname_arg, #director_lname_arg)
SELECT #NewDirectorID = SCOPE_IDENTITY()
INSERT INTO Movie (Movie_id, director_id,genre_id, title, copyright)
VALUES (#movie_id_arg, #NewDirectorID, #genre_id_arg, #title_arg, #copyright_arg);
I don't see why you would pass in the director's ID as a parameter - twice!
Try this one -
ALTER PROCEDURE dbo.Movie_Movie_Creation12
#movie_id_arg DECIMAL(12),
#director_id_arg DECIMAL(12),
#genre_id_arg DECIMAL(12),
#title_arg VARCHAR(64),
#copyright_arg DECIMAL(4),
#director_fname_arg VARCHAR (64),
#director_lname_arg VARCHAR (64)
AS BEGIN
INSERT INTO dbo.Director (Director_id, Director_fname, director_lname)
SELECT #director_id_arg, #director_fname_arg, #director_lname_arg
INSERT INTO dbo.Movie (Movie_id, director_id,genre_id, title, copyright)
SELECT #movie_id_arg, #director_id_arg, #genre_id_arg, #title_arg, #copyright_arg
END
EXECUTE dbo.Movie_Movie_Creation12
#movie_id_arg = 32
, #director_id_arg = 32
, #genre_id_arg = 1
, #title_arg = 'Argo'
, #copyright_arg = 2012
, #director_fname_arg = 'Ben'
, #director_lname_arg = 'Affleck'
We have a table where we store all the exceptions (message, stackTrace, etc..), the table is getting big and we would like to reduce it.
There are plenty of repeated StackTraces, Messages, etc, but enabling compression produces a modest size reduction (10%) while I think much bigger benefits could come if somehow Sql Server will intern the strings in some per-column hash-table.
I could get some of the benefits if I normalize the table and extract StackTraces to another one, but exception messages, exception types, etc.. are also repeated.
Is there a way to enable string interning for some column in Sql Server?
There is no built-in way to do this. You could easily do something like:
SELECT MessageID = IDENTITY(INT, 1, 1), Message
INTO dbo.Messages
FROM dbo.HugeTable GROUP BY Message;
ALTER TABLE dbo.HugeTable ADD MessageID INT;
UPDATE h
SET h.MessageID = m.MessageID
FROM dbo.HugeTable AS h
INNER JOIN dbo.Messages AS m
ON h.Message = m.Message;
ALTER TABLE dbo.HugeTable DROP COLUMN Message;
Now you'll need to do a few things:
Change your logging procedure to perform an upsert to the Messages table
Add proper indexes to the messages table (wasn't sure of Message data type) and PK
Add FK to MessageID column
Rebuild indexes on HugeTable to reclaim space
Do this in a test environment first!
Aaron's posting answers the questions of adding interning to a table, but afterwards you will need to modify your application code and stored-procedures to work with the new schema.
...or so you might think. You can actually create a VIEW that returns data matching the old schema, and you can also support INSERT operations on the view too, which are translated into child operations on the Messages and HugeTable tables. For readability I'll use the names InternedStrings and ExceptionLogs for the tables.
So if the old table was this:
CREATE TABLE ExceptionLogs (
LogId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Message nvarchar(1024) NOT NULL,
ExceptionType nvarchar(512) NOT NULL,
StackTrace nvarchar(4096) NOT NULL
)
And the new tables are:
CREATE TABLE InternedStrings (
StringId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Value nvarchar(max) NOT NULL
)
CREATE TABLE ExceptionLogs2 ( -- note the new name
LogId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Message int NOT NULL,
ExceptionType int NOT NULL,
StackTrace int NOT NULL
)
Add an index to InternedStrings to make the value lookups faster:
CREATE UNIQUE NONCLUSTERED INDEX IX_U_InternedStrings_Value ON InternedStrings ( Value ASC )
Then you would also have a VIEW:
CREATE VIEW ExeptionLogs AS
SELECT
LogId,
MessageStrings .Value AS Message,
ExceptionTypeStrings.Value AS ExceptionType,
StackTraceStrings .Value AS StackTrace
FROM
ExceptionLogs2
INNER JOIN InternedStrings AS MessageStrings ON
MessageStrings.StringId = ExceptionLogs2.Message
INNER JOIN InternedStrings AS ExceptionTypeStrings ON
ExceptionTypeStrings.StringId = ExceptionLogs2.ExceptionType
INNER JOIN InternedStrings AS StackTraceStrings ON
StackTraceStrings.StringId = ExceptionLogs2.StackTrace
And to handle INSERT operations from unmodified clients:
CREATE TRIGGER ExceptionLogsInsertHandler
ON ExceptionLogs INSTEAD OF INSERT AS
DECLARE #messageId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.Message
IF #messageId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.Message )
SET #messageId = SCOPE_IDENTITY()
END
DECLARE #exceptionTypeId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.ExceptionType
IF #exceptionTypeId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.ExceptionType )
SET #exceptionTypeId = SCOPE_IDENTITY()
END
DECLARE #stackTraceId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.StackTrace
IF #stackTraceId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.StackTrace )
SET #stackTraceId = SCOPE_IDENTITY()
END
INSERT INTO ExceptionLogs2 ( Message, ExceptionType, StackTrace )
VALUES ( #messageId, #exceptionTypeId, #stackTraceId )
Note this TRIGGER can be improved: it only supports single-row insertions, and is not entirely concurrency-safe, though because previous data won't be mutated it means that there's a slight risk of data duplication in the InternedStrings table - and because of a UNIQUE index the insert will fail. There are different possible ways to handle this, such as using a TRANSACTION and changing the queries to use holdlock and updlock.
I have a stored procedure that looks up an article based on the article's title. But I also need to increment a column in the same table that counts the number of times the article is viewed.
Trying to be as efficient as possible, I see two possible ways to approach this:
Perform one SELECT to obtain the PK on the target row. Then use that PK to increment the number of views and, finally, another SELECT using the PK to return the article data.
Perform one SELECT to return the article data to my application, and then use the returned PK to make another round trip to the database to increment the number of views.
I know #1 would be pretty fast, but it's three lookups. And #2 requires two round trips to the database. Is there no way to optimize this task?
EDIT Based on feedback, I came up with the following. Thanks for any comments or constructive criticism.
DECLARE #Slug VARCHAR(250) -- Stored procedure argument
-- declare #UpdatedArticle table variable
DECLARE #UpdatedArticle TABLE
(
ArtID INT,
ArtUserID UNIQUEIDENTIFIER,
ArtSubcategoryID INT,
ArtTitle VARCHAR(250),
ArtHtml VARCHAR(MAX),
ArtDescription VARCHAR(350),
ArtKeywords VARCHAR(250),
ArtLicenseID VARCHAR(10),
ArtViews BIGINT,
ArtCreated DATETIME2(7),
ArtUpdated DATETIME2(7)
);
UPDATE Article
SET ArtViews = ArtViews + 1
OUTPUT
INSERTED.ArtID,
INSERTED.ArtUserID,
inserted.ArtSubcategoryID,
INSERTED.ArtTitle,
INSERTED.ArtHtml,
INSERTED.ArtDescription,
INSERTED.ArtKeywords,
INSERTED.ArtLicenseID,
INSERTED.ArtViews,
INSERTED.ArtUpdated,
INSERTED.ArtCreated
INTO #UpdatedArticle
WHERE ArtSlugHash = CHECKSUM(#Slug) AND ArtSlug = #Slug AND ArtApproved = 1
SELECT a.ArtID, a.ArtUserID, a.ArtTitle, a.ArtHtml, a.ArtDescription, a.ArtKeywords, a.ArtLicenseID,
l.licTitle, a.ArtViews, a.ArtCreated, a.ArtUpdated, s.SubID, s.SubTitle, c.CatID, c.CatTitle,
sec.SecID, sec.SecTitle, u.UsrDisplayName AS UserName
FROM #UpdatedArticle a
INNER JOIN Subcategory s ON a.ArtSubcategoryID = s.SubID
INNER JOIN Category c ON s.SubCatID = c.CatID
INNER JOIN [Section] sec ON c.CatSectionID = sec.SecID
INNER JOIN [User] u ON a.ArtUserID = u.UsrID
INNER JOIN License l ON a.ArtLicenseID = l.LicID
Here is a way using the OUTPUT statement (SQL Server 2005 onwards), in a single update statement:
IF OBJECT_ID ('Books', 'U') IS NOT NULL
DROP TABLE dbo.Books;
CREATE TABLE dbo.Books
(
BookID int NOT NULL PRIMARY KEY,
BookTitle nvarchar(50) NOT NULL,
ModifiedDate datetime NOT NULL,
NumViews int not null CONSTRAINT DF_Numviews DEFAULT (0)
);
INSERT INTO dbo.Books
(BookID, BookTitle, ModifiedDate)
VALUES
(106, 'abc', GETDATE()),
(107, 'Great Expectations', GETDATE());
-- declare #UpdateOutput1 table variable
DECLARE #UpdateOutput1 table
(
BookID int,
BookTitle nvarchar(50),
ModifiedDate datetime,
NumViews int
);
-- >>>> here is the update of Numviews and the Fetch
-- update Numviews in Books table, and retrive the row
UPDATE Books
SET
NumViews = NumViews + 1
OUTPUT
INSERTED.BookID,
INSERTED.BookTitle,
INSERTED.ModifiedDate,
INSERTED.NumViews
INTO #UpdateOutput1
WHERE BookID = 106
-- view updated row in Books table
SELECT * FROM Books;
-- view output row in #UpdateOutput1 variable
SELECT * FROM #UpdateOutput1;