【问题标题】:Retrieve >901 rows from SQL Server 2008 linked server to Active Directory从 SQL Server 2008 链接服务器到 Active Directory 检索 >901 行
【发布时间】:2011-04-14 09:48:36
【问题描述】:

在 SQL Server 2008(版本 10.0.4000)中,我创建了一个连接到 Active Directory 服务器的链接服务器。

这个查询:

select  TOP 901 *
from  openquery(adsisca, '
select  givenName,
                sn,
                sAMAccountName          
from    ''LDAP://10.1.2.3:389''
where   objectCategory = ''Person''
        and
        objectClass = ''InetOrgPerson''
')

有效。

但是更改查询并尝试检索 902 行不会:

select  TOP 902 *
    from  openquery(adsisca, '
    select  givenName,
                    sn,
                    sAMAccountName          
    from    ''LDAP://10.1.2.3:389''
    where   objectCategory = ''Person''
            and
            objectClass = ''InetOrgPerson''
    ')

错误是:

消息 7330,第 16 级,状态 2,第 1 行 无法从 OLE DB 获取行 链接的提供程序“ADSDSOObject” 服务器“adsisca”。

我发现其他人在论坛上讨论同样的问题,但他们从未修复它,只是围绕它编写多个视图并将它们联合在一起。

是否有更优雅的修复方法,是否可以更改某个设置以检索超过 901 行?

【问题讨论】:

  • 我不知道是否有链接,但在当前的搜索请求中,Active-Directory 从不回答超过 1000 条记录,并且由于数量的原因,可能存在更严格的值。从这个角度来看,目录并不像数据库。

标签: sql-server-2008 active-directory linked-server


【解决方案1】:

使用联合来规避限制..

像这样:

select  TOP 901 *
from  openquery(adsisca, '
select  givenName,
                sn,
                sAMAccountName          
from    ''LDAP://10.1.2.3:389''
where   objectCategory = ''Person''
        and
        objectClass = ''InetOrgPerson''
        and
        sAMAccountName < ''m''
')
union
select  TOP 901 *
from  openquery(adsisca, '
select  givenName,
                sn,
                sAMAccountName          
from    ''LDAP://10.1.2.3:389''
where   objectCategory = ''Person''
        and
        objectClass = ''InetOrgPerson''
        and
        sAMAccountName >= ''m''
')

【讨论】:

    【解决方案2】:

    我知道这是一篇旧帖子,但我也有同样的问题,并检查了上面提出的解决方案。 (基本上使用一堆较小的选择和不断变化的标准来保持行倒计时)我只是剪切了一个稍微不同的版本,并将它们全部合并到一个 Db 视图中。我无法为 MaxPageSize 的事情烦恼 - 它看起来太费劲了。

    IF NOT EXISTS(SELECT 1 FROM sys.servers WHERE name = 'ADSI') 
        EXEC sp_addlinkedserver 'ADSI', 'Active Directory Services 2.5', 'ADSDSOObject', 'adsdatasource'
    
    -- Create a database view from unions of smaller selects. The max 901 records thing in AD forces us to do this.
    DECLARE @queryFormat VARCHAR(MAX) = '
    SELECT * FROM OPENQUERY(ADSI, ''
        SELECT userPrincipalName, samAccountName, telephoneNumber, mail, middleName, givenName, sn, displayName, distinguishedName
        FROM ''''LDAP://OU=Users, OU=ABC, DC=XYZ, DC=local''''
        WHERE objectClass = ''''User'''' AND objectCategory = ''''Person'''' AND samAccountName = ''''#p0'''''')';
    
    DECLARE @sql VARCHAR(MAX) = 'CREATE VIEW [AdView] AS ';
    DECLARE @asciiValue INT = ASCII('A');
    DECLARE @asciiEnd INT = ASCII('Z');
    WHILE @asciiValue <= @asciiEnd BEGIN 
        SET @sql = @sql + replace(@queryFormat, '#p0', CHAR(@asciiValue) + '*');
        IF @asciiValue < @asciiEnd  SET @sql = @sql + ' UNION ALL ';
        SET @asciiValue = @asciiValue + 1;
    END
    --PRINT @sql;
    
    -- the 'live' view of the active directory data.
    IF OBJECT_ID('[AdView]') IS NOT NULL DROP VIEW [AdView]
    EXEC(@sql);
    
    -- a 'snapshot' of the active directory data, for faster selects. you could execute this on a schedule to keep up to date.
    IF OBJECT_ID('[AdTable]', 'U') IS NOT NULL DROP TABLE [AdTable]
    SELECT * INTO [AdTable] FROM [AdView]
    

    【讨论】:

    • 在 2008 年对我来说工作就像一个魅力。
    【解决方案3】:

    您需要更改 Active Directory 中的 MaxPageSize 设置。为此,您需要使用可以在运行命令中键入的 Ntdsutil.exe,然后按照以下步骤操作

    1. 在 Ntdsutil.exe 命令提示符处,键入 LDAP policies,然后按 Enter。
    2. 在 LDAP 策略命令提示符处,键入 Set MaxPageSize to 2000。 -> 或任何你想要的号码
    3. 要查看更改,请输入Show Values
    4. 要保存更改,请键入Commit Changes
    5. 要退出,请输入q

    【讨论】:

    • 谢谢,我不想更改页面大小,因为它有其他含义。使用 .NET,我可以简单地告诉它页面大小是多少并且它可以工作 - 我正在寻找的实际上是一种在我的 SQL 查询中设置页面大小的方法。 (抱歉回复晚了,离开了)
    【解决方案4】:

    问题

    错误是:

    消息 7330,级别 16,状态 2,第 1 行无法从链接服务器“adsisca”的 OLE DB 提供程序“ADSDSOObject”获取行。

    我发现其他人在论坛上讨论同样的问题,但他们从未修复它,>只是解决它,编写多个视图并将它们联合在一起。

    有没有更优雅的解决方法,我可以在某处更改设置以检索超过 901 行吗?

    解决方案

    我刚刚解决了我面临的同样问题,没有进行任何 Active Directory 设置更改(并且我成功地能够从 AD 检索大约 50k 登录,并且从 AD 域获取单个登录帐户并没有丢失):

    您需要通过遍历属性的字符来解决 ADSI 查询限制。在此处查看解决方案:http://www.sqlservercentral.com/Forums/Topic231658-54-1.aspx#bm1249991

    通过将SELECT TOP 901 ... 替换为SELECT 解决了该错误。

    这个问题是我从2005年迁移到2008年的数据库后出现的,因为在SQL Server 2008中,有901行的限制,在SQL Server 2005中是1000行(不同的是我们需要写select TOP 901, SQL Server 2005 中不需要,否则程序将失败并出现错误)

    【讨论】:

      【解决方案5】:

      我不太喜欢这里发布的其他选项的味道,因为对于一个大型域,很有可能有超过 901 个以相同首字母开头的帐户 - 特别是如果您正在查看计算机帐户,这很可能遵循一些系统的命名约定,使用相同的首字母...

      我做了一些尝试,发现如果您通过 uSNCreated 订购 openquery 并将 TOP 901 子句放在外部查询上,它不会崩溃。

      所以,这是我的 SQL,它将所有活动目录对象(计算机、域控制器、用户和联系人)提取到包含 901 条记录的临时表中,并为您提供有关每个对象的一些有用信息。

      CREATE TABLE #ADData(
          Login           NVARCHAR(256)
          ,CommonName     NVARCHAR(256)
          ,GivenName      NVARCHAR(256)
          ,FamilyName     NVARCHAR(256)   
          ,DisplayName    NVARCHAR(256)
          ,Title          NVARCHAR(256)
          ,Department     NVARCHAR(256)
          ,Location       NVARCHAR(256)
          ,Info           NVARCHAR(256)
          ,LastLogin      BIGINT
          ,flags          INT
          ,Email          NVARCHAR(256)
          ,Phone          NVARCHAR(256)   
          ,Mobile         NVARCHAR(256)
          ,Quickdial      NVARCHAR(256)
          , usnCreated    INT
      )
      
      DECLARE @Query      VARCHAR (2000)
      DECLARE @Filter     VARCHAR(200)
      DECLARE @Rowcount   INT
      
      select @Filter =''
      
      WHILE ISNULL(@rowcount,901)  = 901 BEGIN
      
          SELECT @Query = '
          SELECT top 901
                  Login           = SamAccountName
                  , CommonName    = cn
                  , GivenName
                  , FamilyName    = sn    
                  , DisplayName
                  , Title
                  , Department
                  , Location      = physicalDeliveryOfficeName
                  , Info
                  , LastLogin     = CAST(LastLogon AS bigint)
                  , flags         = CAST (UserAccountControl as int)
                  , Email         = mail
                  , Phone         = telephoneNumber
                  , Mobile        = mobile
                  , QuickDial     = Pager
                  , usnCreated
              FROM OPENROWSET(''ADSDSOObject'', '''', ''
                      SELECT cn, givenName, sn, userAccountControl, lastLogon, displayName, samaccountname, 
                      title,  department, physicalDeliveryOfficeName, info, mail, telephoneNumber, mobile, pager, usncreated
                  FROM ''''LDAP://[ldap-query-string]'''' 
                  WHERE objectClass=''''Person''''
                  AND objectClass = ''''User''''
                  ' + @filter + '
                  ORDER BY usnCreated'')'             
          INSERT INTO #ADData EXEC (@Query) 
          SELECT @Rowcount = @@ROWCOUNT
          SELECT @Filter = 'and usnCreated > '+ LTRIM(STR((SELECT MAX(usnCreated) FROM #ADData)))
      
      END
      
      SELECT LOGIN            
              , CommonName    
              , GivenName
              , FamilyName
              , DisplayName
              , Title         
              , Department
              , Location      
              , Email         
              , Phone         
              , QuickDial     
              , Mobile        
              , Info          
              , Disabled      = CASE WHEN CAST (flags AS INT) & 2 > 0 THEN 'Y' ELSE NULL END 
              , Locked        = CASE WHEN CAST (flags AS INT) & 16  > 0 THEN 'Y' ELSE NULL END 
              , NoPwdExpiry   = CASE WHEN CAST (flags AS INT) & 65536  > 0 THEN 'Y' ELSE NULL END 
              , LastLogin     = CASE WHEN ISNULL(CAST (LastLogin AS BIGINT),0) = 0 THEN NULL ELSE 
                                  DATEADD(ms, (CAST (LastLogin AS BIGINT) / CAST(10000 AS BIGINT)) % 86400000,
                                  DATEADD(day, CAST (LastLogin AS BIGINT) / CAST(864000000000 AS BIGINT) - 109207, 0)) END 
              , Type = CASE WHEN flags  & 512 = 512 THEN 'user' 
                          WHEN flags IS NULL THEN 'contact' 
                          WHEN flags & 4096 = 4096 THEN 'computer'
                          WHEN flags & 532480 = 532480 THEN 'computer (DC)' END
      FROM #ADData
      ORDER BY Login
      
      DROP TABLE #ADData
      

      【讨论】:

      • uSNCreated 可能不是唯一的。是否可以改为使用objectGUID 对结果进行分区?
      【解决方案6】:

      我需要更改 Active Directory 中的 MaxTempTableSize 设置。为此,您需要使用可以在运行命令中键入的 Ntdsutil.exe,然后按照以下步骤操作

      At the Ntdsutil.exe command prompt, type LDAP policies, and then press ENTER.
      At the LDAP policy command prompt, type Set MaxTempTableSize to 2000. -> Or any number you want
      To view the changes, type Show Values
      To save the changes, typeCommit Changes
      To quit, type q
      

      【讨论】:

        【解决方案7】:

        此版本的解决方案处理以指定字符开头的用户数仍然> 901的情况。它使用一个主过程调用另一个存储过程。

        -- This procedure pulls a subset of LDAP users
        
        CREATE PROC [dbo].[Select_LDAP_Rows]
        (
            @MyChar CHAR(1)
        )
        AS
        BEGIN
        
        --DECLARE @MyChar CHAR(1) = 'A';
        
        DECLARE @queryFormat VARCHAR(MAX) = '
            SELECT * FROM OPENQUERY(ADSI, ''
                SELECT displayName, telephoneNumber, mail, mobile, facsimileTelephoneNumber
                FROM ''''LDAP://OU=PHC,dc=MyCompany,dc=org''''
                WHERE objectClass = ''''User'''' AND objectCategory = ''''Person'''' AND displayName = ''''' + @MyChar + '#p0'''''')
            ';
        
        DECLARE @sql VARCHAR(MAX) = 'CREATE VIEW [AdView] AS ';
        DECLARE @asciiValue INT = ASCII('A');
        DECLARE @asciiEnd INT = ASCII('Z');
        
        WHILE @asciiValue <= @asciiEnd BEGIN 
            SET @sql = @sql + replace(@queryFormat, '#p0', CHAR(@asciiValue) + '*');
            IF @asciiValue < @asciiEnd  SET @sql = @sql + ' UNION ALL ';
            SET @asciiValue = @asciiValue + 1;
        END
        
        --PRINT @sql;
        
        -- the 'live' view of the active directory data.
        IF OBJECT_ID('v_ADView') IS NOT NULL DROP VIEW v_ADView
        
        EXEC(@sql);
        
        -- ADTable holds a 'snapshot' of the active directory data.
        IF OBJECT_ID('[Users_AD]', 'U') IS NULL
            SELECT DisplayName, TelephoneNumber AS Phone, Mail, Mobile, FacsimileTelephoneNumber AS Fax 
            INTO [Users_AD] FROM v_ADView;
        ELSE
            INSERT INTO [Users_AD] 
            SELECT DisplayName, TelephoneNumber AS Phone, Mail, Mobile, FacsimileTelephoneNumber AS Fax 
            FROM v_ADView;
        
        END
        
        
        GO
        
        
        -- By calling Select_LDAP_Rows with a separate character each time,
        -- build up a table containing all the LDAP data for each user.
        
        ALTER PROC [dbo].[Select_LDAP_Rows_Master]
        AS
        
        BEGIN
        
        -- ADTable holds a 'snapshot' of the active directory data.
        IF OBJECT_ID('[AdTable]', 'U') IS NOT NULL DROP TABLE [AdTable];
        
        -- Create a database view from unions of smaller selects. The max 901 records thing in AD forces us to do this.
        
        DECLARE @sql VARCHAR(200)
        DECLARE @asciiValue INT = ASCII('A');
        DECLARE @asciiEnd INT = ASCII('Z');
        
        -- Create a view of the active directory data and insert to table Users_AD
        WHILE @asciiValue <= @asciiEnd BEGIN 
        
            SET @sql = 'EXEC dbo.Select_LDAP_Rows ' + CHAR(@asciiValue) + ' ';
            SET @asciiValue = @asciiValue + 1;
        
            --PRINT @sql;
        
            EXEC(@sql);
        
        END
        
        END
        

        【讨论】:

          【解决方案8】:

          我喜欢 union 选项是最好和简单的。

          选择 TOP 901 * 从 openquery(adsisca, ' 选择给定名称, 锡, sAMAccountName
          来自“LDAP://10.1.2.3:389” 其中 objectCategory = ''Person'' 和 objectClass = ''InetOrgPerson'' 和 sAMAccountName 来自“LDAP://10.1.2.3:389” 其中 objectCategory = ''Person'' 和 objectClass = ''InetOrgPerson'' 和 sAMAccountName >= ''m'' ')

          【讨论】:

            【解决方案9】:

            我非常感谢 John Sinclair 的回答,我决定采用最高形式的奉承——模仿。这是我对他的解决方案的演绎。我没有为 OpenRowSet 中的每个查询声明 ADSI LDAP 连接,而是选择了 OpenQuery 方法:

            DECLARE @DomainFQDN VARCHAR(50) = '<your.domain.FQDN>';
            
            IF OBJECT_ID('tempdb..#ADData') IS NOT NULL
              DROP TABLE #ADData;
            
            -- Query AD for all known user accounts
            CREATE TABLE #ADData(
                lanId               NVARCHAR(256),
                firstName           NVARCHAR(256),
                lastName            NVARCHAR(256),
                email               NVARCHAR(256),
                costcenter          NVARCHAR(256),  --Our AD implementation uses the optional extensionAttributes, defining 1 as cost center
                mobile              NVARCHAR(256),  --In @Query below, the name of this column is the same as the LDAP returned parameter, so no equate is applied in the query
                country             NVARCHAR(256),
                usnCreated          BIGINT         --uSNCreated is an INT64 object type
            );
            
            --Define the AD LDAP connection
            IF NOT EXISTS(SELECT 1 FROM sys.servers WHERE name = 'ADSI') 
             EXEC master.dbo.sp_addlinkedserver
                @server = N'ADSI', 
                @srvproduct = N'Active Directory Services',
                @provider = N'ADsDSOObject', 
                @datasrc = @DomainFQDN;
            
            DECLARE @Rowcount int;
            DECLARE @LastCreatedFilter VARCHAR(200) = '';
            DECLARE @ADrecordsToReturn smallint = 901;  --AD will not return more than 901 records per query (changed from 1000 at some point). You can set it to any value smaller to control the 'pagesize' of returned results
            
            --Loop mechanics:
            -- - 1st loop: @Rowcount will be NULL but we need to looping to commence, thus ISNULL function
            -- - Intermediate loops: Rowcount will equal the max number of requested records, indicating there may be more to query from AD
            --SELECT @LastCreatedFilter = 'AND usnCreated = ''''<yourvalue>'''''; --Used during debugging to iniate the loop at a certain value
            --DECLARE @TestStop int = 1;  -- @TestStop is a debug option to halt processing. It needs to be commented in or out at 3 places
            WHILE ISNULL(@Rowcount,@ADrecordsToReturn) = @ADrecordsToReturn --AND @TestStop < 4  --Un-comment the three @TestStop lines to run a reduced sample query of AD, dictated by the value provided on this line (# of loops to process before stopping)
             BEGIN
            
                DECLARE @Query VARCHAR (2000) = 
                 '
                    SELECT TOP ' + CONVERT(varchar(10),@ADrecordsToReturn) + '
                        lanId               = SamAccountName,
                        firstName           = GivenName,
                        lastName            = sn,
                        email               = mail,
                        bsbcc               = extensionAttribute1,
                        mobile,
                        country             = c,
                        usnCreated
                     FROM OpenQuery
                      (
                        ADSI,
                        ''
                            SELECT SamAccountName, GivenName, sn, mail, extensionAttribute1, mobile, c, usnCreated
                             FROM ''''LDAP://' + @DomainFQDN + '''''
                             WHERE objectCategory = ''''Person''''
                             AND objectClass = ''''user''''
                             ' + @LastCreatedFilter + '
                             ORDER BY usnCreated
                        ''
                      )
                 ';  
                INSERT INTO #ADData EXEC (@Query);
                SELECT @Rowcount = @@ROWCOUNT;
                SELECT @LastCreatedFilter = 'AND usnCreated > ' + LTRIM(STR((SELECT MAX(usnCreated) FROM #ADData)));
            --PRINT @LastCreatedFilter;  --While debugging, used to determine progress
            --SET @TestStop = @TestStop + 1;  -- @TestStop is a debug option to halt processing. It needs to be commented in or out at 3 places
             END;
            
            EXEC master.dbo.sp_dropserver 'ADSI';
            
            --Do something with the results...
            SELECT lanId, email, costcenter, mobile, country, usnCreated FROM #ADData order by lanId;
            

            【讨论】:

            • 所以我发现使用上述解决方案很奇怪:它在我的产品广告内容上运行良好,但在阅读我的测试广告时崩溃了。然后当我尝试进行故障排除时,我发现 TSQL 查询为 usnCreated 返回的值是连续的,与 Get-ADUser 返回的 usnCreated 值不匹配。
            • 事实证明,John 的解决方案在某一方面是错误的 - AD 中的 uSNCreated 是一个 INT64 值,因此需要 SQL 'BIGINT' 数据类型。我的代码已更正,并更新了更多 cmets 和调试选项
            【解决方案10】:

            基于sAMAccountName 属性的所有解决方案均无效。
            如果您需要确保收到所有结果,则必须以不同方式构建查询。
            属性sAMAccountName 可以在属性字符串的开头包含除字母/数字以外的字符。
            另一个问题是您必须添加另一个逻辑来处理在第一个位置的一个特定字符的结果给出超过 901 行的情况 - 例如由于特定的命名约定。

            我找到了另一个更精确的解决方案。 可以使用 objectguid 属性对 AD 进行“分区”。

            这个过滤器

            (&(objectclass=user)(!(objectclass=computer))
            (objectguid<=\0f\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff)
            (objectguid>=\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00))
            

            将生成具有对象 guid 属性的所有用户帐户的列表
            0x00000000000000000000000000000000 到 0x0fffffffffffffffffffffffffffffffff

            范围内的下一个查询将是 0x100000000000000000000000000000000x1fffffffffffffffffffffffffffffffff ...

            只需 16 个请求(全部联合),您就可以读取整个 AD,总共大约 14000 条记录。对于大量记录,您必须自定义边距,但这样做比使用sAMAccountName 的第 2/3/... 位置上的字符更简单。

            objectguid 的优点在于它是随机生成的,上面的分区方案为每 16 个请求中的每一个提供了非常接近的行数(在我的情况下,我有大约 8500 个用户帐户,每个请求给我 +/- 500行结果)。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-02-08
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-07-21
              相关资源
              最近更新 更多