Why does SQL Server explicit predicate locking disallow INSERT statements outside of the predicate lock - sql-server

Assuming we have the following database tables:
create table department (
id bigint not null,
budget bigint not null,
name varchar(255),
primary key (id)
)
create table employee (
id bigint not null,
name varchar(255),
salary bigint not null,
department_id bigint,
primary key (id)
)
alter table employee
add constraint FKbejtwvg9bxus2mffsm3swj3u9
foreign key (department_id) references department
And we have 3 department rows:
insert into department (name, budget, id)
values ('Department 1', 100000, 1)
insert into department (name, budget, id)
values ('Department 2', 75000, 2)
insert into department (name, budget, id)
values ('Department 3', 90000, 3)
And we also have 3 employee rows:
insert into employee (department_id, name, salary, id)
values (1, 'CEO', 30000, 1)
insert into employee (department_id, name, salary, id)
values (1, 'CTO', 30000, 2)
insert into employee (department_id, name, salary, id)
values (2, 'CEO', 30000, 3)
Assuming we have two concurrent users: Alice and Bob.
First, Alice locks all the employee belonging to the 1st department:
SELECT *
FROM employee
WITH (HOLDLOCK)
WHERE department_id = 1
Now, in the meanwhile, it's expected that Bob cannot insert a new employee using the same department_id:
INSERT INTO employee WITH(NOWAIT) (department_id, name, salary, id)
VALUES (1, 'Carol', 9000, 6)
The insert statement above ends with Lock request time out period exceeded which is fine since Alice locked that particular range.
However, why is the following insert blocked as well:
INSERT INTO employee WITH(NOWAIT) (department_id, name, salary, id)
VALUES (3, 'Dave', 9000, 7)
This insert statement uses a department_id value which is beyond the range of Alice's predicate lock. But then, this insert statement also ends up with a Lock request time out period exceeded exception.
Why does the SQL Server HOLDLOCK predicate lock extend beyond its range?
UPDATE
By adding an index to the FK:
create index IDX_DEPARTMENT_ID on employee (department_id)
And increasing the number of employee entries in the 1st and 2nd department to 1000, I managed to see that the predicate lock behaves as expected.

The only way that the SELECT query could be satisfied was by performing a table scan. There's no natural resource by which it could locate and lock based only on department_id, and so it ends up locking all rows and preventing any insertions.
In this toy example, just adding an index on department_id will not help since the optimizer will still choose to perform a table scan. However, on a larger more realistic table, I believe that adding such an index would allow the query to apply locks in a more targeted fashion.

What Damien said was correct..When you dont have index on department id (predicate column) ,the range increases and HoldLock means
HOLDLOCK means SERALIZABLE and therefore allows SELECTS, but blocks UPDATE and DELETES of the rows selected by T1, as well as any INSERT in the range selected by T1 .
so in this case, an index of the below form would help and my test confirms the same
Below is a sample test i did
in session1:
create index nci_department_id on dbo.employee(department_id)
include
(id,name,salary)
go
begin tran
SELECT *
FROM employee
WITH (HOLDLOCK)
WHERE department_id = 1
in session2:
INSERT INTO employee WITH(NOWAIT) (department_id, name, salary, id)
VALUES (3, 'Dave', 9000, 7)
Now the above insert succeeds
References:
https://stackoverflow.com/a/7845331/2975396

Related

Select all Main table rows with detail table column constraints with GROUP BY

I've 2 tables tblMain and tblDetail on SQL Server that are linked with tblMain.id=tblDetail.OrderID for orders usage. I've not found exactly the same situation in StackOverflow.
Here below is the sample table design:
/* create and populate tblMain: */
CREATE TABLE tblMain (
ID int IDENTITY(1,1) NOT NULL,
DateOrder datetime NULL,
CONSTRAINT PK_tblMain PRIMARY KEY
(
ID ASC
)
)
GO
INSERT INTO tblMain (DateOrder) VALUES('2021-05-20T12:12:10');
INSERT INTO tblMain (DateOrder) VALUES('2021-05-21T09:13:13');
INSERT INTO tblMain (DateOrder) VALUES('2021-05-22T21:30:28');
GO
/* create and populate tblDetail: */
CREATE TABLE tblDetail (
ID int IDENTITY(1,1) NOT NULL,
OrderID int NULL,
Gencod VARCHAR(255),
Quantity float,
Price float,
CONSTRAINT PK_tblDetail PRIMARY KEY
(
ID ASC
)
)
GO
INSERT INTO tblDetail (OrderID, Gencod, Quantity, Price) VALUES(1, '1234567890123', 8, 12.30);
INSERT INTO tblDetail (OrderID, Gencod, Quantity, Price) VALUES(1, '5825867890321', 2, 2.88);
INSERT INTO tblDetail (OrderID, Gencod, Quantity, Price) VALUES(3, '7788997890333', 1, 1.77);
INSERT INTO tblDetail (OrderID, Gencod, Quantity, Price) VALUES(3, '9882254656215', 3, 5.66);
INSERT INTO tblDetail (OrderID, Gencod, Quantity, Price) VALUES(3, '9665464654654', 4, 10.64);
GO
Here is my SELECT with grouping:
SELECT tblMain.id,SUM(tblDetail.Quantity*tblDetail.Price) AS TotalPrice
FROM tblMain LEFT JOIN tblDetail ON tblMain.id=tblDetail.orderid
WHERE (tblDetail.Quantity<>0) GROUP BY tblMain.id;
GO
This gives:
The wished output:
We see that id=2 is not shown even with LEFT JOIN, as there is no records with OrderID=2 in tblDetail.
How to design a new query to show tblMain.id = 2? Mean while I must keep WHERE (tblDetail.Quantity<>0) constraints. Many thanks.
EDIT:
The above query serves as CTE (Common Table Expression) for a main query that takes into account payments table tblPayments again.
After testing, both solutions work.
In my case, the main table has 15K records, while detail table has some millions. With (tblDetail.Quantity<>0 OR tblDetail.Quantity IS NULL) AND tblDetail.IsActive=1 added on JOIN ON clause it takes 37s to run, while the first solution of #pwilcox, the condition being added on the where clause, it ends up on 29s. So a gain of time of 20%.
tblDetail.IsActive column permits me ignore detail rows that is temporarily ignored by setting it to false.
So the for me it's ( #pwilcox's answer).
where (tblDetail.quantity <> 0 or tblDetail.quantity is null)
Change
WHERE (tblDetail.Quantity<>0)
to
where (tblDetail.quantity <> 0 or tblDetail.quantity is null)
as the former will omit id = 2 because the corresponding quantity would be null in a left join.
And as HABO mentions, you can also make the condition a part of your join logic as opposed to your where statement, avoiding the need for the 'or' condition.
select m.id,
totalPrice = sum(d.quantity * d.price)
from tblMain m
left join tblDetail d
on m.id = d.orderid
and d.quantity <> 0
group by m.id;

SQL-How to name and save a query table after it has been joined by another table?

I am trying to practice using Microsoft SQL Server Management Studio 2014 on my own and I seem to have trouble understanding how to name and save a table after it has been joined by another table. I tried to find similar examples online, but they were either not helpful or they created an error with my code.
Here is the code for my joined table:
CREATE TABLE students (id INTEGER PRIMARY KEY, first_name TEXT, last_name
TEXT, email TEXT, phone TEXT, birthdate TEXT);
INSERT INTO students (id, first_name, last_name, email, phone, birthdate)
VALUES (1, 'Peter', 'Rabbit', 'peter#rabbit.com', '555-6666', '2002-06-24');
INSERT INTO students (id, first_name, last_name, email, phone, birthdate)
VALUES (2, 'Alice', 'Wonderland', 'alice#wonderland.com', '555-4444', '2002-
07-04');
CREATE TABLE student_grades (id INTEGER PRIMARY KEY, student_id INTEGER,
test TEXT, grade INTEGER);
INSERT INTO student_grades (id, student_id, test, grade) VALUES (1, 1,
'Nutrition', 95);
INSERT INTO student_grades (id, student_id, test, grade) VALUES (2, 2,
'Nutrition', 92);
INSERT INTO student_grades (id, student_id, test, grade) VALUES (3, 1,
'Chemistry', 85);
INSERT INTO student_grades (id, student_id, test, grade) VALUES (4, 2,
'Chemistry', 95);
And here is the joined table code:
SELECT *
FROM student_grades
JOIN students
ON student_grades.student_id = students.id
I have successfully been able to join these two tables together, but I seem to have trouble trying to name and save this new joined table called TableA. It goes like this:
SELECT * INTO TableA
FROM student_grades
JOIN students
ON student_grades.student_id = students.id
All that I get from the result is:
Column names in each table must be unique. Column name 'id' in table
'TableA' is specified more than once.
I do not know why it is displaying this error. Is my syntax correct and I am using the incorrect names, or is my syntax incorrect altogether? Thanks for all possible responses.
You cannot have 2 columns with the same name in a table; in your case [id]. Since student_id from student_grades table is the same as id from students table, you can disregard the latter. Please see query below.
SELECT sg.[id] ,
sg.[student_id] ,
sg.[test] ,
sg.[grade] ,
s.[first_name] ,
s.[last_name] ,
s.[email] ,
s.[phone] ,
s.[birthdate]
INTO TableA
FROM student_grades sg
JOIN students s ON sg.student_id = s.id;
Because the two tables have the field name "id" so you have to use alias name with the "id" field like below:
SELECT students.id as studentId, first_name, last_name, email, phone, birthdate
,student_grades.id as gradeId, test, grade INTO TableA
FROM student_grades
JOIN students
ON student_grades.student_id = students.id

ORACLE, display record according to its related

I have three tables, one containing the names of some movies and their 'id', also have a table with the names of the actors and their 'id', and finally the third table that is relational, which has the id of the movie and the id of the actor. What I need? Let's assume that the Actor table there is the record of 'NEMO' 'Dori' and 'Jack' and the table film for the record 'Finding Nemo' and 'TITANIC', and I need ONLY view the name of the movie that contains the actors 'NEMO' and 'DORI' (which were registered with the id of the movie 'Finding Nemo'); Here are the tables below:
Movie table
create table tb_filme(
id_filme NUMBER (4) PRIMARY KEY NOT NULL,
nome_filme VARCHAR (50) NOT NULL
);
Actor table
create table tb_ator(
id_ator NUMBER (4) PRIMARY KEY NOT NULL,
nome_ator VARCHAR (50) NOT NULL
);
Actor and Movie table
create table ator_filme(
id_filme NUMBER (4) references tb_filme(id_filme),
id_ator NUMBER (4) references tb_ator(id_ator)
);
Remembering that my result to be 'Finding Nemo' only
You may need something like this:
select nome_filme
from tb_filme
inner join ator_filme
using(id_filme)
inner join tb_ator
using(id_ator)
where nome_ator in ('NEMO', 'Dori')
group by nome_filme
having count(id_ator) = 2
This simply joins your tables to find all the movies with NEMO and/or Dori, then it aggregates to get only one row for movie and checks that all the 2 actors are in the movie.
This can be re-written in many different ways, I believe this uses basic concepts and can be quite self-explanatory.
With this data
insert into tb_ator values (1, 'NEMO');
insert into tb_ator values (2, 'Jack');
insert into tb_ator values (3, 'Dori');
insert into tb_filme values (1, 'Finding NEMO');
insert into tb_filme values (2, 'TITANIC');
insert into tb_filme values (3, 'NEMO ALONE');
insert into ator_filme values (1,1);
insert into ator_filme values (1,3);
insert into ator_filme values (2,2);
insert into ator_filme values (3,1);
this gives:
NOME_FILME
---------------------
Finding NEMO

Using the count function in oracle to display a student who hasnt registered for a course

I have to list the student name and the number of courses he or she has taken (Include students who have not yet taken any class)
Here are 3 of my 5 tables, shouldn't need faculty and course table
--Student Table
CREATE TABLE Student(
Std_ID NUMBER(4) CONSTRAINT Student_ID_pk PRIMARY KEY,
Std_FN VARCHAR2(20),
Std_LN VARCHAR2(20),
Std_City VARCHAR2(20),
Std_State VARCHAR2(20),
Std_Zip NUMBER(5),
Std_Major VARCHAR2(10),
Std_Class VARCHAR2(2),
Std_GPA NUMBER(3,2) CONSTRAINT Student_GPA_cc CHECK (Std_GPA<= 4.0 AND Std_GPA>=0));
INSERT INTO Student
VALUES('101','Joe','Smith','Eau Clare','WI', '18121','IS','FR','3.0');
INSERT INTO Student
VALUES('102','Jenny','Sneider','Eau Clare','WI', '98011','IS','JR','3.2');
INSERT INTO Student
VALUES('103','Dan','Robinson','Sartell','MN', '98042','IS','JR','3.5');
INSERT INTO Student
VALUES('104','Sue','Williams','St.Cloud','MN', '56301','ACCT','SR','3.2');
-------------------------------------------------------------------------------
--Offering Table
CREATE TABLE Offering(
Offer_No NUMBER(4) CONSTRAINT Offer_No_pk PRIMARY KEY,
Course_No VARCHAR2(10) CONSTRAINT Course_No_fk REFERENCES Course(Course_No),
Off_Term VARCHAR2(7),
Off_Year NUMBER(4),
Off_Loca VARCHAR2(10),
Off_Time Varchar2(8),
Off_Day VARCHAR2(7),
Fac_SSN NUMBER(4) CONSTRAINT Fac_SSn_fk REFERENCES Faculty(Fac_ID));
INSERT INTO OFFERING
VALUES('2201', 'IS 250', 'Spring', '2000', 'BB260', '10:30am', 'MWF', '9002');
INSERT INTO OFFERING
VALUES('2202', 'IS 250', 'Spring', '1999', 'BB118', '8:00am', 'TTH', '9002');
INSERT INTO OFFERING
VALUES('2203', 'IS 350', 'Fall', '2001', 'BB260', '9:30am', 'TTH', '9001');
INSERT INTO OFFERING
VALUES('2204', 'IS 351', 'Fall', '2001', 'BB315', '12:30pm', 'TTH', '9003');
INSERT INTO OFFERING
VALUES('1101', 'ACCT 291', 'Fall', '2000', 'BB320', '12:30pm', 'MWF', '9010');
INSERT INTO OFFERING
VALUES('2205', 'IS 443', 'Fall', '2002', 'BB216', '12:30pm', 'MWF', '9003');
---------------------------------------------------------------------------------
--Enrollment Table
CREATE TABLE Enrollment(
Std_ID NUMBER(4) CONSTRAINT Enroll_Std_ID_fk REFERENCES Student(std_ID),
Offer_No NUMBER(4) CONSTRAINT Enroll_Offer_No_fk REFERENCES Offering(Offer_No),
Enr_Grade Char(1) CONSTRAINT Enroll_grade_cc CHECK (Enr_Grade IN('A','B','C','D','F')),
CONSTRAINT Enroll_pk PRIMARY KEY (Std_ID,Offer_No));
INSERT INTO ENROLLMENT
VALUES('101', '2201', 'A');
INSERT INTO ENROLLMENT
VALUES('101', '2203', 'B');
INSERT INTO ENROLLMENT
VALUES('102', '2203', 'C');
INSERT INTO ENROLLMENT
VALUES('103', '2203', 'B');
INSERT INTO ENROLLMENT
VALUES('103', '2201', 'C');
INSERT INTO ENROLLMENT
VALUES('103', '1101', 'B');
Here is what I came up with as of now. It still doesn't display the student who hasn't registered for a course. How can I fix this so that it does display sue with 0 courses taken.Thanks.
--Q2
SELECT Count(*) as Num_Courses, Std_FN, Std_LN
FROM Enrollment, Student
WHERE Enrollment.Std_ID = Student.Std_ID
Group by Enrollment.Std_ID, Std_FN, Std_LN
Having Count(*)>= 0;
Your query has an inner join, which means that the Std_ID needs to be in both tables or else the row won't be included. In other words, students with no courses aren't part of the result set that's counted.
Use a left join instead, and count on the Enrollment.Std_ID. If the student doesn't have a matching Enrollment row, then Enrollment.Std_ID will be null so it won't be counted, which results in a zero for that student - and that's what you want.
One more thing: I'm pretty sure Oracle will want you to have Std_ID in the SELECT clause if you're grouping by it.
SELECT COUNT(Enrollment.Std_ID) AS Num_Courses, Student.Std_ID, Std_FN, Std_LN
FROM Student
LEFT JOIN Enrollment ON Student.Std_ID = Enrollment.Std_ID
GROUP BY Student.Std_ID, Std_FN, Std_LN
You need a OUTER Join here. Students having no enrolments would be returned with NULL values from the enrollment table, thus the count of it would be 0. And your modified query is
SELECT Count(Enrollment.Std_ID) as Num_Courses, Std_FN, Std_LN
FROM Enrollment, Student
WHERE Student.Std_ID = Enrollment.Std_ID(+)
Group by Student.Std_ID, Std_FN, Std_LN
Having Count(*)>= 0;

Comparing mutiple columns and selecting only one

My table is created like this:
CREATE TABLE Employees (
EmployeeID char(4) PRIMARY KEY,
EmployeeName varchar(30) NOT NULL,
HourlyRate real,
SkillType varchar(20),
SupervisorID char(4) NOT NULL
)
Insert statements:
insert into Employees values ('1235', 'J. Smith', 12.5, 'electrical', '1311')
insert into Employees values ('1412', 'A. Green', 13.75, 'plumbing', '1520')
insert into Employees values ('2920', 'J. Brown', 10.0, 'roofing', '2920')
insert into Employees values ('3231', 'A. Purple', 17.4, 'carpenter', '3231')
insert into Employees values ('1520', 'J. Red', 11.75, 'plumbing', '1520')
insert into Employees values ('1311', 'A. Orange', 15.5, 'electrical', '1311')
insert into Employees values ('3001', 'A. Black', 8.2, 'carpenter', '3231')
What I have is a table that lists employees and their Supervisor's ID, who are also contained in the same Employees table. I want to only select the employees that have a hourly rate higher than their corresponding supervisor.
I've tried lots of different Select statements to try this, but they either all return too many values, or no values at all. I know that I need to have 2 instances of the Employee table in order to compare the supervisors hourly rate with the employees hourly rate.
You can use this query. Notice that you will get only employees which have supervisor in the table. You want also them which hasn't supervisor you can use LEFT JOIN instead.
SELECT
E1.*
FROM Employees E1
JOIN Employees E2
ON E1.SupervisorID = E2.EmployeeID
WHERE E1.HourlyRate > E2.HourlyRate

Resources