EAV Select query from spreaded value tables - sql

I have the following SQL Server database structure I have to use to query data. The model could be wrong; I appreciate arguments if that's the case so I can ask for changes. If not, I need a query to get tabbed data in the format I will detail below.
The structure goes like this:
CLIENTS:
ClientID ClientName
-----------------------
1 James
2 Leonard
3 Montgomery
ATTRIBUTES:
AttributeID AttributeName
-----------------------------
1 Rank
2 Date
3 Salary
4 FileRecordsAmount
ATTRIBUTES_STRING:
ClientID AttributeID AttributeStringValue
1 1 Captain
2 1 Chief Surgeon
3 1 Chief Engineer
ATTRIBUTES_NUMERIC:
ClientID AttributeID AttributeNumericValue
1 4 187
2 4 2
3 4 10
The result I need would be the following:
RESULTS:
----------------------------------------------------------
ClientID ClientName Rank FileRecordsAmount
1 James Captain 187
2 Leonard Chief Surgeon 2
3 Montgomery Chief Engineer 10
How can I achieve this?
Thank you very much!
EDIT: The challenging issue here (for me) is that the attributes are dynamic... I have 5 tables of attributes (ATTRIBUTES_STRING, ATTRIBUTES_NUMERIC, ATTRIBUTES_DATE, ATTRIBUTES_BIT, ATTRIBUTES_INT) and the user should be able to set up it's own attributes.

You need an SQL join. It will look something like this:
select
CLIENTS.ClientID,
CLIENTS.ClientName,
ATTRIBUTES_STRING1.AttributeStringValue as Rank,
ATTRIBUTES_NUMERIC2.AttributeNumericValue as FileRecordsAmount
from
CLIENTS,
ATTRIBUTES ATTRIBUTES1,
ATTRIBUTES ATTRIBUTES2,
ATTRIBUTES_STRING ATTRIBUTES_STRING1,
ATTRIBUTES_NUMERIC ATTRIBUTES_NUMERIC2
where CLIENTS.ClientID = ATTRIBUTES_STRING1.ClientID
and CLIENTS.ClientID = ATTRIBUTES_NUMERIC2.ClientID
and ATTRIBUTES_STRING1.AttributeID = ATTRIBUTES1.AttributeID
and ATTRIBUTES_NUMERIC2.AttributeID = ATTRIBUTES2.AttributeID
and ATTRIBUTES1.AttributeName = 'Rank'
and ATTRIBUTES2.AttributeName = 'FileRecordsAmount'
;
Here is the SQL Fiddle for reference. This is my first EAV schema so I wouldn't put too much trust in it :)
Edit: Schema provided below for reference:
create table CLIENTS (
ClientID integer primary key,
ClientName varchar(50) not null
);
insert into CLIENTS values (1,'James');
insert into CLIENTS values (2,'Leonard');
insert into CLIENTS values (3,'Montgomery');
create table ATTRIBUTES (
AttributeID integer primary key,
AttributeName varchar(50) not null
);
create index ATTRIBUTE_NAME_IDX on ATTRIBUTES (AttributeName);
insert into ATTRIBUTES values (1,'Rank');
insert into ATTRIBUTES values (2,'Date');
insert into ATTRIBUTES values (3,'Salary');
insert into ATTRIBUTES values (4,'FileRecordsAmount');
create table ATTRIBUTES_STRING (
ClientID integer,
AttributeID integer not null,
AttributeStringValue varchar(255) not null,
primary key (ClientID, AttributeID)
);
insert into ATTRIBUTES_STRING values (1,1,'Captain');
insert into ATTRIBUTES_STRING values (2,1,'Chief Surgeon');
insert into ATTRIBUTES_STRING values (3,1,'Chief Engineer');
create table ATTRIBUTES_NUMERIC (
ClientID integer,
AttributeID integer not null,
AttributeNumericValue numeric(10, 5) not null,
primary key (ClientID, AttributeID)
);
insert into ATTRIBUTES_NUMERIC values (1,4,187);
insert into ATTRIBUTES_NUMERIC values (2,4,2);
insert into ATTRIBUTES_NUMERIC values (3,4,10);
Edit: Modified the select to make it easier to extend with extra attributes

Related

Create combination sql table

I'm trying to create a sql table in data base in VS that has room and userid column, but the sql will only accept your input if the userid exists in users table and room exists in rooms tables
Allows:
Users table:
Userid
1
2
3
RoomUsers table:
Room ----- User
1 1
2. 1
1. 2
1. 3
2. 3
Won't allow:
Users table:
Userid
1
2
RoomUsers table:
Room ----- User
1 4
Normal foreign key wont work because it only allows one of each index and not multiple, how can I allow what I need to occur,to happen?
(This would be a mess in comments)
Probably we are having an XY problem here. The thing you describe is simply solved with a foreign key. ie:
CREATE TABLE users (id INT IDENTITY NOT NULL PRIMARY KEY, ad VARCHAR(100));
CREATE TABLE rooms (id INT IDENTITY NOT NULL PRIMARY KEY, ad VARCHAR(100));
CREATE TABLE room_user
(
RoomId INT NOT NULL
, UserId INT NOT NULL
, CONSTRAINT PK_roomuser
PRIMARY KEY(RoomId, UserId)
, CONSTRAINT fk_room
FOREIGN KEY(RoomId)
REFERENCES dbo.rooms(id)
, CONSTRAINT fk_user
FOREIGN KEY(UserId)
REFERENCES dbo.users(id)
);
INSERT INTO dbo.users(ad)
OUTPUT
Inserted.id, Inserted.ad
VALUES('RayBoy')
, ('John')
, ('Frank');
INSERT INTO dbo.rooms(ad)
OUTPUT
Inserted.id, Inserted.ad
VALUES('Room1')
, ('Room2')
, ('Room3');
INSERT INTO dbo.room_user(RoomId, UserId)VALUES(1, 1), (1, 2), (2, 3);
-- won't allow
INSERT INTO dbo.room_user(RoomId, UserId)VALUES(999, 888);

How to insert Auto-Increment using SELECT INTO Statement? SQL SERVER

This my table1:
Name Description
john student
dom teacher
I need to use SELECT * INTO to transfer it to another table (table2) but I want it with a new column named Auto which is auto-incremented.
Which will look like this:
Name Description Auto
John Student 1
Dom Teacher 2
Current Code: SELECT * INTO table2 FROM table1
Use ROW_NUMBER to add sequential number starting from 1.
SELECT *,
Auto = ROW_NUMBER() OVER(ORDER BY(SELECT NULL))
INTO table2
FROM table1
The accepted answer has additional convenience when breaking one table into several smaller ones with the exact number of rows. If necessary, it is possible to remove the column used for autoincrement.
SELECT *,
ID = ROW_NUMBER() OVER(ORDER BY ( SELECT NULL ))
INTO #Table2
FROM Table1
DECLARE #start INT, #end INT;
SET #start = 1;
SET #end = 5000000;
SELECT *
INTO Table3
FROM #Table2
WHERE ID BETWEEN #start AND #end;
ALTER TABLE Table3 DROP COLUMN ID;
You can use an identity field for this, that's what they're for. The logic of the identity(1,1) means that it will start at the number 1 and increment by 1 each time.
Sample data;
CREATE TABLE #OriginalData (Name varchar(4), Description varchar(7))
INSERT INTO #OriginalData (Name, Description)
VALUES
('John','student')
,('Dom','teacher')
Make a new table and insert the data into it;
CREATE TABLE #NewTable (Name varchar(4), Description varchar(7), Auto int identity(1,1))
INSERT INTO #NewTable (Name, Description)
SELECT
Name
,Description
FROM #OriginalData
Gives the results as;
Name Description Auto
John student 1
Dom teacher 2
If you ran the insert a couple more times your results would look like this;
Name Description Auto
John student 1
Dom teacher 2
John student 3
Dom teacher 4
John student 5
Dom teacher 6

Make a copy of parent-child structure in SQL

I have a table MODELS to which several ITEMS can belong. The ITEMS table is a hierarchical table with a self join on the PARENT column. Root level items will have Null in PARENT. Items can go to any level deep.
create table MODELS (
MODELID int identity,
MODELNAME nvarchar(200) not null,
constraint PK_MODELS primary key (MODELID)
)
go
create table ITEMS (
ITEMID int identity,
MODELID int not null,
PARENT int null,
ITEMNUM nvarchar(20) not null,
constraint PK_ITEMS primary key (ITEMID)
)
go
alter table ITEMS
add constraint FK_ITEMS_MODEL foreign key (MODELID)
references MODELS (MODELID)
go
alter table ITEMS
add constraint FK_ITEMS_ITEMS foreign key (PARENT)
references ITEMS (ITEMID)
go
I wish to create stored procedure to copy a row in the MODELS table into a new row and also copy the entire structure in ITEMS as well.
For example, if I have the following in ITEMS:
ITEMID MODELID PARENT ITEMNUM
1 1 Null A
2 1 Null B
3 1 Null C
4 1 1 A.A
5 1 2 B.B
6 1 4 A.A.A
7 1 4 A.A.B
8 1 3 C.A
9 1 3 C.B
10 1 9 C.B.A
I'd like to create new Model row and copies of the 10 Items that should be as follows:
ITEMID MODELID PARENT ITEMNUM
11 2 Null A
12 2 Null B
13 2 Null C
14 2 11 A.A
15 2 12 B.B
16 2 14 A.A.A
17 2 14 A.A.B
18 2 13 C.A
19 2 13 C.B
20 2 19 C.B.A
I will pass the MODELID to be copied as a parameter to the Stored Procedure. The tricky part is setting the PARENT column correctly. I think this will need to be done recursively.
Any suggestions?
The solution described here will work correctly in multi-user environment. You don't need to lock the whole table. You don't need to disable self-referencing foreign key. You don't need recursion.
(ab)use MERGE with OUTPUT clause.
MERGE can INSERT, UPDATE and DELETE rows. In our case we need only to INSERT. 1=0 is always false, so the NOT MATCHED BY TARGET part is always executed. In general, there could be other branches, see docs. WHEN MATCHED is usually used to UPDATE; WHEN NOT MATCHED BY SOURCE is usually used to DELETE, but we don't need them here.
This convoluted form of MERGE is equivalent to simple INSERT, but unlike simple INSERT its OUTPUT clause allows to refer to the columns that we need. It allows to retrieve columns from both source and destination tables thus saving a mapping between old and new IDs.
sample data
DECLARE #Items TABLE (
ITEMID int identity,
MODELID int not null,
PARENT int null,
ITEMNUM nvarchar(20) not null
)
INSERT INTO #Items (MODELID, PARENT, ITEMNUM) VALUES
(1, Null, 'A'),
(1, Null, 'B'),
(1, Null, 'C'),
(1, 1 , 'A.A'),
(1, 2 , 'B.B'),
(1, 4 , 'A.A.A'),
(1, 4 , 'A.A.B'),
(1, 3 , 'C.A'),
(1, 3 , 'C.B'),
(1, 9 , 'C.B.A');
I omit the code that duplicates the Model row. Eventually you'll have ID of original Model and new Model.
DECLARE #SrcModelID int = 1;
DECLARE #DstModelID int = 2;
Declare a table variable (or temp table) to hold the mapping between old and new item IDs.
DECLARE #T TABLE(OldItemID int, NewItemID int);
Make a copy of Items remembering the mapping of IDs in the table variable and keeping old PARENT values.
MERGE INTO #Items
USING
(
SELECT ITEMID, PARENT, ITEMNUM
FROM #Items AS I
WHERE MODELID = #SrcModelID
) AS Src
ON 1 = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT (MODELID, PARENT, ITEMNUM)
VALUES
(#DstModelID
,Src.PARENT
,Src.ITEMNUM)
OUTPUT Src.ITEMID AS OldItemID, inserted.ITEMID AS NewItemID
INTO #T(OldItemID, NewItemID)
;
Update old PARENT values with new IDs
WITH
CTE
AS
(
SELECT I.ITEMID, I.PARENT, T.NewItemID
FROM
#Items AS I
INNER JOIN #T AS T ON T.OldItemID = I.PARENT
WHERE I.MODELID = #DstModelID
)
UPDATE CTE
SET PARENT = NewItemID
;
Check the results
SELECT * FROM #Items;
You can do it without recursion. But you need to lock the table first maybe to be sure that it works fine.
insert into items (Modelid, Parent, ITEMNUM)
select 2 as modelId,
MAP.currId as Parent,
MO.ITEMNUM
from (
( select * from items where MODELID = 1) MO
left join
( select IDENT_CURRENT('ITEMS') + ROW_NUMBER() OVER(ORDER BY itemid ) currID ,
i.ItemID
from ITEMS i
where modelid = 1 ) MAP
on MO.Parent= MAP.ItemID
) ORDER BY MO.ItemID
The idea behind it is that we select all rows from original model in ITEM table and we generate fake ID for them.
The fake ID is :
Row 1 = current identity + 1,
Row 2 = current identity + 2,
etc.
After that we have mapping : oldid -> newid
Then we insert original model to ITEM table like it is but we replace Parent by record from our mapping.
The issue that I can see is that some ItemID may still not exist for Parent when we insert rows (ie. we insert row that will have ItemID 20 but its Parent is 21). For that we may need to disable constraint on Parent for the time of execution of this insert. After that we supposed to enable it again. Data will be correct of course.

SQL Server, self referencing foreign compound key

I have a table with columns task_id (pk), client_id, parent_task_id, title. In other words, tasks are owned by clients, and some tasks have child tasks.
For example, client 7 may have a task "wash the car," with child tasks "vacuum carpet" and "wipe dashboard."
I want a constraint so that a task and its children are always owned by the same client.
Through a bit of experimentation, to do this, I created a self-referencing foreign key (client_id, parent_task_id) referencing (client_id, task_id). At first I received an error (There are no primary or candidate keys in the referenced table that match the referencing column list in the foreign key.) So I added a unique key for columns task_id, client_id. Now it seems to work.
I am wondering if this is the best solution (or at least reasonable one) to enforce this constraint. Any thoughts would be appreciated. Thanks much!
A 'parent' record would not need a [parent_task_id]
TASK ID | CLIENT ID | PARENT TASK ID | TITLE
1 | 7 | NULL | wash the car
(To find all of your parent records, SELECT * FROM TABLE WHERE [parent_task_id] is null)
A 'child' record would need a [parent_task_id], but not a [client_id] (because, as you stipulate, a child has the same client as it's parent).
TASK ID | CLIENT ID | PARENT TASK ID | TITLE
2 | NULL | 1 | vacuum carpent
3 | NULL | 1 | wipe dashboard
In this way, your self-referencing foreign key is all the constraint you need. No constraint / rule concerning [client_id] on child records is necessary, because all [client_id] values on child records will be ignored, in favor of the [client_id] on the parent record.
For example, if you want to know what the [client_id] is for a child record:
SELECT
c.task_id,
p.client_id,
c.title
FROM
table p --parent
INNER JOIN table c --child
ON p.task_id = c.parent_task_id
UPDATE
(How to query for the client ID of a grand-child)
--Create and populate your table (using a table var in this sample)
DECLARE #table table (task_id int, client_id int, parent_task_id int, title varchar(50))
INSERT INTO #table VALUES (1,7,NULL,'wash the car')
INSERT INTO #table VALUES (2,NULL,1,'vacuum carpet')
INSERT INTO #table VALUES (3,NULL,1,'wipe dashboard')
INSERT INTO #table VALUES (4,NULL,2,'Step 1: plug-in the vacuum')
INSERT INTO #table VALUES (5,NULL,2,'Step 2: turn-on the vacuum')
INSERT INTO #table VALUES (6,NULL,2,'Step 3: use the vacuum')
INSERT INTO #table VALUES (7,NULL,2,'Step 4: turn-off the vacuum')
INSERT INTO #table VALUES (8,NULL,2,'Step 5: empty the vacuum')
INSERT INTO #table VALUES (9,NULL,2,'Step 6: put-away the vacuum')
INSERT INTO #table VALUES (10,NULL,3,'Step 1: spray cleaner on the rag')
INSERT INTO #table VALUES (11,NULL,3,'Step 2: use the rag')
INSERT INTO #table VALUES (12,NULL,3,'Step 3: put-away the cleaner')
INSERT INTO #table VALUES (13,NULL,3,'Step 4: toss the rag in the laundry bin')
--Determine which grandchild you want the client_id for
DECLARE #task_id int
SET #task_id = 8 -- grandchild's ID to use to find client_id
--Create your CTE (this is the recursive part)
;WITH myList (task_id, client_id, parent_task_id, title)
AS
(
SELECT a.task_id, a.client_id, a.parent_task_id, a.title
FROM #table a
WHERE a.task_id = #task_id
UNION ALL
SELECT a.task_id, a.client_id, a.parent_task_id, a.title
FROM #table a
INNER JOIN myList m
ON a.task_id = m.parent_task_id
)
--Query your CTE
SELECT task_id, client_id, title FROM myList WHERE client_id is not null
In this example, I used a granchild's task_id (8 -- 'empty the vacuum') to find it's highest-level parent, which holds the client_id.
You can remove the WHERE clause from the last step if you want to see each parent, parent's parent, and so on up to the first-parent's record.

How to insert a record into a table with a column declared with the SERIAL function

My database is using PostgreSQL. One table is using the serial auto-increment macro. If I want to insert a record into the table, do I still need to specify that value, or it is be automatically assigned for me?
CREATE TABLE dataset
(
id serial NOT NULL,
age integer NOT NULL,
name character varying(32) NOT NULL,
description text NOT NULL DEFAULT ''::text
CONSTRAINT dataset_pkey PRIMARY KEY (id)
);
Using the DEFAULT keyword or by omitting the column from the INSERT list:
INSERT INTO dataset (id, age, name, description)
VALUES (DEFAULT, 42, 'fred', 'desc');
INSERT INTO dataset (age, name, description)
VALUES (42, 'fred', 'desc');
If you create a table with a serial column then if you omit the serial column when you insert data into the table PostgreSQL will use the sequence automatically and will keep the order.
Example:
skytf=> create table test_2 (id serial,name varchar(32));
NOTICE: CREATE TABLE will create implicit sequence "test_2_id_seq" for serial column "test_2.id"
CREATE TABLE
skytf=> insert into test_2 (name) values ('a');
INSERT 0 1
skytf=> insert into test_2 (name) values ('b');
INSERT 0 1
skytf=> insert into test_2 (name) values ('c');
INSERT 0 1
skytf=> select * From test_2;
id | name
----+------
1 | a
2 | b
3 | c
(3 rows)
These query work for me:
insert into <table_name> (all columns without id serial)
select (all columns without id serial)
FROM <source> Where <anything>;
Inserting multiple rows wasn't working for me in this scenario:
create table test (
id bigint primary key default gen_id(),
msg text not null
)
insert into test (msg)
select gs
from generate_series(1,10) gs;
because I had mistakenly marked my gen_id function IMMUTABLE.
The insert query was being optimized to only call that function once rather than 10 times. Oops...
For example, you create "person" table with "id" of serial and "name" as shown below:
CREATE TABLE person (
id serial PRIMARY KEY,
name VARCHAR(50)
)
Then, you can use DEFAULT for "id" of serial and insert rows without column(field) names as shown below:
INSERT INTO person VALUES (DEFAULT, 'John'), (DEFAULT, 'Tom');
postgres=# SELECT * FROM person;
id | name
----+------
1 | John
2 | Tom