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

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


The following commit(s) were added to refs/heads/master by this push:
     new 02b1ecd3775 [Performance] Allocate AlignedTVList primitive arrays 
lazily (#18276)
02b1ecd3775 is described below

commit 02b1ecd37759c9211bcfe1ef7bc365db18967f9b
Author: Jiang Tian <[email protected]>
AuthorDate: Thu Jul 23 09:38:10 2026 +0800

    [Performance] Allocate AlignedTVList primitive arrays lazily (#18276)
    
    * Initialize primitive array lazily
    
    * add perf test
    
    * Add tests for lazy aligned array accounting
    
    * fix memory calculation
    
    ---------
    
    Co-authored-by: Caideyipi <[email protected]>
---
 .../db/it/IoTDBAlignedTVListLazyAllocationIT.java  | 142 +++++
 ...DBAlignedTVListPrimitiveArrayPerformanceIT.java | 584 +++++++++++++++++++++
 .../memtable/AlignedWritableMemChunk.java          |   4 +
 .../dataregion/memtable/TsFileProcessor.java       | 295 +++++++++--
 .../db/utils/datastructure/AlignedTVList.java      | 229 +++++---
 .../iotdb/db/utils/datastructure/TVList.java       |  13 +-
 .../dataregion/memtable/TsFileProcessorTest.java   | 132 ++++-
 .../db/utils/datastructure/AlignedTVListTest.java  | 143 +++++
 8 files changed, 1407 insertions(+), 135 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBAlignedTVListLazyAllocationIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBAlignedTVListLazyAllocationIT.java
new file mode 100644
index 00000000000..375f560c9ee
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBAlignedTVListLazyAllocationIT.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.it;
+
+import org.apache.iotdb.db.utils.datastructure.AlignedTVList;
+import org.apache.iotdb.isession.ISession;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.LocalStandaloneIT;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.write.record.Tablet;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static 
org.apache.iotdb.db.storageengine.rescon.memory.PrimitiveArrayManager.ARRAY_SIZE;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({LocalStandaloneIT.class})
+public class IoTDBAlignedTVListLazyAllocationIT {
+
+  private static final String DEVICE = "root.aligned_lazy_allocation.d1";
+  private static final int DATANODE_MAX_HEAP_SIZE_IN_MB = 256;
+  private static final int INITIAL_ROW_COUNT = 40_000;
+  private static final int NEW_COLUMN_COUNT = 1_200;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    EnvFactory.getEnv()
+        .getConfig()
+        .getDataNodeJVMConfig()
+        .setMaxHeapSize(DATANODE_MAX_HEAP_SIZE_IN_MB);
+    EnvFactory.getEnv()
+        .getConfig()
+        .getCommonConfig()
+        .setAutoCreateSchemaEnabled(true)
+        .setEnableMemControl(true)
+        .setPrimitiveArraySize(64)
+        .setMemtableSizeThreshold(512L * 1024 * 1024)
+        .setDatanodeMemoryProportion("6:1:1:1:1:1")
+        .setWriteMemoryProportion("100:1");
+    EnvFactory.getEnv().initClusterEnvironment();
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  @Test
+  public void testAddingManyColumnsAfterManyRowsDoesNotExhaustWriteMemory() 
throws Exception {
+    int historicalBlockCount = (INITIAL_ROW_COUNT + ARRAY_SIZE - 1) / 
ARRAY_SIZE;
+    long eagerAllocationCost =
+        (long) historicalBlockCount
+            * NEW_COLUMN_COUNT
+            * AlignedTVList.valueListArrayMemCost(TSDataType.INT64);
+    long lazyAllocationCost =
+        (long) historicalBlockCount
+                * NEW_COLUMN_COUNT
+                * AlignedTVList.valueListArrayMemCostWithoutPrimitiveArray()
+            + (long) NEW_COLUMN_COUNT * 
AlignedTVList.primitiveArrayMemCost(TSDataType.INT64);
+    long dataNodeMaxHeapSize = DATANODE_MAX_HEAP_SIZE_IN_MB * 1024L * 1024L;
+
+    Assert.assertTrue(
+        "The eager implementation must exceed the entire DataNode heap in this 
scenario",
+        eagerAllocationCost > dataNodeMaxHeapSize);
+    Assert.assertTrue(
+        "The lazy implementation must fit comfortably within the DataNode 
heap",
+        lazyAllocationCost < dataNodeMaxHeapSize / 2);
+
+    try (ISession session = EnvFactory.getEnv().getSessionConnection()) {
+      insertInitialRows(session);
+
+      List<String> measurements = new ArrayList<>(NEW_COLUMN_COUNT);
+      List<TSDataType> dataTypes = new ArrayList<>(NEW_COLUMN_COUNT);
+      List<Object> values = new ArrayList<>(NEW_COLUMN_COUNT);
+      for (int i = 1; i <= NEW_COLUMN_COUNT; i++) {
+        measurements.add("s" + i);
+        dataTypes.add(TSDataType.INT64);
+        values.add((long) i);
+      }
+      session.insertAlignedRecord(DEVICE, INITIAL_ROW_COUNT, measurements, 
dataTypes, values);
+
+      try (SessionDataSet dataSet =
+          session.executeQueryStatement(
+              "SELECT COUNT(s0), COUNT(s" + NEW_COLUMN_COUNT + ") FROM " + 
DEVICE)) {
+        Assert.assertTrue(dataSet.hasNext());
+        List<org.apache.tsfile.read.common.Field> fields = 
dataSet.next().getFields();
+        Assert.assertEquals(INITIAL_ROW_COUNT, fields.get(0).getLongV());
+        Assert.assertEquals(1, fields.get(1).getLongV());
+        Assert.assertFalse(dataSet.hasNext());
+      }
+    }
+  }
+
+  private static void insertInitialRows(ISession session) throws Exception {
+    List<IMeasurementSchema> schemas =
+        Collections.singletonList(new MeasurementSchema("s0", 
TSDataType.INT64));
+    Tablet tablet = new Tablet(DEVICE, schemas);
+    for (int i = 0; i < INITIAL_ROW_COUNT; i++) {
+      int rowIndex = tablet.getRowSize();
+      if (rowIndex == tablet.getMaxRowNumber()) {
+        session.insertAlignedTablet(tablet);
+        tablet.reset();
+        rowIndex = 0;
+      }
+      tablet.addTimestamp(rowIndex, i);
+      tablet.addValue("s0", rowIndex, (long) i);
+    }
+    if (tablet.getRowSize() > 0) {
+      session.insertAlignedTablet(tablet);
+    }
+  }
+}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/db/it/performance/IoTDBAlignedTVListPrimitiveArrayPerformanceIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/db/it/performance/IoTDBAlignedTVListPrimitiveArrayPerformanceIT.java
new file mode 100644
index 00000000000..c0bbf499062
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/db/it/performance/IoTDBAlignedTVListPrimitiveArrayPerformanceIT.java
@@ -0,0 +1,584 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.it.performance;
+
+import org.apache.iotdb.isession.ISession;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.LocalStandaloneIT;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.write.record.Tablet;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({LocalStandaloneIT.class})
+public class IoTDBAlignedTVListPrimitiveArrayPerformanceIT {
+
+  private static final Logger LOGGER =
+      
LoggerFactory.getLogger(IoTDBAlignedTVListPrimitiveArrayPerformanceIT.class);
+
+  private static final String PROPERTY_PREFIX = 
"iotdb.aligned.lazy-allocation.perf.";
+  private static final String ENABLED_PROPERTY = PROPERTY_PREFIX + "enabled";
+  private static final String IMPLEMENTATION_PROPERTY = PROPERTY_PREFIX + 
"implementation";
+  private static final String REVISION_PROPERTY = PROPERTY_PREFIX + "revision";
+  private static final String HISTORICAL_ROW_COUNT_PROPERTY =
+      PROPERTY_PREFIX + "historical.row.count";
+  private static final String NEW_COLUMN_COUNT_PROPERTY = PROPERTY_PREFIX + 
"new.column.count";
+  private static final String NO_NULL_BATCH_COUNT_PROPERTY =
+      PROPERTY_PREFIX + "no-null.batch.count";
+  private static final String WARMUP_ROUND_COUNT_PROPERTY = PROPERTY_PREFIX + 
"warmup.round.count";
+  private static final String ROUND_COUNT_PROPERTY = PROPERTY_PREFIX + 
"round.count";
+  private static final String RESULT_PATH_PROPERTY = PROPERTY_PREFIX + 
"result.path";
+  private static final String BASELINE_RESULT_PATH_PROPERTY =
+      PROPERTY_PREFIX + "baseline.result.path";
+  private static final String REPORT_PATH_PROPERTY = PROPERTY_PREFIX + 
"report.path";
+
+  private static final int DATANODE_MAX_HEAP_SIZE_IN_MB = 1536;
+  private static final int PRIMITIVE_ARRAY_SIZE = 64;
+  private static final int HISTORICAL_ROW_COUNT =
+      Integer.getInteger(HISTORICAL_ROW_COUNT_PROPERTY, 10_000);
+  private static final int NEW_COLUMN_COUNT = 
Integer.getInteger(NEW_COLUMN_COUNT_PROPERTY, 512);
+  private static final int NO_NULL_ROWS_PER_BATCH = PRIMITIVE_ARRAY_SIZE;
+  private static final int NO_NULL_BATCH_COUNT =
+      Integer.getInteger(NO_NULL_BATCH_COUNT_PROPERTY, 50);
+  private static final int WARMUP_ROUND_COUNT = 
Integer.getInteger(WARMUP_ROUND_COUNT_PROPERTY, 1);
+  private static final int ROUND_COUNT = 
Integer.getInteger(ROUND_COUNT_PROPERTY, 5);
+  private static final String IMPLEMENTATION =
+      System.getProperty(IMPLEMENTATION_PROPERTY, "candidate");
+  private static final String REVISION = System.getProperty(REVISION_PROPERTY, 
"unknown");
+  private static final String DEVICE_PREFIX = 
"root.aligned_lazy_allocation_performance.d";
+  private static final String NO_NULL_DEVICE = 
"root.aligned_lazy_allocation_performance.no_null";
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    Assume.assumeTrue(Boolean.getBoolean(ENABLED_PROPERTY));
+    EnvFactory.getEnv()
+        .getConfig()
+        .getDataNodeJVMConfig()
+        .setMaxHeapSize(DATANODE_MAX_HEAP_SIZE_IN_MB);
+    EnvFactory.getEnv()
+        .getConfig()
+        .getCommonConfig()
+        .setAutoCreateSchemaEnabled(false)
+        .setEnableMemControl(true)
+        .setPrimitiveArraySize(PRIMITIVE_ARRAY_SIZE)
+        .setMemtableSizeThreshold(768L * 1024 * 1024)
+        .setDatanodeMemoryProportion("6:1:1:1:1:1")
+        .setWriteMemoryProportion("100:1");
+    EnvFactory.getEnv().initClusterEnvironment();
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  @Test
+  public void testAlignedWritePerformance() throws Exception {
+    Assert.assertTrue(HISTORICAL_ROW_COUNT > 0);
+    Assert.assertTrue(NEW_COLUMN_COUNT > 0);
+    Assert.assertTrue(NO_NULL_BATCH_COUNT > 0);
+    Assert.assertTrue(WARMUP_ROUND_COUNT >= 0);
+    Assert.assertTrue(ROUND_COUNT > 0);
+
+    List<String> newMeasurements = new ArrayList<>(NEW_COLUMN_COUNT);
+    List<TSDataType> newDataTypes = new ArrayList<>(NEW_COLUMN_COUNT);
+    List<Object> newValues = new ArrayList<>(NEW_COLUMN_COUNT);
+    for (int i = 1; i <= NEW_COLUMN_COUNT; i++) {
+      newMeasurements.add("s" + i);
+      newDataTypes.add(TSDataType.INT64);
+      newValues.add((long) i);
+    }
+
+    long[] lazyAllocationElapsedNanos;
+    long[] noNullElapsedNanos;
+    try (ISession session = EnvFactory.getEnv().getSessionConnection()) {
+      lazyAllocationElapsedNanos =
+          runLazyAllocationWorkload(session, newMeasurements, newDataTypes, 
newValues);
+      noNullElapsedNanos = runNoNullWorkload(session);
+    }
+
+    BenchmarkResult result =
+        new BenchmarkResult(
+            IMPLEMENTATION,
+            REVISION,
+            HISTORICAL_ROW_COUNT,
+            NEW_COLUMN_COUNT,
+            WARMUP_ROUND_COUNT,
+            NO_NULL_BATCH_COUNT,
+            lazyAllocationElapsedNanos,
+            noNullElapsedNanos);
+    Path resultPath = getResultPath();
+    writeResult(resultPath, result);
+    LOGGER.info("AlignedTVList lazy-allocation performance result: {}", 
result.toSummary());
+    LOGGER.info("Performance result written to {}", 
resultPath.toAbsolutePath());
+
+    String baselineResultPath = 
System.getProperty(BASELINE_RESULT_PATH_PROPERTY);
+    if (baselineResultPath != null && !baselineResultPath.isEmpty()) {
+      BenchmarkResult baseline = readResult(Paths.get(baselineResultPath));
+      assertComparable(baseline, result);
+      Path reportPath =
+          Paths.get(
+              System.getProperty(
+                  REPORT_PATH_PROPERTY,
+                  
"target/aligned-tvlist-lazy-allocation-performance-report.md"));
+      writeFile(reportPath, buildReport(baseline, result));
+      LOGGER.info("Comparison report written to {}", 
reportPath.toAbsolutePath());
+    }
+  }
+
+  private static long[] runLazyAllocationWorkload(
+      ISession session,
+      List<String> newMeasurements,
+      List<TSDataType> newDataTypes,
+      List<Object> newValues)
+      throws Exception {
+    long[] elapsedNanos = new long[ROUND_COUNT];
+    int totalRoundCount = WARMUP_ROUND_COUNT + ROUND_COUNT;
+    for (int round = 0; round < totalRoundCount; round++) {
+      String device = DEVICE_PREFIX + round;
+      createAlignedTimeseries(session, device, NEW_COLUMN_COUNT + 1);
+      insertHistoricalRows(session, device);
+
+      long startNanos = System.nanoTime();
+      session.insertAlignedRecord(
+          device, HISTORICAL_ROW_COUNT, newMeasurements, newDataTypes, 
newValues);
+      long roundElapsedNanos = System.nanoTime() - startNanos;
+      if (round >= WARMUP_ROUND_COUNT) {
+        elapsedNanos[round - WARMUP_ROUND_COUNT] = roundElapsedNanos;
+      }
+
+      assertLazyAllocationDataWritten(session, device);
+    }
+    return elapsedNanos;
+  }
+
+  private static long[] runNoNullWorkload(ISession session) throws Exception {
+    createAlignedTimeseries(session, NO_NULL_DEVICE, NEW_COLUMN_COUNT);
+    List<IMeasurementSchema> schemas = new ArrayList<>(NEW_COLUMN_COUNT);
+    for (int column = 0; column < NEW_COLUMN_COUNT; column++) {
+      schemas.add(new MeasurementSchema("s" + column, TSDataType.INT64));
+    }
+    Tablet tablet = new Tablet(NO_NULL_DEVICE, schemas, 
NO_NULL_ROWS_PER_BATCH);
+    for (int column = 0; column < NEW_COLUMN_COUNT; column++) {
+      long[] columnValues = (long[]) tablet.getValues()[column];
+      for (int row = 0; row < NO_NULL_ROWS_PER_BATCH; row++) {
+        columnValues[row] = column + row;
+      }
+    }
+    tablet.setRowSize(NO_NULL_ROWS_PER_BATCH);
+
+    long nextTimestamp = 0;
+    long[] elapsedNanos = new long[ROUND_COUNT];
+    int totalRoundCount = WARMUP_ROUND_COUNT + ROUND_COUNT;
+    for (int round = 0; round < totalRoundCount; round++) {
+      long startNanos = System.nanoTime();
+      for (int batch = 0; batch < NO_NULL_BATCH_COUNT; batch++) {
+        for (int row = 0; row < NO_NULL_ROWS_PER_BATCH; row++) {
+          tablet.getTimestamps()[row] = nextTimestamp++;
+        }
+        session.insertAlignedTablet(tablet);
+      }
+      long roundElapsedNanos = System.nanoTime() - startNanos;
+      if (round >= WARMUP_ROUND_COUNT) {
+        elapsedNanos[round - WARMUP_ROUND_COUNT] = roundElapsedNanos;
+      }
+    }
+
+    assertNoNullDataWritten(session, nextTimestamp);
+    return elapsedNanos;
+  }
+
+  private static void createAlignedTimeseries(ISession session, String device, 
int measurementCount)
+      throws Exception {
+    StringBuilder statement = new StringBuilder("CREATE ALIGNED TIMESERIES ");
+    statement.append(device).append("(s0 INT64");
+    for (int i = 1; i < measurementCount; i++) {
+      statement.append(",s").append(i).append(" INT64");
+    }
+    statement.append(')');
+    session.executeNonQueryStatement(statement.toString());
+  }
+
+  private static void insertHistoricalRows(ISession session, String device) 
throws Exception {
+    List<IMeasurementSchema> schemas =
+        Collections.singletonList(new MeasurementSchema("s0", 
TSDataType.INT64));
+    Tablet tablet = new Tablet(device, schemas, Math.min(HISTORICAL_ROW_COUNT, 
1024));
+    for (int row = 0; row < HISTORICAL_ROW_COUNT; row++) {
+      int rowIndex = tablet.getRowSize();
+      if (rowIndex == tablet.getMaxRowNumber()) {
+        session.insertAlignedTablet(tablet);
+        tablet.reset();
+        rowIndex = 0;
+      }
+      tablet.addTimestamp(rowIndex, row);
+      tablet.addValue("s0", rowIndex, (long) row);
+    }
+    if (tablet.getRowSize() > 0) {
+      session.insertAlignedTablet(tablet);
+    }
+  }
+
+  private static void assertLazyAllocationDataWritten(ISession session, String 
device)
+      throws Exception {
+    try (SessionDataSet dataSet =
+        session.executeQueryStatement(
+            "SELECT COUNT(s0), COUNT(s" + NEW_COLUMN_COUNT + ") FROM " + 
device)) {
+      Assert.assertTrue(dataSet.hasNext());
+      List<org.apache.tsfile.read.common.Field> fields = 
dataSet.next().getFields();
+      Assert.assertEquals(HISTORICAL_ROW_COUNT, fields.get(0).getLongV());
+      Assert.assertEquals(1, fields.get(1).getLongV());
+      Assert.assertFalse(dataSet.hasNext());
+    }
+  }
+
+  private static void assertNoNullDataWritten(ISession session, long 
expectedRowCount)
+      throws Exception {
+    try (SessionDataSet dataSet =
+        session.executeQueryStatement(
+            "SELECT COUNT(s0), COUNT(s" + (NEW_COLUMN_COUNT - 1) + ") FROM " + 
NO_NULL_DEVICE)) {
+      Assert.assertTrue(dataSet.hasNext());
+      List<org.apache.tsfile.read.common.Field> fields = 
dataSet.next().getFields();
+      Assert.assertEquals(expectedRowCount, fields.get(0).getLongV());
+      Assert.assertEquals(expectedRowCount, fields.get(1).getLongV());
+      Assert.assertFalse(dataSet.hasNext());
+    }
+  }
+
+  private static Path getResultPath() {
+    String defaultFileName =
+        "target/aligned-tvlist-lazy-allocation-"
+            + IMPLEMENTATION.replaceAll("[^A-Za-z0-9_.-]", "_")
+            + ".properties";
+    return Paths.get(System.getProperty(RESULT_PATH_PROPERTY, 
defaultFileName));
+  }
+
+  private static void writeResult(Path path, BenchmarkResult result) throws 
IOException {
+    Properties properties = new Properties();
+    properties.setProperty("format.version", "2");
+    properties.setProperty("implementation", result.implementation);
+    properties.setProperty("revision", result.revision);
+    properties.setProperty("historical.row.count", 
Integer.toString(result.historicalRowCount));
+    properties.setProperty("new.column.count", 
Integer.toString(result.newColumnCount));
+    properties.setProperty("warmup.round.count", 
Integer.toString(result.warmupRoundCount));
+    properties.setProperty(
+        "round.count", 
Integer.toString(result.lazyAllocationElapsedNanos.length));
+    properties.setProperty("no-null.batch.count", 
Integer.toString(result.noNullBatchCount));
+    properties.setProperty("no-null.rows.per.batch", 
Integer.toString(NO_NULL_ROWS_PER_BATCH));
+    properties.setProperty(
+        "lazy-allocation.latencies.nanos", 
join(result.lazyAllocationElapsedNanos));
+    properties.setProperty("no-null.latencies.nanos", 
join(result.noNullElapsedNanos));
+    createParentDirectories(path);
+    try (OutputStream outputStream = Files.newOutputStream(path)) {
+      properties.store(outputStream, "AlignedTVList lazy-allocation 
integration benchmark");
+    }
+  }
+
+  private static BenchmarkResult readResult(Path path) throws IOException {
+    Properties properties = new Properties();
+    try (InputStream inputStream = Files.newInputStream(path)) {
+      properties.load(inputStream);
+    }
+    return new BenchmarkResult(
+        properties.getProperty("implementation"),
+        properties.getProperty("revision"),
+        Integer.parseInt(properties.getProperty("historical.row.count")),
+        Integer.parseInt(properties.getProperty("new.column.count")),
+        Integer.parseInt(properties.getProperty("warmup.round.count")),
+        Integer.parseInt(properties.getProperty("no-null.batch.count")),
+        readLongArray(properties, "lazy-allocation.latencies.nanos"),
+        readLongArray(properties, "no-null.latencies.nanos"));
+  }
+
+  private static long[] readLongArray(Properties properties, String key) {
+    String[] values = properties.getProperty(key).split(",");
+    long[] result = new long[values.length];
+    for (int i = 0; i < values.length; i++) {
+      result[i] = Long.parseLong(values[i]);
+    }
+    return result;
+  }
+
+  private static void assertComparable(BenchmarkResult baseline, 
BenchmarkResult candidate) {
+    Assert.assertEquals(baseline.historicalRowCount, 
candidate.historicalRowCount);
+    Assert.assertEquals(baseline.newColumnCount, candidate.newColumnCount);
+    Assert.assertEquals(baseline.warmupRoundCount, candidate.warmupRoundCount);
+    Assert.assertEquals(baseline.noNullBatchCount, candidate.noNullBatchCount);
+    Assert.assertEquals(
+        baseline.lazyAllocationElapsedNanos.length, 
candidate.lazyAllocationElapsedNanos.length);
+    Assert.assertEquals(baseline.noNullElapsedNanos.length, 
candidate.noNullElapsedNanos.length);
+  }
+
+  private static String buildReport(BenchmarkResult baseline, BenchmarkResult 
candidate) {
+    StringBuilder report = new StringBuilder();
+    report.append("# AlignedTVList integration performance report\n\n");
+    report.append(
+        "Both revisions were built in separate Git worktrees and ran this same 
"
+            + "`LocalStandaloneIT` against a real 1C1D IoTDB 
environment.\n\n");
+    report.append(
+        String.format(
+            Locale.ROOT,
+            "- Lazy-allocation workload: write %,d historical rows only to 
`s0`, then write "
+                + "one row to %,d newly used columns.%n"
+                + "- No-null workload: write %d aligned tablets per round, 
with %,d columns and "
+                + "%d rows per tablet; every value is non-null.%n"
+                + "- Primitive array size: %d%n"
+                + "- DataNode maximum heap: %,d MiB%n"
+                + "- Warm-up rounds: %d%n"
+                + "- Measured rounds: %d%n%n",
+            candidate.historicalRowCount,
+            candidate.newColumnCount,
+            candidate.noNullBatchCount,
+            candidate.newColumnCount,
+            NO_NULL_ROWS_PER_BATCH,
+            PRIMITIVE_ARRAY_SIZE,
+            DATANODE_MAX_HEAP_SIZE_IN_MB,
+            candidate.warmupRoundCount,
+            candidate.lazyAllocationElapsedNanos.length));
+    report.append("| Workload | Revision | Commit | Median ms | P95 ms | 
Values/s |\n");
+    report.append("| --- | --- | --- | ---: | ---: | ---: |\n");
+    appendReportRow(report, "Lazy allocation", baseline, false);
+    appendReportRow(report, "Lazy allocation", candidate, false);
+    appendReportRow(report, "No nulls", baseline, true);
+    appendReportRow(report, "No nulls", candidate, true);
+    report.append('\n');
+    report.append(
+        String.format(
+            Locale.ROOT,
+            "- Lazy-allocation median latency improvement: %.2f%%%n"
+                + "- Lazy-allocation throughput improvement: %.2f%%%n"
+                + "- No-null median latency improvement: %.2f%%%n"
+                + "- No-null throughput improvement: %.2f%%%n%n",
+            latencyImprovement(baseline.lazyMedianNanos(), 
candidate.lazyMedianNanos()),
+            throughputImprovement(baseline.lazyValuesPerSecond(), 
candidate.lazyValuesPerSecond()),
+            latencyImprovement(baseline.noNullMedianNanos(), 
candidate.noNullMedianNanos()),
+            throughputImprovement(
+                baseline.noNullValuesPerSecond(), 
candidate.noNullValuesPerSecond())));
+    report.append(
+        String.format(
+            Locale.ROOT,
+            "- %s lazy-allocation latencies (ms): `%s`%n"
+                + "- %s lazy-allocation latencies (ms): `%s`%n"
+                + "- %s no-null latencies (ms): `%s`%n"
+                + "- %s no-null latencies (ms): `%s`%n%n",
+            baseline.implementation,
+            latenciesMillis(baseline.lazyAllocationElapsedNanos),
+            candidate.implementation,
+            latenciesMillis(candidate.lazyAllocationElapsedNanos),
+            baseline.implementation,
+            latenciesMillis(baseline.noNullElapsedNanos),
+            candidate.implementation,
+            latenciesMillis(candidate.noNullElapsedNanos)));
+    report.append(
+        "The timed regions contain the client calls and complete server write 
path; schema "
+            + "creation, workload preparation, and result verification are 
outside them. Raw "
+            + "per-round latencies are retained in the generated result 
property files.\n\n");
+    report.append("## Reproduction\n\n");
+    report.append(
+        "1. Create the baseline worktree: `git worktree add --detach 
<master-worktree> master`.\n"
+            + "2. Copy this test source to the identical path in the baseline 
worktree.\n"
+            + "3. In each worktree, build with "
+            + "`mvn -Pwith-integration-tests -pl integration-test -am package 
-DskipTests`.\n"
+            + String.format(
+                Locale.ROOT,
+                "4. Run the baseline with `-D%s=true`, `-D%s=master`, "
+                    + "`-D%s=<master-commit>`, and `-D%s=<baseline-result>`.%n"
+                    + "5. Run the candidate with the corresponding candidate 
values plus "
+                    + "`-D%s=<baseline-result>` and `-D%s=<report-path>`. For 
both runs, select "
+                    + "this class with 
`-Dit.test=IoTDBAlignedTVListPrimitiveArrayPerformanceIT` "
+                    + "and execute `failsafe:integration-test@integration-test 
"
+                    + "failsafe:verify@verify`.%n",
+                ENABLED_PROPERTY,
+                IMPLEMENTATION_PROPERTY,
+                REVISION_PROPERTY,
+                RESULT_PATH_PROPERTY,
+                BASELINE_RESULT_PATH_PROPERTY,
+                REPORT_PATH_PROPERTY));
+    return report.toString();
+  }
+
+  private static void appendReportRow(
+      StringBuilder report, String workload, BenchmarkResult result, boolean 
noNull) {
+    long medianNanos = noNull ? result.noNullMedianNanos() : 
result.lazyMedianNanos();
+    long p95Nanos = noNull ? result.noNullP95Nanos() : result.lazyP95Nanos();
+    double valuesPerSecond = noNull ? result.noNullValuesPerSecond() : 
result.lazyValuesPerSecond();
+    report.append(
+        String.format(
+            Locale.ROOT,
+            "| %s | %s | `%s` | %.3f | %.3f | %.0f |%n",
+            workload,
+            result.implementation,
+            result.revision,
+            medianNanos / 1_000_000.0,
+            p95Nanos / 1_000_000.0,
+            valuesPerSecond));
+  }
+
+  private static double latencyImprovement(double baseline, double candidate) {
+    return (baseline - candidate) * 100.0 / baseline;
+  }
+
+  private static double throughputImprovement(double baseline, double 
candidate) {
+    return (candidate - baseline) * 100.0 / baseline;
+  }
+
+  private static String latenciesMillis(long[] elapsedNanos) {
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < elapsedNanos.length; i++) {
+      if (i > 0) {
+        builder.append(", ");
+      }
+      builder.append(String.format(Locale.ROOT, "%.3f", elapsedNanos[i] / 
1_000_000.0));
+    }
+    return builder.toString();
+  }
+
+  private static void writeFile(Path path, String content) throws IOException {
+    createParentDirectories(path);
+    Files.writeString(path, content, StandardCharsets.UTF_8);
+  }
+
+  private static void createParentDirectories(Path path) throws IOException {
+    Path parent = path.toAbsolutePath().getParent();
+    if (parent != null) {
+      Files.createDirectories(parent);
+    }
+  }
+
+  private static String join(long[] values) {
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < values.length; i++) {
+      if (i > 0) {
+        builder.append(',');
+      }
+      builder.append(values[i]);
+    }
+    return builder.toString();
+  }
+
+  private static class BenchmarkResult {
+    private final String implementation;
+    private final String revision;
+    private final int historicalRowCount;
+    private final int newColumnCount;
+    private final int warmupRoundCount;
+    private final int noNullBatchCount;
+    private final long[] lazyAllocationElapsedNanos;
+    private final long[] noNullElapsedNanos;
+
+    private BenchmarkResult(
+        String implementation,
+        String revision,
+        int historicalRowCount,
+        int newColumnCount,
+        int warmupRoundCount,
+        int noNullBatchCount,
+        long[] lazyAllocationElapsedNanos,
+        long[] noNullElapsedNanos) {
+      this.implementation = implementation;
+      this.revision = revision;
+      this.historicalRowCount = historicalRowCount;
+      this.newColumnCount = newColumnCount;
+      this.warmupRoundCount = warmupRoundCount;
+      this.noNullBatchCount = noNullBatchCount;
+      this.lazyAllocationElapsedNanos = lazyAllocationElapsedNanos;
+      this.noNullElapsedNanos = noNullElapsedNanos;
+    }
+
+    private static long medianNanos(long[] elapsedNanos) {
+      long[] sorted = Arrays.copyOf(elapsedNanos, elapsedNanos.length);
+      Arrays.sort(sorted);
+      return sorted[sorted.length / 2];
+    }
+
+    private static long p95Nanos(long[] elapsedNanos) {
+      long[] sorted = Arrays.copyOf(elapsedNanos, elapsedNanos.length);
+      Arrays.sort(sorted);
+      return sorted[(int) Math.ceil(sorted.length * 0.95) - 1];
+    }
+
+    private long lazyMedianNanos() {
+      return medianNanos(lazyAllocationElapsedNanos);
+    }
+
+    private long lazyP95Nanos() {
+      return p95Nanos(lazyAllocationElapsedNanos);
+    }
+
+    private long noNullMedianNanos() {
+      return medianNanos(noNullElapsedNanos);
+    }
+
+    private long noNullP95Nanos() {
+      return p95Nanos(noNullElapsedNanos);
+    }
+
+    private double lazyValuesPerSecond() {
+      return newColumnCount * 1_000_000_000.0 / lazyMedianNanos();
+    }
+
+    private double noNullValuesPerSecond() {
+      return (double) newColumnCount
+          * NO_NULL_ROWS_PER_BATCH
+          * noNullBatchCount
+          * 1_000_000_000.0
+          / noNullMedianNanos();
+    }
+
+    private String toSummary() {
+      return String.format(
+          Locale.ROOT,
+          "implementation=%s, revision=%s, lazy median=%.3f ms, lazy 
values/s=%.0f, "
+              + "no-null median=%.3f ms, no-null values/s=%.0f",
+          implementation,
+          revision,
+          lazyMedianNanos() / 1_000_000.0,
+          lazyValuesPerSecond(),
+          noNullMedianNanos() / 1_000_000.0,
+          noNullValuesPerSecond());
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunk.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunk.java
index b0d00562a4c..05fd1d4073c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunk.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunk.java
@@ -132,6 +132,10 @@ public class AlignedWritableMemChunk extends 
AbstractWritableMemChunk {
     return measurementIndexMap.containsKey(measurementId);
   }
 
+  int getMeasurementIndex(String measurementId) {
+    return measurementIndexMap.get(measurementId);
+  }
+
   @Override
   public void putLong(long t, long v) {
     throw new UnSupportedDataTypeException(UNSUPPORTED_TYPE + 
TSDataType.VECTOR);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java
index 4528b44f598..8b14459a760 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java
@@ -92,6 +92,7 @@ import org.apache.tsfile.file.metadata.IDeviceID;
 import org.apache.tsfile.file.metadata.TableSchema;
 import org.apache.tsfile.read.filter.basic.Filter;
 import org.apache.tsfile.utils.Binary;
+import org.apache.tsfile.utils.BitMap;
 import org.apache.tsfile.utils.Pair;
 import org.apache.tsfile.write.writer.RestorableTsFileIOWriter;
 import org.slf4j.Logger;
@@ -104,9 +105,11 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -563,6 +566,7 @@ public class TsFileProcessor {
                 insertTabletNode.getMeasurements(),
                 insertTabletNode.getDataTypes(),
                 insertTabletNode.getColumns(),
+                insertTabletNode.getBitMaps(),
                 insertTabletNode.getColumnCategories(),
                 splitStart,
                 splitEnd,
@@ -714,9 +718,11 @@ public class TsFileProcessor {
       Object[] values,
       TsTableColumnCategory[] columnCategories)
       throws WriteProcessException {
-    // Memory of increased PrimitiveArray and TEXT values, e.g., add a 
long[128], add 128*8
+    // Fixed-size TVList structures and materialized value primitive arrays.
     long memTableIncrement = 0L;
+    // Variable-length payload retained by Binary values.
     long textDataIncrement = 0L;
+    // ChunkMetadata entries created for newly introduced writable fields.
     long chunkMetadataIncrement = 0L;
 
     for (int i = 0; dataTypes != null && i < dataTypes.length; i++) {
@@ -808,9 +814,11 @@ public class TsFileProcessor {
       Object[] values,
       TsTableColumnCategory[] columnCategories)
       throws WriteProcessException {
-    // Memory of increased PrimitiveArray and TEXT values, e.g., add a 
long[128], add 128*8
+    // Fixed-size TVList structures and materialized value primitive arrays.
     long memTableIncrement = 0L;
+    // Variable-length payload retained by Binary values.
     long textDataIncrement = 0L;
+    // ChunkMetadata entries created for newly introduced writable fields.
     long chunkMetadataIncrement = 0L;
 
     IWritableMemChunk memChunk =
@@ -818,46 +826,61 @@ public class TsFileProcessor {
     if (memChunk == null) {
       TSDataType[] writableFieldDataTypes =
           getWritableFieldDataTypes(measurements, dataTypes, values, 
columnCategories);
-      // For new device of this mem table
-      // ChunkMetadataIncrement
+      // A new aligned device creates one value ChunkMetadata entry for each 
writable field.
       chunkMetadataIncrement +=
           ChunkMetadata.calculateRamSize(AlignedPath.VECTOR_PLACEHOLDER, 
TSDataType.VECTOR)
               * writableFieldDataTypes.length;
+      // The first row creates the first aligned TVList block. All writable 
fields have a non-null
+      // value, so this includes the timestamp array, value primitive arrays, 
bitmap reservations,
+      // array headers, and ArrayList references for that block.
       memTableIncrement += 
AlignedTVList.alignedTvListArrayMemCost(writableFieldDataTypes, null);
     } else {
       // For existed device of this mem table
       AlignedWritableMemChunk alignedMemChunk = (AlignedWritableMemChunk) 
memChunk;
-      List<TSDataType> dataTypesInTVList = new ArrayList<>();
+      int currentPointNum = alignedMemChunk.alignedListSize();
+      int currentArrayNum =
+          currentPointNum / PrimitiveArrayManager.ARRAY_SIZE
+              + (currentPointNum % PrimitiveArrayManager.ARRAY_SIZE > 0 ? 1 : 
0);
+      int targetArrayIndex = currentPointNum / 
PrimitiveArrayManager.ARRAY_SIZE;
+      int newColumnCount = 0;
       for (int i = 0; dataTypes != null && i < dataTypes.length; i++) {
         // Skip failed Measurements
         if (!isWritableFieldMeasurement(measurements, dataTypes, values, 
columnCategories, i)) {
           continue;
         }
 
-        // add arrays for new columns
         if (!alignedMemChunk.containsMeasurement(measurements[i])) {
-          int currentArrayNum =
-              alignedMemChunk.alignedListSize() / 
PrimitiveArrayManager.ARRAY_SIZE
-                  + (alignedMemChunk.alignedListSize() % 
PrimitiveArrayManager.ARRAY_SIZE > 0
-                      ? 1
-                      : 0);
-          memTableIncrement += currentArrayNum * 
AlignedTVList.valueListArrayMemCost(dataTypes[i]);
-          dataTypesInTVList.add(dataTypes[i]);
+          // Extending a column adds one null value-list placeholder and one 
conservatively reserved
+          // bitmap to every historical block. No value primitive array is 
allocated there.
+          memTableIncrement +=
+              currentArrayNum * 
AlignedTVList.valueListArrayMemCostWithoutPrimitiveArray();
+          newColumnCount++;
         }
-      }
-      // this insertion will result in a new array
-      if ((alignedMemChunk.alignedListSize() % 
PrimitiveArrayManager.ARRAY_SIZE) == 0) {
-        memTableIncrement += 
alignedMemChunk.getWorkingTVList().alignedTvListArrayMemCost();
-        for (TSDataType dataType : dataTypesInTVList) {
-          memTableIncrement += AlignedTVList.valueListArrayMemCost(dataType);
+        if (!isValueArrayMaterialized(alignedMemChunk, measurements[i], 
targetArrayIndex)) {
+          // The non-null value materializes this column's primitive array in 
the target block.
+          // This cost contains the primitive-array payload and its array 
header.
+          memTableIncrement += 
AlignedTVList.primitiveArrayMemCost(dataTypes[i]);
         }
       }
+      if ((currentPointNum % PrimitiveArrayManager.ARRAY_SIZE) == 0) {
+        // Starting a block allocates its timestamp array, optional sort-index 
array, value-list
+        // null
+        // placeholders, bitmap reservations, array headers, and ArrayList 
references for all
+        // existing columns. Value primitive arrays are excluded because they 
were charged above
+        // only for written columns.
+        memTableIncrement +=
+            
alignedMemChunk.getWorkingTVList().alignedTvListArrayMemCostWithoutPrimitiveArrays();
+        // The working TVList does not contain newly extended columns yet, so 
add their value-list
+        // placeholder and bitmap reservation for this new block separately.
+        memTableIncrement +=
+            (long) newColumnCount * 
AlignedTVList.valueListArrayMemCostWithoutPrimitiveArray();
+      }
     }
 
     for (int i = 0; dataTypes != null && i < dataTypes.length; i++) {
-      // TEXT data mem size
       if (isWritableFieldMeasurement(measurements, dataTypes, values, 
columnCategories, i)
           && dataTypes[i].isBinary()) {
+        // Binary values keep their variable-length content outside the fixed 
Binary[] array.
         textDataIncrement += MemUtils.getBinarySize((Binary) values[i]);
       }
     }
@@ -868,12 +891,15 @@ public class TsFileProcessor {
   @SuppressWarnings("squid:S3776") // high Cognitive Complexity
   private long[] checkAlignedMemCostAndAddToTspInfoForRows(List<InsertRowNode> 
insertRowNodeList)
       throws WriteProcessException {
-    // Memory of increased PrimitiveArray and TEXT values, e.g., add a 
long[128], add 128*8
+    // Fixed-size TVList structures and materialized value primitive arrays.
     long memTableIncrement = 0L;
+    // Variable-length payload retained by Binary values.
     long textDataIncrement = 0L;
+    // ChunkMetadata entries created for newly introduced writable fields.
     long chunkMetadataIncrement = 0L;
     // device -> (measurements -> datatype, adding aligned TVList size)
     Map<IDeviceID, Pair<Map<String, TSDataType>, Integer>> 
increasingMemTableInfo = new HashMap<>();
+    Map<IDeviceID, Map<String, Set<Integer>>> materializedArraysInCurrentBatch 
= new HashMap<>();
     for (InsertRowNode insertRowNode : insertRowNodeList) {
       IDeviceID deviceId = insertRowNode.getDeviceID();
       TSDataType[] dataTypes = insertRowNode.getDataTypes();
@@ -888,11 +914,13 @@ public class TsFileProcessor {
                 measurements, dataTypes, values, 
insertRowNode.getColumnCategories());
         Pair<Map<String, TSDataType>, Integer> addingPointNumInfo =
             increasingMemTableInfo.computeIfAbsent(deviceId, k -> new 
Pair<>(new HashMap<>(), 0));
-        // For new device of this mem table
-        // ChunkMetadataIncrement
+        // The first row of a new aligned device creates one value 
ChunkMetadata entry for each
+        // writable field.
         chunkMetadataIncrement +=
             ChunkMetadata.calculateRamSize(AlignedPath.VECTOR_PLACEHOLDER, 
TSDataType.VECTOR)
                 * writableFieldDataTypes.length;
+        // The first row creates the first complete aligned TVList block: 
timestamp array, value
+        // primitive arrays for the writable fields, bitmap reservations, 
headers, and references.
         memTableIncrement += 
AlignedTVList.alignedTvListArrayMemCost(writableFieldDataTypes, null);
         for (int i = 0; dataTypes != null && i < dataTypes.length; i++) {
           // Skip failed Measurements
@@ -901,6 +929,10 @@ public class TsFileProcessor {
             continue;
           }
           addingPointNumInfo.left.put(measurements[i], dataTypes[i]);
+          materializedArraysInCurrentBatch
+              .computeIfAbsent(deviceId, key -> new HashMap<>())
+              .computeIfAbsent(measurements[i], key -> new HashSet<>())
+              .add(0);
         }
         addingPointNumInfo.setRight(1);
 
@@ -908,9 +940,11 @@ public class TsFileProcessor {
         // For existed device of this mem table
         AlignedWritableMemChunk alignedMemChunk = (AlignedWritableMemChunk) 
memChunk;
         int currentChunkPointNum = alignedMemChunk == null ? 0 : 
alignedMemChunk.alignedListSize();
-        List<TSDataType> dataTypesInTVList = new ArrayList<>();
         Pair<Map<String, TSDataType>, Integer> addingPointNumInfo =
             increasingMemTableInfo.computeIfAbsent(deviceId, k -> new 
Pair<>(new HashMap<>(), 0));
+        int addingPointNum = addingPointNumInfo.getRight();
+        int pointNumBeforeCurrentRow = currentChunkPointNum + addingPointNum;
+        int targetArrayIndex = pointNumBeforeCurrentRow / 
PrimitiveArrayManager.ARRAY_SIZE;
         for (int i = 0; dataTypes != null && i < dataTypes.length; i++) {
           // Skip failed Measurements
           if (!isWritableFieldMeasurement(
@@ -918,7 +952,6 @@ public class TsFileProcessor {
             continue;
           }
 
-          int addingPointNum = addingPointNumInfo.getRight();
           // Extending the column of aligned mem chunk
           boolean currentMemChunkContainsMeasurement =
               alignedMemChunk != null && 
alignedMemChunk.containsMeasurement(measurements[i]);
@@ -926,27 +959,46 @@ public class TsFileProcessor {
               && !addingPointNumInfo.left.containsKey(measurements[i])) {
             addingPointNumInfo.left.put(measurements[i], dataTypes[i]);
             int currentArrayNum =
-                (currentChunkPointNum + addingPointNum) / 
PrimitiveArrayManager.ARRAY_SIZE
-                    + ((currentChunkPointNum + addingPointNum) % 
PrimitiveArrayManager.ARRAY_SIZE
-                            > 0
-                        ? 1
-                        : 0);
+                pointNumBeforeCurrentRow / PrimitiveArrayManager.ARRAY_SIZE
+                    + (pointNumBeforeCurrentRow % 
PrimitiveArrayManager.ARRAY_SIZE > 0 ? 1 : 0);
+            // A column first seen in this batch adds a null value-list 
placeholder and a bitmap
+            // reservation to every block that already exists before the 
current row.
             memTableIncrement +=
-                currentArrayNum * 
AlignedTVList.valueListArrayMemCost(dataTypes[i]);
+                currentArrayNum * 
AlignedTVList.valueListArrayMemCostWithoutPrimitiveArray();
+          }
+          Set<Integer> materializedArrayIndexes =
+              materializedArraysInCurrentBatch
+                  .computeIfAbsent(deviceId, key -> new HashMap<>())
+                  .computeIfAbsent(measurements[i], key -> new HashSet<>());
+          if (!isValueArrayMaterialized(alignedMemChunk, measurements[i], 
targetArrayIndex)
+              && materializedArrayIndexes.add(targetArrayIndex)) {
+            // Charge the payload and header of a value primitive array 
exactly once when this
+            // batch first writes a non-null value to the column in the target 
block.
+            memTableIncrement += 
AlignedTVList.primitiveArrayMemCost(dataTypes[i]);
           }
         }
-        int addingPointNum = addingPointNumInfo.right;
-        // Here currentChunkPointNum + addingPointNum >= 1
-        if (((currentChunkPointNum + addingPointNum) % 
PrimitiveArrayManager.ARRAY_SIZE) == 0) {
-          dataTypesInTVList.addAll(addingPointNumInfo.left.values());
-          memTableIncrement +=
-              alignedMemChunk != null
-                  ? 
alignedMemChunk.getWorkingTVList().alignedTvListArrayMemCost()
-                      + dataTypesInTVList.stream()
-                          .mapToLong(AlignedTVList::valueListArrayMemCost)
-                          .sum()
-                  : AlignedTVList.alignedTvListArrayMemCost(
-                      dataTypesInTVList.toArray(new TSDataType[0]), null);
+        if ((pointNumBeforeCurrentRow % PrimitiveArrayManager.ARRAY_SIZE) == 
0) {
+          if (alignedMemChunk == null) {
+            // A device created earlier in this batch starts another block. 
Charge the timestamp
+            // array, value-list placeholders, bitmap reservations, headers, 
and references for all
+            // columns discovered in the batch, excluding their value 
primitive arrays.
+            memTableIncrement +=
+                AlignedTVList.alignedTvListArrayMemCostWithoutPrimitiveArrays(
+                    addingPointNumInfo.left.values().toArray(new 
TSDataType[0]), null);
+          } else {
+            // An existing aligned TVList starts another block. Charge its 
timestamp array, optional
+            // sort-index array, value-list placeholders, bitmap reservations, 
headers, and
+            // references for columns that were already present before this 
batch.
+            memTableIncrement +=
+                alignedMemChunk
+                    .getWorkingTVList()
+                    .alignedTvListArrayMemCostWithoutPrimitiveArrays();
+            // Columns first introduced by this batch are absent from the 
working TVList's type
+            // list, so add one value-list placeholder and bitmap reservation 
for each of them.
+            memTableIncrement +=
+                (long) addingPointNumInfo.left.size()
+                    * 
AlignedTVList.valueListArrayMemCostWithoutPrimitiveArray();
+          }
         }
         addingPointNumInfo.setRight(addingPointNum + 1);
       }
@@ -957,8 +1009,8 @@ public class TsFileProcessor {
             measurements, dataTypes, values, 
insertRowNode.getColumnCategories(), i)) {
           continue;
         }
-        // TEXT data mem size
         if (dataTypes[i].isBinary() && values[i] != null) {
+          // Binary values keep their variable-length content outside the 
fixed Binary[] array.
           textDataIncrement += MemUtils.getBinarySize((Binary) values[i]);
         }
       }
@@ -1000,6 +1052,7 @@ public class TsFileProcessor {
       String[] measurements,
       TSDataType[] dataTypes,
       Object[] columns,
+      BitMap[] bitMaps,
       TsTableColumnCategory[] columnCategories,
       int start,
       int end,
@@ -1018,6 +1071,7 @@ public class TsFileProcessor {
         end,
         memIncrements,
         columns,
+        bitMaps,
         columnCategories,
         results);
     long memTableIncrement = memIncrements[0];
@@ -1035,7 +1089,9 @@ public class TsFileProcessor {
       int end,
       long[] memIncrements,
       Object column) {
-    // memIncrements = [memTable, text, chunk metadata] respectively
+    // memIncrements[0]: fixed-size TVList structures and materialized value 
primitive arrays.
+    // memIncrements[1]: variable-length payload retained by Binary values.
+    // memIncrements[2]: ChunkMetadata entries for newly introduced writable 
fields.
 
     IWritableMemChunk memChunk = workMemTable.getWritableMemChunk(deviceId, 
measurement);
     if (memChunk == null) {
@@ -1074,6 +1130,7 @@ public class TsFileProcessor {
       int end,
       long[] memIncrements,
       Object[] columns,
+      BitMap[] bitMaps,
       TsTableColumnCategory[] columnCategories,
       TSStatus[] results) {
     int incomingPointNum = end - start;
@@ -1085,21 +1142,38 @@ public class TsFileProcessor {
     IWritableMemChunk memChunk =
         workMemTable.getWritableMemChunk(deviceId, 
AlignedPath.VECTOR_PLACEHOLDER);
     if (memChunk == null) {
-      // new devices introduce new ChunkMetadata
-      // ChunkMetadata memory Increment
+      // A new aligned device creates one value ChunkMetadata entry for each 
writable field.
       memIncrements[2] +=
           writableFieldDataTypes.length
               * ChunkMetadata.calculateRamSize(AlignedPath.VECTOR_PLACEHOLDER, 
TSDataType.VECTOR);
-      // TVList memory
 
       int numArraysToAdd =
           incomingPointNum / PrimitiveArrayManager.ARRAY_SIZE
               + (incomingPointNum % PrimitiveArrayManager.ARRAY_SIZE > 0 ? 1 : 
0);
+      // Each new block allocates its timestamp array, value-list null 
placeholders, bitmap
+      // reservations, array headers, and ArrayList references. Value 
primitive arrays are excluded
+      // here because all-null and failed-only column segments leave them 
unmaterialized.
+      memIncrements[0] +=
+          numArraysToAdd
+              * AlignedTVList.alignedTvListArrayMemCostWithoutPrimitiveArrays(
+                  writableFieldDataTypes, null);
+      // Add the payload and header of a value primitive array only for a 
column/block segment that
+      // contains at least one successful non-null value.
       memIncrements[0] +=
-          numArraysToAdd * 
AlignedTVList.alignedTvListArrayMemCost(writableFieldDataTypes, null);
+          calculateTabletPrimitiveArrayMemCost(
+              null,
+              measurementIds,
+              dataTypes,
+              columns,
+              bitMaps,
+              columnCategories,
+              results,
+              start,
+              end,
+              0);
     } else {
       AlignedWritableMemChunk alignedMemChunk = (AlignedWritableMemChunk) 
memChunk;
-      List<TSDataType> dataTypesInTVList = new ArrayList<>();
+      List<TSDataType> newDataTypes = new ArrayList<>();
       int currentPointNum = alignedMemChunk.alignedListSize();
       int newPointNum = currentPointNum + incomingPointNum;
       int currentArrayCnt =
@@ -1112,9 +1186,12 @@ public class TsFileProcessor {
         }
 
         if (!alignedMemChunk.containsMeasurement(measurementIds[i])) {
-          // add a new column in the TVList, the new column should be as long 
as existing ones
-          memIncrements[0] += currentArrayCnt * 
AlignedTVList.valueListArrayMemCost(dataType);
-          dataTypesInTVList.add(dataType);
+          // A new column adds one null value-list placeholder and one 
conservatively reserved
+          // bitmap to every historical block. It does not materialize value 
primitive arrays in
+          // those blocks.
+          memIncrements[0] +=
+              currentArrayCnt * 
AlignedTVList.valueListArrayMemCostWithoutPrimitiveArray();
+          newDataTypes.add(dataType);
         }
       }
 
@@ -1125,13 +1202,36 @@ public class TsFileProcessor {
       long acquireArray = newArrayCnt - currentArrayCnt;
 
       if (acquireArray != 0) {
-        // memory of extending the TVList
+        // Each acquired block adds a timestamp array, optional sort-index 
array, value-list null
+        // placeholders, bitmap reservations, array headers, and ArrayList 
references for columns
+        // already present in the working TVList. Value primitive arrays are 
charged separately
+        // below.
         memIncrements[0] +=
-            acquireArray * 
alignedMemChunk.getWorkingTVList().alignedTvListArrayMemCost();
-        for (TSDataType dataType : dataTypesInTVList) {
-          memIncrements[0] += acquireArray * 
AlignedTVList.valueListArrayMemCost(dataType);
+            acquireArray
+                * alignedMemChunk
+                    .getWorkingTVList()
+                    .alignedTvListArrayMemCostWithoutPrimitiveArrays();
+        for (TSDataType ignored : newDataTypes) {
+          // Newly extended columns are not included in the working TVList's 
per-block cost yet, so
+          // add their value-list placeholder and bitmap reservation to every 
acquired block.
+          memIncrements[0] +=
+              acquireArray * 
AlignedTVList.valueListArrayMemCostWithoutPrimitiveArray();
         }
       }
+      // Charge the payload and header of each value primitive array that this 
tablet actually
+      // materializes. Arrays already present in the working TVList are not 
charged again.
+      memIncrements[0] +=
+          calculateTabletPrimitiveArrayMemCost(
+              alignedMemChunk,
+              measurementIds,
+              dataTypes,
+              columns,
+              bitMaps,
+              columnCategories,
+              results,
+              start,
+              end,
+              currentPointNum);
     }
 
     // flexible-length data size
@@ -1143,11 +1243,92 @@ public class TsFileProcessor {
 
       if (dataType.isBinary()) {
         Binary[] binColumn = (Binary[]) columns[i];
+        // Binary values keep their variable-length content outside the fixed 
Binary[] arrays.
+        // Failed rows are excluded because their values are not inserted into 
the memtable.
         memIncrements[1] += MemUtils.getBinaryColumnSize(binColumn, start, 
end, results);
       }
     }
   }
 
+  /**
+   * Calculates only the value primitive arrays that an aligned tablet will 
materialize. The
+   * per-block timestamp arrays, value-list placeholders, bitmap reservations, 
headers, and
+   * references are charged by the caller. A column/block pair is charged only 
when the tablet has
+   * at least one successful non-null value in that block and the working 
TVList has not already
+   * allocated its value array.
+   */
+  private static long calculateTabletPrimitiveArrayMemCost(
+      AlignedWritableMemChunk alignedMemChunk,
+      String[] measurementIds,
+      TSDataType[] dataTypes,
+      Object[] columns,
+      BitMap[] bitMaps,
+      TsTableColumnCategory[] columnCategories,
+      TSStatus[] results,
+      int start,
+      int end,
+      int currentPointNum) {
+    long size = 0;
+    for (int column = 0; dataTypes != null && column < dataTypes.length; 
column++) {
+      if (!isWritableFieldMeasurement(
+          measurementIds, dataTypes, columns, columnCategories, column)) {
+        continue;
+      }
+      BitMap bitMap = bitMaps == null || column >= bitMaps.length ? null : 
bitMaps[column];
+      int inputIndex = start;
+      int targetArrayIndex = currentPointNum / 
PrimitiveArrayManager.ARRAY_SIZE;
+      int targetElementIndex = currentPointNum % 
PrimitiveArrayManager.ARRAY_SIZE;
+      while (inputIndex < end) {
+        // Map the next tablet segment onto one target TVList block. The first 
segment may fill the
+        // unused tail of the current block; subsequent segments start at 
block offset zero.
+        int length =
+            Math.min(end - inputIndex, PrimitiveArrayManager.ARRAY_SIZE - 
targetElementIndex);
+        if (containsNonNullValue(bitMap, results, inputIndex, length)
+            && !isValueArrayMaterialized(
+                alignedMemChunk, measurementIds[column], targetArrayIndex)) {
+          // One successful non-null write materializes the whole fixed-size 
value array, whose
+          // memory consists of the primitive payload and the array header.
+          size += AlignedTVList.primitiveArrayMemCost(dataTypes[column]);
+        }
+        inputIndex += length;
+        targetArrayIndex++;
+        targetElementIndex = 0;
+      }
+    }
+    return size;
+  }
+
+  private static boolean isValueArrayMaterialized(
+      AlignedWritableMemChunk alignedMemChunk, String measurement, int 
arrayIndex) {
+    if (alignedMemChunk == null || 
!alignedMemChunk.containsMeasurement(measurement)) {
+      return false;
+    }
+    List<Object> valueArrays =
+        alignedMemChunk
+            .getWorkingTVList()
+            .getValues()
+            .get(alignedMemChunk.getMeasurementIndex(measurement));
+    return arrayIndex < valueArrays.size() && valueArrays.get(arrayIndex) != 
null;
+  }
+
+  private static boolean containsNonNullValue(
+      BitMap bitMap, TSStatus[] results, int start, int length) {
+    if (bitMap == null && results == null) {
+      return true;
+    }
+    for (int i = start; i < start + length; i++) {
+      boolean isNull = bitMap != null && bitMap.isMarked(i);
+      boolean isFailed =
+          results != null
+              && results[i] != null
+              && results[i].code != 
TSStatusCode.SUCCESS_STATUS.getStatusCode();
+      if (!isNull && !isFailed) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private static TSDataType[] getWritableFieldDataTypes(
       String[] measurementIds,
       TSDataType[] dataTypes,
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/AlignedTVList.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/AlignedTVList.java
index 464cbe09995..644890d0bed 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/AlignedTVList.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/AlignedTVList.java
@@ -81,6 +81,10 @@ public abstract class AlignedTVList extends TVList {
   // Record total memory size of binary column
   protected long[] memoryBinaryChunkSize;
 
+  // Number and memory cost of primitive arrays that have been allocated for 
each value column
+  private int[] materializedValueArrayCounts;
+  private long materializedValueArrayMemCost;
+
   // Data type list -> list of TVList, add 1 when expanded -> primitive array 
of basic type
   // Index relation: columnIndex(dataTypeIndex) -> arrayIndex -> elementIndex
   protected List<List<Object>> values;
@@ -103,6 +107,7 @@ public abstract class AlignedTVList extends TVList {
     super();
     dataTypes = types;
     memoryBinaryChunkSize = new long[dataTypes.size()];
+    materializedValueArrayCounts = new int[dataTypes.size()];
 
     values = new ArrayList<>(types.size());
     for (int i = 0; i < types.size(); i++) {
@@ -125,7 +130,9 @@ public abstract class AlignedTVList extends TVList {
     if (!to.isCompatible(from)) {
       return null;
     }
-    return originalValues.stream().map(o -> to.castFromArray(from, 
o)).collect(Collectors.toList());
+    return originalValues.stream()
+        .map(o -> o == null ? null : to.castFromArray(from, o))
+        .collect(Collectors.toList());
   }
 
   @Override
@@ -167,6 +174,15 @@ public abstract class AlignedTVList extends TVList {
     alignedTvList.allValueColDeletedMap = ignoreAllNullRows ? 
getAllValueColDeletedMap() : null;
     alignedTvList.timeColDeletedMap = this.timeColDeletedMap;
     alignedTvList.timeDeletedCnt = this.timeDeletedCnt;
+    for (int i = 0; i < columnIndexList.size(); i++) {
+      int columnIndex = columnIndexList.get(i);
+      if (columnIndex != -1 && values.get(i) != null) {
+        int materializedArrayCount = materializedValueArrayCounts[columnIndex];
+        alignedTvList.materializedValueArrayCounts[i] = materializedArrayCount;
+        alignedTvList.materializedValueArrayMemCost +=
+            (long) materializedArrayCount * 
primitiveArrayMemCost(dataTypeList.get(i));
+      }
+    }
 
     return alignedTvList;
   }
@@ -180,6 +196,9 @@ public abstract class AlignedTVList extends TVList {
     cloneList.values = this.values;
     cloneList.bitMaps = this.bitMaps;
     cloneList.timeColDeletedMap = this.timeColDeletedMap;
+    cloneList.materializedValueArrayCounts =
+        Arrays.copyOf(materializedValueArrayCounts, 
materializedValueArrayCounts.length);
+    cloneList.materializedValueArrayMemCost = materializedValueArrayMemCost;
     return cloneList;
   }
 
@@ -215,6 +234,9 @@ public abstract class AlignedTVList extends TVList {
       }
     }
     cloneList.timeColDeletedMap = timeColDeletedMap == null ? null : 
timeColDeletedMap.clone();
+    cloneList.materializedValueArrayCounts =
+        Arrays.copyOf(materializedValueArrayCounts, 
materializedValueArrayCounts.length);
+    cloneList.materializedValueArrayMemCost = materializedValueArrayMemCost;
     return cloneList;
   }
 
@@ -229,43 +251,35 @@ public abstract class AlignedTVList extends TVList {
     timestamps.get(arrayIndex)[elementIndex] = timestamp;
     for (int i = 0; i < values.size(); i++) {
       Object columnValue = value[i];
-      List<Object> columnValues = values.get(i);
       if (columnValue == null) {
         markNullValue(i, arrayIndex, elementIndex);
+        continue;
       }
+      Object valueArray = getOrCreateValueArray(i, arrayIndex);
       switch (dataTypes.get(i)) {
         case TEXT:
         case BLOB:
         case STRING:
         case OBJECT:
-          ((Binary[]) columnValues.get(arrayIndex))[elementIndex] =
-              columnValue != null ? (Binary) columnValue : Binary.EMPTY_VALUE;
-          memoryBinaryChunkSize[i] +=
-              columnValue != null
-                  ? getBinarySize((Binary) columnValue)
-                  : getBinarySize(Binary.EMPTY_VALUE);
+          ((Binary[]) valueArray)[elementIndex] = (Binary) columnValue;
+          memoryBinaryChunkSize[i] += getBinarySize((Binary) columnValue);
           break;
         case FLOAT:
-          ((float[]) columnValues.get(arrayIndex))[elementIndex] =
-              columnValue != null ? (float) columnValue : Float.MIN_VALUE;
+          ((float[]) valueArray)[elementIndex] = (float) columnValue;
           break;
         case INT32:
         case DATE:
-          ((int[]) columnValues.get(arrayIndex))[elementIndex] =
-              columnValue != null ? (int) columnValue : Integer.MIN_VALUE;
+          ((int[]) valueArray)[elementIndex] = (int) columnValue;
           break;
         case INT64:
         case TIMESTAMP:
-          ((long[]) columnValues.get(arrayIndex))[elementIndex] =
-              columnValue != null ? (long) columnValue : Long.MIN_VALUE;
+          ((long[]) valueArray)[elementIndex] = (long) columnValue;
           break;
         case DOUBLE:
-          ((double[]) columnValues.get(arrayIndex))[elementIndex] =
-              columnValue != null ? (double) columnValue : Double.MIN_VALUE;
+          ((double[]) valueArray)[elementIndex] = (double) columnValue;
           break;
         case BOOLEAN:
-          ((boolean[]) columnValues.get(arrayIndex))[elementIndex] =
-              columnValue != null && (boolean) columnValue;
+          ((boolean[]) valueArray)[elementIndex] = (boolean) columnValue;
           break;
         default:
           break;
@@ -396,33 +410,7 @@ public abstract class AlignedTVList extends TVList {
     List<Object> columnValue = new ArrayList<>(timestamps.size());
     List<BitMap> columnBitMaps = new ArrayList<>(timestamps.size());
     for (int i = 0; i < timestamps.size(); i++) {
-      switch (dataType) {
-        case TEXT:
-        case STRING:
-        case BLOB:
-        case OBJECT:
-          columnValue.add(getPrimitiveArraysByType(TSDataType.TEXT));
-          break;
-        case FLOAT:
-          columnValue.add(getPrimitiveArraysByType(TSDataType.FLOAT));
-          break;
-        case INT32:
-        case DATE:
-          columnValue.add(getPrimitiveArraysByType(TSDataType.INT32));
-          break;
-        case INT64:
-        case TIMESTAMP:
-          columnValue.add(getPrimitiveArraysByType(TSDataType.INT64));
-          break;
-        case DOUBLE:
-          columnValue.add(getPrimitiveArraysByType(TSDataType.DOUBLE));
-          break;
-        case BOOLEAN:
-          columnValue.add(getPrimitiveArraysByType(TSDataType.BOOLEAN));
-          break;
-        default:
-          break;
-      }
+      columnValue.add(null);
       BitMap bitMap = BitMap.createBitMapDynamically(ARRAY_SIZE);
       // The following code is for these 2 kinds of scenarios.
 
@@ -448,6 +436,7 @@ public abstract class AlignedTVList extends TVList {
     memoryBinaryChunkSize = new long[dataTypes.size()];
     System.arraycopy(
         tmpValueChunkRawSize, 0, memoryBinaryChunkSize, 0, 
tmpValueChunkRawSize.length);
+    materializedValueArrayCounts = Arrays.copyOf(materializedValueArrayCounts, 
dataTypes.size());
   }
 
   private Object getObjectByValueIndex(int rowIndex, int columnIndex) {
@@ -621,12 +610,15 @@ public abstract class AlignedTVList extends TVList {
     if (columnIndex < 0 || columnIndex >= values.size() || 
values.get(columnIndex) == null) {
       return true;
     }
+    int arrayIndex = unsortedRowIndex / ARRAY_SIZE;
+    if (values.get(columnIndex).get(arrayIndex) == null) {
+      return true;
+    }
     if (bitMaps == null
         || bitMaps.get(columnIndex) == null
-        || bitMaps.get(columnIndex).get(unsortedRowIndex / ARRAY_SIZE) == 
null) {
+        || bitMaps.get(columnIndex).get(arrayIndex) == null) {
       return false;
     }
-    int arrayIndex = unsortedRowIndex / ARRAY_SIZE;
     int elementIndex = unsortedRowIndex % ARRAY_SIZE;
     List<BitMap> columnBitMaps = bitMaps.get(columnIndex);
     return columnBitMaps.get(arrayIndex).isMarked(elementIndex);
@@ -744,6 +736,9 @@ public abstract class AlignedTVList extends TVList {
   }
 
   protected Object cloneValue(TSDataType type, Object value) {
+    if (value == null) {
+      return null;
+    }
     switch (type) {
       case TEXT:
       case BLOB:
@@ -791,12 +786,16 @@ public abstract class AlignedTVList extends TVList {
       List<Object> columnValues = values.get(i);
       if (columnValues != null) {
         for (Object dataArray : columnValues) {
-          PrimitiveArrayManager.release(dataArray);
+          if (dataArray != null) {
+            PrimitiveArrayManager.release(dataArray);
+          }
         }
         columnValues.clear();
       }
       memoryBinaryChunkSize[i] = 0;
     }
+    Arrays.fill(materializedValueArrayCounts, 0);
+    materializedValueArrayMemCost = 0;
   }
 
   @Override
@@ -817,7 +816,7 @@ public abstract class AlignedTVList extends TVList {
       indices.add((int[]) getPrimitiveArraysByType(TSDataType.INT32));
     }
     for (int i = 0; i < dataTypes.size(); i++) {
-      values.get(i).add(getPrimitiveArraysByType(dataTypes.get(i)));
+      values.get(i).add(null);
       if (bitMaps != null && bitMaps.get(i) != null) {
         bitMaps.get(i).add(null);
       }
@@ -887,7 +886,7 @@ public abstract class AlignedTVList extends TVList {
       if (internalRemaining >= inputRemaining) {
         // the remaining inputs can fit the last array, copy all remaining 
inputs into last array
         System.arraycopy(time, idx, timestamps.get(arrayIdx), elementIdx, 
inputRemaining);
-        arrayCopy(value, idx, arrayIdx, elementIdx, inputRemaining);
+        arrayCopy(value, bitMaps, results, idx, arrayIdx, elementIdx, 
inputRemaining);
         for (int i = 0; i < inputRemaining; i++) {
           if (indices != null) {
             indices.get(arrayIdx)[elementIdx + i] = rowCount;
@@ -900,7 +899,7 @@ public abstract class AlignedTVList extends TVList {
         // the remaining inputs cannot fit the last array, fill the last array 
and create a new
         // one and enter the next loop
         System.arraycopy(time, idx, timestamps.get(arrayIdx), elementIdx, 
internalRemaining);
-        arrayCopy(value, idx, arrayIdx, elementIdx, internalRemaining);
+        arrayCopy(value, bitMaps, results, idx, arrayIdx, elementIdx, 
internalRemaining);
         for (int i = 0; i < internalRemaining; i++) {
           if (indices != null) {
             indices.get(arrayIdx)[elementIdx + i] = rowCount;
@@ -1007,18 +1006,25 @@ public abstract class AlignedTVList extends TVList {
     return bitmap;
   }
 
-  private void arrayCopy(Object[] value, int idx, int arrayIndex, int 
elementIndex, int remaining) {
+  private void arrayCopy(
+      Object[] value,
+      BitMap[] bitMaps,
+      TSStatus[] results,
+      int idx,
+      int arrayIndex,
+      int elementIndex,
+      int remaining) {
     for (int i = 0; i < values.size(); i++) {
-      if (value[i] == null) {
+      if (value[i] == null || !containsNonNullValue(bitMaps, results, i, idx, 
remaining)) {
         continue;
       }
-      List<Object> columnValues = values.get(i);
+      Object valueArray = getOrCreateValueArray(i, arrayIndex);
       switch (dataTypes.get(i)) {
         case TEXT:
         case BLOB:
         case STRING:
         case OBJECT:
-          Binary[] arrayT = ((Binary[]) columnValues.get(arrayIndex));
+          Binary[] arrayT = (Binary[]) valueArray;
           System.arraycopy(value[i], idx, arrayT, elementIndex, remaining);
 
           // update raw size of Text chunk
@@ -1028,25 +1034,25 @@ public abstract class AlignedTVList extends TVList {
           }
           break;
         case FLOAT:
-          float[] arrayF = ((float[]) columnValues.get(arrayIndex));
+          float[] arrayF = (float[]) valueArray;
           System.arraycopy(value[i], idx, arrayF, elementIndex, remaining);
           break;
         case INT32:
         case DATE:
-          int[] arrayI = ((int[]) columnValues.get(arrayIndex));
+          int[] arrayI = (int[]) valueArray;
           System.arraycopy(value[i], idx, arrayI, elementIndex, remaining);
           break;
         case INT64:
         case TIMESTAMP:
-          long[] arrayL = ((long[]) columnValues.get(arrayIndex));
+          long[] arrayL = (long[]) valueArray;
           System.arraycopy(value[i], idx, arrayL, elementIndex, remaining);
           break;
         case DOUBLE:
-          double[] arrayD = ((double[]) columnValues.get(arrayIndex));
+          double[] arrayD = (double[]) valueArray;
           System.arraycopy(value[i], idx, arrayD, elementIndex, remaining);
           break;
         case BOOLEAN:
-          boolean[] arrayB = ((boolean[]) columnValues.get(arrayIndex));
+          boolean[] arrayB = (boolean[]) valueArray;
           System.arraycopy(value[i], idx, arrayB, elementIndex, remaining);
           break;
         default:
@@ -1055,6 +1061,39 @@ public abstract class AlignedTVList extends TVList {
     }
   }
 
+  private static boolean containsNonNullValue(
+      BitMap[] bitMaps, TSStatus[] results, int columnIndex, int start, int 
length) {
+    BitMap bitMap = bitMaps == null ? null : bitMaps[columnIndex];
+    boolean containsNull = bitMap != null && containsMarkedBit(bitMap, start, 
length);
+    boolean containsFailure = results != null && containsFailedStatus(results, 
start, length);
+    if (!containsNull && !containsFailure) {
+      return true;
+    }
+    for (int i = start; i < start + length; i++) {
+      boolean isNull =
+          (bitMap != null && bitMap.isMarked(i))
+              || (results != null
+                  && results[i] != null
+                  && results[i].code != 
TSStatusCode.SUCCESS_STATUS.getStatusCode());
+      if (!isNull) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private Object getOrCreateValueArray(int columnIndex, int arrayIndex) {
+    List<Object> columnValues = values.get(columnIndex);
+    Object valueArray = columnValues.get(arrayIndex);
+    if (valueArray == null) {
+      valueArray = getPrimitiveArraysByType(dataTypes.get(columnIndex));
+      columnValues.set(arrayIndex, valueArray);
+      materializedValueArrayCounts[columnIndex]++;
+      materializedValueArrayMemCost += 
primitiveArrayMemCost(dataTypes.get(columnIndex));
+    }
+    return valueArray;
+  }
+
   private BitMap getBitMap(int columnIndex, int arrayIndex) {
     // init BitMaps if doesn't have
     if (bitMaps == null) {
@@ -1101,7 +1140,12 @@ public abstract class AlignedTVList extends TVList {
   @Override
   public synchronized RamInfo calculateRamSize() {
     return new RamInfo(
-        timestamps.size(), alignedTvListArrayMemCost(), rowCount, new 
ArrayList<>(dataTypes));
+        timestamps.size(),
+        alignedTvListArrayMemCost(),
+        (long) timestamps.size() * 
alignedTvListArrayMemCostWithoutPrimitiveArrays()
+            + materializedValueArrayMemCost,
+        rowCount,
+        new ArrayList<>(dataTypes));
   }
 
   /**
@@ -1168,6 +1212,29 @@ public abstract class AlignedTVList extends TVList {
     return size;
   }
 
+  public long alignedTvListArrayMemCostWithoutPrimitiveArrays() {
+    long size = alignedTvListArrayMemCost();
+    for (TSDataType dataType : dataTypes) {
+      if (dataType != null) {
+        size -= primitiveArrayMemCost(dataType);
+      }
+    }
+    return size;
+  }
+
+  public static long alignedTvListArrayMemCostWithoutPrimitiveArrays(
+      TSDataType[] types, TsTableColumnCategory[] columnCategories) {
+    long size = alignedTvListArrayMemCost(types, columnCategories);
+    for (int i = 0; i < types.length; i++) {
+      TSDataType dataType = types[i];
+      if (dataType != null
+          && (columnCategories == null || columnCategories[i] == 
TsTableColumnCategory.FIELD)) {
+        size -= primitiveArrayMemCost(dataType);
+      }
+    }
+    return size;
+  }
+
   /**
    * Get the single column array mem cost by give type.
    *
@@ -1175,14 +1242,20 @@ public abstract class AlignedTVList extends TVList {
    * @return valueListArrayMemCost
    */
   public static long valueListArrayMemCost(TSDataType type) {
+    return primitiveArrayMemCost(type) + 
valueListArrayMemCostWithoutPrimitiveArray();
+  }
+
+  public static long primitiveArrayMemCost(TSDataType type) {
+    // value array payload and header
+    return (long) PrimitiveArrayManager.ARRAY_SIZE * (long) 
type.getDataTypeSize()
+        + NUM_BYTES_ARRAY_HEADER;
+  }
+
+  public static long valueListArrayMemCostWithoutPrimitiveArray() {
     long size = 0;
-    // value array mem size
-    size += (long) PrimitiveArrayManager.ARRAY_SIZE * (long) 
type.getDataTypeSize();
     // bitmap object, byte array, and reference in the bitmap list
     size += BITMAP_RAM_COST_PER_BLOCK;
-    // array headers mem size
-    size += NUM_BYTES_ARRAY_HEADER;
-    // Object references size in ArrayList
+    // null placeholder reference in the value ArrayList
     size += NUM_BYTES_OBJECT_REF;
     return size;
   }
@@ -1404,7 +1477,13 @@ public abstract class AlignedTVList extends TVList {
         case STRING:
         case OBJECT:
           for (int rowIdx = 0; rowIdx < rowCount; ++rowIdx) {
-            size += ReadWriteIOUtils.sizeToWrite(getBinaryByValueIndex(rowIdx, 
columnIndex));
+            int arrayIndex = rowIdx / ARRAY_SIZE;
+            Object valueArray = values.get(columnIndex).get(arrayIndex);
+            Binary value =
+                valueArray == null
+                    ? Binary.EMPTY_VALUE
+                    : ((Binary[]) valueArray)[rowIdx % ARRAY_SIZE];
+            size += ReadWriteIOUtils.sizeToWrite(value == null ? 
Binary.EMPTY_VALUE : value);
           }
           break;
         case FLOAT:
@@ -1458,13 +1537,15 @@ public abstract class AlignedTVList extends TVList {
       for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
         int arrayIndex = rowIndex / ARRAY_SIZE;
         int elementIndex = rowIndex % ARRAY_SIZE;
+        Object valueArray = columnValues.get(arrayIndex);
         // value
         switch (dataTypes.get(columnIndex)) {
           case TEXT:
           case BLOB:
           case STRING:
           case OBJECT:
-            Binary valueT = ((Binary[]) 
columnValues.get(arrayIndex))[elementIndex];
+            Binary valueT =
+                valueArray == null ? Binary.EMPTY_VALUE : ((Binary[]) 
valueArray)[elementIndex];
             // In some scenario, the Binary in AlignedTVList will be null if 
this field is empty in
             // current row. We need to handle this scenario to get rid of NPE. 
See the similar issue
             // here: https://github.com/apache/iotdb/pull/9884
@@ -1474,29 +1555,29 @@ public abstract class AlignedTVList extends TVList {
             if (valueT != null) {
               WALWriteUtils.write(valueT, buffer);
             } else {
-              WALWriteUtils.write(new Binary(new byte[0]), buffer);
+              WALWriteUtils.write(Binary.EMPTY_VALUE, buffer);
             }
             break;
           case FLOAT:
-            float valueF = ((float[]) 
columnValues.get(arrayIndex))[elementIndex];
+            float valueF = valueArray == null ? 0 : ((float[]) 
valueArray)[elementIndex];
             buffer.putFloat(valueF);
             break;
           case INT32:
           case DATE:
-            int valueI = ((int[]) columnValues.get(arrayIndex))[elementIndex];
+            int valueI = valueArray == null ? 0 : ((int[]) 
valueArray)[elementIndex];
             buffer.putInt(valueI);
             break;
           case INT64:
           case TIMESTAMP:
-            long valueL = ((long[]) 
columnValues.get(arrayIndex))[elementIndex];
+            long valueL = valueArray == null ? 0 : ((long[]) 
valueArray)[elementIndex];
             buffer.putLong(valueL);
             break;
           case DOUBLE:
-            double valueD = ((double[]) 
columnValues.get(arrayIndex))[elementIndex];
+            double valueD = valueArray == null ? 0 : ((double[]) 
valueArray)[elementIndex];
             buffer.putDouble(valueD);
             break;
           case BOOLEAN:
-            boolean valueB = ((boolean[]) 
columnValues.get(arrayIndex))[elementIndex];
+            boolean valueB = valueArray != null && ((boolean[]) 
valueArray)[elementIndex];
             WALWriteUtils.write(valueB, buffer);
             break;
           default:
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TVList.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TVList.java
index f96473781ae..3141ad3c8b4 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TVList.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TVList.java
@@ -67,19 +67,30 @@ public abstract class TVList implements WALEntryValue {
   public static class RamInfo {
     private final int timestampsSize;
     private final long arrayMemCost;
+    private final long ramSize;
     private final int rowCount;
     private final List<TSDataType> dataTypes;
 
     public RamInfo(
         int timestampCount, long arrayMemCost, int rowCount, List<TSDataType> 
dataTypes) {
+      this(timestampCount, arrayMemCost, (long) timestampCount * arrayMemCost, 
rowCount, dataTypes);
+    }
+
+    public RamInfo(
+        int timestampCount,
+        long arrayMemCost,
+        long ramSize,
+        int rowCount,
+        List<TSDataType> dataTypes) {
       this.timestampsSize = timestampCount;
       this.rowCount = rowCount;
       this.arrayMemCost = arrayMemCost;
+      this.ramSize = ramSize;
       this.dataTypes = dataTypes;
     }
 
     public long getRamSize() {
-      return timestampsSize * arrayMemCost;
+      return ramSize;
     }
 
     public int getTimestampsSize() {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
index fbd2cfaf7bb..3a85754a71c 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
@@ -45,6 +45,7 @@ import 
org.apache.iotdb.db.storageengine.rescon.memory.PrimitiveArrayManager;
 import org.apache.iotdb.db.storageengine.rescon.memory.SystemInfo;
 import org.apache.iotdb.db.utils.EnvironmentUtils;
 import org.apache.iotdb.db.utils.constant.TestConstant;
+import org.apache.iotdb.db.utils.datastructure.AlignedTVList;
 import org.apache.iotdb.rpc.RpcUtils;
 import org.apache.iotdb.rpc.TSStatusCode;
 
@@ -565,7 +566,7 @@ public class TsFileProcessorTest {
   }
 
   @Test
-  public void alignedTabletKeepsFailedStatusesAndCountsWrittenRows()
+  public void alignedTabletDoesNotChargePrimitiveArrayForFailedOnlyBlock()
       throws MetadataException, WriteProcessException, IOException, 
IllegalPathException {
     final int rowCount = PrimitiveArrayManager.ARRAY_SIZE + 2;
     final List<int[]> rangeList = Collections.singletonList(new int[] {0, 
rowCount - 1});
@@ -585,12 +586,66 @@ public class TsFileProcessorTest {
         genSingleMeasurementTablet(rowCount, true), rangeList, actualResults, 
false, new long[5]);
 
     Assert.assertEquals(
-        expectedProcessor.getWorkMemTable().getTVListsRamCost(),
+        expectedProcessor.getWorkMemTable().getTVListsRamCost()
+            - AlignedTVList.primitiveArrayMemCost(dataType),
         actualProcessor.getWorkMemTable().getTVListsRamCost());
     Assert.assertEquals(
         TSStatusCode.OUT_OF_TTL.getStatusCode(), 
actualResults[failedIndex].getCode());
   }
 
+  @Test
+  public void alignedTabletOnlyChargesMaterializedPrimitiveArrays()
+      throws MetadataException, WriteProcessException, IOException, 
IllegalPathException {
+    processor = newTestProcessor(filePath + ".lazy-allocation");
+    processor.insertTablet(
+        genAlignedTablet(new String[] {"s0", "s1"}, 
PrimitiveArrayManager.ARRAY_SIZE, 0),
+        Collections.singletonList(new int[] {0, 
PrimitiveArrayManager.ARRAY_SIZE}),
+        new TSStatus[PrimitiveArrayManager.ARRAY_SIZE],
+        true,
+        new long[5]);
+
+    long ramCostBeforeNewBlock = 
processor.getWorkMemTable().getTVListsRamCost();
+    processor.insertTablet(
+        genAlignedTablet(new String[] {"s0"}, 1, 
PrimitiveArrayManager.ARRAY_SIZE),
+        Collections.singletonList(new int[] {0, 1}),
+        new TSStatus[1],
+        true,
+        new long[5]);
+
+    long denseBlockCost =
+        AlignedTVList.alignedTvListArrayMemCost(
+            new TSDataType[] {TSDataType.INT32, TSDataType.INT32}, null);
+    Assert.assertEquals(
+        denseBlockCost - AlignedTVList.primitiveArrayMemCost(TSDataType.INT32),
+        processor.getWorkMemTable().getTVListsRamCost() - 
ramCostBeforeNewBlock);
+  }
+
+  @Test
+  public void alignedRowOnlyChargesMaterializedPrimitiveArrays()
+      throws MetadataException, WriteProcessException, IOException, 
IllegalPathException {
+    processor = newTestProcessor(filePath + ".lazy-row-allocation");
+    processor.insertTablet(
+        genAlignedTablet(new String[] {"s0", "s1"}, 
PrimitiveArrayManager.ARRAY_SIZE, 0),
+        Collections.singletonList(new int[] {0, 
PrimitiveArrayManager.ARRAY_SIZE}),
+        new TSStatus[PrimitiveArrayManager.ARRAY_SIZE],
+        true,
+        new long[5]);
+
+    long ramCostBeforeNewBlock = 
processor.getWorkMemTable().getTVListsRamCost();
+    TSRecord record = new TSRecord(deviceId, PrimitiveArrayManager.ARRAY_SIZE);
+    record.addTuple(DataPoint.getDataPoint(TSDataType.INT32, "s0", "1"));
+    InsertRowNode rowNode = buildInsertRowNodeByTSRecord(record);
+    rowNode.setAligned(true);
+    processor.insert(rowNode, new long[5]);
+
+    long denseBlockCost =
+        AlignedTVList.alignedTvListArrayMemCost(
+            new TSDataType[] {TSDataType.INT32, TSDataType.INT32}, null);
+    Assert.assertEquals(
+        denseBlockCost - AlignedTVList.primitiveArrayMemCost(TSDataType.INT32),
+        processor.getWorkMemTable().getTVListsRamCost() - 
ramCostBeforeNewBlock);
+  }
+
   @Test
   public void alignedTvListRamCostTest2()
       throws MetadataException, WriteProcessException, IOException {
@@ -656,7 +711,7 @@ public class TsFileProcessorTest {
         new TSStatus[10],
         true,
         new long[5]);
-    Assert.assertEquals(7009104, memTable.getTVListsRamCost());
+    Assert.assertEquals(5425104, memTable.getTVListsRamCost());
     processor.insertTablet(
         genInsertTableNodeFors3000ToS6000(300, true),
         Collections.singletonList(new int[] {0, 10}),
@@ -933,6 +988,31 @@ public class TsFileProcessorTest {
     Assert.assertEquals(memTable1.memSize(), memTable2.memSize());
   }
 
+  @Test
+  public void 
testAlignedSparseRowDoesNotChargeUnallocatedPrimitiveArrayOnNewBlock()
+      throws IOException, WriteProcessException, IllegalPathException {
+    processor = newTestProcessor(filePath);
+    String sparseDevice = deviceId + ".sparse";
+    String denseDevice = deviceId + ".dense";
+    for (int i = 0; i < PrimitiveArrayManager.ARRAY_SIZE; i++) {
+      insertAlignedRow(processor, sparseDevice, i, true);
+      insertAlignedRow(processor, denseDevice, i, true);
+    }
+
+    IMemTable memTable = processor.getWorkMemTable();
+    long ramCostBeforeSparseRow = memTable.getTVListsRamCost();
+    insertAlignedRow(processor, sparseDevice, 
PrimitiveArrayManager.ARRAY_SIZE, false);
+    long sparseRowRamIncrement = memTable.getTVListsRamCost() - 
ramCostBeforeSparseRow;
+
+    long ramCostBeforeDenseRow = memTable.getTVListsRamCost();
+    insertAlignedRow(processor, denseDevice, PrimitiveArrayManager.ARRAY_SIZE, 
true);
+    long denseRowRamIncrement = memTable.getTVListsRamCost() - 
ramCostBeforeDenseRow;
+
+    Assert.assertEquals(
+        AlignedTVList.primitiveArrayMemCost(dataType),
+        denseRowRamIncrement - sparseRowRamIncrement);
+  }
+
   @Test
   public void testAlignedRamCostIgnoresRelationalNonFieldAndNullFieldColumns()
       throws IllegalPathException, WriteProcessException, IOException {
@@ -1308,6 +1388,22 @@ public class TsFileProcessorTest {
     return newProcessor;
   }
 
+  private void insertAlignedRow(
+      TsFileProcessor targetProcessor,
+      String targetDevice,
+      long timestamp,
+      boolean writeSecondColumn)
+      throws IllegalPathException, WriteProcessException {
+    TSRecord record = new TSRecord(targetDevice, timestamp);
+    record.addTuple(DataPoint.getDataPoint(dataType, measurementId, 
Long.toString(timestamp)));
+    if (writeSecondColumn) {
+      record.addTuple(DataPoint.getDataPoint(dataType, "s1", 
Long.toString(timestamp)));
+    }
+    InsertRowNode node = buildInsertRowNodeByTSRecord(record);
+    node.setAligned(true);
+    targetProcessor.insert(node, new long[5]);
+  }
+
   private InsertTabletNode genSingleMeasurementTablet(int rowCount, boolean 
isAligned)
       throws IllegalPathException {
     String[] measurements = new String[] {measurementId};
@@ -1335,6 +1431,36 @@ public class TsFileProcessorTest {
         rowCount);
   }
 
+  private InsertTabletNode genAlignedTablet(String[] measurements, int 
rowCount, long startTime)
+      throws IllegalPathException {
+    TSDataType[] dataTypes = new TSDataType[measurements.length];
+    MeasurementSchema[] schemas = new MeasurementSchema[measurements.length];
+    Object[] columns = new Object[measurements.length];
+    for (int i = 0; i < measurements.length; i++) {
+      dataTypes[i] = TSDataType.INT32;
+      schemas[i] = new MeasurementSchema(measurements[i], TSDataType.INT32, 
encoding);
+      columns[i] = new int[rowCount];
+    }
+    long[] times = new long[rowCount];
+    for (int row = 0; row < rowCount; row++) {
+      times[row] = startTime + row;
+      for (Object column : columns) {
+        ((int[]) column)[row] = row;
+      }
+    }
+    return new InsertTabletNode(
+        new QueryId("test_write").genPlanNodeId(),
+        new PartialPath(deviceId),
+        true,
+        measurements,
+        dataTypes,
+        schemas,
+        times,
+        null,
+        columns,
+        rowCount);
+  }
+
   private InsertTabletNode genInsertTableNode(long startTime, boolean 
isAligned)
       throws IllegalPathException {
     String deviceId = "root.sg.device5";
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/datastructure/AlignedTVListTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/datastructure/AlignedTVListTest.java
index c6ccd333942..0b6feb78349 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/datastructure/AlignedTVListTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/datastructure/AlignedTVListTest.java
@@ -19,6 +19,7 @@
 package org.apache.iotdb.db.utils.datastructure;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALByteBufferForTest;
 import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.tsfile.common.conf.TSFileConfig;
@@ -29,6 +30,10 @@ import org.apache.tsfile.utils.BitMap;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -224,6 +229,144 @@ public class AlignedTVListTest {
     Assert.assertNull(tvList.getBitMaps());
   }
 
+  @Test
+  public void testPrimitiveArraysAreAllocatedOnFirstWrite() {
+    AlignedTVList tvList =
+        AlignedTVList.newAlignedList(
+            new ArrayList<>(Arrays.asList(TSDataType.INT64, 
TSDataType.INT64)));
+    for (int i = 0; i <= ARRAY_SIZE; i++) {
+      tvList.putAlignedValue(i, new Object[] {(long) i, null});
+    }
+
+    Assert.assertNotNull(tvList.getValues().get(0).get(0));
+    Assert.assertNotNull(tvList.getValues().get(0).get(1));
+    Assert.assertNull(tvList.getValues().get(1).get(0));
+    Assert.assertNull(tvList.getValues().get(1).get(1));
+
+    tvList.putAlignedValue(ARRAY_SIZE + 1L, new Object[] {null, 1L});
+
+    Assert.assertNull(tvList.getValues().get(1).get(0));
+    Assert.assertNotNull(tvList.getValues().get(1).get(1));
+    Assert.assertTrue(tvList.isNullValue(0, 1));
+    Assert.assertEquals(1, tvList.getLongByValueIndex(ARRAY_SIZE + 1, 1));
+
+    tvList.extendColumn(TSDataType.INT32);
+
+    Assert.assertNull(tvList.getValues().get(2).get(0));
+    Assert.assertNull(tvList.getValues().get(2).get(1));
+
+    long ramSizeBeforeExtendedColumnMaterialization = 
tvList.calculateRamSize().getRamSize();
+    tvList.putAlignedValue(ARRAY_SIZE + 2L, new Object[] {null, null, 2});
+
+    Assert.assertNull(tvList.getValues().get(2).get(0));
+    Assert.assertNotNull(tvList.getValues().get(2).get(1));
+    Assert.assertTrue(tvList.isNullValue(0, 2));
+    Assert.assertFalse(tvList.isNullValue(ARRAY_SIZE + 2, 2));
+    Assert.assertEquals(2, tvList.getIntByValueIndex(ARRAY_SIZE + 2, 2));
+    Assert.assertEquals(
+        AlignedTVList.primitiveArrayMemCost(TSDataType.INT32),
+        tvList.calculateRamSize().getRamSize() - 
ramSizeBeforeExtendedColumnMaterialization);
+  }
+
+  @Test
+  public void testCalculateRamSizeCountsMaterializedPrimitiveArrays() {
+    AlignedTVList tvList =
+        AlignedTVList.newAlignedList(Arrays.asList(TSDataType.INT64, 
TSDataType.INT64));
+    for (int i = 0; i <= ARRAY_SIZE; i++) {
+      tvList.putAlignedValue(i, new Object[] {(long) i, null});
+    }
+
+    long ramSizeBeforeMaterialization = tvList.calculateRamSize().getRamSize();
+    tvList.putAlignedValue(ARRAY_SIZE + 1L, new Object[] {1L, 1L});
+
+    Assert.assertEquals(
+        AlignedTVList.primitiveArrayMemCost(TSDataType.INT64),
+        tvList.calculateRamSize().getRamSize() - ramSizeBeforeMaterialization);
+
+    Assert.assertEquals(
+        tvList.calculateRamSize().getRamSize(), 
tvList.clone().calculateRamSize().getRamSize());
+    Assert.assertEquals(
+        tvList.calculateRamSize().getRamSize(),
+        tvList.cloneForFlushSort().calculateRamSize().getRamSize());
+
+    AlignedTVList projectedTvList =
+        (AlignedTVList) tvList.getTvListByColumnIndex(List.of(1), 
List.of(TSDataType.INT64), false);
+    Assert.assertEquals(
+        (long) projectedTvList.getValues().get(0).size()
+                * 
projectedTvList.alignedTvListArrayMemCostWithoutPrimitiveArrays()
+            + AlignedTVList.primitiveArrayMemCost(TSDataType.INT64),
+        projectedTvList.calculateRamSize().getRamSize());
+
+    tvList.clear();
+    Assert.assertEquals(0, tvList.calculateRamSize().getRamSize());
+  }
+
+  @Test
+  public void testCalculateRamSizeExcludesUnallocatedPrimitiveArrays() {
+    AlignedTVList tvList =
+        AlignedTVList.newAlignedList(Arrays.asList(TSDataType.INT64, 
TSDataType.INT64));
+    for (int i = 0; i <= ARRAY_SIZE; i++) {
+      tvList.putAlignedValue(i, new Object[] {(long) i, null});
+    }
+
+    int blockCount = tvList.getValues().get(0).size();
+    long denseRamSize = blockCount * tvList.alignedTvListArrayMemCost();
+    long expectedRamSize =
+        denseRamSize - blockCount * 
AlignedTVList.primitiveArrayMemCost(TSDataType.INT64);
+
+    Assert.assertEquals(expectedRamSize, 
tvList.calculateRamSize().getRamSize());
+  }
+
+  @Test
+  public void testBatchDoesNotAllocateAllNullPrimitiveArray() {
+    AlignedTVList tvList =
+        AlignedTVList.newAlignedList(Arrays.asList(TSDataType.INT64, 
TSDataType.INT64));
+    long[] times = new long[ARRAY_SIZE];
+    long[][] values = new long[2][ARRAY_SIZE];
+    BitMap[] bitMaps = new BitMap[] {null, new BitMap(ARRAY_SIZE)};
+    bitMaps[1].markAll();
+    for (int i = 0; i < ARRAY_SIZE; i++) {
+      times[i] = i;
+      values[0][i] = i;
+      values[1][i] = i;
+    }
+
+    tvList.putAlignedValues(times, values, bitMaps, 0, ARRAY_SIZE, null);
+
+    Assert.assertNotNull(tvList.getValues().get(0).get(0));
+    Assert.assertNull(tvList.getValues().get(1).get(0));
+    Assert.assertTrue(tvList.isNullValue(ARRAY_SIZE - 1, 1));
+  }
+
+  @Test
+  public void testNullPrimitiveArrayCanBeClonedAndSerialized() throws 
IOException {
+    AlignedTVList tvList =
+        AlignedTVList.newAlignedList(Arrays.asList(TSDataType.TEXT, 
TSDataType.INT64));
+    for (int i = 0; i <= ARRAY_SIZE; i++) {
+      tvList.putAlignedValue(i, new Object[] {null, null});
+    }
+    tvList.putAlignedValue(
+        ARRAY_SIZE + 1L, new Object[] {new Binary("value", 
TSFileConfig.STRING_CHARSET), 1L});
+
+    AlignedTVList clonedTvList = tvList.clone();
+    Assert.assertNull(clonedTvList.getValues().get(0).get(0));
+    Assert.assertNull(clonedTvList.getValues().get(1).get(0));
+    Assert.assertEquals("[null, null]", 
clonedTvList.getAlignedValue(0).toString());
+    Assert.assertEquals("[value, 1]", clonedTvList.getAlignedValue(ARRAY_SIZE 
+ 1).toString());
+
+    WALByteBufferForTest walBuffer =
+        new WALByteBufferForTest(ByteBuffer.allocate(tvList.serializedSize()));
+    tvList.serializeToWAL(walBuffer);
+    AlignedTVList deserializedTvList =
+        AlignedTVList.deserialize(
+            new DataInputStream(new 
ByteArrayInputStream(walBuffer.getBuffer().array())));
+
+    Assert.assertEquals(tvList.rowCount(), deserializedTvList.rowCount());
+    Assert.assertEquals("[null, null]", 
deserializedTvList.getAlignedValue(0).toString());
+    Assert.assertEquals(
+        "[value, 1]", deserializedTvList.getAlignedValue(ARRAY_SIZE + 
1).toString());
+  }
+
   @Test
   public void testClone() {
     List<TSDataType> dataTypes = new ArrayList<>();

Reply via email to