Query that gets students with their courses in same row - sql

I need a query that will get students whom get Course1, Course2 and Course3. I can query it like that:
SELECT k.name as firstname, k.surname as lastname, k.Email
FROM Students k
JOIN StudentCourses dn ON dn.StudentID = k.StudentID
WHERE dn.CourseID IN
(SELECT CourseID FROM Courses WHERE CourseName IN ('Course1','Course2','Course3'))
But i need a result set like that: Name, Surname, Email, Course1, Course2, Course3. No multiple rows for a student, one row and write course name if students get that course.
In fact i can imagine how to write that query (with subselects), wonder about better alternatives.

You can use the PIVOT table operator to do this, something like this:
WITH CTE
AS
(
SELECT
k.name as firstname,
k.surname as lastname,
k.Email,
c.CourseName, c.CourseID
FROM Students k
JOIN StudentCourses dn ON dn.StudentID = k.StudentID
INNER JOIN Courses c ON dn.CourseID = c.CourseID
WHERE c.CourseName IN ('Course1','Course2','Course3')
)
SELECT *
FROM CTE AS c
PIVOT
(
MAX(CourseID)
FOR CourseName IN ([Course1], [Course2], [Course3])
) u;
SQL Fiddle Demo
Note that, Since you don't have other columns to display under each column of course for each students, this query will display CourseID value for each course name, null if the student doesn't have this course. You should choose to display more appropriate column instead, like mark for example:
WITH CTE
AS
(
SELECT
k.name as firstname,
k.surname as lastname,
k.Email,
c.CourseName, dn.Mark
FROM Students k
JOIN StudentCourses dn ON dn.StudentID = k.StudentID
INNER JOIN Courses c ON dn.CourseID = c.CourseID
WHERE c.CourseName IN ('Course1','Course2','Course3')
)
SELECT *
FROM CTE AS c
PIVOT
(
MAX(Mark)
FOR CourseName IN ([Course1], [Course2], [Course3])
) u;
Updated SQL Fiddle Demo
This will give you:
| firstname | lastname | Email | Course1 | Course2 | Course3 |
|-----------|----------|-------|---------|---------|---------|
| StudentA | test | test | 19 | 20 | 15 |
| StudentB | test | test | 16 | 17 | 20 |
| StudentC | test | test | 20 | 19 | 15 |
Also note that, you should be careful about the columns you choose in the anchor query:
...
SELECT
k.name as firstname,
k.surname as lastname,
k.Email,
c.CourseName, dn.Mark
...
Because the PIVOT table operator will group by all the columns except the columns you listed and used to pivot:
MAX(CourseID)
FOR CourseName IN
So in your case, it will group by firstname, lastname, Email.

I think you can use a better query than yours and use MAX(CASE ...) to achieve your expected result like this:
SELECT k.name as firstname, k.surname as lastname, k.Email,
MAX(CASE WHEN c.CourseName = 'Course1' THEN 'Yes' ELSE 'No' END) As Course1,
MAX(CASE WHEN c.CourseName = 'Course2' THEN 'Yes' ELSE 'No' END) As Course2,
MAX(CASE WHEN c.CourseName = 'Course3' THEN 'Yes' ELSE 'No' END) As Course3
FROM Students k
JOIN StudentCourses dn ON dn.StudentID = k.StudentID
JOIN Courses c ON c.CourseID = dn.CourseID AND c.CourseName Like 'Course[1-3]'
GROUP BY k.name, k.surname, k.Email;

Related

Need SQL query to export students attendance report by classes

I have 3 tables:
students table
id
name
1
Jhon
2
Emma
3
Oliver
classes table
id
name
1
Math
2
Science
attendance table
id
student_id
class_id
1
1
1
2
1
2
3
2
1
I tried to write an SQL query to retrieve the students who attended both math and science classes:
SELECT s.id, s.name
FROM attendance a
INNER JOIN students s ON a.student_id = s.id
WHERE a.class_id IN (1,2);
the above code result is
id
name
1
Jhon
2
Emma
1
Jhon
But Emma only attended Math class.
I know this behavior is because of WHERE IN, it's the same as WHERE a.class_id = 1 OR a.class_id = 2.
But what should I do to get the expected results, Which is "Jhon" or only Jhon's ID because he is the only one who attended Math and Science Classes?
Either join to attendance twice, once for each class:
SELECT s.id, s.name
FROM students s
JOIN attendance a1 ON a1.student_id = s.id and a1.class_id = 1
JOIN attendance a2 ON a2.student_id = s.id and a2.class_id = 2
or join to both classes at once and use group by with having:
SELECT s.id, s.name
FROM students s
JOIN attendance a ON a.student_id = s.id and class_id in (1, 2)
GROUP BY s.id, s.name
HAVING COUNT(*) = 2
Using a CTE would help, and can help you extend to multiple classes. You are also missing a DISTINCT keyword, which is why Jhon appears twice. Maybe something like this:
WITH attendance_count(student_id, classes_attended)
AS (
SELECT
student_id,
COUNT(id) AS classes_attended
FROM attendance a
-- change this to change the classes
WHERE a.class_id IN (1,2)
GROUP BY student_id
)
SELECT DISTINCT s.id, s.name
FROM attendance_count
INNER JOIN students s ON attendance_count.student_id = s.id
-- if different number of classes, change this
WHERE classes_attended = 2

How to get the count of a particular category for each year?

I am trying out a problem which states me to find " For each year, count the number of movies in that year that had only female actors".
Table schema is as follows:
-------------------- ----------------------- ----------------------
| Movie | | Person | | Cast |
-------------------- ------------------------ ----------------------
| MovieID | year | | PersonID | Gender | | MovieID | PersonID |
-------------------- ------------------------ ----------------------
Running the following query:
SELECT M.YEAR, COUNT(M.MID) NUMBER_OF_FEMALE_ONLY_MOVIES FROM MOVIE M
WHERE M.MID IN (SELECT X.MID FROM (SELECT AX.MID, COUNT(AX.PID) TOTAL_CAST
FROM M_CAST AX GROUP BY AX.MID) X
WHERE
X.TOTAL_CAST = (SELECT COUNT(A.PID) FROM M_CAST A, PERSON B WHERE A.MID =
X.MID AND
TRIM(B.PID) = TRIM(A.PID) AND B.GENDER = 'Female')) GROUP BY M.YEAR
My results are :
---------------------------------------
| year | NUMBER_OF_FEMALE_ONLY_MOVIES |
---------------------------------------
| 1999 | 1 |
| 2005 | 1 |
| 2009 | 1 |
| 2012 | 1 |
| 2018 | 1 |
----------------------------------------
But I need to return 0 as count for the years which do not have any such movies.
Eg.
2013 0
WITH
PERSON_CAST_MERGE AS
(
SELECT P.PID,C.MID,GENDER
FROM PERSON P
INNER JOIN M_CAST C ON C.PID = P.PID
),
MALE_COUNT AS
(
SELECT F.MID FROM PERSON_CAST_MERGE F
WHERE TRIM(F.GENDER) NOT LIKE "%FEMALE%"
),
FEMALE_COUNT AS
(
SELECT F.MID FROM PERSON_CAST_MERGE F
WHERE TRIM(F.GENDER) LIKE "%FEMALE%"
),
ONLY_FEMALE AS
(
SELECT F.MID FROM FEMALE_COUNT F
WHERE F.MID NOT IN (SELECT M.MID FROM MALE_COUNT M)
),
TEST AS
(
SELECT M.YEAR,COUNT(M.MID) AS NO_OF_MOVIES
FROM ONLY_FEMALE F
INNER JOIN MOVIE M ON M.MID = F.MID
GROUP BY M.YEAR
)
SELECT M.YEAR,
CASE
WHEN M.YEAR IN (SELECT F.YEAR FROM TEST F) THEN
(SELECT F.NO_OF_MOVIES FROM TEST F WHERE F.YEAR = M.YEAR)
WHEN M.YEAR <> (SELECT F.YEAR FROM TEST F) THEN
0
END
AS NO_OF_MOVIES
FROM MOVIE M
GROUP BY M.YEAR
I'd suggest exploring the data within the CTE to get a better understanding.
First CTE (all_cast): Return the entire movie cast
Second CTE (male_present): Return movie id's from all_cast where there exists male actors.
Result: Return movies from all_cast where movie id is not present in male_present
WITH all_cast AS (
SELECT SUBSTR(m."year",-4) as 'Year', m.title, trim(m.MID) as MID, p.Name, trim(p.Gender) as Gender
FROM Movie m
JOIN M_Cast mc
ON m.MID = mc.MID
JOIN Person p
ON trim(mc.PID) = p.PID
),
male_present AS (
SELECT year, mid, name
FROM all_cast
WHERE Gender = 'Male'
)
SELECT year, COUNT(DISTINCT mid) as 'All Female Cast'
FROM all_cast a
WHERE NOT EXISTS (SELECT * FROM male_present WHERE a.mid = mid)
GROUP BY year
You need only the group by with subquery as you require reference to the movieids of personids with gender as female in person
SELECT YEAR, COUNT(*) FROM
MOVIE
Where MovieId IN (SELECT MOVIEId
from CAST WHERE PERSONID IN
(Select PersonId from Person Where
Gender ='FEMALE'))
Group by Year
Try this- A DISTINCT MovieID is required as there may have multiple Female casting for a single movie. Distinct will provide the actual count of movies.
SELECT
M.Year,
COUNT(DISTINCT MovieID)
FROM Movie M
INNER JOIN Cast C ON M.MovieID = C.MovieID
INNER JOIN Person P ON C.PersonID = P.PersonID
WHERE P.Gender = 'Female'
GROUP BY M.Year;
I think the problem can be solved by joining all tables and filtering on WHERE clause for female actors. In this case joining tables will also give better performance rather than sub-querying.
Please try the following code:
Select year, count(*)
from movie
join Cast on movie.movieid = cast.movieid
join person on person.personid = cast.personid
where person.gender = 'Female'
group by year
Please let me know if that works fine for you.
By merging your query with the Movie table using the outer left join, you can get the desired results. The time taken will be very low compared to the answer posted by #Lucky
WITH FEMALE_ONLY AS
(SELECT M.YEAR,
COUNT(M.MID) COUNT_ALL_FEMALE
FROM MOVIE M
WHERE M.MID IN
(SELECT Q.MID
FROM
(SELECT MC.MID,
COUNT(MC.PID) total
FROM M_CAST MC
GROUP BY MC.MID) Q
WHERE Q.total =
(SELECT COUNT(A.PID)
FROM M_CAST A,
PERSON B
WHERE A.MID = Q.MID
AND TRIM(B.PID) = TRIM(A.PID)
AND B.Gender = 'Female'))
GROUP BY M.YEAR)
SELECT DISTINCT M.year,
coalesce(FO.COUNT_ALL_FEMALE, 0) FEMALE_ONLY_MOVIES
FROM Movie M
LEFT OUTER JOIN FEMALE_ONLY FO ON M.year = FO.year
ORDER BY M.year;
You can do like this
select z.year, count(*)
from Movie z
where not exists (select *
from Person x, M_Cast xy
where x.PID = xy.PID and xy.MID = z.MID and x.gender!='Female')
group by z.year;

Single SQL SELECT query for one to many relationship

This question is hard to explain without a example.
I have 2 tables, companies and employees, they have one-to-many relationship, one Company can attend many Employees.
Simplest table structure is shown here
Company
| id | name |
Employee
| id | name | company_id | join_date |
Now the question is:
How could I select the first 2 employees and show their joining date as column in company table?
So the result looks like this
| id | company_name | first_employee_join_at | second_employee_join_at |
Assuming there is a foreign key column company_id in the employee table:
with emps as (
select id, name, company_id,
row_number() over (partition by company_id order by join_date) as rn
from employee
)
select c.id, c.name as company_name,
e1.join_date as first_employee_join_at,
e2.join_date as second_employee_join_at
from company c
left join emps e1 on e1.company_id = c.id and e1.rn = 1
left join emps e2 on e2.company_id = c.id and e2.rn = 2;
This is not going to be terribly efficient though. A slightly more efficient version would use conditional aggregation:
with emps as (
select id, name, company_id,
row_number() over (partition by company_id order by join_date) as rn
from employee
)
select c.id, c.name as company_name,
max(e.join_date) filter (where rn = 1) as first_employee_join_at,
max(e.join_date) filter (where rn = 2) as second_employee_join_at
from company c
join emps e on e.company_id = c.id and e.rn in (1,2)
group by c.id, c.name;

How to use UNION with COUNT

I have this table structure:
TABLE: PERSON TABLE: CAR
PersonID PersonID | CarID
------ ---------|---------
1 1 | 51
1 | 52
TABLE: PET TABLE: AGE
PersonID | PetID Person | AgeID
---------|---- -------|----
1 | 81 1 | 20
1 | 82
1 | 81
One person can have many cars and pets, but only one age.
I want to count the number of cars someone has, count the number of pets someone has, and list their age.
This is what I have so far:
select
car.personid as person,
count(car.carid) as cars,
null as pets
from car
where car.personid = 1
group by car.personid
union all
select
pet.personid as person,
null as cars,
count(pet.petid) as pets
from pet
where pet.personid = 1
group by pet.personid
This produces:
Person | Cars | Pets
-------|------|-----
1 | 2 | null
1 | null | 3
But I'd like the results to look like this:
Person | Cars | Pets | Age
-------|------|------|----
1 | 2 | 3 | 20
There's a fiddle here: http://sqlfiddle.com/#!3/f584a/1/0
I'm completely stuck on how to bring the records into one row and add the age column.
SQL Fiddle
Query 1:
SELECT p.PersonID,
( SELECT COUNT(1) FROM CAR c WHERE c.PersonID = p.PersonID ) AS Cars,
( SELECT COUNT(1) FROM PET t WHERE t.PersonID = p.PersonID ) AS Pets,
a.AgeID AS Age
FROM PERSON p
LEFT OUTER JOIN
AGE a
ON ( p.PersonID = a.PersonID )
Results:
| PersonID | Cars | Pets | Age |
|----------|------|------|-----|
| 1 | 2 | 3 | 20 |
Query 2:
WITH numberOfPets AS (
SELECT PersonID,
COUNT(1) AS numberOfPets
FROM PET
GROUP BY PersonID
),
numberOfCars AS (
SELECT PersonID,
COUNT(1) AS numberOfCars
FROM CAR
GROUP BY PersonID
)
SELECT p.PersonID,
COALESCE( numberOfCars, 0 ) AS Cars,
COALESCE( numberOfPets, 0 ) AS Pets,
AgeID AS Age
FROM PERSON p
LEFT OUTER JOIN AGE a ON ( p.PersonID = a.PersonID )
LEFT OUTER JOIN numberOfPets t ON ( p.PersonID = t.PersonID )
LEFT OUTER JOIN numberOfCars c ON ( p.PersonID = c.PersonID )
Results:
| PersonID | Cars | Pets | Age |
|----------|------|------|-----|
| 1 | 2 | 3 | 20 |
Should work with duplicate Petid or duplicate carid
SqlFiddle Demo
WITH person_cte
AS (SELECT *
FROM person),
car_count
AS (SELECT Count(1) AS car,
p.personid
FROM person_cte p
LEFT OUTER JOIN car c
ON p.personid = c.personid
GROUP BY p.personid),
pet_count
AS (SELECT Count(1) AS Pet,
p.personid
FROM person_cte p
LEFT OUTER JOIN pet c
ON p.personid = c.personid
GROUP BY p.personid)
SELECT c.personid,
c.car,
p.pet,
a.ageid
FROM car_count c
INNER JOIN age a
ON c.personid = a.personid
INNER JOIN pet_count p
ON p.personid = c.personid;
If there wont be any duplicates in Carid or Petid then use this
SqlFiddle Demo
SELECT p.personid,
a.ageid,
Count(DISTINCT carid) as carid,
Count(DISTINCT petid) as petid
FROM person p
INNER JOIN age a
ON p.personid = a.personid
LEFT OUTER JOIN car c
ON p.personid = c.personid
LEFT OUTER JOIN pet pe
ON p.personid = pe.personid
GROUP BY p.personid,
a.ageid
One issue I see with most of these responses is that they will only include people who own a car. What if the person doesn't have a vehicle, but has pets? What if they haven't entered their age, yet? You'd lose that metric.
Tie the person table into this as the main requirement. To get the rest of the numbers you could take various approcahes, such as a simple series of left outer joins on the other tables and count their result.
Also note that tagging "ID" at the end of values is a misnomer and considered bad design practice. If it's an age, just call it "age" or "age_value", but not "AgeID". I would also suggest denormalizing your AGE and PERSON tables and make Age (not AgeID) a nullable field.
E.G.
SELECT
PERSON.PersonID,
AgeID AS Age,
CarCount,
PetCount
FROM
#PERSON AS PERSON
LEFT OUTER JOIN AGE AS AGE
ON AGE.PersonID = PERSON.PersonID
LEFT OUTER JOIN
( SELECT PersonID, COUNT( 1 ) AS CarCount FROM CAR GROUP BY PersonID ) AS CAR
ON CAR.PersonID = PERSON.PersonID
LEFT OUTER JOIN
( SELECT PersonID, COUNT( 1 ) AS PetCount FROM PET GROUP BY PersonID ) AS PET
ON PET.PersonID = PERSON.PersonID
You need to be joining on single values, so do your counts within subqueries
select c.PersonID,a.CarID,b.PetID,c.AgeID from (
select person.PersonID, COUNT(car.CarID) as CarID
from Person INNER JOIN Car on Person.PersonID = Car.PersonID
group by Person.PersonID) a
inner join (
select person.PersonID, COUNT(Pet.PetID) as PetID
from Person INNER JOIN Pet on Person.PersonID = Pet.PersonID
group by Person.PersonID) b
on a.PersonID = b.PersonID
inner join (select PersonID,AgeID from Age) c
on a.PersonID = c.PersonID
Another method is
select person,
sum(cars) as cars,
sum(pets) as pets
from
(
select
car.personid as person,
count(car.carid) as cars,
null as pets
from car
where car.personid = 1
group by car.personid
union all
select
pet.personid as person,
null as cars,
count(pet.petid) as pets
from pet
where pet.personid = 1
group by pet.personid
) as t
group by person
Do you want to count distinct number of cars/pets? If so, add a distinct within the count.
select
person.personid as person,
count(car.carid) as cars,
count(pet.petid) as pets
age.ageID
from person
left outer join pet on pet.personid = person.personid
left outer join car on car.personid = person.personid
left outer join age on age.personid = person.personid
where car.personid = 1
group by car.personid, age.ageID;

incrementally updating SQL rows

I'm currently dealing with two tables.
One table contains a set of columns like ID, NAME, AGE, TEAM, SCHOOL, and so forth in a table called PRIMARY_TABLE
And I also have an audit table called SECONDARY_TABLE that records updates in the aforementioned values over time.
I have ATTRIBUTE, PREV_VALUES and RECORD_ID columns in this table. It has the following attributes:
the RECORD_ID column corresponds to the ID column of PRIMARY_TABLE
the ATTRIBUTE column will store the column of the PRIMARY_TABLE that is being altered.
For example, if I have
132 NIKO 18 LANCERS JESUIT
143 KEENAN 25 RAIDERS ROCKLAND
in my first table and
132 'AGE' 22
132 'NAME' STEVAN
in my second,
then I want a combined table that has
132 NIKO 18 LANCERS JESUIT
132 NIKO 22 LANCERS JESUIT
132 STEVAN 22 LANCERS JESUIT
143 KEENAN 25 RAIDERS ROCKLAND .
The issue I have a hard time getting around is preserving the values in the unaffected rows. It seems like any idea I have for joining the two tables together won't work for this reason.
Any thoughts? I think the only solution is to create a stored procedure for this. If you need clarification, let me know as well.
EDIT
One more thing...
Here's another thing. The audit table also has a "time_of_change" column. If multiple rows have the same time of change for an ID, then instead of having multiple rows in our resulting table, there should be only one more row.
For example, if our audit table had
132 'AGE' 22 1:00
132 'NAME' STEVAN 1:00
Then instead of having
132 STEVAN 18 LANCERS JESUIT
132 NIKO 22 LANCERS JESUIT
added, there should only be one added row of
132 STEVAN 22 LANCERS JESUIT.
I can't think of any possible way to do this either.
UPDATE2 If you were to have a column with a datetime of an update in the secondary_table (lets call it updated_at) then you can order the resultset appropriately.
SELECT id, name, age, team, school, GETDATE() updated_at
FROM primary_table
UNION ALL
SELECT p.id,
CASE WHEN s.attribute = 'NAME'
THEN s.prev_values ELSE p.name END name,
CASE WHEN s.attribute = 'AGE'
THEN s.prev_values ELSE p.age END age,
CASE WHEN s.attribute = 'TEAM'
THEN s.prev_values ELSE p.team END team,
CASE WHEN s.attribute = 'SCHOOL'
THEN s.prev_values ELSE p.school END school,
updated_at
FROM primary_table p JOIN secondary_table s
ON p.ID = s.record_id
ORDER BY id, updated_at DESC
Here is SQLFiddle demo
UPDATE1 A version with one UNION and conditional output with CASE
SELECT *
FROM primary_table
UNION ALL
SELECT p.id,
CASE WHEN s.attribute = 'NAME'
THEN s.prev_values ELSE p.name END name,
CASE WHEN s.attribute = 'AGE'
THEN s.prev_values ELSE p.age END age,
CASE WHEN s.attribute = 'TEAM'
THEN s.prev_values ELSE p.team END team,
CASE WHEN s.attribute = 'SCHOOL'
THEN s.prev_values ELSE p.school END school
FROM primary_table p JOIN secondary_table s
ON p.ID = s.record_id
ORDER BY id
Here is SQLFiddle demo
Original version with UNIONs
SELECT *
FROM primary_table
UNION ALL
SELECT p.id, s.prev_values, p.age, p.team, p.school
FROM primary_table p JOIN secondary_table s
ON p.ID = s.record_id
AND s.attribute = 'NAME'
UNION ALL
SELECT p.id, p.name, s.prev_values, p.team, p.school
FROM primary_table p JOIN secondary_table s
ON p.ID = s.record_id
AND s.attribute = 'AGE'
ORDER BY id
Output:
| ID | NAME | AGE | TEAM | SCHOOL |
-------------------------------------------
| 132 | NIKO | 18 | LANCERS | JESUIT |
| 132 | STEVAN | 18 | LANCERS | JESUIT |
| 132 | NIKO | 22 | LANCERS | JESUIT |
| 143 | KEENAN | 25 | RAIDERS | ROCKLAND |
Here is SQLFiddle demo
This is not perfect but try this one:
SELECT * FROM Primary_Table
UNION ALL
SELECT * FROM
(SELECT A.ID, A.Name, B.GetAge as Age, A.School
FROM Primary_Table A INNER JOIN (SELECT Record_id, CONVERT(int,Value) as GetAge FROM Secondary_Table
WHERE Attrib='AGE' ) B
ON A.ID = B.Record_id) UAge
UNION ALL
SELECT * FROM
(SELECT A.ID, B.GetName as Name, A.Age, A.School
FROM Primary_Table A INNER JOIN (SELECT Record_id, Value as GetName FROM Secondary_Table
WHERE Attrib='NAME' ) B
ON A.ID = B.Record_id) UName
ORDER BY ID
Here's some script to get you started. You have a big job ahead of you. Things are made quite a bit more difficult if you don't have the current values in the audit table (which it seems like, so I built my script with that assumption).
WITH Curr AS (
SELECT
Record_ID = T.ID,
V.*,
Time_Of_Change = Convert(datetime, 0)
FROM
dbo.Team T
CROSS APPLY (VALUES
('Name', Convert(varchar(20), T.Name)),
('Age', Convert(varchar(20), T.Age)),
('Team', Convert(varchar(20), T.Team)),
('School', Convert(varchar(20), T.School))
) V (Attribute, Prev_Values)
),
Data AS (
SELECT
H.Record_ID,
H.Time_Of_Change,
C.Attribute,
A.Prev_Values
FROM
(
SELECT DISTINCT Record_ID, Time_Of_Change
FROM dbo.Audit
WHERE TableName = 'Team'
UNION ALL
SELECT DISTINCT ID, GetDate()
FROM dbo.Team
) H
CROSS JOIN (VALUES
('Name'), ('Age'), ('Team'), ('School')
) C (Attribute)
CROSS APPLY (
SELECT TOP 1 *
FROM (
SELECT Record_ID, Attribute, Prev_Values, Time_Of_Change
FROM dbo.Audit
WHERE TableName = 'Team'
UNION ALL
SELECT *
FROM Curr
) A
WHERE
H.Record_ID = A.Record_ID
AND H.Time_Of_Change >= A.Time_Of_Change
AND C.Attribute = A.Attribute
ORDER BY
A.Time_Of_Change DESC
) A
)
SELECT *
FROM
Data
PIVOT (Max(Prev_Values) FOR Attribute IN (Name, Age, Team, School)) P
;
See a Live Demo at SQL Fiddle
You can very easily create a view for each table you want to reconstruct the history of. Build a stored procedure that queries the INFORMATION_SCHEMA.COLUMNS view and builds something similar to what I've given you. Run the SP once after any table change, and it will update the views.
I noticed that my script is not quite right--it's showing 2 rows instead of 3 for the edits. Plus, there's no valid time for the rows for which there are no audit rows. But that part makes sense. In any case, you have a big job ahead of you...