Dynamic table design (common lookup table), need a nice query to get the values

和自甴很熟 提交于 2020-02-25 04:18:11

问题


sql2005

This is my simplified example: (in reality there are 40+ tables in here, I only showed 2)

I got a table called tb_modules, with 3 columns (id, description, tablename as varchar):

 1, UserType, tb_usertype
 2, Religion, tb_religion

(Last column is actually the name of a different table)

I got an other table that looks like this: tb_value (columns:id, tb_modules_ID, usertype_OR_religion_ID) values:

 1111, 1, 45
 1112, 1, 55
 1113, 2, 123
 1114, 2, 234

so, I mean 45, 55, 123, 234 are usertype OR religion ID's (45, 55 usertype, 123, 234 religion ID`s)

Don't judge, I didn't design the database

Question How can I make a select, showing * from tb_value, plus one column That one column would be TITLE from the tb_usertype or RELIGIONNAME from the tb_religion table

I would like to make a general thing. Was thinking initially about maybe a SQL function that returns a string, but I think I would need dynamic SQL, which is not ok in a function.

Anyone a better idea ?


回答1:


At the beginning we have this -- which is quite messy.

To clean-up a bit I add two views and a synonym:

create view v_Value as
select
      ID                      as ValueID
    , tb_modules_ID           as ModuleID
    , usertype_OR_religion_ID as RemoteID
from tb_value ;
go

create view v_Religion as
select
      ID
    , ReligionName as Title
from tb_religion ;
go

create synonym v_UserType for tb_UserType ;
go

And now the model looks like

It is easier now to write the query

;
with 
q_mod as (
    select
          m.ID                          as ModuleID
        , coalesce(x1.ID    , x2.ID)    as RemoteID
        , coalesce(x1.Title , x2.Title) as Title
        , m.Description                 as ModuleType
    from      tb_Modules as m
    left join v_UserType as x1 on m.TableName = 'tb_UserType'
    left join v_Religion as x2 on m.TableName = 'tb_Religion'
)
select
      a.ModuleID
    , v.ValueID
    , a.RemoteID
    , a.ModuleType
    , a.Title
from q_mod   as a
join v_Value as v on (v.ModuleID = a.ModuleID and v.RemoteID = a.RemoteID) ;

There is an obvious pattern in this query, so it can be created as dynamic sql if you have to add another module-type table. When adding another table, use ID and Title to avoid having to use a view.

EDIT

To build dynamic sql (or query on application level)

Modify lines 6 and 7, the x-index is tb_modules.id

coalesce(x1. , x2. , x3. ..) 

Add lines to the left join (below line 11)

left join v_SomeName as x3  on m.TableName = 'tb_SomeName'

The SomeName is tb_modules.description and x-index is matching tb_modules.id

EDIT 2

The simplest would probably be to package the above query into a view and then each time the schema changes dynamically crate and run ALTER VIEW. This way the query would not change from the point of the application.




回答2:


Since we're all agreed the design is flaky, I'll skip any comments on that. The pattern of the query is this:

-- Query 1
select tb_value.*,tb_religion.religion_name as ANY_DESCRIPTION
  from tb_value 
  JOIN tb_religion on tb_value.ANY_KIND_OF_ID = tb_religion.id
 WHERE tb_value.module_id = 2
-- combine it with...
UNION ALL
-- ...Query 2
select tb_value.*,tb_religion.title as ANY_DESCRIPTION
  from tb_value 
  JOIN tb_userType on tb_value.ANY_KIND_OF_ID = tb_userType.id
 WHERE tb_value.module_id = 1
-- combine it with...
UNION ALL
-- ...Query 3
select  lather, rinse, repeat for 40 tables!

You can actually define a view that hardcodes all 40 cases, and then put filters onto queries for the particular modules you want.




回答3:


To do this dynamically you need to be able to create a sql statement that looks like this

select tb_value.*, tb_usertype.title as Descr
from tb_value
    inner join tb_usertype
        on tb_value.extid = tb_usertype.id
where tb_value.tb_module_id = 1
union all
select tb_value.*, tb_religion.religionname as Descr
from tb_value
    inner join tb_religion
        on tb_value.extid = tb_religion.id
where tb_value.tb_module_id = 2
-- union 40 other tables

Currently you can not do that because you do not have any information in the db telling you which column to use from tb_religion and tb_usertype etc. You can add that as a new field in tb_module.

If you have fieldname to use in tb_module you can build a view that does what you want. And you could add a trigger to table tb_modules that alters the view whenever tb_modules is modified. That way you do not need to use dynamic sql from the client when doing queries. The only thing you need to worry about is that the table needs to be created in the db before you add a new row to tb_modules

Edit 1 Of course the code in the trigger needs to dynamically build the alter view statement.

Edit 2 You also need to have a field with information about what column in tb_usertype and tb_religion etc. to join against tb_value.extid (usertype_OR_religion_ID). Or you can assume that the field will always be called id

Edit 3 Here is how you could build the trigger on tb_module that alters the view v_values. I have added fieldname as a column in tb_modules and I assume that the id field in the related tables is called id.

create trigger tb_modules_change on tb_modules after insert, delete, update
as 

declare @sql nvarchar(max)
declare @moduleid int
declare @tablename varchar(50)
declare @fieldname varchar(50)

set @sql = 'alter view v_value as '

declare mcur cursor for
  select id, tablename, fieldname
  from tb_modules

open mcur
fetch next from mcur into @moduleid, @tablename, @fieldname
while @@FETCH_STATUS = 0
begin
    set @sql = @sql + 'select tb_value.*, '+@tablename+'.'+@fieldname+' '+
                      'from tb_value '+
                      'inner join '+@tablename+' '+
                        'on tb_value.extid = '+@tablename+'.id '+
                      'where tb_value.tb_module_id = '+cast(@moduleid as varchar(10))  
  fetch next from mcur into @moduleid, @tablename, @fieldname

  if @@FETCH_STATUS = 0
  begin
    set @sql = @sql + ' union all '
  end
end
close mcur
deallocate mcur

exec sp_executesql @sql



回答4:


Hm..there are probably better solutions available but here's my five cents:

SELECT 
id,tb_modules_ID,usertype_OR_religion_ID,
COALESCE(
 (SELECT TITLE FROM tb_usertype WHERE Id = usertype_OR_religion_ID),
 (SELECT RELIGIONNAME FROM tb_religion WHERE Id = usertype_OR_religion_ID),
 'N/A'
) AS SourceTable
FROM tb_valuehere

Note that I don't have the possibility to check the statement right now so I'm reserving myself for any syntax errors...




回答5:


First, using your current design the only reasonable solution is dynamic SQL. You should write a module in your middle-tier that queries for the appropriate table names and builds the queries on the fly. Trying to accomplish that in T-SQL will be a nightmare. T-SQL was not designed for string construction.

The right solution is to build a new database designed properly, migrate the data and scrap the existing design. The problems you will encounter with your current design will simply grow. It will be harder for new developers to learn the new system. It will be prone to errors. There will be no data integrity (e.g. forcing the attribute "Start Date" to be parsable as a date). Custom queries will be a chore to write and so on. Eventually, you will hit the day when the types of information desired from the system are simply too difficult to extract given the current design.




回答6:


First take the undesigner out the back and put them out of their misery. They are hurting people.

  • Due to their incompetence, every time you add a row to Module, you have to modify every query that uses it. Good for www.dailywtf.com.

  • You do not have Referential Integrity either, because you cannot define an FK on the this_or_that column. Your data is exposed, probably to "code" written by the same undesigner. No doubt you are aware that this is where the deadlocks are created.

  • That it is a "judgement", that is so that you understand the gravity of the undesign, and you can justify replacing it, to your managers.

  • SQL was designed for Relational Databases, that means Normalised. It is not good for mangled files. Sure, some queries may be better than others (just look at the answers), but there is no way to get around the undesign, any SQL query will be hamstrung, and need change whenever a Module row is added.

  • "Dynamic" is reserved for Databases, not possible for flat flies.

Two answers. One to stop the continuing idiocy of changing the existing queries every time a Module row is added (you're welcome); the second to answer your question.

Safe Future Queries

CREATE VIEW UserReligion_vw AS
    SELECT  [XxxxId]     = id,    -- replace Xxxx
            [ReligionId] = usertype_OR_religion_ID
        FROM  tb_value
        WHERE tb_modules_ID = 1

CREATE VIEW UserReligion_vw AS SELECT [XxxxId] = id, [ReligionId] = usertype_OR_religion_ID FROM tb_value WHERE tb_modules_ID = 2 From now on, make sure the all queries currently using the undesign, are modified to use the correct View instead. Do not use the Views for Update/Delete/Insert.

Answer

Ok, now for the main question. I can think of other approaches, but this one is the best. You have stated, you want the third column to also be an unnormalised piece of chicken excreta and the supply Title for [EITHER_Religion_OR_UserType_OR_This_OR_That]. Right, so you are teaching the user to be confused as well; when the no of modules grow, they will have great fun figuring out what the column contains. Yes a problem does always compound itself.

    SELECT  [XxxxId]   = id,
            [Whatever] = CASE tb_modules_ID
                WHEN 1 THEN ( SELECT name      -- title, whatever
                    FROM  tb_religion
                    WHERE id = V.usertype_OR_religion_ID 
                    )
                WHEN 2 THEN ( SELECT name      -- title, whatever
                    FROM  tb_usertype
                    WHERE id = V.usertype_OR_religion_ID 
                    )
                ELSE "(UnknownModule)"         -- do not remove the brackets
                END
        FROM  tb_value V
        WHERE conditions...                    -- you need something here
This is called a Correlated Scalar Subquery.

  • It works on any version of Sybase since 4.9.2 with no limitations. And SQL 2005 (last time I looked, anyway, Aug 2009). But on MS you will get a StackTrace if the volume of tb_value is large, so make sure the WHERE clause has some conditions on it.

  • But MS have broken the server with their "new" 2008 codeline, so it does not work in all circumstances (the worse your mangled files, the less likely it will work; the better your database design, the more likely it will work). That is why some MS people pray every day for the next Service pack, and others never attend church.




回答7:


I guess you want something like this:

Adding tables and one row per table into tb_modules is straight forward.

SET NOCOUNT ON
if OBJECT_ID('tb_modules') > 0 drop table tb_modules;
if OBJECT_ID('tb_value') > 0 drop table tb_value;
if OBJECT_ID('tb_usertype') > 0 drop table tb_usertype;
if OBJECT_ID('tb_religion') > 0 drop table tb_religion;
go

create table dbo.tb_modules (
    id int, 
    description varchar(20), 
    tablename varchar(255)
);

insert into tb_modules values (  1, 'UserType', 'tb_usertype');
insert into tb_modules values (  2, 'Religion', 'tb_religion');

create table dbo.tb_value(
    id int,
    tb_modules_ID int,
    usertype_OR_religion_ID int
);
insert into tb_value values (   1111, 1, 45);
insert into tb_value values (   1112, 1, 55);
insert into tb_value values (   1113, 2, 123);
insert into tb_value values (   1114, 2, 234);

create table dbo.tb_usertype(
    id int,
    UserType varchar(30)
);

insert into tb_usertype values ( 45, 'User_type_45');
insert into tb_usertype values ( 55, 'User_type_55');

create table dbo.tb_religion(
    id int,
    Religion varchar(30)
);

insert into tb_religion values ( 123, 'Religion_123');
insert into tb_religion values ( 234, 'Religion_234');

-- start of query

declare @sql varchar(max) = null

Select @sql = case when @sql is null then '          ' else @sql + char(10) + 'union all '  end 
    + 'Select ' + str(id) + ' type, id, ' + description + ' description from '  + tablename  from   tb_modules 

set @sql = 'select  v.id, tb_modules_ID , usertype_OR_religion_ID , t.description
from tb_value v
    join ( ' + @sql + ') as t
on v.tb_modules_ID = t.type and v.usertype_OR_religion_ID = t.id
'

Print @sql
exec( @sql)



回答8:


I think it's intended to be used with dynamic sql.

Maybe break out each tb_value.tb_modules_ID row into its own temp table, named with the tb_modules.tablename.

Then have an sp iterate through the temp tables matching your naming convention (by prefix or suffix) building the sql and doing your join.



来源:https://stackoverflow.com/questions/4669369/dynamic-table-design-common-lookup-table-need-a-nice-query-to-get-the-values

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!