I am trying to teach myself SQL. I have a web matrix project I am working on to edit and display posts backed by a SQL server Datatabase. A work colleague suggested I use a Stored Procedure to commit the post rather than writing the sql inline.
So far the procedure looks ok but I would like to check if the url slug already exists, and if so return something to say so (The url slug should be unique). I'm struggling with how I am supposed to check before the insert. I have also read that it is bad practice to return from a stored procedure, but I thought it would be a good idea to return something to let the caller know the insert did not go ahead.
Any help would be very much appreciated.
-- =============================================
-- Author: Dean McDonnell
-- Create date: 05/12/2011
-- Description: Commits an article to the database.
-- =============================================
CREATE PROCEDURE CommitPost
#UrlSlug VARCHAR(100),
#Heading VARCHAR(100),
#SubHeading VARCHAR(300),
#Body VARCHAR(MAX)
AS
INSERT INTO Posts(UrlSlug, Heading, SubHeading, Body, Timestamp)
VALUES(#UrlSlug, #Heading, #SubHeading, #Body, GETDATE())
This is what I have so far.
CREATE PROCEDURE CommitPost
#UrlSlug VARCHAR(100),
#Heading VARCHAR(100),
#SubHeading VARCHAR(300),
#Body VARCHAR(MAX)
AS
IF NOT EXISTS (SELECT * FROM Posts WHERE UrlSlug = #UrlSlug)
INSERT INTO Posts(UrlSlug, Heading, SubHeading, Body, Timestamp)
VALUES(#UrlSlug, #Heading, #SubHeading, #Body, GETDATE())
SELECT ##ROWCOUNT
To check for existance, do a SELECT COUNT like so:
CREATE PROCEDURE CommitPost
#UrlSlug VARCHAR(100),
#Heading VARCHAR(100),
#SubHeading VARCHAR(300),
#Body VARCHAR(MAX)
AS
DECLARE #count INT
SELECT #count = COUNT(*) FROM Posts WHERE UrlSlug = #UrlSlug
IF #count = 0 THEN
BEGIN
INSERT INTO Posts(UrlSlug, Heading, SubHeading, Body, Timestamp)
VALUES(#UrlSlug, #Heading, #SubHeading, #Body, GETDATE())
END
You may set an unique index on UrlSlug to make the database reject insertions of urls already in the database, but nonetheless you should check before inserting.
If your caller wants to know if the row was inserted, return the #count value. If it's 0 then the line was inserted, else not. I'm not aware of a "bad practice" regarding to return values from a SP. As a SP does not have a result, though, you need to use an out parameter.
If you do just one SQL statement like this insert you could just use paratemerized query i.e. I assume that you are using .NET.
If you want to return values I would suggest that you use a FUNCTION instead of a STORED PROCEDURE. You can return either tables or whatever you want from a function.
There are some limitations though. You can dig a little deeper into the differences to see what is used when. Here's a link that can help you out get started:
Function vs. Stored Procedure in SQL Server
If you want to use stored procedure anyway, you can either return a single row, single column result set, using SELECT, or just use an output parameter.
If you want to do actions depending of whether the column exists or not I would suggest that you look into MERGE statement.That way you would perform only one query to the database instead of two or more(doing SELECT and then INSERT).
There are also other ways to use database access like various ORMs on top of the database in the code that will make your life easier, like LINQ-to-SQL etc. There are a lot of possibilities out there. You need to determine what's best in a given situation.
Related
Consider an enterprise that captures sensor data for different production facilities. per facility, we create an aggregation query that averages the values to 5min timeslots. This query exists out of a long list of with-clauses and writes data to a table (called aggregation_table).
Now my problem: currently we have n queries running that exactly run the same logic, the only thing that differs are table names (and sometimes column names but let's ignore that for now).
Instead of managing n different scripts that are basically the same, I would like to put it in a stored procedure that is able to work like this:
CALL aggregation_query(facility_name) -> resolve the different tables for that facility and then use them in the different with clauses
On top of that, instead of having this long set of clauses that give me the end-result, I would like to chunk them up in logical blocks that are parametrizable, So for example, if I call the aforementioned stored_procedure for facility A, I want to be able to pass / use this table name in these different functions, where the output can be re-used in the next statement (like you would do with with clauses).
Another argument of why I want to chunk this up in re-usable blocks is because we have many "derivatives" on this aggregation query, for example to manage historical data, to correct data or to have the sensor data on another aggregation level. As these become overly complex, it is much easier to manage them without having to copy paste and adjust these every time.
In the current set-up, it could be useful to know that I am only entitled to use plain BigQuery, As my team is not allowed to access the CI/CD / scheduling and repository. (meaning that I cannot solve the issue by having CI/CD that deploys the n different versions of the procedure and functions)
So in the end, I would like to end up with something like this using only bigquery:
CREATE OR REPLACE PROCEDURE
`aggregation_function`()
BEGIN
DECLARE
tablename STRING;
DECLARE
active_table_name STRING; ##get list OF tables CREATE TEMP TABLE tableNames AS
SELECT
table_catalog,
table_schema,
table_name
FROM
`catalog.schema.INFORMATION_SCHEMA.TABLES`
WHERE
table_name = tablename;
WHILE
(
SELECT
COUNT(*)
FROM
tableNames) >= 1 DO ##build dataset + TABLE name
SET
active_table_name = CONCAT('`',table_catalog,'.',table_schema,'.' ,table_name,'`'); ##use concat TO build string AND execute
EXECUTE IMMEDIATE '''
INSERT INTO
`aggregation_table_for_facility` (timeslot, sensor_name, AVG_VALUE )
WITH
STEP_1 AS (
SELECT
*
FROM
my_table_function_step_1(active_table_name,
parameter1,
parameter2) ),
STEP_2 AS (
SELECT
*
FROM
my_table_function_step_2(STEP_1,
parameter1,
parameter2) )
SELECT * FROM STEP_2
'''
USING active_table_name as active_table_name;
DELETE
FROM
tableNames
WHERE
table_name = tablename;
END WHILE
;
END
;
I was hoping someone could make a snippet on how I can do this in Standard SQL / Bigquery, so basically:
stored procedure that takes in a string variable and is able to use that as a table (partly solved in the approach above, but not sure if there are better ways)
(table) function that is able to take this table_name parameter as well and return back a table that can be used in the next with clause (or alternatively writes to a temp table)
I think below code snippets should provide you with some insights when dealing with procedures, inserts and execute immediate statements.
Here I'm creating a procedure which will insert values into a table that exists on the information schema. Also, as a value I want to return I use OUT active_table_name to return the value I assigned inside the procedure.
CREATE OR REPLACE PROCEDURE `project-id.dataset`.custom_function(tablename STRING,OUT active_table_name STRING)
BEGIN
DECLARE query STRING;
SET active_table_name= (SELECT CONCAT('`',table_catalog,'.',table_schema,'.' ,table_name,'`')
FROM `project-id.dataset.INFORMATION_SCHEMA.TABLES`
WHERE table_name = tablename);
#multine query can be handled by using ''' or """
Set query =
'''
insert into %s (string_field_0,string_field_1,string_field_2,string_field_3,string_field_4,int64_field_5)
with custom_query as (
select string_field_0,string_field_2,'169 BestCity',string_field_3,string_field_4,55677 from %s limit 1
)
select * from custom_query;
''';
# querys must perform operations and must be the last thing to perform
# pass parameters using format
execute immediate (format(query,active_table_name,active_table_name));
END
You can also use a loop to iterate trough records from a working table so it will execute the procedure and also be able to get the value from the procedure to use somewhere else.ie:A second procedure to perform a delete operation.
DECLARE tablename STRING;
DECLARE out_value STRING;
FOR record IN
(SELECT tablename from `my-project-id.dataset.table`)
DO
SET tablename = record.tablename;
LOOP
call `project-id.dataset`.custom_function(tablename,out_value);
select out_value;
END LOOP;
END FOR;
To recap, there are some restrictions such as the possibility to call procedures inside a execute immediate or to use execute immediate inside an execute immediate, to count a few. I think these snippets should help you dealing with your current situation.
For this sample I use the following documentation:
Data Manipulation Language
Dealing with outputs
Information Schema Tables
Execute Immediate
For...In
Loops
I am using SQL Server 2012 Enterprise and I have a stored procedure that accepts two parameters:
#pkgId varchar(16), #siteId varchar(2)
The stored procedure will then do an INSERT like this:
IF #siteId = '01'
BEGIN
INSERT INTO dbo.table**01** (pkgId)
VALUES (#pkgId)
END
IF #siteId = '02'
BEGIN
INSERT INTO dbo.table**02** (pkgId)
VALUES (#pkgId)
END
IF #siteId = '03'
BEGIN
INSERT INTO dbo.table**03** (pkgId)
VALUES(#pkgId)
END
Now we are looking to add 10 more site's. So I would have to add 10 more IF statements, but I DO NOT want to use dynamic SQL as I need the query plans to be cached, because speed is a must. Also, I have many more tables that already end in '01', '02' and '03', so there is a lot more code updates for me to do.
Also, it is a business requirement that these tables be separate. Meaning, I cannot just have one table with siteId as a column.
So the question is: is there some other way I can perform this INSERT by using some other alternative and keep my coding at a minimum? Meaning, I would like to call the INSERT only once, if possible, without the use of dynamic SQL.
FYI - I have seen some other alternatives like setting a synonym at real time, but this will cause concurrency issues.
Query plans for dynamic SQL in the manner you need to use it are cached, see links here and here2
I want to append one article to different menus. Please take a look at a procedure for adding a new article. My implementation works fine, but seems to be far from perfect.
DROP PROCEDURE IF EXISTS new_article;
DELIMITER $$
CREATE PROCEDURE new_article(IN title VARCHAR(255), IN alias VARCHAR(255), IN text MEDIUMTEXT, IN description TEXT, IN articles_users_id INT, IN menus_id INT)
BEGIN
/*how do i know a first VALUE for articles_menus INSERT?*/
SET #articles_id_ai = (SELECT AUTO_INCREMENT
FROM information_schema.tables
WHERE table_name = 'articles'
AND table_schema = DATABASE());
INSERT INTO articles VALUE (#articles_id_ai, articles_users_id, title, alias, text, description, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT);
INSERT INTO articles_menus VALUE (#articles_id_ai, articles_users_id, menus_id);
END$$
DELIMITER ;
I'm not familiar with the way you are getting the next auto generated value for the table. So I don't know what would happen if a different user wrote to the table after your first SELECT but before your first INSERT. MySQL provides an easy way to retrieve the last generated value, LAST_INSERT_ID():
INSERT INTO articles( users_id, title, alias, text, desc )
VALUES( articles_users_id, title, alias, text, description );
INSERT INTO articles_menus( articles_id, articles_users_id, menus_id )
VALUES( LAST_INSERT_ID(), articles_users_id, menus_id );
This uses only two database accesses instead of three and it uses the function provided for the purpose. It's generally a good idea to use the system-provided methods to get information unless you have a good reason. Nobody knows the system better than the developers of system functions.
I have a TSimpleDataSet based on dinamically created SQL query. I need to know which field is a primary key?
SimpleDataSet1.DataSet.SetSchemaInfo(stIndexes, 'myTable' ,'');
This code tells me that i have a primary key with name 'someName', but how can i know which field (column) works with this index?
A Primary Key/Index can belong to several columns (not just one).
The schema stIndexes dataset will return the PK name INDEX_NAME and the columns that construct that PK/Index (COLUMN_NAME). INDEX_TYPE will tell you which index types you have (eSQLNonUnique/eSQLUnique/eSQLPrimaryKey).
I have never worked with TSimpleDataSet but check if the indexes information is stored in
IndexDefs[TIndexDef].Name/Fields/Options - if ixPrimary in Options then this is your PK. and Fields belongs to that index.
Take a look at the source at SqlExpr.pas: TCustomSQLDataSet.AddIndexDefs.
Note how TCustomSQLDataSet returns the TableName (and then the indexs information) from the command text:
...
if FCommandType = ctTable then
TableName := FCommandText
else
TableName := GetTableNameFromSQL(CommandText);
DataSet := FSQLConnection.OpenSchemaTable(stIndexes, TableName, '', '', '');
...
I think the simple data set does not provide that information.
However, i am sure there are components for that. Check, for Oracle database, Devart's ODAC.
Basically, it involves only one query to the database.
However, it is not something that components will offer by default as, because it involves a different query, it leads to slow response times.
For Oracle database, query on user_indexes.
I have a basic table that looks like the following:
SQL> desc comments
Name Null? Type
---------------------------------------
COMMENT_ID NOT NULL NUMBER
POST_ID NOT NULL NUMBER
USER_ID NOT NULL NUMBER
MESSAGE NOT NULL VARCHAR2(2500)
MESSAGE_TIME NOT NULL TIMESTAMP(6)
UPVOTES NOT NULL NUMBER
What I'd like to do is have a stored procedure be called that would increment the upvotes. This seems to make the most sense because I don't want to pass in anything other than the comment_id and post_id.
I think I should be doing something like this (please excuse syntax, I haven't messed with stored procedures in a long long time)
CREATE OR REPLACE procedure proc_upvote_comment (comment_id NUMBER , post_id NUMBER)
BEGIN
SELECT UPVOTES FROM COMMENTS
WHERE COMMENTS.COMMENT_ID = proc_upvote_comment.comment_id
AND COMMENTS.POST_ID = proc_upvote_comment.post_id;
//Call an update
END;
/
But I just don't seem to know the missing piece here. I've tried looking at
http://docs.oracle.com/cd/B19306_01/appdev.102/b14251/adfns_packages.htm#i1007682
http://docs.oracle.com/cd/B12037_01/server.101/b10759/statements_6009.htm
And a few other places on the site - but I'm just missing something.
Any help in the right direction would be great
Is there any reason this is not working for you:
CREATE OR REPLACE procedure proc_upvote_comment (comment_id NUMBER , post_id NUMBER)
AS
BEGIN
UPDATE COMMENTS
SET UPVOTES = UPVOTES + 1
WHERE COMMENTS.COMMENT_ID = proc_upvote_comment.comment_id
AND COMMENTS.POST_ID = proc_upvote_comment.post_id;
END;
Remember that SQL works with sets ;)
You probably just want
CREATE OR REPLACE procedure proc_upvote_comment (p_comment_id NUMBER , p_post_id NUMBER)
AS
BEGIN
UPDATE comments
SET upvotes = upvotes + 1
WHERE comment_id = p_comment_id
AND post_id = p_post_id;
END;
/
Generally, you want to have a naming convention to differentiate parameters from column names. I prefer using the p_ prefix for parameters and the l_ prefix for local variables but there are other conventions. If you don't do this and you use the same name for a column and for a parameter, the SQL statement will resolve identifiers first using the name of the column and then using the name of the parameter. Resorting to prefixing the name of the stored procedure to provide scope resolution works but it's rather cumbersome.