SQL : Select a dynamic number of rows as columns

前端 未结 4 1243
春和景丽
春和景丽 2021-01-22 23:21

I need to select static colums + a dynamic number of rows as columns in SQL

TABLE 1
-------
HotelID
BlockID
BlockName

TABLE 2
-------
BlockDate (unknown number          


        
相关标签:
4条回答
  • 2021-01-22 23:54

    Do this in the client.

    SQL is a fixed column language: you can't have a varible number of columns (even with PIVOT etc). Dynamic SQL is not a good idea.

    0 讨论(0)
  • 2021-01-22 23:55

    I once wrote a stored procedure that did just something like this. Given are a users table with basic details and a variable number of profile properties for users. (The number of profile properties varies per DotNetNuke Portal)

    This is straight from DotNetNuke 4.9, check the database schema from there and you'll get the details. Tables involved are Users, UserPortals, UserProfile, ProfilePropertyDefinition

    In short words :

    1. I create a temp table with all the profile properties as columns (dynamically)
    2. I fill the temp table with userid (as foreign key to link to users table and all the profile properties data
    3. I do a join on users table and temp table

    This gives me one row per user with all profile properties.

    Here the code - not perfect but hey its tailored for my needs but should be easy enough to re-use (tried to avoid cursors btw)

    set ANSI_NULLS ON
    set QUOTED_IDENTIFIER ON
    go
    
    ALTER PROCEDURE [dbo].[rows2cols] @portalId INT
    AS BEGIN
    print 'PortalID=' + convert(varchar,@portalId)
        --SET NOCOUNT ON;
        declare @idx int    
        declare @rowcount int
        declare @tmpStr nvarchar(max)
        declare @ctype nvarchar(max)
        declare @cname nvarchar(max)
        declare @clen int
        declare @createStr nvarchar(max)    
    ---------------------------------------------------------------------------
    -- create tmp table --
    ---------------------------------------------------------------------------   
    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[xxxx]') AND type in (N'U'))
    DROP TABLE [dbo].[xxxx]
    
    print 'Building Temp Table Cols for profile properties...'  
    set @rowcount = (select count(*) from ProfilePropertyDefinition where PortalID=0 and deleted=0)
    set @idx = 1
    set @tmpStr = ''
    while (@idx <= @rowcount)
    begin
        -- dynamically generate rownumbers to be able to do loop over them (avoid cursors)
        select @cname = t1.PropertyName from
        ( select ROW_NUMBER() OVER (ORDER BY ViewOrder) as Idx, PropertyName from ProfilePropertyDefinition 
          where PortalID=0 and deleted=0
        ) as t1 where t1.Idx = @idx
    
        if (@cname = 'Email' or @cname = 'FirstName' or @cname = 'LastName') begin
            set @clen = 1 
        end else begin 
            set @tmpStr = @tmpStr + '[' + @cname + '] [nvarchar](500), '
        end
        set @idx = @idx + 1
    end
    
    set @tmpStr = @tmpStr + '[userid] [int] '
    set @createStr = 'create table xxxx ( ' + @tmpStr + ' )'
    
    Print @createStr
    Exec (@createStr)
    
    ---------------------------------------------------------------------------
    -- fill tmp table --
    ---------------------------------------------------------------------------    
    declare @propName nvarchar(max)
    declare @propVal nvarchar(max)
    declare @userId int
    declare @idx2 int
    declare @rowcount2 int
    declare @inscol nvarchar(max)
    declare @insval nvarchar(max)
    
    set @rowcount = (select count(*) FROM Users LEFT OUTER JOIN UserPortals ON Users.UserID = UserPortals.UserId WHERE UserPortals.PortalId = @portalId)
    set @idx = 1
        while (@idx <= @rowcount)
        begin
            -- get userId
            select @userId = t1.UserID from (select u.UserID, ROW_NUMBER() OVER (ORDER BY u.UserID) as Idx
            from Users as u LEFT OUTER JOIN UserPortals as up ON u.UserID = up.UserId where up.PortalId = @portalId) as t1 
            where t1.Idx = @idx 
    
            set @idx2 = 1
            set @rowcount2 = (select count(*) from UserProfile where UserID = @userId)
            set @inscol = ''
            set @insval = ''
    
            while (@idx2 < @rowcount2)
            begin
                -- build insert for a specific user
                select @propName = t1.PropertyName , @propVal=t1.PropertyValue from
                ( select ROW_NUMBER() OVER (ORDER BY ProfileID) as Idx, up.PropertyDefinitionID,ppd.PropertyName, up.PropertyValue 
                  from UserProfile as up 
                   inner join ProfilePropertyDefinition as ppd on up.PropertyDefinitionID = ppd.PropertyDefinitionID 
                  where UserID = @userId
                ) as t1 where t1.Idx = @idx2
    
                if (@propName != 'Firstname' and @propName != 'LastName' and @propName != 'Email')
                begin
                    set @inscol = @inscol + @propName + ', '
                    set @insval = @insval + 'N''' + replace(@propVal,'''','''''') + ''', '
                end
    
                set @idx2 = @idx2 + 1
            end
    
            set @inscol = @inscol + 'userid'
            set @insval = @insval + convert(nvarchar,@userId) 
    
            set @tmpStr = 'insert into xxxx (' + @inscol + ') values (' + @insval + ')'
            --print @tmpStr
            Exec(@tmpStr)
            set @idx = @idx + 1
        end
    
    -- -------------------------------------------------------------------------
    -- return tmp table & dump  --
    -- -------------------------------------------------------------------------    
    SELECT Users.*, xxxx.* FROM xxxx INNER JOIN Users ON xxxx.userid = Users.UserID
    
    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[xxxx]') AND type in (N'U'))
    DROP TABLE [dbo].[xxxx]
    END
    
    0 讨论(0)
  • 2021-01-23 00:07

    What you require is a pivot query, to convert row data into columnar:

      SELECT t.hotelid,
             t.blockid,
             t.blockname,
             MAX(CASE WHEN t2.blockdate = '02-10-10' THEN t.numberofrooms ELSE NULL END) AS 02_10_10,
             MAX(CASE WHEN t2.blockdate = '02-11-10' THEN t.numberofrooms ELSE NULL END) AS 02_11_10,
             MAX(CASE WHEN t2.blockdate = '02-12-10' THEN t.numberofrooms ELSE NULL END) AS 02_12_10,
             ...
        FROM TABLE_1 t
        JOIN TABLE_2 t2
    GROUP BY t.hotelid, t.blockid, t.blockname
    
    1. Mind that there's nothing to link the tables - realistically TABLE_2 needs hotelid and blockid attributes. As-is, this will return the results of TABLE_2 for every record in TABLE_1...

    2. The database is important, because it will need dynamic SQL to create the MAX(CASE... statements

    0 讨论(0)
  • 2021-01-23 00:14

    you are missing a foreign key. I have to assume that BlockId should be PK in table 2?

    Also, assuming that this is a legacy db and changing the structure is not an option, i have to ask which platform?

    If ms sql, this could easily be achieved using a dynamic sql statement.

    0 讨论(0)
提交回复
热议问题