PL/SQL Procedure data freezing (reserving random lotto ticket) - sql

I am using a stored procedure to select a "Random" row from Available_Tickets table, after selection if the user like the number, and buy the ticket, the row will be deleted from the table.
my procedure looks like this:
CREATE OR REPLACE GET_RANDOM_TICKET (RESULT OUT INTEGER) IS
co,mn,mx integer;
BEGIN
SELECT COUNT(ID) , MIN(ID) ,MAX(ID) INTO CO,MN,MX FROM TICKETS;
SELECT TICKET_NUMBER INTO RESULT FROM (
SELECT TICKET_NUMBER
FROM TICKETS WHERE ID >= DBMS_RANDOM(MN,MX)
) WHERE ROWNUM = 1;
END GET_RANDOM_TICKET;
if the user agrees on the returned number the selected row is deleted.
can I get in a worst case scenario where the row with max(id) is delete after executing the first select statement ?
edit 1----
Would the two SELECT statements see the same data in-spite off the changes in the table? and why?

i would redesign it as below
1) add a column (ticket status [free,reserved,sold]) to mark the returned ticket as reserved until the user confirmed his selection
2) use a cursor with for update clause to update that ticket status column after returning it to the user - also add where condition for free tickets
3) check ticket status again after user confirmation if it still reserved then update to sold. (in the very slim chance that 2 users ran the program in the same time and got the same number one of them will get the number and the other one should receive error message since the ticket is not reserved anymore.
hope that help

Related

Oracle : Create session sequence?

I have a table as follows
The table contains my application users and stores their clients. The column User Client ID refers to a foreign key linked to a different table that stores the clients details.
I need another column (User Client Counter) which is a just a counter of the clients of each user. I need it to start from 1 and goes up for each individual application user.
For the moment I'm populating this by counting the number of clients for each user + 1 before inserting a new row in the table :
select count(*) + 1 into MyVariable from Mytable where UserClientId = Something
Then I use MyVariable in the column User Client Counter
This methods works quite well, but in case the user is connected from two different sessions, the query may produce a wrong number of counts... in addition to that the performance may be bad in case of big tables...
Is there anyway better way to replace such process by using sequences ?
I've been looking to session sequences but there are reset after the end of each session.
(This column is a business need and cannot be replaced by something like rownumber in restitution queries. Since every client has to keep always the same identifier for the application user)
Thank you in advance.
Cheers,
I think you can just create a unique index on the app user and the running number:
create unique index idx on mytable (app_user_id, num);
And then insert with max + 1:
insert into mytable (app_user_id, client_id, num)
values
(
:app_user_id,
:client_id,
coalesce((select max(num) + 1 from mytable where app_user_id = :app_user_id), 1)
);
For this sort of requirement to be safe you will need to be able to lock rows at the right level so that you don't have two sessions that think the they are allowed to use the same value. The impact of this is that while one session is inserting a row for the 'Company X' user, another session will wait for the first user to commit if they're also trying to insert a row for 'Company X'.
This is super easy when you just have a table that stores information at the right level.
You can have a table of your users with a counter column which starts at 0.
MY_APPLICATION_USER CLIENT_COUNTER
-------------------------------------------------- --------------
Company X 1
Company Y 3
Company Z 1
As you insert rows into your main table, you update this table first setting the client_counter to be client_counter + 1 (you do this as one insert statement, no risky select then update!), then you return the updated value into your value for the client_id. This can all be done with a simple trigger.
create or replace trigger app_clients_ins
before insert
on app_clients
for each row
declare
begin
update app_users
set client_counter = client_counter + 1
where my_application_user = :new.my_application_user
return client_counter into :new.user_client_number;
end;
/
Of course, like any sequence if you delete a row it's not going to allow your next insert to fill that gap.
(db<>fiddle https://dbfiddle.uk/?rdbms=oracle_18&fiddle=7f1b4f4b6316d5983b921cae7b89317a )
if you want to have unique values to be inserted and there are chances that multiple users can insert rows into the same table at the same time then it is better to user Oracle Sequence.
CREATE SEQUENCE id_seq INCREMENT BY 1;
INSERT INTO Mytable(id) VALUES (id_seq.nextval);
In you case I think you want different sequence created for each Customer, How many different Customers you have, if you have in 100's then i don't think create sequence will work as you may have to create as many sequence .

get the count for different data in single column as set

I am having table "Activation" with the columns account ID and email and product name
product name column is having data like this below
Platform
Access
Onboard
OnGuard
Platform
Access
Onboard
OnGuard
when user activated we will insert the data as a set like this ( Platform
,Access
,Onboard
,OnGuard)
and i need to give a warning to the user if he is inserting more than one set (Platform,Access,Onboard,OnGuard)
I can get the count for single product name insertion like (platform or access), but i need to get the count for single set insertion here ..
Could any one please help on this query how to get count for single set insertion and i am using sql server as a DB for these insertions.
Many thanks in advance..
UPDATE
sorry for confusion i need to give a warning to user if he inserted more than one time any of the product name defined in the set not as a single set ..
Assuming that unique products count is 4, you can check it using aggregation along with having.
select accountid
from activation
group by accountid
having count(distinct productname) = 4
Or you can reverse the logic according to your need(if you want to get the users against whom not all products are defined by changing = to <>
DEMO
You Can Use this In Trigger.
DECLARE #Product_Name NVARCHAR(50)
IF EXISTS (SELECT 1
FROM Activation WHERE productname = #Product_Name) BEGIN
RAISERROR ('Product Name Already Exists', 16, 1)
END

Get Id from a conditional INSERT

For a table like this one:
CREATE TABLE Users(
id SERIAL PRIMARY KEY,
name TEXT UNIQUE
);
What would be the correct one-query insert for the following operation:
Given a user name, insert a new record and return the new id. But if the name already exists, just return the id.
I am aware of the new syntax within PostgreSQL 9.5 for ON CONFLICT(column) DO UPDATE/NOTHING, but I can't figure out how, if at all, it can help, given that I need the id to be returned.
It seems that RETURNING id and ON CONFLICT do not belong together.
The UPSERT implementation is hugely complex to be safe against concurrent write access. Take a look at this Postgres Wiki that served as log during initial development. The Postgres hackers decided not to include "excluded" rows in the RETURNING clause for the first release in Postgres 9.5. They might build something in for the next release.
This is the crucial statement in the manual to explain your situation:
The syntax of the RETURNING list is identical to that of the output
list of SELECT. Only rows that were successfully inserted or updated
will be returned. For example, if a row was locked but not updated
because an ON CONFLICT DO UPDATE ... WHERE clause condition was not
satisfied, the row will not be returned.
Bold emphasis mine.
For a single row to insert:
Without concurrent write load on the same table
WITH ins AS (
INSERT INTO users(name)
VALUES ('new_usr_name') -- input value
ON CONFLICT(name) DO NOTHING
RETURNING users.id
)
SELECT id FROM ins
UNION ALL
SELECT id FROM users -- 2nd SELECT never executed if INSERT successful
WHERE name = 'new_usr_name' -- input value a 2nd time
LIMIT 1;
With possible concurrent write load on the table
Consider this instead (for single row INSERT):
Is SELECT or INSERT in a function prone to race conditions?
To insert a set of rows:
How to use RETURNING with ON CONFLICT in PostgreSQL?
How to include excluded rows in RETURNING from INSERT ... ON CONFLICT
All three with very detailed explanation.
For a single row insert and no update:
with i as (
insert into users (name)
select 'the name'
where not exists (
select 1
from users
where name = 'the name'
)
returning id
)
select id
from users
where name = 'the name'
union all
select id from i
The manual about the primary and the with subqueries parts:
The primary query and the WITH queries are all (notionally) executed at the same time
Although that sounds to me "same snapshot" I'm not sure since I don't know what notionally means in that context.
But there is also:
The sub-statements in WITH are executed concurrently with each other and with the main query. Therefore, when using data-modifying statements in WITH, the order in which the specified updates actually happen is unpredictable. All the statements are executed with the same snapshot
If I understand correctly that same snapshot bit prevents a race condition. But again I'm not sure if by all the statements it refers only to the statements in the with subqueries excluding the main query. To avoid any doubt move the select in the previous query to a with subquery:
with s as (
select id
from users
where name = 'the name'
), i as (
insert into users (name)
select 'the name'
where not exists (select 1 from s)
returning id
)
select id from s
union all
select id from i

Update SQL Server table with one time use values from another table

I have a table of users that has the usual suspects, name, email, etc. As the users complete an activity (queried from another table), I need to award them a gift card code.
update users
set giftcardcode = 'code from other table'
where email in (select email from useractivity where necessary conditions are met)
I have a table of unique gift card codes that are unique, one-time use codes. So I need to update my user table, setting the award code field equal to a distinct, unused gift card code from the gift card code table. Then I need to mark the 'used' field in the gift card table to 'Y'.
The goal is to do this with SQL and not any programming. I'm stumped.
I think there is a Many To Many relationship between User table and Activity table.
So, you can use a trigger to execute a query when update.
Each time a row will be updated in the Activity table, the trigger will do something.
It will UPDATE the User table by adding a new gift code.
I think you can add an attribute in your GiftCode table to easily check if the code as already been used. An you can get an unused code like that :
// Retrieve an unused code based on a BIT attribute.
SELECT TOP 1 [Code] FROM [GiftCode] WHERE IS_UNUSED = 1;
Don't forget to update this Gift code after using it.
You can use a SELECT statement including a sub SELECT statement to get a code too :
// Retrieve an unused code based on User table used codes.
SELECT TOP 1 [Code] FROM [GiftCode] WHERE [Code] NOT IN (SELECT [Code] FROM [User]);
It works well if you don't have too much users.
Otherwise , the first statement will be more efficient.
Don't forget to update the User table.
Now you can easily use one of these previous statement in a UPDATE statement.
It will be something like that :
UPDATE [User] SET [Code] = (
SELECT TOP 1 [Code] FROM [GiftCode] WHERE [Code] NOT IN (
SELECT [Code] FROM [User]))
WHERE USER_ID = // ...;
You can perform this in a trigger.
You can use a stored procedure, it's more efficient and will wrap all the SQL code in a compiled function. Then you can call it in your trigger.
You can execute a stored procedure in a job (see SQL Server Agent jobs) too.
create a Trigger on your table for update and do what you want inside it using inserted and deleted

How to guarantee only one process picks up a processing task

I have multiple computers that have the task of sending out emails found in a table on a common SQL Server. Each computer polls the email table to look for messages it can send by looking at a status flag set to 0. If a computer does a
SELECT * FROM tblEmailQueue where StatusFlag=0
if it returns a record it immediately sets the StatusFlag to 1 which should cause the other computer polling the same table not to find this record. My fear is that if two computer find the record at the same time before either can update the StatusFlag, the email will be sent twice. Does anyone have ideas on how to ensure only one computer will get the record? I know I might be able to do a table lock but I would rather now have to do this.
Instead of using two queries which may cause a race condition, you can update the values and output the updated rows at once using the OUTPUT clause.
This will update the rows with statusflag=0 and output all of the updated ones;
UPDATE tblEmailQueue
SET statusflag=1
OUTPUT DELETED.*
WHERE statusflag=0;
An SQLfiddle to test with.
EDIT: If you're picking one row, you may want some ordering. Since the update itself can't order, you can use a common table expression to do the update;
WITH cte AS (
SELECT TOP 1 id, statusflag FROM tblEmailQueue
WHERE statusflag = 0 ORDER BY id
)
UPDATE cte SET statusflag=1 OUTPUT DELETED.*;
Another SQLfiddle.
You can perform select and send email in the same transaction. Also you can use ROWLOCK hint and don't commit transaction until you send email or set new value for StatusFlag. It means that nobody (exept transaction with hint NOLOCK or READ UNCOMMITED isolation level) can read this row as long as you commit transaction.
SELECT * FROM tblEmailQueue WITH(ROWLOCK) where StatusFlag=0
In addition you should check isolation level. For your case isolation level should be READ COMMITED or REPEATABLE READ.
See information about isolation levels here
Add another column to your table tblEmailQueue (say UserID), then try to pull one email such as
--Let flag an email and assign it to the application who made the request
--#CurrentUserID is an id unique to each application or user and supplied by the application
--or user who made the request, this will also ensures that the record is going to
--the right application and perhaps you can use it for other purpose such as monitoring.
UPDATE tblEmailQueue set UserID = #CurrentUserID, StatusFlag=1 where ID = isnull(
select top 1 ID from tblEmailQueue where StatusFlag=0 order by ID
), 0)
--Lets get an email that had a flag for the current user id
SELECT * FROM tblEmailQueue where StatusFlag=1 and UserID = #CurrentUserID
Here in Indianapolis, we are familiar with race conditions ;-)
Lets assume you actually have and ID field and a StatusFlag field and create a stored proc that includes
declare #id int
select top 1 #id = id from tblEmailQuaue where StatusFlag=0
if ##rowcount = 1
begin
update tblEmailQuaue set StatusFlag=1 where ID = #id AND StatusFlag=0
if ##rowcount = 1
begin
-- I won the race, continue processing
...
end
end
ADDED
An explicit handling like this is inferior to Joachim's method if all you want is the result of the select. But this method this method also works with old versions of SQL server as well as other databases.