This is an automated email from the ASF dual-hosted git repository.

gortiz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 4480f3b523d Build v1 DataTable sections in heap pages to avoid 
humongous response allocations (#19906)
4480f3b523d is described below

commit 4480f3b523d2e2f90183b3208ea4c4df31412928
Author: Gonzalo Ortiz Jaureguizar <[email protected]>
AuthorDate: Mon Oct 19 16:30:53 2026 +0200

    Build v1 DataTable sections in heap pages to avoid humongous response 
allocations (#19906)
---
 .../apache/pinot/common/datatable/DataTable.java   |   5 +
 .../pinot/common/datatable/DataTableImplV4.java    | 320 +++++++++++----------
 .../common/datatable/BaseDataTableBuilder.java     |  57 ++--
 .../core/common/datatable/DataTableBuilderV4.java  |  14 +-
 .../core/common/datatable/DataTableSerDeTest.java  |  90 ++++++
 .../segment/spi/memory/CompoundDataBuffer.java     |  27 +-
 .../segment/spi/memory/CompoundDataBufferTest.java |  26 ++
 7 files changed, 342 insertions(+), 197 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTable.java 
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTable.java
index 989b148405f..d8486e5a190 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTable.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTable.java
@@ -84,6 +84,11 @@ public interface DataTable {
   @Nullable
   Map<String, Object> getMap(int rowId, int colId);
 
+  /// Returns the custom object at the given cell, or `null` for a null value.
+  ///
+  /// The buffer of the returned object can be a read-only view, for example 
on a table built in this process. Code
+  /// that reads it, such as 
`AggregationFunction.deserializeIntermediateResult` implementations and
+  /// `ObjectSerDeUtils` deserializers, must read it through the `ByteBuffer` 
API and must not call `array()` on it.
   @Nullable
   CustomObject getCustomObject(int rowId, int colId);
 
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
index d2ab8c43b23..8b0b58cd4ba 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
@@ -34,6 +34,8 @@ import org.apache.pinot.common.datablock.DataBlockUtils;
 import org.apache.pinot.common.utils.DataSchema;
 import org.apache.pinot.common.utils.HashUtil;
 import org.apache.pinot.common.utils.RoaringBitmapUtils;
+import org.apache.pinot.segment.spi.memory.DataBuffer;
+import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
 import org.apache.pinot.spi.accounting.ThreadResourceSnapshot;
 import org.apache.pinot.spi.accounting.ThreadResourceUsageProvider;
 import org.apache.pinot.spi.query.QueryThreadContext;
@@ -88,10 +90,12 @@ public class DataTableImplV4 implements DataTable {
   protected int _fixDataSize;
   protected DataSchema _dataSchema;
   protected String[] _stringDictionary;
-  protected byte[] _fixedSizeDataBytes;
-  protected ByteBuffer _fixedSizeData;
-  protected byte[] _variableSizeDataBytes;
-  protected ByteBuffer _variableSizeData;
+  /// Both sections are read with absolute offsets only, so they can be split 
in pages (as the server builds them) or
+  /// be a single array (as the broker deserializes them).
+  @Nullable
+  protected DataBuffer _fixedSizeData;
+  @Nullable
+  protected DataBuffer _variableSizeData;
   protected Map<String, String> _metadata;
   protected int[] _columnOffsets;
   protected int _rowSizeInBytes;
@@ -102,9 +106,7 @@ public class DataTableImplV4 implements DataTable {
     _fixDataSize = 0;
     _dataSchema = null;
     _stringDictionary = null;
-    _fixedSizeDataBytes = null;
     _fixedSizeData = null;
-    _variableSizeDataBytes = null;
     _variableSizeData = null;
     _rowSizeInBytes = 0;
     _columnOffsets = null;
@@ -112,17 +114,17 @@ public class DataTableImplV4 implements DataTable {
     _errCodeToExceptionMap = new HashMap<>();
   }
 
-  public DataTableImplV4(int numRows, DataSchema dataSchema, String[] 
stringDictionary, byte[] fixedSizeDataBytes,
-      byte[] variableSizeDataBytes) {
+  /// Creates a table that reads its fixed and variable size sections from the 
given big-endian buffers, without
+  /// copying them.
+  public DataTableImplV4(int numRows, DataSchema dataSchema, String[] 
stringDictionary, DataBuffer fixedSizeData,
+      DataBuffer variableSizeData) {
     _numRows = numRows;
     _dataSchema = dataSchema;
     _numColumns = dataSchema == null ? 0 : dataSchema.size();
     _fixDataSize = 0;
     _stringDictionary = stringDictionary;
-    _fixedSizeDataBytes = fixedSizeDataBytes;
-    _fixedSizeData = ByteBuffer.wrap(fixedSizeDataBytes);
-    _variableSizeDataBytes = variableSizeDataBytes;
-    _variableSizeData = ByteBuffer.wrap(variableSizeDataBytes);
+    _fixedSizeData = fixedSizeData;
+    _variableSizeData = variableSizeData;
     _metadata = new HashMap<>();
     _errCodeToExceptionMap = new HashMap<>();
     computeBlockObjectConstants();
@@ -170,22 +172,21 @@ public class DataTableImplV4 implements DataTable {
 
     // Read fixed size data.
     if (fixedSizeDataLength != 0) {
-      _fixedSizeDataBytes = new byte[fixedSizeDataLength];
+      byte[] fixedSizeDataBytes = new byte[fixedSizeDataLength];
       byteBuffer.position(fixedSizeDataStart);
-      byteBuffer.get(_fixedSizeDataBytes);
-      _fixedSizeData = ByteBuffer.wrap(_fixedSizeDataBytes);
+      byteBuffer.get(fixedSizeDataBytes);
+      _fixedSizeData = PinotByteBuffer.wrap(fixedSizeDataBytes);
     } else {
-      _fixedSizeDataBytes = null;
       _fixedSizeData = null;
     }
 
     // Read variable size data.
-    _variableSizeDataBytes = new byte[variableSizeDataLength];
+    byte[] variableSizeDataBytes = new byte[variableSizeDataLength];
     if (variableSizeDataLength != 0) {
       byteBuffer.position(variableSizeDataStart);
-      byteBuffer.get(_variableSizeDataBytes);
+      byteBuffer.get(variableSizeDataBytes);
     }
-    _variableSizeData = ByteBuffer.wrap(_variableSizeDataBytes);
+    _variableSizeData = PinotByteBuffer.wrap(variableSizeDataBytes);
 
     // Read metadata.
     int metadataLength = byteBuffer.getInt();
@@ -243,10 +244,8 @@ public class DataTableImplV4 implements DataTable {
 
   @Override
   public BigDecimal getBigDecimal(int rowId, int colId) {
-    int size = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    ByteBuffer byteBuffer = _variableSizeData.slice();
-    byteBuffer.limit(size);
-    return BigDecimalUtils.deserialize(byteBuffer);
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    return BigDecimalUtils.deserialize(copyFromVariableBuffer(offsetInFixed));
   }
 
   @Override
@@ -256,64 +255,70 @@ public class DataTableImplV4 implements DataTable {
 
   @Override
   public ByteArray getBytes(int rowId, int colId) {
-    int size = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    byte[] buffer = new byte[size];
-    _variableSizeData.get(buffer);
-    return new ByteArray(buffer);
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    return new ByteArray(copyFromVariableBuffer(offsetInFixed));
   }
 
   // --------------------------------------------------------------------------
   // Variable sized element access.
   // --------------------------------------------------------------------------
+  // These read with absolute offsets instead of a stream: a stream over a 
paged buffer creates a view of every page
+  // up to the end of the section on each call.
 
   @Override
   public int[] getIntArray(int rowId, int colId) {
-    int length = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    int[] ints = new int[length];
-    for (int i = 0; i < length; i++) {
-      ints[i] = _variableSizeData.getInt();
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    long offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    int[] ints = new int[_fixedSizeData.getInt(offsetInFixed + Integer.BYTES)];
+    for (int i = 0; i < ints.length; i++) {
+      ints[i] = _variableSizeData.getInt(offsetInVar + (long) i * 
Integer.BYTES);
     }
     return ints;
   }
 
   @Override
   public long[] getLongArray(int rowId, int colId) {
-    int length = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    long[] longs = new long[length];
-    for (int i = 0; i < length; i++) {
-      longs[i] = _variableSizeData.getLong();
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    long offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    long[] longs = new long[_fixedSizeData.getInt(offsetInFixed + 
Integer.BYTES)];
+    for (int i = 0; i < longs.length; i++) {
+      longs[i] = _variableSizeData.getLong(offsetInVar + (long) i * 
Long.BYTES);
     }
     return longs;
   }
 
   @Override
   public float[] getFloatArray(int rowId, int colId) {
-    int length = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    float[] floats = new float[length];
-    for (int i = 0; i < length; i++) {
-      floats[i] = _variableSizeData.getFloat();
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    long offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    float[] floats = new float[_fixedSizeData.getInt(offsetInFixed + 
Integer.BYTES)];
+    for (int i = 0; i < floats.length; i++) {
+      floats[i] = _variableSizeData.getFloat(offsetInVar + (long) i * 
Float.BYTES);
     }
     return floats;
   }
 
   @Override
   public double[] getDoubleArray(int rowId, int colId) {
-    int length = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    double[] doubles = new double[length];
-    for (int i = 0; i < length; i++) {
-      doubles[i] = _variableSizeData.getDouble();
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    long offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    double[] doubles = new double[_fixedSizeData.getInt(offsetInFixed + 
Integer.BYTES)];
+    for (int i = 0; i < doubles.length; i++) {
+      doubles[i] = _variableSizeData.getDouble(offsetInVar + (long) i * 
Double.BYTES);
     }
     return doubles;
   }
 
   @Override
   public BigDecimal[] getBigDecimalArray(int rowId, int colId) {
-    int length = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    BigDecimal[] bigDecimals = new BigDecimal[length];
-    for (int i = 0; i < length; i++) {
-      int byteLength = _variableSizeData.getInt();
-      byte[] bytes = new byte[byteLength];
-      _variableSizeData.get(bytes);
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    long offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    BigDecimal[] bigDecimals = new 
BigDecimal[_fixedSizeData.getInt(offsetInFixed + Integer.BYTES)];
+    for (int i = 0; i < bigDecimals.length; i++) {
+      byte[] bytes = new byte[_variableSizeData.getInt(offsetInVar)];
+      offsetInVar += Integer.BYTES;
+      _variableSizeData.copyTo(offsetInVar, bytes, 0, bytes.length);
+      offsetInVar += bytes.length;
       bigDecimals[i] = BigDecimalUtils.deserialize(bytes);
     }
     return bigDecimals;
@@ -321,22 +326,25 @@ public class DataTableImplV4 implements DataTable {
 
   @Override
   public String[] getStringArray(int rowId, int colId) {
-    int length = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    String[] strings = new String[length];
-    for (int i = 0; i < length; i++) {
-      strings[i] = _stringDictionary[_variableSizeData.getInt()];
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    long offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    String[] strings = new String[_fixedSizeData.getInt(offsetInFixed + 
Integer.BYTES)];
+    for (int i = 0; i < strings.length; i++) {
+      strings[i] = _stringDictionary[_variableSizeData.getInt(offsetInVar + 
(long) i * Integer.BYTES)];
     }
     return strings;
   }
 
   @Override
   public ByteArray[] getBytesArray(int rowId, int colId) {
-    int length = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    ByteArray[] bytes = new ByteArray[length];
-    for (int i = 0; i < length; i++) {
-      int byteLength = _variableSizeData.getInt();
-      byte[] value = new byte[byteLength];
-      _variableSizeData.get(value);
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    long offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    ByteArray[] bytes = new ByteArray[_fixedSizeData.getInt(offsetInFixed + 
Integer.BYTES)];
+    for (int i = 0; i < bytes.length; i++) {
+      byte[] value = new byte[_variableSizeData.getInt(offsetInVar)];
+      offsetInVar += Integer.BYTES;
+      _variableSizeData.copyTo(offsetInVar, value, 0, value.length);
+      offsetInVar += value.length;
       bytes[i] = new ByteArray(value);
     }
     return bytes;
@@ -345,27 +353,27 @@ public class DataTableImplV4 implements DataTable {
   @Nullable
   @Override
   public Map<String, Object> getMap(int rowId, int colId) {
-    int size = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    if (size == 0) {
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    if (_fixedSizeData.getInt(offsetInFixed + Integer.BYTES) == 0) {
       return null;
     }
-    ByteBuffer buffer = _variableSizeData.slice();
-    buffer.limit(size);
-    return MapUtils.deserializeMap(buffer);
+    return MapUtils.deserializeMap(copyFromVariableBuffer(offsetInFixed));
   }
 
   @Nullable
   @Override
   public CustomObject getCustomObject(int rowId, int colId) {
-    int size = positionOffsetInVariableBufferAndGetLength(rowId, colId);
-    int type = _variableSizeData.getInt();
+    int offsetInFixed = getOffsetInFixedBuffer(rowId, colId);
+    int offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    int size = _fixedSizeData.getInt(offsetInFixed + Integer.BYTES);
+    int type = _variableSizeData.getInt(offsetInVar);
     if (size == 0) {
       assert type == CustomObject.NULL_TYPE_VALUE;
       return null;
     }
-    ByteBuffer buffer = _variableSizeData.slice();
-    buffer.limit(size);
-    return new CustomObject(type, buffer);
+    // A view when the value fits in one page, otherwise a copy. On a table 
built in this process the pages are
+    // read-only, so the view is read-only too. See DataTable.getCustomObject 
for the contract.
+    return new CustomObject(type, _variableSizeData.copyOrView(offsetInVar + 
Integer.BYTES, size));
   }
 
   @Nullable
@@ -373,16 +381,13 @@ public class DataTableImplV4 implements DataTable {
   public RoaringBitmap getNullRowIds(int colId) {
     // _fixedSizeData stores two ints per col's null bitmap: offset, and 
length.
     int position = _fixDataSize + colId * Integer.BYTES * 2;
-    if (_fixedSizeData == null || position >= _fixedSizeData.limit()) {
+    if (_fixedSizeData == null || position >= _fixedSizeData.size()) {
       return null;
     }
-    _fixedSizeData.position(position);
-    int offset = _fixedSizeData.getInt();
-    int bytesLength = _fixedSizeData.getInt();
+    int bytesLength = _fixedSizeData.getInt(position + Integer.BYTES);
     if (bytesLength > 0) {
-      _variableSizeData.position(offset);
       byte[] nullBitmapBytes = new byte[bytesLength];
-      _variableSizeData.get(nullBitmapBytes);
+      _variableSizeData.copyTo(_fixedSizeData.getInt(position), 
nullBitmapBytes, 0, bytesLength);
       return RoaringBitmapUtils.deserialize(nullBitmapBytes);
     } else {
       return null;
@@ -428,23 +433,44 @@ public class DataTableImplV4 implements DataTable {
     return _errCodeToExceptionMap;
   }
 
+  /// Serializes the table into one array of the exact final size, so the 
fixed and variable size sections are copied
+  /// once and no buffer grows by doubling.
   @Override
   public byte[] toBytes()
       throws IOException {
     ThreadResourceSnapshot resourceSnapshot = new ThreadResourceSnapshot();
 
-    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-    DataOutputStream dataOutputStream = new 
DataOutputStream(byteArrayOutputStream);
-    writeLeadingSections(dataOutputStream);
-
     // Add table serialization time and memory metadata when the corresponding 
measurement is enabled.
     // When CPU time/memory usage is not collectable, we omit these values 
from the metadata.
-    if (ThreadResourceUsageProvider.isThreadCpuTimeMeasurementEnabled()) {
-      getMetadata().put(MetadataKey.RESPONSE_SER_CPU_TIME_NS.getName(),
-          String.valueOf(resourceSnapshot.getCpuTimeNs()));
-    }
-    if (ThreadResourceUsageProvider.isThreadMemoryMeasurementEnabled()) {
-      getMetadata().put(MetadataKey.RESPONSE_SER_MEM_ALLOCATED_BYTES.getName(),
+    // The entries are reserved before the array is sized and filled in after 
the sections are copied. Both are LONG
+    // keys, whose encoded length does not depend on the value, so the 
metadata length does not change.
+    boolean measureCpuTime = 
ThreadResourceUsageProvider.isThreadCpuTimeMeasurementEnabled();
+    boolean measureAllocatedBytes = 
ThreadResourceUsageProvider.isThreadMemoryMeasurementEnabled();
+    if (measureCpuTime) {
+      _metadata.put(MetadataKey.RESPONSE_SER_CPU_TIME_NS.getName(), "0");
+    }
+    if (measureAllocatedBytes) {
+      _metadata.put(MetadataKey.RESPONSE_SER_MEM_ALLOCATED_BYTES.getName(), 
"0");
+    }
+
+    byte[] exceptionsBytes = serializeExceptions();
+    byte[] dictionaryBytes = _stringDictionary != null ? 
serializeStringDictionary() : null;
+    byte[] dataSchemaBytes = _dataSchema != null ? _dataSchema.toBytes() : 
null;
+    int metadataLength = serializeMetadata().length;
+    long totalLength = HEADER_SIZE + exceptionsBytes.length + 
lengthOf(dictionaryBytes) + lengthOf(dataSchemaBytes)
+        + sizeOf(_fixedSizeData) + sizeOf(_variableSizeData) + Integer.BYTES + 
metadataLength;
+    if (totalLength > Integer.MAX_VALUE) {
+      throw new IllegalStateException("Serialized data table size " + 
totalLength + " exceeds the 2 GB limit");
+    }
+    byte[] bytes = new byte[(int) totalLength];
+    ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+    writeLeadingSections(byteBuffer, exceptionsBytes, dictionaryBytes, 
dataSchemaBytes);
+
+    if (measureCpuTime) {
+      _metadata.put(MetadataKey.RESPONSE_SER_CPU_TIME_NS.getName(), 
String.valueOf(resourceSnapshot.getCpuTimeNs()));
+    }
+    if (measureAllocatedBytes) {
+      _metadata.put(MetadataKey.RESPONSE_SER_MEM_ALLOCATED_BYTES.getName(),
           String.valueOf(resourceSnapshot.getAllocatedBytes()));
     }
 
@@ -452,83 +478,74 @@ public class DataTableImplV4 implements DataTable {
     // NOTE: We ignore metadata serialization time in 
"responseSerializationCpuTimeNs" as it's negligible while
     // considering it will bring a lot code complexity.
     byte[] metadataBytes = serializeMetadata();
-    dataOutputStream.writeInt(metadataBytes.length);
-    dataOutputStream.write(metadataBytes);
+    if (metadataBytes.length != metadataLength) {
+      throw new IllegalStateException(
+          "Metadata length changed during serialization from " + 
metadataLength + " to " + metadataBytes.length);
+    }
+    byteBuffer.putInt(metadataBytes.length);
+    byteBuffer.put(metadataBytes);
+    assert !byteBuffer.hasRemaining();
+    return bytes;
+  }
 
-    return byteArrayOutputStream.toByteArray();
+  private static int lengthOf(@Nullable byte[] bytes) {
+    return bytes != null ? bytes.length : 0;
   }
 
-  private void writeLeadingSections(DataOutputStream dataOutputStream)
-      throws IOException {
-    dataOutputStream.writeInt(getVersion());
-    dataOutputStream.writeInt(_numRows);
-    dataOutputStream.writeInt(_numColumns);
+  private static long sizeOf(@Nullable DataBuffer dataBuffer) {
+    return dataBuffer != null ? dataBuffer.size() : 0;
+  }
+
+  private void writeLeadingSections(ByteBuffer byteBuffer, byte[] 
exceptionsBytes, @Nullable byte[] dictionaryBytes,
+      @Nullable byte[] dataSchemaBytes) {
+    int fixedSizeDataLength = (int) sizeOf(_fixedSizeData);
+    int variableSizeDataLength = (int) sizeOf(_variableSizeData);
+
+    byteBuffer.putInt(getVersion());
+    byteBuffer.putInt(_numRows);
+    byteBuffer.putInt(_numColumns);
     int dataOffset = HEADER_SIZE;
 
     // Write exceptions section offset(START|SIZE).
-    dataOutputStream.writeInt(dataOffset);
-    byte[] exceptionsBytes;
-    exceptionsBytes = serializeExceptions();
-    dataOutputStream.writeInt(exceptionsBytes.length);
+    byteBuffer.putInt(dataOffset);
+    byteBuffer.putInt(exceptionsBytes.length);
     dataOffset += exceptionsBytes.length;
 
     // Write dictionary map section offset(START|SIZE).
-    dataOutputStream.writeInt(dataOffset);
-    byte[] dictionaryBytes = null;
-    if (_stringDictionary != null) {
-      dictionaryBytes = serializeStringDictionary();
-      dataOutputStream.writeInt(dictionaryBytes.length);
-      dataOffset += dictionaryBytes.length;
-    } else {
-      dataOutputStream.writeInt(0);
-    }
+    byteBuffer.putInt(dataOffset);
+    byteBuffer.putInt(lengthOf(dictionaryBytes));
+    dataOffset += lengthOf(dictionaryBytes);
 
     // Write data schema section offset(START|SIZE).
-    dataOutputStream.writeInt(dataOffset);
-    byte[] dataSchemaBytes = null;
-    if (_dataSchema != null) {
-      dataSchemaBytes = _dataSchema.toBytes();
-      dataOutputStream.writeInt(dataSchemaBytes.length);
-      dataOffset += dataSchemaBytes.length;
-    } else {
-      dataOutputStream.writeInt(0);
-    }
+    byteBuffer.putInt(dataOffset);
+    byteBuffer.putInt(lengthOf(dataSchemaBytes));
+    dataOffset += lengthOf(dataSchemaBytes);
 
     // Write fixed size data section offset(START|SIZE).
-    dataOutputStream.writeInt(dataOffset);
-    if (_fixedSizeDataBytes != null) {
-      dataOutputStream.writeInt(_fixedSizeDataBytes.length);
-      dataOffset += _fixedSizeDataBytes.length;
-    } else {
-      dataOutputStream.writeInt(0);
-    }
+    byteBuffer.putInt(dataOffset);
+    byteBuffer.putInt(fixedSizeDataLength);
+    dataOffset += fixedSizeDataLength;
 
     // Write variable size data section offset(START|SIZE).
-    dataOutputStream.writeInt(dataOffset);
-    if (_variableSizeDataBytes != null) {
-      dataOutputStream.writeInt(_variableSizeDataBytes.length);
-    } else {
-      dataOutputStream.writeInt(0);
-    }
+    byteBuffer.putInt(dataOffset);
+    byteBuffer.putInt(variableSizeDataLength);
 
     // Write actual data.
-    // Write exceptions bytes.
-    dataOutputStream.write(exceptionsBytes);
-    // Write dictionary map bytes.
+    byteBuffer.put(exceptionsBytes);
     if (dictionaryBytes != null) {
-      dataOutputStream.write(dictionaryBytes);
+      byteBuffer.put(dictionaryBytes);
     }
-    // Write data schema bytes.
     if (dataSchemaBytes != null) {
-      dataOutputStream.write(dataSchemaBytes);
+      byteBuffer.put(dataSchemaBytes);
     }
-    // Write fixed size data bytes.
-    if (_fixedSizeDataBytes != null) {
-      dataOutputStream.write(_fixedSizeDataBytes);
-    }
-    // Write variable size data bytes.
-    if (_variableSizeDataBytes != null) {
-      dataOutputStream.write(_variableSizeDataBytes);
+    copySection(_fixedSizeData, byteBuffer, fixedSizeDataLength);
+    copySection(_variableSizeData, byteBuffer, variableSizeDataLength);
+  }
+
+  private static void copySection(@Nullable DataBuffer section, ByteBuffer 
byteBuffer, int length) {
+    if (length > 0) {
+      section.copyTo(0, byteBuffer.array(), byteBuffer.position(), length);
+      byteBuffer.position(byteBuffer.position() + length);
     }
   }
 
@@ -648,7 +665,7 @@ public class DataTableImplV4 implements DataTable {
     }
   }
 
-  /// return the offset in `_fixedSizeDataBytes` of the row/column ID.
+  /// Returns the offset in the fixed size buffer of the row/column ID.
   /// @param rowId row ID
   /// @param colId column ID
   /// @return the offset in the fixed size buffer for the row/columnID.
@@ -656,16 +673,13 @@ public class DataTableImplV4 implements DataTable {
     return rowId * _rowSizeInBytes + _columnOffsets[colId];
   }
 
-  /// position the `_variableSizeDataBytes` to the corresponding row/column 
ID. and return the
-  /// length of bytes to extract from the variable size buffer.
-  ///
-  /// @param rowId row ID
-  /// @param colId column ID
-  /// @return the length to extract from variable size buffer.
-  protected int positionOffsetInVariableBufferAndGetLength(int rowId, int 
colId) {
-    int offset = getOffsetInFixedBuffer(rowId, colId);
-    _variableSizeData.position(_fixedSizeData.getInt(offset));
-    return _fixedSizeData.getInt(offset + 4);
+  /// Copies the variable size value referenced by the (offset, length) pair 
at `offsetInFixed` into a new array.
+  private byte[] copyFromVariableBuffer(int offsetInFixed) {
+    int offsetInVar = _fixedSizeData.getInt(offsetInFixed);
+    int length = _fixedSizeData.getInt(offsetInFixed + Integer.BYTES);
+    byte[] bytes = new byte[length];
+    _variableSizeData.copyTo(offsetInVar, bytes, 0, length);
+    return bytes;
   }
 
   @Override
@@ -678,7 +692,7 @@ public class DataTableImplV4 implements DataTable {
 
   @Override
   public DataTable toDataOnlyDataTable() {
-    return new DataTableImplV4(_numRows, _dataSchema, _stringDictionary, 
_fixedSizeDataBytes, _variableSizeDataBytes);
+    return new DataTableImplV4(_numRows, _dataSchema, _stringDictionary, 
_fixedSizeData, _variableSizeData);
   }
 
   public int getRowSizeInBytes() {
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/common/datatable/BaseDataTableBuilder.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/common/datatable/BaseDataTableBuilder.java
index ca62f32c723..41f054027ff 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/common/datatable/BaseDataTableBuilder.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/common/datatable/BaseDataTableBuilder.java
@@ -18,8 +18,6 @@
  */
 package org.apache.pinot.core.common.datatable;
 
-import java.io.ByteArrayOutputStream;
-import java.io.DataOutputStream;
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.nio.ByteBuffer;
@@ -32,6 +30,7 @@ import org.apache.pinot.common.utils.DataSchema;
 import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
 import org.apache.pinot.common.utils.RoaringBitmapUtils;
 import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
+import org.apache.pinot.segment.spi.memory.PagedPinotOutputStream;
 import org.apache.pinot.spi.utils.BigDecimalUtils;
 import org.apache.pinot.spi.utils.ByteArray;
 import org.apache.pinot.spi.utils.MapUtils;
@@ -60,17 +59,19 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   /// decodes as garbage in an 8-byte `LONG` / `DOUBLE` one.
   private static final EnumSet<ColumnDataType> IN_BAND_NULL_TYPES =
       EnumSet.of(ColumnDataType.OBJECT, ColumnDataType.UNKNOWN, 
ColumnDataType.MAP);
+  /// Heap pages need no release, so one stateless allocator serves every 
builder.
+  private static final PagedPinotOutputStream.PageAllocator PAGE_ALLOCATOR =
+      PagedPinotOutputStream.HeapPageAllocator.createSmall();
 
   protected final DataSchema _dataSchema;
   protected final int _version;
   protected final int[] _columnOffsets;
   protected final ByteBuffer _currentRowDataByteBuffer;
-  protected final ByteArrayOutputStream _fixedSizeDataByteArrayOutputStream = 
new ByteArrayOutputStream();
-  protected final DataOutputStream _fixedSizeDataOutputStream =
-      new DataOutputStream(_fixedSizeDataByteArrayOutputStream);
-  protected final ByteArrayOutputStream _variableSizeDataByteArrayOutputStream 
= new ByteArrayOutputStream();
-  protected final DataOutputStream _variableSizeDataOutputStream =
-      new DataOutputStream(_variableSizeDataByteArrayOutputStream);
+  /// Both sections are written in heap pages of 
[PagedPinotOutputStream.PageAllocator#MIN_RECOMMENDED_PAGE_SIZE]
+  /// bytes and handed to the built table as is, without copying them into one 
contiguous array. A large response
+  /// therefore never needs a humongous allocation while it is built.
+  protected final PagedPinotOutputStream _fixedSizeDataOutputStream = new 
PagedPinotOutputStream(PAGE_ALLOCATOR);
+  protected final PagedPinotOutputStream _variableSizeDataOutputStream = new 
PagedPinotOutputStream(PAGE_ALLOCATOR);
 
   private final ColumnDataType[] _storedColumnDataTypes;
   /// Entries are allocated lazily so a table without nulls pays nothing.
@@ -94,6 +95,12 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
     _nullBitmaps = new RoaringBitmap[dataSchema.size()];
   }
 
+  /// Returns the offset of the next byte in the variable size section. The 
wire format stores it as an int. The
+  /// section had the same 2 GB limit when it was a `ByteArrayOutputStream`, 
which failed with an `OutOfMemoryError`.
+  protected int variableSizeDataOffset() {
+    return Math.toIntExact(_variableSizeDataOutputStream.getCurrentOffset());
+  }
+
   @Override
   public void startRow() {
     _numRows++;
@@ -127,23 +134,23 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   public void setColumn(int colId, BigDecimal value)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     byte[] bytes = BigDecimalUtils.serialize(value);
     _currentRowDataByteBuffer.putInt(bytes.length);
-    _variableSizeDataByteArrayOutputStream.write(bytes);
+    _variableSizeDataOutputStream.write(bytes);
   }
 
   @Override
   public void setColumn(int colId, @Nullable Map<String, Object> value)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     if (value == null) {
       _currentRowDataByteBuffer.putInt(0);
     } else {
       byte[] bytes = MapUtils.serializeMap(value, false);
       _currentRowDataByteBuffer.putInt(bytes.length);
-      _variableSizeDataByteArrayOutputStream.write(bytes);
+      _variableSizeDataOutputStream.write(bytes);
     }
   }
 
@@ -151,7 +158,7 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   public void setColumn(int colId, int[] values)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     _currentRowDataByteBuffer.putInt(values.length);
     for (int value : values) {
       _variableSizeDataOutputStream.writeInt(value);
@@ -162,7 +169,7 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   public void setColumn(int colId, long[] values)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     _currentRowDataByteBuffer.putInt(values.length);
     for (long value : values) {
       _variableSizeDataOutputStream.writeLong(value);
@@ -173,7 +180,7 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   public void setColumn(int colId, float[] values)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     _currentRowDataByteBuffer.putInt(values.length);
     for (float value : values) {
       _variableSizeDataOutputStream.writeFloat(value);
@@ -184,7 +191,7 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   public void setColumn(int colId, double[] values)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     _currentRowDataByteBuffer.putInt(values.length);
     for (double value : values) {
       _variableSizeDataOutputStream.writeDouble(value);
@@ -195,12 +202,12 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   public void setColumn(int colId, BigDecimal[] values)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     _currentRowDataByteBuffer.putInt(values.length);
     for (BigDecimal value : values) {
       byte[] bytes = BigDecimalUtils.serialize(value);
       _variableSizeDataOutputStream.writeInt(bytes.length);
-      _variableSizeDataByteArrayOutputStream.write(bytes);
+      _variableSizeDataOutputStream.write(bytes);
     }
   }
 
@@ -208,12 +215,12 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   public void setColumn(int colId, ByteArray[] values)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     _currentRowDataByteBuffer.putInt(values.length);
     for (ByteArray value : values) {
       byte[] bytes = value.getBytes();
       _variableSizeDataOutputStream.writeInt(bytes.length);
-      _variableSizeDataByteArrayOutputStream.write(bytes);
+      _variableSizeDataOutputStream.write(bytes);
     }
   }
 
@@ -221,7 +228,7 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   public void setColumn(int colId, 
AggregationFunction.SerializedIntermediateResult value)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     _currentRowDataByteBuffer.putInt(value.getSize());
     _variableSizeDataOutputStream.writeInt(value.getType());
     value.writeTo(_variableSizeDataOutputStream);
@@ -233,7 +240,7 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
     ColumnDataType storedColumnDataType = _storedColumnDataTypes[colId];
     if (IN_BAND_NULL_TYPES.contains(storedColumnDataType)) {
       _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-      
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+      _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
       _currentRowDataByteBuffer.putInt(0);
       _variableSizeDataOutputStream.writeInt(CustomObject.NULL_TYPE_VALUE);
       return;
@@ -256,7 +263,7 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
   @Override
   public void finishRow()
       throws IOException {
-    
_fixedSizeDataByteArrayOutputStream.write(_currentRowDataByteBuffer.array());
+    _fixedSizeDataOutputStream.write(_currentRowDataByteBuffer.array());
   }
 
   @Override
@@ -289,13 +296,13 @@ public abstract class BaseDataTableBuilder implements 
DataTableBuilder {
     }
     _nullBitmapsWritten = true;
     for (RoaringBitmap nullBitmap : _nullBitmaps) {
-      
_fixedSizeDataOutputStream.writeInt(_variableSizeDataByteArrayOutputStream.size());
+      _fixedSizeDataOutputStream.writeInt(variableSizeDataOffset());
       if (nullBitmap == null) {
         _fixedSizeDataOutputStream.writeInt(0);
       } else {
         byte[] bitmapBytes = RoaringBitmapUtils.serialize(nullBitmap);
         _fixedSizeDataOutputStream.writeInt(bitmapBytes.length);
-        _variableSizeDataByteArrayOutputStream.write(bitmapBytes);
+        _variableSizeDataOutputStream.write(bitmapBytes);
       }
     }
   }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/common/datatable/DataTableBuilderV4.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/common/datatable/DataTableBuilderV4.java
index f4da8848140..1e4fcf1bed8 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/common/datatable/DataTableBuilderV4.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/common/datatable/DataTableBuilderV4.java
@@ -22,6 +22,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.nio.ByteOrder;
 import org.apache.pinot.common.datatable.DataTable;
 import org.apache.pinot.common.datatable.DataTableFactory;
 import org.apache.pinot.common.datatable.DataTableImplV4;
@@ -47,17 +48,17 @@ public class DataTableBuilderV4 extends 
BaseDataTableBuilder {
   public void setColumn(int colId, ByteArray value)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     byte[] bytes = value.getBytes();
     _currentRowDataByteBuffer.putInt(bytes.length);
-    _variableSizeDataByteArrayOutputStream.write(bytes);
+    _variableSizeDataOutputStream.write(bytes);
   }
 
   @Override
   public void setColumn(int colId, String[] values)
       throws IOException {
     _currentRowDataByteBuffer.position(_columnOffsets[colId]);
-    
_currentRowDataByteBuffer.putInt(_variableSizeDataByteArrayOutputStream.size());
+    _currentRowDataByteBuffer.putInt(variableSizeDataOffset());
     _currentRowDataByteBuffer.putInt(values.length);
     for (String value : values) {
       int dictId = _dictionary.computeIntIfAbsent(value, k -> 
_dictionary.size());
@@ -70,14 +71,17 @@ public class DataTableBuilderV4 extends 
BaseDataTableBuilder {
     try {
       writeNullBitmaps();
     } catch (IOException e) {
-      // Both buffers are in-memory byte arrays, so this cannot happen.
+      // Both buffers are in-memory heap pages, so this cannot happen.
       throw new UncheckedIOException(e);
     }
     String[] reverseDictionary = new String[_dictionary.size()];
     for (Object2IntMap.Entry<String> entry : _dictionary.object2IntEntrySet()) 
{
       reverseDictionary[entry.getIntValue()] = entry.getKey();
     }
+    // The table views the written pages directly. Pages are heap memory, so 
nothing has to be released. The views
+    // end at the bytes written so far, and the builder only appends, so later 
writes do not change this table.
     return new DataTableImplV4(_numRows, _dataSchema, reverseDictionary,
-        _fixedSizeDataByteArrayOutputStream.toByteArray(), 
_variableSizeDataByteArrayOutputStream.toByteArray());
+        _fixedSizeDataOutputStream.asBuffer(ByteOrder.BIG_ENDIAN, false),
+        _variableSizeDataOutputStream.asBuffer(ByteOrder.BIG_ENDIAN, false));
   }
 }
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
index 521d2082916..41b2ed68b15 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
@@ -20,6 +20,7 @@ package org.apache.pinot.core.common.datatable;
 
 import java.io.IOException;
 import java.math.BigDecimal;
+import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -28,11 +29,13 @@ import java.util.Random;
 import java.util.UUID;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.pinot.common.CustomObject;
 import org.apache.pinot.common.datatable.DataTable;
 import org.apache.pinot.common.datatable.DataTable.MetadataKey;
 import org.apache.pinot.common.datatable.DataTableFactory;
 import org.apache.pinot.common.utils.DataSchema;
 import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
 import org.apache.pinot.spi.accounting.ThreadResourceUsageProvider;
 import org.apache.pinot.spi.exception.QueryErrorCode;
 import org.apache.pinot.spi.utils.ByteArray;
@@ -305,12 +308,99 @@ public class DataTableSerDeTest {
     fillDataTableWithRandomData(dataTableBuilder, columnDataTypes, numColumns);
 
     DataTable dataTable = dataTableBuilder.build();
+    // The built table reads the builder's pages, the deserialized one reads a 
single array. Both must agree.
+    verifyDataIsSame(dataTable, columnDataTypes, numColumns);
     DataTable newDataTable = 
DataTableFactory.getDataTable(dataTable.toBytes());
     assertEquals(newDataTable.getDataSchema(), dataSchema, ERROR_MESSAGE);
     assertEquals(newDataTable.getNumberOfRows(), NUM_ROWS, ERROR_MESSAGE);
     verifyDataIsSame(newDataTable, columnDataTypes, numColumns);
   }
 
+  /// The builder writes the fixed and variable size sections in pages. Values 
larger than a page, and values that
+  /// straddle a page boundary, must read back the same from the built table 
and from the deserialized one.
+  @Test
+  public void testValuesSpanningPages()
+      throws IOException {
+    DataTableBuilderFactory.setDataTableVersion(DataTableFactory.VERSION_4);
+    DataSchema dataSchema = new DataSchema(
+        new String[]{"bytes", "ints", "object", "long", "strings", 
"bigDecimals", "nullableInt"},
+        new ColumnDataType[]{ColumnDataType.BYTES, ColumnDataType.INT_ARRAY, 
ColumnDataType.OBJECT,
+            ColumnDataType.LONG, ColumnDataType.STRING_ARRAY, 
ColumnDataType.BIG_DECIMAL_ARRAY, ColumnDataType.INT});
+    // 52 bytes per row, so the fixed size section spans several pages as well.
+    int numRows = 2_000;
+    byte[][] bytesValues = new byte[numRows][];
+    int[][] intArrayValues = new int[numRows][];
+    byte[][] objectValues = new byte[numRows][];
+    String[][] stringArrayValues = new String[numRows][];
+    BigDecimal[][] bigDecimalArrayValues = new BigDecimal[numRows][];
+    DataTableBuilder dataTableBuilder = 
DataTableBuilderFactory.getDataTableBuilder(dataSchema);
+    for (int rowId = 0; rowId < numRows; rowId++) {
+      // Every 97th row carries values larger than a page.
+      boolean large = rowId % 97 == 0;
+      dataTableBuilder.startRow();
+      bytesValues[rowId] = new byte[large ? 20_000 + RANDOM.nextInt(20_000) : 
RANDOM.nextInt(300)];
+      RANDOM.nextBytes(bytesValues[rowId]);
+      dataTableBuilder.setColumn(0, new ByteArray(bytesValues[rowId]));
+      intArrayValues[rowId] = RANDOM.ints(large ? 10_000 : 
RANDOM.nextInt(100)).toArray();
+      dataTableBuilder.setColumn(1, intArrayValues[rowId]);
+      if (rowId % 10 == 1) {
+        dataTableBuilder.setNull(2);
+      } else {
+        objectValues[rowId] = new byte[large ? 30_000 : 1 + 
RANDOM.nextInt(500)];
+        RANDOM.nextBytes(objectValues[rowId]);
+        dataTableBuilder.setColumn(2, new 
AggregationFunction.SerializedIntermediateResult(rowId, objectValues[rowId]));
+      }
+      dataTableBuilder.setColumn(3, (long) rowId);
+      stringArrayValues[rowId] = new String[large ? 5_000 : 
RANDOM.nextInt(20)];
+      for (int i = 0; i < stringArrayValues[rowId].length; i++) {
+        stringArrayValues[rowId][i] = "s" + RANDOM.nextInt(1_000);
+      }
+      dataTableBuilder.setColumn(4, stringArrayValues[rowId]);
+      bigDecimalArrayValues[rowId] = new BigDecimal[large ? 2_000 : 
RANDOM.nextInt(20)];
+      for (int i = 0; i < bigDecimalArrayValues[rowId].length; i++) {
+        bigDecimalArrayValues[rowId][i] = 
BigDecimal.valueOf(RANDOM.nextLong(), RANDOM.nextInt(10));
+      }
+      dataTableBuilder.setColumn(5, bigDecimalArrayValues[rowId]);
+      // A third of the rows are null, so the null bitmap section is not 
trivial.
+      if (rowId % 3 == 0) {
+        dataTableBuilder.setNull(6);
+      } else {
+        dataTableBuilder.setColumn(6, rowId);
+      }
+      dataTableBuilder.finishRow();
+    }
+
+    DataTable dataTable = dataTableBuilder.build();
+    DataTable newDataTable = 
DataTableFactory.getDataTable(dataTable.toBytes());
+    for (DataTable table : new DataTable[]{dataTable, 
dataTable.toDataOnlyDataTable(), newDataTable}) {
+      assertEquals(table.getNumberOfRows(), numRows, ERROR_MESSAGE);
+      RoaringBitmap nullRowIds = table.getNullRowIds(6);
+      assertNotNull(nullRowIds, ERROR_MESSAGE);
+      for (int rowId = 0; rowId < numRows; rowId++) {
+        assertEquals(table.getBytes(rowId, 0).getBytes(), bytesValues[rowId], 
ERROR_MESSAGE);
+        assertEquals(table.getIntArray(rowId, 1), intArrayValues[rowId], 
ERROR_MESSAGE);
+        CustomObject customObject = table.getCustomObject(rowId, 2);
+        if (objectValues[rowId] == null) {
+          assertNull(customObject, ERROR_MESSAGE);
+        } else {
+          assertNotNull(customObject, ERROR_MESSAGE);
+          assertEquals(customObject.getType(), rowId, ERROR_MESSAGE);
+          ByteBuffer buffer = customObject.getBuffer();
+          byte[] objectBytes = new byte[buffer.remaining()];
+          buffer.get(objectBytes);
+          assertEquals(objectBytes, objectValues[rowId], ERROR_MESSAGE);
+        }
+        assertEquals(table.getLong(rowId, 3), rowId, ERROR_MESSAGE);
+        assertEquals(table.getStringArray(rowId, 4), stringArrayValues[rowId], 
ERROR_MESSAGE);
+        assertEquals(table.getBigDecimalArray(rowId, 5), 
bigDecimalArrayValues[rowId], ERROR_MESSAGE);
+        assertEquals(nullRowIds.contains(rowId), rowId % 3 == 0, 
ERROR_MESSAGE);
+        if (rowId % 3 != 0) {
+          assertEquals(table.getInt(rowId, 6), rowId, ERROR_MESSAGE);
+        }
+      }
+    }
+  }
+
   @Test(dataProvider = "versionProvider")
   public void testAllDataTypes(int dataTableVersion)
       throws IOException {
diff --git 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/memory/CompoundDataBuffer.java
 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/memory/CompoundDataBuffer.java
index ed6d2894676..0942bccfdcd 100644
--- 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/memory/CompoundDataBuffer.java
+++ 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/memory/CompoundDataBuffer.java
@@ -25,6 +25,7 @@ import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
 
@@ -112,21 +113,19 @@ public class CompoundDataBuffer implements DataBuffer {
   }
 
   private int getBufferIndex(long offset) {
-    // this optimistically assumes that lookups are going to be in ascending 
order
-    // we don't care about concurrency here given that this is only used to 
speed up the lookup
+    // Most lookups land in the same buffer as the previous one, so check it 
first. Otherwise use a binary search,
+    // which keeps random and backward access at O(log n) instead of a scan 
from the first buffer.
+    // We don't care about concurrency here given that this is only used to 
speed up the lookup.
     int lastBufferIndex = _lastBufferIndex;
-    if (_bufferOffsets[lastBufferIndex] > offset) {
-      lastBufferIndex = 0;
-    }
-
-    for (int i = lastBufferIndex; i < _bufferOffsets.length; i++) {
-      if (offset < _bufferOffsets[i]) {
-        int result = i - 1;
-        _lastBufferIndex = result;
-        return result;
-      }
-    }
-    int result = _bufferOffsets.length - 1;
+    int nextBufferIndex = lastBufferIndex + 1;
+    if (_bufferOffsets[lastBufferIndex] <= offset
+        && (nextBufferIndex == _bufferOffsets.length || offset < 
_bufferOffsets[nextBufferIndex])) {
+      return lastBufferIndex;
+    }
+    // Offsets are strictly ascending because empty buffers are rejected. On a 
miss, binarySearch returns
+    // (-insertionPoint - 1), and the buffer that contains the offset is the 
one before the insertion point.
+    int index = Arrays.binarySearch(_bufferOffsets, offset);
+    int result = index >= 0 ? index : -index - 2;
     _lastBufferIndex = result;
     return result;
   }
diff --git 
a/pinot-segment-spi/src/test/java/org/apache/pinot/segment/spi/memory/CompoundDataBufferTest.java
 
b/pinot-segment-spi/src/test/java/org/apache/pinot/segment/spi/memory/CompoundDataBufferTest.java
index 23a37d6fec0..8bd205adc14 100644
--- 
a/pinot-segment-spi/src/test/java/org/apache/pinot/segment/spi/memory/CompoundDataBufferTest.java
+++ 
b/pinot-segment-spi/src/test/java/org/apache/pinot/segment/spi/memory/CompoundDataBufferTest.java
@@ -186,6 +186,32 @@ public class CompoundDataBufferTest {
     }
   }
 
+  /// Reads in random order, so that lookups move backward and forward across 
buffers of different sizes, and also land
+  /// on the first and last byte of each buffer.
+  @Test
+  void randomAccessAcrossBuffersOfDifferentSizes() {
+    Random r = new Random(42);
+    int[] sizes = {1, 7, 16, 3, 64, 1, 32};
+    int totalSize = Arrays.stream(sizes).sum();
+    byte[] expected = new byte[totalSize];
+    r.nextBytes(expected);
+    DataBuffer[] buffers = new DataBuffer[sizes.length];
+    int start = 0;
+    for (int i = 0; i < sizes.length; i++) {
+      buffers[i] = PinotByteBuffer.wrap(Arrays.copyOfRange(expected, start, 
start + sizes[i]));
+      start += sizes[i];
+    }
+    CompoundDataBuffer compoundDataBuffer = new CompoundDataBuffer(buffers, 
ByteOrder.BIG_ENDIAN, false);
+
+    for (int i = 0; i < 10_000; i++) {
+      int offset = r.nextInt(totalSize);
+      assertEquals(compoundDataBuffer.getByte(offset), expected[offset], 
"Offset " + offset);
+    }
+    for (int offset = totalSize - 1; offset >= 0; offset--) {
+      assertEquals(compoundDataBuffer.getByte(offset), expected[offset], 
"Offset " + offset);
+    }
+  }
+
   @Test
   void readWriteByteBuffer() {
     Random r = new Random(42);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to