应用看到的是订单表、商品表和一条条 SQL,存储引擎看到的却是页号、槽号、脏页和等待中的 I/O。两种视角之间隔着一整套物理组织:数据先编码成记录,记录装进固定大小的块,块组成文件,热点块进入缓冲池,最终由磁盘或固态硬盘保存。
我们用“星河生鲜配送”贯穿全文。平台每天接收大量订单;一份订单有多条订单行;商品名称、配送备注长度不固定;运营经常按日期汇总销售额,客服则频繁按订单号读取完整订单。这个场景既有短事务,也有大范围分析,正适合观察不同存储选择怎样改变性能与可靠性。

图:逻辑表经过记录编码、页内组织、文件布局和缓冲管理后,才成为存储设备上的块。
数据库不能只选“最快”的介质。越靠近处理器的存储通常越快、越贵、容量越小;越远离处理器,单位容量越便宜,但访问等待更长。存储引擎的任务是在这条层次上搬运数据,并保证该持久化的内容确实进入非易失介质。
处理器缓存速度最高,容量也最小,通常由硬件自动管理。数据库实现者仍会关心缓存行,因为连续访问同一列和访问互不相邻的字段,会产生完全不同的 CPU 缓存命中率。
主存可以被处理器直接寻址,是数据库执行算子、哈希表、排序区和缓冲池所在的地方。普通主存是易失的:掉电或系统崩溃后,里面尚未持久化的状态会消失。星河平台即使把当日全部订单放进内存,也不能把“内存里已经更新”当成“订单已经可靠保存”。
闪存和磁盘都是非易失的在线存储。它们通常向上提供块接口:上层按块号请求一段固定大小的数据,而不是直接操纵某个物理晶体管或磁道。闪存随机读取快,没有机械运动;磁盘单位容量便宜,仍适合大规模冷数据和连续读写。
光盘和磁带更适合归档。磁带按顺序移动到目标位置,随机取一个对象可能等待很久,但每盒介质成本低、可离线保管。数据库备份可以落到这类介质,活跃订单页却不适合放在那里。
光介质可分为出厂只读、一次写入多次读取和可反复写入等类型。一次写入介质能降低历史归档被覆盖的风险,却仍要配合校验与多副本。大型磁带库则用机械装置自动装载介质;这里的主要等待常来自取带、装带和顺序定位,而不只是传输带宽。
“非易失”只说明断电后信息仍可保留,不代表设备永不损坏。真正的持久性还需要日志、冗余、校验、备份和恢复流程共同保证。
容易混淆的地方是把“快”“可随机访问”“断电不丢”当成同一属性。主存很快且可直接寻址,却通常易失;磁带非易失,却是顺序访问;磁盘与固态硬盘都非易失且支持直接块访问,但随机访问延迟相差很大。
在工程决策中,我们至少同时比较以下指标:
设备怎样接入同样会改变延迟。SATA、SAS 让磁盘或固态盘以块设备形式连接主机;NVMe 针对并行度高、延迟低的非易失设备设计,常运行在 PCIe 之上,允许多个队列并发提交请求。
存储区域网络把远端磁盘阵列呈现为块设备,数据库仍按逻辑块读写。网络附加存储呈现的是文件系统接口,数据库看到文件、目录和文件偏移。对象存储通过 API 读写对象,跨网络访问的延迟通常远高于本机块设备,更适合备份、归档、数据湖或大对象,而不是直接承载对延迟敏感的随机页访问。
存储区域网络可以通过 iSCSI 在 IP 网络上传递块命令,也可以使用光纤通道或低延迟互连。协议不同,数据库首先要确认的仍是语义:远端暴露的是可寻址块、共享文件,还是只能整体读写的对象。语义决定一致性、锁和恢复应由哪一层负责。
接口标称带宽不是单次查询的保证。一次 8 KiB 随机读如果主要耗在排队、网络往返或设备定位上,即使链路带宽很高,响应仍可能很慢;大量连续块才能更充分地利用带宽。
机械磁盘把信息记录在旋转盘片的磁性表面。理解它的物理动作,才能解释为什么一次只读几 KiB 时,传输本身反而不是主要成本。

图:同一磁臂上的磁头一起移动;各盘面相同半径的磁道共同构成柱面。
每个盘面被划分为同心磁道,磁道再切成扇区。扇区是设备能够读写的基本物理单位之一,控制器在扇区旁保存校验信息,用来发现读取结果是否损坏。外圈周长更大,通常能容纳更多扇区。
一块设备可有多个盘片和多个磁头。磁头固定在同一磁臂组件上,会一起移动到相同半径;多个盘面上同一编号的磁道合称一个柱面。磁头离盘面很近,若发生接触,可能破坏局部记录层。
数据库和文件系统通常不直接指定磁道、扇区,而是使用逻辑块号。磁盘控制器负责把逻辑地址映射到物理位置,并可把坏扇区重映射到备用区域。这个抽象让上层容易使用设备,也意味着数据库通常不知道每个逻辑块精确落在哪条磁道。
读取目标块大致经过三段等待:磁臂移动到目标磁道的寻道时间、目标扇区转到磁头下方的旋转延迟,以及真正把字节传出的传输时间。可用下面的近似式估算:
其中 是读取字节数, 是持续传输率。若转速为 RPM,随机请求平均等待半圈,则平均旋转延迟为:
7200 RPM 的设备平均旋转延迟约为 ms。若再加 5 ms 寻道,而 8 KiB 在 150 MiB/s 下的传输只需约 ms,那么总时间几乎都花在机械定位上。

图:小块随机 I/O 中,寻道和旋转等待常远大于实际传输时间。
顺序访问先为第一个块完成定位,后续相邻块多半只需持续传输或进行很短的相邻磁道移动。随机访问则让每个小块重复支付定位成本。若一次随机访问平均需要 10 ms,设备理论上大约只能完成 100 次这样的操作,也就是约 100 IOPS;这和顺序带宽是两种不同指标。
数据库会用多种方法减少随机动作:
文件若经历许多零散追加,原本连续的区段会逐渐散开,顺序扫描不得不在区段之间重新定位。重写文件、备份后连续恢复或专门的碎片整理可以重新聚合区段,但都会搬动大量数据,必须评估维护窗口和额外写入。
磁盘可靠性常用平均无故障时间衡量,但它是对设备群体故障概率的统计,不是一块盘可以连续工作多少年的承诺。假设单盘平均无故障时间为 ,由 块相同设备组成且故障相互独立的系统,首次出现某盘故障的平均时间可粗略看成 。设备越多,“总有一块出故障”就越常见,这也是大规模存储必须配置冗余、巡检和备件的原因。
固态硬盘没有磁臂和旋转盘片,因此随机读取延迟远低于机械磁盘,也能并行处理多个队列中的请求。但 NAND 闪存并不是“更快的磁盘材料”,它有自己的写入规则:按页读取和写入,按更大的擦除块清空,已写页不能直接原地覆盖。

图:更新先写入空闲物理页,旧页标记失效;垃圾回收搬走有效页后再擦除整块。
闪存读写单位通常是页,擦除单位却包含许多页。更新逻辑页 42 时,控制器可以把新版本写到一个已经擦净的空闲物理页,再把旧物理页标记为无效。等某个擦除块积累了较多无效页,垃圾回收会先复制仍有效的页,再擦除整个块。
应用只提交了一页更新,设备内部却可能发生有效页复制、映射更新和整块擦除。内部写入量与主机请求写入量之比叫写放大。空闲空间越紧张、数据冷热混在同一擦除块中,垃圾回收越可能频繁搬运有效页。
闪存转换层维护“逻辑页号 → 物理页号”的映射。常用映射保留在内存中,物理页的附加区域也记录必要信息,以便重建。这个层让上层继续使用熟悉的块接口,却把不可原地覆盖、坏页处理和垃圾回收藏在设备内部。
物理单元能承受的擦除次数有限。磨损均衡会避免少数块被反复擦写:更新频繁的热数据尽量使用磨损较少的块,长期不变的冷数据可以迁到磨损较多的块。控制器还会淘汰损坏页,并使用备用空间维持逻辑容量。
固态盘的随机读性能常用队列深度下的 IOPS 表达。队列深度为 1 反映单请求串行行为,更高队列深度可利用内部通道并行。写入性能还会受垃圾回收、缓存耗尽、可用预留空间和磨损状态影响,所以短时间峰值与长时间稳态可能差别很大。
混合存储会把热点或只读数据放在固态盘,把大容量冷数据放在磁盘。星河平台可以把近 30 天订单和索引放在固态盘,把多年历史明细迁到磁盘;但只有访问模式稳定时,这种分层才真正省钱。
一块设备的容量和性能有限,也会故障。把数据分散到多块设备可以并行读写;加入镜像或校验信息,还能在部分设备失效时重建数据。RAID 是这些组织方式的统称。
镜像把每个逻辑块写到两个物理设备。读取可从任一副本完成,单盘损坏后仍能读取另一份;代价是容量大约减半,每次数据写入要落到两个副本。
块级条带把连续逻辑块轮流放到 块设备。逻辑块 可映射到设备 ,设备内块号为 。大范围读取可同时从多盘取块,小随机请求也能分散到不同设备。只有条带而没有冗余时,任何一盘损坏都会使部分逻辑数据丢失。
早期方案也把一个字节的各个位分散到多盘,使一次访问由全部设备共同传输。它能提高单次大传输速率,却让每个小请求占用整个阵列,无法让多块盘并行服务不同请求。块级条带既能并行大读,也能把独立小读分给不同设备,因此更实用。
单校验可用逐位异或生成:
任意一个数据块丢失,都可由其余数据块和 再次异或还原。校验块必须与它保护的数据块放在不同设备上,否则同一设备故障会同时带走数据和恢复信息。
只更新一块数据时,不必重新读取整条带。若旧数据为 、新数据为 、旧校验为 ,新校验可以增量计算:
镜像的数据丢失时间还与修复速度有关。在理想的独立故障近似下,若单盘平均无故障时间为 、平均修复时间为 ,双副本镜像的数据丢失平均时间量级可估成:
这个估算会被共同电源故障、同批次老化、机房灾害和重建压力削弱,所以不能把“独立故障”当成现实保证。
RAID 5 更新一个小数据块时,常见“读—改—写”路径要读取旧数据和旧校验,再写新数据和新校验,合计两读两写。整条带顺序写则可直接从新数据计算校验,避免读旧值,所以小随机写和大顺序写的代价差别很大。
若所有校验都集中在一块设备上,该设备会成为随机写热点;分布式校验把不同条带的校验轮流放到各盘。相同思想也会用在固态硬盘内部的闪存页、磁带阵列或分片传输中:只要额外冗余单元与数据单元相互独立,就能用幸存单元重建缺失内容。
可靠性不能只看“允许坏几块盘”。设备失效后,阵列要读取幸存设备并重建到新盘。容量越大、负载越高,重建越久;重建期间的性能会下降,幸存设备还承受更大读取压力。
潜伏扇区错误是另一类风险:某个扇区早已不可读,却直到重建时才被发现。定期巡检会在空闲时读出所有扇区,利用冗余修复坏数据。热备盘能在故障后立即开始重建;热插拔缩短更换等待。电源、控制器、链路若只有一份,仍可能成为单点故障。
硬件 RAID 可用非易失内存记录未完成写入,掉电后继续完成;软件 RAID 也能实现冗余,但必须有明确的一致性检测和恢复流程。RAID 保护设备故障,不会自动防住误删、逻辑错误、恶意加密或机房级灾难,所以不能替代备份。
持久设备按块传输,关系模型却按记录工作。数据库通常借助操作系统文件保存块,同时自己保留页级视角,以便控制记录布局、缓冲和恢复。常见页大小是几 KiB 到十几 KiB,一页可容纳多条记录。

图:定长记录按固定槽位排列;变长记录用固定头、偏移长度对和槽目录完成定位。
假设订单状态记录固定为 80 字节,页大小为 4096 字节,页头占 96 字节。单页最多可放:
条记录。剩余空间不足以容纳完整记录时保持不用,从而避免一条记录跨两页。这样读一条记录最多只需取一个页,缓冲管理也更简单。
删除记录可以把后续所有记录向前搬,也可以把最后一条记录填入空位,但两者都会改变记录位置。更常见的基础办法是让被删槽位组成空闲链表:文件头指向第一个空槽,每个空槽再指向下一个。插入优先复用链头,链空时才扩展文件。
订单中的收货人、地址、备注都是变长字段。记录可分为两部分:前部保持固定形状,保存定长值、变长字段的“偏移 + 长度”对和空值位图;后部连续保存各变长字段的字节。
若某字段为空,空值位图对应位设为 1。实现可以保留该字段的固定位置并忽略其内容,也可以完全省掉空字段的元信息和负载;后者更省空间,但读取第 个属性时需要额外解析前面的位。
记录内部的偏移量通常相对于记录起点,而不是内存绝对地址。记录从磁盘进入不同缓冲帧后,内部结构仍可解释。
槽页页头保存槽数、连续空闲区边界,以及每个槽的记录偏移和长度。槽目录从页头向后增长,记录从页尾向前增长,中间是连续空闲空间。
插入时在记录区分配字节,并新增或复用槽条目。删除时把槽标为无效;数据库可移动其他记录消除碎片,只需更新槽目录中的偏移。外部引用使用“页号 + 槽号”,而不是记录的字节地址,所以页内整理不会使引用失效。
槽号稳定不等于记录永远留在同一页。若更新后的记录在本页放不下,系统可能采用转发指针、行迁移或版本链。任何实现都要明确外部记录标识在这些变化下怎样保持可追踪。
商品图片、签收视频或电子合同可能远大于一个页。常见做法是在主记录里保存逻辑定位符,把对象内容放在受数据库管理的独立文件或分块结构中。分块树结构可以支持整对象顺序读,也能定位某段字节。
把对象放到普通文件或对象存储能减小数据库备份,但应用必须处理一致性:数据库中不能长期留下指向不存在对象的路径,删除对象也不能绕过授权。若数据库与外部存储没有共同事务,通常需要状态机、延迟删除和定期对账。
记录装进页之后,还要决定整张表的页和记录怎样排列。没有一种文件组织同时让点查、范围扫描、插入、删除和多表连接都最优,选择必须服从主要负载。

图:同一批订单记录采用不同文件组织后,插入位置与读取路径不同。
堆文件不要求记录按键排序,插入可放到任何有足够空间的页。若每次都逐页读取来找空位,代价很高,因此系统维护空闲空间图:每个页对应一个近似空闲比例。
大型文件会把空闲空间图做成多级结构。上层条目保存一组下层页的最大空闲等级,插入先在上层找到“可能容得下”的分组,再检查少量下层条目。空闲图可延迟落盘,因此它允许保守误差:某页实际已满却被标为空闲时,读页后再换一个;某页实际有空位却被标满,最多暂时浪费空间。系统可周期扫描校正。
堆文件插入快、全表扫描直接,但没有索引时按订单号点查必须扫描许多页。实际系统常用“堆表 + 独立索引”,由索引返回记录标识。
顺序文件按检索键尽量物理排序。检索键不一定唯一,也不一定是主键。若星河平台按下单时间排列订单,时间范围报表可以连续读取相邻页。
插入中间键值时,移动后面所有记录代价太大。系统可把新记录放到当前页空槽,或放入溢出块,再用指针保持逻辑顺序。溢出记录越多,物理顺序和逻辑顺序越分离,范围扫描就会产生更多跳转;因此要在低负载时重组文件。
B+ 树组织把有序访问和动态分裂结合起来,更适合持续插入的有序负载。它的代价是维护树结构与页面填充率。
散列文件对订单号等检索键计算散列值,并据此选择桶或页。分布均匀时,等值查找可直接跳到很少的页;范围条件却无法利用散列顺序,因为相邻键值会散到不同位置。
桶满时需要溢出页,数据增长还会带来扩容问题。静态散列若一开始桶数不合适,要么浪费空间,要么形成长溢出链;可扩展散列会用目录或逐步分裂缓解这个问题。
通常一张表使用自己的文件。若“订单头 + 订单行”几乎总被一起读取,可以按订单号把两种记录放进同一页或相邻页,这就是多表聚簇。一次读页可能同时拿到订单地址、总额和全部明细,减少连接中的随机 I/O。
代价也很明确:只扫描订单头时,每页夹杂许多订单行,能容纳的订单头更少;某个超大订单还可能溢出到其他页。是否聚簇,要看稳定的高频查询,而不是表之间是否存在外键。
按月份分区后,查询“本月订单”只需扫描当前分区,旧分区可以放到容量更便宜的磁盘,新分区放在固态盘。插入找空位、重建索引、备份和清理历史数据也能以较小分区为单位。
分区裁剪依赖查询条件能推导出分区键范围。若报表完全不带日期条件,系统仍要访问全部相关分区。分区也不是索引的替代品:它先减少候选文件,分区内部仍可能需要索引或顺序扫描。
数据库不仅保存订单,还要保存“订单表有哪些列、各列是什么类型、文件在哪里、有哪些索引和权限”。这些描述数据的数据叫元数据,集中保存在数据字典或系统目录中。
一套可工作的目录通常包括:
解析“按订单号查配送地址”时,系统先确认关系和列存在、类型兼容,再查权限、可用索引、统计信息和物理位置。目录若每次都从慢速设备读取,所有 SQL 都会被拖慢,因此热点元数据通常在启动时进入内存结构。
关系数据库可以把元数据本身也存成系统关系。这样目录查询复用已有的索引、事务和查询机制。为了减少启动和解析路径上的连接次数,目录表有时会保留非规范化字段,例如把索引列列表编码在一条元数据记录里;这是有明确读路径目标的内部取舍。
这里会遇到自举问题:要找到“关系元数据表”,似乎先要查关系元数据表。解决办法是把最核心目录的格式和入口页写在固定位置或内核代码中。系统先用这个最小入口加载基础目录,再由目录找到其余对象。
数据字典不是给管理员看的附属文档,而是解析器、权限检查器、优化器和存储管理器共同依赖的运行时数据。修改目录必须通过受支持的 DDL 或系统接口,直接改内部记录可能破坏自举和缓存一致性。
处理器只能直接操作内存中的数据。缓冲池在主存中保存持久页的副本,缓冲管理器负责查找、装入、替换和刷回。缓冲帧是内存中的容器,页是被装入其中的持久数据单位;两者不能混为一谈。

图:页表把页号映射到缓冲帧;只有未钉住的帧可被替换,脏页淘汰前需要受恢复协议约束地刷回。
请求页 时,缓冲管理器先查内存页表。若命中,直接返回帧地址;若未命中,选择空闲帧或淘汰候选,把 从存储设备读入。
内存页被修改后标记为脏页,表示它比持久副本新。淘汰干净页可以直接复用帧;淘汰脏页通常先要刷回,但何时允许刷回受日志与恢复规则约束。后台写进程会提前刷出部分脏页,避免请求急需帧时才同步等待。
强制输出要求某个页在特定时点前落盘。事务提交并不一定强制所有数据页立即落盘;常见恢复协议会先保证必要日志持久,再允许数据页稍后刷出。这里的关键是顺序和可恢复性,而不是“所有脏页越快写越好”。
进程使用某帧时会钉住页面,钉住计数大于 0 的帧不能淘汰。多个读者可分别增加计数;全部解除钉住后,页面才重新成为候选。若代码忘记解除,最终所有帧都可能被钉住,新的未命中请求无帧可用。
钉住解决“页面会不会突然被换走”,页级共享/排他闩解决“页面内容会不会在读取时被改乱”。读前获得共享闩,多个读者可并存;修改页内结构前获得排他闩,此时不能有其他读写者。一般先钉住,再加闩;释放闩后再解除钉住。
最近最少使用策略淘汰最久未访问的未钉住页,利用时间局部性。它对重复热点有效,但顺序扫描一个大于缓冲池的文件时,扫描页会逐步挤出真正的热点页,随后自己也不再使用。
数据库知道执行计划,能做得更有针对性:
没有一种策略对所有负载最好。现代实现常用近似 LRU、时钟、访问频率、扫描识别和多缓冲池组合,目标是减少真正昂贵的未来 I/O。
缓冲池和文件系统都可能把写操作延后并重排。如果元数据指针先落盘,而它指向的新节点尚未落盘,崩溃后指针就会指向垃圾内容。日志化文件系统先顺序记录“要把什么写到哪个块”,实际位置可稍后更新;重启后重放尚未完成的写入。
数据库也维护自己的日志,因为普通文件系统日志通常只保证文件系统元数据完整,未必理解事务的提交与回滚。非易失写缓冲可以先按要求接受写入,再优化真正的设备顺序;易失缓存若谎报完成,则会破坏恢复假设。
行式存储把一条订单的各列放在一起;列式存储把同一列的连续值放在一起。选择的关键不是“哪种更新”,而是一次查询到底取少量行的很多列,还是取大量行的少数列。

图:客服点查需要重建完整订单,行存路径短;销售汇总只读金额与日期,列存可跳过无关列并向量计算。
运营计算“按日汇总实付金额”时,只需要下单日期、状态和金额。行存仍会把地址、电话、备注等无关字段读进内存;列存只取所需列,因此减少 I/O。
同类型值连续排列还有三项收益:
按订单号读取一条完整订单时,列存要从多个列片段取同一位置的值并重建行。单行更新还可能改动多个列文件;若列段以大块压缩,修改一条记录可能要重写压缩单元。
压缩也不是免费午餐。点查若必须从段首解压到目标位置,会读取许多无关值。工程格式会把数据划成条带或行组,为每组保存起始位置、最小/最大值、计数等索引。查询可从目标组开始解压,也能根据统计跳过不可能命中的组。
ORC 与 Parquet 都采用这类分组思路。以条带式布局为例,一批行先拆成各列的数据流,列数据分别压缩;条带内的索引记录每个行组在列流中的位置和必要统计,条带尾与文件尾再保存结构元数据。查询若判断某行组的金额范围不可能满足条件,就能同时跳过这一组在多个列流中的字节,而不必逐值解压。
系统不必在“整表纯行存”和“每列单独一个文件”之间二选一。常见折中包括:
星河平台的客服接口适合行存订单主表,运营历史明细适合列存;若要一套系统同时处理两类负载,应测量行重建、写入合并和数据新鲜度的总成本。
如果数据库在可预见的增长后仍能完整放进主存,最简单的办法是给传统缓冲池更多内存。但真正的内存数据库会进一步移除“页是否在缓冲池”的检查,并围绕直接指针、缓存行和无磁盘读路径重新组织数据。
磁盘型记录指针通常是“页号 + 槽号”。解引用时先查页表,确认页面在哪个缓冲帧;未命中还要装入。数据始终驻留内存后,只要对象不移动,就可以保存内存直接指针,一次指针跳转即可到达记录。
这也解释了为什么一些内存数据库不使用会频繁移动记录的槽页,而是单独分配记录并保持地址稳定。变长对象反复插入和删除会造成内存碎片,系统需要尺寸分级分配器、区域分配、版本回收或阶段性压缩。若压缩会移动对象,就必须更新间接表或在安全时点修正引用。
内存中的一列若必须保持一整段连续数组,每次追加到容量末尾都可能重新分配并复制全部数据。更实用的办法是把逻辑列拆成多个固定大小物理数组,再由间接表记录各段起点。
查找第 个值时,先计算它属于哪一段,再在段内计算偏移。多一次间接访问换来了便宜的扩容,也允许不同段独立压缩或回收。段大小要在缓存局部性、追加成本和元数据开销之间平衡。
“所有活动数据都在内存”不等于“只保存在内存”。系统仍可用日志、检查点、复制或持久内存恢复崩溃前状态。直接字节寻址的持久内存还要考虑 CPU 缓存是否已刷新、写入次序是否被处理器重排,以及崩溃时哪些结构处于中间状态。
星河平台若把实时库存放在内存数据库中,应同时回答:断电后从哪里恢复、日志落后多少、恢复需要多久、容量增长是否越界。只有访问路径和恢复路径都成立,内存常驻才是完整方案。
物理设计没有脱离负载的“最佳结构”。我们可以沿一条可验证的链路做决定:先统计访问形状,再确定记录与页布局,接着选择文件组织、介质和冗余,最后用缓冲与恢复策略把它们连起来。
对每条高频或高代价路径记录这些信息:按什么键过滤、返回多少行、读取哪些列、是否排序、更新比例、可接受延迟、故障后允许丢多少数据。客服按订单号点查与运营按月聚合,即使访问同一批订单,也会导出不同布局。
记录平均长度、页头和槽目录决定单页可容纳多少行。过滤选择率与聚簇程度决定要读多少页。机械盘要区分随机 IOPS 与连续带宽;固态盘要区分队列深度、读写比例和稳态写入;RAID 要把校验写、降级读和重建算进去。
基准测试不应只测正常状态。还要模拟脏页刷写、缓存冷启动、单盘故障、阵列重建、日志回放和目录恢复。能跑得快但无法说明崩溃后状态的方案,不能承载已提交订单。
下面是一份简洁的落地检查: