How to link database rows to and fro? - sql

I have a query on database table structure.
My Patient Details table in a SQL Server 2008 database looks like this:
--PatientId(PK)-- --PatientType-- --DoctorId(FK)-- --DateOfVisit-- --PrescriptionId(FK)--
Each patient may visit the hospital multiple times. I need to link all the visits of a patient and show them in a linked way that the admin can navigate with the previous and next visits..
So I think I can do it only by having a record for each visit with a VisitId.
I need to be Careful that the database should not be loaded with unnecessary fields. Also it should not affect the fetching time badly.
I think these are the available ways. Suggest me if any available.
--PatientId(PK)-- --PatientType-- --DoctorId(FK)-- --DateOfVisit-- --PrescriptionId(FK)-- --VisitId-- --PrevVisitId-- NextVisitId--
or
having a separate table for Visit as
--VisitId(PK)-- -PrevVisitId(FK)-- --NextVisitId(FK)--
Apologies if my post is duplicate.. Correct me, redirect me wherever/whenever needed.

So I think I can do it only by having a record for each visit with a
VisitId.
You need a row for each visit. You need a key for each row. The key doesn't need to be a single id number. I think the very least you need to record for each visit is
create table visits (
patient_id integer not null, -- references patients (patient_id), not shown
doctor_id integer not null, -- references doctors (doctor_id), not shown
office_visit_start timestamp not null default current_timestamp,
primary key (patient_id, office_visit_start)
);
insert into visits values (1, 1, '2012-02-01 08:00');
insert into visits values (1, 1, '2012-02-01 15:00');
insert into visits values (1, 1, '2012-03-01 09:33');
insert into visits values (2, 1, '2012-02-01 09:00');
(Exact syntax varies depending on your dbms platform.) You can find the previous and next visits by fairly simple queries--"office_visit_start" will give you the order of a patient's visits.
Example queries . . .
-- Previous visit for patient # 1 (before 2012-02-01 15:00)
select patient_id, max(office_visit_start)
from visits
where patient_id = 1
and office_visit_start < '2012-02-01 15:00'
group by patient_id
-- Next visit for patient # 1 (after 2012-02-01 15:00)
select patient_id, min(office_visit_start)
from visits
where patient_id = 1
and office_visit_start > '2012-02-01 15:00'
group by patient_id

Related

Inserting multiple records in database table using PK from another table

I have DB2 table "organization" which holds organizations data including the following columns
organization_id (PK), name, description
Some organizations are deleted so lot of "organization_id" (i.e. rows) doesn't exist anymore so it is not continuous like 1,2,3,4,5... but more like 1, 2, 5, 7, 11,12,21....
Then there is another table "title" with some other data, and there is organization_id from organization table in it as FK.
Now there is some data which I have to insert for all organizations, some title it is going to be shown for all of them in web app.
In total there is approximately 3000 records to be added.
If I would do it one by one it would look like this:
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
VALUES
(
'This is new title',
XXXX,
CURRENT TIMESTAMP,
1,
1,
1
);
where XXXX represent "organization_id" which I should get from table "organization" so that insert do it only for existing organization_id.
So only "organization_id" is changing matching to "organization_id" from table "organization".
What would be best way to do it?
I checked several similar qustions but none of them seems to be equal to this?
SQL Server 2008 Insert with WHILE LOOP
While loop answer interates over continuous IDs, other answer also assumes that ID is autoincremented.
Same here:
How to use a SQL for loop to insert rows into database?
Not sure about this one (as question itself is not quite clear)
Inserting a multiple records in a table with while loop
Any advice on this? How should I do it?
If you seriously want a row for every organization record in Title with the exact same data something like this should work:
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
SELECT
'This is new title' as name,
o.organization_id,
CURRENT TIMESTAMP as datetime_added,
1 as added_by,
1 as special_fl,
1 as title_type_id
FROM
organizations o
;
you shouldn't need the column aliases in the select but I am including for readability and good measure.
https://www.ibm.com/support/knowledgecenter/ssw_i5_54/sqlp/rbafymultrow.htm
and for good measure in case you process errors out or whatever... you can also do something like this to only insert a record in title if that organization_id and title does not exist.
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
SELECT
'This is new title' as name,
o.organization_id,
CURRENT TIMESTAMP as datetime_added,
1 as added_by,
1 as special_fl,
1 as title_type_id
FROM
organizations o
LEFT JOIN Title t
ON o.organization_id = t.organization_id
AND t.name = 'This is new title'
WHERE
t.organization_id IS NULL
;

Exclude a duplicate record from a view based on condtions and keep 1 - SQL Server 2012

I have three joined tables Student, StudentTransportOrder and Transport in SQL Server 2012.
I have created a StudentActivity view for this.
In the StudentTransportOrder table, each student record has a unique TransportOrderID.
When transport is ordered, VehicleID this is recorded into the StudentTransportOrder table.
Unfortunately the same VehicleID has been entered for the same date and time for a student records.
The StudentActivity view already returns records based on where conditions, but I also need to remove the duplicate records, preferably keep the records where if a student has used transport on the same date and time, that only one distinct VehicleID is returned and preferably the where the transport type is TransportVehicle = Car
How can I amend the view , without deleting records from the main tables, also bring back records even if no transport has been ordered
Please help
Append
ROW_NUMBER() OVER(PARTITION BY StudentID,VehicleID ORDER BY (SELECT NULL)) AS PartitionedCount
... to your query and use a filter like WHERE PartitionedCount=1.
This will number your rows for each combination of Student and vehicle separately (you'll have to include the date or datetime probably).
You can use the (ORDER BY (SELECT NULL)) to force a car sorted on top...
UPDATE
As I do not know your actual tables here's a general example:
DECLARE #tbl TABLE(ID INT IDENTITY,GroupID INT,SomeValue VARCHAR(100));
INSERT INTO #tbl(GroupID,SomeValue) VALUES
(1,'val 1 for 1')
,(1,'val 2 for 1')
,(1,'val 3 for 1')
,(2,'val 1 for 2')
,(2,'val 2 for 2')
,(3,'val 1 for 3')
,(3,'val 2 for 3');
WITH Numbered AS
(
SELECT tbl.*
,ROW_NUMBER() OVER(PARTITION BY GroupID ORDER BY ID /*DESC*/) AS Number
FROM #tbl AS tbl
)
SELECT * FROM Numbered
WHERE Number=1
Try first without the WHERE. You will get all rows with partitioned numbering. Using the WHERE will reduce the output to the one with number =1.
It is on you to find a clever ORDER BY which brings your cars to the top (CASE will help you probably).
Just good to know: In my ORDER BY you find an inactive DESC. If you take away the /**/ you can use the same approach to find the last row to a given sort order.

PostgreSQL: Get last updates by joining 2 tables

I have 2 tables that I need to join to get the last/latest update in the 2nd table based on valid rows in the 1st table.
Code below is en example.
Table 1: Registered users
This table contains a list of users registered in the system.
When a user gets registered it gets added into this table. A user is registered with a name, and a registration time.
A user can get de-registered from the system. When this is done, the de-registration column gets updated to the time that the user was removed. If this value is NULL, it means that the user is still registered.
CREATE TABLE users (
entry_idx SERIAL PRIMARY KEY,
name TEXT NOT NULL,
reg_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
dereg_time TIMESTAMP WITH TIME ZONE DEFAULT NULL
);
Table 2: User updates
This table contains updates on the users. Each time a user changes a property (example position) the change gets stored in this table. No updates must be removed since there is a requirement to keep history in the table.
CREATE TABLE user_updates (
entry_idx SERIAL PRIMARY KEY,
name TEXT NOT NULL,
position INTEGER NOT NULL,
time TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Required output
So given the above information, I need to get a new table that contains only the last update for the current registered users.
Test Data
The following data can be used as test data for the above tables:
-- Register 3 users
INSERT INTO users(name) VALUES ('Person1');
INSERT INTO users(name) VALUES ('Person2');
INSERT INTO users(name) VALUES ('Person3');
-- Add some updates for all users
INSERT INTO user_updates(name, position) VALUES ('Person1', 0);
INSERT INTO user_updates(name, position) VALUES ('Person1', 1);
INSERT INTO user_updates(name, position) VALUES ('Person1', 2);
INSERT INTO user_updates(name, position) VALUES ('Person2', 1);
INSERT INTO user_updates(name, position) VALUES ('Person3', 1);
-- Unregister the 2nd user
UPDATE users SET dereg_time = NOW() WHERE name = 'Person2';
From the above, I want the last updates for Person 1 and Person 3.
Failed attempt
I have tried using joins and other methods but the results are not what I am looking for. The question is almost the same as one asked here. I have used the solution in answer 1 and it does give the correct answer, but it takes too long to get too the answer in my system.
Based on the above link I have created the following query that 'works':
SELECT
t1.*
, t2.*
FROM
users t1
JOIN (
SELECT
t.*,
row_number()
OVER (
PARTITION BY
t.name
ORDER BY t.entry_idx DESC
) rn
FROM user_updates t
) t2
ON
t1.name = t2.name
AND
t2.rn = 1
WHERE
t1.dereg_time IS NULL;
Problem
The problem with the above query is that it takes very long to complete. Table 1 contains a small list of users, while table 2 contains a huge amount of updates. I think that the query might be inefficient in the way that it handles the 2 tables (based on my limited understanding of the query). From pgAdmin's explain it does a lot of sorting and aggregation on the updates 1st before joining with the registered table.
Question
How can I formulate a query to efficiently and fast get the latest updates for registered users?
PostgreSQL have a special distinct on syntax for such type of queries:
select distinct on(t1.name)
--it's better to specify columns explicitly, * just for example
t1.*, t2.*
from users as t1
left outer join user_updates as t2 on t2.name = t1.name
where t1.dereg_time is null
order by t1.name, t2.entry_idx desc
sql fiddle demo
you can try it, but for me your query should work fine too.
I am using q1 to get the last update of each user. Then joining with users to remove entries that have been deregistered. Then joining with q2 to get rest of user_update fields.
select users.*,q2.* from users
join
(select name,max(time) t from user_updates group by name) q1
on users.name=q1.name
join user_updates q2 on q1.t=q2.time and q1.name=q2.name
where
users.dereg_time is null
(I haven't tested it. have edited some things)

How to implement Foursquare's "Mayor" feature - find the user with the highest score in the last N days?

In Foursquare, the user who has the highest score for a place in the last N days is awarded the Mayorship of that place.
What is the most efficient way to implement that?
A user could have checked into hundreds of places. To display all the mayorships that belong to a user, it'd be necessary to go through all those hundreds of places one by one and check if he has the highest score in the last 60 days for each place-- that sounds very inefficient.
Is there any SQL or algorithmic magic that could perform the task quickly?
UPDATE: I'm using MySQL and Django
I would keep the 'current major' in the place table, and update that from time to time. Example (I have no idea if the data model is correct):
drop table place;
create table place(name varchar(20) primary key, major varchar(20));
insert into place values('NY', null), ('LA', null);
create index idx_p_m on place(major);
drop table visits;
create table visits(user varchar(20), place varchar(20), points int, day int);
create index idx_v_p on visits(place, day desc);
insert into visits values
('Ben', 'NY', 1, 100),
('Ben', 'LA', 3, 102),
('Joe', 'NY', 2, 103),
('Joe', 'LA', 1, 104);
-- just to prove this is efficient
explain select user from visits v where v.place = 'NY'
and day > 90
group by user
order by sum(points) desc
limit 1;
update place p set major =
(select user from visits v where p.name = v.place
and day > 90
group by user
order by sum(points) desc
limit 1);
select * from place where major = 'Joe';
select * from place where name = 'LA';

SQL Query for count of records matching day in a date range?

I have a table with records that look like this:
CREATE TABLE sample (
ix int unsigned auto_increment primary key,
start_active datetime,
last_active datetime
);
I need to know how many records were active on each of the last 30 days. The days should also be sorted incrementing so they are returned oldest to newest.
I'm using MySQL and the query will be run from PHP but I don't really need the PHP code, just the query.
Here's my start:
SELECT COUNT(1) cnt, DATE(?each of last 30 days?) adate
FROM sample
WHERE adate BETWEEN start_active AND last_active
GROUP BY adate;
Do an outer join.
No table? Make a table. I always keep a dummy table around just for this.
create table artificial_range(
id int not null primary key auto_increment,
name varchar( 20 ) null ) ;
-- or whatever your database requires for an auto increment column
insert into artificial_range( name ) values ( null )
-- create one row.
insert into artificial_range( name ) select name from artificial_range;
-- you now have two rows
insert into artificial_range( name ) select name from artificial_range;
-- you now have four rows
insert into artificial_range( name ) select name from artificial_range;
-- you now have eight rows
--etc.
insert into artificial_range( name ) select name from artificial_range;
-- you now have 1024 rows, with ids 1-1024
Now make it convenient to use, and limit it to 30 days, with a view:
Edit: JR Lawhorne notes:
You need to change "date_add" to "date_sub" to get the previous 30 days in the created view.
Thanks JR!
create view each_of_the_last_30_days as
select date_sub( now(), interval (id - 1) day ) as adate
from artificial_range where id < 32;
Now use this in your query (I haven't actually tested your query, I'm just assuming it works correctly):
Edit: I should be joining the other way:
SELECT COUNT(*) cnt, b.adate
FROM each_of_the_last_30_days b
left outer join sample a
on ( b.adate BETWEEN a.start_active AND a.last_active)
GROUP BY b.adate;
SQL is great at matching sets of values that are stored in the database, but it isn't so great at matching sets of values that aren't in the database. So one easy workaround is to create a temp table containing the values you need:
CREATE TEMPORARY TABLE days_ago (d SMALLINT);
INSERT INTO days_ago (d) VALUES
(0), (1), (2), ... (29), (30);
Now you can compare a date that is d days ago to the span between start_active and last_active of each row. Count how many matching rows in the group per value of d and you've got your count.
SELECT CURRENT_DATE - d DAYS, COUNT(*) cnt,
FROM days_ago
LEFT JOIN sample ON (CURRENT_DATE - d DAYS BETWEEN start_active AND last_active)
GROUP BY d
ORDER BY d DESC; -- oldest to newest
Another note: you can't use column aliases defined in your select-list in expressions until you get to the GROUP BY clause. Actually, in standard SQL you can't use them until the ORDER BY clause, but MySQL supports using aliases in GROUP BY and HAVING clauses as well.
Turn the date into a unix timestamp, which is seconds, in your query and then just look for the difference to be <= the number of seconds in a month.
You can find more information here:
http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_unix-timestamp
If you need help with the query please let me know, but MySQL has nice functions for dealing with datetime.
[Edit] Since I was confused as to the real question, I need to finish the lawn but before I forget I want to write this down.
To get a count of the number by day you will want your where clause to be as I described above, to limit to the past 30 days, but you will need to group by day, and so select by converting each start to a day of the month and then do a count of those.
This assumes that each use will be limited to one day, if the start and end dates can span several days then it will be trickier.