大型30亿关系数据库的SQL数据库模式设计

时间:2010-12-28 19:18:53

标签: sql performance

让你的怪人。你能解决这个问题吗?

我正在为SQL Server 2008 R2 Ed设计产品数据库。 (不是Enterprise Ed。)将用于存储超过30,000种不同产品的定制产品配置。该数据库一次最多可包含500个用户。

这是设计问题......

每个产品都有一系列零件(每个产品最多50个零件) 因此,如果我有30,000个产品,并且每个产品最多可以有50个零件,那就是150万个不同的产品到零件关系

                                  …or as an equation…

30,000(产品)X 50(零件)= 150万件产品到零件的记录。

......如果......

每个零件最多可以有2000个饰面选项(饰面是油漆颜色)。

注意:用户在运行时只会选择一个完成。我需要存储的2000个完成选项是特定产品特定部分的允许选项。

因此,如果我有150万个不同的产品到零件的关系/记录,并且每个零件都可以有多达2,000个完成,即30亿个允许的产品到零件,以完成关系/记录

                                  …or as an equation…

150万(零件)x 2,000(完成)= 30亿件产品到零件的完成记录。

如何设计此数据库,以便我可以为特定产品执行快速有效的查询,并返回其零件清单和每个零件的所有允许的完成,而不需要30亿产品到部分完成记录?读取时间比写入时间更重要。

如果您有大型数据库的经验,请发表您的想法/建议。

谢谢!

5 个答案:

答案 0 :(得分:6)

为什么这甚至是极具挑战性的?如果关系数据库擅长一件事,那就是完全你描述的问题:3个表和2个多对多关系。只有一些失控的完整卡特兹联盟才能运行,才会出现“30亿”的数字。只需进行基本的标准化设计:

:setvar dbname test
:setvar PRODUCTSCOUNT 30000
:setvar PARTSCOUNT 5000
:setvar FINISHESCOUNT 2000
:setvar PRODUCTSPARTS 50
:setvar PARTFINISHES 1

use master;
set nocount on;
go

rollback
go

:on error exit

if db_id('$(dbname)') is not null
begin
    alter database [$(dbname)] set single_user with rollback immediate;
    drop database [$(dbname)];
end 
go

create database [$(dbname)] 
    on (name = test_data, filename='c:\temp\test.mdf', size = 10GB)
    log on (name = test_log, filename='c:\temp\test.ldf', size = 100MB);
go

use [$(dbname)];
go

create table Products (
    Product_Id int not null identity(0,1) primary key,
    Description varchar(256));
go      

create table Parts (
    Part_Id int not null identity(0,1) primary key,
    Description varchar(256));

create table Finishes (
    Finish_Id smallint not null identity(0,1) primary key,
    Description varchar(256));

create table ProductParts (
    Product_Id int not null,
    Part_Id int not null,
    constraint fk_products_parts_product
        foreign key (Product_Id)
        references Products (Product_Id),
    constraint fk_product_parts_part 
        foreign key (Part_Id)
        references Parts (Part_Id),
    constraint pk_product_parts
        primary key (Product_Id, Part_Id));

create table PartFinishes (
    Part_Id int not null,
    Finish_Id smallint not null,
    constraint fk_part_finishes_part
        foreign key (Part_Id)
        references Parts (Part_Id),
    constraint fk_part_finishes_finish
        foreign key (Finish_Id)
        references Finishes (Finish_Id),
    constraint pk_part_finishes
        primary key (Part_Id, Finish_Id));
go      

-- populate Products
declare @cnt int = 0, @description varchar(256);
begin transaction;
while @cnt < $(PRODUCTSCOUNT)
begin
    set @description = 'Product ' + cast(@cnt as varchar(10));
    insert into Products (Description) values (@description);
    set @cnt += 1;
    if @cnt % 1000 = 0
    begin
        commit;
        raiserror (N'Inserted %d products', 0,1, @cnt);
        begin transaction;
    end
end
commit;
raiserror (N'Done. %d products', 0,1, @cnt);
go

-- populate Parts
declare @cnt int = 0, @description varchar(256);
begin transaction;
while @cnt < $(PARTSCOUNT)
begin
    set @description = 'Part ' + cast(@cnt as varchar(10));
    insert into Parts (Description) values (@description);
    set @cnt += 1;
    if @cnt % 1000 = 0
    begin
        commit;
        raiserror (N'Inserted %d parts', 0,1, @cnt);
        begin transaction;
    end
end
commit;
raiserror (N'Done. %d parts', 0,1, @cnt);
go

-- populate Finishes
declare @cnt int = 0, @description varchar(256);
begin transaction;
while @cnt < $(FINISHESCOUNT)
begin
    set @description = 'Finish ' + cast(@cnt as varchar(10));
    insert into Finishes (Description) values (@description);
    set @cnt += 1;
    if @cnt % 1000 = 0
    begin
        commit;
        raiserror (N'Inserted %d finishes', 0,1, @cnt);
        begin transaction;
    end
end
raiserror (N'Done. %d finishes', 0,1, @cnt);
commit;
go

-- populate product parts
declare @cnt int = 0, @parts int = 0, @part int, @product int = 0;
begin transaction;
while @product < $(PRODUCTSCOUNT)
begin
    set @parts = rand() * ($(PRODUCTSPARTS)-1) + 1;
    set @part = rand() * $(PARTSCOUNT);     
    while 0 < @parts 
    begin
        insert into ProductParts (Product_Id, Part_Id)
            values (@product, @part);
        set @parts -= 1;
        set @part += rand()*10+1;
        if @part >= $(PARTSCOUNT)
            set @part = rand()*10;
        set @cnt += 1;
        if @cnt % 1000 = 0
        begin
            commit;
            raiserror (N'Inserted %d product-parts', 0,1, @cnt);
            begin transaction;
        end
    end
    set @product += 1;
end
commit;
raiserror (N'Done. %d product-parts', 0,1, @cnt);
go      

-- populate part finishes
declare @cnt int = 0, @part int = 0, @finish int, @finishes int;
begin transaction;
while @part < $(PARTSCOUNT)
begin
    set @finishes = rand() * ($(PARTFINISHES)-1) + 1;
    set @finish = rand() * $(FINISHESCOUNT);
    while 0 < @finishes 
    begin
        insert into PartFinishes (Part_Id, Finish_Id)
            values (@part, @finish);
        set @finish += rand()*10+1;
        if @finish >= $(FINISHESCOUNT)
            set @finish = rand()*10+1;
        set @finishes -= 1;
        set @cnt += 1;
        if @cnt % 1000 = 0
        begin
            commit;
            raiserror (N'Inserted %d part-finishes', 0,1, @cnt);
            begin transaction;
        end
    end
    set @part += 1;
end
commit;
raiserror (N'done. %d part-finishes', 0,1, @cnt);
go

现在,如果我们通过基本测试运行,结果非常好:

set statistics time on;
set statistics io on;

declare @product int = rand()*30000;
select *
from Products po
join ProductParts pp on po.Product_Id = pp.Product_Id
join Parts pa on pa.Part_Id = pp.Part_Id
join PartFinishes pf on pf.Part_Id = pa.Part_Id
join Finishes f on pf.Finish_id = f.Finish_Id
where po.Product_Id = @product;

执行时间:

(33 row(s) affected)
Table 'Finishes'. Scan count 0, logical reads 66, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Parts'. Scan count 0, logical reads 66, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'PartFinishes'. Scan count 33, logical reads 66, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'ProductParts'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Products'. Scan count 0, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 5 ms.

这是随机产品的5ms执行时间。这远不是“服务器”,我在笔记本电脑上运行它。没有惊喜,所有访问都被这些表上的聚簇索引所覆盖。我会让你为500个用户设置一个压力测试,并自己测量它在并发下的表现。我希望它能很好地保持下去。

答案 1 :(得分:3)

首先,30亿是一个上限。最有可能的是,现实世界的组合会少得多。那说......

这里最重要的是拥有良好的索引。

第二件事是在服务器(和cpu power)中有足够的ram来处理你可能正在进行的查询类型。

那么,您的查询会是什么样的?

我的猜测是,您的产品将以某种方式进行分组/分类。

如果这是一个订购系统,那么这意味着该级别的查询可能一次只能返回几百个产品。

选择产品后,您将加载所选产品的关联部件等内容。同样,这将导致每个产品返回的记录少于50条。相当小。完成类型的数据量也不是很大。

即使这只是一个参考系统,任何一个查询中使用的数据量都不是很大。

我们真正留下的只是物理存储和RAM。物理存储必须足够大才能存储数据。可能大约1GB左右;这仍然很小。

对于RAM,您需要足够让SQL服务器将相关表保留在内存中。如果物理尺寸大约是正确的那么我会说8GB系统可能很好,可能是四处理器,具体取决于负载。它们便宜,所以有两个。

您提到了500个用户,但这些用户的工作负载类型是什么?它们是否一直在同时进行?他们多久查询一次服务器?他们一次需要多少数据?

这些问题将引导您确定数据库硬件需要支持的每秒查询的实际数量(以及类型)。

作为旁注,您的计算方法已经过时了。例如,您不应将完成选项的总数乘以产品/部件的总数。我严重怀疑是否存在2000种涂料颜色选择的任何部分。

计算出来的一个更好的方法是看看一个零件的精加工选项的MEAN数量乘以给定产品的MEAN零件数量。然后,您将更加了解可能的组合数量。但这只是一个无用的数据点,因为无论如何,这个数字确实没什么意义......

答案 2 :(得分:0)

两件事:

  1. 索引要查询的列。
  2. 如果像80:20 :: read:write那样使用缓存,例如分布式缓存

答案 3 :(得分:0)

另一种方法是使用对象关系或嵌套关系,以便构成程序集的部件的id包含在程序集记录中,而不是通过中间表链接到程序集。

答案 4 :(得分:0)

@Remus设计将运行良好,但您需要在某些列上分区表。它将加快查询速度。如果它在分区上,您可以使用switch语句轻松归档。

即使您在所有过程中都有索引查找,但对于没有分区的大量行,它仍然会更慢。您可以使用产品代码或基于某些命名约定告诉您这是新产品或旧产品。您可以在查询中包含此分区列,但速度很快。

将大量数据导入这些表也是个问题。您需要考虑在插入和计时之后要重建哪些索引。如果将数据添加到新分区,则可以接受性能。