Recently we had a post by a reader stating that with 2000 tables in a
database it was taking 45 seconds to login it. He also indicated that adding
them to the db_reader role made the problem go away. To see the entire thread
(which is so far unresolved), follow this link:
Poor login speed with 2000+ tables
This seems like a good thread to explore. At first glance I'd expect
Enterprise Manager to be slow because it would need to load first all 2000
tables, then select the permissions on all 2000 objects when you go to view
permissions on a user. It's not slow to select 2000 rows out of sysobjects, but
DMO is object based, so you lose most of the advantages of doing set
based work as it loads one object at a time.
But just to log in to the db? The reader indicates that a different owner
owns the objects, so maybe some overhead from having to follow all the ownership
chains?
From what I can see in Profiler, if I connect as a user to master and then
change to a test database, the only procedure that affects security is
sp_MShasdbaccess. Looking at that proc, we see it's not very complicated:
;select name as 'dbname', owner = substring(suser_sname(sid), 1, 24), DATABASEPROPERTY(name, N'IsDboOnly') as 'DboOnly', DATABASEPROPERTY(name, N'IsReadOnly') as 'ReadOnly', DATABASEPROPERTY(name, N'IsSingleUser') as 'SingleUser', DATABASEPROPERTY(name, N'IsDetached') as 'Detached', DATABASEPROPERTY(name, N'IsSuspect') as 'Suspect', DATABASEPROPERTY(name, N'IsOffline') as 'Offline', DATABASEPROPERTY(name, N'IsInLoad') as 'InLoad', DATABASEPROPERTY(name, N'IsEmergencyMode') as 'EmergencyMode', DATABASEPROPERTY(name, N'IsInStandBy') as 'StandBy', DATABASEPROPERTY(name, N'IsShutdown') as 'ShutDown', DATABASEPROPERTY(name, N'IsInRecovery') as 'InRecovery', DATABASEPROPERTY(name, N'IsNotRecovered') as 'NotRecovered' from master.dbo.sysdatabases where has_dbaccess(name) = 1 order by name
Of course we can't see what the has_dbaccess function is doing (or at least
I'm not profiling the correct events). So...it would seem the thing to do is try
to reproduce the problem. I created 500 tables using the following code. You can
download the complete solution file here. This is
VB.Net 2003 code, but overall it's very much like what you would do using VB6
other than the error handling. Even with 500 tables I saw no noticeable delay
when switching to the database via Query Analyzer.
Sub CreateTables(ByVal HowMany As Integer, ByVal UseRoleForTesting As Boolean) Dim oserver As SQLDMO.SQLServer Dim odb As SQLDMO.Database Dim oTable As SQLDMO.Table Dim oCol As SQLDMO.Column Dim J As Integer Dim oLogin As SQLDMO.Login Dim bLoginFound As Boolean Dim oUser As SQLDMO.User Dim oRole As SQLDMO.DatabaseRole Const TBL_NAME As String = "TEST" Try 'connect to local using a trusted connection oserver = New SQLDMO.SQLServer oserver.LoginSecure = True oserver.Connect(".") 'drop previous test db if it exists For Each odb In oserver.Databases If odb.Name = DB_NAME Then oserver.KillDatabase(DB_NAME) End If Next 'create a testing database odb = New SQLDMO.Database odb.Name = DB_NAME oserver.Databases.Add(odb) 'add a test login if it doesnt exist For Each oLogin In oserver.Logins If oLogin.Name = DB_NAME & "USER" Then bLoginFound = True Exit For End If Next If bLoginFound = False Then oLogin = New SQLDMO.Login oLogin.Name = DB_NAME & "USER" oLogin.SetPassword("", PASSWORD) oserver.Logins.Add(oLogin) End If 'clunky, but repeat for the object owner bLoginFound = False For Each oLogin In oserver.Logins If oLogin.Name = DB_NAME & "USEROBJ" Then bLoginFound = True Exit For End If Next If bLoginFound = False Then oLogin = New SQLDMO.Login oLogin.Name = DB_NAME & "USEROBJ" oserver.Logins.Add(oLogin) End If 'add user to db oUser = New SQLDMO.User oUser.Name = DB_NAME & "USER" oUser.Login = DB_NAME & "USER" odb.Users.Add(oUser) 'add user to db oUser = New SQLDMO.User oUser.Name = DB_NAME & "USEROBJ" oUser.Login = DB_NAME & "USEROBJ" odb.Users.Add(oUser) 'add role, then add user to role oRole = New SQLDMO.DatabaseRole oRole.Name = "SSCTESTROLE" odb.DatabaseRoles.Add(oRole) oRole.AddMember(DB_NAME & "USER") 'create a lot of tables For J = 1 To HowMany 'name the tables sequentially oTable = New SQLDMO.Table oTable.Name = TBL_NAME & J.ToString oTable.Owner = DB_NAME & "USEROBJ" 'just do a simple one column table oCol = New SQLDMO.Column oCol.Datatype = "int" oCol.Identity = True oCol.Name = "TestColumn" oTable.Columns.Add(oCol) odb.Tables.Add(oTable) If UseRoleForTesting = True Then oTable.Grant(SQLDMO.SQLDMO_PRIVILEGE_TYPE.SQLDMOPriv_Select, "SSCTESTROLE") Else 'grant access to our test user oTable.Grant(SQLDMO.SQLDMO_PRIVILEGE_TYPE.SQLDMOPriv_Select, DB_NAME & "USER") End If Next Catch ex As Exception MessageBox.Show(ex.Message) Finally oserver.DisConnect() oserver = Nothing End Try
It's been a while since we've posted any DMO code, this combines a lot of
different objects to give you a basic framework for creating test environments.
To use it, you just call it like this:
CreateTables(500, True)
So, I'm stumped. Is hardware masking the problem? Maybe I haven't recreated
the situation exactly enough to see the problem? What's the largest number of
tables you've worked with and have you noticed similar issues? Where would you
look next to solve the problem?