I need some helps to join the tables I have currently.
Leave, Overtime And Roster's Date, EmployeeID need to match
Note: ShiftDuration is set to default value = 8.25
Note: Leave and Overtime table will only have entries when an employee applies for leave and overtime.
Employee
EmplyeeeID | Username | Password | GivenName | FamilyName | TeamID | ContactNo | StaffType
------------------------------------------------------------------------------------------
123 123 abc John Snow 1 999 1
1234 1234 abcd Jack Waller 2 223 1
12345 12345 abcde Ali Saw 1 123 1
123456 123456 abcdef Peter Peter 2 223 1
1234567 1234567 abcdeg Bryan Peter 1 333 1
Roster
Duty_ID | EmployeeID | Date | ShiftType | ShiftDuration
--------------------------------------------------------------------
2 123 2018-05-05 1 8.25
4 1234 2018-05-04 1 8.25
5 12345 2018-05-05 1 8.25
7 123456 2018-05-04 1 8.25
8 1234567 2018-05-05 1 8.25
Overtime
OTID | EmployeeID | Date | OT_Duration | OT_Reason
------------------------------------------------------------
2 1234 2018-05-04 2 Cover Duty
Leave
LeaveID | EmployeeID | Date | Duration_Off | Reason
----------------------------------------------------------
3 123 2018-05-05 2 NIL
IdealTable (Via Query)
Date | EmployeeID | GivenName | FamilyName | TeamID | ShiftType | ShiftDuration | Duration_Off | OT_Duration | Total_Hours
---------------------------------------------------------------------------------------------------------------------------------
2018-05-05 123 John Snow 1 1 8.25 2 0 6.25
2018-05-04 1234 Jack Waller 1 1 8.25 0 2 10.25
2018-05-05 12345 Ali Saw 1 1 8.25 0 0 8.25
2018-05-04 123456 Peter Peter 1 1 8.25 0 0 8.25
2018-05-05 1234567 Bryan Peter 1 1 8.25 2 0 8.25
I have 4 tables, they are Employee, Leave, Overtime, Roster
Employee
-EmployeeID (PK)
-Username
-Password
-GivenName
-FamilyName
-TeamID
-ContactNo
-StaffType
Leave
-LeaveID (PK)
-EmployeeID (FK)
-Date
-Duration_Off
-Reason
Overtime
-OTID (PK)
-EmployeeID (FK)
-Date
-OT_Duation
-OT_Reason
Roster
-DutyID (PK)
-EmployeeID (FK)
-Date
-ShiftType
-Shift Duration (Default Value = 8.25)
What I am trying to do is join the data from this 4 tables using Query
Ideal Table
-Date (From Leave, Overtime and Roster Table)
-EmployeeID (Employee Table)
-GivenName (Employee Table)
-FamilyName (Employee Table)
-TeamID (Employee Table)
-ShiftType (Roster Table)
-ShiftDuration (Roster Table)
-Duration_Off (Leave Table)
-OT_Duration (Overtime Table)
-Total_Hours (Calculation from joint table [(ShiftDuration + OT_Duration) - Duration_Off]
My database diagram design Do ignore the TimeData table as I initially wanted to use the TimeData table to achieve the IdealTable
My current query
USE [SMRT Dashboard]
GO
;With Dates
AS
(
SELECT [Date] FROM dbo.Roster
UNION
SELECT [Date] FROM dbo.Leave
UNION
SELECT [Date] FROM dbo.Overtime
),
Work_Matrix
AS
(
SELECT EmployeeID,[Date],ShiftType,ShiftDuration,CAST(NULL AS Decimal(30,2)) AS Duration_Off,CAST(NULL AS Decimal(30,2)) AS OT_Duration
FROM dbo.Roster
UNION ALL
SELECT EmployeeID,[Date], NULL, NULL,Duration_Off
FROM dbo.Leave
UNION ALL
SELECT EmployeeID,[Date],NULL,NULL,NULL,OT_Duration
FROM dbo.Overtime
)
SELECT d.[Date],
e.EmployeeID,
e.GivenName,
e.FamilyName,
e.TeamID,
w.ShiftType,
w.ShiftDuration,
w.Duration_Off,
w.OT_Duration,
w.Total_Hours
FROM Dates d
INNER JOIN
(
SELECT EmployeeID,
[Date],
MAX(ShiftType) AS ShiftType,
SUM(ShiftDuration) AS ShiftDuration,
SUM(Duration_Off) AS Duration_Off,
SUM(OT_Duration) AS OT_Duration,
SUM(ShiftDuration) + SUM(OT_Duration) - SUM(Duration_Off) AS Total_Hours
FROM Work_Matrix
GROUP BY EmployeeID,
[Date]
)w
ON d.[Date] = w.[Date]
JOIN dbo.Employee e
ON e.EmployeeID = w.EmployeeID
Current Errors:
Msg 205, Level 16, State 1, Line 4
All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.
I have not tested this but you may now check this. Hope your problem will be solved.
SELECT
a.EmployeeID,
a.GivenName,
a.FamilyName,
a.TeamID,
d.ShiftType,
d.ShiftDuration,
b.Duration_Off,
c.OT_Duration,
b.Date,
(d.ShiftDuration + c.OT_Duration) - b.Duration_Off as Total_Hours
FROM Employee a
INNER JOIN Roster d ON a.EmployeeID = d.EmployeeID
LEFT JOIN Leave b ON a.EmployeeID = b.EmployeeID
LEFT JOIN Overtime c ON a.EmployeeID = c.EmployeeID
I assume Date is same from all three tables (Leave, Overtime and Roster Table)
Try this query :
select
l.Date,
o.Date,
r.Date,
e.EmployeeID,
e.GivenName,
e.FamilyName,
e.TeamID,
r.ShiftType,
r.ShiftDuration,
l.Duration_Off,
o.OT_Duration,
((r.ShiftDuration+o.OT_Duration)-l.Duration_Off) as Total_Hours
FROM Employee e
INNER JOIN Leave l ON e.EmployeeID = l.EmployeeID
INNER JOIN Overtime o ON e.EmployeeID = o.EmployeeID
INNER JOIN Roster r ON r.EmployeeID = r.EmployeeID;
Try this Query ! You will get the correct output .
SELECT
e.EmployeeID AS 'Emp ID',
e.GivenName AS 'Emp Name',
l.Date AS Date,
e.FamilyName AS 'Family Name',
e.TeamID AS 'Team ID',
r.ShiftType AS 'Shift Type',
r.ShiftDuration AS 'Shift Duration',
l.Duration_Off AS 'Duration Off',
o.OT_Duration AS 'OT Duration',
(r.ShiftDuration + o.OT_Duration) - l.Duration_Off as 'Total Hours'
FROM
Employee e,
Leave l,
Overtime o,
Roster r
WHERE
e.EmployeeID = l.EmployeeID
AND
e.EmployeeID = o.EmployeeID
AND
e.EmployeeID = r.EmployeeID
I think the issue here is that an employee may have leave, may have overtime, or neither. By using INNER JOIN, you're filtering down the set of employees to ones that have Leave, and on a Roster, and have Overtime.
I created a SqlFiddle with the data you present (see here) that should get you closer.
SELECT
COALESCE(r.Date, o.Date, l.Date) as Date,
e.EmployeeID,
e.GivenName,
e.FamilyName,
e.TeamID,
r.ShiftType,
r.ShiftDuration,
IFNULL(l.Duration_Off, 0) as Duration_Off,
IFNULL(o.OT_Duration, 0) as OT_Duration,
r.ShiftDuration + IFNULL(o.OT_Duration, 0) - IFNULL(l.Duration_Off, 0) as Total_Hours
FROM Employee e
INNER JOIN Roster r on
e.EmployeeID = r.EmployeeID
LEFT JOIN Overtime o on
e.EmployeeID = o.EmployeeID
LEFT JOIN `Leave` l on
e.EmployeeID = l.EmployeeID
There's a few cavaets that this query won't solve:
If an employee has multiple leave entries or multiple overtime entries, the person will be listed N times. You can potentially use a GROUP BY statement to deal with this
The sample data in your question doesn't reconcile correctly. For example, Employe 1234 has a shift on 5/4/2018 and OT on 5/5/2018 but gets 10.25 on 5/4/2018.
If you use TimeData, this becomes a lot easier (assuming one record per date per employee):
SELECT
COALESCE(r.Date, o.Date, l.Date) as Date,
e.EmployeeID,
e.GivenName,
e.FamilyName,
e.TeamID,
r.ShiftType,
IFNULL(r.ShiftDuration, 0) as ShiftDuration,
IFNULL(l.Duration_Off, 0) as Duration_Off,
IFNULL(o.OT_Duration, 0) as OT_Duration,
IFNULL(r.ShiftDuration, 0) + IFNULL(o.OT_Duration, 0) - IFNULL(l.Duration_Off, 0) as Total_Hours
from TimeData t
INNER JOIN Employee e on
t.EmployeeID = e.EmployeeID
LEFT JOIN Roster r on
t.Duty_ID = r.Duty_ID
LEFT JOIN Overtime o on
t.OTID = o.OTID
LEFT JOIN `Leave` l on
t.LeaveID = l.LeaveID
Example SqlFiddle can be found here
Related
This question already has answers here:
Get top 1 row of each group
(19 answers)
Closed 4 months ago.
We have 2 Tables Employees and Department.
We want to show the maximum salary from each department and their corresponding employee name from the employee table and the department name from the department table.
Employee Table
EmpId | EmpName |salary |DeptId
101 shubh1 1000 1
101 shubh2 4000 1
102 shubh3 3000 2
102 shubh4 5000 2
103 shubh5 12000 3
103 shubh6 1000 3
104 shubh7 1400 4
104 shubh8 1000 4
Department Table
DeptId | DeptName
1 ComputerScience
2 Mechanical
3 Aeronautics
4 Civil
I tried doing it but was getting error
SELECT DeptName FROM Department where deptid IN(select MAX(salary),empname,deptid
FROM Employee
GROUP By Employee.deptid)
Error
Token error: 'Column 'Employee.EmpName' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.' on server 4e0652f832fd executing on line 1 (code: 8120, state: 1, class: 16)
Can someone please help me.
select salary
,EmpName
,DeptName
from (
select e.salary
,e.EmpName
,d.DeptName
,rank() over(partition by e.DeptId order by e.salary desc) as rnk
from Employee e join Department d on d.DeptId = e.DeptId
) t
where rnk = 1
salary
EmpName
DeptName
4000
shubh2
ComputerScience
5000
shubh4
Mechanical
12000
shubh5
Aeronautics
1400
shubh7
Civil
Fiddle
Now that I know it's MS SQL Server, technically; we could use cross or outer Apply; it's a table value function not a join per say... but this will depend on the version of SQL Server; and if you want data if it doesn't exist in another
I find this the "Best" Design pattern to use for this type of query.
What the engine does is for each record in department, it runs a query for the employees Finding those in that department returning the 1 record having the max salary. With top we could specify with ties to return more than one. but we would need to know how to handle Ties of salary. Use top 1 with ties or order the results so you get the "Top" result you want.
Demo: dbfddle.uk
SELECT Sub.empName, Sub.Salary, D.DeptName
FROM Department D
CROSS Apply (SELECT Top 1 *
--(SELECT TOP 1 with Ties * -- could use this if we ties
FROM Employee E
WHERE E.DeptID = D.DeptID
ORDER BY Salary Desc) Sub --add additional order by if we don't want ties.
The cross apply gives us:
+---------+--------+-----------------+
| empName | Salary | DeptName |
+---------+--------+-----------------+
| shubh2 | 4000 | ComputerScience |
| shubh4 | 5000 | Mechanical |
| shubh5 | 12000 | Aeronautics |
| shubh7 | 1400 | Civil |
+---------+--------+-----------------+
Before window functions, before cross Apply or lateral... We'd write an inline view
It would get us the max salary for each dept, we then join that back to our base tables to find the employee within each dept with max salary...
Demo: DbFiddle.uk
SELECT E.*, D.*
FROM Employee E
INNER JOIN Department D
on E.DeptID = D.DeptID
INNER JOIN (SELECT MAX(SALARY) maxSal , DeptID
FROM Employee
GROUP BY DeptID) Sub
on Sub.DeptID = E.DeptID
and Sub.MaxSal = E.Salary
One has to do a join to get the department info an the employee info. However, we can eliminate the join for salarymax by using exists and correlation instead.
Demo DbFiddle.uk
SELECT E.*, D.*
FROM Employee E
INNER JOIN Department D
on E.DeptID = D.DeptID
WHERE EXISTS (SELECT MAX(Sub.SALARY) maxSal , Sub.DeptID
FROM Employee Sub
WHERE sub.DeptID=E.DeptID --correlation 1
GROUP BY Sub.DeptID
HAVING E.Salary = max(Sub.Salary)) --correlation 2
We could eliminate the last join too I suppose:
Demo: Dbfiddle.uk
SELECT E.*, (SELECT DeptName from Department where E.DeptID = DeptID)
FROM Employee E
WHERE EXISTS (SELECT MAX(Sub.SALARY) maxSal , Sub.DeptID
FROM Employee Sub
WHERE sub.DeptID=E.DeptID --correlation 1
GROUP BY Sub.DeptID
HAVING E.Salary = max(Sub.Salary)) --correlation 2
The top 3 give us this result:
+-----+---------+--------+--------+--------+-----------------+
| id | empName | salary | deptID | DeptID | DeptName |
+-----+---------+--------+--------+--------+-----------------+
| 101 | shubh2 | 4000 | 1 | 1 | ComputerScience |
| 102 | shubh4 | 5000 | 2 | 2 | Mechanical |
| 103 | shubh5 | 12000 | 3 | 3 | Aeronautics |
| 104 | shubh7 | 1400 | 4 | 4 | Civil |
+-----+---------+--------+--------+--------+-----------------+
I have two tables Customers, Sales like below:
Customers :
id
first_name
last_name
gender
age
customer_since
1
Daniel
Black
M
34
2014-10-13
2
Erik
Brown
M
25
2015-06-10
3
Diana
Trump
F
39
2015-10-25
4
Anna
Yao
F
19
2017-02-20
5
Christian
Sanders
M
42
2018-01-31
Sales:
id
date
book_id
customer_id
quantity
amount
1
2019-09-02
2
3
1
14.99
2
2019-10-01
1
2
1
12.99
3
2019-10-01
3
4
1
15.75
I need to get all customers and their data in sales table even if they don't have records in sales table so I did left join like below :
SELECT c.id as customer_id, c.first_name, c.last_name, c.gender, c.age, c.customer_since,
s.sdate AS sales_date, s.id as sale_id
FROM customers c
LEFT JOIN sales s
ON c.id = s.customer_id
and I got this result:
CUSTOMER_ID
FIRST_NAME
LAST_NAME
GENDER
AGE
CUSTOMER_SINCE
SALES_DATE
SALE_ID
3
Diana
Trump
F
39
25-OCT-15
02-SEP-19
1
2
Erik
Brown
M
25
10-JUN-15
01-OCT-19
2
1
Daniel
Black
M
34
13-OCT-14
-
-
4
Anna
Yao
F
19
20-FEB-17
-
-
5
Christian
Sanders
M
42
31-JAN-18
-
-
which is expected my question is what if I need to get all customers that only in the source table and customers from the target table which has specific condition I run the below query
SELECT c.id as customer_id, c.first_name, c.last_name, c.gender, c.age, c.customer_since,
s.sdate AS sales_date, s.id as sale_id
FROM customers c
LEFT JOIN sales s
ON c.id = s.customer_id
where s.id=1
and I got only one record like this
CUSTOMER_ID
FIRST_NAME
LAST_NAME
GENDER
AGE
CUSTOMER_SINCE
SALES_DATE
SALE_ID
3
Diana
Trump
F
39
25-OCT-15
02-SEP-19
1
but I need this result
CUSTOMER_ID
FIRST_NAME
LAST_NAME
GENDER
AGE
CUSTOMER_SINCE
SALES_DATE
SALE_ID
3
Diana
Trump
F
39
25-OCT-15
02-SEP-19
1
1
Daniel
Black
M
34
13-OCT-14
-
-
4
Anna
Yao
F
19
20-FEB-17
-
-
5
Christian
Sanders
M
42
31-JAN-18
-
-
What should I do?
You need to move the criteria from the WHERE clause of the second query to the ON clause of the join:
SELECT c.id as customer_id, c.first_name, c.last_name, c.gender, c.age,
c.customer_since, s.sdate AS sales_date, s.id AS sale_id
FROM customers c
LEFT JOIN sales s
ON c.id = s.customer_id AND s.id = 1;
By restricting the sales ID in the WHERE clause, you filter off any non matching records. By moving this restriction to the join, all records would be retained, but NULL values would appear for all non matching sales fields.
The objective of the query is get a count of employees absent under each manager.
Attendance (Dates when employees are present)
id date
1 16/05/2020
2 16/05/2020
1 17/05/2020
2 18/05/2020
3 18/05/2020
Employee
id manager_id
1 2
2 3
3 NA
The desired output should be in this format:
Date manager_id Number_of_absent_employees
16/05/2020 NA 1
17/05/2020 3 1
17/05/2020 NA 1
18/05/2020 2 1
I have tried writing code but partially understood it, intuition being calculating total number of actual employees under each manager and subtracting it from number of employees present on given day. Please help me in completing this query, many thanks!
with t1 as /* for counting total employees under each manager */
(
select employee.manager_id,count(*) as totalc
from employee as e
inner join employee on e.employee_id=employee.employee_id
group by employee.manager_id
)
,t2 as /* for counting total employees present each day */
(
select Attendence.date, employee.manager_id,count(*) as present
from employee
Left join Attendence on employee.employee_id=Attendence.employee_id
group by Attendence.date, employee.manager_id
)
select * from t2
Left join t1 on t2.manager_id=t1.manager_id
order by date
Cross join the distinct dates from Attendance to Employee and left join Attendance to filter out the matching rows.
The remaining rows are the absences so then you need to aggregate:
select d.date, e.manager_id,
count(*) Number_of_absent_employees
from (select distinct date from Attendance) d
cross join Employee e
left join Attendance a on a.date = d.date and a.id = e.id
where a.id is null
group by d.date, e.manager_id
See the demo.
Results:
| date | manager_id | Number_of_absent_employees |
| ---------- | ---------- | -------------------------- |
| 16/05/2020 | NA | 1 |
| 17/05/2020 | 3 | 1 |
| 17/05/2020 | NA | 1 |
| 18/05/2020 | 2 | 1 |
Try this query. In first cte just simplify your code. And in the last query calculate absent employees.
--in this CTE just simplify counting
with t1 as /* for counting total employees under each manager */
(
select employee.manager_id,count(*) as totalc
from employee
group by manager_id
)
,t2 as
(
select Attendence.date, employee.manager_id,count(*) as present
from employee
Left join Attendence on employee.employee_id=Attendence.employee_id
group by Attendence.date, employee.manager_id
)
select t2.date,t2.manager_id, (t1.totalc-t2.present) as employees_absent from t2
Left join t1 on t2.manager_id=t1.manager_id
order by date
Select ec.manager_id, date, (total_employees - employee_attended) as employees_absent from
(Select manager_id, count(id) as total_employees
from employee
group by manager_id) ec,
(Select distinct e.manager_id, a.date, count(a.id) over (partition by e.manager_id, a.date) as employee_attended
from Employee e, attendence, a
where e.id = a.id(+)) ea
where ec.manager_id = ea.manager_id (+)
I guess this should work
I have three tables:
Person
person_id
-------------
10001
10002
10003
10004
Dates
date_type date
-------------- -----------------------
PUBLIC_HOLIDAY 2020-04-10 00:00:00.000
PUBLIC_HOLIDAY 2020-04-13 00:00:00.000
Absence
person_id date absence_type
--------- ----------------------- ------------
10001 2020-04-10 00:00:00.000 HOLIDAY
10001 2020-04-13 00:00:00.000 HOLIDAY
10002 2020-04-10 00:00:00.000 HOLIDAY
10003 2020-04-13 00:00:00.000 HOLIDAY
I need to find all of the person_id's in the Person table and the date's from the Dates table who have not booked any absence matching the following criteria:
Dates.date_type = 'PUBLIC_HOLIDAY'
Absence.absence_type = 'HOLIDAY'
Basically, I need to find the people and the dates which are public holidays they have not booked an absence for as holiday.
You can try this below logic-
DEMO HERE
SELECT Person.person_id,Dates.dat,ISNULL(Absence.dat, 'Not Bokked')
FROM Dates
CROSS JOIN Person
LEFT JOIN Absence ON Person.person_id = Absence.person_id AND Dates.dat = Absence.dat
WHERE Dates.date_type = 'PUBLIC_HOLIDAY'
If you wants only information with not booked, just simply add below line to the script-
AND Absence.dat IS NULL
I think that you want a cross join to generate all combinations of persons and dates, and then not exists to filter on those that do not exist in the absence table:
select p.*, d.*
from person p
cross join dates d
where
d.date_type = 'PUBLIC_HOLIDAY'
and not exists (
select 1
from absence a
where a.person_id = p.person_id and a.date = d.date and a.absence_type = 'HOLIDAY'
)
Try:
select distinct person_id from absence a
where absence_type = 'HOLIDAY'
and not exists (select 1 from dates
where date = a.date
and date_type = 'PUBLIC_HOLIDAY')
union all
select person_id from person p
where not exists ( select 1 from absence
where p.person_id = person_id)
If you want to have them with dates, use below query:
select person_id, date from absence a
where absence_type = 'HOLIDAY'
and not exists (select 1 from dates
where date = a.date
and date_type = 'PUBLIC_HOLIDAY')
union all
-- in person table we don;t have any dates
select person_id, null from person p
where not exists ( select 1 from absence
where p.person_id = person_id)
you can use below query. I have tested this in SQL Server 2014.
CREATE TABLE #Person(person_id INT)
INSERT INTO #person
values
(10001),
(10002),
(10003),
(10004);
CREATE TABLE #Dates (date_Type VARCHAR(50), [datevalue] datetime)
INSERT INTO #Dates
VALUES
('PUBLIC_HOLIDAY','2020-04-10 00:00:00.000'),
('PUBLIC_HOLIDAY','2020-04-13 00:00:00.000');
CREATE TABLE #Absence (person_id int, datevalue datetime, absence_type VARCHAR(50))
INSERT INTO #Absence
VALUES
(10001,'2020-04-10 00:00:00.000','HOLIDAY'),
(10001,'2020-04-13 00:00:00.000','HOLIDAY'),
(10002,'2020-04-10 00:00:00.000','HOLIDAY'),
(10003,'2020-04-13 00:00:00.000','HOLIDAY');
SELECT p.person_id, od.datevalue
FROM #Person AS p
CROSS JOIN (SELECT * FROM #Dates WHERE date_type ='PUBLIC_HOLIDAY') AS od
WHERE NOT EXISTS
(
SELECT 1 FROM
#Absence AS a
INNER JOIN #Dates AS d
ON a.datevalue = d.datevalue
WHERE a.absence_type = 'Holiday' AND d.date_type = 'PUBLIC_HOLIDAY'
AND a.person_id = p.person_id and d.datevalue = od.datevalue)
Below is the resultset:
+-----------+-------------------------+
| person_id | datevalue |
+-----------+-------------------------+
| 10003 | 2020-04-10 00:00:00.000 |
| 10004 | 2020-04-10 00:00:00.000 |
| 10002 | 2020-04-13 00:00:00.000 |
| 10004 | 2020-04-13 00:00:00.000 |
+-----------+-------------------------+
The context: a health care clinic has 2 tables: one on patient visits, and one on patient appointments. They are not 1:1; it is possible to have an appointment without a visit, or to have a visit without an appointment. The identifier for both tables is the encounter ID, enc_ID. I'm trying to outer join these tables together and to pull in patient names.
A boiled-down example of the visit table (V):
enc_ID Visit_date Patient_ID
1 2018-06-01 10
2 2018-06-02 11
And the appointment data (A):
enc_ID Appointment_time Patient_ID
1 2018-06-01 13:00 10
3 2018-06-03 14:00 12
Outer-joining these on visit_ID would produce something like:
enc_ID V.Visit_date A.Appointment_time V.Patient_ID A.Patient_ID
1 2018-06-01 2018-06-01 13:00 10 10
2 2018-06-02 NULL 11 NULL
3 NULL 2018-06-03 14:00 NULL 12
Say I want to basically combine V.Patient_ID and A.Patient_ID, and pull in patient name from another table (P), joined on Patient_ID. The desired output:
enc_ID V.Visit_date A.Appointment_time Patient_ID Patient_Name
1 2018-06-01 2018-06-01 13:00 10 Patient A
2 2018-06-02 NULL 11 Patient B
3 NULL 2018-06-03 14:00 13 Patient C
How might this be accomplished? I'm probably missing something obvious, but I don't see how I can join in P.Patient_Name without having to join it to either V.Patient_ID or A.Patient_ID, either of which would result in null patient names.
Thanks in advance!
As long as enc_ID in both tables are not nullable, you can combine those fields with a COALESCE:
SELECT
COALESCE(V.Patient_ID, A.Patient_ID) AS enc_ID,
V.Visit_date,
A.Appointment_time,
COALESCE(V.Patient_ID, A.Patient_ID) AS Patient_ID,
P.Patient_Name
FROM V
FULL JOIN A ON V.enc_ID = A.enc_ID
INNER JOIN P
ON P.Patient_ID = V.Patient_ID
OR P.Patient_ID = A.Patient_ID
-- ON P.Patient_ID = COALESCE(V.Patient_ID, A.Patient_ID) also works
You can use ISNULL to join table P.
...(Your outer join query)
LEFT JOIN P
ON P.Patient_ID = ISNULL(V.Patient_ID, A.PatientID)